GeneralManager 0.9.0__tar.gz → 0.10.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 (63) hide show
  1. {generalmanager-0.9.0 → generalmanager-0.10.0}/GeneralManager.egg-info/PKG-INFO +1 -1
  2. {generalmanager-0.9.0 → generalmanager-0.10.0}/GeneralManager.egg-info/SOURCES.txt +8 -8
  3. {generalmanager-0.9.0 → generalmanager-0.10.0}/PKG-INFO +1 -1
  4. {generalmanager-0.9.0 → generalmanager-0.10.0}/pyproject.toml +1 -1
  5. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/api/mutation.py +4 -4
  6. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/apps.py +31 -8
  7. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/bucket/calculationBucket.py +11 -6
  8. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/cache/cacheDecorator.py +1 -1
  9. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/interface/baseInterface.py +1 -1
  10. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/interface/databaseBasedInterface.py +7 -7
  11. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/interface/databaseInterface.py +4 -1
  12. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/manager/generalManager.py +20 -22
  13. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/manager/meta.py +7 -5
  14. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/measurement/measurement.py +126 -51
  15. {generalmanager-0.9.0/src/general_manager/auxiliary → generalmanager-0.10.0/src/general_manager/utils}/makeCacheKey.py +4 -4
  16. {generalmanager-0.9.0 → generalmanager-0.10.0}/GeneralManager.egg-info/dependency_links.txt +0 -0
  17. {generalmanager-0.9.0 → generalmanager-0.10.0}/GeneralManager.egg-info/requires.txt +0 -0
  18. {generalmanager-0.9.0 → generalmanager-0.10.0}/GeneralManager.egg-info/top_level.txt +0 -0
  19. {generalmanager-0.9.0 → generalmanager-0.10.0}/LICENSE +0 -0
  20. {generalmanager-0.9.0 → generalmanager-0.10.0}/README.md +0 -0
  21. {generalmanager-0.9.0 → generalmanager-0.10.0}/setup.cfg +0 -0
  22. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/__init__.py +0 -0
  23. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/api/graphql.py +0 -0
  24. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/api/property.py +0 -0
  25. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/bucket/baseBucket.py +0 -0
  26. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/bucket/databaseBucket.py +0 -0
  27. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/bucket/groupBucket.py +0 -0
  28. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/cache/cacheTracker.py +0 -0
  29. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/cache/dependencyIndex.py +0 -0
  30. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/cache/modelDependencyCollector.py +0 -0
  31. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/cache/signals.py +0 -0
  32. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/factory/__init__.py +0 -0
  33. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/factory/autoFactory.py +0 -0
  34. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/factory/factories.py +0 -0
  35. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/factory/factoryMethods.py +0 -0
  36. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/interface/__init__.py +0 -0
  37. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/interface/calculationInterface.py +0 -0
  38. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/interface/models.py +0 -0
  39. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/interface/readOnlyInterface.py +0 -0
  40. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/manager/__init__.py +0 -0
  41. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/manager/groupManager.py +0 -0
  42. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/manager/input.py +0 -0
  43. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/measurement/__init__.py +0 -0
  44. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/measurement/measurementField.py +0 -0
  45. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/permission/__init__.py +0 -0
  46. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/permission/basePermission.py +0 -0
  47. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/permission/fileBasedPermission.py +0 -0
  48. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/permission/managerBasedPermission.py +0 -0
  49. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/permission/permissionChecks.py +0 -0
  50. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/permission/permissionDataManager.py +0 -0
  51. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/rule/__init__.py +0 -0
  52. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/rule/handler.py +0 -0
  53. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/rule/rule.py +0 -0
  54. {generalmanager-0.9.0/src/general_manager/auxiliary → generalmanager-0.10.0/src/general_manager/utils}/__init__.py +0 -0
  55. {generalmanager-0.9.0/src/general_manager/auxiliary → generalmanager-0.10.0/src/general_manager/utils}/argsToKwargs.py +0 -0
  56. {generalmanager-0.9.0/src/general_manager/auxiliary → generalmanager-0.10.0/src/general_manager/utils}/filterParser.py +0 -0
  57. {generalmanager-0.9.0/src/general_manager/auxiliary → generalmanager-0.10.0/src/general_manager/utils}/formatString.py +0 -0
  58. {generalmanager-0.9.0/src/general_manager/auxiliary → generalmanager-0.10.0/src/general_manager/utils}/jsonEncoder.py +0 -0
  59. {generalmanager-0.9.0/src/general_manager/auxiliary → generalmanager-0.10.0/src/general_manager/utils}/noneToZero.py +0 -0
  60. {generalmanager-0.9.0/src/general_manager/auxiliary → generalmanager-0.10.0/src/general_manager/utils}/pathMapping.py +0 -0
  61. {generalmanager-0.9.0 → generalmanager-0.10.0}/src/general_manager/utils/testing.py +0 -0
  62. {generalmanager-0.9.0 → generalmanager-0.10.0}/tests/test_settings.py +0 -0
  63. {generalmanager-0.9.0 → generalmanager-0.10.0}/tests/test_urls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.9.0
3
+ Version: 0.10.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
@@ -11,14 +11,6 @@ src/general_manager/apps.py
11
11
  src/general_manager/api/graphql.py
12
12
  src/general_manager/api/mutation.py
13
13
  src/general_manager/api/property.py
