bootgraph 1.15.0.dev26211__tar.gz → 1.15.0.dev26232__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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bootgraph
3
- Version: 1.15.0.dev26211
3
+ Version: 1.15.0.dev26232
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
5
  License: MIT
6
6
  Author: Matheus Doreto
@@ -0,0 +1,71 @@
1
+ from enum import Enum
2
+ from typing import List, Tuple, Union
3
+ from sqlmodel import SQLModel
4
+ from sqlalchemy import cast, and_
5
+ from sqlalchemy.dialects.postgresql import JSONB
6
+ from sqlalchemy import func
7
+
8
+
9
+ def extract_enum_values(values: List[Union[Enum, List[Enum]]]) -> List[str]:
10
+ """
11
+ Extracts string values from a list of Enums or nested lists of Enums.
12
+ Ensures all values are converted to strings.
13
+ """
14
+ return [
15
+ str((item[0] if isinstance(item, list) else item).value)
16
+ if hasattr((item[0] if isinstance(item, list) else item), "value")
17
+ else str(item[0] if isinstance(item, list) else item)
18
+ for item in values
19
+ ]
20
+
21
+
22
+ def apply_jsonb_contains_filter(
23
+ stmt,
24
+ count_stmt,
25
+ filter_value: List[Union[Enum, List[Enum]]],
26
+ model_class: SQLModel,
27
+ field_name: str
28
+ ) -> Tuple:
29
+ """
30
+ Applies a JSONB containment filter (`@>`) on a model field.
31
+ """
32
+ if not filter_value:
33
+ return stmt, count_stmt
34
+
35
+ values = extract_enum_values(filter_value)
36
+ field = getattr(model_class, field_name)
37
+ condition = cast(field, JSONB).contains(values)
38
+
39
+ stmt = stmt.where(condition)
40
+ if count_stmt is not None:
41
+ count_stmt = count_stmt.where(condition)
42
+
43
+ return stmt, count_stmt
44
+
45
+
46
+ def apply_comma_separated_filter(
47
+ stmt,
48
+ count_stmt,
49
+ filter_value: List[Union[Enum, List[Enum]]],
50
+ model_class: SQLModel,
51
+ field_name: str
52
+ ) -> Tuple:
53
+ """
54
+ Filters entries where ALL tags in filter_value are present
55
+ in the comma-separated string field.
56
+ """
57
+ if not filter_value:
58
+ return stmt, count_stmt
59
+
60
+ tag_values = extract_enum_values(filter_value)
61
+ field = getattr(model_class, field_name)
62
+ padded_field = func.concat(',', field, ',')
63
+
64
+ conditions = [padded_field.ilike(f'%,{tag},%') for tag in tag_values]
65
+ combined_condition = and_(*conditions)
66
+
67
+ stmt = stmt.where(combined_condition)
68
+ if count_stmt is not None:
69
+ count_stmt = count_stmt.where(combined_condition)
70
+
71
+ return stmt, count_stmt
@@ -1,11 +1,12 @@
1
1
  import re
2
- from typing import Any, Optional, Callable
2
+ from enum import Enum
3
+ from typing import Any, Optional, Callable, Dict, Type
3
4
 
4
5
  from sqlmodel import SQLModel
5
6
  from strawberry.types.base import StrawberryType
6
7
 
7
8
  from .dl import Dl, ManyRelation
8
- from .schemas.generators import get_dl_function, get_many_relation_function, get_query
9
+ from .schemas.generators import get_dl_function, get_many_relation_function, get_query, OrderBy
9
10
  from .setup import Setup
10
11
 
11
12
 
@@ -46,7 +47,9 @@ class Graphemy(SQLModel):
46
47
  specific mutation names to their respective permission classes. This allows
47
48
  for fine-grained control over which permissions apply to each mutation. If a
48
49
  mutation is not listed, it defaults to having no specific permissions.
49
- __default_order_by__ (str): Order by for database queries
50
+ __default_order_by__ (dict[str, OrderBy] | str): Order by for database queries. Can be a string for
51
+ backward compatibility or a dictionary with attribute as key and OrderBy as value.
52
+ Example: {"id": OrderBy.ASC}
50
53
  Classes:
51
54
  Graphemy: An extended SQLModel that incorporates GraphQL functionalities by using
52
55
  Strawberry GraphQL. This class allows defining GraphQL schemas, query names,
