diff --git a/README.md b/README.md index 593232f..d8940c0 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,9 @@ The result set can be mapped to a nested Pydantic model like so: ```python from typing import Annotated -from fastapi import FastAPI +from fastapi import FastAPI, Query from pydantic import BaseModel, ConfigDict -from rdfproxy import Page, SPARQLBinding, SPARQLModelAdapter +from rdfproxy import Page, QueryParameters, SPARQLBinding, SPARQLModelAdapter class Work(BaseModel): model_config = ConfigDict(group_by="workName") @@ -77,8 +77,8 @@ The `SPARQLModelAdapter.query` method runs the query and constructs a `Page` obj app = FastAPI() @app.get("/") -def base_route(page: int = 1, size: int = 100) -> Page[Author]: - return adapter.query(page=page, size=size) +def base_route(query_parameters: Annotated[QueryParameters, Query()]) -> Page[Author]: + return adapter.query(query_parameters) ``` This results in the following JSON output: diff --git a/examples/full_static_fastapi_example.py b/examples/full_static_fastapi_example.py index 4d3b8b4..5210238 100644 --- a/examples/full_static_fastapi_example.py +++ b/examples/full_static_fastapi_example.py @@ -2,9 +2,9 @@ from typing import Annotated -from fastapi import FastAPI +from fastapi import FastAPI, Query from pydantic import BaseModel, ConfigDict -from rdfproxy import Page, SPARQLBinding, SPARQLModelAdapter +from rdfproxy import Page, QueryParameters, SPARQLBinding, SPARQLModelAdapter query = """ @@ -48,5 +48,5 @@ class Author(BaseModel): @app.get("/") -def base_route(page: int = 1, size: int = 100) -> Page[Author]: - return adapter.query(page=page, size=size) +def base_route(query_parameters: Annotated[QueryParameters, Query()]) -> Page[Author]: + return adapter.query(query_parameters) diff --git a/examples/releven_person_fastapi_example.py b/examples/releven_person_fastapi_example.py index 15c31f0..1b98280 100644 --- a/examples/releven_person_fastapi_example.py +++ b/examples/releven_person_fastapi_example.py @@ -1,8 +1,10 @@ """RDFProxy-based FastAPI route example: CRM query targeting Releven GraphDB with simple ungrouped Person model.""" -from fastapi import FastAPI +from typing import Annotated + +from fastapi import FastAPI, Query from pydantic import BaseModel -from rdfproxy import Page, SPARQLModelAdapter +from rdfproxy import Page, QueryParameters, SPARQLModelAdapter query = """ @@ -66,5 +68,7 @@ class R11PersonModel(BaseModel): @app.get("/") -def base(page: int = 1, size: int = 100) -> Page[R11PersonModel]: - return adapter.query(page=page, size=size) +def base_route( + query_parameters: Annotated[QueryParameters, Query()], +) -> Page[R11PersonModel]: + return adapter.query(query_parameters) diff --git a/examples/wikidata_grouped_person_fastapi_example.py b/examples/wikidata_grouped_person_fastapi_example.py index c6a8868..0e06def 100644 --- a/examples/wikidata_grouped_person_fastapi_example.py +++ b/examples/wikidata_grouped_person_fastapi_example.py @@ -2,9 +2,9 @@ from typing import Annotated -from fastapi import FastAPI +from fastapi import FastAPI, Query from pydantic import BaseModel, ConfigDict -from rdfproxy import Page, SPARQLBinding, SPARQLModelAdapter +from rdfproxy import Page, QueryParameters, SPARQLBinding, SPARQLModelAdapter query = """ @@ -40,5 +40,5 @@ class Person(BaseModel): @app.get("/") -def base(page: int = 1, size: int = 100) -> Page[Person]: - return adapter.query(page=page, size=size) +def base_route(query_parameters: Annotated[QueryParameters, Query()]) -> Page[Person]: + return adapter.query(query_parameters) diff --git a/examples/wikidata_ungrouped_person_fastapi_example.py b/examples/wikidata_ungrouped_person_fastapi_example.py index 8387f9d..9421210 100644 --- a/examples/wikidata_ungrouped_person_fastapi_example.py +++ b/examples/wikidata_ungrouped_person_fastapi_example.py @@ -2,9 +2,9 @@ from typing import Annotated -from fastapi import FastAPI +from fastapi import FastAPI, Query from pydantic import BaseModel -from rdfproxy import Page, SPARQLBinding, SPARQLModelAdapter +from rdfproxy import Page, QueryParameters, SPARQLBinding, SPARQLModelAdapter query = """ @@ -38,5 +38,5 @@ class Person(BaseModel): @app.get("/") -def base(page: int = 1, size: int = 100) -> Page[Person]: - return adapter.query(page=page, size=size) +def base_route(query_parameters: Annotated[QueryParameters, Query()]) -> Page[Person]: + return adapter.query(query_parameters) diff --git a/rdfproxy/__init__.py b/rdfproxy/__init__.py index caaad1e..6b95c77 100644 --- a/rdfproxy/__init__.py +++ b/rdfproxy/__init__.py @@ -1,4 +1,4 @@ from rdfproxy.adapter import SPARQLModelAdapter # noqa: F401 from rdfproxy.mapper import ModelBindingsMapper # noqa: F401 from rdfproxy.utils._types import SPARQLBinding # noqa: F401 -from rdfproxy.utils.models import Page # noqa: F401 +from rdfproxy.utils.models import Page, QueryParameters # noqa: F401 diff --git a/rdfproxy/adapter.py b/rdfproxy/adapter.py index 9836b9b..ef93ad3 100644 --- a/rdfproxy/adapter.py +++ b/rdfproxy/adapter.py @@ -7,7 +7,7 @@ from SPARQLWrapper import JSON, SPARQLWrapper from rdfproxy.mapper import ModelBindingsMapper from rdfproxy.utils._types import _TModelInstance -from rdfproxy.utils.models import Page +from rdfproxy.utils.models import Page, QueryParameters from rdfproxy.utils.sparql_utils import ( calculate_offset, construct_count_query, @@ -42,19 +42,14 @@ def __init__( ) self.sparql_wrapper.setReturnFormat(JSON) - def query( - self, - *, - page: int = 1, - size: int = 100, - ) -> Page[_TModelInstance]: + def query(self, query_parameters: QueryParameters) -> Page[_TModelInstance]: """Run a query against an endpoint and return a Page model object.""" count_query: str = construct_count_query(query=self._query, model=self._model) items_query: str = construct_items_query( query=self._query, model=self._model, - limit=size, - offset=calculate_offset(page, size), + limit=query_parameters.size, + offset=calculate_offset(query_parameters.page, query_parameters.size), ) items_query_bindings: Iterator[dict] = query_with_wrapper( @@ -65,9 +60,15 @@ def query( items: list[_TModelInstance] = mapper.get_models() total: int = self._get_count(count_query) - pages: int = math.ceil(total / size) - - return Page(items=items, page=page, size=size, total=total, pages=pages) + pages: int = math.ceil(total / query_parameters.size) + + return Page( + items=items, + page=query_parameters.page, + size=query_parameters.size, + total=total, + pages=pages, + ) def _get_count(self, query: str) -> int: """Run a count query and return the count result. diff --git a/rdfproxy/utils/models.py b/rdfproxy/utils/models.py index d4ec9e4..9eef6a9 100644 --- a/rdfproxy/utils/models.py +++ b/rdfproxy/utils/models.py @@ -2,7 +2,7 @@ from typing import Generic -from pydantic import BaseModel +from pydantic import BaseModel, Field from rdfproxy.utils._types import _TModelInstance @@ -21,3 +21,13 @@ class Page(BaseModel, Generic[_TModelInstance]): size: int total: int pages: int + + +class QueryParameters(BaseModel): + """Query parameter model for SPARQLModelAdapter.query. + + See https://fastapi.tiangolo.com/tutorial/query-param-models/ + """ + + page: int = Field(default=1, gt=0) + size: int = Field(default=100, ge=1) diff --git a/tests/tests_adapter/test_adapter_grouped_pagination.py b/tests/tests_adapter/test_adapter_grouped_pagination.py index f201f22..ae444f7 100644 --- a/tests/tests_adapter/test_adapter_grouped_pagination.py +++ b/tests/tests_adapter/test_adapter_grouped_pagination.py @@ -5,7 +5,7 @@ import pytest from pydantic import BaseModel, ConfigDict -from rdfproxy import Page, SPARQLModelAdapter +from rdfproxy import Page, QueryParameters, SPARQLModelAdapter query = """ @@ -104,4 +104,5 @@ class AdapterParameter(NamedTuple): ["adapter", "query_parameters", "expected"], adapter_parameters ) def test_basic_adapter_grouped_pagination(adapter, query_parameters, expected): - assert adapter.query(**query_parameters) == expected + parameters = QueryParameters(**query_parameters) + assert adapter.query(parameters) == expected