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.
@@ -1,8 +1,7 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bootgraph
3
- Version: 1.9.0.dev24129
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 model_mutations_resolver() -> cls_mutations:
173
- return cls_mutations()
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__}")(model_mutations_resolver)
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
- context = {"engine": engine}
189
- if context_session:
190
- context = {"session": context_session(engine)}
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, "id")
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
- if declared_filters[field_name].get("required"):
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[graphql_type]
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, "id")
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.9.0.dev24129"
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"]