bootgraph 1.9.0.dev24129__tar.gz → 1.11.0.dev24840__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bootgraph-1.9.0.dev24129 → bootgraph-1.11.0.dev24840}/PKG-INFO +2 -2
- {bootgraph-1.9.0.dev24129 → bootgraph-1.11.0.dev24840}/bootgraph/models.py +2 -0
- {bootgraph-1.9.0.dev24129 → bootgraph-1.11.0.dev24840}/bootgraph/router.py +19 -7
- {bootgraph-1.9.0.dev24129 → bootgraph-1.11.0.dev24840}/bootgraph/schemas/generators.py +71 -30
- {bootgraph-1.9.0.dev24129 → bootgraph-1.11.0.dev24840}/pyproject.toml +3 -1
- {bootgraph-1.9.0.dev24129 → bootgraph-1.11.0.dev24840}/LICENSE +0 -0
- {bootgraph-1.9.0.dev24129 → bootgraph-1.11.0.dev24840}/README.md +0 -0
- {bootgraph-1.9.0.dev24129 → bootgraph-1.11.0.dev24840}/bootgraph/__init__.py +0 -0
- {bootgraph-1.9.0.dev24129 → bootgraph-1.11.0.dev24840}/bootgraph/database/__init__.py +0 -0
- {bootgraph-1.9.0.dev24129 → bootgraph-1.11.0.dev24840}/bootgraph/database/operations.py +0 -0
- {bootgraph-1.9.0.dev24129 → bootgraph-1.11.0.dev24840}/bootgraph/database/utils.py +0 -0
- {bootgraph-1.9.0.dev24129 → bootgraph-1.11.0.dev24840}/bootgraph/dl.py +0 -0
- {bootgraph-1.9.0.dev24129 → bootgraph-1.11.0.dev24840}/bootgraph/schemas/__init__.py +0 -0
- {bootgraph-1.9.0.dev24129 → bootgraph-1.11.0.dev24840}/bootgraph/schemas/models.py +0 -0
- {bootgraph-1.9.0.dev24129 → bootgraph-1.11.0.dev24840}/bootgraph/setup.py +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: bootgraph
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.11.0.dev24840
|
|
4
4
|
Summary: A Python library for integrating SQLModel and Strawberry, providing a seamless GraphQL integration with FastAPI and advanced features for database interactions.
|
|
5
|
-
Home-page: https://github.com/MDoreto/graphemy
|
|
6
5
|
License: MIT
|
|
7
6
|
Author: Matheus Doreto
|
|
8
7
|
Author-email: matheusdoreto.md@gmail.com
|
|
@@ -23,6 +22,7 @@ Requires-Dist: sqlmodel (>=0.0.22,<0.0.23)
|
|
|
23
22
|
Requires-Dist: strawberry-graphql[debug-server] (>=0.240.0,<0.241.0)
|
|
24
23
|
Project-URL: Bug Tracker, https://github.com/MDoreto/graphemy/issues
|
|
25
24
|
Project-URL: Documentation, https://graphemy.readthedocs.io/en/latest/
|
|
25
|
+
Project-URL: Homepage, https://github.com/MDoreto/graphemy
|
|
26
26
|
Project-URL: Repository, https://github.com/MDoreto/graphemy
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
|
|
@@ -44,6 +44,7 @@ class Graphemy(SQLModel):
|
|
|
44
44
|
specific mutation names to their respective permission classes. This allows
|
|
45
45
|
for fine-grained control over which permissions apply to each mutation. If a
|
|
46
46
|
mutation is not listed, it defaults to having no specific permissions.
|
|
47
|
+
__default_order_by__ (str): Order by for database queries
|
|
47
48
|
Classes:
|
|
48
49
|
Graphemy: An extended SQLModel that incorporates GraphQL functionalities by using
|
|
49
50
|
Strawberry GraphQL. This class allows defining GraphQL schemas, query names,
|
|
@@ -66,6 +67,7 @@ class Graphemy(SQLModel):
|
|
|
66
67
|
__enable_query__: bool | None = None
|
|
67
68
|
__queryname__: str = ""
|
|
68
69
|
__enginename__: str = "default"
|
|
70
|
+
__default_order_by__: str = "id"
|
|
69
71
|
__filter_attributes__: Optional[dict[str, dict[str, Any]]] = None
|
|
70
72
|
__custom_resolvers__: Optional[list[Callable]] = None
|
|
71
73
|
__custom_mutations__: Optional[list[Callable]] = None
|
|
@@ -11,6 +11,7 @@ from fastapi import Request, Response
|
|
|
11
11
|
from graphql.error import GraphQLError
|
|
12
12
|
from graphql.error.graphql_error import format_error as format_graphql_error
|
|
13
13
|
from sqlalchemy.engine.base import Engine
|
|
14
|
+
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
14
15
|
from strawberry.fastapi import GraphQLRouter
|
|
15
16
|
from strawberry.http import GraphQLHTTPResponse
|
|
16
17
|
from strawberry.types import ExecutionResult
|
|
@@ -112,7 +113,7 @@ class GraphemyRouter(GraphQLRouter):
|
|
|
112
113
|
permission_getter: Callable | None = None,
|
|
113
114
|
dl_filter: Callable | None = None,
|
|
114
115
|
query_filter: Callable | None = None,
|
|
115
|
-
engine: Engine | Dict[str, Engine] = None,
|
|
116
|
+
engine: AsyncEngine | Engine | Dict[str, Engine] = None,
|
|
116
117
|
extensions: list = None,
|
|
117
118
|
enable_queries: bool = True,
|
|
118
119
|
enable_put_mutations: bool = False,
|
|
@@ -169,11 +170,16 @@ class GraphemyRouter(GraphQLRouter):
|
|
|
169
170
|
# model_mutation_fields = {}
|
|
170
171
|
if cls_mutations:
|
|
171
172
|
# Create a resolver that returns an instance of ModelMutations
|
|
172
|
-
def
|
|
173
|
-
|
|
173
|
+
def make_mutation_resolver(mutations):
|
|
174
|
+
def resolver() -> mutations:
|
|
175
|
+
return mutations()
|
|
176
|
+
|
|
177
|
+
return resolver
|
|
178
|
+
|
|
179
|
+
resolver = make_mutation_resolver(cls_mutations)
|
|
174
180
|
|
|
175
181
|
# Create a Strawberry field for this query
|
|
176
|
-
mutation_field = strawberry.field(description=f"Mutations for {cls.__queryname__}")(
|
|
182
|
+
mutation_field = strawberry.field(description=f"Mutations for {cls.__queryname__}")(resolver)
|
|
177
183
|
|
|
178
184
|
need_mutation = False
|
|
179
185
|
setattr(mutation, cls.__queryname__, mutation_field)
|
|
@@ -185,9 +191,15 @@ class GraphemyRouter(GraphQLRouter):
|
|
|
185
191
|
)
|
|
186
192
|
|
|
187
193
|
async def get_context(request: Request, response: Response) -> dict:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
194
|
+
if type(engine) == AsyncEngine:
|
|
195
|
+
context = {"engine": engine}
|
|
196
|
+
if context_session:
|
|
197
|
+
session_generator = context_session()
|
|
198
|
+
context = {"session": await anext(session_generator)}
|
|
199
|
+
else:
|
|
200
|
+
context = {"engine": engine}
|
|
201
|
+
if context_session:
|
|
202
|
+
context = {"session": context_session(engine)}
|
|
191
203
|
if context_getter:
|
|
192
204
|
context = await context_getter(request, response, context)
|
|
193
205
|
for k, (func, return_class) in functions.items():
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from enum import Enum
|
|
1
2
|
from typing import (
|
|
2
3
|
TYPE_CHECKING,
|
|
3
4
|
Annotated,
|
|
@@ -11,6 +12,9 @@ from typing import (
|
|
|
11
12
|
Type
|
|
12
13
|
)
|
|
13
14
|
|
|
15
|
+
from sqlalchemy.dialects.postgresql import array
|
|
16
|
+
from sqlalchemy.orm import sessionmaker
|
|
17
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
14
18
|
from sqlalchemy.orm import Mapped
|
|
15
19
|
from sqlalchemy import ForeignKeyConstraint, func
|
|
16
20
|
from sqlalchemy.inspection import inspect
|
|
@@ -31,6 +35,7 @@ class GraphemyFilterMode:
|
|
|
31
35
|
STARTSWITH = "STARTSWITH"
|
|
32
36
|
ENDSWITH = "ENDSWITH"
|
|
33
37
|
IN = "IN"
|
|
38
|
+
IN_STRING_ARRAY = "IN_STRING_ARRAY"
|
|
34
39
|
|
|
35
40
|
|
|
36
41
|
def set_schema(
|
|
@@ -227,7 +232,7 @@ def get_many_relation_function(
|
|
|
227
232
|
|
|
228
233
|
# Determine ordering
|
|
229
234
|
# Assume `orderBy` is the name of a column in `cls`. If not provided, default to primary key.
|
|
230
|
-
order_column = getattr(cls, orderBy) if orderBy else getattr(cls,
|
|
235
|
+
order_column = getattr(cls, orderBy) if orderBy else getattr(cls, cls.__default_order_by__)
|
|
231
236
|
stmt = stmt.order_by(order_column.asc())
|
|
232
237
|
|
|
233
238
|
# Handle pagination cursors: decode them and apply filters
|
|
@@ -564,14 +569,17 @@ def create_filter_input(cls: Type[SQLModel], name_suffix="Filter"):
|
|
|
564
569
|
class FilterInput:
|
|
565
570
|
pass
|
|
566
571
|
|
|
572
|
+
internal_type_filters = set()
|
|
567
573
|
for field_name, field_type in cls.__annotations__.items():
|
|
568
574
|
if field_name in declared_filters:
|
|
569
|
-
|
|
570
|
-
graphql_type = field_type
|
|
571
|
-
else:
|
|
572
|
-
graphql_type = Optional[field_type]
|
|
575
|
+
internal_type_filters.add(field_name)
|
|
573
576
|
if declared_filters[field_name].get("is_list"):
|
|
574
|
-
graphql_type = List[
|
|
577
|
+
graphql_type = List[field_type]
|
|
578
|
+
else:
|
|
579
|
+
graphql_type = field_type
|
|
580
|
+
|
|
581
|
+
if not declared_filters[field_name].get("required"):
|
|
582
|
+
graphql_type = Optional[graphql_type]
|
|
575
583
|
setattr(
|
|
576
584
|
FilterInput,
|
|
577
585
|
field_name,
|
|
@@ -580,6 +588,25 @@ def create_filter_input(cls: Type[SQLModel], name_suffix="Filter"):
|
|
|
580
588
|
description=declared_filters[field_name].get("description"),
|
|
581
589
|
),
|
|
582
590
|
)
|
|
591
|
+
|
|
592
|
+
# process external field_names
|
|
593
|
+
for field_name in (set(declared_filters.keys()) - internal_type_filters):
|
|
594
|
+
graphql_type = declared_filters[field_name]["type"]
|
|
595
|
+
if declared_filters[field_name].get("is_list"):
|
|
596
|
+
graphql_type = List[graphql_type]
|
|
597
|
+
else:
|
|
598
|
+
graphql_type = graphql_type
|
|
599
|
+
|
|
600
|
+
if not declared_filters[field_name].get("required"):
|
|
601
|
+
graphql_type = Optional[graphql_type]
|
|
602
|
+
setattr(
|
|
603
|
+
FilterInput,
|
|
604
|
+
field_name,
|
|
605
|
+
strawberry.field(
|
|
606
|
+
default=declared_filters[field_name].get("default"), graphql_type=graphql_type,
|
|
607
|
+
description=declared_filters[field_name].get("description"),
|
|
608
|
+
),
|
|
609
|
+
)
|
|
583
610
|
return strawberry.input(name=f"{cls.__name__}{name_suffix}")(FilterInput)
|
|
584
611
|
|
|
585
612
|
|
|
@@ -657,8 +684,26 @@ def get_query(cls: "Graphemy"):
|
|
|
657
684
|
for k, v in vars(filter).items():
|
|
658
685
|
if v is None or k not in declared_filters:
|
|
659
686
|
continue
|
|
660
|
-
column = getattr(cls, k)
|
|
661
687
|
mode = declared_filters[k].get("mode", GraphemyFilterMode.EXACT)
|
|
688
|
+
|
|
689
|
+
# Check if this is a simple direct column filter or a join-based filter
|
|
690
|
+
join_model = declared_filters[k].get("join_model")
|
|
691
|
+
join_source_col = declared_filters[k].get("join_source_col")
|
|
692
|
+
join_target_col = declared_filters[k].get("join_target_col")
|
|
693
|
+
|
|
694
|
+
if join_model and join_source_col and join_target_col:
|
|
695
|
+
# We have a join-based filter. Something like:
|
|
696
|
+
# stmt.join(CategoryShop, CategoryShop.shop_id == Shop.id)
|
|
697
|
+
# stmt.filter(CategoryShop.category_id.in_(v))
|
|
698
|
+
stmt = stmt.join(join_model, getattr(join_model, join_source_col) == cls.id)
|
|
699
|
+
count_stmt = count_stmt.join(join_model, getattr(join_model, join_source_col) == cls.id)
|
|
700
|
+
|
|
701
|
+
# Because `mode` is likely IN or EXACT, handle accordingly
|
|
702
|
+
if mode == GraphemyFilterMode.IN:
|
|
703
|
+
stmt = stmt.where(getattr(join_model, join_target_col).in_(v))
|
|
704
|
+
count_stmt = count_stmt.where(getattr(join_model, join_target_col).in_(v))
|
|
705
|
+
continue
|
|
706
|
+
column = getattr(cls, k)
|
|
662
707
|
if mode == GraphemyFilterMode.EXACT:
|
|
663
708
|
stmt = stmt.filter(column.ilike(v))
|
|
664
709
|
count_stmt = count_stmt.filter(column.ilike(v))
|
|
@@ -674,6 +719,19 @@ def get_query(cls: "Graphemy"):
|
|
|
674
719
|
elif mode == GraphemyFilterMode.IN:
|
|
675
720
|
stmt = stmt.where(column.in_(v))
|
|
676
721
|
count_stmt = count_stmt.filter(column.in_(v))
|
|
722
|
+
elif mode == GraphemyFilterMode.IN_STRING_ARRAY:
|
|
723
|
+
if v and isinstance(v[0], list):
|
|
724
|
+
v = v[0]
|
|
725
|
+
if v and type(v[0]) == Enum:
|
|
726
|
+
v = [enum_type.value for enum_type in v]
|
|
727
|
+
stmt = stmt.where(
|
|
728
|
+
func.string_to_array(column, ",")
|
|
729
|
+
.op("&&")(array(v))
|
|
730
|
+
)
|
|
731
|
+
count_stmt = count_stmt.filter(
|
|
732
|
+
func.string_to_array(column, ",")
|
|
733
|
+
.op("&&")(array(v))
|
|
734
|
+
)
|
|
677
735
|
|
|
678
736
|
# Apply any additional query filters defined by Setup
|
|
679
737
|
qf = Setup.query_filter(cls, info.context)
|
|
@@ -682,7 +740,7 @@ def get_query(cls: "Graphemy"):
|
|
|
682
740
|
|
|
683
741
|
# Determine ordering
|
|
684
742
|
# Assume `orderBy` is the name of a column in `cls`. If not provided, default to primary key.
|
|
685
|
-
order_column = getattr(cls, orderBy) if orderBy else getattr(cls,
|
|
743
|
+
order_column = getattr(cls, orderBy) if orderBy else getattr(cls, cls.__default_order_by__)
|
|
686
744
|
|
|
687
745
|
# Handle pagination cursors: decode them and apply filters
|
|
688
746
|
# We assume the cursors are encoded in a way that allows retrieval of the order_column value.
|
|
@@ -759,28 +817,6 @@ def get_query(cls: "Graphemy"):
|
|
|
759
817
|
return ModelQuery, Filter
|
|
760
818
|
|
|
761
819
|
|
|
762
|
-
def get_put_mutation(cls: "Graphemy") -> StrawberryField:
|
|
763
|
-
pk = [pk.name for pk in inspect(cls).primary_key]
|
|
764
|
-
|
|
765
|
-
class Filter:
|
|
766
|
-
pass
|
|
767
|
-
|
|
768
|
-
for field_name, field in cls.__annotations__.items():
|
|
769
|
-
setattr(
|
|
770
|
-
Filter,
|
|
771
|
-
field_name,
|
|
772
|
-
strawberry.field(default=None, graphql_type=Optional[field]),
|
|
773
|
-
)
|
|
774
|
-
input = strawberry.input(name=f"{cls.__name__}Input")(Filter)
|
|
775
|
-
|
|
776
|
-
async def mutation(self, params: input) -> cls.__strawberry_schema__:
|
|
777
|
-
return await put_item(cls, params, pk)
|
|
778
|
-
|
|
779
|
-
return strawberry.mutation(
|
|
780
|
-
mutation,
|
|
781
|
-
permission_classes=[Setup.get_auth(cls, "mutation")],
|
|
782
|
-
)
|
|
783
|
-
|
|
784
820
|
|
|
785
821
|
def get_delete_mutation(cls: "Graphemy") -> StrawberryField:
|
|
786
822
|
pk = [pk.name for pk in inspect(cls).primary_key]
|
|
@@ -882,6 +918,7 @@ def get_mutations(cls: "Graphemy"):
|
|
|
882
918
|
"""
|
|
883
919
|
fields = {}
|
|
884
920
|
|
|
921
|
+
@strawberry.type
|
|
885
922
|
class ModelMutations:
|
|
886
923
|
pass
|
|
887
924
|
|
|
@@ -911,6 +948,10 @@ def get_mutations(cls: "Graphemy"):
|
|
|
911
948
|
for permission_class in permission_classes:
|
|
912
949
|
permission_class.item_model = cls
|
|
913
950
|
|
|
951
|
+
fields[f"{custom_mutation.__name__}Model"] = strawberry.mutation(
|
|
952
|
+
resolver=custom_mutation,
|
|
953
|
+
permission_classes=permission_classes
|
|
954
|
+
)
|
|
914
955
|
setattr(
|
|
915
956
|
ModelMutations,
|
|
916
957
|
custom_mutation.__name__,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "bootgraph"
|
|
3
|
-
version = "v1.
|
|
3
|
+
version = "v1.11.0.dev24840"
|
|
4
4
|
description = "A Python library for integrating SQLModel and Strawberry, providing a seamless GraphQL integration with FastAPI and advanced features for database interactions."
|
|
5
5
|
authors = ["Matheus Doreto <matheusdoreto.md@gmail.com>", "Pavel Mulin <mulin.pasha@gmail.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -51,6 +51,8 @@ jinja2 = "^3.1.2"
|
|
|
51
51
|
mdx-include = "^1.4.2"
|
|
52
52
|
mkdocstrings = "^0.26.1"
|
|
53
53
|
mkdocstrings-python = "^1.11.1"
|
|
54
|
+
asyncpg = ">=0.29.0"
|
|
55
|
+
greenlet = ">=3.0.3"
|
|
54
56
|
|
|
55
57
|
[build-system]
|
|
56
58
|
requires = ["poetry-core"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|