diff --git a/openeo_fastapi/api/app.py b/openeo_fastapi/api/app.py index ffe7b41..dfd62e6 100644 --- a/openeo_fastapi/api/app.py +++ b/openeo_fastapi/api/app.py @@ -1,10 +1,15 @@ +import re + import attr from attrs import define, field from fastapi import APIRouter, Response from starlette.responses import JSONResponse +from starlette.routing import Route from openeo_fastapi.client import models +HIDDEN_PATHS = ["/openapi.json", "/docs", "/docs/oauth2-redirect", "/redoc"] + @define class OpenEOApi: @@ -15,9 +20,22 @@ class OpenEOApi: router: APIRouter = attr.ib(default=attr.Factory(APIRouter)) response_class: type[Response] = attr.ib(default=JSONResponse) - def _route_filter(self): - """ """ - pass + def register_well_known(self): + """Register well known page (GET /). + + + Returns: + None + """ + self.router.add_api_route( + name=".well-known", + path="/.well-known/openeo", + response_model=models.WellKnownOpeneoGetResponse, + response_model_exclude_unset=False, + response_model_exclude_none=True, + methods=["GET"], + endpoint=self.client.get_well_known, + ) def register_get_capabilities(self): """Register landing page (GET /). @@ -25,10 +43,9 @@ def register_get_capabilities(self): Returns: None """ - self.router.add_api_route( name="capabilities", - path="/", + path=f"/{self.client.settings.OPENEO_VERSION}" + "/", response_model=models.Capabilities, response_model_exclude_unset=False, response_model_exclude_none=True, @@ -43,7 +60,7 @@ def register_get_collections(self): """ self.router.add_api_route( name="collections", - path="/collections", + path=f"/{self.client.settings.OPENEO_VERSION}/collections", response_model=None, response_model_exclude_unset=False, response_model_exclude_none=True, @@ -58,7 +75,8 @@ def register_get_collection(self): """ self.router.add_api_route( name="collection", - path="/collections/{collection_id}", + path=f"/{self.client.settings.OPENEO_VERSION}" + + "/collections/{collection_id}", response_model=None, response_model_exclude_unset=False, response_model_exclude_none=True, @@ -73,7 +91,7 @@ def register_get_conformance(self): """ self.router.add_api_route( name="conformance", - path="/conformance", + path=f"/{self.client.settings.OPENEO_VERSION}/conformance", response_model=models.ConformanceGetResponse, response_model_exclude_unset=False, response_model_exclude_none=True, @@ -81,23 +99,6 @@ def register_get_conformance(self): endpoint=self.client.get_conformance, ) - def register_well_known(self): - """Register well known page (GET /). - - - Returns: - None - """ - self.router.add_api_route( - name=".well-known", - path="/.well-known/openeo", - response_model=models.WellKnownOpeneoGetResponse, - response_model_exclude_unset=False, - response_model_exclude_none=True, - methods=["GET"], - endpoint=self.client.get_well_know, - ) - def register_get_processes(self): """Register Endpoint for Processes (GET /processes). @@ -106,7 +107,7 @@ def register_get_processes(self): """ self.router.add_api_route( name="processes", - path="/processes", + path=f"/{self.client.settings.OPENEO_VERSION}/processes", response_model=None, response_model_exclude_unset=False, response_model_exclude_none=True, @@ -130,7 +131,6 @@ def register_core(self): Returns: None """ - self.register_get_capabilities() self.register_get_conformance() self.register_get_collections() self.register_get_collection() @@ -148,4 +148,6 @@ def __attrs_post_init__(self): # Register core endpoints self.register_core() + + self.register_get_capabilities() self.app.include_router(router=self.router) diff --git a/openeo_fastapi/client/collections.py b/openeo_fastapi/client/collections.py index 9413d6a..f2f6fe2 100644 --- a/openeo_fastapi/client/collections.py +++ b/openeo_fastapi/client/collections.py @@ -1,14 +1,31 @@ +from typing import List + import aiohttp -from openeo_fastapi.client.models import Collection, Collections +from openeo_fastapi.client.models import Collection, Collections, Endpoint +from openeo_fastapi.client.register import EndpointRegister from openeo_fastapi.client.settings import AppSettings -class CollectionCore: +class CollectionRegister(EndpointRegister): def __init__(self, settings) -> None: + super().__init__() + self.endpoints = self._initialize_endpoints() self.settings: AppSettings = settings pass + def _initialize_endpoints(self) -> list[Endpoint]: + return [ + Endpoint( + path="/collections", + methods=["GET"], + ), + Endpoint( + path="/collections/{collection_id}", + methods=["GET"], + ), + ] + async def get_collections(self): """ Returns Basic metadata for all datasets diff --git a/openeo_fastapi/client/core.py b/openeo_fastapi/client/core.py index fb56ce9..8c28def 100644 --- a/openeo_fastapi/client/core.py +++ b/openeo_fastapi/client/core.py @@ -5,8 +5,8 @@ from attrs import define, field from openeo_fastapi.client import conformance, models -from openeo_fastapi.client.collections import CollectionCore -from openeo_fastapi.client.processes import ProcessCore +from openeo_fastapi.client.collections import CollectionRegister +from openeo_fastapi.client.processes import ProcessRegister from openeo_fastapi.client.settings import AppSettings @@ -15,18 +15,26 @@ class OpenEOCore: """Base client for the OpenEO Api.""" billing: str = field() - endpoints: list = field() links: list = field() settings: AppSettings = field() _id: str = field(default="OpenEOApi") - _collections = CollectionCore(settings) - _processes = ProcessCore() + _collections = CollectionRegister(settings) + _processes = ProcessRegister() - @abc.abstractmethod - def get_well_know(self) -> models.WellKnownOpeneoGetResponse: + def _combine_endpoints(self): + """For the various registers that hold endpoint functions, concat those endpoints to register in get_capabilities.""" + registers = [self._collections, self._processes] + + endpoints = [] + for register in registers: + if register: + endpoints.extend(register.endpoints) + return endpoints + + def get_well_known(self) -> models.WellKnownOpeneoGetResponse: """ """ prefix = "https" if self.settings.API_TLS else "http" @@ -56,7 +64,6 @@ def get_well_know(self) -> models.WellKnownOpeneoGetResponse: ] ) - @abc.abstractmethod def get_capabilities(self) -> models.Capabilities: """ """ return models.Capabilities( @@ -68,7 +75,7 @@ def get_capabilities(self) -> models.Capabilities: backend_version=self.settings.OPENEO_VERSION, billing=self.billing, links=self.links, - endpoints=self.endpoints, + endpoints=self._combine_endpoints(), ) @abc.abstractclassmethod @@ -86,7 +93,7 @@ def get_processes(self) -> dict: processes = self._processes.list_processes() return processes - @abc.abstractmethod + @abc.abstractclassmethod def get_conformance(self) -> models.ConformanceGetResponse: """ """ return models.ConformanceGetResponse( diff --git a/openeo_fastapi/client/models.py b/openeo_fastapi/client/models.py index b226137..1cd232d 100644 --- a/openeo_fastapi/client/models.py +++ b/openeo_fastapi/client/models.py @@ -1,4 +1,3 @@ - import sys from enum import Enum from pathlib import Path @@ -29,7 +28,6 @@ class Type2(Enum): other = "other" - class Type5(Enum): Catalog = "Catalog" @@ -232,7 +230,7 @@ class Capabilities(BaseModel): ], ) - + class CollectionId(str): collection_id: constr(regex=rb"^[\w\-\.~\/]+$") = Field( ..., @@ -800,7 +798,7 @@ class Error(BaseModel): ) links: Optional[LogLinks] = None - + class ConformanceGetResponse(BaseModel): conformsTo: list[AnyUrl] diff --git a/openeo_fastapi/client/processes.py b/openeo_fastapi/client/processes.py index b9891fc..e66bd00 100644 --- a/openeo_fastapi/client/processes.py +++ b/openeo_fastapi/client/processes.py @@ -1,16 +1,34 @@ import functools -from typing import Union +from typing import List, Union import openeo_pg_parser_networkx import openeo_processes_dask.specs from openeo_pg_parser_networkx import ProcessRegistry -from openeo_fastapi.client.models import Error, Process, ProcessesGetResponse +from openeo_fastapi.client.models import Endpoint, Error, Process, ProcessesGetResponse +from openeo_fastapi.client.register import EndpointRegister -class ProcessCore: +class ProcessRegister(EndpointRegister): def __init__(self) -> None: - self.process_registry = ProcessRegistry() + super().__init__() + self.endpoints = self._initialize_endpoints() + self.process_registry = self._create_process_registry() + pass + + def _initialize_endpoints(self) -> list[Endpoint]: + return [ + Endpoint( + path="/processes", + methods=["GET"], + ) + ] + + def _create_process_registry(self): + """ + Returns the process registry based on the predefinied specifications from the openeo_processes_dask module. + """ + process_registry = ProcessRegistry() predefined_processes_specs = { process_id: getattr(openeo_processes_dask.specs, process_id) @@ -18,11 +36,11 @@ def __init__(self) -> None: } for process_id, spec in predefined_processes_specs.items(): - self.process_registry[ + process_registry[ ("predefined", process_id) ] = openeo_pg_parser_networkx.Process(spec) - pass + return process_registry @functools.cache def get_available_processes(self): diff --git a/openeo_fastapi/client/register.py b/openeo_fastapi/client/register.py new file mode 100644 index 0000000..c1eca7b --- /dev/null +++ b/openeo_fastapi/client/register.py @@ -0,0 +1,13 @@ +import abc +from typing import List + +from openeo_fastapi.client.models import Endpoint + + +class EndpointRegister(abc.ABC): + def __init__(self): + self.endpoints = self._initialize_endpoints() + + @abc.abstractmethod + def _initialize_endpoints(self) -> list[Endpoint]: + pass diff --git a/tests/api/test_api.py b/tests/api/test_api.py index ac0a2b0..5b8da80 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -1,4 +1,3 @@ -import json import os from unittest import mock @@ -18,27 +17,25 @@ def test_api_core(core_api): assert isinstance(core_api.app, FastAPI) -def test_get_capabilities(core_api): +def test_get_capabilities(core_api, app_settings): """Test the OpenEOApi and OpenEOCore classes interact as intended.""" test_app = TestClient(core_api.app) - # core_api.register_get_capabilities() - - response = test_app.get("/") + response = test_app.get(f"/{app_settings.OPENEO_VERSION}/") assert response.status_code == 200 assert response.json()["title"] == "Test Api" -def test_get_conformance(core_api): +def test_get_conformance(core_api, app_settings): """Test the /conformance endpoint as intended.""" from openeo_fastapi.client.conformance import BASIC_CONFORMANCE_CLASSES test_app = TestClient(core_api.app) - response = test_app.get("/conformance") + response = test_app.get(f"/{app_settings.OPENEO_VERSION}/conformance") assert response.status_code == 200 assert len(BASIC_CONFORMANCE_CLASSES) == len(response.json()["conformsTo"]) @@ -47,20 +44,22 @@ def test_get_conformance(core_api): @pytest.mark.asyncio async def test_get_collections(collections_core, collections): with aioresponses() as m: - m.get("http://test-stac-api.mock.com/api/collections", payload=collections) + get_collections_url = f"http://test-stac-api.mock.com/api/collections" + m.get(get_collections_url, payload=collections) data = await collections_core.get_collections() assert data == collections - m.assert_called_once_with("http://test-stac-api.mock.com/api/collections") + m.assert_called_once_with(get_collections_url) @pytest.mark.asyncio async def test_get_collections_whitelist(collections_core, collections, s2a_collection): with mock.patch.dict(os.environ, {"STAC_COLLECTIONS_WHITELIST": "Sentinel-2A"}): with aioresponses() as m: - resp = m.get( - "http://test-stac-api.mock.com/api/collections", + get_collections_url = f"http://test-stac-api.mock.com/api/collections" + m.get( + get_collections_url, payload={ "collections": [s2a_collection], "links": collections["links"], @@ -72,31 +71,32 @@ async def test_get_collections_whitelist(collections_core, collections, s2a_coll col = data["collections"][0] assert col == s2a_collection - m.assert_called_once_with("http://test-stac-api.mock.com/api/collections") + m.assert_called_once_with(get_collections_url) @pytest.mark.asyncio async def test_get_collection(collections_core, s2a_collection): with aioresponses() as m: + get_collection_url = ( + f"http://test-stac-api.mock.com/api/collections/Sentinel-2A" + ) m.get( - "http://test-stac-api.mock.com/api/collections/Sentinel-2A", + get_collection_url, payload=s2a_collection, ) data = await collections_core.get_collection("Sentinel-2A") assert data == Collection(**s2a_collection) - m.assert_called_once_with( - "http://test-stac-api.mock.com/api/collections/Sentinel-2A" - ) + m.assert_called_once_with(get_collection_url) -def test_get_processes(core_api): +def test_get_processes(core_api, app_settings): """Test the /processes endpoint as intended.""" test_app = TestClient(core_api.app) - response = test_app.get("/processes") + response = test_app.get(f"/{app_settings.OPENEO_VERSION}/processes") assert response.status_code == 200 assert "processes" in response.json().keys() diff --git a/tests/conftest.py b/tests/conftest.py index 9855c38..0b054a5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,7 @@ from openeo_fastapi.api.app import OpenEOApi from openeo_fastapi.client import auth, models, settings -from openeo_fastapi.client.core import CollectionCore, OpenEOCore +from openeo_fastapi.client.core import CollectionRegister, OpenEOCore pytestmark = pytest.mark.unit path_to_current_file = os.path.realpath(__file__) @@ -31,6 +31,11 @@ def mock_settings_env_vars(): yield +@pytest.fixture() +def app_settings(): + return settings.AppSettings() + + @pytest.fixture() def core_api(): client = OpenEOCore( @@ -50,20 +55,6 @@ def core_api(): models.Plan(name="user", description="Subscription plan.", paid=True) ], ), - endpoints=[ - models.Endpoint( - path="/", - methods=["GET"], - ), - models.Endpoint( - path="/collections", - methods=["GET"], - ), - models.Endpoint( - path="/collections/{collection_id}", - methods=["GET"], - ), - ], ) api = OpenEOApi(client=client, app=FastAPI()) @@ -73,7 +64,7 @@ def core_api(): @pytest.fixture() def collections_core(): - return CollectionCore(settings.AppSettings()) + return CollectionRegister(settings.AppSettings()) @pytest.fixture()