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.
- {bootgraph-1.15.0.dev26211 → bootgraph-1.15.0.dev26232}/PKG-INFO +1 -1
- bootgraph-1.15.0.dev26232/bootgraph/database/filter_builders/__init__.py +71 -0
- {bootgraph-1.15.0.dev26211 → bootgraph-1.15.0.dev26232}/bootgraph/models.py +7 -4
- {bootgraph-1.15.0.dev26211 → bootgraph-1.15.0.dev26232}/bootgraph/schemas/generators.py +63 -13
- bootgraph-1.15.0.dev26232/bootgraph/schemas/models.py +26 -0
- {bootgraph-1.15.0.dev26211 → bootgraph-1.15.0.dev26232}/pyproject.toml +1 -1
- bootgraph-1.15.0.dev26211/bootgraph/schemas/models.py +0 -10
- {bootgraph-1.15.0.dev26211 → bootgraph-1.15.0.dev26232}/LICENSE +0 -0
- {bootgraph-1.15.0.dev26211 → bootgraph-1.15.0.dev26232}/README.md +0 -0
- {bootgraph-1.15.0.dev26211 → bootgraph-1.15.0.dev26232}/bootgraph/__init__.py +0 -0
- {bootgraph-1.15.0.dev26211 → bootgraph-1.15.0.dev26232}/bootgraph/database/__init__.py +0 -0
- {bootgraph-1.15.0.dev26211 → bootgraph-1.15.0.dev26232}/bootgraph/database/operations.py +0 -0
- {bootgraph-1.15.0.dev26211 → bootgraph-1.15.0.dev26232}/bootgraph/database/utils.py +0 -0
- {bootgraph-1.15.0.dev26211 → bootgraph-1.15.0.dev26232}/bootgraph/dl.py +0 -0
- {bootgraph-1.15.0.dev26211 → bootgraph-1.15.0.dev26232}/bootgraph/router.py +0 -0
- {bootgraph-1.15.0.dev26211 → bootgraph-1.15.0.dev26232}/bootgraph/schemas/__init__.py +0 -0
- {bootgraph-1.15.0.dev26211 → bootgraph-1.15.0.dev26232}/bootgraph/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: bootgraph
|
|
3
|
-
Version: 1.15.0.
|
|
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
|
|
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[
|
|
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
|
-
|
|
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] =
|
|
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[
|
|
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
|
-
|
|
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.
|
|
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"
|
|
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
|