diff --git a/.gitignore b/.gitignore index 6faf8c7..6d463e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ -__pycache__ *.sqlite /.coverage /.pytest_cache /.ruff_cache -.idea + +# python +.python-version +.venv +__pycache__ +.env + +.idea \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index be46ff7..d733efb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,7 @@ omit = [ addopts="--cov=stat_fastapi" filterwarnings = [ "ignore:The 'app' shortcut is now deprecated.:DeprecationWarning", + "ignore:Pydantic serializer warnings:UserWarning", ] [build-system] diff --git a/stat_fastapi/api.py b/stat_fastapi/api.py index 3fbcde3..1dcfc04 100644 --- a/stat_fastapi/api.py +++ b/stat_fastapi/api.py @@ -9,7 +9,7 @@ OpportunityCollection, OpportunitySearch, ) -from stat_fastapi.models.order import Order, OrderPayload +from stat_fastapi.models.order import Order from stat_fastapi.models.product import Product, ProductsCollection from stat_fastapi.models.root import RootResponse from stat_fastapi.models.shared import HTTPException as HTTPExceptionModel @@ -161,13 +161,13 @@ async def search_opportunities( ) async def create_order( - self, payload: OrderPayload, request: Request + self, search: OpportunitySearch, request: Request ) -> JSONResponse: """ Create a new order. """ try: - order = await self.backend.create_order(payload, request) + order = await self.backend.create_order(search, request) except ConstraintsException as exc: raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=exc.detail) diff --git a/stat_fastapi/backend.py b/stat_fastapi/backend.py index 88a2eb1..d4f138c 100644 --- a/stat_fastapi/backend.py +++ b/stat_fastapi/backend.py @@ -3,7 +3,7 @@ from fastapi import Request from stat_fastapi.models.opportunity import Opportunity, OpportunitySearch -from stat_fastapi.models.order import Order, OrderPayload +from stat_fastapi.models.order import Order from stat_fastapi.models.product import Product @@ -29,7 +29,7 @@ async def search_opportunities( `stat_fastapi.backend.exceptions.ConstraintsException` if not valid. """ - async def create_order(self, payload: OrderPayload, request: Request) -> Order: + async def create_order(self, search: OpportunitySearch, request: Request) -> Order: """ Create a new order. diff --git a/stat_fastapi/models/constraints.py b/stat_fastapi/models/constraints.py index e66ef93..ad3e6de 100644 --- a/stat_fastapi/models/constraints.py +++ b/stat_fastapi/models/constraints.py @@ -1,9 +1,5 @@ from pydantic import BaseModel, ConfigDict -from stat_fastapi.types.datetime_interval import DatetimeInterval - class Constraints(BaseModel): - datetime: DatetimeInterval - model_config = ConfigDict(extra="allow") diff --git a/stat_fastapi/models/opportunity.py b/stat_fastapi/models/opportunity.py index 56144fe..25e44d8 100644 --- a/stat_fastapi/models/opportunity.py +++ b/stat_fastapi/models/opportunity.py @@ -1,20 +1,26 @@ -from typing import Any, Literal, Mapping +from typing import Literal from geojson_pydantic import Feature, FeatureCollection from geojson_pydantic.geometries import Geometry +from pydantic import BaseModel from stat_fastapi.models.constraints import Constraints +from stat_fastapi.types.datetime_interval import DatetimeInterval -OpportunityProperties = Mapping[str, Any] - -class OpportunitySearch(Feature[Geometry, Constraints]): +# Copied and modified from stack_pydantic.item.ItemProperties +class OpportunityProperties(BaseModel): + datetime: DatetimeInterval product_id: str + constraints: Constraints + + +class OpportunitySearch(OpportunityProperties): + geometry: Geometry class Opportunity(Feature[Geometry, OpportunityProperties]): type: Literal["Feature"] = "Feature" - constraints: Constraints class OpportunityCollection(FeatureCollection[Opportunity]): diff --git a/stat_fastapi/models/order.py b/stat_fastapi/models/order.py index c36aacb..e737db7 100644 --- a/stat_fastapi/models/order.py +++ b/stat_fastapi/models/order.py @@ -3,15 +3,10 @@ from geojson_pydantic import Feature from geojson_pydantic.geometries import Geometry -from stat_fastapi.models.constraints import Constraints +from stat_fastapi.models.opportunity import OpportunityProperties from stat_fastapi.models.shared import Link -class OrderPayload(Feature[Geometry, Constraints]): - product_id: str - - -class Order(Feature[Geometry, Constraints]): +class Order(Feature[Geometry, OpportunityProperties]): type: Literal["Feature"] = "Feature" - product_id: str links: list[Link] diff --git a/stat_fastapi_test_backend/backend.py b/stat_fastapi_test_backend/backend.py index aad6743..0c80733 100644 --- a/stat_fastapi_test_backend/backend.py +++ b/stat_fastapi_test_backend/backend.py @@ -5,14 +5,14 @@ from stat_fastapi.exceptions import ConstraintsException, NotFoundException from stat_fastapi.models.opportunity import Opportunity, OpportunitySearch -from stat_fastapi.models.order import Order, OrderPayload +from stat_fastapi.models.order import Order from stat_fastapi.models.product import Product class TestBackend: _products: list[Product] = [] _opportunities: list[Opportunity] = [] - _allowed_order_payloads: list[OrderPayload] = [] + _allowed_payloads: list[OpportunitySearch] = [] _orders: Mapping[str, Order] = {} def products(self, request: Request) -> list[Product]: @@ -36,22 +36,22 @@ def product(self, product_id: str, request: Request) -> Product | None: async def search_opportunities( self, search: OpportunitySearch, request: Request ) -> list[Opportunity]: - return [ - o.model_copy(update={"constraints": search.properties}) - for o in self._opportunities - ] + return [o.model_copy(update=search.model_dump()) for o in self._opportunities] - async def create_order(self, payload: OrderPayload, request: Request) -> Order: + async def create_order(self, payload: OpportunitySearch, request: Request) -> Order: """ Create a new order. """ - allowed = any(allowed == payload for allowed in self._allowed_order_payloads) + allowed = any(allowed == payload for allowed in self._allowed_payloads) if allowed: order = Order( id=str(uuid4()), geometry=payload.geometry, - properties=payload.properties, - product_id=payload.product_id, + properties={ + "constraints": payload.constraints, + "datetime": payload.datetime, + "product_id": payload.product_id, + }, links=[], ) self._orders[order.id] = order diff --git a/stat_fastapi_tle_backend/backend.py b/stat_fastapi_tle_backend/backend.py index 9df7aac..5d5d6bb 100644 --- a/stat_fastapi_tle_backend/backend.py +++ b/stat_fastapi_tle_backend/backend.py @@ -3,12 +3,9 @@ from stat_fastapi.exceptions import ConstraintsException, NotFoundException from stat_fastapi.models.opportunity import Opportunity, OpportunitySearch -from stat_fastapi.models.order import Order, OrderPayload +from stat_fastapi.models.order import Order from stat_fastapi.models.product import Product, Provider, ProviderRole -from stat_fastapi_tle_backend.models import ( - ValidatedOpportunitySearch, - ValidatedOrderPayload, -) +from stat_fastapi_tle_backend.models import ValidatedOpportunitySearch from stat_fastapi_tle_backend.repository import Repository from stat_fastapi_tle_backend.satellite import EarthObservationSatelliteModel from stat_fastapi_tle_backend.settings import Settings @@ -116,12 +113,12 @@ async def search_opportunities( ] return opportunities - async def create_order(self, payload: OrderPayload, request: Request) -> Order: + async def create_order(self, search: OpportunitySearch, request: Request) -> Order: """ Create a new order. """ try: - validated = ValidatedOrderPayload(**payload.model_dump(by_alias=True)) + validated = ValidatedOpportunitySearch(**search.model_dump(by_alias=True)) except ValidationError as exc: raise ConstraintsException(exc.errors()) from exc diff --git a/stat_fastapi_tle_backend/models.py b/stat_fastapi_tle_backend/models.py index b437cb4..ebc4a56 100644 --- a/stat_fastapi_tle_backend/models.py +++ b/stat_fastapi_tle_backend/models.py @@ -12,7 +12,6 @@ from stat_fastapi.models.constraints import Constraints as BaseConstraints from stat_fastapi.models.opportunity import OpportunitySearch -from stat_fastapi.models.order import OrderPayload class Satellite(BaseModel): @@ -77,7 +76,3 @@ class Constraints(BaseConstraints): class ValidatedOpportunitySearch(OpportunitySearch): properties: Constraints - - -class ValidatedOrderPayload(OrderPayload): - properties: Constraints diff --git a/stat_fastapi_tle_backend/repository.py b/stat_fastapi_tle_backend/repository.py index 0786035..2cb868d 100644 --- a/stat_fastapi_tle_backend/repository.py +++ b/stat_fastapi_tle_backend/repository.py @@ -13,7 +13,7 @@ from stat_fastapi.models.constraints import Constraints from stat_fastapi.models.order import Order -from .models import OffNadirRange, ValidatedOrderPayload +from .models import OffNadirRange, ValidatedOpportunitySearch logger = getLogger(__name__) @@ -41,17 +41,17 @@ class OrderEntity(Base): ) @classmethod - def from_payload(cls, payload: ValidatedOrderPayload) -> "OrderEntity": + def from_search(cls, search: ValidatedOpportunitySearch) -> "OrderEntity": order_id = str(uuid4()) - geom = from_geojson(payload.geometry.model_dump_json(by_alias=True)) + geom = from_geojson(search.geometry.model_dump_json(by_alias=True)) return OrderEntity( id=order_id, - product_id=payload.product_id, + product_id=search.product_id, geom=f"SRID=4326;{to_wkt(geom)}", - dt_start=payload.properties.datetime[0], - dt_end=payload.properties.datetime[1], - off_nadir_min=payload.properties.off_nadir.minimum, - off_nadir_max=payload.properties.off_nadir.maximum, + dt_start=search.datetime[0], + dt_end=search.datetime[1], + off_nadir_min=search.constraints.off_nadir.minimum, + off_nadir_max=search.constraints.off_nadir.maximum, status="pending", ) @@ -107,8 +107,8 @@ def __enter__(self) -> Session: def __exit__(self, exception_type, exception_value, exception_traceback): self.session.close() - def add_order(self, payload: ValidatedOrderPayload) -> Order: - entity = OrderEntity.from_payload(payload) + def add_order(self, search: ValidatedOpportunitySearch) -> Order: + entity = OrderEntity.from_search(search) with self as session: session.add(entity) session.commit() diff --git a/tests/conftest.py b/tests/conftest.py index 05171f1..b09d361 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,8 +5,7 @@ from pytest import fixture from stat_fastapi.models.constraints import Constraints -from stat_fastapi.models.opportunity import Opportunity -from stat_fastapi.models.order import OrderPayload +from stat_fastapi.models.opportunity import Opportunity, OpportunitySearch from stat_fastapi.models.product import Product, Provider, ProviderRole @@ -39,23 +38,26 @@ class Constraints(BaseModel): @fixture -def opportunities(): +def opportunities(products: list[Product]): yield [ Opportunity( geometry=Point(type="Point", coordinates=[13.4, 52.5]), - properties={}, - constraints=Constraints(datetime=(datetime.now(UTC), datetime.now(UTC))), + properties={ + "product_id": products[0].id, + "datetime": (datetime.now(UTC), datetime.now(UTC)), + "constraints": {}, + }, ) ] @fixture -def allowed_order_payloads(products: list[Product]): +def allowed_payloads(products: list[Product]): yield [ - OrderPayload( - type="Feature", + OpportunitySearch( geometry=Point(type="Point", coordinates=[13.4, 52.5]), product_id=products[0].id, - properties=Constraints(datetime=(datetime.now(UTC), datetime.now(UTC))), + datetime=(datetime.now(UTC), datetime.now(UTC)), + constraints=Constraints(), ), ] diff --git a/tests/opportunity_test.py b/tests/opportunity_test.py index 619fb12..dbfb50e 100644 --- a/tests/opportunity_test.py +++ b/tests/opportunity_test.py @@ -30,8 +30,8 @@ def test_search_opportunities_response( "coordinates": [0, 0], }, "product_id": products[0].id, - "properties": { - "datetime": f"{start.isoformat()}/{end.isoformat()}", + "datetime": f"{start.isoformat()}/{end.isoformat()}", + "constraints": { "off_nadir": { "minimum": 0, "maximum": 45, diff --git a/tests/order_test.py b/tests/order_test.py index ddd3508..921a4d3 100644 --- a/tests/order_test.py +++ b/tests/order_test.py @@ -6,7 +6,7 @@ from httpx import Response from pytest import fixture -from stat_fastapi.models.order import OrderPayload +from stat_fastapi.models.opportunity import OpportunitySearch from stat_fastapi_test_backend.backend import TestBackend from .utils import find_link @@ -20,13 +20,13 @@ def new_order_response( stat_backend: TestBackend, stat_client: TestClient, - allowed_order_payloads: list[OrderPayload], + allowed_payloads: list[OpportunitySearch], ) -> Generator[Response, None, None]: - stat_backend._allowed_order_payloads = allowed_order_payloads + stat_backend._allowed_payloads = allowed_payloads res = stat_client.post( "/orders", - json=allowed_order_payloads[0].model_dump(), + json=allowed_payloads[0].model_dump(), ) assert res.status_code == status.HTTP_201_CREATED @@ -48,21 +48,19 @@ def get_order_response( order_id = new_order_response.json()["id"] res = stat_client.get(f"/orders/{order_id}") - print(res.text) assert res.status_code == status.HTTP_200_OK assert res.headers["Content-Type"] == "application/geo+json" yield res -def test_get_order_properties(get_order_response: Response, allowed_order_payloads): +def test_get_order_properties(get_order_response: Response, allowed_payloads): order = get_order_response.json() assert order["geometry"] == { "type": "Point", - "coordinates": list(allowed_order_payloads[0].geometry.coordinates), + "coordinates": list(allowed_payloads[0].geometry.coordinates), } assert ( - order["properties"]["datetime"] - == allowed_order_payloads[0].properties.model_dump()["datetime"] + order["properties"]["datetime"] == allowed_payloads[0].model_dump()["datetime"] )