Skip to content

Commit

Permalink
Openapi schema now seems to validate:
Browse files Browse the repository at this point in the history
-> % st run --checks all --experimental=openapi-3.1 --hypothesis-suppress-health-check=filter_too_much http://0.0.0.0:5000/openapi.json
======================================================= Schemathesis test session starts ======================================================
Schema location: http://0.0.0.0:5000/openapi.json
Base URL: http://0.0.0.0:5000/
Specification version: Open API 3.1.0
Random seed: 205554008151466352027775861975580443555
Workers: 1
Collected API operations: 8
Collected API links: 0

GET / .                                                                                                                                  [ 12%]
GET /conformance/ .                                                                                                                      [ 25%]
GET /collections/ .                                                                                                                      [ 37%]
GET /collections/{collection_id}/ .                                                                                                      [ 50%]
GET /collections/{collection_id}/instances/{instance_id}/ .                                                                              [ 62%]
GET /collections/isobaric/position/ .                                                                                                    [ 75%]
GET /collections/isobaric/instances/{instance_id}/position .                                                                             [ 87%]
GET /collections/isobaric/instances/ .                                                                                                   [100%]

=================================================================== SUMMARY ===================================================================

Performed checks:
    not_a_server_error                              244 / 244 passed          PASSED
    status_code_conformance                         244 / 244 passed          PASSED
    content_type_conformance                        244 / 244 passed          PASSED
    response_headers_conformance                    244 / 244 passed          PASSED
    response_schema_conformance                     244 / 244 passed          PASSED

Experimental Features:
  - OpenAPI 3.1: Support for response validation
    Feedback: schemathesis/schemathesis#1822

Your feedback is crucial for experimental features. Please visit the provided URL(s) to share your thoughts.

Tip: Use the `--report` CLI option to visualize test results via Schemathesis.io.
We run additional conformance checks on reports from public repos.
  • Loading branch information
ways committed Jan 9, 2024
1 parent edbf078 commit ceeda64
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 39 deletions.
23 changes: 12 additions & 11 deletions app/initialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,19 +173,28 @@ def format_instance_id(timestamp: datetime) -> str:
return timestamp.strftime("%Y%m%d%H0000")


def check_instance_exists(ds: xr.Dataset, instance_id: str) -> Tuple[bool, str]:
def check_instance_exists(ds: xr.Dataset, instance_id: str) -> Tuple[bool, dict]:
"""Check instance id exists in dataset."""
instance_dates = [get_temporal_extent(ds)]
for d in instance_dates:
if format_instance_id(d) == instance_id:
logger.info("instance_id %s is valid", instance_id)
return True, ""
return True, {}

logger.error("instance_id %s does not exist in dataset", instance_id)
valid_dates = [format_instance_id(x) for x in instance_dates]
return (
False,
f"instance_id {instance_id} does not exist in dataset. Valid dates are {valid_dates}.",
{
"detail": [
{
"type": "string",
"loc": ["query", "instance"],
"msg": f"instance_id {instance_id} does not exist in dataset. Valid dates are {valid_dates}.",
"input": instance_id,
}
]
},
)


Expand All @@ -194,11 +203,3 @@ def check_instance_exists(ds: xr.Dataset, instance_id: str) -> Tuple[bool, str]:
BASE_URL = args.base_url
BIND_HOST = args.bind_host
API_URL = args.api_url

# Open datafile at start
# _ = get_dataset()

# class InstanceID(str, Enum):
# """List of instances, created when opening data file."""
# blank = ""
# default = get_temporal_extent(get_dataset()).strftime("%Y%m%d%H0000")
96 changes: 69 additions & 27 deletions app/routes/position_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import List, Tuple, Annotated
import logging
from fastapi import APIRouter, status, Response, Request, Query, Path
from fastapi.responses import JSONResponse
import xarray as xr
from pydantic import AwareDatetime
from shapely import wkt, GEOSException, Point
Expand All @@ -22,6 +23,8 @@
ISOBARIC_LABEL,
)

POINT_REGEX = "^POINT\\(\\d+\\.?\\d* \\d+\\.?\\d*\\)$"

router = APIRouter()
logger = logging.getLogger()

Expand All @@ -33,6 +36,7 @@
title="Instance ID, consisting of date in format %Y%m%d%H0000",
)


def create_point(coords: str, instance_id: str = "") -> dict:
"""Return data for all isometric layers at a point."""
# Parse coordinates given as WKT
Expand All @@ -42,18 +46,32 @@ def create_point(coords: str, instance_id: str = "") -> dict:
except GEOSException:
errmsg = (
"Error, coords should be a Well Known Text, for example "
+ f'"POINT(11.0 59.0)". You gave "{coords}"'
+ f"POINT(11.0 59.0). You gave <{coords}>"
)
logger.error(errmsg)
return Response(status_code=status.HTTP_400_BAD_REQUEST, content=errmsg)
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"detail": [
{
"type": "string",
"loc": ["query", "coords"],
"msg": errmsg,
"input": coords,
}
]
},
)

logger.info("create_data for coord %s, %s", point.y, point.x)
dataset = get_dataset()

# Sanity check on coordinates
coords_ok, errmsg = check_coords_within_bounds(dataset, point)
if not coords_ok:
return Response(status_code=status.HTTP_400_BAD_REQUEST, content=errmsg)
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content=errmsg
)