@@ -69,7 +72,7 @@ class Graphemy(SQLModel):
69
72
  __enable_query__: bool | None = None
70
73
  __queryname__: str = ""
71
74
  __enginename__: str = "default"
72
- __default_order_by__: str = "id"
75
+ __default_order_by__: Dict[str, OrderBy] = {"id": OrderBy.ASC}
73
76
  __filter_attributes__: Optional[dict[str, dict[str, Any]]] = None
74
77
  __custom_resolvers__: Optional[list[Callable]] = None
75
78
  __custom_mutations__: Optional[list[Callable]] = None
@@ -22,10 +22,12 @@ from sqlalchemy.inspection import inspect
22
22
  from strawberry.federation.schema_directives import Shareable
23
23
  from strawberry.tools import merge_types
24
24
  from strawberry.types.field import StrawberryField
25
-
25
+ from .models import OrderBy, OrderByInput
26
26
  from ..database.operations import delete_item, get_items, put_item
27
27
  from ..dl import Dl, ManyRelation
28
28
 
29
+
30
+
29
31
  if TYPE_CHECKING:
30
32
  from ..models import Graphemy
31
33
 
@@ -41,6 +43,53 @@ class GraphemyFilterMode:
41
43
  CUSTOM_FILTER_BUILDER = "CUSTOM_FILTER_BUILDER"
42
44
 
43
45
 
46
+
47
+ def process_order_by(cls, orderBy, stmt):
48
+ """
49
+ Process the orderBy parameter and return the order column and a statement with ordering applied.
50
+
51
+ Args:
52
+ cls: The model class
53
+ orderBy: Either a string column name, an OrderByInput instance, or a list of OrderByInput instances
54
+
55
+ Returns:
56
+ Tuple of (primary_order_column, stmt_with_ordering_applied)
57
+ """
58
+ if isinstance(orderBy, list) and orderBy:
59
+ # List of OrderByInput objects
60
+ primary_order = orderBy[0] # First one is the primary for cursors
61
+ primary_order_column = getattr(cls, primary_order.field)
62
+
63
+ # Apply all orderings
64
+ for order_item in orderBy:
65
+ col = getattr(cls, order_item.field)
66
+ if order_item.direction == OrderBy.DESC:
67
+ stmt = stmt.order_by(col.desc())
68
+ else:
69
+ stmt = stmt.order_by(col.asc())
70
+
71
+ return primary_order_column, stmt
72
+
73
+ # Default ordering from the model
74
+ default_order_by = cls.__default_order_by__
75
+ if isinstance(default_order_by, dict):
76
+ # For dict-based ordering, use the first key as the primary order column for cursors
77
+ column_name = next(iter(default_order_by))
78
+ direction = default_order_by[column_name]
79
+ order_column = getattr(cls, column_name)
80
+
81
+ # Apply all orderings to the statement
82
+ for col_name, dir_value in default_order_by.items():
83
+ col = getattr(cls, col_name)
84
+ if dir_value == "DESC":
85
+ stmt = stmt.order_by(col.desc())
86
+ else:
87
+ stmt = stmt.order_by(col.asc())
88
+ else:
89
+ raise Exception(f"Invalid orderBy on {cls.__name__} {cls.__default_order_by__}")
90
+
91
+ return order_column, stmt
92
+
44
93
  def set_schema(
45
94
  cls: "Graphemy",
46
95
  functions: Dict[str, Tuple[Callable, "Graphemy"]],
@@ -176,7 +225,7 @@ def get_many_relation_function(
176
225
  ]
177
226
  | None
178
227
  ) = None,
179
- orderBy: Optional[str] = None,
228
+ orderBy: Optional[list[OrderByInput]] = None,
180
229
  ) -> return_type:
181
230
  # Check permissions asynchronously
182
231
  if not await Setup.has_permission(cls, info.context, "query"):
@@ -222,7 +271,7 @@ def get_many_relation_function(
222
271
  .filter(source_field == source_value).select_from(cls))
223
272
 
224
273
  # Apply filters if provided
225
- apply_filters(stmt, count_stmt, cls, filters)
274
+ stmt, count_stmt = apply_filters(stmt, count_stmt, cls, filters)
226
275
 
227
276
  # Apply any additional query filters defined by Setup
228
277
  qf = Setup.query_filter(cls, info.context)