14
- src/general_manager/auxiliary/__init__.py
15
- src/general_manager/auxiliary/argsToKwargs.py
16
- src/general_manager/auxiliary/filterParser.py
17
- src/general_manager/auxiliary/formatString.py
18
- src/general_manager/auxiliary/jsonEncoder.py
19
- src/general_manager/auxiliary/makeCacheKey.py
20
- src/general_manager/auxiliary/noneToZero.py
21
- src/general_manager/auxiliary/pathMapping.py
22
14
  src/general_manager/bucket/baseBucket.py
23
15
  src/general_manager/bucket/calculationBucket.py
24
16
  src/general_manager/bucket/databaseBucket.py
@@ -56,6 +48,14 @@ src/general_manager/permission/permissionDataManager.py
56
48
  src/general_manager/rule/__init__.py
57
49
  src/general_manager/rule/handler.py
58
50
  src/general_manager/rule/rule.py
51
+ src/general_manager/utils/__init__.py
52
+ src/general_manager/utils/argsToKwargs.py
53
+ src/general_manager/utils/filterParser.py
54
+ src/general_manager/utils/formatString.py
55
+ src/general_manager/utils/jsonEncoder.py
56
+ src/general_manager/utils/makeCacheKey.py
57
+ src/general_manager/utils/noneToZero.py
58
+ src/general_manager/utils/pathMapping.py
59
59
  src/general_manager/utils/testing.py
60
60
  tests/test_settings.py
61
61
  tests/test_urls.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.9.0
3
+ Version: 0.10.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.9.0"
7
+ version = "0.10.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 @@ import graphene
5
5
  from general_manager.api.graphql import GraphQL
6
6
  from general_manager.manager.generalManager import GeneralManager
7
7
 
8
- from general_manager.auxiliary.formatString import snake_to_camel
8
+ from general_manager.utils.formatString import snake_to_camel
9
9
 
10
10
 
11
11
  def graphQlMutation(needs_role: Optional[str] = None, auth_required: bool = False):
12
12
  """
13
13
  Decorator that transforms a function into a GraphQL mutation class and registers it for use in a Graphene-based API.
14
-
14
+
15
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
-
16
+
17
17
  Parameters:
18
18
  needs_role (Optional[str]): Reserved for future use to specify a required user role.
19
19
  auth_required (bool): If True, the mutation requires an authenticated user.
20
-
20
+
21
21
  Returns:
22
22
  Callable: A decorator that registers the mutation and returns the original function.
23
23
  """
@@ -30,7 +30,7 @@ class GeneralmanagerConfig(AppConfig):
30
30
  def ready(self):
31
31
  """
32
32
  Performs initialization tasks for the general_manager app when Django starts.
33
-
33
+
34
34
  Sets up synchronization and schema validation for read-only interfaces, initializes attributes and property accessors for general manager classes, and configures the GraphQL schema and endpoint if enabled in settings.
35
35
  """
36
36
  self.handleReadOnlyInterface(GeneralManagerMeta.read_only_classes)
@@ -47,7 +47,7 @@ class GeneralmanagerConfig(AppConfig):
47
47
  ):
48
48
  """
49
49
  Configures synchronization and schema validation for the provided read-only interface classes.
50
-
50
+
51
51
  Ensures that each read-only interface is synchronized before Django management commands run, and registers system checks to validate that their schemas are up to date.
52
52
  """
53
53
  GeneralmanagerConfig.patchReadOnlyInterfaceSync(read_only_classes)
@@ -72,7 +72,7 @@ class GeneralmanagerConfig(AppConfig):
72
72
  ):
73
73
  """
74
74
  Monkey-patches Django's management command runner to synchronize all provided read-only interfaces before executing any management command, except during autoreload subprocesses of 'runserver'.
75
-
75
+
76
76
  For each class in `general_manager_classes`, the associated read-only interface's `syncData` method is called prior to command execution, ensuring data consistency before management operations.
77
77
  """
78
78
  from general_manager.interface.readOnlyInterface import ReadOnlyInterface
@@ -83,10 +83,10 @@ class GeneralmanagerConfig(AppConfig):
83
83
  # Ensure syncData is only called at real run of runserver
84
84
  """
85
85
  Executes a Django management command, synchronizing all registered read-only interfaces before execution unless running in an autoreload subprocess of 'runserver'.
86
-
86
+
87
87
  Parameters:
88
88
  argv (list): Command-line arguments for the management command.
89
-
89
+
90
90
  Returns:
91
91
  The result of the original management command execution.
92
92
  """
@@ -113,7 +113,7 @@ class GeneralmanagerConfig(AppConfig):
113
113
  ):
114
114
  """
115
115
  Initializes attributes and establishes dynamic relationships for GeneralManager classes.
116
-
116
+
117
117
  For each class pending attribute initialization, assigns interface attributes and creates property accessors. Then, for all registered GeneralManager classes, connects input fields referencing other GeneralManager subclasses by adding GraphQL properties to enable filtered access to related objects.
118
118
  """
119
119
  logger.debug("Initializing GeneralManager classes...")
@@ -144,6 +144,8 @@ class GeneralmanagerConfig(AppConfig):
144
144
  f"{general_manager_class.__name__.lower()}_list",
145
145
  graphQlProperty(func),
146
146
  )
147
+ for general_manager_class in all_classes:
148
+ GeneralmanagerConfig.checkPermissionClass(general_manager_class)
147
149
 
148
150
  @staticmethod
149
151
  def handleGraphQL(
@@ -177,10 +179,10 @@ class GeneralmanagerConfig(AppConfig):
177
179
  def addGraphqlUrl(schema):
178
180
  """
179
181
  Adds a GraphQL endpoint to the Django URL configuration using the provided schema.
180
-
182
+
181
183
  Parameters:
182
184
  schema: The GraphQL schema to use for the endpoint.
183
-
185
+
184
186
  Raises:
185
187
  Exception: If the ROOT_URLCONF setting is not defined in Django settings.
186
188
  """
@@ -196,3 +198,24 @@ class GeneralmanagerConfig(AppConfig):
196
198
  GraphQLView.as_view(graphiql=True, schema=schema),
197
199
  )
