GeneralManager 0.6.2__tar.gz → 0.8.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.
Files changed (96) hide show
  1. {generalmanager-0.6.2 → generalmanager-0.8.0}/GeneralManager.egg-info/PKG-INFO +1 -1
  2. {generalmanager-0.6.2 → generalmanager-0.8.0}/GeneralManager.egg-info/SOURCES.txt +3 -0
  3. {generalmanager-0.6.2 → generalmanager-0.8.0}/PKG-INFO +1 -1
  4. {generalmanager-0.6.2 → generalmanager-0.8.0}/pyproject.toml +1 -1
  5. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/api/graphql.py +117 -65
  6. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/api/mutation.py +11 -10
  7. generalmanager-0.8.0/src/general_manager/apps.py +183 -0
  8. generalmanager-0.8.0/src/general_manager/auxiliary/formatString.py +60 -0
  9. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/cache/dependencyIndex.py +10 -2
  10. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/interface/baseInterface.py +9 -9
  11. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/interface/databaseBasedInterface.py +55 -32
  12. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/interface/databaseInterface.py +43 -13
  13. generalmanager-0.8.0/src/general_manager/interface/readOnlyInterface.py +272 -0
  14. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/manager/generalManager.py +73 -14
  15. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/manager/input.py +9 -12
  16. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/manager/meta.py +17 -11
  17. generalmanager-0.8.0/tests/test_formatString.py +61 -0
  18. generalmanager-0.8.0/tests/test_graph_ql.py +641 -0
  19. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_input.py +50 -39
  20. generalmanager-0.8.0/tests/test_readOnlyInterface.py +370 -0
  21. generalmanager-0.6.2/src/general_manager/apps.py +0 -83
  22. generalmanager-0.6.2/src/general_manager/interface/readOnlyInterface.py +0 -107
  23. generalmanager-0.6.2/tests/test_graph_ql.py +0 -215
  24. {generalmanager-0.6.2 → generalmanager-0.8.0}/GeneralManager.egg-info/dependency_links.txt +0 -0
  25. {generalmanager-0.6.2 → generalmanager-0.8.0}/GeneralManager.egg-info/requires.txt +0 -0
  26. {generalmanager-0.6.2 → generalmanager-0.8.0}/GeneralManager.egg-info/top_level.txt +0 -0
  27. {generalmanager-0.6.2 → generalmanager-0.8.0}/LICENSE +0 -0
  28. {generalmanager-0.6.2 → generalmanager-0.8.0}/README.md +0 -0
  29. {generalmanager-0.6.2 → generalmanager-0.8.0}/setup.cfg +0 -0
  30. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/__init__.py +0 -0
  31. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/api/property.py +0 -0
  32. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/auxiliary/__init__.py +0 -0
  33. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/auxiliary/argsToKwargs.py +0 -0
  34. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/auxiliary/filterParser.py +0 -0
  35. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/auxiliary/jsonEncoder.py +0 -0
  36. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/auxiliary/makeCacheKey.py +0 -0
  37. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/auxiliary/noneToZero.py +0 -0
  38. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/auxiliary/pathMapping.py +0 -0
  39. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/bucket/baseBucket.py +0 -0
  40. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/bucket/calculationBucket.py +0 -0
  41. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/bucket/databaseBucket.py +0 -0
  42. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/bucket/groupBucket.py +0 -0
  43. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/cache/cacheDecorator.py +0 -0
  44. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/cache/cacheTracker.py +0 -0
  45. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/cache/modelDependencyCollector.py +0 -0
  46. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/cache/signals.py +0 -0
  47. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/factory/__init__.py +0 -0
  48. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/factory/autoFactory.py +0 -0
  49. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/factory/factories.py +0 -0
  50. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/factory/factoryMethods.py +0 -0
  51. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/interface/__init__.py +0 -0
  52. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/interface/calculationInterface.py +0 -0
  53. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/manager/__init__.py +0 -0
  54. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/manager/groupManager.py +0 -0
  55. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/measurement/__init__.py +0 -0
  56. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/measurement/measurement.py +0 -0
  57. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/measurement/measurementField.py +0 -0
  58. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/permission/__init__.py +0 -0
  59. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/permission/basePermission.py +0 -0
  60. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/permission/fileBasedPermission.py +0 -0
  61. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/permission/managerBasedPermission.py +0 -0
  62. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/permission/permissionChecks.py +0 -0
  63. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/permission/permissionDataManager.py +0 -0
  64. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/rule/__init__.py +0 -0
  65. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/rule/handler.py +0 -0
  66. {generalmanager-0.6.2 → generalmanager-0.8.0}/src/general_manager/rule/rule.py +0 -0
  67. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_argsToKwargs.py +0 -0
  68. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_autoFactory.py +0 -0
  69. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_baseBucket.py +0 -0
  70. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_baseInterface.py +0 -0
  71. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_basePermission.py +0 -0
  72. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_cacheDecorator.py +0 -0
  73. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_cacheTracker.py +0 -0
  74. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_calculationBucket.py +0 -0
  75. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_calculationInterface.py +0 -0
  76. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_databaseBasedInterface.py +0 -0
  77. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_databaseBucket.py +0 -0
  78. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_databaseInterface.py +0 -0
  79. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_dependencyIndex.py +0 -0
  80. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_factories.py +0 -0
  81. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_factoryMethods.py +0 -0
  82. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_filterParser.py +0 -0
  83. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_generalManager.py +0 -0
  84. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_generalManagerMeta.py +0 -0
  85. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_groupManager.py +0 -0
  86. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_jsonEncoder.py +0 -0
  87. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_makeCacheKey.py +0 -0
  88. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_managerBasedPermission.py +0 -0
  89. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_measurement.py +0 -0
  90. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_measurement_field.py +0 -0
  91. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_modelDependencyCollector.py +0 -0
  92. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_noneToZero.py +0 -0
  93. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_rule_handler.py +0 -0
  94. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_rules.py +0 -0
  95. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_settings.py +0 -0
  96. {generalmanager-0.6.2 → generalmanager-0.8.0}/tests/test_signals.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.6.2
