Skip to content

Commit

Permalink
feat: implement pagination behavior for grouped models
Browse files Browse the repository at this point in the history
Closes #114.
  • Loading branch information
lu-pl committed Oct 30, 2024
1 parent 319de5a commit 988f836
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 13 deletions.
10 changes: 7 additions & 3 deletions rdfproxy/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@

from SPARQLWrapper import JSON, SPARQLWrapper
from rdfproxy.mapper import ModelBindingsMapper
from rdfproxy.utils._types import _TModelInstance
from rdfproxy.utils._types import ItemsQueryConstructor, _TModelInstance
from rdfproxy.utils.models import Page
from rdfproxy.utils.sparql_utils import (
calculate_offset,
construct_count_query,
get_items_query_constructor,
query_with_wrapper,
ungrouped_pagination_base_query,
)


Expand Down Expand Up @@ -50,7 +50,11 @@ def query(
) -> 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 = ungrouped_pagination_base_query.substitute(

_items_query_constructor: ItemsQueryConstructor = get_items_query_constructor(
self._model
)
items_query: str = _items_query_constructor(
query=self._query, offset=calculate_offset(page, size), limit=size
)

Expand Down
6 changes: 5 additions & 1 deletion rdfproxy/utils/_types.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
"""Type definitions for rdfproxy."""

from typing import TypeVar
from typing import Protocol, TypeVar

from pydantic import BaseModel


_TModelInstance = TypeVar("_TModelInstance", bound=BaseModel)


class ItemsQueryConstructor(Protocol):
def __call__(self, query: str, offset: int, limit: int) -> str: ...


class SPARQLBinding(str):
"""SPARQLBinding type for explicit SPARQL binding to model field allocation.
Expand Down
63 changes: 54 additions & 9 deletions rdfproxy/utils/sparql_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,25 @@

from collections.abc import Iterator
from contextlib import contextmanager
from functools import partial
import re
from string import Template
from typing import Annotated
from textwrap import indent
from typing import cast

from SPARQLWrapper import QueryResult, SPARQLWrapper
from rdfproxy.utils._types import _TModelInstance
from rdfproxy.utils._types import ItemsQueryConstructor, _TModelInstance


ungrouped_pagination_base_query: Annotated[
str, "SPARQL template for query pagination."
] = Template("""
$query
limit $limit
offset $offset
""")
def construct_ungrouped_pagination_query(query: str, limit: int, offset: int) -> str:
"""Construct an ungrouped pagination query."""
template: Template = Template("""
$query
limit $limit
offset $offset
""")

return template.substitute(query=query, limit=limit, offset=offset)


def replace_query_select_clause(query: str, repl: str) -> str:
Expand All @@ -36,6 +39,48 @@ def replace_query_select_clause(query: str, repl: str) -> str:
return count_query


def inject_subquery(
query: str, subquery: str, indent_depth: int = 4, indent_char: str = " "
) -> str:
"""Inject a SPARQL query with a subquery.
Also apply some basic indentation.
"""
indent_value = indent_char * indent_depth
indented_subquery = indent(f"\n{subquery}\n", indent_value)
indented_subclause = indent(f"\n{{{indented_subquery}}}", indent_value)
return re.sub(r".*\}$", f"{indented_subclause}\n}}", query)


def construct_grouped_pagination_query(
query: str, group_by_value: str, limit: int, offset: int
) -> str:
"""Construct a grouped pagination query."""
_subquery_base: str = replace_query_select_clause(
query=query, repl=f"select distinct ?{group_by_value}"
)
subquery: str = construct_ungrouped_pagination_query(
query=_subquery_base, limit=limit, offset=offset
)

grouped_pagination_query: str = inject_subquery(query=query, subquery=subquery)
return grouped_pagination_query


def get_items_query_constructor(
model: type[_TModelInstance],
) -> ItemsQueryConstructor:
"""Get the applicable query constructor function given a model class."""
if (group_by_value := model.model_config.get("group_by", None)) is None:
query_constructor = construct_ungrouped_pagination_query
else:
query_constructor = partial(
construct_grouped_pagination_query, group_by_value=group_by_value
)

return cast(ItemsQueryConstructor, query_constructor)


def construct_count_query(query: str, model: type[_TModelInstance]) -> str:
"""Construct a generic count query from a SELECT query."""
try:
Expand Down

0 comments on commit 988f836

Please sign in to comment.