198
200
  )
201
+
202
+ @staticmethod
203
+ def checkPermissionClass(general_manager_class: Type[GeneralManager]):
204
+ """
205
+ Checks if the class has a Permission attribute and if it is a subclass of BasePermission.
206
+ If so, it sets the Permission attribute on the class.
207
+ """
208
+ from general_manager.permission.basePermission import BasePermission
209
+ from general_manager.permission.managerBasedPermission import (
210
+ ManagerBasedPermission,
211
+ )
212
+
213
+ if hasattr(general_manager_class, "Permission"):
214
+ permission = general_manager_class.Permission
215
+ if not issubclass(permission, BasePermission):
216
+ raise TypeError(
217
+ f"{permission.__name__} must be a subclass of BasePermission"
218
+ )
219
+ general_manager_class.Permission = permission
220
+ else:
221
+ general_manager_class.Permission = ManagerBasedPermission
@@ -15,10 +15,11 @@ from general_manager.interface.baseInterface import (
15
15
  )
16
16
  from general_manager.bucket.baseBucket import Bucket
17
17
  from general_manager.manager.input import Input
18
- from general_manager.auxiliary.filterParser import parse_filters
18
+ from general_manager.utils.filterParser import parse_filters
19
19
 
20
20
  if TYPE_CHECKING:
21
21
  from general_manager.manager.generalManager import GeneralManager
22
+ from general_manager.interface.calculationInterface import CalculationInterface
22
23
 
23
24
 
24
25
  class CalculationBucket(Bucket[GeneralManagerType]):
@@ -90,7 +91,11 @@ class CalculationBucket(Bucket[GeneralManagerType]):
90
91
  self._current_combinations = state.get("current_combinations")
91
92
 
92
93
  def __or__(
93
- self, other: Bucket[GeneralManagerType] | GeneralManager[GeneralManagerType]
94
+ self,
95
+ other: (
96
+ Bucket[GeneralManagerType]
97
+ | GeneralManager[GeneralManagerType, CalculationInterface]
98
+ ),
94
99
  ) -> CalculationBucket[GeneralManagerType]:
95
100
  """
96
101
  Combines this CalculationBucket with another bucket or manager of the same type.
@@ -158,7 +163,7 @@ class CalculationBucket(Bucket[GeneralManagerType]):
158
163
  def filter(self, **kwargs: Any) -> CalculationBucket:
159
164
  """
160
165
  Returns a new CalculationBucket with additional filters applied.
161
-
166
+
162
167
  Merges the provided filter criteria with existing filters to further restrict valid input combinations.
163
168
  """
164
169
  filters = self.filters.copy()
@@ -316,12 +321,12 @@ class CalculationBucket(Bucket[GeneralManagerType]):
316
321
  ) -> List[dict[str, Any]]:
317
322
  """
318
323
  Recursively generates all valid input combinations for the specified input fields, applying filters and exclusions.
319
-
324
+
320
325
  Args:
321
326
  sorted_inputs: Input field names ordered to respect dependency constraints.
322
327
  filters: Mapping of input field names to filter definitions.
323
328
  excludes: Mapping of input field names to exclusion definitions.
324
-
329
+
325
330
  Returns:
326
331
  A list of dictionaries, each representing a valid combination of input values that satisfy all filters and exclusions.
327
332
  """
@@ -329,7 +334,7 @@ class CalculationBucket(Bucket[GeneralManagerType]):
329
334
  def helper(index, current_combo):
330
335
  """
331
336
  Recursively generates all valid input combinations for calculation inputs.
332
-
337
+
333
338
  Yields:
334
339
  Dict[str, Any]: A dictionary representing a valid combination of input values, filtered and excluded according to the provided criteria.
335
340
  """
@@ -4,7 +4,7 @@ from django.core.cache import cache as django_cache
4
4
  from general_manager.cache.cacheTracker import DependencyTracker
5
5
  from general_manager.cache.dependencyIndex import record_dependencies, Dependency
6
6
  from general_manager.cache.modelDependencyCollector import ModelDependencyCollector
7
- from general_manager.auxiliary.makeCacheKey import make_cache_key
7
+ from general_manager.utils.makeCacheKey import make_cache_key
8
8
 
9
9
 
10
10
  class CacheBackend(Protocol):
@@ -13,7 +13,7 @@ from typing import (
13
13
  from datetime import datetime
14
14
  from django.conf import settings
15
15
  from django.db.models import Model
16
- from general_manager.auxiliary import args_to_kwargs
16
+ from general_manager.utils import args_to_kwargs
17
17
 
18
18
  if TYPE_CHECKING:
19
19
  from general_manager.manager.input import Input
@@ -52,7 +52,7 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
52
52
  ):
53
53
  """
54
54
  Initialize the interface and load the associated model instance.
55
-
55
+
56
56
  If `search_date` is provided, loads the historical record as of that date; otherwise, loads the current record.
57
57
  """
58
58
  super().__init__(*args, **kwargs)
@@ -141,9 +141,9 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
141
141
  def getAttributeTypes(cls) -> dict[str, AttributeTypedDict]:
142
142
  """
143
143
  Return a dictionary mapping attribute names to metadata describing their types and properties.
