GeneralManager 0.13.0__tar.gz → 0.14.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 (67) hide show
  1. {generalmanager-0.13.0 → generalmanager-0.14.0}/GeneralManager.egg-info/PKG-INFO +1 -1
  2. {generalmanager-0.13.0 → generalmanager-0.14.0}/PKG-INFO +1 -1
  3. {generalmanager-0.13.0 → generalmanager-0.14.0}/pyproject.toml +1 -1
  4. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/api/graphql.py +162 -67
  5. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/api/mutation.py +15 -7
  6. generalmanager-0.14.0/src/general_manager/api/property.py +105 -0
  7. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/apps.py +14 -11
  8. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/bucket/calculationBucket.py +203 -45
  9. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/bucket/databaseBucket.py +149 -17
  10. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/interface/baseInterface.py +32 -10
  11. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/permission/mutationPermission.py +3 -1
  12. generalmanager-0.14.0/src/general_manager/utils/argsToKwargs.py +25 -0
  13. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/utils/pathMapping.py +12 -11
  14. generalmanager-0.13.0/src/general_manager/api/property.py +0 -20
  15. generalmanager-0.13.0/src/general_manager/utils/argsToKwargs.py +0 -25
  16. {generalmanager-0.13.0 → generalmanager-0.14.0}/GeneralManager.egg-info/SOURCES.txt +0 -0
  17. {generalmanager-0.13.0 → generalmanager-0.14.0}/GeneralManager.egg-info/dependency_links.txt +0 -0
  18. {generalmanager-0.13.0 → generalmanager-0.14.0}/GeneralManager.egg-info/requires.txt +0 -0
  19. {generalmanager-0.13.0 → generalmanager-0.14.0}/GeneralManager.egg-info/top_level.txt +0 -0
  20. {generalmanager-0.13.0 → generalmanager-0.14.0}/LICENSE +0 -0
  21. {generalmanager-0.13.0 → generalmanager-0.14.0}/README.md +0 -0
  22. {generalmanager-0.13.0 → generalmanager-0.14.0}/setup.cfg +0 -0
  23. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/__init__.py +0 -0
  24. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/bucket/baseBucket.py +0 -0
  25. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/bucket/groupBucket.py +0 -0
  26. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/cache/cacheDecorator.py +0 -0
  27. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/cache/cacheTracker.py +0 -0
  28. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/cache/dependencyIndex.py +0 -0
  29. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/cache/modelDependencyCollector.py +0 -0
  30. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/cache/signals.py +0 -0
  31. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/factory/__init__.py +0 -0
  32. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/factory/autoFactory.py +0 -0
  33. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/factory/factories.py +0 -0
  34. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/factory/factoryMethods.py +0 -0
  35. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/interface/__init__.py +0 -0
  36. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/interface/calculationInterface.py +0 -0
  37. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/interface/databaseBasedInterface.py +0 -0
  38. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/interface/databaseInterface.py +0 -0
  39. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/interface/models.py +0 -0
  40. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/interface/readOnlyInterface.py +0 -0
  41. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/manager/__init__.py +0 -0
  42. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/manager/generalManager.py +0 -0
  43. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/manager/groupManager.py +0 -0
  44. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/manager/input.py +0 -0
  45. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/manager/meta.py +0 -0
  46. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/measurement/__init__.py +0 -0
  47. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/measurement/measurement.py +0 -0
  48. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/measurement/measurementField.py +0 -0
  49. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/permission/__init__.py +0 -0
  50. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/permission/basePermission.py +0 -0
  51. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/permission/fileBasedPermission.py +0 -0
  52. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/permission/managerBasedPermission.py +0 -0
  53. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/permission/permissionChecks.py +0 -0
  54. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/permission/permissionDataManager.py +0 -0
  55. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/permission/utils.py +0 -0
  56. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/rule/__init__.py +0 -0
  57. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/rule/handler.py +0 -0
  58. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/rule/rule.py +0 -0
  59. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/utils/__init__.py +0 -0
  60. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/utils/filterParser.py +0 -0
  61. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/utils/formatString.py +0 -0
  62. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/utils/jsonEncoder.py +0 -0
  63. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/utils/makeCacheKey.py +0 -0
  64. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/utils/noneToZero.py +0 -0
  65. {generalmanager-0.13.0 → generalmanager-0.14.0}/src/general_manager/utils/testing.py +0 -0
  66. {generalmanager-0.13.0 → generalmanager-0.14.0}/tests/test_settings.py +0 -0
  67. {generalmanager-0.13.0 → generalmanager-0.14.0}/tests/test_urls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.13.0