# Sanity check on instance id
if len(instance_id) > 0:
Expand Down Expand Up @@ -197,32 +215,42 @@ def create_point(coords: str, instance_id: str = "") -> dict:
return cov.model_dump(exclude_none=True)


def check_coords_within_bounds(ds: xr.Dataset, point: Point) -> Tuple[bool, str]:
def check_coords_within_bounds(ds: xr.Dataset, point: Point) -> Tuple[bool, dict]:
"""Check coordinates are within bounds of dataset."""
if (
point.y > ds[TEMPERATURE_LABEL][LAT_LABEL].values.max()
or point.y < ds[TEMPERATURE_LABEL][LAT_LABEL].values.min()
):
errmsg = (
f"Error, coord {point.y} out of bounds. Min/max is "
+ f"{ds[TEMPERATURE_LABEL][LAT_LABEL].values.min()}/"
+ f"{ds[TEMPERATURE_LABEL][LAT_LABEL].values.max()}"
)
errmsg = {
"detail": [
{
"loc": ["string", 0],
"msg": f"Error, coord {point.y} out of bounds. Min/max is {ds[TEMPERATURE_LABEL][LAT_LABEL].values.min()}/{ds[TEMPERATURE_LABEL][LAT_LABEL].values.max()}",
"type": "string",
"input": point.y,
}
]
}
logger.error(errmsg)
return False, errmsg

if (
point.x > ds[TEMPERATURE_LABEL][LON_LABEL].values.max()
or point.x < ds[TEMPERATURE_LABEL][LON_LABEL].values.min()
):
errmsg = (
f"Error, coord {point.x} out of bounds. Min/max is "
+ f"{ds[TEMPERATURE_LABEL][LON_LABEL].values.min()}/ "
+ f"{ds[TEMPERATURE_LABEL][LON_LABEL].values.max()}"
)
errmsg = {
"detail": [
{
"loc": ["string", 0],
"msg": f"Error, coord {point.x} out of bounds. Min/max is {ds[TEMPERATURE_LABEL][LON_LABEL].values.min()}/{ds[TEMPERATURE_LABEL][LON_LABEL].values.max()}",
"type": "string",
"input": point.x,
}
]
}
logger.error(errmsg)
return False, errmsg
return True, ""
return True, {}


@router.get("/collections/isobaric/position/", response_model=Coverage)
Expand All @@ -233,8 +261,8 @@ async def get_isobaric_page(
Query(
min_length=9,
max_length=50,
pattern="^POINT\\(\\d+.?\\d+ \\d+.?\\d+\\)$",
title="Coordinates, formated as WKT POINT(11.9384 60.1699)",
pattern=POINT_REGEX,
title="Coordinates, formated as a WKT point: POINT(11.9384 60.1699)",
),
] = "POINT(11.9384 60.1699)",
) -> dict:
Expand All @@ -243,9 +271,21 @@ async def get_isobaric_page(
This is the main function of this API. Needs a string with the coordinates, formated as a WKT. Example: POINT(11.9384 60.1699) or POINT(11 60).
"""
if len(coords) == 0:
return {
"body": f'Error: No coordinates provided. Example: {str(request.base_url)[0:-1]}{request.scope["path"]}?coords=POINT(11.9384 60.1699)'
}
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"body": {
"detail": [
{
"loc": ["string", 0],
"msg": f'Error: No coordinates provided. Example: {str(request.base_url)[0:-1]}{request.scope["path"]}?coords=POINT(11.9384 60.1699)',
"type": "string",
}
]
}
},
)

return create_point(coords=coords)


Expand All @@ -261,13 +301,15 @@ async def get_instance_isobaric_page(
coords: Annotated[
str,
Query(
min_length=9, max_length=50, pattern="^POINT\\(\\d+.?\\d+ \\d+.?\\d+\\)$",
examples={
"example1": {
"summary": "First example",
"coords": "POINT(11.9384 60.1699)",
},
},
min_length=9,
max_length=50,
pattern=POINT_REGEX,
# examples={
# "example1": {
# "summary": "First example",
# "values": "POINT(11.9384 60.1699)",
# },
# },
),
] = "POINT(11.9384 60.1699)",
) -> dict:
Expand Down
8 changes: 8 additions & 0 deletions app/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ def test_collections(self) -> None:
# '{"links":[{"href":"http://localhost:5000/collections/","hreflang":"en","rel":"self","type":"aplication/json"}],"collections":[{"id":"isobaric","title":"IsobaricGRIB - GRIB files"...

def test_point(self) -> None:
# Test various coord formats, which should all work
response = client.get(f"/collections/isobaric/position?coords=POINT(11 60)")
self.assertEqual(response.status_code, 200)
response = client.get(f"/collections/isobaric/position?coords=POINT(11.0 60.0)")
self.assertEqual(response.status_code, 200)
response = client.get(f"/collections/isobaric/position?coords=POINT(11. 60.)")
self.assertEqual(response.status_code, 200)

response = client.get(f"/collections/isobaric/position?{sample_coords}")
self.assertEqual(response.status_code, 200)
# Test for values in range -> temperature
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@ commands = bandit --recursive {toxinidir}/app/
; deps =
; -r requirements.txt
; -r requirements-dev.txt
; commands = st run --checks all --experimental=openapi-3.1 http://0.0.0.0:5000/openapi.json
; commands = st run --checks all --experimental=openapi-3.1 --hypothesis-suppress-health-check=filter_too_much http://0.0.0.0:5000/openapi.json

0 comments on commit ceeda64

Please sign in to comment.