From 8d00976cb95ee894d720770b3ce6ca2db7445f3c Mon Sep 17 00:00:00 2001 From: Robin VAN DE MERGHEL Date: Tue, 5 Aug 2025 08:35:37 +0200 Subject: [PATCH 1/2] refactor: Made search and summary generic to reuse it easily --- .../_generated/aio/operations/_operations.py | 24 +-- .../client/_generated/models/__init__.py | 16 +- .../client/_generated/models/_models.py | 176 +++++++++--------- .../_generated/operations/_operations.py | 24 +-- .../src/diracx/client/patches/jobs/common.py | 2 +- diracx-core/src/diracx/core/models.py | 4 +- diracx-db/src/diracx/db/sql/dummy/db.py | 17 +- diracx-db/src/diracx/db/sql/job/db.py | 69 ++----- diracx-db/src/diracx/db/sql/utils/__init__.py | 2 + diracx-db/src/diracx/db/sql/utils/base.py | 86 ++++++++- diracx-db/tests/test_dummy_db.py | 2 +- diracx-logic/src/diracx/logic/jobs/query.py | 10 +- diracx-logic/src/diracx/logic/jobs/status.py | 11 +- .../diracx/routers/jobs/access_policies.py | 9 +- .../src/diracx/routers/jobs/query.py | 8 +- .../_generated/aio/operations/_operations.py | 24 +-- .../client/_generated/models/__init__.py | 16 +- .../client/_generated/models/_models.py | 176 +++++++++--------- .../_generated/operations/_operations.py | 24 +-- .../src/gubbins/db/sql/lollygag/db.py | 17 +- 20 files changed, 376 insertions(+), 341 deletions(-) diff --git a/diracx-client/src/diracx/client/_generated/aio/operations/_operations.py b/diracx-client/src/diracx/client/_generated/aio/operations/_operations.py index 0916d8a28..ab639e526 100644 --- a/diracx-client/src/diracx/client/_generated/aio/operations/_operations.py +++ b/diracx-client/src/diracx/client/_generated/aio/operations/_operations.py @@ -1826,7 +1826,7 @@ async def patch_metadata(self, body: Union[Dict[str, Dict[str, Any]], IO[bytes]] @overload async def search( self, - body: Optional[_models.JobSearchParams] = None, + body: Optional[_models.SearchParams] = None, *, page: int = 1, per_page: int = 100, @@ -1840,7 +1840,7 @@ async def search( **TODO: Add more docs**. :param body: Default value is None. - :type body: ~_generated.models.JobSearchParams + :type body: ~_generated.models.SearchParams :keyword page: Default value is 1. :paramtype page: int :keyword per_page: Default value is 100. @@ -1886,7 +1886,7 @@ async def search( @distributed_trace_async async def search( self, - body: Optional[Union[_models.JobSearchParams, IO[bytes]]] = None, + body: Optional[Union[_models.SearchParams, IO[bytes]]] = None, *, page: int = 1, per_page: int = 100, @@ -1898,8 +1898,8 @@ async def search( **TODO: Add more docs**. - :param body: Is either a JobSearchParams type or a IO[bytes] type. Default value is None. - :type body: ~_generated.models.JobSearchParams or IO[bytes] + :param body: Is either a SearchParams type or a IO[bytes] type. Default value is None. + :type body: ~_generated.models.SearchParams or IO[bytes] :keyword page: Default value is 1. :paramtype page: int :keyword per_page: Default value is 100. @@ -1929,7 +1929,7 @@ async def search( _content = body else: if body is not None: - _json = self._serialize.body(body, "JobSearchParams") + _json = self._serialize.body(body, "SearchParams") else: _json = None @@ -1968,14 +1968,14 @@ async def search( @overload async def summary( - self, body: _models.JobSummaryParams, *, content_type: str = "application/json", **kwargs: Any + self, body: _models.SummaryParams, *, content_type: str = "application/json", **kwargs: Any ) -> Any: """Summary. Show information suitable for plotting. :param body: Required. - :type body: ~_generated.models.JobSummaryParams + :type body: ~_generated.models.SummaryParams :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str @@ -2001,13 +2001,13 @@ async def summary(self, body: IO[bytes], *, content_type: str = "application/jso """ @distributed_trace_async - async def summary(self, body: Union[_models.JobSummaryParams, IO[bytes]], **kwargs: Any) -> Any: + async def summary(self, body: Union[_models.SummaryParams, IO[bytes]], **kwargs: Any) -> Any: """Summary. Show information suitable for plotting. - :param body: Is either a JobSummaryParams type or a IO[bytes] type. Required. - :type body: ~_generated.models.JobSummaryParams or IO[bytes] + :param body: Is either a SummaryParams type or a IO[bytes] type. Required. + :type body: ~_generated.models.SummaryParams or IO[bytes] :return: any :rtype: any :raises ~azure.core.exceptions.HttpResponseError: @@ -2032,7 +2032,7 @@ async def summary(self, body: Union[_models.JobSummaryParams, IO[bytes]], **kwar if isinstance(body, (IOBase, bytes)): _content = body else: - _json = self._serialize.body(body, "JobSummaryParams") + _json = self._serialize.body(body, "SummaryParams") _request = build_jobs_summary_request( content_type=content_type, diff --git a/diracx-client/src/diracx/client/_generated/models/__init__.py b/diracx-client/src/diracx/client/_generated/models/__init__.py index 7343700e4..98386ed1f 100644 --- a/diracx-client/src/diracx/client/_generated/models/__init__.py +++ b/diracx-client/src/diracx/client/_generated/models/__init__.py @@ -20,11 +20,7 @@ InitiateDeviceFlowResponse, InsertedJob, JobCommand, - JobSearchParams, - JobSearchParamsSearchItem, JobStatusUpdate, - JobSummaryParams, - JobSummaryParamsSearchItem, Metadata, OpenIDConfiguration, SandboxDownloadResponse, @@ -32,9 +28,13 @@ SandboxUploadResponse, ScalarSearchSpec, ScalarSearchSpecValue, + SearchParams, + SearchParamsSearchItem, SetJobStatusReturn, SetJobStatusReturnSuccess, SortSpec, + SummaryParams, + SummaryParamsSearchItem, SupportInfo, TokenResponse, UserInfoResponse, @@ -67,11 +67,7 @@ "InitiateDeviceFlowResponse", "InsertedJob", "JobCommand", - "JobSearchParams", - "JobSearchParamsSearchItem", "JobStatusUpdate", - "JobSummaryParams", - "JobSummaryParamsSearchItem", "Metadata", "OpenIDConfiguration", "SandboxDownloadResponse", @@ -79,9 +75,13 @@ "SandboxUploadResponse", "ScalarSearchSpec", "ScalarSearchSpecValue", + "SearchParams", + "SearchParamsSearchItem", "SetJobStatusReturn", "SetJobStatusReturnSuccess", "SortSpec", + "SummaryParams", + "SummaryParamsSearchItem", "SupportInfo", "TokenResponse", "UserInfoResponse", diff --git a/diracx-client/src/diracx/client/_generated/models/_models.py b/diracx-client/src/diracx/client/_generated/models/_models.py index be192ae12..b549b357f 100644 --- a/diracx-client/src/diracx/client/_generated/models/_models.py +++ b/diracx-client/src/diracx/client/_generated/models/_models.py @@ -358,56 +358,6 @@ def __init__(self, *, job_id: int, command: str, arguments: Optional[str] = None self.arguments = arguments -class JobSearchParams(_serialization.Model): - """JobSearchParams. - - :ivar parameters: Parameters. - :vartype parameters: list[str] - :ivar search: Search. - :vartype search: list[~_generated.models.JobSearchParamsSearchItem] - :ivar sort: Sort. - :vartype sort: list[~_generated.models.SortSpec] - :ivar distinct: Distinct. - :vartype distinct: bool - """ - - _attribute_map = { - "parameters": {"key": "parameters", "type": "[str]"}, - "search": {"key": "search", "type": "[JobSearchParamsSearchItem]"}, - "sort": {"key": "sort", "type": "[SortSpec]"}, - "distinct": {"key": "distinct", "type": "bool"}, - } - - def __init__( - self, - *, - parameters: Optional[List[str]] = None, - search: List["_models.JobSearchParamsSearchItem"] = [], - sort: List["_models.SortSpec"] = [], - distinct: bool = False, - **kwargs: Any - ) -> None: - """ - :keyword parameters: Parameters. - :paramtype parameters: list[str] - :keyword search: Search. - :paramtype search: list[~_generated.models.JobSearchParamsSearchItem] - :keyword sort: Sort. - :paramtype sort: list[~_generated.models.SortSpec] - :keyword distinct: Distinct. - :paramtype distinct: bool - """ - super().__init__(**kwargs) - self.parameters = parameters - self.search = search - self.sort = sort - self.distinct = distinct - - -class JobSearchParamsSearchItem(_serialization.Model): - """JobSearchParamsSearchItem.""" - - class JobStatusUpdate(_serialization.Model): """JobStatusUpdate. @@ -458,44 +408,6 @@ def __init__( self.source = source -class JobSummaryParams(_serialization.Model): - """JobSummaryParams. - - All required parameters must be populated in order to send to server. - - :ivar grouping: Grouping. Required. - :vartype grouping: list[str] - :ivar search: Search. - :vartype search: list[~_generated.models.JobSummaryParamsSearchItem] - """ - - _validation = { - "grouping": {"required": True}, - } - - _attribute_map = { - "grouping": {"key": "grouping", "type": "[str]"}, - "search": {"key": "search", "type": "[JobSummaryParamsSearchItem]"}, - } - - def __init__( - self, *, grouping: List[str], search: List["_models.JobSummaryParamsSearchItem"] = [], **kwargs: Any - ) -> None: - """ - :keyword grouping: Grouping. Required. - :paramtype grouping: list[str] - :keyword search: Search. - :paramtype search: list[~_generated.models.JobSummaryParamsSearchItem] - """ - super().__init__(**kwargs) - self.grouping = grouping - self.search = search - - -class JobSummaryParamsSearchItem(_serialization.Model): - """JobSummaryParamsSearchItem.""" - - class Metadata(_serialization.Model): """Metadata. @@ -836,6 +748,56 @@ class ScalarSearchSpecValue(_serialization.Model): """Value.""" +class SearchParams(_serialization.Model): + """SearchParams. + + :ivar parameters: Parameters. + :vartype parameters: list[str] + :ivar search: Search. + :vartype search: list[~_generated.models.SearchParamsSearchItem] + :ivar sort: Sort. + :vartype sort: list[~_generated.models.SortSpec] + :ivar distinct: Distinct. + :vartype distinct: bool + """ + + _attribute_map = { + "parameters": {"key": "parameters", "type": "[str]"}, + "search": {"key": "search", "type": "[SearchParamsSearchItem]"}, + "sort": {"key": "sort", "type": "[SortSpec]"}, + "distinct": {"key": "distinct", "type": "bool"}, + } + + def __init__( + self, + *, + parameters: Optional[List[str]] = None, + search: List["_models.SearchParamsSearchItem"] = [], + sort: List["_models.SortSpec"] = [], + distinct: bool = False, + **kwargs: Any + ) -> None: + """ + :keyword parameters: Parameters. + :paramtype parameters: list[str] + :keyword search: Search. + :paramtype search: list[~_generated.models.SearchParamsSearchItem] + :keyword sort: Sort. + :paramtype sort: list[~_generated.models.SortSpec] + :keyword distinct: Distinct. + :paramtype distinct: bool + """ + super().__init__(**kwargs) + self.parameters = parameters + self.search = search + self.sort = sort + self.distinct = distinct + + +class SearchParamsSearchItem(_serialization.Model): + """SearchParamsSearchItem.""" + + class SetJobStatusReturn(_serialization.Model): """SetJobStatusReturn. @@ -979,6 +941,44 @@ def __init__(self, *, parameter: str, direction: Union[str, "_models.SortDirecti self.direction = direction +class SummaryParams(_serialization.Model): + """SummaryParams. + + All required parameters must be populated in order to send to server. + + :ivar grouping: Grouping. Required. + :vartype grouping: list[str] + :ivar search: Search. + :vartype search: list[~_generated.models.SummaryParamsSearchItem] + """ + + _validation = { + "grouping": {"required": True}, + } + + _attribute_map = { + "grouping": {"key": "grouping", "type": "[str]"}, + "search": {"key": "search", "type": "[SummaryParamsSearchItem]"}, + } + + def __init__( + self, *, grouping: List[str], search: List["_models.SummaryParamsSearchItem"] = [], **kwargs: Any + ) -> None: + """ + :keyword grouping: Grouping. Required. + :paramtype grouping: list[str] + :keyword search: Search. + :paramtype search: list[~_generated.models.SummaryParamsSearchItem] + """ + super().__init__(**kwargs) + self.grouping = grouping + self.search = search + + +class SummaryParamsSearchItem(_serialization.Model): + """SummaryParamsSearchItem.""" + + class SupportInfo(_serialization.Model): """SupportInfo. diff --git a/diracx-client/src/diracx/client/_generated/operations/_operations.py b/diracx-client/src/diracx/client/_generated/operations/_operations.py index 0259e5aaf..1b716f93e 100644 --- a/diracx-client/src/diracx/client/_generated/operations/_operations.py +++ b/diracx-client/src/diracx/client/_generated/operations/_operations.py @@ -2351,7 +2351,7 @@ def patch_metadata( # pylint: disable=inconsistent-return-statements @overload def search( self, - body: Optional[_models.JobSearchParams] = None, + body: Optional[_models.SearchParams] = None, *, page: int = 1, per_page: int = 100, @@ -2365,7 +2365,7 @@ def search( **TODO: Add more docs**. :param body: Default value is None. - :type body: ~_generated.models.JobSearchParams + :type body: ~_generated.models.SearchParams :keyword page: Default value is 1. :paramtype page: int :keyword per_page: Default value is 100. @@ -2411,7 +2411,7 @@ def search( @distributed_trace def search( self, - body: Optional[Union[_models.JobSearchParams, IO[bytes]]] = None, + body: Optional[Union[_models.SearchParams, IO[bytes]]] = None, *, page: int = 1, per_page: int = 100, @@ -2423,8 +2423,8 @@ def search( **TODO: Add more docs**. - :param body: Is either a JobSearchParams type or a IO[bytes] type. Default value is None. - :type body: ~_generated.models.JobSearchParams or IO[bytes] + :param body: Is either a SearchParams type or a IO[bytes] type. Default value is None. + :type body: ~_generated.models.SearchParams or IO[bytes] :keyword page: Default value is 1. :paramtype page: int :keyword per_page: Default value is 100. @@ -2454,7 +2454,7 @@ def search( _content = body else: if body is not None: - _json = self._serialize.body(body, "JobSearchParams") + _json = self._serialize.body(body, "SearchParams") else: _json = None @@ -2492,13 +2492,13 @@ def search( return deserialized # type: ignore @overload - def summary(self, body: _models.JobSummaryParams, *, content_type: str = "application/json", **kwargs: Any) -> Any: + def summary(self, body: _models.SummaryParams, *, content_type: str = "application/json", **kwargs: Any) -> Any: """Summary. Show information suitable for plotting. :param body: Required. - :type body: ~_generated.models.JobSummaryParams + :type body: ~_generated.models.SummaryParams :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str @@ -2524,13 +2524,13 @@ def summary(self, body: IO[bytes], *, content_type: str = "application/json", ** """ @distributed_trace - def summary(self, body: Union[_models.JobSummaryParams, IO[bytes]], **kwargs: Any) -> Any: + def summary(self, body: Union[_models.SummaryParams, IO[bytes]], **kwargs: Any) -> Any: """Summary. Show information suitable for plotting. - :param body: Is either a JobSummaryParams type or a IO[bytes] type. Required. - :type body: ~_generated.models.JobSummaryParams or IO[bytes] + :param body: Is either a SummaryParams type or a IO[bytes] type. Required. + :type body: ~_generated.models.SummaryParams or IO[bytes] :return: any :rtype: any :raises ~azure.core.exceptions.HttpResponseError: @@ -2555,7 +2555,7 @@ def summary(self, body: Union[_models.JobSummaryParams, IO[bytes]], **kwargs: An if isinstance(body, (IOBase, bytes)): _content = body else: - _json = self._serialize.body(body, "JobSummaryParams") + _json = self._serialize.body(body, "SummaryParams") _request = build_jobs_summary_request( content_type=content_type, diff --git a/diracx-client/src/diracx/client/patches/jobs/common.py b/diracx-client/src/diracx/client/patches/jobs/common.py index fa080ffd8..108f45bb7 100644 --- a/diracx-client/src/diracx/client/patches/jobs/common.py +++ b/diracx-client/src/diracx/client/patches/jobs/common.py @@ -59,7 +59,7 @@ def make_search_body(**kwargs: Unpack[SearchKwargs]) -> UnderlyingSearchArgs: class SummaryBody(TypedDict, total=False): grouping: list[str] - search: list[str] + search: list[SearchSpec] class SummaryKwargs(SummaryBody, ResponseExtra): ... diff --git a/diracx-core/src/diracx/core/models.py b/diracx-core/src/diracx/core/models.py index 772265942..1840975a3 100644 --- a/diracx-core/src/diracx/core/models.py +++ b/diracx-core/src/diracx/core/models.py @@ -61,13 +61,13 @@ class InsertedJob(TypedDict): TimeStamp: datetime -class JobSummaryParams(BaseModel): +class SummaryParams(BaseModel): grouping: list[str] search: list[SearchSpec] = [] # TODO: Add more validation -class JobSearchParams(BaseModel): +class SearchParams(BaseModel): parameters: list[str] | None = None search: list[SearchSpec] = [] sort: list[SortSpec] = [] diff --git a/diracx-db/src/diracx/db/sql/dummy/db.py b/diracx-db/src/diracx/db/sql/dummy/db.py index 0c25df43c..5735b43bb 100644 --- a/diracx-db/src/diracx/db/sql/dummy/db.py +++ b/diracx-db/src/diracx/db/sql/dummy/db.py @@ -1,9 +1,9 @@ from __future__ import annotations -from sqlalchemy import func, insert, select +from sqlalchemy import insert from uuid_utils import UUID -from diracx.db.sql.utils import BaseSQLDB, apply_search_filters +from diracx.db.sql.utils import BaseSQLDB from .schema import Base as DummyDBBase from .schema import Cars, Owners @@ -22,18 +22,7 @@ class DummyDB(BaseSQLDB): metadata = DummyDBBase.metadata async def summary(self, group_by, search) -> list[dict[str, str | int]]: - columns = [Cars.__table__.columns[x] for x in group_by] - - stmt = select(*columns, func.count(Cars.license_plate).label("count")) - stmt = apply_search_filters(Cars.__table__.columns.__getitem__, stmt, search) - stmt = stmt.group_by(*columns) - - # Execute the query - return [ - dict(row._mapping) - async for row in (await self.conn.stream(stmt)) - if row.count > 0 # type: ignore - ] + return await self._summary(Cars, group_by, search) async def insert_owner(self, name: str) -> int: stmt = insert(Owners).values(name=name) diff --git a/diracx-db/src/diracx/db/sql/job/db.py b/diracx-db/src/diracx/db/sql/job/db.py index 89f2bb49d..01cdb83a1 100644 --- a/diracx-db/src/diracx/db/sql/job/db.py +++ b/diracx-db/src/diracx/db/sql/job/db.py @@ -5,7 +5,7 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING, Any, Iterable -from sqlalchemy import bindparam, case, delete, func, insert, select, update +from sqlalchemy import bindparam, case, delete, insert, select, update if TYPE_CHECKING: from sqlalchemy.sql.elements import BindParameter @@ -13,7 +13,7 @@ from diracx.core.exceptions import InvalidQueryError from diracx.core.models import JobCommand, SearchSpec, SortSpec -from ..utils import BaseSQLDB, apply_search_filters, apply_sort_constraints +from ..utils import BaseSQLDB, _get_columns from ..utils.functions import utcnow from .schema import ( HeartBeatLoggingInfo, @@ -25,17 +25,6 @@ ) -def _get_columns(table, parameters): - columns = [x for x in table.columns] - if parameters: - if unrecognised_parameters := set(parameters) - set(table.columns.keys()): - raise InvalidQueryError( - f"Unrecognised parameters requested {unrecognised_parameters}" - ) - columns = [c for c in columns if c.name in parameters] - return columns - - class JobDB(BaseSQLDB): metadata = JobDBBase.metadata @@ -54,20 +43,11 @@ class JobDB(BaseSQLDB): # to find a way to make it dynamic jdl_2_db_parameters = ["JobName", "JobType", "JobGroup"] - async def summary(self, group_by, search) -> list[dict[str, str | int]]: + async def summary( + self, group_by: list[str], search: list[SearchSpec] + ) -> list[dict[str, str | int]]: """Get a summary of the jobs.""" - columns = _get_columns(Jobs.__table__, group_by) - - stmt = select(*columns, func.count(Jobs.job_id).label("count")) - stmt = apply_search_filters(Jobs.__table__.columns.__getitem__, stmt, search) - stmt = stmt.group_by(*columns) - - # Execute the query - return [ - dict(row._mapping) - async for row in (await self.conn.stream(stmt)) - if row.count > 0 # type: ignore - ] + return await self._summary(table=Jobs, group_by=group_by, search=search) async def search( self, @@ -80,34 +60,15 @@ async def search( page: int | None = None, ) -> tuple[int, list[dict[Any, Any]]]: """Search for jobs in the database.""" - # Find which columns to select - columns = _get_columns(Jobs.__table__, parameters) - - stmt = select(*columns) - - stmt = apply_search_filters(Jobs.__table__.columns.__getitem__, stmt, search) - stmt = apply_sort_constraints(Jobs.__table__.columns.__getitem__, stmt, sorts) - - if distinct: - stmt = stmt.distinct() - - # Calculate total count before applying pagination - total_count_subquery = stmt.alias() - total_count_stmt = select(func.count()).select_from(total_count_subquery) - total = (await self.conn.execute(total_count_stmt)).scalar_one() - - # Apply pagination - if page is not None: - if page < 1: - raise InvalidQueryError("Page must be a positive integer") - if per_page < 1: - raise InvalidQueryError("Per page must be a positive integer") - stmt = stmt.offset((page - 1) * per_page).limit(per_page) - - # Execute the query - return total, [ - dict(row._mapping) async for row in (await self.conn.stream(stmt)) - ] + return await self._search( + table=Jobs, + parameters=parameters, + search=search, + sorts=sorts, + distinct=distinct, + per_page=per_page, + page=page, + ) async def create_job(self, compressed_original_jdl: str): """Used to insert a new job with original JDL. Returns inserted job id.""" diff --git a/diracx-db/src/diracx/db/sql/utils/__init__.py b/diracx-db/src/diracx/db/sql/utils/__init__.py index 69b78b4bf..5cbb31b3f 100644 --- a/diracx-db/src/diracx/db/sql/utils/__init__.py +++ b/diracx-db/src/diracx/db/sql/utils/__init__.py @@ -3,6 +3,7 @@ from .base import ( BaseSQLDB, SQLDBUnavailableError, + _get_columns, apply_search_filters, apply_sort_constraints, ) @@ -10,6 +11,7 @@ from .types import Column, DateNowColumn, EnumBackedBool, EnumColumn, NullColumn __all__ = ( + "_get_columns", "utcnow", "Column", "NullColumn", diff --git a/diracx-db/src/diracx/db/sql/utils/base.py b/diracx-db/src/diracx/db/sql/utils/base.py index 0c5ddee5d..0cfda6612 100644 --- a/diracx-db/src/diracx/db/sql/utils/base.py +++ b/diracx-db/src/diracx/db/sql/utils/base.py @@ -8,16 +8,20 @@ from collections.abc import AsyncIterator from contextvars import ContextVar from datetime import datetime -from typing import Self, cast +from typing import Any, Self, cast from pydantic import TypeAdapter -from sqlalchemy import DateTime, MetaData, select +from sqlalchemy import DateTime, MetaData, func, select from sqlalchemy.exc import OperationalError from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine from diracx.core.exceptions import InvalidQueryError from diracx.core.extensions import select_from_extension -from diracx.core.models import SortDirection +from diracx.core.models import ( + SearchSpec, + SortDirection, + SortSpec, +) from diracx.core.settings import SqlalchemyDsn from diracx.db.exceptions import DBUnavailableError @@ -227,6 +231,71 @@ async def ping(self): except OperationalError as e: raise SQLDBUnavailableError("Cannot ping the DB") from e + async def _search( + self, + table: Any, + parameters: list[str] | None, + search: list[SearchSpec], + sorts: list[SortSpec], + *, + distinct: bool = False, + per_page: int = 100, + page: int | None = None, + ) -> tuple[int, list[dict[Any, Any]]]: + """Search for elements in a table.""" + # Find which columns to select + columns = _get_columns(table.__table__, parameters) + + stmt = select(*columns) + + stmt = apply_search_filters(table.__table__.columns.__getitem__, stmt, search) + stmt = apply_sort_constraints(table.__table__.columns.__getitem__, stmt, sorts) + + if distinct: + stmt = stmt.distinct() + + # Calculate total count before applying pagination + total_count_subquery = stmt.alias() + total_count_stmt = select(func.count()).select_from(total_count_subquery) + total = (await self.conn.execute(total_count_stmt)).scalar_one() + + # Apply pagination + if page is not None: + if page < 1: + raise InvalidQueryError("Page must be a positive integer") + if per_page < 1: + raise InvalidQueryError("Per page must be a positive integer") + stmt = stmt.offset((page - 1) * per_page).limit(per_page) + + # Execute the query + return total, [ + dict(row._mapping) async for row in (await self.conn.stream(stmt)) + ] + + async def _summary( + self, table: Any, group_by: list[str], search: list[SearchSpec] + ) -> list[dict[str, str | int]]: + """Get a summary of the elements of a table.""" + columns = _get_columns(table.__table__, group_by) + + pk_columns = list(table.__table__.primary_key.columns) + if not pk_columns: + raise ValueError( + "Model has no primary key and no count_column was provided." + ) + count_col = pk_columns[0] + + stmt = select(*columns, func.count(count_col).label("count")) + stmt = apply_search_filters(table.__table__.columns.__getitem__, stmt, search) + stmt = stmt.group_by(*columns) + + # Execute the query + return [ + dict(row._mapping) + async for row in (await self.conn.stream(stmt)) + if row.count > 0 # type: ignore + ] + def find_time_resolution(value): if isinstance(value, datetime): @@ -258,6 +327,17 @@ def find_time_resolution(value): raise InvalidQueryError(f"Cannot parse {value=}") +def _get_columns(table, parameters): + columns = [x for x in table.columns] + if parameters: + if unrecognised_parameters := set(parameters) - set(table.columns.keys()): + raise InvalidQueryError( + f"Unrecognised parameters requested {unrecognised_parameters}" + ) + columns = [c for c in columns if c.name in parameters] + return columns + + def apply_search_filters(column_mapping, stmt, search): for query in search: try: diff --git a/diracx-db/tests/test_dummy_db.py b/diracx-db/tests/test_dummy_db.py index e0106d833..f94eda5b7 100644 --- a/diracx-db/tests/test_dummy_db.py +++ b/diracx-db/tests/test_dummy_db.py @@ -129,7 +129,7 @@ async def test_failed_transaction(dummy_db): # The connection is created when the context manager is entered # This is our transaction - with pytest.raises(KeyError): + with pytest.raises(InvalidQueryError): async with dummy_db as dummy_db: assert dummy_db.conn diff --git a/diracx-logic/src/diracx/logic/jobs/query.py b/diracx-logic/src/diracx/logic/jobs/query.py index efb4b2fc5..7c02ba013 100644 --- a/diracx-logic/src/diracx/logic/jobs/query.py +++ b/diracx-logic/src/diracx/logic/jobs/query.py @@ -5,9 +5,9 @@ from diracx.core.config.schema import Config from diracx.core.models import ( - JobSearchParams, - JobSummaryParams, ScalarSearchOperator, + SearchParams, + SummaryParams, ) from diracx.db.os.job_parameters import JobParametersDB from diracx.db.sql.job.db import JobDB @@ -27,7 +27,7 @@ async def search( preferred_username: str | None, page: int = 1, per_page: int = 100, - body: JobSearchParams | None = None, + body: SearchParams | None = None, ) -> tuple[int, list[dict[str, Any]]]: """Retrieve information about jobs.""" # Apply a limit to per_page to prevent abuse of the API @@ -35,7 +35,7 @@ async def search( per_page = MAX_PER_PAGE if body is None: - body = JobSearchParams() + body = SearchParams() if query_logging_info := ("LoggingInfo" in (body.parameters or [])): if body.parameters: @@ -85,7 +85,7 @@ async def summary( config: Config, job_db: JobDB, preferred_username: str, - body: JobSummaryParams, + body: SummaryParams, ): """Show information suitable for plotting.""" if not config.Operations["Defaults"].Services.JobMonitoring.GlobalJobsInfo: diff --git a/diracx-logic/src/diracx/logic/jobs/status.py b/diracx-logic/src/diracx/logic/jobs/status.py index 82b670137..37df16545 100644 --- a/diracx-logic/src/diracx/logic/jobs/status.py +++ b/diracx-logic/src/diracx/logic/jobs/status.py @@ -41,11 +41,12 @@ VectorSearchSpec, ) from diracx.db.os.job_parameters import JobParametersDB -from diracx.db.sql.job.db import JobDB, _get_columns +from diracx.db.sql.job.db import JobDB from diracx.db.sql.job.schema import Jobs from diracx.db.sql.job_logging.db import JobLoggingDB from diracx.db.sql.sandbox_metadata.db import SandboxMetadataDB from diracx.db.sql.task_queue.db import TaskQueueDB +from diracx.db.sql.utils import _get_columns from diracx.db.sql.utils.functions import utcnow from diracx.logic.jobs.utils import check_and_prepare_job from diracx.logic.task_queues.priority import recalculate_tq_shares_for_entity @@ -625,7 +626,13 @@ async def _insert_parameters( # Get the VOs for the job IDs (required for the index template) job_vos = await job_db.summary( ["JobID", "VO"], - [{"parameter": "JobID", "operator": "in", "values": list(updates)}], + [ + { + "parameter": "JobID", + "operator": VectorSearchOperator.IN, + "values": list(updates), + } + ], ) job_id_to_vo = {int(x["JobID"]): str(x["VO"]) for x in job_vos} # Upsert the parameters into the JobParametersDB diff --git a/diracx-routers/src/diracx/routers/jobs/access_policies.py b/diracx-routers/src/diracx/routers/jobs/access_policies.py index 400b6cd04..695197755 100644 --- a/diracx-routers/src/diracx/routers/jobs/access_policies.py +++ b/diracx-routers/src/diracx/routers/jobs/access_policies.py @@ -6,6 +6,7 @@ from fastapi import Depends, HTTPException, status +from diracx.core.models import VectorSearchOperator from diracx.core.properties import GENERIC_PILOT, JOB_ADMINISTRATOR, NORMAL_USER from diracx.db.sql import JobDB, SandboxMetadataDB from diracx.routers.access_policies import BaseAccessPolicy @@ -88,7 +89,13 @@ async def policy( # to the current user job_owners = await job_db.summary( ["Owner", "VO"], - [{"parameter": "JobID", "operator": "in", "values": job_ids}], + [ + { + "parameter": "JobID", + "operator": VectorSearchOperator.IN, + "values": job_ids, + } + ], ) expected_owner = { diff --git a/diracx-routers/src/diracx/routers/jobs/query.py b/diracx-routers/src/diracx/routers/jobs/query.py index a8667b7dd..db270ca4d 100644 --- a/diracx-routers/src/diracx/routers/jobs/query.py +++ b/diracx-routers/src/diracx/routers/jobs/query.py @@ -6,8 +6,8 @@ from fastapi import Body, Depends, Response from diracx.core.models import ( - JobSearchParams, - JobSummaryParams, + SearchParams, + SummaryParams, ) from diracx.core.properties import JOB_ADMINISTRATOR from diracx.logic.jobs.query import search as search_bl @@ -135,7 +135,7 @@ async def search( page: int = 1, per_page: int = 100, body: Annotated[ - JobSearchParams | None, Body(openapi_examples=EXAMPLE_SEARCHES) + SearchParams | None, Body(openapi_examples=EXAMPLE_SEARCHES) ] = None, ) -> list[dict[str, Any]]: """Retrieve information about jobs. @@ -183,7 +183,7 @@ async def summary( config: Config, job_db: JobDB, user_info: Annotated[AuthorizedUserInfo, Depends(verify_dirac_access_token)], - body: JobSummaryParams, + body: SummaryParams, check_permissions: CheckWMSPolicyCallable, ): """Show information suitable for plotting.""" diff --git a/extensions/gubbins/gubbins-client/src/gubbins/client/_generated/aio/operations/_operations.py b/extensions/gubbins/gubbins-client/src/gubbins/client/_generated/aio/operations/_operations.py index 30d2e1c17..632d6e928 100644 --- a/extensions/gubbins/gubbins-client/src/gubbins/client/_generated/aio/operations/_operations.py +++ b/extensions/gubbins/gubbins-client/src/gubbins/client/_generated/aio/operations/_operations.py @@ -1829,7 +1829,7 @@ async def patch_metadata(self, body: Union[Dict[str, Dict[str, Any]], IO[bytes]] @overload async def search( self, - body: Optional[_models.JobSearchParams] = None, + body: Optional[_models.SearchParams] = None, *, page: int = 1, per_page: int = 100, @@ -1843,7 +1843,7 @@ async def search( **TODO: Add more docs**. :param body: Default value is None. - :type body: ~_generated.models.JobSearchParams + :type body: ~_generated.models.SearchParams :keyword page: Default value is 1. :paramtype page: int :keyword per_page: Default value is 100. @@ -1889,7 +1889,7 @@ async def search( @distributed_trace_async async def search( self, - body: Optional[Union[_models.JobSearchParams, IO[bytes]]] = None, + body: Optional[Union[_models.SearchParams, IO[bytes]]] = None, *, page: int = 1, per_page: int = 100, @@ -1901,8 +1901,8 @@ async def search( **TODO: Add more docs**. - :param body: Is either a JobSearchParams type or a IO[bytes] type. Default value is None. - :type body: ~_generated.models.JobSearchParams or IO[bytes] + :param body: Is either a SearchParams type or a IO[bytes] type. Default value is None. + :type body: ~_generated.models.SearchParams or IO[bytes] :keyword page: Default value is 1. :paramtype page: int :keyword per_page: Default value is 100. @@ -1932,7 +1932,7 @@ async def search( _content = body else: if body is not None: - _json = self._serialize.body(body, "JobSearchParams") + _json = self._serialize.body(body, "SearchParams") else: _json = None @@ -1971,14 +1971,14 @@ async def search( @overload async def summary( - self, body: _models.JobSummaryParams, *, content_type: str = "application/json", **kwargs: Any + self, body: _models.SummaryParams, *, content_type: str = "application/json", **kwargs: Any ) -> Any: """Summary. Show information suitable for plotting. :param body: Required. - :type body: ~_generated.models.JobSummaryParams + :type body: ~_generated.models.SummaryParams :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str @@ -2004,13 +2004,13 @@ async def summary(self, body: IO[bytes], *, content_type: str = "application/jso """ @distributed_trace_async - async def summary(self, body: Union[_models.JobSummaryParams, IO[bytes]], **kwargs: Any) -> Any: + async def summary(self, body: Union[_models.SummaryParams, IO[bytes]], **kwargs: Any) -> Any: """Summary. Show information suitable for plotting. - :param body: Is either a JobSummaryParams type or a IO[bytes] type. Required. - :type body: ~_generated.models.JobSummaryParams or IO[bytes] + :param body: Is either a SummaryParams type or a IO[bytes] type. Required. + :type body: ~_generated.models.SummaryParams or IO[bytes] :return: any :rtype: any :raises ~azure.core.exceptions.HttpResponseError: @@ -2035,7 +2035,7 @@ async def summary(self, body: Union[_models.JobSummaryParams, IO[bytes]], **kwar if isinstance(body, (IOBase, bytes)): _content = body else: - _json = self._serialize.body(body, "JobSummaryParams") + _json = self._serialize.body(body, "SummaryParams") _request = build_jobs_summary_request( content_type=content_type, diff --git a/extensions/gubbins/gubbins-client/src/gubbins/client/_generated/models/__init__.py b/extensions/gubbins/gubbins-client/src/gubbins/client/_generated/models/__init__.py index 2c1fc99e9..1bbb2ad8e 100644 --- a/extensions/gubbins/gubbins-client/src/gubbins/client/_generated/models/__init__.py +++ b/extensions/gubbins/gubbins-client/src/gubbins/client/_generated/models/__init__.py @@ -21,20 +21,20 @@ InitiateDeviceFlowResponse, InsertedJob, JobCommand, - JobSearchParams, - JobSearchParamsSearchItem, JobStatusUpdate, - JobSummaryParams, - JobSummaryParamsSearchItem, OpenIDConfiguration, SandboxDownloadResponse, SandboxInfo, SandboxUploadResponse, ScalarSearchSpec, ScalarSearchSpecValue, + SearchParams, + SearchParamsSearchItem, SetJobStatusReturn, SetJobStatusReturnSuccess, SortSpec, + SummaryParams, + SummaryParamsSearchItem, SupportInfo, TokenResponse, UserInfoResponse, @@ -68,20 +68,20 @@ "InitiateDeviceFlowResponse", "InsertedJob", "JobCommand", - "JobSearchParams", - "JobSearchParamsSearchItem", "JobStatusUpdate", - "JobSummaryParams", - "JobSummaryParamsSearchItem", "OpenIDConfiguration", "SandboxDownloadResponse", "SandboxInfo", "SandboxUploadResponse", "ScalarSearchSpec", "ScalarSearchSpecValue", + "SearchParams", + "SearchParamsSearchItem", "SetJobStatusReturn", "SetJobStatusReturnSuccess", "SortSpec", + "SummaryParams", + "SummaryParamsSearchItem", "SupportInfo", "TokenResponse", "UserInfoResponse", diff --git a/extensions/gubbins/gubbins-client/src/gubbins/client/_generated/models/_models.py b/extensions/gubbins/gubbins-client/src/gubbins/client/_generated/models/_models.py index 7d37c2433..4e947a438 100644 --- a/extensions/gubbins/gubbins-client/src/gubbins/client/_generated/models/_models.py +++ b/extensions/gubbins/gubbins-client/src/gubbins/client/_generated/models/_models.py @@ -405,56 +405,6 @@ def __init__(self, *, job_id: int, command: str, arguments: Optional[str] = None self.arguments = arguments -class JobSearchParams(_serialization.Model): - """JobSearchParams. - - :ivar parameters: Parameters. - :vartype parameters: list[str] - :ivar search: Search. - :vartype search: list[~_generated.models.JobSearchParamsSearchItem] - :ivar sort: Sort. - :vartype sort: list[~_generated.models.SortSpec] - :ivar distinct: Distinct. - :vartype distinct: bool - """ - - _attribute_map = { - "parameters": {"key": "parameters", "type": "[str]"}, - "search": {"key": "search", "type": "[JobSearchParamsSearchItem]"}, - "sort": {"key": "sort", "type": "[SortSpec]"}, - "distinct": {"key": "distinct", "type": "bool"}, - } - - def __init__( - self, - *, - parameters: Optional[List[str]] = None, - search: List["_models.JobSearchParamsSearchItem"] = [], - sort: List["_models.SortSpec"] = [], - distinct: bool = False, - **kwargs: Any - ) -> None: - """ - :keyword parameters: Parameters. - :paramtype parameters: list[str] - :keyword search: Search. - :paramtype search: list[~_generated.models.JobSearchParamsSearchItem] - :keyword sort: Sort. - :paramtype sort: list[~_generated.models.SortSpec] - :keyword distinct: Distinct. - :paramtype distinct: bool - """ - super().__init__(**kwargs) - self.parameters = parameters - self.search = search - self.sort = sort - self.distinct = distinct - - -class JobSearchParamsSearchItem(_serialization.Model): - """JobSearchParamsSearchItem.""" - - class JobStatusUpdate(_serialization.Model): """JobStatusUpdate. @@ -505,44 +455,6 @@ def __init__( self.source = source -class JobSummaryParams(_serialization.Model): - """JobSummaryParams. - - All required parameters must be populated in order to send to server. - - :ivar grouping: Grouping. Required. - :vartype grouping: list[str] - :ivar search: Search. - :vartype search: list[~_generated.models.JobSummaryParamsSearchItem] - """ - - _validation = { - "grouping": {"required": True}, - } - - _attribute_map = { - "grouping": {"key": "grouping", "type": "[str]"}, - "search": {"key": "search", "type": "[JobSummaryParamsSearchItem]"}, - } - - def __init__( - self, *, grouping: List[str], search: List["_models.JobSummaryParamsSearchItem"] = [], **kwargs: Any - ) -> None: - """ - :keyword grouping: Grouping. Required. - :paramtype grouping: list[str] - :keyword search: Search. - :paramtype search: list[~_generated.models.JobSummaryParamsSearchItem] - """ - super().__init__(**kwargs) - self.grouping = grouping - self.search = search - - -class JobSummaryParamsSearchItem(_serialization.Model): - """JobSummaryParamsSearchItem.""" - - class OpenIDConfiguration(_serialization.Model): """OpenIDConfiguration. @@ -857,6 +769,56 @@ class ScalarSearchSpecValue(_serialization.Model): """Value.""" +class SearchParams(_serialization.Model): + """SearchParams. + + :ivar parameters: Parameters. + :vartype parameters: list[str] + :ivar search: Search. + :vartype search: list[~_generated.models.SearchParamsSearchItem] + :ivar sort: Sort. + :vartype sort: list[~_generated.models.SortSpec] + :ivar distinct: Distinct. + :vartype distinct: bool + """ + + _attribute_map = { + "parameters": {"key": "parameters", "type": "[str]"}, + "search": {"key": "search", "type": "[SearchParamsSearchItem]"}, + "sort": {"key": "sort", "type": "[SortSpec]"}, + "distinct": {"key": "distinct", "type": "bool"}, + } + + def __init__( + self, + *, + parameters: Optional[List[str]] = None, + search: List["_models.SearchParamsSearchItem"] = [], + sort: List["_models.SortSpec"] = [], + distinct: bool = False, + **kwargs: Any + ) -> None: + """ + :keyword parameters: Parameters. + :paramtype parameters: list[str] + :keyword search: Search. + :paramtype search: list[~_generated.models.SearchParamsSearchItem] + :keyword sort: Sort. + :paramtype sort: list[~_generated.models.SortSpec] + :keyword distinct: Distinct. + :paramtype distinct: bool + """ + super().__init__(**kwargs) + self.parameters = parameters + self.search = search + self.sort = sort + self.distinct = distinct + + +class SearchParamsSearchItem(_serialization.Model): + """SearchParamsSearchItem.""" + + class SetJobStatusReturn(_serialization.Model): """SetJobStatusReturn. @@ -1000,6 +962,44 @@ def __init__(self, *, parameter: str, direction: Union[str, "_models.SortDirecti self.direction = direction +class SummaryParams(_serialization.Model): + """SummaryParams. + + All required parameters must be populated in order to send to server. + + :ivar grouping: Grouping. Required. + :vartype grouping: list[str] + :ivar search: Search. + :vartype search: list[~_generated.models.SummaryParamsSearchItem] + """ + + _validation = { + "grouping": {"required": True}, + } + + _attribute_map = { + "grouping": {"key": "grouping", "type": "[str]"}, + "search": {"key": "search", "type": "[SummaryParamsSearchItem]"}, + } + + def __init__( + self, *, grouping: List[str], search: List["_models.SummaryParamsSearchItem"] = [], **kwargs: Any + ) -> None: + """ + :keyword grouping: Grouping. Required. + :paramtype grouping: list[str] + :keyword search: Search. + :paramtype search: list[~_generated.models.SummaryParamsSearchItem] + """ + super().__init__(**kwargs) + self.grouping = grouping + self.search = search + + +class SummaryParamsSearchItem(_serialization.Model): + """SummaryParamsSearchItem.""" + + class SupportInfo(_serialization.Model): """SupportInfo. diff --git a/extensions/gubbins/gubbins-client/src/gubbins/client/_generated/operations/_operations.py b/extensions/gubbins/gubbins-client/src/gubbins/client/_generated/operations/_operations.py index 4e429a056..0ce55f070 100644 --- a/extensions/gubbins/gubbins-client/src/gubbins/client/_generated/operations/_operations.py +++ b/extensions/gubbins/gubbins-client/src/gubbins/client/_generated/operations/_operations.py @@ -2400,7 +2400,7 @@ def patch_metadata( # pylint: disable=inconsistent-return-statements @overload def search( self, - body: Optional[_models.JobSearchParams] = None, + body: Optional[_models.SearchParams] = None, *, page: int = 1, per_page: int = 100, @@ -2414,7 +2414,7 @@ def search( **TODO: Add more docs**. :param body: Default value is None. - :type body: ~_generated.models.JobSearchParams + :type body: ~_generated.models.SearchParams :keyword page: Default value is 1. :paramtype page: int :keyword per_page: Default value is 100. @@ -2460,7 +2460,7 @@ def search( @distributed_trace def search( self, - body: Optional[Union[_models.JobSearchParams, IO[bytes]]] = None, + body: Optional[Union[_models.SearchParams, IO[bytes]]] = None, *, page: int = 1, per_page: int = 100, @@ -2472,8 +2472,8 @@ def search( **TODO: Add more docs**. - :param body: Is either a JobSearchParams type or a IO[bytes] type. Default value is None. - :type body: ~_generated.models.JobSearchParams or IO[bytes] + :param body: Is either a SearchParams type or a IO[bytes] type. Default value is None. + :type body: ~_generated.models.SearchParams or IO[bytes] :keyword page: Default value is 1. :paramtype page: int :keyword per_page: Default value is 100. @@ -2503,7 +2503,7 @@ def search( _content = body else: if body is not None: - _json = self._serialize.body(body, "JobSearchParams") + _json = self._serialize.body(body, "SearchParams") else: _json = None @@ -2541,13 +2541,13 @@ def search( return deserialized # type: ignore @overload - def summary(self, body: _models.JobSummaryParams, *, content_type: str = "application/json", **kwargs: Any) -> Any: + def summary(self, body: _models.SummaryParams, *, content_type: str = "application/json", **kwargs: Any) -> Any: """Summary. Show information suitable for plotting. :param body: Required. - :type body: ~_generated.models.JobSummaryParams + :type body: ~_generated.models.SummaryParams :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. Default value is "application/json". :paramtype content_type: str @@ -2573,13 +2573,13 @@ def summary(self, body: IO[bytes], *, content_type: str = "application/json", ** """ @distributed_trace - def summary(self, body: Union[_models.JobSummaryParams, IO[bytes]], **kwargs: Any) -> Any: + def summary(self, body: Union[_models.SummaryParams, IO[bytes]], **kwargs: Any) -> Any: """Summary. Show information suitable for plotting. - :param body: Is either a JobSummaryParams type or a IO[bytes] type. Required. - :type body: ~_generated.models.JobSummaryParams or IO[bytes] + :param body: Is either a SummaryParams type or a IO[bytes] type. Required. + :type body: ~_generated.models.SummaryParams or IO[bytes] :return: any :rtype: any :raises ~azure.core.exceptions.HttpResponseError: @@ -2604,7 +2604,7 @@ def summary(self, body: Union[_models.JobSummaryParams, IO[bytes]], **kwargs: An if isinstance(body, (IOBase, bytes)): _content = body else: - _json = self._serialize.body(body, "JobSummaryParams") + _json = self._serialize.body(body, "SummaryParams") _request = build_jobs_summary_request( content_type=content_type, diff --git a/extensions/gubbins/gubbins-db/src/gubbins/db/sql/lollygag/db.py b/extensions/gubbins/gubbins-db/src/gubbins/db/sql/lollygag/db.py index 467577394..2964a5a38 100644 --- a/extensions/gubbins/gubbins-db/src/gubbins/db/sql/lollygag/db.py +++ b/extensions/gubbins/gubbins-db/src/gubbins/db/sql/lollygag/db.py @@ -1,7 +1,7 @@ from __future__ import annotations -from diracx.db.sql.utils import BaseSQLDB, apply_search_filters -from sqlalchemy import func, insert, select +from diracx.db.sql.utils import BaseSQLDB +from sqlalchemy import insert, select from uuid_utils import UUID from .schema import Base as LollygagDBBase @@ -22,18 +22,7 @@ class LollygagDB(BaseSQLDB): metadata = LollygagDBBase.metadata async def summary(self, group_by, search) -> list[dict[str, str | int]]: - columns = [Cars.__table__.columns[x] for x in group_by] - - stmt = select(*columns, func.count(Cars.license_plate).label("count")) - stmt = apply_search_filters(Cars.__table__.columns.__getitem__, stmt, search) - stmt = stmt.group_by(*columns) - - # Execute the query - return [ - dict(row._mapping) - async for row in (await self.conn.stream(stmt)) - if row.count > 0 # type: ignore - ] + return await self._summary(Cars, group_by, search) async def insert_owner(self, name: str) -> int: stmt = insert(Owners).values(name=name) From 380da6263254452428430c8244eb59b17a0edd67 Mon Sep 17 00:00:00 2001 From: Robin VAN DE MERGHEL Date: Tue, 5 Aug 2025 09:13:24 +0200 Subject: [PATCH 2/2] fix: Change from Any,Any to str,Any as search returns columns to value mapping --- diracx-db/src/diracx/db/sql/utils/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diracx-db/src/diracx/db/sql/utils/base.py b/diracx-db/src/diracx/db/sql/utils/base.py index 0cfda6612..9b349e14e 100644 --- a/diracx-db/src/diracx/db/sql/utils/base.py +++ b/diracx-db/src/diracx/db/sql/utils/base.py @@ -241,7 +241,7 @@ async def _search( distinct: bool = False, per_page: int = 100, page: int | None = None, - ) -> tuple[int, list[dict[Any, Any]]]: + ) -> tuple[int, list[dict[str, Any]]]: """Search for elements in a table.""" # Find which columns to select columns = _get_columns(table.__table__, parameters)