144
-
144
+
145
145
  The dictionary includes all model fields, custom fields, foreign keys, many-to-many, and reverse relation fields. For each attribute, the metadata specifies its Python type (translated from Django field types when possible), whether it is required, editable, derived, and its default value. For related models with a general manager class, the type is set to that class.
146
-
146
+
147
147
  Returns:
148
148
  dict[str, AttributeTypedDict]: Mapping of attribute names to their type information and metadata.
149
149
  """
@@ -309,7 +309,7 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
309
309
  model: Type[models.Model] | models.Model,
310
310
  ) -> tuple[list[str], list[str]]:
311
311
  """
312
- Identifies custom fields on a model and their associated auxiliary fields to ignore.
312
+ Identifies custom fields on a model and their associated utils fields to ignore.
313
313
 
314
314
  Returns:
315
315
  A tuple containing a list of custom field names and a list of related field names
@@ -401,15 +401,15 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
401
401
  # Felder aus der Interface-Klasse sammeln
402
402
  """
403
403
  Dynamically generates a Django model class, its associated interface class, and a factory class from an interface definition.
404
-
404
+
405
405
  This method collects fields and metadata from the provided interface class, creates a new Django model inheriting from the specified base model class, attaches custom validation rules if present, and constructs corresponding interface and factory classes. The updated attributes dictionary, the new interface class, and the newly created model class are returned for integration into the general manager framework.
406
-
406
+
407
407
  Parameters:
408
408
  name: The name for the dynamically created model class.
409
409
  attrs: The attributes dictionary to be updated with the new interface and factory classes.
410
410
  interface: The interface base class defining the model structure and metadata.
411
411
  base_model_class: The base class to use for the new model (defaults to GeneralManagerModel).
412
-
412
+
413
413
  Returns:
414
414
  tuple: A tuple containing the updated attributes dictionary, the new interface class, and the newly created model class.
415
415
  """
@@ -9,6 +9,7 @@ from general_manager.interface.databaseBasedInterface import (
9
9
  DBBasedInterface,
10
10
  GeneralManagerModel,
11
11
  )
12
+ from django.db.models import NOT_PROVIDED
12
13
 
13
14
 
14
15
  class DatabaseInterface(DBBasedInterface[GeneralManagerModel]):
@@ -111,6 +112,8 @@ class DatabaseInterface(DBBasedInterface[GeneralManagerModel]):
111
112
  if isinstance(value, GeneralManager):
112
113
  value = value.identification["id"]
113
114
  key = f"{key}_id"
115
+ if value is NOT_PROVIDED:
116
+ continue
114
117
  try:
115
118
  setattr(instance, key, value)
116
119
  except ValueError as e:
@@ -158,8 +161,8 @@ class DatabaseInterface(DBBasedInterface[GeneralManagerModel]):
158
161
  """
159
162
  instance.changed_by_id = creator_id
160
163
  instance.full_clean()
164
+ instance.save()
161
165
  if history_comment:
162
166
  update_change_reason(instance, history_comment)
163
- instance.save()
164
167
 
165
168
  return instance.pk
@@ -20,12 +20,13 @@ class GeneralManager(
20
20
  Generic[GeneralManagerType, InterfaceType], metaclass=GeneralManagerMeta
21
21
  ):
22
22
  Interface: Type[InterfaceType]
23
+ Permission: Type[BasePermission]
23
24
  _attributes: dict[str, Any]
24
25
 
25
26
  def __init__(self, *args: Any, **kwargs: Any):
26
27
  """
27
28
  Initialize the manager by creating an interface instance with the provided arguments and storing its identification.
28
-
29
+
29
30
  The identification is registered with the dependency tracker for tracking purposes.
30
31
  """
31
32
  self._interface = self.Interface(*args, **kwargs)
@@ -55,9 +56,9 @@ class GeneralManager(
55
56
  ) -> Bucket[GeneralManagerType]:
56
57
  """
57
58
  Combine this manager with another manager of the same class or a Bucket using the union operator.
58
-
59
+
59
60
  If combined with a Bucket, returns the union of the Bucket and this manager. If combined with another manager of the same class, returns a Bucket containing both instances. Raises a TypeError for unsupported types.
60
-
61
+
61
62
  Returns:
62
63
  Bucket[GeneralManagerType]: A Bucket containing the union of the involved managers.
63
64
  """
@@ -93,20 +94,19 @@ class GeneralManager(
93
94
  ) -> GeneralManager[GeneralManagerType, InterfaceType]:
94
95
  """
95
96
  Creates a new managed object using the underlying interface and returns a corresponding manager instance.
96
-
97
+
97
98
  Performs a permission check if a `Permission` class is defined and permission checks are not ignored. Passes all provided arguments to the interface's `create` method.
98
-
99
+
99
100
  Parameters:
100
101
  creator_id (int | None): Optional identifier for the creator of the object.
101
102
  history_comment (str | None): Optional comment for audit or history tracking.
102
103
  ignore_permission (bool): If True, skips the permission check.
103
-
104
+
104
105
  Returns:
105
106
  GeneralManager[GeneralManagerType, InterfaceType]: A new manager instance for the created object.
106
107
  """
107
- Permission: Type[BasePermission] | None = getattr(cls, "Permission", None)
108
- if Permission is not None and not ignore_permission:
109
- Permission.checkCreatePermission(kwargs, cls, creator_id)
108
+ if not ignore_permission:
109
+ cls.Permission.checkCreatePermission(kwargs, cls, creator_id)
110
110
  identification = cls.Interface.create(
111
111
  creator_id=creator_id, history_comment=history_comment, **kwargs
112
112
  )
@@ -122,19 +122,18 @@ class GeneralManager(
122
122
  ) -> GeneralManager[GeneralManagerType, InterfaceType]:
123
123
  """