3
+ Version: 0.14.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.13.0
3
+ Version: 0.14.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.13.0"
7
+ version = "0.14.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" }]
@@ -1,6 +1,18 @@
1
1
  from __future__ import annotations
2
2
  import graphene
3
- from typing import Any, Callable, get_args, TYPE_CHECKING, cast, Type
3
+ from typing import (
4
+ Any,
5
+ Callable,
6
+ get_args,
7
+ get_origin,
8
+ TYPE_CHECKING,
9
+ cast,
10
+ Type,
11
+ Generator,
12
+ Union,
13
+ )
14
+ from types import UnionType
15
+
4
16
  from decimal import Decimal
5
17
  from datetime import date, datetime
6
18
  import json
@@ -156,18 +168,45 @@ class GraphQL:
156
168
  fields[resolver_name] = cls._createResolver(field_name, field_type)
157
169
 
158
170
  # handle GraphQLProperty attributes
159
- for attr_name, attr_value in generalManagerClass.__dict__.items():
160
- if isinstance(attr_value, GraphQLProperty):
161
- type_hints = get_args(attr_value.graphql_type_hint)
162
- field_type = (
163
- type_hints[0]
164
- if type_hints
165
- else cast(type, attr_value.graphql_type_hint)
171
+ for (
172
+ attr_name,
173
+ attr_value,
174
+ ) in generalManagerClass.Interface.getGraphQLProperties().items():
175
+ raw_hint = attr_value.graphql_type_hint
176
+ origin = get_origin(raw_hint)
177
+ type_args = [t for t in get_args(raw_hint) if t is not type(None)]
178
+
179
+ if origin in (Union, UnionType) and type_args:
180
+ raw_hint = type_args[0]
181
+ origin = get_origin(raw_hint)
182
+ type_args = [t for t in get_args(raw_hint) if t is not type(None)]
183
+
184
+ if origin in (list, tuple, set):
185
+ element = type_args[0] if type_args else Any
186
+ if isinstance(element, type) and issubclass(element, GeneralManager): # type: ignore
187
+ graphene_field = graphene.List(
188
+ lambda elem=element: GraphQL.graphql_type_registry[
189
+ elem.__name__
190
+ ]
191
+ )
192
+ else:
193
+ base_type = GraphQL._mapFieldToGrapheneBaseType(
194
+ cast(type, element if isinstance(element, type) else str)
195
+ )
196
+ graphene_field = graphene.List(base_type)
197
+ resolved_type = cast(
198
+ type, element if isinstance(element, type) else str
166
199
  )
167
- fields[attr_name] = cls._mapFieldToGrapheneRead(field_type, attr_name)
168
- fields[f"resolve_{attr_name}"] = cls._createResolver(
169
- attr_name, field_type
200
+ else:
201
+ resolved_type = (
202
+ cast(type, type_args[0]) if type_args else cast(type, raw_hint)
170
203
  )
204
+ graphene_field = cls._mapFieldToGrapheneRead(resolved_type, attr_name)
205
+
206
+ fields[attr_name] = graphene_field
207
+ fields[f"resolve_{attr_name}"] = cls._createResolver(
208
+ attr_name, resolved_type
209
+ )
171
210
 
172
211
  graphene_type = type(graphene_type_name, (graphene.ObjectType,), fields)
173
212
  cls.graphql_type_registry[generalManagerClass.__name__] = graphene_type
@@ -194,6 +233,20 @@ class GraphQL:
194
233
  else:
195
234
  sort_options.append(field_name)
196
235
 
236
+ for (
237
+ prop_name,
238
+ prop,
239
+ ) in generalManagerClass.Interface.getGraphQLProperties().items():
240
+ if prop.sortable is False:
241
+ continue
242
+ type_hints = [
243
+ t for t in get_args(prop.graphql_type_hint) if t is not type(None)
244
+ ]
245
+ field_type = (
246
+ type_hints[0] if type_hints else cast(type, prop.graphql_type_hint)
247
+ )
248
+ sort_options.append(prop_name)
249
+
197
250
  if not sort_options:
198
251
  return None
199
252
 
@@ -203,9 +256,54 @@ class GraphQL:
203
256
  {option: option for option in sort_options},
204
257
  )
205
258
 
259
+ @staticmethod
260
+ def _getFilterOptions(attribute_type: type, attribute_name: str) -> Generator[
261
+ tuple[
262
+ str, type[graphene.ObjectType] | MeasurementScalar | graphene.List | None
263
+ ],
264
+ None,
265
+ None,
266
+ ]:
267
+ number_options = ["exact", "gt", "gte", "lt", "lte"]
268
+ string_options = [
269
+ "exact",
270
+ "icontains",
271
+ "contains",
272
+ "in",
273
+ "startswith",
274
+ "endswith",
275
+ ]
276
+
277
+ if issubclass(attribute_type, GeneralManager):
278
+ yield attribute_name, None
279
+ elif issubclass(attribute_type, Measurement):
280
+ yield attribute_name, MeasurementScalar()
281
+ for option in number_options:
282
+ yield f"{attribute_name}__{option}", MeasurementScalar()
283
+ else:
284
+ yield attribute_name, GraphQL._mapFieldToGrapheneRead(
285
+ attribute_type, attribute_name
286
+ )
287
+ if issubclass(attribute_type, (int, float, Decimal, date, datetime)):
288
+ for option in number_options:
289
+ yield f"{attribute_name}__{option}", (
290
+ GraphQL._mapFieldToGrapheneRead(attribute_type, attribute_name)
291
+ )
292
+ elif issubclass(attribute_type, str):
293
+ base_type = GraphQL._mapFieldToGrapheneBaseType(attribute_type)
294
+ for option in string_options:
295
+ if option == "in":
296
+ yield f"{attribute_name}__in", graphene.List(base_type)
297
+ else:
298
+ yield f"{attribute_name}__{option}", (
299
+ GraphQL._mapFieldToGrapheneRead(
300
+ attribute_type, attribute_name
301
+ )
302
+ )
303
+
206
304
  @staticmethod
207
305
  def _createFilterOptions(
208
- field_name: str, field_type: GeneralManagerMeta
306
+ field_type: GeneralManagerMeta,
209
307
  ) -> type[graphene.InputObjectType] | None:
210
308
  """
211
309
  Dynamically generates a Graphene InputObjectType for filtering fields of a GeneralManager subclass.
@@ -213,21 +311,11 @@ class GraphQL:
213
311
  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.
214
312
 
215
313
  Parameters:
216
- field_name (str): The name of the field to generate filter options for.
217
314
  field_type (GeneralManagerMeta): The manager class whose attributes are used to build filter fields.
218
315
 
219
316
  Returns:
220
317
  type[graphene.InputObjectType] | None: The generated filter input type, or None if no filter fields are applicable.
221
318
  """
222
- number_options = ["exact", "gt", "gte", "lt", "lte"]
223
- string_options = [
224
- "exact",
225
- "icontains",
226
- "contains",
227
- "in",
228
- "startswith",
229
- "endswith",
230
- ]
231
319
 
232
320
  graphene_filter_type_name = f"{field_type.__name__}FilterType"
233
321
  if graphene_filter_type_name in GraphQL.graphql_filter_type_registry:
@@ -236,26 +324,28 @@ class GraphQL:
236
324
  filter_fields = {}
237
325
  for attr_name, attr_info in field_type.Interface.getAttributeTypes().items():
238
326
  attr_type = attr_info["type"]
239
- if issubclass(attr_type, GeneralManager):
327
+ filter_fields = {
328
+ **filter_fields,
329
+ **{
330
+ k: v
331
+ for k, v in GraphQL._getFilterOptions(attr_type, attr_name)
332
+ if v is not None
333
+ },
334
+ }
335
+ for prop_name, prop in field_type.Interface.getGraphQLProperties().items():
336
+ if not prop.filterable:
240
337
  continue
241
- elif issubclass(attr_type, Measurement):
242
- filter_fields[f"{attr_name}"] = MeasurementScalar()
243
- for option in number_options:
244
- filter_fields[f"{attr_name}__{option}"] = MeasurementScalar()
245
- else:
246
- filter_fields[attr_name] = GraphQL._mapFieldToGrapheneRead(
247
- attr_type, attr_name
248
- )
249
- if issubclass(attr_type, (int, float, Decimal, date, datetime)):
250
- for option in number_options:
251
- filter_fields[f"{attr_name}__{option}"] = (
252
- GraphQL._mapFieldToGrapheneRead(attr_type, attr_name)
253
- )
254
- elif issubclass(attr_type, str):
255
- for option in string_options:
256
- filter_fields[f"{attr_name}__{option}"] = (
257
- GraphQL._mapFieldToGrapheneRead(attr_type, attr_name)
258
- )
338
+ hints = [t for t in get_args(prop.graphql_type_hint) if t is not type(None)]
339
+ prop_type = hints[0] if hints else cast(type, prop.graphql_type_hint)
340
+ filter_fields = {
341
+ **filter_fields,
342
+ **{
343
+ k: v
344
+ for k, v in GraphQL._getFilterOptions(prop_type, prop_name)
345
+ if v is not None
346
+ },
347
+ }
348
+
259
349
  if not filter_fields:
260
350
  return None
261
351
 
@@ -284,14 +374,14 @@ class GraphQL:
284
374
  "page_size": graphene.Int(),
285
375
  "group_by": graphene.List(graphene.String),
286
376
  }