@@ -231,9 +280,7 @@ def get_many_relation_function(
231
280
  count_stmt = qf(count_stmt)
232
281
 
233
282
  # Determine ordering
234
- # Assume `orderBy` is the name of a column in `cls`. If not provided, default to primary key.
235
- order_column = getattr(cls, orderBy) if orderBy else getattr(cls, cls.__default_order_by__)
236
- stmt = stmt.order_by(order_column.asc())
283
+ order_column, stmt = process_order_by(cls, orderBy, stmt)
237
284
 
238
285
  # Handle pagination cursors: decode them and apply filters
239
286
  # We assume the cursors are encoded in a way that allows retrieval of the order_column value.
@@ -505,9 +552,8 @@ def apply_filters(
505
552
  elif mode == GraphemyFilterMode.CUSTOM_FILTER_BUILDER:
506
553
  filter_builder = cfg.get("filter_builder")
507
554
  if callable(filter_builder):
508
- stmt, count_stmt = filter_builder(stmt, count_stmt, value, cls)
509
-
510
- return (stmt, count_stmt)
555
+ stmt, count_stmt = filter_builder(stmt, count_stmt, value, cls, key)
556
+ return stmt, count_stmt
511
557
 
512
558
 
513
559
  async def get_one(cls: Type[SQLModel], filter_obj: Any, query_filter: callable = None):
@@ -791,17 +837,21 @@ def get_query(cls: "Graphemy"):
791
837
  async def many_resolver(
792
838
  self,
793
839
  info: "Info",
794
- first: Optional[int] = None,
840
+ first: Optional[int] = 100,
795
841
  after: Optional[str] = None,
796
842
  last: Optional[int] = None,
797
843
  before: Optional[str] = None,
798
844
  filter: Optional[Filter] = None,
799
- orderBy: Optional[str] = None,
845
+ orderBy: Optional[list[OrderByInput]] = None,
800
846
  ) -> CountableConnectionForModel:
801
847
  # Check permissions asynchronously
802
848
  if not await Setup.has_permission(cls, info.context, "query"):
803
849
  # Return empty connection if no permission
804
850
  return convert_to_countable_connection([], first, after, last, before, CountableConnectionForModel)
851
+
852
+ if first and first > 100 or last and last > 100:
853
+ raise ValueError("first and last must be less than 100")
854
+
805
855
 
806
856
  # Obtain the engine for this model
807
857
  engine = Setup.engine[cls.__enginename__]
@@ -816,10 +866,10 @@ def get_query(cls: "Graphemy"):
816
866
  qf = Setup.query_filter(cls, info.context)
817
867
  if qf and callable(qf):
818
868
  stmt = qf(stmt)
869
+ count_stmt = qf(count_stmt)
819
870
 
820
871
  # Determine ordering
821
- # Assume `orderBy` is the name of a column in `cls`. If not provided, default to primary key.
822
- order_column = getattr(cls, orderBy) if orderBy else getattr(cls, cls.__default_order_by__)
872
+ order_column, stmt = process_order_by(cls, orderBy, stmt)
823
873
 
824
874
  # Handle pagination cursors: decode them and apply filters
825
875
  # We assume the cursors are encoded in a way that allows retrieval of the order_column value.
@@ -0,0 +1,26 @@
1
+ from datetime import date
2
+ import enum
3
+
4
+ import strawberry
5
+
6
+
7
+ @strawberry.input
8
+ class DateFilter:
9
+ range: list[date | None] | None = None
10
+ items: list[date] | None = None
11
+ year: int | None = None
12
+
13
+
14
+ @strawberry.enum
15
+ class OrderBy(str, enum.Enum):
16
+ """
17
+ Enum representing the ordering direction for database queries.
18
+ """
19
+ ASC = "ASC"
20
+ DESC = "DESC"
21
+
22
+
23
+ @strawberry.input
24
+ class OrderByInput:
25
+ field: str
26
+ direction: OrderBy = OrderBy.ASC
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "bootgraph"
3
- version = "v1.15.0.dev26211"
3
+ version = "v1.15.0.dev26232"
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"
@@ -1,10 +0,0 @@
1
- from datetime import date
2
-
3
- import strawberry
4
-
5
-
6
- @strawberry.input
7
- class DateFilter:
8
- range: list[date | None] | None = None
9
- items: list[date] | None = None
10
- year: int | None = None