124
124
  Update the underlying interface object with new data and return a new manager instance.
125
-
125
+
126
126
  Parameters:
127
127
  creator_id (int | None): Optional identifier for the user performing the update.
128
128
  history_comment (str | None): Optional comment describing the update.
129
129
  ignore_permission (bool): If True, skips permission checks.
130
130
  **kwargs: Additional fields to update on the interface object.
131
-
131
+
132
132
  Returns:
133
133
  GeneralManager[GeneralManagerType, InterfaceType]: A new manager instance reflecting the updated object.
134
134
  """
135
- Permission: Type[BasePermission] | None = getattr(self, "Permission", None)
136
- if Permission is not None and not ignore_permission:
137
- Permission.checkUpdatePermission(kwargs, self, creator_id)
135
+ if not ignore_permission:
136
+ self.Permission.checkUpdatePermission(kwargs, self, creator_id)
138
137
  self._interface.update(
139
138
  creator_id=creator_id,
140
139
  history_comment=history_comment,
@@ -151,18 +150,17 @@ class GeneralManager(
151
150
  ) -> GeneralManager[GeneralManagerType, InterfaceType]:
152
151
  """
153
152
  Deactivates the underlying interface object and returns a new manager instance.
154
-
153
+
155
154
  Parameters:
156
155
  creator_id (int | None): Optional identifier for the user performing the deactivation.
157
156
  history_comment (str | None): Optional comment describing the reason for deactivation.
158
157
  ignore_permission (bool): If True, skips permission checks.
159
-
158
+
160
159
  Returns:
161
160
  GeneralManager[GeneralManagerType, InterfaceType]: A new instance representing the deactivated object.
162
161
  """
163
- Permission: Type[BasePermission] | None = getattr(self, "Permission", None)
164
- if Permission is not None and not ignore_permission:
165
- Permission.checkDeletePermission(self, creator_id)
162
+ if not ignore_permission:
163
+ self.Permission.checkDeletePermission(self, creator_id)
166
164
  self._interface.deactivate(
167
165
  creator_id=creator_id, history_comment=history_comment
168
166
  )
@@ -190,12 +188,12 @@ class GeneralManager(
190
188
  def __parse_identification(kwargs: dict[str, Any]) -> dict[str, Any] | None:
191
189
  """
192
190
  Return a dictionary with all GeneralManager instances in the input replaced by their identification dictionaries.
193
-
191
+
194
192
  For each key-value pair in the input, any GeneralManager instance is replaced by its identification. Lists and tuples are processed recursively, substituting contained GeneralManager instances with their identifications. Returns None if the resulting dictionary is empty.
195
-
193
+
196
194
  Parameters:
197
195
  kwargs (dict[str, Any]): Dictionary to process.
198
-
196
+
199
197
  Returns:
200
198
  dict[str, Any] | None: Processed dictionary with identifications, or None if empty.
201
199
  """
@@ -26,9 +26,9 @@ class GeneralManagerMeta(type):
26
26
  def __new__(mcs, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> type:
27
27
  """
28
28
  Creates a new class using the metaclass, integrating interface hooks and registering the class for attribute initialization and tracking.
29
-
29
+
30
30
  If the class definition includes an `Interface` attribute, validates it as a subclass of `InterfaceBase`, applies pre- and post-creation hooks from the interface, and registers the resulting class for attribute initialization and management. Regardless of interface presence, the new class is tracked for pending GraphQL interface creation.
31
-
31
+
32
32
  Returns:
33
33
  The newly created class, potentially augmented with interface integration and registration logic.
34
34
  """
@@ -38,7 +38,7 @@ class GeneralManagerMeta(type):
38
38
  ) -> Type[GeneralManager]:
39
39
  """
40
40
  Create a new GeneralManager class using the standard metaclass instantiation process.
41
-
41
+
42
42
  Returns:
43
43
  The newly created GeneralManager subclass.
44
44
  """
@@ -59,6 +59,7 @@ class GeneralManagerMeta(type):
59
59
 
60
60
  else:
61
61
  new_class = createNewGeneralManagerClass(mcs, name, bases, attrs)
62
+
62
63
  if getattr(settings, "AUTOCREATE_GRAPHQL", False):
63
64
  mcs.pending_graphql_interfaces.append(new_class)
64
65
 
@@ -70,7 +71,7 @@ class GeneralManagerMeta(type):
70
71
  ):
71
72
  """
72
73
  Dynamically assigns property descriptors to a class for each specified attribute name.
73
-
74
+
74
75
  For each attribute, creates a descriptor that:
75
76
  - Returns the field type from the class's interface when accessed on the class.
76
77
  - Retrieves the value from the instance's `_attributes` dictionary when accessed on an instance.
@@ -81,9 +82,10 @@ class GeneralManagerMeta(type):
81
82
  def desciptorMethod(attr_name: str, new_class: type):
82
83
  """
83
84
  Creates a property descriptor for an attribute, enabling dynamic access and callable resolution.
84
-
85
+
85
86
  When accessed on the class, returns the field type from the associated interface. When accessed on an instance, retrieves the attribute value from the instance's `_attributes` dictionary, invoking it with the instance's interface if the value is callable. Raises `AttributeError` if the attribute is missing or if a callable attribute raises an exception.
86
87
  """
88
+
87
89
  class Descriptor(Generic[GeneralManagerType]):
88
90
  def __init__(self, attr_name: str, new_class: Type[GeneralManager]):
89
91
  self.attr_name = attr_name