287
- filter_options = GraphQL._createFilterOptions(field_name, field_type)
377
+ filter_options = GraphQL._createFilterOptions(field_type)
288
378
  if filter_options:
289
- attributes["filter"] = filter_options()
290
- attributes["exclude"] = filter_options()
379
+ attributes["filter"] = graphene.Argument(filter_options)
380
+ attributes["exclude"] = graphene.Argument(filter_options)
291
381
 
292
382
  sort_by_options = GraphQL._sortByOptions(field_type)
293
383
  if sort_by_options:
294
- attributes["sort_by"] = sort_by_options()
384
+ attributes["sort_by"] = graphene.Argument(sort_by_options)
295
385
 
296
386
  page_type = GraphQL._getOrCreatePageType(
297
387
  field_type.__name__ + "Page",
@@ -299,10 +389,6 @@ class GraphQL:
299
389
  )
300
390
  return graphene.Field(page_type, **attributes)
301
391
 
302
- return graphene.List(
303
- lambda: GraphQL.graphql_type_registry[field_type.__name__],
304
- **attributes,
305
- )
306
392
  return graphene.Field(
307
393
  lambda: GraphQL.graphql_type_registry[field_type.__name__]
308
394
  )
@@ -314,6 +400,8 @@ class GraphQL:
314
400
  """
315
401
  Ordnet einen Python-Typ einem entsprechenden Graphene-Feld zu.
316
402
  """
