From 1b6423c079c92684789683840f855c12b2582793 Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 27 Sep 2025 04:08:32 +0200 Subject: [PATCH] [WIP] collection #181 --- example/api/src/queries/hello.py | 35 +++++++++++ src/cpl-graphql/cpl/graphql/schema/field.py | 4 +- .../cpl/graphql/schema/filter/__init__.py | 0 .../cpl/graphql/schema/filter/filter.py | 26 ++++++++ src/cpl-graphql/cpl/graphql/schema/query.py | 16 ++++- .../cpl/graphql/schema/sort/__init__.py | 0 .../cpl/graphql/schema/sort/sort.py | 25 ++++++++ .../cpl/graphql/schema/sort/sort_order.py | 6 ++ src/cpl-graphql/cpl/graphql/service/schema.py | 5 +- .../cpl/graphql/utils/type_converter.py | 60 ++++++++++--------- 10 files changed, 144 insertions(+), 33 deletions(-) create mode 100644 src/cpl-graphql/cpl/graphql/schema/filter/__init__.py create mode 100644 src/cpl-graphql/cpl/graphql/schema/filter/filter.py create mode 100644 src/cpl-graphql/cpl/graphql/schema/sort/__init__.py create mode 100644 src/cpl-graphql/cpl/graphql/schema/sort/sort.py create mode 100644 src/cpl-graphql/cpl/graphql/schema/sort/sort_order.py diff --git a/example/api/src/queries/hello.py b/example/api/src/queries/hello.py index 58c747b1..14c8fd4e 100644 --- a/example/api/src/queries/hello.py +++ b/example/api/src/queries/hello.py @@ -1,6 +1,29 @@ from cpl.api.middleware.request import get_request +from cpl.graphql.schema.filter.filter import Filter from cpl.graphql.schema.query import Query +from cpl.graphql.schema.sort.sort import Sort + + +class User: + def __init__(self, id: int, name: str): + self.id = id + self.name = name + + +class UserFilter(Filter[User]): + def __init__(self, obj: dict): + Filter.__init__(self, obj) + self.field("id", int) + self.field("name", str) + + +class UserSort(Sort[User]): + def __init__(self, obj: dict): + Sort.__init__(self, obj) + self.field("id") + self.field("name") + class HelloQuery(Query): def __init__(self): @@ -9,3 +32,15 @@ class HelloQuery(Query): "message", resolver=lambda *_, name: f"Hello {name} {get_request().state.request_id}", ).with_argument(str, "name", "Name to greet", "world") + + self.collection_field( + int, + "items", + UserFilter, + UserSort, + resolver=self._resolve_collection, + ) + + @staticmethod + def _resolve_collection(filter, sort, skip, take): + return [] diff --git a/src/cpl-graphql/cpl/graphql/schema/field.py b/src/cpl-graphql/cpl/graphql/schema/field.py index 2744f6d2..a6b16baf 100644 --- a/src/cpl-graphql/cpl/graphql/schema/field.py +++ b/src/cpl-graphql/cpl/graphql/schema/field.py @@ -37,7 +37,7 @@ class Field: def with_argument(self, arg_type: type, name: str, description: str = None, default_value=None) -> Self: if name in self._args: raise ValueError(f"Argument with name '{name}' already exists in field '{self._name}'") - self._args[name] = Argument(name, arg_type, description, default_value) + self._args[name] = Argument(arg_type, name, description, default_value) return self def with_arguments(self, args: list[Argument]) -> Self: @@ -45,5 +45,5 @@ class Field: if not isinstance(arg, Argument): raise ValueError(f"Expected Argument instance, got {type(arg)}") - self.with_argument(arg.name, arg.type, arg.description, arg.default_value) + self.with_argument(arg.type, arg.name, arg.description, arg.default_value) return self diff --git a/src/cpl-graphql/cpl/graphql/schema/filter/__init__.py b/src/cpl-graphql/cpl/graphql/schema/filter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/cpl-graphql/cpl/graphql/schema/filter/filter.py b/src/cpl-graphql/cpl/graphql/schema/filter/filter.py new file mode 100644 index 00000000..b4591777 --- /dev/null +++ b/src/cpl-graphql/cpl/graphql/schema/filter/filter.py @@ -0,0 +1,26 @@ +from abc import ABC +from datetime import datetime +from typing import Type + + +class Filter[T](ABC): + def __init__( + self, + obj: dict | None, + ): + ABC.__init__(self) + self._obj = obj + + self._values = {} + + def field( + self, + field: str, + filter_type: Type["Filter"] | Type[int | str | bool | datetime | list], + ): + if field not in self._obj: + return + + # if issubclass + + self._values[field] = filter_type(self._obj[field]) diff --git a/src/cpl-graphql/cpl/graphql/schema/query.py b/src/cpl-graphql/cpl/graphql/schema/query.py index ba93c3a1..d4e417d5 100644 --- a/src/cpl-graphql/cpl/graphql/schema/query.py +++ b/src/cpl-graphql/cpl/graphql/schema/query.py @@ -2,9 +2,9 @@ from typing import Callable, Type from graphene import ObjectType +from cpl.graphql.schema.argument import Argument from cpl.graphql.schema.field import Field from cpl.graphql.typing import Resolver -from cpl.graphql.utils.type_converter import TypeConverter class Query(ObjectType): @@ -32,7 +32,7 @@ class Query(ObjectType): def with_query(self, name: str, subquery: Type["Query"]): from cpl.graphql.schema.field import Field - f = Field(name=name, gql_type=object, resolver=lambda root, info, **kwargs: {}, subquery=subquery) + f = Field(name=name, gql_type=subquery, resolver=lambda root, info, **kwargs: {}, subquery=subquery) self._fields[name] = f return self._fields[name] @@ -47,3 +47,15 @@ class Query(ObjectType): def bool_field(self, name: str, resolver: Resolver = None) -> "Field": return self.field(name, bool, resolver) + + def collection_field( + self, t: type, name: str, filter_type: type, sort_type: type, resolver: Resolver = None + ) -> "Field": + return self.field(name, list[t], resolver).with_arguments( + [ + Argument(filter_type, "filter"), + Argument(sort_type, "sort"), + Argument(int, "skip", default_value=0), + Argument(int, "take", default_value=10), + ] + ) diff --git a/src/cpl-graphql/cpl/graphql/schema/sort/__init__.py b/src/cpl-graphql/cpl/graphql/schema/sort/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/cpl-graphql/cpl/graphql/schema/sort/sort.py b/src/cpl-graphql/cpl/graphql/schema/sort/sort.py new file mode 100644 index 00000000..72ac5354 --- /dev/null +++ b/src/cpl-graphql/cpl/graphql/schema/sort/sort.py @@ -0,0 +1,25 @@ +from abc import ABC + +from cpl.graphql.schema.sort.sort_order import SortOrder + + +class Sort[T](ABC): + def __init__( + self, + obj: dict | None, + ): + ABC.__init__(self) + self._obj = obj + + self._values = {} + + def field( + self, + field: str, + ): + if field not in self._obj: + return + + # if issubclass + + self._values[field] = SortOrder.DESC diff --git a/src/cpl-graphql/cpl/graphql/schema/sort/sort_order.py b/src/cpl-graphql/cpl/graphql/schema/sort/sort_order.py new file mode 100644 index 00000000..cc3122a4 --- /dev/null +++ b/src/cpl-graphql/cpl/graphql/schema/sort/sort_order.py @@ -0,0 +1,6 @@ +from enum import Enum, auto + + +class SortOrder(Enum): + ASC = auto() + DESC = auto() \ No newline at end of file diff --git a/src/cpl-graphql/cpl/graphql/service/schema.py b/src/cpl-graphql/cpl/graphql/service/schema.py index 69e76e48..ba49b972 100644 --- a/src/cpl-graphql/cpl/graphql/service/schema.py +++ b/src/cpl-graphql/cpl/graphql/service/schema.py @@ -1,3 +1,4 @@ +from inspect import isclass from typing import Type import graphene @@ -41,7 +42,7 @@ class Schema: arguments = {} if args is not None: arguments = { - arg.name: graphene.Argument(TypeConverter.to_graphene(arg.type), description=arg.description, default_value=arg.default_value) + arg.name: graphene.Argument(TypeConverter.to_graphene(arg.type), name=arg.name, description=arg.description, default_value=arg.default_value) for arg in args.values() } @@ -52,7 +53,7 @@ class Schema: attrs = {} for field in query.get_fields().values(): - if field.type == object and field.subquery is not None: + if isclass(field.type) and issubclass(field.type, Query) and field.subquery is not None: subquery = self._provider.get_service(field.subquery) sub = self.to_graphene(subquery, name=field.name.capitalize()) attrs[field.name] = self._field_to_graphene(sub, field.args, field.resolver) diff --git a/src/cpl-graphql/cpl/graphql/utils/type_converter.py b/src/cpl-graphql/cpl/graphql/utils/type_converter.py index c89b8928..54460b20 100644 --- a/src/cpl-graphql/cpl/graphql/utils/type_converter.py +++ b/src/cpl-graphql/cpl/graphql/utils/type_converter.py @@ -1,38 +1,44 @@ -from typing import Type - +import typing import graphene +from typing import Any, get_origin, get_args -from cpl.graphql.typing import ScalarType +from cpl.graphql.schema.filter.filter import Filter +from cpl.graphql.schema.sort.sort import Sort class TypeConverter: + scalar_map: dict[Any, type[graphene.Scalar]] = { + str: graphene.String, + int: graphene.Int, + float: graphene.Float, + bool: graphene.Boolean, + } - @staticmethod - def from_graphene(t: Type[graphene.Scalar]) -> ScalarType: - graphene_type_map: dict[Type[graphene.Scalar], ScalarType] = { - graphene.String: str, - graphene.Int: int, - graphene.Float: float, - graphene.Boolean: bool, - graphene.ObjectType: object, - } + @classmethod + def to_graphene(cls, t: Any) -> Any: + origin = get_origin(t) + args = get_args(t) - if t not in graphene_type_map: - raise ValueError(f"Unsupported field type: {t}") + if t in cls.scalar_map: + return cls.scalar_map[t] - return graphene_type_map[t] + if origin in (list, typing.List): + if not args: + raise ValueError("List must specify element type, e.g. list[str]") + inner = cls.to_graphene(args[0]) + return graphene.List(inner) - @staticmethod - def to_graphene(t: ScalarType) -> Type[graphene.Scalar]: - type_graphene_map: dict[ScalarType, Type[graphene.Scalar]] = { - str: graphene.String, - int: graphene.Int, - float: graphene.Float, - bool: graphene.Boolean, - object: graphene.ObjectType, - } + if t is list or t is typing.List: + raise ValueError("List must be parametrized: list[str], list[int], list[UserQuery]") - if t not in type_graphene_map: - raise ValueError(f"Unsupported field type: {t}") + from cpl.graphql.schema.query import Query + if isinstance(t, type) and issubclass(t, Query): + return t - return type_graphene_map[t] + if isinstance(t, type) and issubclass(t, Filter): + return t + + if isinstance(t, type) and issubclass(t, Sort): + return t + + raise ValueError(f"Unsupported field type: {t}")