GeneralManager 0.8.0__py3-none-any.whl → 0.9.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
general_manager/apps.py CHANGED
@@ -29,26 +29,32 @@ class GeneralmanagerConfig(AppConfig):
29
29
 
30
30
  def ready(self):
31
31
  """
32
- Initializes the general_manager app on Django startup.
32
+ Performs initialization tasks for the general_manager app when Django starts.
33
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.
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
- self.handleReadOnlyInterface()
37
- self.initializeGeneralManagerClasses()
36
+ self.handleReadOnlyInterface(GeneralManagerMeta.read_only_classes)
37
+ self.initializeGeneralManagerClasses(
38
+ GeneralManagerMeta.pending_attribute_initialization,
39
+ GeneralManagerMeta.all_classes,
40
+ )
38
41
  if getattr(settings, "AUTOCREATE_GRAPHQL", False):
39
- self.handleGraphQL()
42
+ self.handleGraphQL(GeneralManagerMeta.pending_graphql_interfaces)
40
43
 
41
- def handleReadOnlyInterface(self):
44
+ @staticmethod
45
+ def handleReadOnlyInterface(
46
+ read_only_classes: list[Type[GeneralManager[Any, ReadOnlyInterface]]],
47
+ ):
42
48
  """
43
- Configures synchronization and schema validation for all registered read-only interfaces.
49
+ Configures synchronization and schema validation for the provided read-only interface classes.
44
50
 
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.
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.
46
52
  """
47
- self.patchReadOnlyInterfaceSync(GeneralManagerMeta.read_only_classes)
53
+ GeneralmanagerConfig.patchReadOnlyInterfaceSync(read_only_classes)
48
54
  from general_manager.interface.readOnlyInterface import ReadOnlyInterface
49
55
 
50
56
  logger.debug("starting to register ReadOnlyInterface schema warnings...")
51
- for general_manager_class in GeneralManagerMeta.read_only_classes:
57
+ for general_manager_class in read_only_classes:
52
58
  read_only_interface = cast(
53
59
  Type[ReadOnlyInterface], general_manager_class.Interface
54
60
  )
@@ -65,9 +71,9 @@ class GeneralmanagerConfig(AppConfig):
65
71
  general_manager_classes: list[Type[GeneralManager[Any, ReadOnlyInterface]]],
66
72
  ):
67
73
  """