403
+ if issubclass(field_type, dict):
404
+ raise TypeError("GraphQL does not support dict fields")
317
405
  if issubclass(field_type, str):
318
406
  return graphene.String
319
407
  elif issubclass(field_type, bool):
@@ -322,7 +410,9 @@ class GraphQL:
322
410
  return graphene.Int
323
411
  elif issubclass(field_type, (float, Decimal)):
324
412
  return graphene.Float
325
- elif issubclass(field_type, (date, datetime)):
413
+ elif issubclass(field_type, datetime):
414
+ return graphene.DateTime
415
+ elif issubclass(field_type, date):
326
416
  return graphene.Date
327
417
  elif issubclass(field_type, Measurement):
328
418
  return MeasurementScalar
@@ -385,7 +475,7 @@ class GraphQL:
385
475
  ) -> Bucket:
386
476
  """
387
477
  Applies permission-based filters to a queryset according to the permission interface of the given manager class.
388
-
478
+
389
479
  Returns:
390
480
  A queryset containing only the items allowed by the user's read permissions. If no permission filters are defined, returns the original queryset unchanged.
391
481
  """
@@ -541,7 +631,7 @@ class GraphQL:
541
631
  result = result.to(target_unit)
542
632
  return {
543
633
  "value": result.quantity.magnitude,
544
- "unit": result.quantity.units,
634
+ "unit": str(result.quantity.units),
545
635
  }
