GeneralManager 0.11.1__py3-none-any.whl → 0.12.0__py3-none-any.whl

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.
@@ -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
- Ermittelt die Filter, die auf Basis der read-Permission für den angegebenen
31
- Manager angewendet werden müssen.
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
- Erzeugt ein GraphQL-Mutation-Interface für die übergebene Manager-Klasse.
65
- Dabei werden:
66
- - Attribute aus dem Interface in Graphene-Felder abgebildet
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
- Generates and registers a GraphQL ObjectType for the specified GeneralManager class.
115
+ Creates and registers a GraphQL ObjectType for the given GeneralManager subclass.
106
116
 
107
- This method maps interface attributes and GraphQLProperty fields to Graphene fields, creates appropriate resolvers, registers the resulting type in the internal registry, and adds related query fields to the schema.
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
- Creates a Graphene Enum type representing sortable fields for a given GeneralManager class.
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 exist.
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 options, as well as specialized handling for Measurement attributes. Returns the generated InputObjectType or None if no filter fields are applicable.
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` types, returns a field with an optional `target_unit` argument. For `GeneralManager` subclasses, returns either a list field (with optional filtering, exclusion, sorting, pagination, and grouping arguments if available) or a single field, depending on the field name. For other types, returns the corresponding Graphene scalar field.
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
- Wendet Filter, Excludes, Sortierung und Paginierung auf das Queryset an.
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
- Create a resolver function for GraphQL list fields that retrieves and returns a queryset with permission filters, query filters, sorting, pagination, and optional grouping applied.
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
- Returns:
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 a queryset with permission filters, query filters, sorting, pagination, and optional grouping applied.
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, as a Graphene Enum.
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
- The resulting queryset after applying permissions, filters, sorting, pagination, and grouping.
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
- qs, filter, exclude, sort_by, reverse, page, page_size
417
- )
418
- if group_by is not None:
419
- if group_by == [""]:
420
- qs = qs.group_by()
421
- else:
422
- qs = qs.group_by(*group_by)
423
- return qs
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
- Erzeugt einen Resolver für Felder vom Typ Measurement.
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
- Wählt anhand des Feldtyps den passenden Resolver aus.
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
- Add list and single-item query fields for a GeneralManager subclass to the GraphQL schema.
573
+ Adds paginated list and single-item query fields for a GeneralManager subclass to the GraphQL schema.
484
574
 
485
- Registers a list query field with optional filtering, exclusion, sorting, pagination, and grouping arguments, as well as a single-item query field using identification fields from the manager's interface. Attaches the corresponding resolvers for both queries to the schema.
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
- list_field = graphene.List(
513
- graphene_type,
514
- **attributes,
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, and returns a dictionary containing a success flag, any errors, and the created instance. Returns None if the manager class does not define an interface.
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
- Create a new instance of the manager class using the provided arguments.
719
+ Creates a new instance of the manager class with the provided arguments.
629
720
 
630
- Filters out any fields with values set to `NOT_PROVIDED` before invoking the creation method. Returns a dictionary with a success flag, a list of errors if creation fails, and the created instance keyed by the manager class name.
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, calls 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, any error messages, and the updated instance. Returns None if the manager class does not define an `Interface`.
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 a GeneralManager instance with the provided fields.
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, typically containing user and request information.
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: A dictionary with keys 'success' (bool), 'errors' (list of str), and the updated instance keyed by its class name.
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 the operation's success status, any error messages, and the deactivated instance keyed by the class name.
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 the manager class does not define an interface.
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 operation result.
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: A dictionary with keys:
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 transforms a function into a GraphQL mutation class and registers it for use in a Graphene-based API.
25
-
26
- The decorated function must have type hints for all parameters (except `info`) and a return annotation. The decorator dynamically generates a mutation class with arguments and output fields based on the function's signature and return type. It also enforces authentication if `auth_required` is set to True, returning an error if the user is not authenticated.
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
- needs_role (Optional[str]): Reserved for future use to specify a required user role.
30
- auth_required (bool): If True, the mutation requires an authenticated user.
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, errors, + fn return types
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/errors
138
+ # unpack according to outputs ordering after success
130
139
  for (field, _), val in zip(
131
- outputs.items(), [None, None] + list(result)
140
+ outputs.items(),
141
+ [None, *list(result)], # None for success field to be set later
132
142
  ):
133
- # skip success/errors
134
- if field in ("success", "errors"):
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 not in ("success", "errors"))
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
- return mutation_class(**{"success": False, "errors": [str(e)]})
153
+ GraphQL._handleGraphQLError(e)
154
+ return mutation_class(**{"success": False})
145
155
 
146
156
  # Assemble class dict
147
157
  class_dict = {
@@ -17,12 +17,12 @@ modelsModel = TypeVar("modelsModel", bound=models.Model)
17
17
  def getFullCleanMethode(model: Type[models.Model]) -> Callable[..., None]:
18
18
  """
19
19
  Return a custom `full_clean` method for a Django model that performs both standard validation and additional rule-based checks.
20
-
20
+
21
21
  The generated method first applies Django's built-in model validation, then evaluates custom rules defined in the model's `_meta.rules` attribute. If any validation or rule fails, it raises a `ValidationError` containing all collected errors.
22
-
22
+
23
23
  Parameters:
24
24
  model (Type[models.Model]): The Django model class for which to generate the custom `full_clean` method.
25
-
25
+
26
26
  Returns:
27
27
  Callable[..., None]: A `full_clean` method that can be assigned to the model class.
28
28
  """
@@ -30,7 +30,7 @@ def getFullCleanMethode(model: Type[models.Model]) -> Callable[..., None]:
30
30
  def full_clean(self: models.Model, *args: Any, **kwargs: Any):
31
31
  """
32
32
  Performs full validation on the model instance, including both standard Django validation and custom rule-based checks.
33
-
33
+
34
34
  Aggregates errors from Django's built-in validation and any additional rules defined in the model's `_meta.rules` attribute. Raises a `ValidationError` containing all collected errors if any validation or rule check fails.
35
35
  """
36
36
  errors: dict[str, Any] = {}
@@ -41,7 +41,7 @@ def getFullCleanMethode(model: Type[models.Model]) -> Callable[..., None]:
41
41
 
42
42
  rules: list[Rule] = getattr(self._meta, "rules")
43
43
  for rule in rules:
44
- if not rule.evaluate(self):
44
+ if rule.evaluate(self) is False:
45
45
  error_message = rule.getErrorMessage()
46
46
  if error_message:
47
47
  errors.update(error_message)
@@ -78,7 +78,7 @@ class GeneralManagerModel(GeneralManagerBasisModel):
78
78
  def _history_user(self, value: AbstractUser) -> None:
79
79
  """
80
80
  Set the user responsible for the most recent change to the model instance.
81
-
81
+
82
82
  Parameters:
83
83
  value (AbstractUser): The user to associate with the latest modification.
84
84
  """
@@ -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
- Create a Measurement with the specified numeric value and unit.
26
-
27
- Parameters:
28
- value: The numeric value to associate with the unit. Accepts Decimal, float, int, or a string convertible to Decimal.
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
- TypeError: If the value cannot be converted to a Decimal.
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 TypeError("Value must be a Decimal, float, int or compatible.")
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
- Multiply this measurement by another measurement or a numeric value.
229
+ Multiplies this measurement by another measurement or a numeric value.
232
230
 
233
- Multiplying two currency measurements is not allowed. When multiplying by another measurement, the resulting measurement combines their units. When multiplying by a numeric value, only the magnitude is scaled.
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: The result of the multiplication.
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 not a Measurement or numeric value.
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():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.11.1
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,7 +1,7 @@
1
1
  general_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  general_manager/apps.py,sha256=ii-48klC2kBRjxtdQU9-RtPq66lZ8fDa7BmqyH-mbzk,9904
3
- general_manager/api/graphql.py,sha256=jpdtaep_ql94krsVvGJRpuqT-MR6iZCFpezeJAl4Mlw,33382
4
- general_manager/api/mutation.py,sha256=41BvkY4qYU_Kq2R6GUK14AnU7sSTydw4tSw1f4CAlxI,5954
3
+ general_manager/api/graphql.py,sha256=xNL2jt2x2U7x7UIFfoDm6zxH-gag2yWS3MbOu9ccIEQ,39033
4
+ general_manager/api/mutation.py,sha256=9T892INzH4ip5sX3_BxB2WX145zt8S_0HGVPhGzyXZA,6736
5
5
  general_manager/api/property.py,sha256=oc93p1P8dcIvrNorRuqD1EJVsd6eYttYhZuAS0s28gs,696
6
6
  general_manager/bucket/baseBucket.py,sha256=UjEai7cVxgDJysoVY2-7s3YULW7bi-sx2mPqRKFWyTw,7728
7
7
  general_manager/bucket/calculationBucket.py,sha256=a41YVdfhxKf_gpWlRKjXYmS1YuO-6VC0hn60RyLKByU,18777
@@ -21,7 +21,7 @@ general_manager/interface/baseInterface.py,sha256=cFsDU-nhj_O6Gir3eO0ukGKNn9Pplh
21
21
  general_manager/interface/calculationInterface.py,sha256=fTD3WQpsn3ImaxGW5S-JwVJyJJPoPp2mR6lAambdB8U,4755
22
22
  general_manager/interface/databaseBasedInterface.py,sha256=F-a6P9rvPUmlcoZw1rKSWEAzbYGccQRL6uokv6C46qo,21782
23
23
  general_manager/interface/databaseInterface.py,sha256=rhKVXhg0ztdIxKikTWtgjrkA7cwZTOYlEsRh0RWajDQ,6732
24
- general_manager/interface/models.py,sha256=gGYW5f1AUBpBakV3O0qsZwqMiWxZGdKRYXWaCBjt1oI,3334
24
+ general_manager/interface/models.py,sha256=P4bYb-Q7-moZR-hgIqwruMuftDgUW6FSqjPtWfHW04o,3311
25
25
  general_manager/interface/readOnlyInterface.py,sha256=TkfbOeaa2wCq5kCv0a3IwJWcYOTVbtNsdNWmGAz0Mns,11217
26
26
  general_manager/manager/__init__.py,sha256=l3RYp62aEhj3Y975_XUTIzo35LUnkTJHkb_hgChnXXI,111
27
27
  general_manager/manager/generalManager.py,sha256=4Qn9TYpZpqh5qC95BEAQhpiZgDrRXrAJjO2BbbXUdNg,9129
@@ -29,7 +29,7 @@ general_manager/manager/groupManager.py,sha256=8dpZUfm7aFL4lraUWv4qbbDRClQZaYxw4
29
29
  general_manager/manager/input.py,sha256=-pJXGJ-g2-OxZfl4Buj3mQkf05fN4p8MsR2Lh9BQcEo,3208
30
30
  general_manager/manager/meta.py,sha256=-9celpo-oZmkTb8TnHfvcd_4XWTy1cn2UO-jp13NFmQ,6387
31
31
  general_manager/measurement/__init__.py,sha256=X97meFujBldE5v0WMF7SmKeGpC5R0JTczfLo_Lq1Xek,84
32
- general_manager/measurement/measurement.py,sha256=T3R2qqDkqPY_91DzN0zJF4ug62aCNyN5-75WE9Tm7ko,16067
32
+ general_manager/measurement/measurement.py,sha256=YaGcYS1ZyHMEPp8b_RojXU9GMVk7NCvlPwWdTdtgRR0,16090
33
33
  general_manager/measurement/measurementField.py,sha256=ixkZR_t--HGckK2iYi6sXOOa_Vbni4QRxK5Ngmy5YKc,6863
34
34
  general_manager/permission/__init__.py,sha256=5UlDERN60Vn8obGVkT-cOM8kHjzmoxgK5w5FgTCDhGE,59
35
35
  general_manager/permission/basePermission.py,sha256=DeiAX2sQQhtdquO13jys2MSkp0kPdg2oo7PSqB9q5Bw,5653
@@ -51,8 +51,8 @@ general_manager/utils/makeCacheKey.py,sha256=UlFsxHXgsYy69AAelkF6GDvY4h7AImT2bBn
51
51
  general_manager/utils/noneToZero.py,sha256=KfQtMQnrT6vsYST0K7lv6pVujkDcK3XL8czHYOhgqKQ,539
52
52
  general_manager/utils/pathMapping.py,sha256=nrz5owQg2a69Yig1eCXorR9U0NSw7NmBAk5OkeoHTdA,6842
53
53
  general_manager/utils/testing.py,sha256=ElZ8p4iZHxsHjDN8Lm5TmI6527CW747ltDOmtY6gAhk,11872
54
- generalmanager-0.11.1.dist-info/licenses/LICENSE,sha256=YGFm0ieb4KpkMRRt2qnWue6uFh0cUMtobwEBkHwajhc,1450
55
- generalmanager-0.11.1.dist-info/METADATA,sha256=Sam_MBu9NlrJgwolfC2jmDM5eq8kHOMXy-bhdyiEXkE,6206
56
- generalmanager-0.11.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
57
- generalmanager-0.11.1.dist-info/top_level.txt,sha256=sTDtExP9ga-YP3h3h42yivUY-A2Q23C2nw6LNKOho4I,16
58
- generalmanager-0.11.1.dist-info/RECORD,,
54
+ generalmanager-0.12.0.dist-info/licenses/LICENSE,sha256=YGFm0ieb4KpkMRRt2qnWue6uFh0cUMtobwEBkHwajhc,1450
55
+ generalmanager-0.12.0.dist-info/METADATA,sha256=I7psaouHX8DtSRATISXfZRFECK-1x4srKdSe2CvuW2Q,6206
56
+ generalmanager-0.12.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
57
+ generalmanager-0.12.0.dist-info/top_level.txt,sha256=sTDtExP9ga-YP3h3h42yivUY-A2Q23C2nw6LNKOho4I,16
58
+ generalmanager-0.12.0.dist-info/RECORD,,