diff --git a/ohsome_quality_api/api/api.py b/ohsome_quality_api/api/api.py index 0122b932f..8f54552b5 100644 --- a/ohsome_quality_api/api/api.py +++ b/ohsome_quality_api/api/api.py @@ -13,7 +13,6 @@ ) from fastapi.responses import JSONResponse from fastapi_i18n import i18n -from geojson import FeatureCollection from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.staticfiles import StaticFiles @@ -28,7 +27,6 @@ from ohsome_quality_api.api.request_models import ( AttributeCompletenessFilterRequest, AttributeCompletenessKeyRequest, - IndicatorDataRequest, IndicatorRequest, LandCoverThematicAccuracyRequest, ) @@ -70,10 +68,8 @@ from ohsome_quality_api.utils.exceptions import ( OhsomeApiError, SizeRestrictionError, - TopicDataSchemaError, ) from ohsome_quality_api.utils.helper import ( - get_class_from_key, get_project_root, json_serialize, ) @@ -182,11 +178,10 @@ async def validation_exception_handler( ) -@app.exception_handler(TopicDataSchemaError) @app.exception_handler(OhsomeApiError) @app.exception_handler(SizeRestrictionError) async def custom_exception_handler( - _: Request, exception: TopicDataSchemaError | OhsomeApiError | SizeRestrictionError + _: Request, exception: OhsomeApiError | SizeRestrictionError ): """Exception handler for custom exceptions.""" return JSONResponse( @@ -228,27 +223,6 @@ def empty_api_response() -> dict: } -@app.post("/indicators/mapping-saturation/data", include_in_schema=False) -async def post_indicator_ms(parameters: IndicatorDataRequest) -> CustomJSONResponse: - """Legacy support for computing the Mapping Saturation indicator for given data.""" - indicators = await main.create_indicator( - key="mapping-saturation", - bpolys=parameters.bpolys, - topic=parameters.topic, - include_figure=parameters.include_figure, - ) - geojson_object = FeatureCollection( - features=[i.as_feature(parameters.include_data) for i in indicators] - ) - response = empty_api_response() - response["attribution"]["text"] = get_class_from_key( - class_type="indicator", - key="mapping-saturation", - ).attribution() - response["result"] = [feature.properties for feature in geojson_object.features] - return CustomJSONResponse(content=response, media_type=MEDIA_TYPE_JSON) - - @app.post( "/indicators/attribute-completeness", tags=["indicator"], diff --git a/ohsome_quality_api/api/request_models.py b/ohsome_quality_api/api/request_models.py index 51212d0f7..1ed6c6ca3 100644 --- a/ohsome_quality_api/api/request_models.py +++ b/ohsome_quality_api/api/request_models.py @@ -15,7 +15,7 @@ from ohsome_quality_api.attributes.definitions import AttributeEnum, get_attributes from ohsome_quality_api.indicators.definitions import get_valid_indicators from ohsome_quality_api.topics.definitions import TopicEnum, get_topic_preset -from ohsome_quality_api.topics.models import Topic, TopicData +from ohsome_quality_api.topics.models import Topic from ohsome_quality_api.utils.helper import snake_to_lower_camel @@ -231,14 +231,3 @@ def validate_indicator_topic_combination(self): ) ) return self - - -class IndicatorDataRequest(BaseBpolys): - """Model for the `/indicators/mapping-saturation/data` endpoint. - - The Topic consists of name, description and data. - """ - - topic: TopicData = Field(..., title="Topic", alias="topic") - include_figure: bool = True - include_data: bool = False diff --git a/ohsome_quality_api/indicators/mapping_saturation/indicator.py b/ohsome_quality_api/indicators/mapping_saturation/indicator.py index edf590326..e81d8a276 100644 --- a/ohsome_quality_api/indicators/mapping_saturation/indicator.py +++ b/ohsome_quality_api/indicators/mapping_saturation/indicator.py @@ -13,7 +13,7 @@ from ohsome_quality_api.indicators.base import BaseIndicator from ohsome_quality_api.indicators.mapping_saturation import models from ohsome_quality_api.ohsome import client as ohsome_client -from ohsome_quality_api.topics.models import Topic, TopicData +from ohsome_quality_api.topics.models import Topic logger = logging.getLogger(__name__) @@ -50,7 +50,7 @@ class MappingSaturation(BaseIndicator): def __init__( self, - topic: Topic | TopicData, + topic: Topic, feature: Feature, time_range: str = "2008-01-01//P1M", ) -> None: @@ -190,10 +190,7 @@ def create_figure(self) -> None: title_text=_("Date"), ticks="outside", ) - if isinstance(self.topic, TopicData): - fig.update_yaxes(title_text=_("Value")) - else: - fig.update_yaxes(title_text=self.topic.aggregation_type.capitalize()) + fig.update_yaxes(title_text=self.topic.aggregation_type.capitalize()) # plot asymptote asymptote = self.data["best_fit"]["asymptote"] diff --git a/ohsome_quality_api/main.py b/ohsome_quality_api/main.py index acc4fc71f..7e5e1d4ee 100644 --- a/ohsome_quality_api/main.py +++ b/ohsome_quality_api/main.py @@ -6,7 +6,7 @@ from geojson import Feature, FeatureCollection from ohsome_quality_api.indicators.base import BaseIndicator as Indicator -from ohsome_quality_api.topics.models import Topic, TopicData +from ohsome_quality_api.topics.models import Topic from ohsome_quality_api.utils.helper import get_class_from_key from ohsome_quality_api.utils.helper_asyncio import gather_with_semaphore from ohsome_quality_api.utils.validators import validate_area @@ -17,7 +17,7 @@ async def create_indicator( key: str, bpolys: FeatureCollection, - topic: TopicData | Topic, + topic: Topic, include_figure: bool = True, **kwargs, ) -> list[Indicator]: diff --git a/ohsome_quality_api/ohsome/client.py b/ohsome_quality_api/ohsome/client.py index 1137a9278..b6ffb8d49 100644 --- a/ohsome_quality_api/ohsome/client.py +++ b/ohsome_quality_api/ohsome/client.py @@ -1,6 +1,5 @@ import datetime import json -from functools import singledispatch import geojson import httpx @@ -15,20 +14,11 @@ from json import JSONDecodeError from ohsome_quality_api.config import get_config_value -from ohsome_quality_api.topics.models import Topic, TopicData -from ohsome_quality_api.utils.exceptions import OhsomeApiError, TopicDataSchemaError +from ohsome_quality_api.topics.models import Topic +from ohsome_quality_api.utils.exceptions import OhsomeApiError -@singledispatch -async def query(topic) -> dict: - """Query ohsome API.""" - raise NotImplementedError( - "Cannot query ohsome API for Topic of type: " + str(type(topic)) - ) - - -@query.register -async def _( +async def query( topic: Topic, bpolys: Feature | FeatureCollection, time: str | None = None, @@ -66,35 +56,6 @@ async def _( return validate_query_results(response, attribute_filter, group_by_boundary) -@query.register -async def _( - topic: TopicData, - bpolys: Feature | FeatureCollection, - attribute_filter: str | None = False, - group_by_boundary: bool | None = False, - **_kargs, -) -> dict: - """Validate data attached to the Topic object and return data. - - Data will only be validated and returned immediately. - The ohsome API will not be queried. - - Args: - topic: Topic with name, description and data attached to it. - bpolys: Feature for a single bounding (multi)polygon. - FeatureCollection for "group by boundaries" queries. In this case the - argument 'group_by' needs to be set to 'True'. - group_by: Group by boundary. - """ - try: - return validate_query_results(topic.data, attribute_filter, group_by_boundary) - except SchemaError as error: - raise TopicDataSchemaError( - "Invalid Topic data input to the Mapping Saturation Indicator.", - error, - ) from error - - async def query_ohsome_api(url: str, data: dict) -> dict: """Query the ohsome API. diff --git a/ohsome_quality_api/topics/models.py b/ohsome_quality_api/topics/models.py index 3d94be3f3..2f1ff87e3 100644 --- a/ohsome_quality_api/topics/models.py +++ b/ohsome_quality_api/topics/models.py @@ -14,10 +14,19 @@ from ohsome_quality_api.utils.helper import snake_to_lower_camel -class BaseTopic(BaseModel): +class Topic(BaseModel): + """Includes the ohsome API endpoint and parameters needed to retrieve the data.""" + key: str name: str description: str + endpoint: Literal["elements"] + aggregation_type: Literal["area", "count", "length", "perimeter", "area/density"] + filter: str + indicators: list[str] + projects: list[ProjectEnum] + source: str | None = None + ratio_filter: str | None = None model_config = ConfigDict( alias_generator=snake_to_lower_camel, extra="forbid", @@ -30,21 +39,3 @@ class BaseTopic(BaseModel): @classmethod def translate(cls, value: str) -> str: return _(value) - - -class Topic(BaseTopic): - """Includes the ohsome API endpoint and parameters needed to retrieve the data.""" - - endpoint: Literal["elements"] - aggregation_type: Literal["area", "count", "length", "perimeter", "area/density"] - filter: str - indicators: list[str] - projects: list[ProjectEnum] - source: str | None = None - ratio_filter: str | None = None - - -class TopicData(BaseTopic): - """Includes the data associated with the topic.""" - - data: dict diff --git a/ohsome_quality_api/utils/exceptions.py b/ohsome_quality_api/utils/exceptions.py index e721b97a3..ae538653c 100644 --- a/ohsome_quality_api/utils/exceptions.py +++ b/ohsome_quality_api/utils/exceptions.py @@ -1,7 +1,6 @@ """Custom exception classes.""" from fastapi_i18n import _ -from schema import SchemaError class OhsomeApiError(Exception): @@ -31,9 +30,3 @@ class EmptyRecordError(DatabaseError): def __init__(self): self.name = "EmptyRecordError" self.message = _("Query returned no record.") - - -class TopicDataSchemaError(Exception): - def __init__(self, message, schema_error: SchemaError): - self.name = "TopicDataSchemaError" - self.message = "{0}\n{1}".format(message, schema_error) diff --git a/tests/integrationtests/api/test_indicators_mapping_saturation_data.py b/tests/integrationtests/api/test_indicators_mapping_saturation_data.py deleted file mode 100644 index 7447ec3bf..000000000 --- a/tests/integrationtests/api/test_indicators_mapping_saturation_data.py +++ /dev/null @@ -1,82 +0,0 @@ -from datetime import datetime, timedelta - -from schema import Optional, Or, Schema - -from tests.unittests.mapping_saturation.fixtures import VALUES_1 as DATA - -ENDPOINT = "/indicators/mapping-saturation/data" - - -RESPONSE_SCHEMA_JSON = Schema( - schema={ - "apiVersion": str, - "attribution": { - "url": str, - Optional("text"): str, - }, - "result": [ - { - Optional("id"): Or(str, int), - "metadata": { - "name": str, - "description": str, - }, - "topic": { - "name": str, - "description": str, - }, - "result": { - "timestamp": str, - "timestampOSM": Or(str), - "value": Or(float, str, int, None), - "label": str, - "description": str, - "figure": dict, - }, - } - ], - }, - name="json", - ignore_extra_keys=True, -) - - -def test_mapping_saturation_data(client, bpolys): - """Test parameter Topic with custom data attached.""" - timestamp_objects = [ - datetime(2020, 7, 17, 9, 10, 0) + timedelta(days=1 * x) - for x in range(DATA.size) - ] - timestamp_iso_string = [t.strftime("%Y-%m-%dT%H:%M:%S") for t in timestamp_objects] - # Data is ohsome API response result for the topic 'building-count' and the bpolys - # of for Heidelberg - parameters = { - "bpolys": bpolys, - "topic": { - "key": "foo", - "name": "bar", - "description": "", - "data": { - "result": [ - {"value": v, "timestamp": t} - for v, t in zip(DATA, timestamp_iso_string, strict=False) - ] - }, - }, - } - response = client.post(ENDPOINT, json=parameters) - assert RESPONSE_SCHEMA_JSON.is_valid(response.json()) - - -def test_mapping_saturation_data_invalid(client, bpolys): - parameters = { - "bpolys": bpolys, - "topic": { - "key": "foo", - "name": "bar", - "description": "", - "data": {"result": [{"value": 1.0}]}, # Missing timestamp item - }, - } - response = client.post(ENDPOINT, json=parameters) - assert response.status_code == 422 diff --git a/tests/integrationtests/test_main.py b/tests/integrationtests/test_main.py index d200a90d8..74965ebe3 100644 --- a/tests/integrationtests/test_main.py +++ b/tests/integrationtests/test_main.py @@ -4,7 +4,6 @@ import pytest from ohsome_quality_api import main -from ohsome_quality_api.topics.models import TopicData from tests.integrationtests.utils import oqapi_vcr @@ -103,26 +102,6 @@ def test_create_indicator_size_limit_bpolys_ms(bpolys, topic_building_count): ) -@mock.patch.dict("os.environ", {"OQAPI_GEOM_SIZE_LIMIT": "1"}, clear=True) -@oqapi_vcr.use_cassette -def test_create_indicator_size_limit_bpolys_data(bpolys): - # Size limit is disabled for request with custom data. - topic = TopicData( - key="key", - name="name", - description="description", - data={ - "result": [ - { - "value": 1.0, - "timestamp": "2020-03-20T01:30:08.180856", - } - ] - }, - ) - asyncio.run(main.create_indicator("mapping-saturation", bpolys, topic)) - - @oqapi_vcr.use_cassette def test_create_indicator_public_feature_collection_single_attribute_completeness( bpolys, topic_building_count, attribute_key diff --git a/tests/unittests/api/test_request_models_data.py b/tests/unittests/api/test_request_models_data.py deleted file mode 100644 index c48c85fd0..000000000 --- a/tests/unittests/api/test_request_models_data.py +++ /dev/null @@ -1,35 +0,0 @@ -import pytest - -from ohsome_quality_api.api.request_models import ( - IndicatorDataRequest, -) - - -def test_indicator_data(bpolys): - topic = {"key": "foo", "name": "bar", "description": "buz", "data": {}} - IndicatorDataRequest( - bpolys=bpolys, - topic=topic, - ) - IndicatorDataRequest(bpolys=bpolys, topic=topic) - - -def test_topic_data_valid(bpolys): - topic = { - "key": "foo", - "name": "bar", - "description": "buz", - "data": {}, - } - IndicatorDataRequest(bpolys=bpolys, topic=topic) - - -def test_topic_data_invalid(bpolys): - for topic in ( - {"key": "foo", "name": "bar", "data": {}}, - {"key": "foo", "description": "bar", "data": {}}, - {"key": "foo", "name": "bar", "description": "buz"}, - {"key": "foo", "name": "bar", "description": "buz", "data": "fis"}, - ): - with pytest.raises(ValueError): - IndicatorDataRequest(bpolys=bpolys, topic=topic) diff --git a/tests/unittests/test_ohsome_client.py b/tests/unittests/test_ohsome_client.py index 2a2c3bc9a..e253a1c7d 100644 --- a/tests/unittests/test_ohsome_client.py +++ b/tests/unittests/test_ohsome_client.py @@ -7,16 +7,11 @@ import httpx import pytest from geojson import FeatureCollection -from schema import Schema +from schema import Schema, SchemaError from ohsome_quality_api.attributes.definitions import build_attribute_filter from ohsome_quality_api.ohsome import client as ohsome_client -from ohsome_quality_api.topics.models import TopicData -from ohsome_quality_api.utils.exceptions import ( - OhsomeApiError, - SchemaError, - TopicDataSchemaError, -) +from ohsome_quality_api.utils.exceptions import OhsomeApiError from .utils import get_geojson_fixture, get_topic_fixture @@ -76,11 +71,6 @@ def test_status_code_400(self) -> None: with self.assertRaises(OhsomeApiError): asyncio.run(ohsome_client.query(self.topic, self.bpolys)) - def test_not_implemeted(self) -> None: - """Query for topic type is not implemeted.""" - with self.assertRaises(NotImplementedError): - asyncio.run(ohsome_client.query("")) - def test_user_agent(self) -> None: with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_request: mock_request.return_value = httpx.Response( @@ -94,102 +84,6 @@ def test_user_agent(self) -> None: mock_request.call_args[1]["headers"]["user-agent"].split("/")[0], ) - def test_topic_data_valid_1(self): - data = asyncio.run( - ohsome_client.query( - TopicData( - key="key", - name="name", - description="description", - data={ - "result": [ - {"value": 1.0, "timestamp": "2020-03-20T01:30:08.180856"} - ] - }, - ), - self.bpolys, - ) - ) - self.assertDictEqual( - {"result": [{"value": 1.0, "timestamp": "2020-03-20T01:30:08.180856"}]}, - data, - ) - - def test_topic_data_valid_2(self): - data = asyncio.run( - ohsome_client.query( - TopicData( - key="key", - name="name", - description="description", - data={ - "result": [ - { - "value": 1.0, - "fromTimestamp": "2020-03-20T01:30:08.180856", - "toTimestamp": "2020-04-20T01:30:08.180856", - } - ] - }, - ), - self.bpolys, - ) - ) - self.assertDictEqual( - { - "result": [ - { - "value": 1.0, - "fromTimestamp": "2020-03-20T01:30:08.180856", - "toTimestamp": "2020-04-20T01:30:08.180856", - } - ] - }, - data, - ) - - def test_topic_data_invalid_empty(self): - with self.assertRaises(TopicDataSchemaError): - asyncio.run( - ohsome_client.query( - TopicData( - key="key", - name="name", - description="description", - data={}, - ), - self.bpolys, - ) - ) - - def test_topic_data_invalid_empty_list(self): - with self.assertRaises(TopicDataSchemaError): - asyncio.run( - ohsome_client.query( - TopicData( - key="key", - name="name", - description="description", - data={"result": []}, - ), - self.bpolys, - ) - ) - - def test_topic_data_invalid_missing_key(self): - with self.assertRaises(TopicDataSchemaError): - asyncio.run( - ohsome_client.query( - TopicData( - key="key", - name="name", - description="description", - data={"result": [{"value": 1.0}]}, - ), - self.bpolys, - ) - ) - class TestOhsomeClientBuildUrl(TestCase): def setUp(self) -> None: diff --git a/tests/unittests/test_topic.py b/tests/unittests/test_topic.py index 3975e46bc..798f76ff9 100644 --- a/tests/unittests/test_topic.py +++ b/tests/unittests/test_topic.py @@ -1,23 +1,7 @@ import pytest from pydantic import ValidationError -from ohsome_quality_api.topics.models import BaseTopic, Topic, TopicData - - -def test_base_topic(): - BaseTopic(key="key", name="name", description="description") - - -def test_base_topic_missing(): - with pytest.raises(ValidationError): - BaseTopic(key="key") - BaseTopic(name="name") - BaseTopic(description="description") - - -def test_base_topic_extra(): - with pytest.raises(ValidationError): - BaseTopic(name="name", key="key", description="description", foo="bar") +from ohsome_quality_api.topics.models import Topic def test_topic_definition(): @@ -105,17 +89,3 @@ def test_topic_definition_extra(): filter="filter", foo="bar", ) - - -def test_topic_data(): - TopicData(key="key", name="name", description="description", data={}) - - -def test_topic_missing(): - with pytest.raises(ValidationError): - TopicData(key="key", name="name", description="description") - - -def test_topic_extra(): - with pytest.raises(ValidationError): - TopicData(key="key", name="name", description="description", data={}, foo="bar")