546
636
 
547
637
  return resolver
@@ -621,15 +711,13 @@ class GraphQL:
621
711
  "page_size": graphene.Int(),
622
712
  "group_by": graphene.List(graphene.String),
623
713
  }
624
- filter_options = cls._createFilterOptions(
625
- generalManagerClass.__name__.lower(), generalManagerClass
626
- )
714
+ filter_options = cls._createFilterOptions(generalManagerClass)
627
715
  if filter_options:
628
- attributes["filter"] = filter_options()
629
- attributes["exclude"] = filter_options()
716
+ attributes["filter"] = graphene.Argument(filter_options)
717
+ attributes["exclude"] = graphene.Argument(filter_options)
630
718
  sort_by_options = cls._sortByOptions(generalManagerClass)
631
719
  if sort_by_options:
632
- attributes["sort_by"] = sort_by_options()
720
+ attributes["sort_by"] = graphene.Argument(sort_by_options)
633
721
 
634
722
  page_type = cls._getOrCreatePageType(
635
723
  graphene_type.__name__ + "Page", graphene_type
@@ -829,7 +917,9 @@ class GraphQL:
829
917
  """
830
918
  try:
831
919
  manager_id = kwargs.pop("id", None)
832
- instance = generalManagerClass(manager_id).update(
920
+ if manager_id is None:
921
+ raise ValueError("id is required")
922
+ instance = generalManagerClass(id=manager_id).update(
833
923
  creator_id=info.context.user.id, **kwargs
834
924
  )
835
925
  except Exception as e:
@@ -844,7 +934,7 @@ class GraphQL:
844
934
  }
845
935
 
846
936
  return type(
847
- f"Create{generalManagerClass.__name__}",
937
+ f"Update{generalManagerClass.__name__}",
848
938
  (graphene.Mutation,),
849
939
  {
850
940
  **default_return_values,
@@ -853,11 +943,14 @@ class GraphQL:
853
943
  "Arguments",
854
944
  (),
855
945
  {
856
- field_name: field
857
- for field_name, field in cls.createWriteFields(
858
- interface_cls
859
- ).items()
860
- if field.editable
946
+ "id": graphene.ID(required=True),
947
+ **{
948
+ field_name: field
949
+ for field_name, field in cls.createWriteFields(
950
+ interface_cls
951
+ ).items()
952
+ if field.editable
953
+ },
861
954
  },
862
955
  ),
863
956
  "mutate": update_mutation,
@@ -897,7 +990,9 @@ class GraphQL:
897
990
  """
898
991
  try:
899
992
  manager_id = kwargs.pop("id", None)
900
- instance = generalManagerClass(manager_id).deactivate(
993
+ if manager_id is None:
994
+ raise ValueError("id is required")
995
+ instance = generalManagerClass(id=manager_id).deactivate(
901
996
  creator_id=info.context.user.id
902
997
  )
903
998
  except Exception as e:
@@ -946,7 +1041,7 @@ class GraphQL:
946
1041
  "code": "PERMISSION_DENIED",
947
1042
  },
948
1043
  )
