GeneralManager 0.11.2__tar.gz → 0.12.0__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.
- {generalmanager-0.11.2 → generalmanager-0.12.0}/GeneralManager.egg-info/PKG-INFO +1 -1
- {generalmanager-0.11.2 → generalmanager-0.12.0}/PKG-INFO +1 -1
- {generalmanager-0.11.2 → generalmanager-0.12.0}/pyproject.toml +1 -1
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/api/graphql.py +196 -76
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/api/mutation.py +26 -16
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/measurement/measurement.py +10 -12
- {generalmanager-0.11.2 → generalmanager-0.12.0}/GeneralManager.egg-info/SOURCES.txt +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/GeneralManager.egg-info/dependency_links.txt +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/GeneralManager.egg-info/requires.txt +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/GeneralManager.egg-info/top_level.txt +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/LICENSE +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/README.md +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/setup.cfg +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/__init__.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/api/property.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/apps.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/bucket/baseBucket.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/bucket/calculationBucket.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/bucket/databaseBucket.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/bucket/groupBucket.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/cache/cacheDecorator.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/cache/cacheTracker.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/cache/dependencyIndex.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/cache/modelDependencyCollector.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/cache/signals.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/factory/__init__.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/factory/autoFactory.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/factory/factories.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/factory/factoryMethods.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/interface/__init__.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/interface/baseInterface.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/interface/calculationInterface.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/interface/databaseBasedInterface.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/interface/databaseInterface.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/interface/models.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/interface/readOnlyInterface.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/manager/__init__.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/manager/generalManager.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/manager/groupManager.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/manager/input.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/manager/meta.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/measurement/__init__.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/measurement/measurementField.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/permission/__init__.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/permission/basePermission.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/permission/fileBasedPermission.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/permission/managerBasedPermission.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/permission/mutationPermission.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/permission/permissionChecks.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/permission/permissionDataManager.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/permission/utils.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/rule/__init__.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/rule/handler.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/rule/rule.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/utils/__init__.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/utils/argsToKwargs.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/utils/filterParser.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/utils/formatString.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/utils/jsonEncoder.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/utils/makeCacheKey.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/utils/noneToZero.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/utils/pathMapping.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/utils/testing.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/tests/test_settings.py +0 -0
- {generalmanager-0.11.2 → generalmanager-0.12.0}/tests/test_urls.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: GeneralManager
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.12.0
|
4
4
|
Summary: Modular Django-based data management framework with ORM, GraphQL, fine-grained permissions, rule validation, calculations and caching.
|
5
5
|
Author-email: Tim Kleindick <tkleindick@yahoo.de>
|
6
6
|
License-Expression: MIT
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: GeneralManager
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.12.0
|
4
4
|
Summary: Modular Django-based data management framework with ORM, GraphQL, fine-grained permissions, rule validation, calculations and caching.
|
5
5
|
Author-email: Tim Kleindick <tkleindick@yahoo.de>
|
6
6
|
License-Expression: MIT
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "GeneralManager"
|
7
|
-
version = "0.
|
7
|
+
version = "0.12.0"
|
8
8
|
description = "Modular Django-based data management framework with ORM, GraphQL, fine-grained permissions, rule validation, calculations and caching."
|
9
9
|
readme = "README.md"
|
10
10
|
authors = [{ name = "Tim Kleindick", email = "tkleindick@yahoo.de" }]
|
@@ -11,6 +11,10 @@ from general_manager.api.property import GraphQLProperty
|
|
11
11
|
from general_manager.bucket.baseBucket import Bucket
|
12
12
|
from general_manager.interface.baseInterface import InterfaceBase
|
13
13
|
from django.db.models import NOT_PROVIDED
|
14
|
+
from django.core.exceptions import ValidationError
|
15
|
+
|
16
|
+
from graphql import GraphQLError
|
17
|
+
|
14
18
|
|
15
19
|
if TYPE_CHECKING:
|
16
20
|
from general_manager.permission.basePermission import BasePermission
|
@@ -22,13 +26,21 @@ class MeasurementType(graphene.ObjectType):
|
|
22
26
|
unit = graphene.String()
|
23
27
|
|
24
28
|
|
29
|
+
class PageInfo(graphene.ObjectType):
|
30
|
+
total_count = graphene.Int(required=True)
|
31
|
+
page_size = graphene.Int(required=False)
|
32
|
+
current_page = graphene.Int(required=True)
|
33
|
+
total_pages = graphene.Int(required=True)
|
34
|
+
|
35
|
+
|
25
36
|
def getReadPermissionFilter(
|
26
37
|
generalManagerClass: GeneralManagerMeta,
|
27
38
|
info: GraphQLResolveInfo,
|
28
39
|
) -> list[tuple[dict[str, Any], dict[str, Any]]]:
|
29
40
|
"""
|
30
|
-
|
31
|
-
|
41
|
+
Returns a list of filter and exclude dictionaries based on the read permissions for the specified manager class and user context.
|
42
|
+
|
43
|
+
Each tuple in the returned list contains a filter dictionary and an exclude dictionary, representing permission-based constraints to be applied to queries.
|
32
44
|
"""
|
33
45
|
filters = []
|
34
46
|
PermissionClass: type[BasePermission] | None = getattr(
|
@@ -55,17 +67,16 @@ class GraphQL:
|
|
55
67
|
_mutation_class: type[graphene.ObjectType] | None = None
|
56
68
|
_mutations: dict[str, Any] = {}
|
57
69
|
_query_fields: dict[str, Any] = {}
|
70
|
+
_page_type_registry: dict[str, type[graphene.ObjectType]] = {}
|
58
71
|
graphql_type_registry: dict[str, type] = {}
|
59
72
|
graphql_filter_type_registry: dict[str, type] = {}
|
60
73
|
|
61
74
|
@classmethod
|
62
75
|
def createGraphqlMutation(cls, generalManagerClass: type[GeneralManager]) -> None:
|
63
76
|
"""
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
- Zu jedem Feld ein Resolver generiert und hinzugefügt
|
68
|
-
- Der neue Type in das Registry eingetragen und Mutationen angehängt.
|
77
|
+
Creates and registers GraphQL mutation classes (create, update, delete) for the given manager class if its interface overrides the corresponding base methods.
|
78
|
+
|
79
|
+
For each supported mutation, generates a GraphQL mutation class with appropriate input and output fields, and adds it to the mutation registry.
|
69
80
|
"""
|
70
81
|
|
71
82
|
interface_cls: InterfaceBase | None = getattr(
|
@@ -76,7 +87,6 @@ class GraphQL:
|
|
76
87
|
|
77
88
|
default_return_values = {
|
78
89
|
"success": graphene.Boolean(),
|
79
|
-
"errors": graphene.List(graphene.String),
|
80
90
|
generalManagerClass.__name__: graphene.Field(
|
81
91
|
lambda: GraphQL.graphql_type_registry[generalManagerClass.__name__]
|
82
92
|
),
|
@@ -102,9 +112,9 @@ class GraphQL:
|
|
102
112
|
@classmethod
|
103
113
|
def createGraphqlInterface(cls, generalManagerClass: GeneralManagerMeta) -> None:
|
104
114
|
"""
|
105
|
-
|
115
|
+
Creates and registers a GraphQL ObjectType for the given GeneralManager subclass.
|
106
116
|
|
107
|
-
This method
|
117
|
+
This method introspects the manager's interface and GraphQLProperty fields, maps them to Graphene fields with appropriate resolvers, registers the resulting type in the internal registry, and adds corresponding query fields to the schema.
|
108
118
|
"""
|
109
119
|
interface_cls: InterfaceBase | None = getattr(
|
110
120
|
generalManagerClass, "Interface", None
|
@@ -145,10 +155,10 @@ class GraphQL:
|
|
145
155
|
generalManagerClass: GeneralManagerMeta,
|
146
156
|
) -> type[graphene.Enum] | None:
|
147
157
|
"""
|
148
|
-
|
158
|
+
Generate a Graphene Enum type listing the sortable fields for a given GeneralManager class.
|
149
159
|
|
150
160
|
Returns:
|
151
|
-
A Graphene Enum type with options for each sortable attribute, or None if no sortable fields
|
161
|
+
A Graphene Enum type with options for each sortable attribute, including separate options for the value and unit of Measurement fields, or None if no sortable fields are found.
|
152
162
|
"""
|
153
163
|
sort_options = []
|
154
164
|
for (
|
@@ -180,7 +190,7 @@ class GraphQL:
|
|
180
190
|
"""
|
181
191
|
Dynamically generates a Graphene InputObjectType for filtering fields of a GeneralManager subclass.
|
182
192
|
|
183
|
-
Creates filter fields for each attribute based on its type, supporting numeric and string filter
|
193
|
+
Creates filter fields for each attribute based on its type, supporting numeric and string filter operations, and specialized handling for Measurement attributes. Returns the generated InputObjectType, or None if no applicable filter fields exist.
|
184
194
|
"""
|
185
195
|
number_options = ["exact", "gt", "gte", "lt", "lte"]
|
186
196
|
string_options = [
|
@@ -237,7 +247,7 @@ class GraphQL:
|
|
237
247
|
"""
|
238
248
|
Maps a Python field type and name to the appropriate Graphene field for GraphQL schema generation.
|
239
249
|
|
240
|
-
For `Measurement`
|
250
|
+
For `Measurement` fields, returns a field with an optional `target_unit` argument. For `GeneralManager` subclasses, returns a paginated field with filtering, exclusion, sorting, pagination, and grouping arguments if the field name ends with `_list`; otherwise, returns a single object field. For all other types, returns the corresponding Graphene scalar field.
|
241
251
|
"""
|
242
252
|
if issubclass(field_type, Measurement):
|
243
253
|
return graphene.Field(MeasurementType, target_unit=graphene.String())
|
@@ -257,6 +267,13 @@ class GraphQL:
|
|
257
267
|
sort_by_options = GraphQL._sortByOptions(field_type)
|
258
268
|
if sort_by_options:
|
259
269
|
attributes["sort_by"] = sort_by_options()
|
270
|
+
|
271
|
+
page_type = GraphQL._getOrCreatePageType(
|
272
|
+
field_type.__name__ + "Page",
|
273
|
+
lambda: GraphQL.graphql_type_registry[field_type.__name__],
|
274
|
+
)
|
275
|
+
return graphene.Field(page_type, **attributes)
|
276
|
+
|
260
277
|
return graphene.List(
|
261
278
|
lambda: GraphQL.graphql_type_registry[field_type.__name__],
|
262
279
|
**attributes,
|
@@ -306,11 +323,19 @@ class GraphQL:
|
|
306
323
|
exclude_input: dict[str, Any] | str | None,
|
307
324
|
sort_by: graphene.Enum | None,
|
308
325
|
reverse: bool,
|
309
|
-
page: int | None,
|
310
|
-
page_size: int | None,
|
311
326
|
) -> Bucket[GeneralManager]:
|
312
327
|
"""
|
313
|
-
|
328
|
+
Applies filtering, exclusion, and sorting parameters to a queryset.
|
329
|
+
|
330
|
+
Parameters:
|
331
|
+
queryset (Bucket[GeneralManager]): The queryset to modify.
|
332
|
+
filter_input (dict | str | None): Filters to apply, as a dict or JSON string.
|
333
|
+
exclude_input (dict | str | None): Exclusions to apply, as a dict or JSON string.
|
334
|
+
sort_by (graphene.Enum | None): Field to sort by, if provided.
|
335
|
+
reverse (bool): Whether to reverse the sort order.
|
336
|
+
|
337
|
+
Returns:
|
338
|
+
Bucket[GeneralManager]: The modified queryset after applying filters, exclusions, and sorting.
|
314
339
|
"""
|
315
340
|
filters = GraphQL._parseInput(filter_input)
|
316
341
|
if filters:
|
@@ -324,12 +349,6 @@ class GraphQL:
|
|
324
349
|
sort_by_str = cast(str, getattr(sort_by, "value", sort_by))
|
325
350
|
queryset = queryset.sort(sort_by_str, reverse=reverse)
|
326
351
|
|
327
|
-
if page is not None or page_size is not None:
|
328
|
-
page = page or 1
|
329
|
-
page_size = page_size or 10
|
330
|
-
offset = (page - 1) * page_size
|
331
|
-
queryset = cast(Bucket, queryset[offset : offset + page_size])
|
332
|
-
|
333
352
|
return queryset
|
334
353
|
|
335
354
|
@staticmethod
|
@@ -370,14 +389,9 @@ class GraphQL:
|
|
370
389
|
base_getter: Callable[[Any], Any], fallback_manager_class: type[GeneralManager]
|
371
390
|
) -> Callable[..., Any]:
|
372
391
|
"""
|
373
|
-
|
374
|
-
|
375
|
-
Parameters:
|
376
|
-
base_getter: Function to obtain the base queryset from the parent instance.
|
377
|
-
fallback_manager_class: Manager class to use if the queryset does not specify one.
|
392
|
+
Creates a resolver for GraphQL list fields that returns paginated, filtered, sorted, and optionally grouped results with permission checks applied.
|
378
393
|
|
379
|
-
|
380
|
-
A resolver function that processes list queries with filtering, sorting, pagination, and grouping.
|
394
|
+
The returned resolver processes list queries by applying permission-based filtering, user-specified filters and exclusions, sorting, grouping, and pagination. It returns a dictionary containing the paginated items and pagination metadata.
|
381
395
|
"""
|
382
396
|
|
383
397
|
def resolver(
|
@@ -390,21 +404,21 @@ class GraphQL:
|
|
390
404
|
page: int | None = None,
|
391
405
|
page_size: int | None = None,
|
392
406
|
group_by: list[str] | None = None,
|
393
|
-
) -> Any:
|
407
|
+
) -> dict[str, Any]:
|
394
408
|
"""
|
395
|
-
Resolves a list field by returning
|
409
|
+
Resolves a list field by returning paginated, filtered, sorted, and optionally grouped results with permission checks applied.
|
396
410
|
|
397
411
|
Parameters:
|
398
|
-
filter: Filter criteria as a dictionary or JSON string.
|
399
|
-
exclude: Exclusion criteria as a dictionary or JSON string.
|
400
|
-
sort_by: Field to sort by
|
401
|
-
reverse: Whether to reverse the sort order.
|
402
|
-
page: Page number for pagination.
|
403
|
-
page_size: Number of items per page.
|
404
|
-
group_by: List of field names to group results by.
|
412
|
+
filter (dict[str, Any] | str | None): Filter criteria as a dictionary or JSON string.
|
413
|
+
exclude (dict[str, Any] | str | None): Exclusion criteria as a dictionary or JSON string.
|
414
|
+
sort_by (graphene.Enum | None): Field to sort by.
|
415
|
+
reverse (bool): Whether to reverse the sort order.
|
416
|
+
page (int | None): Page number for pagination.
|
417
|
+
page_size (int | None): Number of items per page.
|
418
|
+
group_by (list[str] | None): List of field names to group results by.
|
405
419
|
|
406
420
|
Returns:
|
407
|
-
|
421
|
+
dict[str, Any]: A dictionary containing the paginated items under "items" and pagination metadata under "pageInfo".
|
408
422
|
"""
|
409
423
|
base_queryset = base_getter(self)
|
410
424
|
# use _manager_class from the attribute if available, otherwise fallback
|
@@ -412,22 +426,70 @@ class GraphQL:
|
|
412
426
|
base_queryset, "_manager_class", fallback_manager_class
|
413
427
|
)
|
414
428
|
qs = GraphQL._applyPermissionFilters(base_queryset, manager_class, info)
|
415
|
-
qs = GraphQL._applyQueryParameters(
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
429
|
+
qs = GraphQL._applyQueryParameters(qs, filter, exclude, sort_by, reverse)
|
430
|
+
qs = GraphQL._applyGrouping(qs, group_by)
|
431
|
+
|
432
|
+
total_count = len(qs)
|
433
|
+
|
434
|
+
qs_paginated = GraphQL._applyPagination(qs, page, page_size)
|
435
|
+
|
436
|
+
page_info = {
|
437
|
+
"total_count": total_count,
|
438
|
+
"page_size": page_size,
|
439
|
+
"current_page": page or 1,
|
440
|
+
"total_pages": (
|
441
|
+
((total_count + page_size - 1) // page_size) if page_size else 1
|
442
|
+
),
|
443
|
+
}
|
444
|
+
return {
|
445
|
+
"items": qs_paginated,
|
446
|
+
"pageInfo": page_info,
|
447
|
+
}
|
424
448
|
|
425
449
|
return resolver
|
426
450
|
|
451
|
+
@staticmethod
|
452
|
+
def _applyPagination(
|
453
|
+
queryset: Bucket[GeneralManager], page: int | None, page_size: int | None
|
454
|
+
) -> Bucket[GeneralManager]:
|
455
|
+
"""
|
456
|
+
Return a paginated subset of the queryset based on the specified page number and page size.
|
457
|
+
|
458
|
+
If either `page` or `page_size` is provided, pagination is applied; otherwise, the original queryset is returned.
|
459
|
+
"""
|
460
|
+
if page is not None or page_size is not None:
|
461
|
+
page = page or 1
|
462
|
+
page_size = page_size or 10
|
463
|
+
offset = (page - 1) * page_size
|
464
|
+
queryset = cast(Bucket, queryset[offset : offset + page_size])
|
465
|
+
return queryset
|
466
|
+
|
467
|
+
@staticmethod
|
468
|
+
def _applyGrouping(
|
469
|
+
queryset: Bucket[GeneralManager], group_by: list[str] | None
|
470
|
+
) -> Bucket[GeneralManager]:
|
471
|
+
"""
|
472
|
+
Group a queryset by the specified fields.
|
473
|
+
|
474
|
+
If `group_by` is `[""]`, groups by all default fields. If `group_by` is a list of field names, groups by those fields. Returns the grouped queryset.
|
475
|
+
"""
|
476
|
+
if group_by is not None:
|
477
|
+
if group_by == [""]:
|
478
|
+
queryset = queryset.group_by()
|
479
|
+
else:
|
480
|
+
queryset = queryset.group_by(*group_by)
|
481
|
+
return queryset
|
482
|
+
|
427
483
|
@staticmethod
|
428
484
|
def _createMeasurementResolver(field_name: str) -> Callable[..., Any]:
|
429
485
|
"""
|
430
|
-
|
486
|
+
Creates a resolver function for Measurement fields that returns the value and unit, optionally converting to a specified target unit.
|
487
|
+
|
488
|
+
Parameters:
|
489
|
+
field_name (str): The name of the Measurement field to resolve.
|
490
|
+
|
491
|
+
Returns:
|
492
|
+
Callable[..., dict[str, Any] | None]: A resolver that returns a dictionary with 'value' and 'unit' keys, or None if permission is denied or the field is not a Measurement.
|
431
493
|
"""
|
432
494
|
|
433
495
|
def resolver(
|
@@ -465,7 +527,9 @@ class GraphQL:
|
|
465
527
|
@classmethod
|
466
528
|
def _createResolver(cls, field_name: str, field_type: type) -> Callable[..., Any]:
|
467
529
|
"""
|
468
|
-
|
530
|
+
Selects and returns the appropriate resolver function for a given field based on its type and name.
|
531
|
+
|
532
|
+
For fields ending with `_list` and referencing a `GeneralManager` subclass, returns a list resolver supporting pagination and filtering. For `Measurement` fields, returns a measurement resolver. For all other fields, returns a standard resolver.
|
469
533
|
"""
|
470
534
|
if field_name.endswith("_list") and issubclass(field_type, GeneralManager):
|
471
535
|
return cls._createListResolver(
|
@@ -475,14 +539,40 @@ class GraphQL:
|
|
475
539
|
return cls._createMeasurementResolver(field_name)
|
476
540
|
return cls._createNormalResolver(field_name)
|
477
541
|
|
542
|
+
@classmethod
|
543
|
+
def _getOrCreatePageType(
|
544
|
+
cls,
|
545
|
+
page_type_name: str,
|
546
|
+
item_type: type[graphene.ObjectType] | Callable[[], type[graphene.ObjectType]],
|
547
|
+
) -> type[graphene.ObjectType]:
|
548
|
+
"""
|
549
|
+
Return a paginated GraphQL ObjectType for the given item type, creating and caching it if it does not already exist.
|
550
|
+
|
551
|
+
Parameters:
|
552
|
+
page_type_name (str): The name to use for the paginated type.
|
553
|
+
|
554
|
+
Returns:
|
555
|
+
type[graphene.ObjectType]: A GraphQL ObjectType with `items` (list of item_type) and `pageInfo` (pagination metadata).
|
556
|
+
"""
|
557
|
+
if page_type_name not in cls._page_type_registry:
|
558
|
+
cls._page_type_registry[page_type_name] = type(
|
559
|
+
page_type_name,
|
560
|
+
(graphene.ObjectType,),
|
561
|
+
{
|
562
|
+
"items": graphene.List(item_type, required=True),
|
563
|
+
"pageInfo": graphene.Field(PageInfo, required=True),
|
564
|
+
},
|
565
|
+
)
|
566
|
+
return cls._page_type_registry[page_type_name]
|
567
|
+
|
478
568
|
@classmethod
|
479
569
|
def _addQueriesToSchema(
|
480
570
|
cls, graphene_type: type, generalManagerClass: GeneralManagerMeta
|
481
571
|
) -> None:
|
482
572
|
"""
|
483
|
-
|
573
|
+
Adds paginated list and single-item query fields for a GeneralManager subclass to the GraphQL schema.
|
484
574
|
|
485
|
-
|
575
|
+
The list query field supports filtering, exclusion, sorting, pagination, and grouping, returning a paginated result with metadata. The single-item query field retrieves an instance by its identification fields. Both queries are registered with their corresponding resolvers.
|
486
576
|
"""
|
487
577
|
if not issubclass(generalManagerClass, GeneralManager):
|
488
578
|
raise TypeError(
|
@@ -509,10 +599,11 @@ class GraphQL:
|
|
509
599
|
sort_by_options = cls._sortByOptions(generalManagerClass)
|
510
600
|
if sort_by_options:
|
511
601
|
attributes["sort_by"] = sort_by_options()
|
512
|
-
|
513
|
-
|
514
|
-
|
602
|
+
|
603
|
+
page_type = cls._getOrCreatePageType(
|
604
|
+
graphene_type.__name__ + "Page", graphene_type
|
515
605
|
)
|
606
|
+
list_field = graphene.Field(page_type, **attributes)
|
516
607
|
|
517
608
|
list_resolver = cls._createListResolver(
|
518
609
|
lambda self: generalManagerClass.all(), generalManagerClass
|
@@ -608,7 +699,7 @@ class GraphQL:
|
|
608
699
|
"""
|
609
700
|
Dynamically generates a Graphene mutation class for creating an instance of a specified GeneralManager subclass.
|
610
701
|
|
611
|
-
The generated mutation class filters out fields with `NOT_PROVIDED` values, calls the manager's `create` method with the provided arguments and the current user's ID,
|
702
|
+
The generated mutation class accepts input fields defined by the manager's interface, filters out fields with `NOT_PROVIDED` values, and calls the manager's `create` method with the provided arguments and the current user's ID. If creation succeeds, it returns a dictionary with a success flag and the created instance; if an error occurs, a GraphQL error is raised. Returns None if the manager class does not define an interface.
|
612
703
|
|
613
704
|
Returns:
|
614
705
|
The generated Graphene mutation class, or None if the manager class does not define an interface.
|
@@ -625,9 +716,9 @@ class GraphQL:
|
|
625
716
|
**kwargs: Any,
|
626
717
|
) -> dict:
|
627
718
|
"""
|
628
|
-
|
719
|
+
Creates a new instance of the manager class with the provided arguments.
|
629
720
|
|
630
|
-
Filters out
|
721
|
+
Filters out fields set to `NOT_PROVIDED` before creation. Returns a dictionary containing a success flag and the created instance keyed by the manager class name. Raises a GraphQL error if creation fails.
|
631
722
|
"""
|
632
723
|
try:
|
633
724
|
kwargs = {
|
@@ -639,14 +730,13 @@ class GraphQL:
|
|
639
730
|
**kwargs, creator_id=info.context.user.id
|
640
731
|
)
|
641
732
|
except Exception as e:
|
733
|
+
GraphQL._handleGraphQLError(e)
|
642
734
|
return {
|
643
735
|
"success": False,
|
644
|
-
"errors": [str(e)],
|
645
736
|
}
|
646
737
|
|
647
738
|
return {
|
648
739
|
"success": True,
|
649
|
-
"errors": [],
|
650
740
|
generalManagerClass.__name__: instance,
|
651
741
|
}
|
652
742
|
|
@@ -680,7 +770,7 @@ class GraphQL:
|
|
680
770
|
"""
|
681
771
|
Generates a GraphQL mutation class for updating an instance of a GeneralManager subclass.
|
682
772
|
|
683
|
-
The generated mutation accepts editable fields as arguments,
|
773
|
+
The generated mutation accepts editable fields as arguments, invokes the manager's `update` method with the provided values and the current user's ID, and returns a dictionary containing the operation's success status and the updated instance. If the manager class does not define an `Interface`, returns None.
|
684
774
|
|
685
775
|
Returns:
|
686
776
|
The generated Graphene mutation class, or None if no interface is defined.
|
@@ -697,14 +787,14 @@ class GraphQL:
|
|
697
787
|
**kwargs: Any,
|
698
788
|
) -> dict:
|
699
789
|
"""
|
700
|
-
Updates
|
790
|
+
Updates an instance of the specified GeneralManager class with the provided field values.
|
701
791
|
|
702
792
|
Parameters:
|
703
|
-
info (GraphQLResolveInfo): The GraphQL resolver context,
|
793
|
+
info (GraphQLResolveInfo): The GraphQL resolver context, containing user and request information.
|
704
794
|
**kwargs: Fields to update, including the required 'id' of the instance.
|
705
795
|
|
706
796
|
Returns:
|
707
|
-
dict:
|
797
|
+
dict: Contains 'success' (bool) and the updated instance keyed by its class name.
|
708
798
|
"""
|
709
799
|
try:
|
710
800
|
manager_id = kwargs.pop("id", None)
|
@@ -712,13 +802,13 @@ class GraphQL:
|
|
712
802
|
creator_id=info.context.user.id, **kwargs
|
713
803
|
)
|
714
804
|
except Exception as e:
|
805
|
+
GraphQL._handleGraphQLError(e)
|
715
806
|
return {
|
716
807
|
"success": False,
|
717
|
-
"errors": [str(e)],
|
718
808
|
}
|
809
|
+
|
719
810
|
return {
|
720
811
|
"success": True,
|
721
|
-
"errors": [],
|
722
812
|
generalManagerClass.__name__: instance,
|
723
813
|
}
|
724
814
|
|
@@ -752,10 +842,10 @@ class GraphQL:
|
|
752
842
|
"""
|
753
843
|
Generates a GraphQL mutation class for deactivating (soft-deleting) an instance of a GeneralManager subclass.
|
754
844
|
|
755
|
-
The generated mutation accepts input fields defined by the manager's interface, deactivates the specified instance using its ID, and returns a dictionary containing
|
845
|
+
The generated mutation accepts input fields defined by the manager's interface, deactivates the specified instance using its ID, and returns a dictionary containing a success status and the deactivated instance keyed by the class name. If the manager class does not define an interface, returns None.
|
756
846
|
|
757
847
|
Returns:
|
758
|
-
The generated Graphene mutation class, or None if
|
848
|
+
The generated Graphene mutation class, or None if no interface is defined.
|
759
849
|
"""
|
760
850
|
interface_cls: InterfaceBase | None = getattr(
|
761
851
|
generalManagerClass, "Interface", None
|
@@ -769,13 +859,14 @@ class GraphQL:
|
|
769
859
|
**kwargs: Any,
|
770
860
|
) -> dict:
|
771
861
|
"""
|
772
|
-
Deactivates an instance of the specified GeneralManager class and returns the
|
862
|
+
Deactivates an instance of the specified GeneralManager class and returns the result.
|
863
|
+
|
864
|
+
Parameters:
|
865
|
+
info (GraphQLResolveInfo): GraphQL resolver context containing user information.
|
866
|
+
**kwargs: Arguments including the instance ID to deactivate.
|
773
867
|
|
774
868
|
Returns:
|
775
|
-
dict:
|
776
|
-
- "success": Boolean indicating if the operation was successful.
|
777
|
-
- "errors": List of error messages, empty if successful.
|
778
|
-
- <ClassName>: The deactivated instance, keyed by the class name.
|
869
|
+
dict: Contains "success" (bool) and the deactivated instance keyed by its class name.
|
779
870
|
"""
|
780
871
|
try:
|
781
872
|
manager_id = kwargs.pop("id", None)
|
@@ -783,13 +874,13 @@ class GraphQL:
|
|
783
874
|
creator_id=info.context.user.id
|
784
875
|
)
|
785
876
|
except Exception as e:
|
877
|
+
GraphQL._handleGraphQLError(e)
|
786
878
|
return {
|
787
879
|
"success": False,
|
788
|
-
"errors": [str(e)],
|
789
880
|
}
|
881
|
+
|
790
882
|
return {
|
791
883
|
"success": True,
|
792
|
-
"errors": [],
|
793
884
|
generalManagerClass.__name__: instance,
|
794
885
|
}
|
795
886
|
|
@@ -813,3 +904,32 @@ class GraphQL:
|
|
813
904
|
"mutate": delete_mutation,
|
814
905
|
},
|
815
906
|
)
|
907
|
+
|
908
|
+
@staticmethod
|
909
|
+
def _handleGraphQLError(error: Exception) -> None:
|
910
|
+
"""
|
911
|
+
Raises a GraphQLError with an appropriate error code based on the type of exception.
|
912
|
+
|
913
|
+
If the error is a PermissionError, the code is set to "PERMISSION_DENIED". For ValueError or ValidationError, the code is "BAD_USER_INPUT". All other exceptions result in a code of "INTERNAL_SERVER_ERROR".
|
914
|
+
"""
|
915
|
+
if isinstance(error, PermissionError):
|
916
|
+
raise GraphQLError(
|
917
|
+
str(error),
|
918
|
+
extensions={
|
919
|
+
"code": "PERMISSION_DENIED",
|
920
|
+
},
|
921
|
+
)
|
922
|
+
elif isinstance(error, (ValueError, ValidationError)):
|
923
|
+
raise GraphQLError(
|
924
|
+
str(error),
|
925
|
+
extensions={
|
926
|
+
"code": "BAD_USER_INPUT",
|
927
|
+
},
|
928
|
+
)
|
929
|
+
else:
|
930
|
+
raise GraphQLError(
|
931
|
+
str(error),
|
932
|
+
extensions={
|
933
|
+
"code": "INTERNAL_SERVER_ERROR",
|
934
|
+
},
|
935
|
+
)
|
@@ -21,19 +21,23 @@ from general_manager.permission.mutationPermission import MutationPermission
|
|
21
21
|
|
22
22
|
def graphQlMutation(permission: Optional[Type[MutationPermission]] = None):
|
23
23
|
"""
|
24
|
-
Decorator that
|
25
|
-
|
26
|
-
The decorated function must
|
27
|
-
|
24
|
+
Decorator that converts a function into a GraphQL mutation class for use with Graphene, automatically generating argument and output fields from the function's signature and type annotations.
|
25
|
+
|
26
|
+
The decorated function must provide type hints for all parameters (except `info`) and a return annotation. The decorator dynamically constructs a mutation class with appropriate Graphene fields, enforces permission checks if a `permission` class is provided, and registers the mutation for use in the GraphQL API.
|
27
|
+
|
28
28
|
Parameters:
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
permission (Optional[Type[MutationPermission]]): An optional permission class to enforce access control on the mutation.
|
30
|
+
|
32
31
|
Returns:
|
33
32
|
Callable: A decorator that registers the mutation and returns the original function.
|
34
33
|
"""
|
35
34
|
|
36
35
|
def decorator(fn):
|
36
|
+
"""
|
37
|
+
Decorator that transforms a function into a GraphQL mutation class compatible with Graphene.
|
38
|
+
|
39
|
+
Analyzes the decorated function's signature and type hints to dynamically generate a mutation class with appropriate argument and output fields. Handles permission checks if a permission class is provided, manages mutation execution, and registers the mutation for use in the GraphQL API. On success, returns output fields and a `success` flag; on error, returns only `success=False`.
|
40
|
+
"""
|
37
41
|
sig = inspect.signature(fn)
|
38
42
|
hints = get_type_hints(fn)
|
39
43
|
|
@@ -86,10 +90,9 @@ def graphQlMutation(permission: Optional[Type[MutationPermission]] = None):
|
|
86
90
|
|
87
91
|
Arguments = type("Arguments", (), arg_fields)
|
88
92
|
|
89
|
-
# Build output fields: success
|
93
|
+
# Build output fields: success + fn return types
|
90
94
|
outputs = {
|
91
95
|
"success": graphene.Boolean(required=True),
|
92
|
-
"errors": graphene.List(graphene.String),
|
93
96
|
}
|
94
97
|
return_ann: type | tuple[type] | None = hints.get("return")
|
95
98
|
if return_ann is None:
|
@@ -120,28 +123,35 @@ def graphQlMutation(permission: Optional[Type[MutationPermission]] = None):
|
|
120
123
|
# Define mutate method
|
121
124
|
def _mutate(root, info, **kwargs):
|
122
125
|
|
126
|
+
"""
|
127
|
+
Handles the execution of a GraphQL mutation, including permission checks, result unpacking, and error handling.
|
128
|
+
|
129
|
+
Returns:
|
130
|
+
An instance of the mutation class with output fields populated from the mutation result and a success status.
|
131
|
+
"""
|
123
132
|
if permission:
|
124
133
|
permission.check(kwargs, info.context.user)
|
125
134
|
try:
|
126
135
|
result = fn(info, **kwargs)
|
127
136
|
data = {}
|
128
137
|
if isinstance(result, tuple):
|
129
|
-
# unpack according to outputs ordering after success
|
138
|
+
# unpack according to outputs ordering after success
|
130
139
|
for (field, _), val in zip(
|
131
|
-
outputs.items(),
|
140
|
+
outputs.items(),
|
141
|
+
[None, *list(result)], # None for success field to be set later
|
132
142
|
):
|
133
|
-
# skip success
|
134
|
-
if field
|
143
|
+
# skip success
|
144
|
+
if field == "success":
|
135
145
|
continue
|
136
146
|
data[field] = val
|
137
147
|
else:
|
138
|
-
only = next(k for k in outputs if k
|
148
|
+
only = next(k for k in outputs if k != "success")
|
139
149
|
data[only] = result
|
140
150
|
data["success"] = True
|
141
|
-
data["errors"] = []
|
142
151
|
return mutation_class(**data)
|
143
152
|
except Exception as e:
|
144
|
-
|
153
|
+
GraphQL._handleGraphQLError(e)
|
154
|
+
return mutation_class(**{"success": False})
|
145
155
|
|
146
156
|
# Assemble class dict
|
147
157
|
class_dict = {
|
{generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/measurement/measurement.py
RENAMED
@@ -22,20 +22,18 @@ for currency in currency_units:
|
|
22
22
|
class Measurement:
|
23
23
|
def __init__(self, value: Decimal | float | int | str, unit: str):
|
24
24
|
"""
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
unit: The unit of measurement as a string.
|
30
|
-
|
25
|
+
Initialize a Measurement with a numeric value and unit.
|
26
|
+
|
27
|
+
Creates a Measurement instance by converting the provided value to a Decimal and associating it with the specified unit. Accepts numeric types or strings convertible to Decimal.
|
28
|
+
|
31
29
|
Raises:
|
32
|
-
|
30
|
+
ValueError: If the value cannot be converted to a Decimal.
|
33
31
|
"""
|
34
32
|
if not isinstance(value, (Decimal, float, int)):
|
35
33
|
try:
|
36
34
|
value = Decimal(str(value))
|
37
35
|
except Exception:
|
38
|
-
raise
|
36
|
+
raise ValueError("Value must be a Decimal, float, int or compatible.")
|
39
37
|
if not isinstance(value, Decimal):
|
40
38
|
value = Decimal(str(value))
|
41
39
|
self.__quantity = ureg.Quantity(self.formatDecimal(value), unit)
|
@@ -228,15 +226,15 @@ class Measurement:
|
|
228
226
|
|
229
227
|
def __mul__(self, other: Any) -> Measurement:
|
230
228
|
"""
|
231
|
-
|
229
|
+
Multiplies this measurement by another measurement or a numeric value.
|
232
230
|
|
233
|
-
|
231
|
+
Multiplication between two currency measurements is not permitted. When multiplying by another measurement, the resulting measurement combines their units. When multiplying by a numeric value, only the magnitude is scaled.
|
234
232
|
|
235
233
|
Returns:
|
236
|
-
Measurement:
|
234
|
+
Measurement: A new measurement representing the product.
|
237
235
|
|
238
236
|
Raises:
|
239
|
-
TypeError: If both operands are currency measurements, or if the operand is
|
237
|
+
TypeError: If both operands are currency measurements, or if the operand is neither a Measurement nor a numeric value.
|
240
238
|
"""
|
241
239
|
if isinstance(other, Measurement):
|
242
240
|
if self.is_currency() and other.is_currency():
|
File without changes
|
{generalmanager-0.11.2 → generalmanager-0.12.0}/GeneralManager.egg-info/dependency_links.txt
RENAMED
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
|
{generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/bucket/calculationBucket.py
RENAMED
File without changes
|
{generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/bucket/databaseBucket.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/cache/dependencyIndex.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/factory/factoryMethods.py
RENAMED
File without changes
|
File without changes
|
{generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/interface/baseInterface.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/interface/databaseInterface.py
RENAMED
File without changes
|
File without changes
|
{generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/interface/readOnlyInterface.py
RENAMED
File without changes
|
File without changes
|
{generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/manager/generalManager.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/measurement/measurementField.py
RENAMED
File without changes
|
File without changes
|
{generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/permission/basePermission.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/permission/mutationPermission.py
RENAMED
File without changes
|
{generalmanager-0.11.2 → generalmanager-0.12.0}/src/general_manager/permission/permissionChecks.py
RENAMED
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|