68
- Monkey-patches Django's management command runner to synchronize data for all provided read-only interfaces before executing management commands.
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'.
69
75
 
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`.
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.
71
77
  """
72
78
  from general_manager.interface.readOnlyInterface import ReadOnlyInterface
73
79
 
@@ -76,7 +82,7 @@ class GeneralmanagerConfig(AppConfig):
76
82
  def run_from_argv_with_sync(self, argv):
77
83
  # Ensure syncData is only called at real run of runserver
78
84
  """
79
- Executes a Django management command, synchronizing all registered read-only interfaces before execution unless running an autoreload subprocess of 'runserver'.
85
+ Executes a Django management command, synchronizing all registered read-only interfaces before execution unless running in an autoreload subprocess of 'runserver'.
80
86
 
81
87
  Parameters:
82
88
  argv (list): Command-line arguments for the management command.
@@ -100,18 +106,20 @@ class GeneralmanagerConfig(AppConfig):
100
106
 
101
107
  BaseCommand.run_from_argv = run_from_argv_with_sync
102
108
 
103
- def initializeGeneralManagerClasses(self):
109
+ @staticmethod
110
+ def initializeGeneralManagerClasses(
111
+ pending_attribute_initialization: list[Type[GeneralManager]],
112
+ all_classes: list[Type[GeneralManager]],
113
+ ):
104
114
  """
105
- Initializes attributes and sets up dynamic relationships for all registered GeneralManager classes.
115
+ Initializes attributes and establishes dynamic relationships for GeneralManager classes.
106
116
 
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.
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.
108
118
  """
109
119
  logger.debug("Initializing GeneralManager classes...")
110
120
 
111
121
  logger.debug("starting to create attributes for GeneralManager classes...")
112
- for (
113
- general_manager_class
114
- ) in GeneralManagerMeta.pending_attribute_initialization:
122
+ for general_manager_class in pending_attribute_initialization:
115
123
  attributes = general_manager_class.Interface.getAttributes()
116
124
  setattr(general_manager_class, "_attributes", attributes)
117
125
  GeneralManagerMeta.createAtPropertiesForAttributes(
@@ -119,7 +127,7 @@ class GeneralmanagerConfig(AppConfig):
119
127
  )
120
128
 
121
129
  logger.debug("starting to connect inputs to other general manager classes...")
122
- for general_manager_class in GeneralManagerMeta.all_classes:
130
+ for general_manager_class in all_classes:
123
131
  attributes = getattr(general_manager_class.Interface, "input_fields", {})
124
132
  for attribute_name, attribute in attributes.items():
125
133
  if isinstance(attribute, Input) and issubclass(
@@ -137,12 +145,15 @@ class GeneralmanagerConfig(AppConfig):
137
145
  graphQlProperty(func),
138
146
  )
139
147
 
140
- def handleGraphQL(self):
148
+ @staticmethod
149
+ def handleGraphQL(
150
+ pending_graphql_interfaces: list[Type[GeneralManager]],
151
+ ):
141
152
  """
142
- Sets up GraphQL interfaces, mutations, and schema for all pending general manager classes, and adds the GraphQL endpoint to the Django URL configuration.
153
+ Creates GraphQL interfaces and mutations for the provided general manager classes, builds the GraphQL schema, and registers the GraphQL endpoint in the Django URL configuration.
143
154
  """
144
155
  logger.debug("Starting to create GraphQL interfaces and mutations...")
145
- for general_manager_class in GeneralManagerMeta.pending_graphql_interfaces:
156
+ for general_manager_class in pending_graphql_interfaces:
146
157
  GraphQL.createGraphqlInterface(general_manager_class)
147
158
  GraphQL.createGraphqlMutation(general_manager_class)
148
159
 
@@ -160,18 +171,22 @@ class GeneralmanagerConfig(AppConfig):
160
171
  query=GraphQL._query_class,
161
172
  mutation=GraphQL._mutation_class,
162
173
  )
163
- self.addGraphqlUrl(schema)
174
+ GeneralmanagerConfig.addGraphqlUrl(schema)
164
175
 
165
- def addGraphqlUrl(self, schema):
176
+ @staticmethod
177
+ def addGraphqlUrl(schema):
166
178
  """
167
- Dynamically appends a GraphQL endpoint to the Django URL configuration using the given schema.
179
+ Adds a GraphQL endpoint to the Django URL configuration using the provided schema.
180
+
181
+ Parameters:
182
+ schema: The GraphQL schema to use for the endpoint.
168
183
 
169
184
  Raises:
170
185
  Exception: If the ROOT_URLCONF setting is not defined in Django settings.
171
186
  """
172
187
  logging.debug("Adding GraphQL URL to Django settings...")
173
188
  root_url_conf_path = getattr(settings, "ROOT_URLCONF", None)
174
- graph_ql_url = getattr(settings, "GRAPHQL_URL", "graphql/")
189
+ graph_ql_url = getattr(settings, "GRAPHQL_URL", "graphql")
175
190
  if not root_url_conf_path:
176
191
  raise Exception("ROOT_URLCONF not found in settings")
177
192
  urlconf = import_module(root_url_conf_path)
@@ -23,6 +23,11 @@ from general_manager.interface.baseInterface import (
23
23
  )
24
24
  from general_manager.manager.input import Input
25
25
  from general_manager.bucket.databaseBucket import DatabaseBucket
26
+ from general_manager.interface.models import (
27
+ GeneralManagerBasisModel,
28
+ GeneralManagerModel,
29
+ getFullCleanMethode,
30
+ )
26
31
 
27
32
  if TYPE_CHECKING:
28
33
  from general_manager.manager.generalManager import GeneralManager
@@ -32,72 +37,6 @@ if TYPE_CHECKING:
32
37
  modelsModel = TypeVar("modelsModel", bound=models.Model)
33
38
 
34
39
 
35
- def getFullCleanMethode(model: Type[models.Model]) -> Callable[..., None]:
36
- """
37
- Generates a custom `full_clean` method for a Django model that combines standard validation with additional rule-based checks.
38
-
39
- The returned method first performs Django's built-in model validation, then evaluates any custom rules defined in the model's `_meta.rules` attribute. If any validation or rule fails, a `ValidationError` is raised containing all collected errors.
40
- """
41
-
42
- def full_clean(self: models.Model, *args: Any, **kwargs: Any):
43
- errors: dict[str, Any] = {}
44
- try:
45
- super(model, self).full_clean(*args, **kwargs) # type: ignore
46
- except ValidationError as e:
47
- errors.update(e.message_dict)
48
-
49
- rules: list[Rule] = getattr(self._meta, "rules")
50
- for rule in rules:
51
- if not rule.evaluate(self):
52
- error_message = rule.getErrorMessage()
53
- if error_message:
54
- errors.update(error_message)
55
-
56
- if errors:
57
- raise ValidationError(errors)
58
-
59
- return full_clean
60
-
61
-
62
- class GeneralManagerBasisModel(models.Model):
63
- _general_manager_class: ClassVar[Type[GeneralManager]]
64
- is_active = models.BooleanField(default=True)
65
- history = HistoricalRecords(inherit=True)
66
-
67
- class Meta:
68
- abstract = True
69
-
70
-
71
- class GeneralManagerModel(GeneralManagerBasisModel):
72
- changed_by = models.ForeignKey(
73
- settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True
74
- )
75
- changed_by_id: int | None
76
-
77
- @property
78
- def _history_user(self) -> AbstractUser | None:
79
- """
80
- Gets the user who last modified this model instance, or None if not set.
81
-
82
- Returns:
83
- AbstractUser | None: The user who last changed the instance, or None if unavailable.
84
- """
85
- return self.changed_by
86
-
87
- @_history_user.setter
88
- def _history_user(self, value: AbstractUser) -> None:
89
- """
90
- Sets the user responsible for the latest change to the model instance.
91
-
92
- Args:
93
- value: The user to associate with the change.
94
- """
95
- self.changed_by = value
96
-
97
- class Meta: # type: ignore
98
- abstract = True
99
-
100
-
101
40
  MODEL_TYPE = TypeVar("MODEL_TYPE", bound=GeneralManagerBasisModel)
102
41
 
103
42
 
@@ -112,9 +51,9 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
112
51
  **kwargs: dict[str, Any],
113
52
  ):
114
53
  """
115
- Initialize the interface instance and load the associated model record.
54
+ Initialize the interface and load the associated model instance.
116
55
 
117
- If `search_date` is provided, retrieves the historical record as of that date; otherwise, loads the current record.
56
+ If `search_date` is provided, loads the historical record as of that date; otherwise, loads the current record.
118
57
  """
119
58
  super().__init__(*args, **kwargs)
120
59
  self.pk = self.identification["id"]
@@ -201,10 +140,10 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
201
140
  @classmethod
202
141
  def getAttributeTypes(cls) -> dict[str, AttributeTypedDict]:
203
142
  """
204
- Return a dictionary mapping attribute names to metadata describing their type and properties.
143
+ Return a dictionary mapping attribute names to metadata describing their types and properties.
144
+
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.
205
146
 
206
- The returned dictionary includes all model fields, custom fields, foreign keys, many-to-many, and reverse relation fields. For each attribute, the metadata includes 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.
207
-
208
147
  Returns:
209
148
  dict[str, AttributeTypedDict]: Mapping of attribute names to their type information and metadata.
210
149
  """
@@ -461,9 +400,9 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
461
400
  ) -> tuple[attributes, interfaceBaseClass, relatedClass]:
462
401
  # Felder aus der Interface-Klasse sammeln
463
402
  """
464
- Dynamically creates a Django model class, its associated interface class, and a factory class from an interface definition.
403
+ Dynamically generates a Django model class, its associated interface class, and a factory class from an interface definition.
465
404
 
466
- This method extracts fields and meta information from the provided interface class, constructs a new Django model inheriting from the specified base model class, attaches custom validation rules if present, and generates corresponding interface and factory classes. The resulting classes are returned for integration into the general manager framework.
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.
467
406
 
468
407
  Parameters:
469
408
  name: The name for the dynamically created model class.
@@ -111,7 +111,12 @@ class DatabaseInterface(DBBasedInterface[GeneralManagerModel]):
111
111
  if isinstance(value, GeneralManager):
112
112
  value = value.identification["id"]
113
113
  key = f"{key}_id"
114
- setattr(instance, key, value)
114
+ try:
115
+ setattr(instance, key, value)
116
+ except ValueError as e:
117
+ raise ValueError(f"Invalid value for {key}: {value}") from e
118
+ except TypeError as e:
119
+ raise TypeError(f"Type error for {key}: {e}") from e
115
120
  return instance
116
121
 
117
122
  @staticmethod
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+ from typing import Type, ClassVar, Any, Callable, TYPE_CHECKING, TypeVar
3
+ from django.db import models
4
+ from django.conf import settings
5
+ from simple_history.models import HistoricalRecords # type: ignore
6
+ from django.core.exceptions import ValidationError
7
+
8
+
9
+ if TYPE_CHECKING:
10
+ from general_manager.manager.generalManager import GeneralManager
11
+ from django.contrib.auth.models import AbstractUser
12
+ from general_manager.rule.rule import Rule
13
+
14
+ modelsModel = TypeVar("modelsModel", bound=models.Model)
15
+
16
+
17
+ def getFullCleanMethode(model: Type[models.Model]) -> Callable[..., None]:
18
+ """
19
+ Return a custom `full_clean` method for a Django model that performs both standard validation and additional rule-based checks.
20
+
21
+ The generated method first applies Django's built-in model validation, then evaluates custom rules defined in the model's `_meta.rules` attribute. If any validation or rule fails, it raises a `ValidationError` containing all collected errors.
22
+
23
+ Parameters:
24
+ model (Type[models.Model]): The Django model class for which to generate the custom `full_clean` method.
25
+
26
+ Returns:
27
+ Callable[..., None]: A `full_clean` method that can be assigned to the model class.
28
+ """
29
+
30
+ def full_clean(self: models.Model, *args: Any, **kwargs: Any):
31
+ """
32
+ Performs full validation on the model instance, including both standard Django validation and custom rule-based checks.
33
+
34
+ Aggregates errors from Django's built-in validation and any additional rules defined in the model's `_meta.rules` attribute. Raises a `ValidationError` containing all collected errors if any validation or rule check fails.
35
+ """
36
+ errors: dict[str, Any] = {}
37
+ try:
38
+ super(model, self).full_clean(*args, **kwargs) # type: ignore
39
+ except ValidationError as e:
40
+ errors.update(e.message_dict)
41
+
42
+ rules: list[Rule] = getattr(self._meta, "rules")
43
+ for rule in rules:
44
+ if not rule.evaluate(self):
45
+ error_message = rule.getErrorMessage()
46
+ if error_message:
47
+ errors.update(error_message)
48
+
49
+ if errors:
50
+ raise ValidationError(errors)
51
+
52
+ return full_clean
53
+
54
+
55
+ class GeneralManagerBasisModel(models.Model):
56
+ _general_manager_class: ClassVar[Type[GeneralManager]]
57
+ is_active = models.BooleanField(default=True)
58
+ history = HistoricalRecords(inherit=True)
59
+
60
+ class Meta:
61
+ abstract = True
62
+
63
+
64
+ class GeneralManagerModel(GeneralManagerBasisModel):
65
+ changed_by = models.ForeignKey(
66
+ settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True
67
+ )
68
+ changed_by_id: int | None
69
+
70
+ @property
71
+ def _history_user(self) -> AbstractUser | None:
72
+ """
73
+ Returns the user who last modified this model instance, or None if no user is set.
74
+ """
75
+ return self.changed_by
76
+
77
+ @_history_user.setter
78
+ def _history_user(self, value: AbstractUser) -> None:
79
+ """
80
+ Set the user responsible for the most recent change to the model instance.
81
+
82
+ Parameters:
83
+ value (AbstractUser): The user to associate with the latest modification.
84
+ """
85
+ self.changed_by = value
86
+
87
+ class Meta: # type: ignore
88
+ abstract = True
@@ -25,9 +25,9 @@ class GeneralManagerMeta(type):
25
25
 
26
26
  def __new__(mcs, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> type:
27
27
  """
28
- Create a new class using the metaclass, integrating interface hooks and registering the class for attribute initialization and tracking.
28
+ Creates a new class using the metaclass, integrating interface hooks and registering the class for attribute initialization and tracking.
29
29
 
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. If the `AUTOCREATE_GRAPHQL` setting is enabled, also registers the class for pending GraphQL interface creation.
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.
@@ -37,10 +37,10 @@ class GeneralManagerMeta(type):
37
37
  mcs, name: str, bases: tuple[type, ...], attrs: dict[str, Any]
38
38
  ) -> Type[GeneralManager]:
39
39
  """
40
- Create a new general manager class using the standard metaclass instantiation process.
40
+ Create a new GeneralManager class using the standard metaclass instantiation process.
41
41
 
42
42
  Returns:
43
- The newly created general manager class.
43
+ The newly created GeneralManager subclass.
44
44
  """
45
45
  return super().__new__(mcs, name, bases, attrs)
46
46
 
@@ -59,7 +59,6 @@ class GeneralManagerMeta(type):
59
59
 
60
60
  else:
61
61
  new_class = createNewGeneralManagerClass(mcs, name, bases, attrs)
62
-
63
62
  if getattr(settings, "AUTOCREATE_GRAPHQL", False):
64
63
  mcs.pending_graphql_interfaces.append(new_class)
65
64
 
@@ -69,13 +68,22 @@ class GeneralManagerMeta(type):
69
68
  def createAtPropertiesForAttributes(
70
69
  attributes: Iterable[str], new_class: Type[GeneralManager]
71
70
  ):
72
-
73
71
  """
74
- Dynamically assigns property descriptors to a class for the specified attribute names.
72
+ Dynamically assigns property descriptors to a class for each specified attribute name.
75
73
 
76
- For each attribute name, creates a descriptor that retrieves the value from an instance's `_attributes` dictionary. If accessed on the class, returns the field type from the class's interface. If the attribute is callable, it is invoked with the instance's interface. Raises `AttributeError` if the attribute is missing or if an error occurs during callable invocation.
74
+ For each attribute, creates a descriptor that:
75
+ - Returns the field type from the class's interface when accessed on the class.
76
+ - Retrieves the value from the instance's `_attributes` dictionary when accessed on an instance.
77
+ - Invokes the attribute with the instance's interface if it is callable.
78
+ - Raises `AttributeError` if the attribute is missing or if an error occurs during callable invocation.
77
79
  """
80
+
78
81
  def desciptorMethod(attr_name: str, new_class: type):
82
+ """
83
+ Creates a property descriptor for an attribute, enabling dynamic access and callable resolution.
84
+
85
+ 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
+ """
79
87
  class Descriptor(Generic[GeneralManagerType]):
80
88
  def __init__(self, attr_name: str, new_class: Type[GeneralManager]):
81
89
  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)))
@@ -0,0 +1,124 @@
1
+ from graphene_django.utils.testing import GraphQLTransactionTestCase
2
+ from general_manager.apps import GeneralmanagerConfig
3
+ from importlib import import_module
4
+ from django.db import connection
5
+ from django.conf import settings
6
+ from typing import cast
7
+ from django.db import models
8
+ from general_manager.manager.generalManager import GeneralManager
9
+ from general_manager.api.graphql import GraphQL
10
+ from django.apps import apps as global_apps
11
+
12
+
13
+ _original_get_app = global_apps.get_containing_app_config
14
+
15
+
16
+ def createFallbackGetApp(fallback_app: str):
17
+ """
18
+ Creates a fallback function for getting the app config, which returns the specified fallback app if the original lookup fails.
19
+
20
+ Parameters:
21
+ fallback_app (str): The name of the app to return if the original lookup fails.
22
+
23
+ Returns:
24
+ function: A function that attempts to get the app config for a given object name, falling back to the specified app if not found.
25
+ """
26
+
27
+ def _fallback_get_app(object_name: str):
28
+ cfg = _original_get_app(object_name)
29
+ if cfg is not None:
30
+ return cfg
31
+ try:
32
+ return global_apps.get_app_config(fallback_app)
33
+ except LookupError:
34
+ return None
35
+
36
+ return _fallback_get_app
37
+
38
+
39
+ def _default_graphql_url_clear():
40
+ """
41
+ Removes the first URL pattern for the GraphQL view from the project's root URL configuration.
42
+
43
+ This function searches the root URL patterns for a pattern whose callback is a `GraphQLView` and removes it, effectively clearing the default GraphQL endpoint from the URL configuration.
44
+ """
45
+ urlconf = import_module(settings.ROOT_URLCONF)
46
+ for pattern in urlconf.urlpatterns:
47
+ if (
48
+ hasattr(pattern, "callback")
49
+ and hasattr(pattern.callback, "view_class")
50
+ and pattern.callback.view_class.__name__ == "GraphQLView"
51
+ ):
52
+ urlconf.urlpatterns.remove(pattern)
53
+ break
54
+
55
+
56
+ class GMTestCaseMeta(type):
57
+ """
58
+ Metaclass that wraps setUpClass: first calls user-defined setup,
59
+ then performs GM environment initialization, then super().setUpClass().
60
+ """
61
+
62
+ def __new__(mcs, name, bases, attrs):
63
+ """
64
+ Creates a new test case class with a customized setUpClass that prepares the database schema and GraphQL environment for GeneralManager integration tests.
65
+
66
+ The generated setUpClass method resets GraphQL class registries, invokes any user-defined setUpClass, clears default GraphQL URL patterns, creates missing database tables for specified GeneralManager classes and their history models, initializes GeneralManager and GraphQL configurations, and finally calls the original GraphQLTransactionTestCase setUpClass.
67
+ """
68
+ user_setup = attrs.get("setUpClass")
69
+ fallback_app = attrs.get("fallback_app", "general_manager")
70
+ # MERKE dir das echte GraphQLTransactionTestCase.setUpClass
71
+ base_setup = GraphQLTransactionTestCase.setUpClass
72
+
73
+ def wrapped_setUpClass(cls):
74
+ """
75
+ Performs comprehensive setup for a test case class, initializing GraphQL and GeneralManager environments and ensuring required database tables exist.
76
+
77
+ This method resets internal GraphQL registries, invokes any user-defined setup, removes default GraphQL URL patterns, creates missing database tables for models and their history associated with specified GeneralManager classes, initializes GeneralManager and GraphQL configurations, and finally calls the base test case setup.
78
+ """
79
+ GraphQL._query_class = None
80
+ GraphQL._mutation_class = None
81
+ GraphQL._mutations = {}
82
+ GraphQL._query_fields = {}
83
+ GraphQL.graphql_type_registry = {}
84
+ GraphQL.graphql_filter_type_registry = {}
85
+
86
+ if fallback_app is not None:
87
+ global_apps.get_containing_app_config = createFallbackGetApp(
88
+ fallback_app
89
+ )
90
+
91
+ # 1) user-defined setUpClass (if any)
92
+ if user_setup:
93
+ user_setup.__func__(cls)
94
+ # 2) clear URL patterns
95
+ _default_graphql_url_clear()
96
+ # 3) register models & create tables
97
+ existing = connection.introspection.table_names()
98
+ with connection.schema_editor() as editor:
99
+ for manager_class in cls.general_manager_classes:
100
+ model_class = cast(
101
+ type[models.Model], manager_class.Interface._model # type: ignore
102
+ )
103
+ if model_class._meta.db_table not in existing:
104
+ editor.create_model(model_class)
105
+ editor.create_model(model_class.history.model) # type: ignore
106
+ # 4) GM & GraphQL initialization
107
+ GeneralmanagerConfig.initializeGeneralManagerClasses(
108
+ cls.general_manager_classes, cls.general_manager_classes
109
+ )
110
+ GeneralmanagerConfig.handleReadOnlyInterface(cls.read_only_classes)
111
+ GeneralmanagerConfig.handleGraphQL(cls.general_manager_classes)
112
+ # 5) GraphQLTransactionTestCase.setUpClass
113
+ base_setup.__func__(cls)
114
+
115
+ attrs["setUpClass"] = classmethod(wrapped_setUpClass)
116
+ return super().__new__(mcs, name, bases, attrs)
117
+
118
+
119
+ class GeneralManagerTransactionTestCase(
120
+ GraphQLTransactionTestCase, metaclass=GMTestCaseMeta
121
+ ):
122
+ general_manager_classes: list[type[GeneralManager]] = []
123
+ read_only_classes: list[type[GeneralManager]] = []
124
+ fallback_app: str | None = "general_manager"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.8.0
3
+ Version: 0.9.1
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,5 +1,5 @@
1
1
  general_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- general_manager/apps.py,sha256=P_SmEPHto0Wl7cgP6l2MAOQJgXjuGBLzRIidpfHsdc0,8316
2
+ general_manager/apps.py,sha256=Q6_by4VT4BtL6TxqyI0tG3XeiU3C3QZSiH4ok9a4XJY,8928
3
3
  general_manager/api/graphql.py,sha256=ZMzlpgmyn2X9Zwxo12r-b5UZ2R-u1pXys_nhNczxyX8,31622
4
4
  general_manager/api/mutation.py,sha256=RYCogAdUpUedyh2B9keMAzq9u-iIhEKAsoiw5xXmhrQ,5669
5
5
  general_manager/api/property.py,sha256=oc93p1P8dcIvrNorRuqD1EJVsd6eYttYhZuAS0s28gs,696
@@ -27,16 +27,17 @@ general_manager/factory/factoryMethods.py,sha256=9Bag891j0XHe3dUBAFi7gUKcKeUwcBZ
27
27
  general_manager/interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  general_manager/interface/baseInterface.py,sha256=GCMo0MGlaRAElovfI34qfuWuVYOyTQLG0OA-ZJx8i3s,8604
29
29
  general_manager/interface/calculationInterface.py,sha256=Kg_OqLw67tcLwdzYNLq31eKVLzkM7taw-8Mzmk0CYi0,4232
30
- general_manager/interface/databaseBasedInterface.py,sha256=twdqUyvPci77WXqCX0k2dpFajhx38bbUfgxrV_BjcxQ,22788
31
- general_manager/interface/databaseInterface.py,sha256=kYPuwjAVcVMbxozc-eg-LwGY1C0k-sqt73b7DSlVxWI,6389
30
+ general_manager/interface/databaseBasedInterface.py,sha256=S7xSEYnguihx5RD633hwXcDk830a9-NwOwOnCFmeJMo,20738
31
+ general_manager/interface/databaseInterface.py,sha256=UBmsfS6gF-3G6slvrGsq3UbKhSkWBWqtSOzRmdsw3Cw,6627
32
+ general_manager/interface/models.py,sha256=gGYW5f1AUBpBakV3O0qsZwqMiWxZGdKRYXWaCBjt1oI,3334
32
33
  general_manager/interface/readOnlyInterface.py,sha256=TkfbOeaa2wCq5kCv0a3IwJWcYOTVbtNsdNWmGAz0Mns,11217
33
34
  general_manager/manager/__init__.py,sha256=l3RYp62aEhj3Y975_XUTIzo35LUnkTJHkb_hgChnXXI,111
34
35
  general_manager/manager/generalManager.py,sha256=AVFZICHzqiIyn7lgPU_WLH8X8WLP1edvWAE5CljZPrk,9178
35
36
  general_manager/manager/groupManager.py,sha256=8dpZUfm7aFL4lraUWv4qbbDRClQZaYxw4prclhBZYZs,4367
36
37
  general_manager/manager/input.py,sha256=-pJXGJ-g2-OxZfl4Buj3mQkf05fN4p8MsR2Lh9BQcEo,3208
37
- general_manager/manager/meta.py,sha256=gtzFKBMCvyAx5qo2BVuTBqHZzngR-ivu8YLY7oiSUEk,5059
38
+ general_manager/manager/meta.py,sha256=IN5Xzz4lcUBe2umqvBz84qoyjkzKubNaMwfuYFQjFGU,5631
38
39
  general_manager/measurement/__init__.py,sha256=X97meFujBldE5v0WMF7SmKeGpC5R0JTczfLo_Lq1Xek,84
39
- general_manager/measurement/measurement.py,sha256=e_FjHieeJbBtjXGCO9J7vRPw6KCkMrOxwWjaD0m8ee4,11777
40
+ general_manager/measurement/measurement.py,sha256=0jVU6eZmEwDs0CW_W_RpUxaWDPd9J4uQXWPpxMkfJWQ,16079
40
41
  general_manager/measurement/measurementField.py,sha256=iq9Hqe6ZGX8CxXm4nIqTAWTRkQVptzpqE9ExX-jFyNs,5928
41
42
  general_manager/permission/__init__.py,sha256=5UlDERN60Vn8obGVkT-cOM8kHjzmoxgK5w5FgTCDhGE,59
42
43
  general_manager/permission/basePermission.py,sha256=14iKo6qVmaUdg1sAz-gSZyNtpVKAAapIhutVAMDf93c,6056
@@ -47,8 +48,9 @@ general_manager/permission/permissionDataManager.py,sha256=Ji7fsnuaKTa6M8yzCGyzr
47
48
  general_manager/rule/__init__.py,sha256=4Har5cfPD1fmOsilTDod-ZUz3Com-tkl58jz7yY4fD0,23
48
49
  general_manager/rule/handler.py,sha256=z8SFHTIZ0LbLh3fV56Mud0V4_OvWkqJjlHvFqau7Qfk,7334
49
50
  general_manager/rule/rule.py,sha256=3FVCKGL7BTVoStdgOTdWQwuoVRIxAIAilV4VOzouDpc,10759
50
- generalmanager-0.8.0.dist-info/licenses/LICENSE,sha256=YGFm0ieb4KpkMRRt2qnWue6uFh0cUMtobwEBkHwajhc,1450
51
- generalmanager-0.8.0.dist-info/METADATA,sha256=awQ6aVm9myhb0KpGFMC_2Pc_McXqbMzECdrwxeORSkM,6205
52
- generalmanager-0.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
- generalmanager-0.8.0.dist-info/top_level.txt,sha256=sTDtExP9ga-YP3h3h42yivUY-A2Q23C2nw6LNKOho4I,16
54
- generalmanager-0.8.0.dist-info/RECORD,,
51
+ general_manager/utils/testing.py,sha256=R6l-9PVAgxeVywvynkzSR6xXcHCu4z2UzRqzHDVrBUY,5591
52
+ generalmanager-0.9.1.dist-info/licenses/LICENSE,sha256=YGFm0ieb4KpkMRRt2qnWue6uFh0cUMtobwEBkHwajhc,1450
53
+ generalmanager-0.9.1.dist-info/METADATA,sha256=KlP-5CSEpz1tbI4ec2SzBKT8aSCBWhiUboTS5zgLzoQ,6205
54
+ generalmanager-0.9.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
55
+ generalmanager-0.9.1.dist-info/top_level.txt,sha256=sTDtExP9ga-YP3h3h42yivUY-A2Q23C2nw6LNKOho4I,16
56
+ generalmanager-0.9.1.dist-info/RECORD,,