949
- elif isinstance(error, (ValueError, ValidationError)):
1044
+ elif isinstance(error, (ValueError, ValidationError, TypeError)):
950
1045
  raise GraphQLError(
951
1046
  str(error),
952
1047
  extensions={
@@ -19,23 +19,30 @@ from typing import TypeAliasType
19
19
  from general_manager.permission.mutationPermission import MutationPermission
20
20
 
21
21
 
22
- def graphQlMutation(permission: Optional[Type[MutationPermission]] = None):
22
+ def graphQlMutation(_func=None, permission: Optional[Type[MutationPermission]] = None):
23
23
  """
24
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
-
25
+
26
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
-
27
+
28
28
  Parameters:
29
29
  permission (Optional[Type[MutationPermission]]): An optional permission class to enforce access control on the mutation.
30
-
30
+
31
31
  Returns:
32
32
  Callable: A decorator that registers the mutation and returns the original function.
33
33
  """
34
+ if (
35
+ _func is not None
36
+ and inspect.isclass(_func)
37
+ and issubclass(_func, MutationPermission)
38
+ ):
39
+ permission = _func
40
+ _func = None
34
41
 
35
42
  def decorator(fn):
36
43
  """
37
44
  Decorator that transforms a function into a GraphQL mutation class compatible with Graphene.
38
-
45
+
39
46
  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
47
  """
41
48
  sig = inspect.signature(fn)
@@ -122,10 +129,9 @@ def graphQlMutation(permission: Optional[Type[MutationPermission]] = None):
122
129
 
123
130
  # Define mutate method
124
131
  def _mutate(root, info, **kwargs):
125
-
126
132
  """
127
133
  Handles the execution of a GraphQL mutation, including permission checks, result unpacking, and error handling.
128
-
134
+
129
135
  Returns:
130
136
  An instance of the mutation class with output fields populated from the mutation result and a success status.
131
137
  """
@@ -169,4 +175,6 @@ def graphQlMutation(permission: Optional[Type[MutationPermission]] = None):
169
175
 
170
176
  return fn
171
177
 
178
+ if _func is not None and inspect.isfunction(_func):
179
+ return decorator(_func)
172
180
  return decorator
@@ -0,0 +1,105 @@
1
+ from typing import Any, Callable, get_type_hints, overload, TypeVar
2
+ import sys
3
+
4
+ T = TypeVar("T", bound=Callable[..., Any])
5
+
6
+
7
+ class GraphQLProperty(property):
8
+ sortable: bool
9
+ filterable: bool
10
+ query_annotation: Any | None
11
+
12
+ def __init__(
13
+ self,
14
+ fget: Callable[..., Any],
15
+ doc: str | None = None,
16
+ *,
17
+ sortable: bool = False,
18
+ filterable: bool = False,
19
+ query_annotation: Any | None = None,
20
+ ) -> None:
21
+ super().__init__(fget, doc=doc)
22
+ self.is_graphql_resolver = True
23
+ self._owner: type | None = None
24
+ self._name: str | None = None
25
+ self._graphql_type_hint: Any | None = None
26
+
27
+ self.sortable = sortable
28
+ self.filterable = filterable
29
+ self.query_annotation = query_annotation
30
+
31
+ orig = getattr(
32
+ fget, "__wrapped__", fget
33
+ ) # falls decorator Annotations durchreicht
34
+ ann = getattr(orig, "__annotations__", {}) or {}
35
+ if "return" not in ann:
36
+ raise TypeError(
37
+ "GraphQLProperty requires a return type hint for the property function."
38
+ )
39
+
40
+ def __set_name__(self, owner: type, name: str) -> None:
41
+ self._owner = owner
42
+ self._name = name
43
+
44
+ def _try_resolve_type_hint(self) -> None:
45
+ if self._graphql_type_hint is not None:
46
+ return
47
+
48
+ try:
49
+ mod = sys.modules.get(self.fget.__module__)
50
+ globalns = vars(mod) if mod else {}
51
+
52
+ localns: dict[str, Any] = {}
53
+ if self._owner is not None:
54
+ localns = dict(self._owner.__dict__)
55
+ localns[self._owner.__name__] = self._owner
56
+
57
+ hints = get_type_hints(self.fget, globalns=globalns, localns=localns)
58
+ self._graphql_type_hint = hints.get("return", None)
59
+ except Exception:
60
+ self._graphql_type_hint = None
61
+
62
+ @property
63
+ def graphql_type_hint(self) -> Any | None:
64
+ if self._graphql_type_hint is None:
65
+ self._try_resolve_type_hint()
66
+ return self._graphql_type_hint
67
+
68
+
69
+ @overload
70
+ def graphQlProperty(func: T) -> GraphQLProperty: ...
71
+ @overload
72
+ def graphQlProperty(
73
+ *,
74
+ sortable: bool = False,
75
+ filterable: bool = False,
76
+ query_annotation: Any | None = None,
77
+ ) -> Callable[[T], GraphQLProperty]: ...
78
+
79
+
80
+ def graphQlProperty(
81
+ func: Callable[..., Any] | None = None,
82
+ *,
83
+ sortable: bool = False,
84
+ filterable: bool = False,
85
+ query_annotation: Any | None = None,
86
+ ) -> GraphQLProperty | Callable[[T], GraphQLProperty]:
87
+ from general_manager.cache.cacheDecorator import cached
88
+
89
+ """Decorator to create a :class:`GraphQLProperty`.
90
+
91
+ It can be used without arguments or with optional configuration for
92
+ filtering, sorting and queryset annotation.
93
+ """
94
+
95
+ def wrapper(f: Callable[..., Any]) -> GraphQLProperty:
96
+ return GraphQLProperty(
97
+ cached()(f),
98
+ sortable=sortable,
99
+ query_annotation=query_annotation,
100
+ filterable=filterable,
101
+ )
102
+
103
+ if func is None:
104
+ return wrapper
105
+ return wrapper(func)
@@ -162,17 +162,20 @@ class GeneralmanagerConfig(AppConfig):
162
162
  query_class = type("Query", (graphene.ObjectType,), GraphQL._query_fields)
163
163
  GraphQL._query_class = query_class
164
164
 
165
- mutation_class = type(
166
- "Mutation",
167
- (graphene.ObjectType,),
168
- {name: mutation.Field() for name, mutation in GraphQL._mutations.items()},
169
- )
170
- GraphQL._mutation_class = mutation_class
171
-
172
- schema = graphene.Schema(
173
- query=GraphQL._query_class,
174
- mutation=GraphQL._mutation_class,
175
- )
165
+ if GraphQL._mutations:
166
+ mutation_class = type(
167
+ "Mutation",
168
+ (graphene.ObjectType,),
169
+ {name: mutation.Field() for name, mutation in GraphQL._mutations.items()},
170
+ )
171
+ GraphQL._mutation_class = mutation_class
172
+ schema = graphene.Schema(
173
+ query=GraphQL._query_class,
174
+ mutation=mutation_class,
175
+ )
176
+ else:
177
+ GraphQL._mutation_class = None
178
+ schema = graphene.Schema(query=GraphQL._query_class)
176
179
  GeneralmanagerConfig.addGraphqlUrl(schema)
177
180
 
178
181
  @staticmethod