diff --git a/hdx_hapi/config/doc_snippets.py b/hdx_hapi/config/doc_snippets.py index bf30b338..d016c3a2 100644 --- a/hdx_hapi/config/doc_snippets.py +++ b/hdx_hapi/config/doc_snippets.py @@ -43,3 +43,5 @@ DOC_SEE_DATASET = 'See the dataset endpoint for details.' DOC_SEE_LOC = 'See the location endpoint for details.' DOC_SEE_ORG_TYPE = 'See the org type endpoint for details.' + +DOC_CURRENCY_CODE = 'Filter the response by the currency code.' diff --git a/hdx_hapi/db/dao/currency_view_dao.py b/hdx_hapi/db/dao/currency_view_dao.py new file mode 100644 index 00000000..c8f4acf1 --- /dev/null +++ b/hdx_hapi/db/dao/currency_view_dao.py @@ -0,0 +1,33 @@ +import logging +from typing import Optional +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select + +from hdx_hapi.db.models.views.all_views import CurrencyView +from hdx_hapi.db.dao.util.util import apply_pagination, case_insensitive_filter +from hdx_hapi.endpoints.util.util import PaginationParams + +logger = logging.getLogger(__name__) + + +async def currencies_view_list( + pagination_parameters: PaginationParams, + db: AsyncSession, + code: Optional[str] = None, +): + logger.info(f'currency_view_list called with params: code={code}') + + query = select(CurrencyView) + if code: + query = case_insensitive_filter(query, CurrencyView.code, code) + + query = apply_pagination(query, pagination_parameters) + + logger.debug(f'Executing SQL query: {query}') + + result = await db.execute(query) + currencies = result.scalars().all() + + logger.info(f'Retrieved {len(currencies)} rows from the database') + + return currencies diff --git a/hdx_hapi/endpoints/get_currency.py b/hdx_hapi/endpoints/get_currency.py new file mode 100644 index 00000000..fb2e26a2 --- /dev/null +++ b/hdx_hapi/endpoints/get_currency.py @@ -0,0 +1,49 @@ +from typing import Annotated +from fastapi import Depends, Query, APIRouter + + +from sqlalchemy.ext.asyncio import AsyncSession +from hdx_hapi.config.doc_snippets import DOC_CURRENCY_CODE + +from hdx_hapi.endpoints.models.base import HapiGenericResponse +from hdx_hapi.endpoints.models.currency import CurrencyResponse +from hdx_hapi.endpoints.util.util import ( + CommonEndpointParams, + OutputFormat, + common_endpoint_parameters, +) +from hdx_hapi.services.csv_transform_logic import transform_result_to_csv_stream_if_requested + +from hdx_hapi.services.currency_logic import get_currencies_srv +from hdx_hapi.services.sql_alchemy_session import get_db + +router = APIRouter( + tags=['Metadata'], +) + + +@router.get( + '/api/metadata/currency', + response_model=HapiGenericResponse[CurrencyResponse], + summary='Get information about how currencies are classified', + include_in_schema=False, +) +@router.get( + '/api/v1/metadata/currency', + response_model=HapiGenericResponse[CurrencyResponse], + summary='Get information about how currencies are classified', +) +async def get_currencies( + common_parameters: Annotated[CommonEndpointParams, Depends(common_endpoint_parameters)], + db: AsyncSession = Depends(get_db), + code: Annotated[ + str, Query(max_length=32, description=f'{DOC_CURRENCY_CODE}', openapi_examples={'usd': {'value': 'usd'}}) + ] = None, + output_format: OutputFormat = OutputFormat.JSON, +): + result = await get_currencies_srv( + pagination_parameters=common_parameters, + db=db, + code=code, + ) + return transform_result_to_csv_stream_if_requested(result, output_format, CurrencyResponse) diff --git a/hdx_hapi/endpoints/models/currency.py b/hdx_hapi/endpoints/models/currency.py new file mode 100644 index 00000000..9f14bfb8 --- /dev/null +++ b/hdx_hapi/endpoints/models/currency.py @@ -0,0 +1,9 @@ +from pydantic import ConfigDict, Field +from hdx_hapi.endpoints.models.base import HapiBaseModel + + +class CurrencyResponse(HapiBaseModel): + code: str = Field(max_length=32) + name: str = Field(max_length=512) + + model_config = ConfigDict(from_attributes=True) diff --git a/hdx_hapi/services/currency_logic.py b/hdx_hapi/services/currency_logic.py new file mode 100644 index 00000000..5c11580f --- /dev/null +++ b/hdx_hapi/services/currency_logic.py @@ -0,0 +1,17 @@ +from typing import Optional +from sqlalchemy.ext.asyncio import AsyncSession + +from hdx_hapi.db.dao.currency_view_dao import currencies_view_list +from hdx_hapi.endpoints.util.util import PaginationParams + + +async def get_currencies_srv( + pagination_parameters: PaginationParams, + db: AsyncSession, + code: Optional[str] = None, +): + return await currencies_view_list( + pagination_parameters=pagination_parameters, + db=db, + code=code, + ) diff --git a/main.py b/main.py index c7ff27ee..728c3c8b 100644 --- a/main.py +++ b/main.py @@ -27,6 +27,7 @@ from hdx_hapi.endpoints.get_national_risk import router as national_risk_router # noqa from hdx_hapi.endpoints.get_wfp_commodity import router as wfp_commodity_router # noqa from hdx_hapi.endpoints.get_food_security import router as food_security_router # noqa +from hdx_hapi.endpoints.get_currency import router as currency_router # noqa # from hdx_hapi.endpoints.delete_example import delete_dataset @@ -59,6 +60,7 @@ app.include_router(dataset_router) app.include_router(wfp_commodity_router) app.include_router(food_security_router) +app.include_router(currency_router) # add middleware diff --git a/tests/conftest.py b/tests/conftest.py index b0bfacbb..60ed9123 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,6 +27,7 @@ from hapi_schema.db_conflict_event import view_params_conflict_event from hapi_schema.db_poverty_rate import view_params_poverty_rate from hapi_schema.db_wfp_commodity import view_params_wfp_commodity +from hapi_schema.db_currency import view_params_currency from hdx_hapi.config.config import get_config from hdx_hapi.db.models.base import Base @@ -48,6 +49,7 @@ 'tests/sample_data/poverty_rate.sql', 'tests/sample_data/food_security.sql', 'tests/sample_data/wfp_commodity.sql', + 'tests/sample_data/currency.sql', ] VIEW_LIST = [ @@ -69,6 +71,7 @@ view_params_conflict_event, view_params_poverty_rate, view_params_wfp_commodity, + view_params_currency, ] diff --git a/tests/sample_data/currency.sql b/tests/sample_data/currency.sql new file mode 100644 index 00000000..5dd99419 --- /dev/null +++ b/tests/sample_data/currency.sql @@ -0,0 +1,6 @@ +-- dummy data +INSERT INTO currency (code, name) +VALUES +('USD', 'United states dollar'), +('RON', 'Romanian leu'), +('EUR', 'Euro'); diff --git a/tests/test_endpoints/endpoint_data.py b/tests/test_endpoints/endpoint_data.py index 28efea5b..5d9264c1 100644 --- a/tests/test_endpoints/endpoint_data.py +++ b/tests/test_endpoints/endpoint_data.py @@ -446,6 +446,12 @@ }, 'expected_fields': ['code', 'name'], }, + '/api/v1/metadata/currency': { + 'query_parameters': { + 'code': 'usD', + }, + 'expected_fields': ['code', 'name'], + }, '/api/v1/metadata/wfp_commodity': { 'query_parameters': { 'code': '001', diff --git a/tests/test_endpoints/test_currency_endpoint.py b/tests/test_endpoints/test_currency_endpoint.py new file mode 100644 index 00000000..ae07a13c --- /dev/null +++ b/tests/test_endpoints/test_currency_endpoint.py @@ -0,0 +1,60 @@ +import pytest +import logging + +from httpx import AsyncClient +from main import app +from tests.test_endpoints.endpoint_data import endpoint_data + +log = logging.getLogger(__name__) + +ENDPOINT_ROUTER = '/api/v1/metadata/currency' +endpoint_data = endpoint_data[ENDPOINT_ROUTER] +query_parameters = endpoint_data['query_parameters'] +expected_fields = endpoint_data['expected_fields'] + + +@pytest.mark.asyncio +async def test_get_currencies(event_loop, refresh_db): + log.info('started test_get_currencies') + async with AsyncClient(app=app, base_url='http://test') as ac: + response = await ac.get(ENDPOINT_ROUTER) + assert response.status_code == 200 + assert len(response.json()['data']) > 0, 'There should be at least one currency in the database' + + +@pytest.mark.asyncio +async def test_get_currency_params(event_loop, refresh_db): + log.info('started test_get_currency_params') + + for param_name, param_value in query_parameters.items(): + async with AsyncClient(app=app, base_url='http://test', params={param_name: param_value}) as ac: + response = await ac.get(ENDPOINT_ROUTER) + + assert response.status_code == 200 + assert len(response.json()['data']) > 0, ( + f'There should be at least one currency entry for parameter "{param_name}" with value "{param_value}" ' + 'in the database' + ) + + async with AsyncClient(app=app, base_url='http://test', params=query_parameters) as ac: + response = await ac.get(ENDPOINT_ROUTER) + + assert response.status_code == 200 + assert ( + len(response.json()['data']) > 0 + ), 'There should be at least one currency entry for all parameters in the database' + + +@pytest.mark.asyncio +async def test_get_currency_result(event_loop, refresh_db): + log.info('started test_get_currency_result') + + async with AsyncClient(app=app, base_url='http://test', params=query_parameters) as ac: + response = await ac.get(ENDPOINT_ROUTER) + + for field in expected_fields: + assert field in response.json()['data'][0], f'Field "{field}" not found in the response' + + assert len(response.json()['data'][0]) == len( + expected_fields + ), 'Response has a different number of fields than expected' diff --git a/tests/test_endpoints/test_endpoints_vs_encode_identifier.py b/tests/test_endpoints/test_endpoints_vs_encode_identifier.py index c21cd57e..e58bcd47 100644 --- a/tests/test_endpoints/test_endpoints_vs_encode_identifier.py +++ b/tests/test_endpoints/test_endpoints_vs_encode_identifier.py @@ -26,6 +26,7 @@ '/api/v1/coordination-context/funding', '/api/v1/coordination-context/conflict-event', '/api/v1/food/food-security', + '/api/v1/metadata/currency', ]