diff --git a/openeo_fastapi/api/app.py b/openeo_fastapi/api/app.py index 71dc0fd..f9eda56 100644 --- a/openeo_fastapi/api/app.py +++ b/openeo_fastapi/api/app.py @@ -64,6 +64,21 @@ def register_get_conformance(self): endpoint=self.client.get_conformance, ) + def register_get_file_formats(self): + """Register conformance page (GET /file_formats). + Returns: + None + """ + self.router.add_api_route( + name="conformance", + path=f"/{self.client.settings.OPENEO_VERSION}/file_formats", + response_model=responses.FileFormatsGetResponse, + response_model_exclude_unset=False, + response_model_exclude_none=True, + methods=["GET"], + endpoint=self.client.get_file_formats, + ) + def register_get_collections(self): """Register collection Endpoint (GET /collections). Returns: @@ -76,7 +91,7 @@ def register_get_collections(self): response_model_exclude_unset=False, response_model_exclude_none=True, methods=["GET"], - endpoint=self.client._collections.get_collections, + endpoint=self.client.collections.get_collections, ) def register_get_collection(self): @@ -92,7 +107,7 @@ def register_get_collection(self): response_model_exclude_unset=False, response_model_exclude_none=True, methods=["GET"], - endpoint=self.client._collections.get_collection, + endpoint=self.client.collections.get_collection, ) def register_get_processes(self): @@ -108,7 +123,7 @@ def register_get_processes(self): response_model_exclude_unset=False, response_model_exclude_none=True, methods=["GET"], - endpoint=self.client._processes.list_processes, + endpoint=self.client.processes.list_processes, ) def register_get_jobs(self): @@ -124,7 +139,7 @@ def register_get_jobs(self): response_model_exclude_unset=False, response_model_exclude_none=True, methods=["GET"], - endpoint=self.client._jobs.list_jobs, + endpoint=self.client.jobs.list_jobs, ) def register_create_job(self): @@ -140,7 +155,7 @@ def register_create_job(self): response_model_exclude_unset=False, response_model_exclude_none=True, methods=["POST"], - endpoint=self.client._jobs.create_job, + endpoint=self.client.jobs.create_job, ) def register_update_job(self): @@ -156,7 +171,7 @@ def register_update_job(self): response_model_exclude_unset=False, response_model_exclude_none=True, methods=["PATCH"], - endpoint=self.client._jobs.update_job, + endpoint=self.client.jobs.update_job, ) def register_get_job(self): @@ -172,7 +187,7 @@ def register_get_job(self): response_model_exclude_unset=False, response_model_exclude_none=True, methods=["GET"], - endpoint=self.client._jobs.get_job, + endpoint=self.client.jobs.get_job, ) def register_delete_job(self): @@ -188,7 +203,7 @@ def register_delete_job(self): response_model_exclude_unset=False, response_model_exclude_none=True, methods=["DELETE"], - endpoint=self.client._jobs.delete_job, + endpoint=self.client.jobs.delete_job, ) def register_get_estimate(self): @@ -204,7 +219,7 @@ def register_get_estimate(self): response_model_exclude_unset=False, response_model_exclude_none=True, methods=["GET"], - endpoint=self.client._jobs.estimate, + endpoint=self.client.jobs.estimate, ) def register_get_logs(self): @@ -220,7 +235,7 @@ def register_get_logs(self): response_model_exclude_unset=False, response_model_exclude_none=True, methods=["GET"], - endpoint=self.client._jobs.logs, + endpoint=self.client.jobs.logs, ) def register_get_results(self): @@ -236,7 +251,7 @@ def register_get_results(self): response_model_exclude_unset=False, response_model_exclude_none=True, methods=["GET"], - endpoint=self.client._jobs.get_results, + endpoint=self.client.jobs.get_results, ) def register_start_job(self): @@ -252,7 +267,7 @@ def register_start_job(self): response_model_exclude_unset=False, response_model_exclude_none=True, methods=["POST"], - endpoint=self.client._jobs.start_job, + endpoint=self.client.jobs.start_job, ) def register_cancel_job(self): @@ -262,13 +277,77 @@ def register_cancel_job(self): None """ self.router.add_api_route( - name="start_job", + name="cancel_job", path=f"/{self.client.settings.OPENEO_VERSION}/jobs" + "/{job_id}/results", response_model=None, response_model_exclude_unset=False, response_model_exclude_none=True, methods=["DELETE"], - endpoint=self.client._jobs.cancel_job, + endpoint=self.client.jobs.cancel_job, + ) + + def register_list_files(self): + """Register Endpoint for Files (GET /files). + + Returns: + None + """ + self.router.add_api_route( + name="list_files", + path=f"/{self.client.settings.OPENEO_VERSION}/files", + response_model=None, + response_model_exclude_unset=False, + response_model_exclude_none=True, + methods=["GET"], + endpoint=self.client.files.list_files, + ) + + def register_download_file(self): + """Register Endpoint for Files (GET /files/{path}). + + Returns: + None + """ + self.router.add_api_route( + name="download_file", + path=f"/{self.client.settings.OPENEO_VERSION}/files" + "/{path}", + response_model=None, + response_model_exclude_unset=False, + response_model_exclude_none=True, + methods=["GET"], + endpoint=self.client.files.download_file, + ) + + def register_upload_file(self): + """Register Endpoint for Files (PUT /files/{path}). + + Returns: + None + """ + self.router.add_api_route( + name="upload_file", + path=f"/{self.client.settings.OPENEO_VERSION}/files" + "/{path}", + response_model=None, + response_model_exclude_unset=False, + response_model_exclude_none=True, + methods=["PUT"], + endpoint=self.client.files.upload_file, + ) + + def register_delete_file(self): + """Register Endpoint for Files (DELETE /files/{path}). + + Returns: + None + """ + self.router.add_api_route( + name="delete_file", + path=f"/{self.client.settings.OPENEO_VERSION}/files" + "/{path}", + response_model=None, + response_model_exclude_unset=False, + response_model_exclude_none=True, + methods=["DELETE"], + endpoint=self.client.files.delete_file, ) def register_core(self): @@ -288,6 +367,7 @@ def register_core(self): None """ self.register_get_conformance() + self.register_get_file_formats() self.register_get_collections() self.register_get_collection() self.register_get_processes() @@ -301,6 +381,10 @@ def register_core(self): self.register_get_results() self.register_start_job() self.register_cancel_job() + self.register_list_files() + self.register_download_file() + self.register_upload_file() + self.register_delete_file() self.register_well_known() def http_exception_handler(self, request, exception): diff --git a/openeo_fastapi/api/responses.py b/openeo_fastapi/api/responses.py index cfbff7d..87dc417 100644 --- a/openeo_fastapi/api/responses.py +++ b/openeo_fastapi/api/responses.py @@ -1,12 +1,14 @@ import uuid from enum import Enum -from typing import Any, Optional, TypedDict, Union +from typing import Any, Dict, List, Optional, TypedDict, Union from pydantic import AnyUrl, BaseModel, Extra, Field, validator from openeo_fastapi.api.types import ( Billing, Endpoint, + File, + FileFormat, Link, Process, RFC3339Datetime, @@ -451,3 +453,21 @@ class JobsGetEstimateGetResponse(BaseModel): description="Time until which the estimate is valid, formatted as a [RFC 3339](https://www.rfc-editor.org/rfc/RFC3339Datetime.html) date-time.", example="2020-11-01T00:00:00Z", ) + + +class FilesGetResponse(BaseModel): + files: list[File] + links: list[Link] + + +class FileFormatsGetResponse(BaseModel): + input: dict[str, FileFormat] = Field( + ..., + description="Map of supported input file formats, i.e. file formats a back-end can **read** from. The property keys are the file format names that are used by clients and users, for example in process graphs.", + title="Input File Formats", + ) + output: dict[str, FileFormat] = Field( + ..., + description="Map of supported output file formats, i.e. file formats a back-end can **write** to. The property keys are the file format names that are used by clients and users, for example in process graphs.", + title="Output File Formats", + ) diff --git a/openeo_fastapi/api/types.py b/openeo_fastapi/api/types.py index 6836dc3..84c783a 100644 --- a/openeo_fastapi/api/types.py +++ b/openeo_fastapi/api/types.py @@ -1,7 +1,7 @@ import datetime from enum import Enum from pathlib import Path -from typing import Any, List, Optional, Union +from typing import Any, Dict, List, Optional, Union from pydantic import AnyUrl, BaseModel, Extra, Field, validator @@ -57,6 +57,13 @@ class Level(Enum): debug = "debug" +class GisDataType(Enum): + raster = "raster" + vector = "vector" + table = "table" + other = "other" + + class RFC3339Datetime(BaseModel): """Class to consistently represent datetimes as strings compliant to RFC3339Datetime.""" @@ -180,6 +187,20 @@ class Billing(BaseModel): ) +class File(BaseModel): + path: str = Field( + ..., + description="Path of the file, relative to the root directory of the user's server-side workspace.\nMUST NOT start with a slash `/` and MUST NOT be url-encoded.\n\nThe Windows-style path name component separator `\\` is not supported,\nalways use `/` instead.\n\nNote: The pattern only specifies a minimal subset of invalid characters.\nThe back-ends MAY enforce additional restrictions depending on their OS/environment.", + example="folder/file.txt", + ) + size: Optional[int] = Field(None, description="File size in bytes.", example=1024) + modified: Optional[RFC3339Datetime] = Field( + None, + description="Date and time the file has lastly been modified, formatted as a [RFC 3339](https://www.rfc-editor.org/rfc/RFC3339Datetime.html) date-time.", + example="2018-01-03T10:55:29Z", + ) + + class UsageMetric(BaseModel): value: float unit: str @@ -303,3 +324,23 @@ class Error(BaseModel): example="Parameter 'sample' is missing.", ) links: Optional[list[Link]] = None + + +class FileFormat(BaseModel): + title: str + description: Optional[str] = None + gis_data_types: list[GisDataType] = Field( + ..., + description="Specifies the supported GIS spatial data types for this format.\nIt is RECOMMENDED to specify at least one of the data types, which will likely become a requirement in a future API version.", + ) + deprecated: Optional[bool] = None + experimental: Optional[bool] = None + parameters: dict[str, Any] = Field( + ..., + description="Specifies the supported parameters for this file format.", + title="File Format Parameters", + ) + links: Optional[list[Link]] = Field( + None, + description="Links related to this file format, e.g. external documentation.\n\nFor relation types see the lists of\n[common relation types in openEO](#section/API-Principles/Web-Linking).", + ) diff --git a/openeo_fastapi/client/collections.py b/openeo_fastapi/client/collections.py index b39b4e9..9dc2d77 100644 --- a/openeo_fastapi/client/collections.py +++ b/openeo_fastapi/client/collections.py @@ -5,6 +5,17 @@ from openeo_fastapi.api.types import Endpoint, Error from openeo_fastapi.client.register import EndpointRegister +COLLECTIONS_ENDPOINTS = [ + Endpoint( + path="/collections", + methods=["GET"], + ), + Endpoint( + path="/collections/{collection_id}", + methods=["GET"], + ), +] + class CollectionRegister(EndpointRegister): def __init__(self, settings) -> None: @@ -13,16 +24,7 @@ def __init__(self, settings) -> None: self.settings = settings def _initialize_endpoints(self) -> list[Endpoint]: - return [ - Endpoint( - path="/collections", - methods=["GET"], - ), - Endpoint( - path="/collections/{collection_id}", - methods=["GET"], - ), - ] + return COLLECTIONS_ENDPOINTS async def _proxy_request(self, path): """ diff --git a/openeo_fastapi/client/core.py b/openeo_fastapi/client/core.py index 2baf324..d607128 100644 --- a/openeo_fastapi/client/core.py +++ b/openeo_fastapi/client/core.py @@ -7,6 +7,7 @@ from openeo_fastapi.api import responses from openeo_fastapi.client import conformance from openeo_fastapi.client.collections import CollectionRegister +from openeo_fastapi.client.files import FilesRegister from openeo_fastapi.client.jobs import JobsRegister from openeo_fastapi.client.processes import ProcessRegister from openeo_fastapi.client.settings import AppSettings @@ -17,27 +18,31 @@ class OpenEOCore: """Base client for the OpenEO Api.""" billing: str = field() + input_formats: list = field() + output_formats: list = field() links: list = field() settings = AppSettings() _id: str = field(default="OpenEOApi") - _collections: Optional[CollectionRegister] = None - _jobs: Optional[JobsRegister] = None - _processes: Optional[ProcessRegister] = None + collections: Optional[CollectionRegister] = None + files: Optional[FilesRegister] = None + jobs: Optional[JobsRegister] = None + processes: Optional[ProcessRegister] = None def __attrs_post_init__(self): """ Post init hook to set the register objects for the class if none where provided by the user! """ - self._collections = self._collections or CollectionRegister(self.settings) - self._jobs = self._jobs or JobsRegister(self.settings, self.links) - self._processes = self._processes or ProcessRegister(self.links) + self.collections = self.collections or CollectionRegister(self.settings) + self.files = self.files or FilesRegister(self.settings, self.links) + self.jobs = self.jobs or JobsRegister(self.settings, self.links) + self.processes = self.processes or ProcessRegister(self.links) 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, self._jobs] + registers = [self.collections, self.files, self.jobs, self.processes] endpoints = [] for register in registers: @@ -94,3 +99,10 @@ def get_conformance(self) -> responses.ConformanceGetResponse: return responses.ConformanceGetResponse( conformsTo=conformance.BASIC_CONFORMANCE_CLASSES ) + + def get_file_formats(self) -> responses.FileFormatsGetResponse: + """ """ + return responses.FileFormatsGetResponse( + input={_format.title: _format for _format in self.input_formats}, + output={_format.title: _format for _format in self.output_formats}, + ) diff --git a/openeo_fastapi/client/files.py b/openeo_fastapi/client/files.py new file mode 100644 index 0000000..f52d9d4 --- /dev/null +++ b/openeo_fastapi/client/files.py @@ -0,0 +1,123 @@ +from typing import Optional + +from fastapi import Depends, HTTPException + +from openeo_fastapi.api.types import Endpoint, Error +from openeo_fastapi.client.auth import Authenticator, User +from openeo_fastapi.client.register import EndpointRegister + +FILE_ENDPOINTS = [ + Endpoint( + path="/files", + methods=["GET"], + ), + Endpoint( + path="/files/{path}", + methods=["GET"], + ), + Endpoint( + path="/files/{path}", + methods=["PUT"], + ), + Endpoint( + path="/files/{path}", + methods=["DELETE"], + ), +] + + +class FilesRegister(EndpointRegister): + def __init__(self, settings, links) -> None: + super().__init__() + self.endpoints = self._initialize_endpoints() + self.settings = settings + self.links = links + + def _initialize_endpoints(self) -> list[Endpoint]: + return FILE_ENDPOINTS + + def list_files( + self, limit: Optional[int] = 10, user: User = Depends(Authenticator.validate) + ): + """_summary_ + + Args: + limit (int): _description_ + user (User): _description_ + + Raises: + HTTPException: _description_ + HTTPException: _description_ + HTTPException: _description_ + HTTPException: _description_ + + Returns: + _type_: _description_ + """ + raise HTTPException( + status_code=501, + detail=Error(code="FeatureUnsupported", message="Feature not supported."), + ) + + def download_file(self, path: str, user: User = Depends(Authenticator.validate)): + """_summary_ + + Args: + path (str): _description_ + user (User): _description_ + + Raises: + HTTPException: _description_ + HTTPException: _description_ + HTTPException: _description_ + HTTPException: _description_ + + Returns: + _type_: _description_ + """ + raise HTTPException( + status_code=501, + detail=Error(code="FeatureUnsupported", message="Feature not supported."), + ) + + def upload_file(self, path: str, user: User = Depends(Authenticator.validate)): + """_summary_ + + Args: + path (str): _description_ + user (User): _description_ + + Raises: + HTTPException: _description_ + HTTPException: _description_ + HTTPException: _description_ + HTTPException: _description_ + + Returns: + _type_: _description_ + """ + raise HTTPException( + status_code=501, + detail=Error(code="FeatureUnsupported", message="Feature not supported."), + ) + + def delete_file(self, path: str, user: User = Depends(Authenticator.validate)): + """_summary_ + + Args: + path (str): _description_ + user (User): _description_ + + Raises: + HTTPException: _description_ + HTTPException: _description_ + HTTPException: _description_ + HTTPException: _description_ + + Returns: + _type_: _description_ + """ + raise HTTPException( + status_code=501, + detail=Error(code="FeatureUnsupported", message="Feature not supported."), + ) diff --git a/openeo_fastapi/client/jobs.py b/openeo_fastapi/client/jobs.py index 841aed4..4123c00 100644 --- a/openeo_fastapi/client/jobs.py +++ b/openeo_fastapi/client/jobs.py @@ -19,6 +19,49 @@ from openeo_fastapi.client.psql.models import JobORM from openeo_fastapi.client.register import EndpointRegister +JOBS_ENDPOINTS = [ + Endpoint( + path="/jobs", + methods=["GET"], + ), + Endpoint( + path="/jobs", + methods=["POST"], + ), + Endpoint( + path="/jobs/{job_id}", + methods=["GET"], + ), + Endpoint( + path="/jobs/{job_id}", + methods=["POST"], + ), + Endpoint( + path="/jobs/{job_id}", + methods=["DELETE"], + ), + Endpoint( + path="/jobs/{job_id}/estimate", + methods=["GET"], + ), + Endpoint( + path="/jobs/{job_id}/logs", + methods=["GET"], + ), + Endpoint( + path="/jobs/{job_id}/results", + methods=["GET"], + ), + Endpoint( + path="/jobs/{job_id}/results", + methods=["POST"], + ), + Endpoint( + path="/jobs/{job_id}/results", + methods=["DELETE"], + ), +] + class Job(BaseModel): """Pydantic model manipulating jobs.""" @@ -62,48 +105,7 @@ def __init__(self, settings, links) -> None: self.links = links def _initialize_endpoints(self) -> list[Endpoint]: - return [ - Endpoint( - path="/jobs", - methods=["GET"], - ), - Endpoint( - path="/jobs", - methods=["POST"], - ), - Endpoint( - path="/jobs/{job_id}", - methods=["GET"], - ), - Endpoint( - path="/jobs/{job_id}", - methods=["POST"], - ), - Endpoint( - path="/jobs/{job_id}", - methods=["DELETE"], - ), - Endpoint( - path="/jobs/{job_id}/estimate", - methods=["GET"], - ), - Endpoint( - path="/jobs/{job_id}/logs", - methods=["GET"], - ), - Endpoint( - path="/jobs/{job_id}/results", - methods=["GET"], - ), - Endpoint( - path="/jobs/{job_id}/results", - methods=["POST"], - ), - Endpoint( - path="/jobs/{job_id}/results", - methods=["DELETE"], - ), - ] + return JOBS_ENDPOINTS def list_jobs( self, limit: Optional[int] = 10, user: User = Depends(Authenticator.validate) @@ -285,7 +287,7 @@ def update_job( status_code=204, content="Changes to the job applied successfully." ) - def get_job(self, job_id: uuid.UUID): + def get_job(self, job_id: uuid.UUID, user: User = Depends(Authenticator.validate)): """_summary_ Args: @@ -316,7 +318,9 @@ def get_job(self, job_id: uuid.UUID): return BatchJob(id=job.job_id.__str__(), process=process_graph, **job.dict()) - def delete_job(self, job_id: uuid.UUID): + def delete_job( + self, job_id: uuid.UUID, user: User = Depends(Authenticator.validate) + ): """_summary_ Args: @@ -338,7 +342,7 @@ def delete_job(self, job_id: uuid.UUID): detail=Error(code="FeatureUnsupported", message="Feature not supported."), ) - def estimate(self, job_id: uuid.UUID): + def estimate(self, job_id: uuid.UUID, user: User = Depends(Authenticator.validate)): """_summary_ Args: @@ -360,7 +364,7 @@ def estimate(self, job_id: uuid.UUID): detail=Error(code="FeatureUnsupported", message="Feature not supported."), ) - def logs(self, job_id: uuid.UUID): + def logs(self, job_id: uuid.UUID, user: User = Depends(Authenticator.validate)): """_summary_ Args: @@ -382,7 +386,9 @@ def logs(self, job_id: uuid.UUID): detail=Error(code="FeatureUnsupported", message="Feature not supported."), ) - def get_results(self, job_id: uuid.UUID): + def get_results( + self, job_id: uuid.UUID, user: User = Depends(Authenticator.validate) + ): """_summary_ Args: @@ -404,7 +410,9 @@ def get_results(self, job_id: uuid.UUID): detail=Error(code="FeatureUnsupported", message="Feature not supported."), ) - def start_job(self, job_id: uuid.UUID): + def start_job( + self, job_id: uuid.UUID, user: User = Depends(Authenticator.validate) + ): """_summary_ Args: @@ -426,7 +434,9 @@ def start_job(self, job_id: uuid.UUID): detail=Error(code="FeatureUnsupported", message="Feature not supported."), ) - def cancel_job(self, job_id: uuid.UUID): + def cancel_job( + self, job_id: uuid.UUID, user: User = Depends(Authenticator.validate) + ): """_summary_ Args: @@ -448,7 +458,9 @@ def cancel_job(self, job_id: uuid.UUID): detail=Error(code="FeatureUnsupported", message="Feature not supported."), ) - def delete_job(self, job_id: uuid.UUID): + def delete_job( + self, job_id: uuid.UUID, user: User = Depends(Authenticator.validate) + ): """_summary_ Args: diff --git a/openeo_fastapi/client/processes.py b/openeo_fastapi/client/processes.py index 9ffe8d5..d7b392c 100644 --- a/openeo_fastapi/client/processes.py +++ b/openeo_fastapi/client/processes.py @@ -13,6 +13,13 @@ from openeo_fastapi.client.psql.models import ProcessGraphORM from openeo_fastapi.client.register import EndpointRegister +PROCESSES_ENDPOINTS = [ + Endpoint( + path="/processes", + methods=["GET"], + ) +] + class ProcessGraph(BaseModel): process_graph_id: str @@ -38,12 +45,7 @@ def __init__(self, links) -> None: self.links = links def _initialize_endpoints(self) -> list[Endpoint]: - return [ - Endpoint( - path="/processes", - methods=["GET"], - ) - ] + return PROCESSES_ENDPOINTS def _create_process_registry(self): """ diff --git a/tests/api/test_api.py b/tests/api/test_api.py index b2a14be..f4ca719 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -1,7 +1,22 @@ -from fastapi import FastAPI, HTTPException +from typing import Optional + +from fastapi import Depends, FastAPI, HTTPException, Response from fastapi.testclient import TestClient from openeo_fastapi.api.app import OpenEOApi +from openeo_fastapi.api.responses import FilesGetResponse +from openeo_fastapi.api.types import ( + Billing, + Endpoint, + File, + FileFormat, + GisDataType, + Link, + Plan, +) +from openeo_fastapi.client.auth import Authenticator, User +from openeo_fastapi.client.core import OpenEOCore +from openeo_fastapi.client.files import FILE_ENDPOINTS, FilesRegister def test_api_core(core_api): @@ -35,6 +50,18 @@ def test_get_conformance(core_api, app_settings): assert len(BASIC_CONFORMANCE_CLASSES) == len(response.json()["conformsTo"]) +def test_get_file_formats(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(f"/{app_settings.OPENEO_VERSION}/file_formats") + + assert response.status_code == 200 + + def test_exception_handler(core_api): test_client = TestClient(core_api.app) @@ -53,3 +80,174 @@ def test_exception(): # Assert that the response body matches the expected response generated by the exception handler expected_response = {"code": "NotFound", "message": "This is a test exception"} assert response.json() == expected_response + + +def test_overwriting_register(mocked_oidc_config, mocked_oidc_userinfo, app_settings): + """Test we are able to over write the file register, and in turn the API endpoint.""" + + class ExtendedFileRegister(FilesRegister): + def __init__(self, settings, links) -> None: + super().__init__(settings, links) + + def list_files( + self, + limit: Optional[int] = 10, + user: User = Depends(Authenticator.validate), + ): + """ """ + return FilesGetResponse( + files=[File(path="/somefile.txt", size=10)], + links=[ + Link( + href="https://eodc.eu/", + rel="about", + type="text/html", + title="Homepage of the service provider", + ) + ], + ) + + test_links = [ + Link( + href="https://test.eu/", + rel="about", + type="text/html", + title="Homepage of the service provider", + ) + ] + + extended_register = ExtendedFileRegister(app_settings, test_links) + + formats = [ + FileFormat( + title="json", + gis_data_types=[GisDataType("vector")], + parameters={}, + ) + ] + + client = OpenEOCore( + input_formats=formats, + output_formats=formats, + links=[ + Link( + href="https://eodc.eu/", + rel="about", + type="text/html", + title="Homepage of the service provider", + ) + ], + billing=Billing( + currency="credits", + default_plan="a-cloud", + plans=[Plan(name="user", description="Subscription plan.", paid=True)], + ), + files=extended_register, + ) + + api = OpenEOApi(client=client, app=FastAPI()) + + test_client = test_client = TestClient(api.app) + response = test_client.get( + f"/{app_settings.OPENEO_VERSION}/files", + headers={"Authorization": "Bearer /oidc/egi/not-real"}, + ) + + # assert response.content == '' + assert response.status_code == 200 + + +def test_extending_register(mocked_oidc_config, mocked_oidc_userinfo, app_settings): + """Test we are able to extend the file register, and in turn the API.""" + + new_endpoint = Endpoint( + path="/files/{path}", + methods=["HEAD"], + ) + + class ExtendedFileRegister(FilesRegister): + def __init__(self, settings, links) -> None: + super().__init__(settings, links) + self.endpoints = self._initialize_endpoints() + + def _initialize_endpoints(self) -> list[Endpoint]: + endpoints = list(FILE_ENDPOINTS) + endpoints.append(new_endpoint) + return endpoints + + def get_file_headers( + self, path: str, user: User = Depends(Authenticator.validate) + ): + """ """ + return Response( + status_code=200, + headers={ + "Accept-Ranges": "bytes", + }, + ) + + test_links = [ + Link( + href="https://test.eu/", + rel="about", + type="text/html", + title="Homepage of the service provider", + ) + ] + + extended_register = ExtendedFileRegister(app_settings, test_links) + + # Asser the new endpoint has been added to the register endpoints + assert len(extended_register.endpoints) == 5 + assert new_endpoint in extended_register.endpoints + + formats = [ + FileFormat( + title="json", + gis_data_types=[GisDataType("vector")], + parameters={}, + ) + ] + + client = OpenEOCore( + input_formats=formats, + output_formats=formats, + links=[ + Link( + href="https://eodc.eu/", + rel="about", + type="text/html", + title="Homepage of the service provider", + ) + ], + billing=Billing( + currency="credits", + default_plan="a-cloud", + plans=[Plan(name="user", description="Subscription plan.", paid=True)], + ), + files=extended_register, + ) + + api = OpenEOApi(client=client, app=FastAPI()) + + # Assert we have not brokebn the api initialisation + assert api + + # Add the new route from the api to the app router + api.app.router.add_api_route( + name="file_headers", + path=f"/{api.client.settings.OPENEO_VERSION}/files" + "/{path}", + response_model=None, + response_model_exclude_unset=False, + response_model_exclude_none=True, + methods=["HEAD"], + endpoint=api.client.files.get_file_headers, + ) + + test_client = test_client = TestClient(api.app) + response = test_client.head( + f"/{app_settings.OPENEO_VERSION}/files/somefile.txt", + headers={"Authorization": "Bearer /oidc/egi/not-real"}, + ) + + assert response.status_code == 200 diff --git a/tests/api/test_files.py b/tests/api/test_files.py new file mode 100644 index 0000000..0b3b2bf --- /dev/null +++ b/tests/api/test_files.py @@ -0,0 +1,55 @@ +import uuid + +from fastapi.testclient import TestClient + + +def test_not_implemented( + mocked_oidc_config, mocked_oidc_userinfo, core_api, app_settings +): + """ + Test the following endpoints are initialised correctly, but return an error. + + /files GET + /files/{path} GET + /files/{path} PUT + /files/{path} DELETE + """ + + def assert_not(response): + assert response.status_code == 501 + assert response.json()["code"] == "FeatureUnsupported" + + test_app = TestClient(core_api.app) + + gets = [ + f"/{app_settings.OPENEO_VERSION}/files", + f"/{app_settings.OPENEO_VERSION}/files/somefile.txt", + ] + + for get in gets: + assert_not( + test_app.get( + get, + headers={"Authorization": "Bearer /oidc/egi/not-real"}, + ) + ) + + puts = [f"/{app_settings.OPENEO_VERSION}/files/somefile.txt"] + + for post in puts: + assert_not( + test_app.put( + post, + headers={"Authorization": "Bearer /oidc/egi/not-real"}, + ) + ) + + deletes = [f"/{app_settings.OPENEO_VERSION}/files/somefile.txt"] + + for delete in deletes: + assert_not( + test_app.delete( + delete, + headers={"Authorization": "Bearer /oidc/egi/not-real"}, + ) + ) diff --git a/tests/api/test_jobs.py b/tests/api/test_jobs.py index 48e7b87..c7407a5 100644 --- a/tests/api/test_jobs.py +++ b/tests/api/test_jobs.py @@ -169,7 +169,7 @@ def assert_not(response): for post in posts: assert_not( - test_app.get( + test_app.post( post, headers={"Authorization": "Bearer /oidc/egi/not-real"}, ) diff --git a/tests/conftest.py b/tests/conftest.py index c582933..913898c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,7 +44,7 @@ os.environ["OIDC_ROLES"] = "tester,developer" from openeo_fastapi.api.app import OpenEOApi -from openeo_fastapi.api.types import Billing, Link, Plan +from openeo_fastapi.api.types import Billing, FileFormat, GisDataType, Link, Plan from openeo_fastapi.client import auth, settings from openeo_fastapi.client.core import CollectionRegister, OpenEOCore @@ -56,7 +56,16 @@ def app_settings(): @pytest.fixture() def core_api(): + formats = [ + FileFormat( + title="json", + gis_data_types=[GisDataType("vector")], + parameters={}, + ) + ] client = OpenEOCore( + input_formats=formats, + output_formats=formats, links=[ Link( href="https://eodc.eu/",