@@ -4,7 +4,7 @@ from typing import Any, Callable
4
4
  import pint
5
5
  from decimal import Decimal, getcontext, InvalidOperation
6
6
  from operator import eq, ne, lt, le, gt, ge
7
-
7
+ from pint.facets.plain import PlainQuantity
8
8
 
9
9
  # Set precision for Decimal
10
10
  getcontext().prec = 28
@@ -21,6 +21,16 @@ for currency in currency_units:
21
21
 
22
22
  class Measurement:
23
23
  def __init__(self, value: Decimal | float | int | str, unit: str):
24
+ """
25
+ Initialize a Measurement instance with a numeric value and unit.
26
+
27
+ Parameters:
28
+ value (Decimal | float | int | str): The numeric value to be associated with the unit. Can be a Decimal, float, int, or a string convertible to Decimal.
29
+ unit (str): The unit of measurement as a string.
30
+
31
+ Raises:
32
+ TypeError: If the value cannot be converted to a Decimal.
33
+ """
24
34
  if not isinstance(value, (Decimal, float, int)):
25
35
  try:
26
36
  value = Decimal(str(value))
@@ -28,22 +38,37 @@ class Measurement:
28
38
  raise TypeError("Value must be a Decimal, float, int or compatible.")
29
39
  if not isinstance(value, Decimal):
30
40
  value = Decimal(str(value))
31
- self.__quantity = self.formatDecimal(value) * ureg.Quantity(1, unit)
41
+ self.__quantity = ureg.Quantity(self.formatDecimal(value), unit)
32
42
 
33
43
  def __getstate__(self):
44
+ """
45
+ Return a serializable state dictionary containing the magnitude and unit of the measurement.
46
+
47
+ Returns:
48
+ dict: A dictionary with 'magnitude' as a string and 'unit' as a string, suitable for pickling or other serialization.
49
+ """
34
50
  state = {
35
- "magnitude": str(self.quantity.magnitude),
36
- "unit": str(self.quantity.units),
51
+ "magnitude": str(self.magnitude),
52
+ "unit": str(self.unit),
37
53
  }
38
54
  return state
39
55
 
40
56
  def __setstate__(self, state):
57
+ """
58
+ Restore the Measurement object from a serialized state dictionary.
59
+
60
+ Parameters:
61
+ state (dict): A dictionary containing 'magnitude' as a string and 'unit' as a string.
62
+ """
41
63
  value = Decimal(state["magnitude"])
42
64
  unit = state["unit"]
43
- self.__quantity = self.formatDecimal(value) * ureg.Quantity(1, unit)
65
+ self.__quantity = ureg.Quantity(self.formatDecimal(value), unit)
44
66
 
45
67
  @property
46
- def quantity(self) -> pint.Quantity:
68
+ def quantity(self) -> PlainQuantity:
69
+ """
70
+ Return the internal quantity as a `PlainQuantity` object from the `pint` library.
71
+ """
47
72
  return self.__quantity
48
73
 
49
74
  @property
@@ -57,18 +82,24 @@ class Measurement:
57
82
  @classmethod
58
83
  def from_string(cls, value: str) -> Measurement:
59
84
  """
60
- Creates a Measurement instance from a string in the format 'value unit'.
85
+ Parse a string of the form 'value unit' and return a Measurement instance.
61
86
 
62
- Args:
63
- value: A string containing a numeric value and a unit separated by a space (e.g., '10.5 kg').
87
+ Parameters:
88
+ value (str): String containing a numeric value and a unit, separated by a space (e.g., '10.5 kg').
64
89
 
65
90
  Returns:
66
- A Measurement instance representing the parsed value and unit.
91
+ Measurement: The corresponding Measurement object.
67
92
 
68
93
  Raises:
69
94
  ValueError: If the input string does not contain exactly two parts separated by a space.
70
95
  """
71
96
  splitted = value.split(" ")
97
+ if len(splitted) == 1:
98
+ # If only one part, assume it's a dimensionless value
99
+ try:
100
+ return cls(Decimal(splitted[0]), "dimensionless")
101
+ except InvalidOperation:
102
+ raise ValueError("Invalid value for dimensionless measurement.")
72
103
  if len(splitted) != 2:
73
104
  raise ValueError("String must be in the format 'value unit'.")
74
105
  value, unit = splitted
@@ -86,12 +117,27 @@ class Measurement:
86
117
  return value
87
118
 
88
119
  def to(self, target_unit: str, exchange_rate: float | None = None):
120
+ """
121
+ Convert the measurement to a specified target unit, handling both currency and physical unit conversions.
122
+
123
+ For currency units, an explicit exchange rate must be provided if converting between different currencies; otherwise, the original measurement is returned. For physical units, standard unit conversion is performed using the underlying unit registry.
124
+
125
+ Parameters:
126
+ target_unit (str): The unit to convert the measurement to.
127
+ exchange_rate (float, optional): The exchange rate to use for currency conversion. Required if converting between different currencies.
128
+
129
+ Returns:
130
+ Measurement: A new Measurement instance in the target unit.
131
+
132
+ Raises:
133
+ ValueError: If converting between different currencies without providing an exchange rate.
134
+ """
89
135
  if self.is_currency():
90
- if self.quantity.units == ureg(target_unit):
136
+ if self.unit == ureg(target_unit):
91
137
  return self # Same currency, no conversion needed
92
138
  elif exchange_rate is not None:
93
139
  # Convert using the provided exchange rate
94
- value = self.quantity.magnitude * Decimal(str(exchange_rate))
140
+ value = self.magnitude * Decimal(str(exchange_rate))
95
141
  return Measurement(value, target_unit)
