Skip to content

Commit

Permalink
update application to include middleware exception handler (#30)
Browse files Browse the repository at this point in the history
* update application to include middleware exception handler

* remove not found exception

* remove and shuffle imports

* fix pg parser process
  • Loading branch information
SerRichard authored Mar 15, 2024
1 parent 14039a1 commit aedca75
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 88 deletions.
21 changes: 17 additions & 4 deletions openeo_fastapi/api/app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import re

import attr
from attrs import define, field
from fastapi import APIRouter, Response
from fastapi import APIRouter, HTTPException, Response
from starlette.responses import JSONResponse
from starlette.routing import Route

from openeo_fastapi.client import models

Expand Down Expand Up @@ -137,6 +134,21 @@ def register_core(self):
self.register_get_processes()
self.register_well_known()

def http_exception_handler(self, request, exception):
"""Register exception handler to turn python exceptions into expected OpenEO error output."""
exception_headers = {
"allow_origin": "*",
"allow_credentials": "true",
"allow_methods": "*",
}
from fastapi.encoders import jsonable_encoder

return JSONResponse(
headers=exception_headers,
status_code=exception.status_code,
content=jsonable_encoder(exception.detail),
)

def __attrs_post_init__(self):
"""Post-init hook.
Expand All @@ -151,3 +163,4 @@ def __attrs_post_init__(self):

self.register_get_capabilities()
self.app.include_router(router=self.router)
self.app.add_exception_handler(HTTPException, self.http_exception_handler)
2 changes: 1 addition & 1 deletion openeo_fastapi/client/auth.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from abc import ABC, abstractmethod, abstractproperty
from abc import ABC, abstractmethod
from enum import Enum

import requests
Expand Down
102 changes: 49 additions & 53 deletions openeo_fastapi/client/collections.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
from typing import List

import aiohttp
from fastapi import HTTPException

from openeo_fastapi.client.models import Collection, Collections, Endpoint
from openeo_fastapi.client.register import EndpointRegister
from openeo_fastapi.client.settings import AppSettings


class CollectionRegister(EndpointRegister):
def __init__(self, settings) -> None:
super().__init__()
self.endpoints = self._initialize_endpoints()
self.settings: AppSettings = settings
self.settings = settings
pass

def _initialize_endpoints(self) -> list[Endpoint]:
Expand All @@ -26,62 +24,60 @@ def _initialize_endpoints(self) -> list[Endpoint]:
),
]

async def get_collections(self):
async def _proxy_request(self, path):
"""
Returns Basic metadata for all datasets
Proxy the request with aiohttp.
"""
stac_url = (
self.settings.STAC_API_URL
if self.settings.STAC_API_URL.endswith("/")
else self.settings.STAC_API_URL + "/"
)

try:
async with aiohttp.ClientSession() as client:
async with client.get(stac_url + "collections") as response:
resp = await response.json()
if response.status == 200 and resp.get("collections"):
collections_list = []
for collection_json in resp["collections"]:
if (
not self.settings.STAC_COLLECTIONS_WHITELIST
or collection_json["id"]
in self.settings.STAC_COLLECTIONS_WHITELIST
):
collections_list.append(collection_json)

return Collections(
collections=collections_list, links=resp["links"]
)
else:
return {"Error": "No Collections found."}
except Exception as e:
raise Exception("Ran into: ", e)
async with aiohttp.ClientSession() as client:
async with client.get(self.settings.STAC_API_URL + path) as response:
resp = await response.json()
if response.status == 200:
return resp

async def get_collection(self, collection_id):
"""
Returns Metadata for specific datasetsbased on collection_id (str).
"""
stac_url = (
self.settings.STAC_API_URL
if self.settings.STAC_API_URL.endswith("/")
else self.settings.STAC_API_URL + "/"
not_found = HTTPException(
status_code=404,
detail={
"code": "NotFound",
"message": f"Collection {collection_id} not found.",
},
)

try:
async with aiohttp.ClientSession() as client:
async with client.get(
stac_url + f"collections/{collection_id}"
) as response:
resp = await response.json()
if response.status == 200 and resp.get("id"):
if (
not self.settings.STAC_COLLECTIONS_WHITELIST
or resp["id"] in self.settings.STAC_COLLECTIONS_WHITELIST
):
return Collection(**resp)
else:
return {"Error": "Collection not found."}
if (
not self.settings.STAC_COLLECTIONS_WHITELIST
or collection_id in self.settings.STAC_COLLECTIONS_WHITELIST
):
path = f"collections/{collection_id}"
resp = await self._proxy_request(path)

if resp:
return Collection(**resp)
raise not_found
raise not_found

async def get_collections(self):
"""
Returns Basic metadata for all datasets
"""
path = "collections"
resp = await self._proxy_request(path)

if resp:
collections_list = [
collection
for collection in resp["collections"]
if (
not self.settings.STAC_COLLECTIONS_WHITELIST
or collection["id"] in self.settings.STAC_COLLECTIONS_WHITELIST
)
]

except Exception as e:
raise Exception("Ran into: ", e)
return Collections(collections=collections_list, links=resp["links"])
else:
raise HTTPException(
status_code=404,
detail={"code": "NotFound", "message": "No Collections found."},
)
2 changes: 1 addition & 1 deletion openeo_fastapi/client/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class OpenEOCore:
billing: str = field()
links: list = field()

settings: AppSettings = field()
settings = AppSettings()

_id: str = field(default="OpenEOApi")

Expand Down
8 changes: 3 additions & 5 deletions openeo_fastapi/client/processes.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import functools
from typing import List, Union
from typing import Union

import openeo_pg_parser_networkx
import openeo_processes_dask.specs
from openeo_pg_parser_networkx import Process as pgProcess
from openeo_pg_parser_networkx import ProcessRegistry

from openeo_fastapi.client.models import Endpoint, Error, Process, ProcessesGetResponse
Expand Down Expand Up @@ -36,9 +36,7 @@ def _create_process_registry(self):
}

for process_id, spec in predefined_processes_specs.items():
process_registry[
("predefined", process_id)
] = openeo_pg_parser_networkx.Process(spec)
process_registry[("predefined", process_id)] = pgProcess(spec)

return process_registry

Expand Down
1 change: 0 additions & 1 deletion openeo_fastapi/client/register.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import abc
from typing import List

from openeo_fastapi.client.models import Endpoint

Expand Down
18 changes: 14 additions & 4 deletions openeo_fastapi/client/settings.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from pathlib import Path
from typing import Any, Optional

from pydantic import BaseSettings, HttpUrl
from pydantic import BaseSettings, HttpUrl, validator


class AppSettings(BaseSettings):
"""Place to store application settings."""

API_DNS = HttpUrl
API_TLS: str = "True"
API_DNS: HttpUrl
API_TLS: bool = True

ALEMBIC_DIR: Path

API_TITLE: str
API_DESCRIPTION: str
Expand All @@ -17,9 +20,16 @@ class AppSettings(BaseSettings):

# External APIs
STAC_VERSION: str = "1.0.0"
STAC_API_URL: Optional[HttpUrl]
STAC_API_URL: HttpUrl
STAC_COLLECTIONS_WHITELIST: Optional[list[str]] = []

@validator("STAC_API_URL")
@classmethod
def name_must_contain_space(cls, v: str) -> str:
if v.endswith("/"):
return v
return v.__add__("/")

class Config:
@classmethod
def parse_env_var(cls, field_name: str, raw_val: str) -> Any:
Expand Down
26 changes: 23 additions & 3 deletions tests/api/test_api.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import os
from unittest import mock
from unittest.mock import patch

import pytest
from aioresponses import aioresponses
from fastapi import FastAPI
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient

from openeo_fastapi.api.app import OpenEOApi
Expand Down Expand Up @@ -55,7 +55,7 @@ async def test_get_collections(collections_core, collections):

@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 patch.dict(os.environ, {"STAC_COLLECTIONS_WHITELIST": "Sentinel-2A"}):
with aioresponses() as m:
get_collections_url = f"http://test-stac-api.mock.com/api/collections"
m.get(
Expand Down Expand Up @@ -100,3 +100,23 @@ def test_get_processes(core_api, app_settings):

assert response.status_code == 200
assert "processes" in response.json().keys()


def test_exception_handler(core_api):
test_client = TestClient(core_api.app)

# Define a route that raises an exception
@core_api.app.get("/test-exception")
def test_exception():
raise HTTPException(
status_code=404,
detail={"code": "NotFound", "message": "This is a test exception"},
)

response = test_client.get("/test-exception")

assert response.status_code == 404

# 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
2 changes: 0 additions & 2 deletions tests/client/test_auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from unittest.mock import patch

import pytest
from pydantic import ValidationError

Expand Down
36 changes: 22 additions & 14 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@
from fastapi import FastAPI
from requests import Response

from openeo_fastapi.api.app import OpenEOApi
from openeo_fastapi.client import auth, models, settings
from openeo_fastapi.client.core import CollectionRegister, OpenEOCore

pytestmark = pytest.mark.unit
path_to_current_file = os.path.realpath(__file__)
current_directory = os.path.split(path_to_current_file)[0]
Expand All @@ -26,32 +22,44 @@

fs = fsspec.filesystem(protocol="file")

SETTINGS_DICT = {
"API_DNS": "http://test.api.org",
"API_TLS": "False",
"API_TITLE": "Test Api",
"API_DESCRIPTION": "My Test Api",
"STAC_API_URL": "http://test-stac-api.mock.com/api/",
"ALEMBIC_DIR": str(ALEMBIC_DIR),
}

os.environ["API_DNS"] = "http://test.api.org"
os.environ["API_TLS"] = "False"
os.environ["API_TITLE"] = "Test Api"
os.environ["API_DESCRIPTION"] = "My Test Api"
os.environ["STAC_API_URL"] = "http://test-stac-api.mock.com/api/"
os.environ["ALEMBIC_DIR"] = str(ALEMBIC_DIR)

from openeo_fastapi.api.app import OpenEOApi
from openeo_fastapi.client import auth, models, settings
from openeo_fastapi.client.core import CollectionRegister, OpenEOCore


@pytest.fixture(autouse=True)
def mock_settings_env_vars():
with mock.patch.dict(
os.environ,
{
"API_DNS": "test.api.org",
"API_TLS": "False",
"API_TITLE": "Test Api",
"API_DESCRIPTION": "My Test Api",
"STAC_API_URL": "http://test-stac-api.mock.com/api/",
"ALEMBIC_DIR": str(ALEMBIC_DIR),
},
SETTINGS_DICT,
):
yield


@pytest.fixture()
def app_settings():
return settings.AppSettings()
return settings.AppSettings(**SETTINGS_DICT)


@pytest.fixture()
def core_api():
client = OpenEOCore(
settings=settings.AppSettings(),
links=[
models.Link(
href="https://eodc.eu/",
Expand Down

0 comments on commit aedca75

Please sign in to comment.