3
+ Version: 0.8.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
@@ -14,6 +14,7 @@ src/general_manager/api/property.py
14
14
  src/general_manager/auxiliary/__init__.py
15
15
  src/general_manager/auxiliary/argsToKwargs.py
16
16
  src/general_manager/auxiliary/filterParser.py
17
+ src/general_manager/auxiliary/formatString.py
17
18
  src/general_manager/auxiliary/jsonEncoder.py
18
19
  src/general_manager/auxiliary/makeCacheKey.py
19
20
  src/general_manager/auxiliary/noneToZero.py
@@ -70,6 +71,7 @@ tests/test_dependencyIndex.py
70
71
  tests/test_factories.py
71
72
  tests/test_factoryMethods.py
72
73
  tests/test_filterParser.py
74
+ tests/test_formatString.py
73
75
  tests/test_generalManager.py
74
76
  tests/test_generalManagerMeta.py
75
77
  tests/test_graph_ql.py
@@ -82,6 +84,7 @@ tests/test_measurement.py
82
84
  tests/test_measurement_field.py
83
85
  tests/test_modelDependencyCollector.py
84
86
  tests/test_noneToZero.py
87
+ tests/test_readOnlyInterface.py
85
88
  tests/test_rule_handler.py
86
89
  tests/test_rules.py
87
90
  tests/test_settings.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.6.2
3
+ Version: 0.8.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.6.2"
7
+ version = "0.8.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" }]
@@ -5,19 +5,19 @@ from decimal import Decimal
5
5
  from datetime import date, datetime
6
6
  import json
7
7
 
8
- # Eigene Module
9
8
  from general_manager.measurement.measurement import Measurement
10
9
  from general_manager.manager.generalManager import GeneralManagerMeta, GeneralManager
11
10
  from general_manager.api.property import GraphQLProperty
12
11
  from general_manager.bucket.baseBucket import Bucket
13
12
  from general_manager.interface.baseInterface import InterfaceBase
13
+ from django.db.models import NOT_PROVIDED
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from general_manager.permission.basePermission import BasePermission
17
17
  from graphene import ResolveInfo as GraphQLResolveInfo
18
18
 
19
19
 
20
- class MeasurementType(graphene.ObjectType): # type: ignore
20
+ class MeasurementType(graphene.ObjectType):
21
21
  value = graphene.Float()
22
22
  unit = graphene.String()
23
23
 
@@ -102,11 +102,9 @@ class GraphQL:
102
102
  @classmethod
103
103
  def createGraphqlInterface(cls, generalManagerClass: GeneralManagerMeta) -> None:
104
104
  """
105
- Erzeugt ein GraphQL-Interface für die übergebene Manager-Klasse.
106
- Dabei werden:
107
- - Attribute aus dem Interface in Graphene-Felder abgebildet
108
- - Zu jedem Feld ein Resolver generiert und hinzugefügt
109
- - Der neue Type in das Registry eingetragen und Queries angehängt.
105
+ Generates and registers a GraphQL ObjectType for the specified GeneralManager class.
106
+
107
+ This method maps interface attributes and GraphQLProperty attributes to Graphene fields, creates corresponding resolvers, registers the resulting type in the internal registry, and attaches relevant query fields to the schema.
110
108
  """
111
109
  interface_cls: InterfaceBase | None = getattr(
112
110
  generalManagerClass, "Interface", None
@@ -117,14 +115,14 @@ class GraphQL:
117
115
  graphene_type_name = f"{generalManagerClass.__name__}Type"
118
116
  fields: dict[str, Any] = {}
119
117
 
120
- # Felder aus dem Interface mappen
118
+ # Map Attribute Types to Graphene Fields
121
119
  for field_name, field_info in interface_cls.getAttributeTypes().items():
122
120
  field_type = field_info["type"]
123
121
  fields[field_name] = cls._mapFieldToGrapheneRead(field_type, field_name)
124
122
  resolver_name = f"resolve_{field_name}"
125
123
  fields[resolver_name] = cls._createResolver(field_name, field_type)
126
124
 
127
- # Zusätzliche GraphQLPropertys verarbeiten
125
+ # handle GraphQLProperty attributes
128
126
  for attr_name, attr_value in generalManagerClass.__dict__.items():
129
127
  if isinstance(attr_value, GraphQLProperty):
130
128
  type_hints = get_args(attr_value.graphql_type_hint)
@@ -350,10 +348,14 @@ class GraphQL:
350
348
  base_getter: Callable[[Any], Any], fallback_manager_class: type[GeneralManager]
351
349
  ) -> Callable[..., Any]:
352
350
  """
353
- Erzeugt einen Resolver für List-Felder, der:
354
- - Eine Basisabfrage (base_queryset) über den base_getter ermittelt
355
- - Zuerst die permission-basierten Filter anwendet
356
- - Anschließend Filter, Excludes, Sortierung und Paginierung übernimmt
351
+ Creates a resolver function for list fields that applies permission filters, query filters, sorting, pagination, and optional grouping to a queryset.
352
+
353
+ Parameters:
354
+ base_getter (Callable): Function to obtain the base queryset from the parent instance.
355
+ fallback_manager_class (type[GeneralManager]): Manager class to use if the queryset does not specify one.
356
+
357
+ Returns:
358
+ Callable: A resolver function for use in GraphQL list fields.
357
359
  """
358
360
 
359
361
  def resolver(
@@ -367,8 +369,23 @@ class GraphQL:
367
369
  page_size: int | None = None,
368
370
  group_by: list[str] | None = None,
369
371
  ) -> Any:
372
+ """
373
+ Resolves a list field by applying permission filters, query parameters, sorting, pagination, and optional grouping to a queryset.
374
+
375
+ Parameters:
376
+ filter: Optional filter criteria as a dictionary or JSON string.
377
+ exclude: Optional exclusion criteria as a dictionary or JSON string.
378
+ sort_by: Optional sorting field as a Graphene Enum.
379
+ reverse: If True, reverses the sort order.
380
+ page: Optional page number for pagination.
381
+ page_size: Optional number of items per page.
382
+ group_by: Optional list of field names to group results by.
383
+
384
+ Returns:
385
+ The filtered, sorted, paginated, and optionally grouped queryset.
386
+ """
370
387
  base_queryset = base_getter(self)
371
- # Verwende _manager_class aus dem Attribut falls vorhanden, ansonsten das Fallback
388
+ # use _manager_class from the attribute if available, otherwise fallback
372
389
  manager_class = getattr(
373
390
  base_queryset, "_manager_class", fallback_manager_class
374
391
  )
@@ -441,8 +458,9 @@ class GraphQL:
441
458
  cls, graphene_type: type, generalManagerClass: GeneralManagerMeta
442
459
  ) -> None:
443
460
  """
444
- Fügt dem Schema Abfragen hinzu (Liste und Einzelobjekt) basierend auf der
445
- GeneralManager-Klasse.
461
+ Adds list and single-item query fields for a GeneralManager-derived class to the GraphQL schema.
462
+
463
+ This method registers both a list query (with filtering, sorting, pagination, and grouping) and a single-item query (using identification fields) for the specified manager class. The corresponding resolvers are also attached to the schema.
446
464
  """
447
465
  if not issubclass(generalManagerClass, GeneralManager):
448
466
  raise TypeError(
@@ -452,7 +470,7 @@ class GraphQL:
452
470
  if not hasattr(cls, "_query_fields"):
453
471
  cls._query_fields: dict[str, Any] = {}
454
472
 
455
- # Resolver und Feld für die Listenabfrage
473
+ # resolver and field for the list query
456
474
  list_field_name = f"{generalManagerClass.__name__.lower()}_list"
457
475
  filter_options = cls._createFilterOptions(
458
476
  generalManagerClass.__name__.lower(), generalManagerClass
@@ -475,7 +493,7 @@ class GraphQL:
475
493
  cls._query_fields[list_field_name] = list_field
476
494
  cls._query_fields[f"resolve_{list_field_name}"] = list_resolver
477
495
 
478
- # Resolver und Feld für die Einzelobjektabfrage
496
+ # resolver and field for the single item query
479
497
  item_field_name = generalManagerClass.__name__.lower()
480
498
  identification_fields = {}
481
499
  for (
@@ -505,11 +523,21 @@ class GraphQL:
505
523
 
506
524
  @classmethod
507
525
  def createWriteFields(cls, interface_cls: InterfaceBase) -> dict[str, Any]:
526
+ """
527
+ Generate a dictionary of Graphene input fields for mutations based on the attributes of the provided interface class.
528
+
529
+ Skips fields that are system-managed (`changed_by`, `created_at`, `updated_at`) or marked as derived. For attributes referencing `GeneralManager` subclasses, uses ID fields; for list references, uses a list of IDs. All other types are mapped to their corresponding Graphene scalar types. Each field is annotated with an `editable` attribute indicating if it can be modified. Adds an optional `history_comment` field marked as editable.
530
+
531
+ Returns:
532
+ dict[str, Any]: A dictionary mapping attribute names to Graphene input fields for use in mutation arguments.
533
+ """
508
534
  fields: dict[str, Any] = {}
509
535
 
510
536
  for name, info in interface_cls.getAttributeTypes().items():
511
537
  if name in ["changed_by", "created_at", "updated_at"]:
512
538
  continue
539
+ if info["is_derived"]:
540
+ continue
513
541
 
514
542
  typ = info["type"]
515
543
  req = info["is_required"]
@@ -534,11 +562,11 @@ class GraphQL:
534
562
  default_value=default,
535
563
  )
536
564
 
537
- # Markierung, damit Dein generate*-Code weiß, was editable ist
565
+ # mark for generate* code to know what is editable
538
566
  setattr(fld, "editable", info["is_editable"])
539
567
  fields[name] = fld
540
568
 
541
- # history_comment bleibt optional ohne Default
569
+ # history_comment is always optional without a default value
542
570
  fields["history_comment"] = graphene.String()
543
571
  setattr(fields["history_comment"], "editable", True)
544
572
 
@@ -551,7 +579,12 @@ class GraphQL:
551
579
  default_return_values: dict[str, Any],
552
580
  ) -> type[graphene.Mutation] | None:
553
581
  """
554
- Generiert eine Mutation-Klasse für die Erstellung eines Objekts.
582
+ Generates a Graphene mutation class for creating an instance of the specified GeneralManager subclass.
583
+
584
+ The generated mutation class defines a `mutate` method that filters out fields with `NOT_PROVIDED` values, invokes the `create` method on the manager class with the provided arguments and the current user's ID, and returns a dictionary indicating success or failure along with any errors and the created instance.
585
+
586
+ Returns:
587
+ The generated Graphene mutation class, or None if the manager class does not define an interface.
555
588
  """
556
589
  interface_cls: InterfaceBase | None = getattr(
557
590
  generalManagerClass, "Interface", None
@@ -563,26 +596,32 @@ class GraphQL:
563
596
  self,
564
597
  info: GraphQLResolveInfo,
565
598
  **kwargs: dict[str, Any],
566
- ) -> GeneralManager:
599
+ ) -> dict:
600
+ """
601
+ Creates a new instance of the specified manager class using provided input arguments.
602
+
603
+ Filters out fields with default "not provided" values before creation. Returns a dictionary indicating success status, any errors encountered, and the created instance under a key named after the manager class.
604
+ """
567
605
  try:
606
+ kwargs = {
607
+ field_name: value
608
+ for field_name, value in kwargs.items()
609
+ if value is not NOT_PROVIDED
610
+ }
568
611
  instance = generalManagerClass.create(
569
612
  **kwargs, creator_id=info.context.user.id
570
613
  )
571
614
  except Exception as e:
572
- return self.__class__(
573
- **{
574
- "success": False,
575
- "errors": [str(e)],
576
- generalManagerClass.__name__: None,
577
- }
578
- )
579
- return self.__class__(
580
- **{
581
- "success": True,
582
- "errors": [],
583
- generalManagerClass.__name__: instance,
615
+ return {
616
+ "success": False,
617
+ "errors": [str(e)],
584
618
  }
585
- )
619
+
620
+ return {
621
+ "success": True,
622
+ "errors": [],
623
+ generalManagerClass.__name__: instance,
624
+ }
586
625
 
587
626
  return type(
588
627
  f"Create{generalManagerClass.__name__}",
@@ -612,7 +651,12 @@ class GraphQL:
612
651
  default_return_values: dict[str, Any],
613
652
  ) -> type[graphene.Mutation] | None:
614
653
  """
615
- Generiert eine Mutation-Klasse für die Aktualisierung eines Objekts.
654
+ Generates a GraphQL mutation class for updating an instance of a GeneralManager subclass.
655
+
656
+ The generated mutation accepts editable fields as arguments, calls the `update` method on the manager instance, and returns a dictionary indicating success, errors, and the updated instance. If the manager class does not define an `Interface`, returns None.
657
+
658
+ Returns:
659
+ The generated Graphene mutation class, or None if no interface is defined.
616
660
  """
617
661
  interface_cls: InterfaceBase | None = getattr(
618
662
  generalManagerClass, "Interface", None
@@ -624,27 +668,32 @@ class GraphQL:
624
668
  self,
625
669
  info: GraphQLResolveInfo,
626
670
  **kwargs: dict[str, Any],
627
- ) -> GeneralManager:
671
+ ) -> dict:
672
+ """
673
+ Handles the update mutation for a GeneralManager instance, applying provided field updates and returning the operation result.
674
+
675
+ Parameters:
676
+ info (GraphQLResolveInfo): GraphQL resolver context containing user and request information.
677
+ **kwargs: Fields to update, including the required 'id' of the instance.
678
+
679
+ Returns:
680
+ dict: A dictionary containing the success status, any error messages, and the updated instance keyed by its class name.
681
+ """
628
682
  try:
629
683
  manager_id = kwargs.pop("id", None)
630
684
  instance = generalManagerClass(manager_id).update(
631
685
  creator_id=info.context.user.id, **kwargs
632
686
  )
633
687
  except Exception as e:
634
- return self.__class__(
635
- **{
636
- "success": False,
637
- "errors": [str(e)],
638
- generalManagerClass.__name__: None,
639
- }
640
- )
641
- return self.__class__(
642
- **{
643
- "success": True,
644
- "errors": [],
645
- generalManagerClass.__name__: instance,
688
+ return {
689
+ "success": False,
690
+ "errors": [str(e)],
646
691
  }
647
- )
692
+ return {
693
+ "success": True,
694
+ "errors": [],
695
+ generalManagerClass.__name__: instance,
696
+ }
648
697
 
649
698
  return type(
650
699
  f"Create{generalManagerClass.__name__}",
@@ -674,7 +723,9 @@ class GraphQL:
674
723
  default_return_values: dict[str, Any],
675
724
  ) -> type[graphene.Mutation] | None:
676
725
  """
677
- Generiert eine Mutation-Klasse für die Löschung eines Objekts.
726
+ Generates a GraphQL mutation class for deleting (deactivating) an instance of a GeneralManager subclass.
727
+
728
+ The generated mutation accepts input fields defined in the manager's interface, deactivates the specified instance, and returns a dictionary indicating success or failure, along with any errors and the deleted instance.
678
729
  """
679
730
  interface_cls: InterfaceBase | None = getattr(
680
731
  generalManagerClass, "Interface", None
@@ -686,27 +737,28 @@ class GraphQL:
686
737
  self,
687
738
  info: GraphQLResolveInfo,
688
739
  **kwargs: dict[str, Any],
689
- ) -> GeneralManager:
740
+ ) -> dict:
741
+ """
742
+ Deletes (deactivates) an instance of the specified GeneralManager class and returns the operation result.
743
+
744
+ Returns:
745
+ dict: A dictionary containing the success status, any error messages, and the deactivated instance under the class name key.
746
+ """
690
747
  try:
691
748
  manager_id = kwargs.pop("id", None)
692
749
  instance = generalManagerClass(manager_id).deactivate(
693
750
  creator_id=info.context.user.id
694
751
  )
695
752
  except Exception as e:
696
- return self.__class__(
697
- **{
698
- "success": False,
699
- "errors": [str(e)],
700
- generalManagerClass.__name__: None,
701
- }
702
- )
703
- return self.__class__(
704
- **{
705
- "success": True,
706
- "errors": [],
707
- generalManagerClass.__name__: instance,
753
+ return {
754
+ "success": False,
755
+ "errors": [str(e)],
708
756
  }
709
- )
757
+ return {
758
+ "success": True,
759
+ "errors": [],
760
+ generalManagerClass.__name__: instance,
761
+ }
710
762
 
711
763
  return type(
712
764
  f"Delete{generalManagerClass.__name__}",
@@ -5,20 +5,21 @@ import graphene
5
5
  from general_manager.api.graphql import GraphQL
6
6
  from general_manager.manager.generalManager import GeneralManager
7
7
 
8
-
9
- def snake_to_pascal(s: str) -> str:
10
- return "".join(p.title() for p in s.split("_"))
11
-
12
-
13
- def snake_to_camel(s: str) -> str:
14
- parts = s.split("_")
15
- return parts[0] + "".join(p.title() for p in parts[1:])
8
+ from general_manager.auxiliary.formatString import snake_to_camel
16
9
 
17
10
 
18
11
  def graphQlMutation(needs_role: Optional[str] = None, auth_required: bool = False):
19
12
  """
20
- Decorator to generate a graphene.Mutation from a function and register it.
21
- :param auth_required: if True, enforces that info.context.user is authenticated.
13
+ Decorator that transforms a function into a GraphQL mutation class and registers it for use in a Graphene-based API.
14
+
15
+ 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.
16
+
17
+ Parameters:
18
+ needs_role (Optional[str]): Reserved for future use to specify a required user role.
19
+ auth_required (bool): If True, the mutation requires an authenticated user.
20
+
21
+ Returns:
22
+ Callable: A decorator that registers the mutation and returns the original function.
22
23
  """
23
24
 
24
25
  def decorator(fn):
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+ from django.apps import AppConfig
3
+ import graphene
4
+ import os
5
+ from django.conf import settings
6
+ from django.urls import path
7
+ from graphene_django.views import GraphQLView
8
+ from importlib import import_module
9
+ from general_manager.manager.generalManager import GeneralManager
10
+ from general_manager.manager.meta import GeneralManagerMeta
11
+ from general_manager.manager.input import Input
12
+ from general_manager.api.property import graphQlProperty
13
+ from general_manager.api.graphql import GraphQL
14
+ from typing import TYPE_CHECKING, Type, Any, cast
15
+ from django.core.checks import register
16
+ import logging
17
+ from django.core.management.base import BaseCommand
18
+
19
+
20
+ if TYPE_CHECKING:
21
+ from general_manager.interface.readOnlyInterface import ReadOnlyInterface
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class GeneralmanagerConfig(AppConfig):
27
+ default_auto_field = "django.db.models.BigAutoField"
28
+ name = "general_manager"
29
+
30
+ def ready(self):
31
+ """
32
+ Initializes the general_manager app on Django startup.
33
+
34
+ Sets up read-only interface synchronization and schema validation, initializes general manager class attributes and interconnections, and configures the GraphQL schema and endpoint if enabled in settings.
35
+ """
36
+ self.handleReadOnlyInterface()
37
+ self.initializeGeneralManagerClasses()
38
+ if getattr(settings, "AUTOCREATE_GRAPHQL", False):
39
+ self.handleGraphQL()
40
+
41
+ def handleReadOnlyInterface(self):
42
+ """
43
+ Configures synchronization and schema validation for all registered read-only interfaces.
44
+
45
+ This method ensures that read-only interfaces are synchronized before Django management commands execute and registers system checks to validate that each read-only interface's schema remains current.
46
+ """
47
+ self.patchReadOnlyInterfaceSync(GeneralManagerMeta.read_only_classes)
48
+ from general_manager.interface.readOnlyInterface import ReadOnlyInterface
49
+
50
+ logger.debug("starting to register ReadOnlyInterface schema warnings...")
51
+ for general_manager_class in GeneralManagerMeta.read_only_classes:
52
+ read_only_interface = cast(
53
+ Type[ReadOnlyInterface], general_manager_class.Interface
54
+ )
55
+
56
+ register(
57
+ lambda app_configs, model=read_only_interface._model, manager_class=general_manager_class, **kwargs: ReadOnlyInterface.ensureSchemaIsUpToDate(
58
+ manager_class, model
59
+ ),
60
+ "general_manager",
61
+ )
62
+
63
+ @staticmethod
64
+ def patchReadOnlyInterfaceSync(
65
+ general_manager_classes: list[Type[GeneralManager[Any, ReadOnlyInterface]]],
66
+ ):
67
+ """
68
+ Monkey-patches Django's management command runner to synchronize data for all provided read-only interfaces before executing management commands.
69
+
70
+ For each general manager class, its associated read-only interface's `syncData` method is called prior to command execution, except during autoreload subprocesses of `runserver`.
71
+ """
72
+ from general_manager.interface.readOnlyInterface import ReadOnlyInterface
73
+
74
+ original_run_from_argv = BaseCommand.run_from_argv
75
+
76
+ def run_from_argv_with_sync(self, argv):
77
+ # Ensure syncData is only called at real run of runserver
78
+ """
79
+ Executes a Django management command, synchronizing all registered read-only interfaces before execution unless running an autoreload subprocess of 'runserver'.
80
+
81
+ Parameters:
82
+ argv (list): Command-line arguments for the management command.
83
+
84
+ Returns:
85
+ The result of the original management command execution.
86
+ """
87
+ run_main = os.environ.get("RUN_MAIN") == "true"
88
+ command = argv[1] if len(argv) > 1 else None
89
+ if command != "runserver" or run_main:
90
+ logger.debug("start syncing ReadOnlyInterface data...")
91
+ for general_manager_class in general_manager_classes:
92
+ read_only_interface = cast(
93
+ Type[ReadOnlyInterface], general_manager_class.Interface
94
+ )
95
+ read_only_interface.syncData()
96
+
97
+ logger.debug("finished syncing ReadOnlyInterface data.")
98
+
99
+ return original_run_from_argv(self, argv)
100
+
101
+ BaseCommand.run_from_argv = run_from_argv_with_sync
102
+
103
+ def initializeGeneralManagerClasses(self):
104
+ """
105
+ Initializes attributes and sets up dynamic relationships for all registered GeneralManager classes.
106
+
107
+ For each pending GeneralManager class, assigns its interface attributes and creates property accessors. Then, for all GeneralManager classes, dynamically connects input fields that reference other GeneralManager subclasses by adding GraphQL properties to enable filtered access to related objects.
108
+ """
109
+ logger.debug("Initializing GeneralManager classes...")
110
+
111
+ logger.debug("starting to create attributes for GeneralManager classes...")
112
+ for (
113
+ general_manager_class
114
+ ) in GeneralManagerMeta.pending_attribute_initialization:
115
+ attributes = general_manager_class.Interface.getAttributes()
116
+ setattr(general_manager_class, "_attributes", attributes)
117
+ GeneralManagerMeta.createAtPropertiesForAttributes(
118
+ attributes.keys(), general_manager_class
119
+ )
120
+
121
+ logger.debug("starting to connect inputs to other general manager classes...")
122
+ for general_manager_class in GeneralManagerMeta.all_classes:
123
+ attributes = getattr(general_manager_class.Interface, "input_fields", {})
124
+ for attribute_name, attribute in attributes.items():
125
+ if isinstance(attribute, Input) and issubclass(
126
+ attribute.type, GeneralManager
127
+ ):
128
+ connected_manager = attribute.type
129
+ func = lambda x, attribute_name=attribute_name: general_manager_class.filter(
130
+ **{attribute_name: x}
131
+ )
132
+
133
+ func.__annotations__ = {"return": general_manager_class}
134
+ setattr(
135
+ connected_manager,
136
+ f"{general_manager_class.__name__.lower()}_list",
137
+ graphQlProperty(func),
138
+ )
139
+
140
+ def handleGraphQL(self):
141
+ """
142
+ Sets up GraphQL interfaces, mutations, and schema for all pending general manager classes, and adds the GraphQL endpoint to the Django URL configuration.
143
+ """
144
+ logger.debug("Starting to create GraphQL interfaces and mutations...")
145
+ for general_manager_class in GeneralManagerMeta.pending_graphql_interfaces:
146
+ GraphQL.createGraphqlInterface(general_manager_class)
147
+ GraphQL.createGraphqlMutation(general_manager_class)
148
+
149
+ query_class = type("Query", (graphene.ObjectType,), GraphQL._query_fields)
150
+ GraphQL._query_class = query_class
151
+
152
+ mutation_class = type(
153
+ "Mutation",
154
+ (graphene.ObjectType,),
155
+ {name: mutation.Field() for name, mutation in GraphQL._mutations.items()},
156
+ )
157
+ GraphQL._mutation_class = mutation_class
158
+
159
+ schema = graphene.Schema(
160
+ query=GraphQL._query_class,
161
+ mutation=GraphQL._mutation_class,
162
+ )
163
+ self.addGraphqlUrl(schema)
164
+
165
+ def addGraphqlUrl(self, schema):
166
+ """
167
+ Dynamically appends a GraphQL endpoint to the Django URL configuration using the given schema.
168
+
169
+ Raises:
170
+ Exception: If the ROOT_URLCONF setting is not defined in Django settings.
171
+ """
172
+ logging.debug("Adding GraphQL URL to Django settings...")
173
+ root_url_conf_path = getattr(settings, "ROOT_URLCONF", None)
174
+ graph_ql_url = getattr(settings, "GRAPHQL_URL", "graphql/")
175
+ if not root_url_conf_path:
176
+ raise Exception("ROOT_URLCONF not found in settings")
177
+ urlconf = import_module(root_url_conf_path)
178
+ urlconf.urlpatterns.append(
179
+ path(
180
+ graph_ql_url,
181
+ GraphQLView.as_view(graphiql=True, schema=schema),
182
+ )
183
+ )
@@ -0,0 +1,60 @@
1
+ def snake_to_pascal(s: str) -> str:
2
+ """
3
+ Convert a snake_case string to PascalCase.
4
+
5
+ Parameters:
6
+ s (str): The input string in snake_case format.
7
+
8
+ Returns:
9
+ str: The converted string in PascalCase format.
10
+ """
11
+ return "".join(p.title() for p in s.split("_"))
12
+
13
+
14
+ def snake_to_camel(s: str) -> str:
15
+ """
16
+ Convert a snake_case string to camelCase.
17
+
18
+ Parameters:
19
+ s (str): The snake_case string to convert.
20
+
21
+ Returns:
22
+ str: The input string converted to camelCase.
23
+ """
24
+ parts = s.split("_")
25
+ return parts[0] + "".join(p.title() for p in parts[1:])
26
+
27
+
28
+ def pascal_to_snake(s: str) -> str:
29
+ """
30
+ Convert a PascalCase string to snake_case.
31
+
32
+ Parameters:
33
+ s (str): The PascalCase string to convert.
34
+
35
+ Returns:
36
+ str: The converted snake_case string.
37
+ """
38
+ return "".join(["_" + c.lower() if c.isupper() else c for c in s]).lstrip("_")
39
+
40
+
41
+ def camel_to_snake(s: str) -> str:
42
+ """
43
+ Convert a camelCase string to snake_case.
44
+
45
+ Parameters:
46
+ s (str): The camelCase string to convert.
47
+
48
+ Returns:
49
+ str: The converted snake_case string. Returns an empty string if the input is empty.
50
+ """
51
+ if not s:
52
+ return ""
53
+ parts = [s[0].lower()]
54
+ for c in s[1:]:
55
+ if c.isupper():
56
+ parts.append("_")
57
+ parts.append(c.lower())
58
+ else:
59
+ parts.append(c)
60
+ return "".join(parts)