96
142
  else:
97
143
  raise ValueError(
@@ -106,14 +152,25 @@ class Measurement:
106
152
 
107
153
  def is_currency(self):
108
154
  # Check if the unit is a defined currency
109
- return str(self.quantity.units) in currency_units
155
+ """
156
+ Return True if the measurement's unit is one of the defined currency units.
157
+ """
158
+ return str(self.unit) in currency_units
110
159
 
111
160
  def __add__(self, other: Any) -> Measurement:
161
+ """
162
+ Add two Measurement instances, supporting both currency and physical units.
163
+
164
+ Addition is allowed only if both operands are currencies of the same unit or both are physical units with compatible dimensions. Raises a TypeError if operands are of different types (currency vs. physical unit) or not Measurement instances, and raises a ValueError if units are incompatible.
165
+
166
+ Returns:
167
+ Measurement: A new Measurement representing the sum.
168
+ """
112
169
  if not isinstance(other, Measurement):
113
170
  raise TypeError("Addition is only allowed between Measurement instances.")
114
171
  if self.is_currency() and other.is_currency():
115
172
  # Both are currencies
116
- if self.quantity.units != other.quantity.units:
173
+ if self.unit != other.unit:
117
174
  raise ValueError(
118
175
  "Addition between different currencies is not allowed."
119
176
  )
@@ -139,34 +196,49 @@ class Measurement:
139
196
  )
140
197
 
141
198
  def __sub__(self, other: Any) -> Measurement:
199
+ """
200
+ Subtracts another Measurement from this one, enforcing unit compatibility.
201
+
202
+ Subtraction is allowed only between two currencies of the same unit or two physical units with compatible dimensions. Raises a TypeError if the operand is not a Measurement or if attempting to subtract a currency from a physical unit (or vice versa). Raises a ValueError if subtracting different currencies or incompatible physical units.
203
+
204
+ Returns:
205
+ Measurement: The result of the subtraction as a new Measurement instance.
206
+ """
142
207
  if not isinstance(other, Measurement):
143
208
  raise TypeError(
144
209
  "Subtraction is only allowed between Measurement instances."
145
210
  )
146
211
  if self.is_currency() and other.is_currency():
147
212
  # Both are currencies
148
- if self.quantity.units != other.quantity.units:
213
+ if self.unit != other.unit:
149
214
  raise ValueError(
150
215
  "Subtraction between different currencies is not allowed."
151
216
  )
152
217
  result_quantity = self.quantity - other.quantity
153
- return Measurement(
154
- Decimal(str(result_quantity.magnitude)), str(self.quantity.units)
155
- )
218
+ return Measurement(Decimal(str(result_quantity.magnitude)), str(self.unit))
156
219
  elif not self.is_currency() and not other.is_currency():
157
220
  # Both are physical units
158
221
  if self.quantity.dimensionality != other.quantity.dimensionality:
159
222
  raise ValueError("Units are not compatible for subtraction.")
160
223
  result_quantity = self.quantity - other.quantity
161
- return Measurement(
162
- Decimal(str(result_quantity.magnitude)), str(self.quantity.units)
163
- )
224
+ return Measurement(Decimal(str(result_quantity.magnitude)), str(self.unit))
164
225
  else:
165
226
  raise TypeError(
166
227
  "Subtraction between currency and physical unit is not allowed."
167
228
  )
168
229
 
169
230
  def __mul__(self, other: Any) -> Measurement:
231
+ """
232
+ Multiply this measurement by another measurement or a numeric value.
233
+
234
+ Multiplication between two currency measurements is not allowed. If multiplied by another measurement, returns a new Measurement with the combined units. If multiplied by a numeric value, returns a new Measurement with the same unit and scaled magnitude.
235
+
236
+ Returns:
237
+ Measurement: The result of the multiplication as a new Measurement instance.
238
+
239
+ Raises:
240
+ TypeError: If attempting to multiply two currency measurements or if the operand is not a Measurement or numeric value.
241
+ """
170
242
  if isinstance(other, Measurement):
171
243
  if self.is_currency() or other.is_currency():
172
244
  raise TypeError(
@@ -180,15 +252,21 @@ class Measurement:
180
252
  if not isinstance(other, Decimal):
181
253
  other = Decimal(str(other))
182
254
  result_quantity = self.quantity * other
183
- return Measurement(
184
- Decimal(str(result_quantity.magnitude)), str(self.quantity.units)
185
- )
255
+ return Measurement(Decimal(str(result_quantity.magnitude)), str(self.unit))
186
256
  else:
187
257
  raise TypeError(
188
258
  "Multiplication is only allowed with Measurement or numeric values."
189
259
  )
190
260
 
191
261
  def __truediv__(self, other: Any) -> Measurement:
262
+ """
263
+ Divide this measurement by another measurement or a numeric value.
264
+
265
+ If dividing by another `Measurement`, both must not be currencies. Returns a new `Measurement` with the resulting value and unit. If dividing by a numeric value, returns a new `Measurement` with the same unit and divided magnitude.
266
+
267
+ Raises:
268
+ TypeError: If dividing two currency measurements, or if the operand is not a `Measurement` or numeric value.
269
+ """
192
270
  if isinstance(other, Measurement):
193
271
  if self.is_currency() and other.is_currency():
194
272
  raise TypeError("Division between two currency amounts is not allowed.")
@@ -200,41 +278,38 @@ class Measurement:
200
278
  if not isinstance(other, Decimal):
201
279
  other = Decimal(str(other))
202
280
  result_quantity = self.quantity / other
203
- return Measurement(
204
- Decimal(str(result_quantity.magnitude)), str(self.quantity.units)
205
- )
281
+ return Measurement(Decimal(str(result_quantity.magnitude)), str(self.unit))
206
282
  else:
207
283
  raise TypeError(
208
284
  "Division is only allowed with Measurement or numeric values."
209
285
  )
210
286
 
211
287
  def __str__(self):
212
- if not str(self.quantity.units) == "dimensionless":
213
- return f"{self.quantity.magnitude} {self.quantity.units}"
214
- return f"{self.quantity.magnitude}"
288
+ """
289
+ Return a string representation of the measurement, including the unit unless it is dimensionless.
290
+ """
291
+ if not str(self.unit) == "dimensionless":
292
+ return f"{self.magnitude} {self.unit}"
293
+ return f"{self.magnitude}"
215
294
 
216
295
  def __repr__(self):
217
- return f"Measurement({self.quantity.magnitude}, '{self.quantity.units}')"
296
+ """
297
+ Return a string representation of the Measurement instance for debugging, showing its magnitude and unit.
298
+ """
299
+ return f"Measurement({self.magnitude}, '{self.unit}')"
218
300
 
219
301
  def _compare(self, other: Any, operation: Callable[..., bool]) -> bool:
220
302
  """
221
- Compares this Measurement with another using the specified comparison operation.
303
+ Compare this Measurement to another using a specified comparison operation.
222
304
 
223
- If `other` is a string, it is parsed into a Measurement. Raises a TypeError if
224
- `other` is not a Measurement instance. Converts `other` to this instance's unit
225
- before applying the comparison. Raises a ValueError if the measurements have
226
- incompatible dimensions.
305
+ If `other` is a string, it is parsed into a Measurement. The comparison is performed after converting `other` to this instance's unit. Raises a TypeError if `other` is not a Measurement or a valid string, and a ValueError if the measurements have incompatible dimensions.
227
306
 
228
- Args:
229
- other: The object to compare with, either a Measurement or a string in the format "value unit".
230
- operation: A callable that takes two magnitudes and returns a boolean result.
307
+ Parameters:
308
+ other: The object to compare, either a Measurement or a string in the format "value unit".
309
+ operation: A callable that takes two magnitudes and returns a boolean.
231
310
 
232
311
  Returns:
233
- The result of the comparison operation.
234
-
235
- Raises:
236
- TypeError: If `other` is not a Measurement instance or a valid string representation.
237
- ValueError: If the measurements have different dimensions and cannot be compared.
312
+ bool: The result of the comparison.
238
313
  """
239
314
  if isinstance(other, str):
240
315
  other = Measurement.from_string(other)
@@ -244,9 +319,9 @@ class Measurement:
244
319
  raise TypeError("Comparison is only allowed between Measurement instances.")
245
320
  try:
246
321
  # Convert `other` to the same units as `self`
247
- other_converted: pint.Quantity = other.quantity.to(self.quantity.units) # type: ignore
322
+ other_converted: pint.Quantity = other.quantity.to(self.unit) # type: ignore
248
323
  # Apply the comparison operation
249
- return operation(self.quantity.magnitude, other_converted.magnitude)
324
+ return operation(self.magnitude, other_converted.magnitude)
250
325
  except pint.DimensionalityError:
251
326
  raise ValueError("Cannot compare measurements with different dimensions.")
252
327
 
@@ -273,16 +348,16 @@ class Measurement:
273
348
 
274
349
  def __ge__(self, other: Any) -> bool:
275
350
  """
276
- Returns True if this measurement is greater than or equal to another measurement.
351
+ Return True if this measurement is greater than or equal to another measurement.
277
352
 
278
- Comparison is performed after converting the other operand to the same unit. Raises a TypeError if the other object is not a Measurement instance or a compatible string, or a ValueError if the units are not compatible.
353
+ The comparison is performed after converting the other operand to the same unit as this measurement. Raises a TypeError if the other object is not a Measurement instance or a compatible string, or a ValueError if the units are incompatible.
279
354
  """
280
355
  return self._compare(other, ge)
281
356
 
282
357
  def __hash__(self) -> int:
283
358
  """
284
- Returns a hash value based on the magnitude and unit of the measurement.
359
+ Return a hash value derived from the measurement's magnitude and unit.
285
360
 
286
- This enables Measurement instances to be used in hash-based collections such as sets and dictionaries.
361
+ Enables use of Measurement instances in hash-based collections such as sets and dictionaries.
287
362
  """
288
- return hash((self.quantity.magnitude, str(self.quantity.units)))
363
+ return hash((self.magnitude, str(self.unit)))
@@ -1,21 +1,21 @@
1
1
  import inspect
2
2
  import json
3
- from general_manager.auxiliary.jsonEncoder import CustomJSONEncoder
3
+ from general_manager.utils.jsonEncoder import CustomJSONEncoder
4
4
  from hashlib import sha256
5
5
 
6
6
 
7
7
  def make_cache_key(func, args, kwargs):
8
8
  """
9
9
  Generates a unique, deterministic cache key for a specific function call.
10
-
10
+
11
11
  The key is derived from the function's module, qualified name, and bound arguments,
12
12
  serialized to JSON and hashed with SHA-256 to ensure uniqueness for each call signature.
13
-
13
+
14
14
  Args:
15
15
  func: The target function to be identified.
16
16
  args: Positional arguments for the function call.
17
17
  kwargs: Keyword arguments for the function call.
18
-
18
+
19
19
  Returns:
20
20
  A hexadecimal SHA-256 hash string uniquely representing the function call.
21
21
  """
File without changes