GeneralManager 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. general_manager/__init__.py +0 -0
  2. general_manager/api/graphql.py +732 -0
  3. general_manager/api/mutation.py +143 -0
  4. general_manager/api/property.py +20 -0
  5. general_manager/apps.py +83 -0
  6. general_manager/auxiliary/__init__.py +2 -0
  7. general_manager/auxiliary/argsToKwargs.py +25 -0
  8. general_manager/auxiliary/filterParser.py +97 -0
  9. general_manager/auxiliary/noneToZero.py +12 -0
  10. general_manager/cache/cacheDecorator.py +72 -0
  11. general_manager/cache/cacheTracker.py +33 -0
  12. general_manager/cache/dependencyIndex.py +300 -0
  13. general_manager/cache/pathMapping.py +151 -0
  14. general_manager/cache/signals.py +48 -0
  15. general_manager/factory/__init__.py +5 -0
  16. general_manager/factory/factories.py +287 -0
  17. general_manager/factory/lazy_methods.py +38 -0
  18. general_manager/interface/__init__.py +3 -0
  19. general_manager/interface/baseInterface.py +308 -0
  20. general_manager/interface/calculationInterface.py +406 -0
  21. general_manager/interface/databaseInterface.py +726 -0
  22. general_manager/manager/__init__.py +3 -0
  23. general_manager/manager/generalManager.py +136 -0
  24. general_manager/manager/groupManager.py +288 -0
  25. general_manager/manager/input.py +48 -0
  26. general_manager/manager/meta.py +75 -0
  27. general_manager/measurement/__init__.py +2 -0
  28. general_manager/measurement/measurement.py +233 -0
  29. general_manager/measurement/measurementField.py +152 -0
  30. general_manager/permission/__init__.py +1 -0
  31. general_manager/permission/basePermission.py +178 -0
  32. general_manager/permission/fileBasedPermission.py +0 -0
  33. general_manager/permission/managerBasedPermission.py +171 -0
  34. general_manager/permission/permissionChecks.py +53 -0
  35. general_manager/permission/permissionDataManager.py +55 -0
  36. general_manager/rule/__init__.py +1 -0
  37. general_manager/rule/handler.py +122 -0
  38. general_manager/rule/rule.py +313 -0
  39. generalmanager-0.0.0.dist-info/METADATA +207 -0
  40. generalmanager-0.0.0.dist-info/RECORD +43 -0
  41. generalmanager-0.0.0.dist-info/WHEEL +5 -0
  42. generalmanager-0.0.0.dist-info/licenses/LICENSE +29 -0
  43. generalmanager-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,233 @@
1
+ # units.py
2
+ from __future__ import annotations
3
+ from typing import Any, Callable
4
+ import pint
5
+ from decimal import Decimal, getcontext, InvalidOperation
6
+ from operator import eq, ne, lt, le, gt, ge
7
+
8
+
9
+ # Set precision for Decimal
10
+ getcontext().prec = 28
11
+
12
+ # Create a new UnitRegistry
13
+ ureg = pint.UnitRegistry(auto_reduce_dimensions=True)
14
+
15
+ # Define currency units
16
+ currency_units = ["EUR", "USD", "GBP", "JPY", "CHF", "AUD", "CAD"]
17
+ for currency in currency_units:
18
+ # Define each currency as its own dimension
19
+ ureg.define(f"{currency} = [{currency}]")
20
+
21
+
22
+ class Measurement:
23
+ def __init__(self, value: Decimal | float | int | str, unit: str):
24
+ if not isinstance(value, (Decimal, float, int)):
25
+ try:
26
+ value = Decimal(str(value))
27
+ except Exception:
28
+ raise TypeError("Value must be a Decimal, float, int or compatible.")
29
+ if not isinstance(value, Decimal):
30
+ value = Decimal(str(value))
31
+ self.__quantity = self.formatDecimal(value) * ureg.Quantity(1, unit)
32
+
33
+ def __getstate__(self):
34
+ state = {
35
+ "magnitude": str(self.quantity.magnitude),
36
+ "unit": str(self.quantity.units),
37
+ }
38
+ return state
39
+
40
+ def __setstate__(self, state):
41
+ value = Decimal(state["magnitude"])
42
+ unit = state["unit"]
43
+ self.__quantity = self.formatDecimal(value) * ureg.Quantity(1, unit)
44
+
45
+ @property
46
+ def quantity(self) -> pint.Quantity:
47
+ return self.__quantity
48
+
49
+ @classmethod
50
+ def from_string(cls, value: str) -> Measurement:
51
+ value, unit = value.split(" ")
52
+ return cls(value, unit)
53
+
54
+ @staticmethod
55
+ def formatDecimal(value: Decimal) -> Decimal:
56
+ value = value.normalize()
57
+ if value == value.to_integral_value():
58
+ try:
59
+ return value.quantize(Decimal("1"))
60
+ except InvalidOperation:
61
+ return value
62
+ else:
63
+ return value
64
+
65
+ def to(self, target_unit: str, exchange_rate: float | None = None):
66
+ if self.is_currency():
67
+ if self.quantity.units == ureg(target_unit):
68
+ return self # Same currency, no conversion needed
69
+ elif exchange_rate is not None:
70
+ # Convert using the provided exchange rate
71
+ value = self.quantity.magnitude * Decimal(str(exchange_rate))
72
+ return Measurement(value, target_unit)
73
+ else:
74
+ raise ValueError(
75
+ "Conversion between currencies requires an exchange rate."
76
+ )
77
+ else:
78
+ # Standard conversion for physical units
79
+ converted_quantity: pint.Quantity = self.quantity.to(target_unit) # type: ignore
80
+ value = Decimal(str(converted_quantity.magnitude))
81
+ unit = str(converted_quantity.units)
82
+ return Measurement(value, unit)
83
+
84
+ def is_currency(self):
85
+ # Check if the unit is a defined currency
86
+ return str(self.quantity.units) in currency_units
87
+
88
+ def __add__(self, other: Any) -> Measurement:
89
+ if not isinstance(other, Measurement):
90
+ raise TypeError("Addition is only allowed between Measurement instances.")
91
+ if self.is_currency() and other.is_currency():
92
+ # Both are currencies
93
+ if self.quantity.units != other.quantity.units:
94
+ raise ValueError(
95
+ "Addition between different currencies is not allowed."
96
+ )
97
+ result_quantity = self.quantity + other.quantity
98
+ if not isinstance(result_quantity, pint.Quantity):
99
+ raise ValueError("Units are not compatible for addition.")
100
+ return Measurement(
101
+ Decimal(str(result_quantity.magnitude)), str(result_quantity.units)
102
+ )
103
+ elif not self.is_currency() and not other.is_currency():
104
+ # Both are physical units
105
+ if self.quantity.dimensionality != other.quantity.dimensionality:
106
+ raise ValueError("Units are not compatible for addition.")
107
+ result_quantity = self.quantity + other.quantity
108
+ if not isinstance(result_quantity, pint.Quantity):
109
+ raise ValueError("Units are not compatible for addition.")
110
+ return Measurement(
111
+ Decimal(str(result_quantity.magnitude)), str(result_quantity.units)
112
+ )
113
+ else:
114
+ raise TypeError(
115
+ "Addition between currency and physical unit is not allowed."
116
+ )
117
+
118
+ def __sub__(self, other: Any) -> Measurement:
119
+ if not isinstance(other, Measurement):
120
+ raise TypeError(
121
+ "Subtraction is only allowed between Measurement instances."
122
+ )
123
+ if self.is_currency() and other.is_currency():
124
+ # Both are currencies
125
+ if self.quantity.units != other.quantity.units:
126
+ raise ValueError(
127
+ "Subtraction between different currencies is not allowed."
128
+ )
129
+ result_quantity = self.quantity - other.quantity
130
+ return Measurement(
131
+ Decimal(str(result_quantity.magnitude)), str(self.quantity.units)
132
+ )
133
+ elif not self.is_currency() and not other.is_currency():
134
+ # Both are physical units
135
+ if self.quantity.dimensionality != other.quantity.dimensionality:
136
+ raise ValueError("Units are not compatible for subtraction.")
137
+ result_quantity = self.quantity - other.quantity
138
+ return Measurement(
139
+ Decimal(str(result_quantity.magnitude)), str(self.quantity.units)
140
+ )
141
+ else:
142
+ raise TypeError(
143
+ "Subtraction between currency and physical unit is not allowed."
144
+ )
145
+
146
+ def __mul__(self, other: Any) -> Measurement:
147
+ if isinstance(other, Measurement):
148
+ if self.is_currency() or other.is_currency():
149
+ raise TypeError(
150
+ "Multiplication between two currency amounts is not allowed."
151
+ )
152
+ result_quantity = self.quantity * other.quantity
153
+ return Measurement(
154
+ Decimal(str(result_quantity.magnitude)), str(result_quantity.units)
155
+ )
156
+ elif isinstance(other, (Decimal, float, int)):
157
+ if not isinstance(other, Decimal):
158
+ other = Decimal(str(other))
159
+ result_quantity = self.quantity * other
160
+ return Measurement(
161
+ Decimal(str(result_quantity.magnitude)), str(self.quantity.units)
162
+ )
163
+ else:
164
+ raise TypeError(
165
+ "Multiplication is only allowed with Measurement or numeric values."
166
+ )
167
+
168
+ def __truediv__(self, other: Any) -> Measurement:
169
+ if isinstance(other, Measurement):
170
+ if self.is_currency() and other.is_currency():
171
+ raise TypeError("Division between two currency amounts is not allowed.")
172
+ result_quantity = self.quantity / other.quantity
173
+ return Measurement(
174
+ Decimal(str(result_quantity.magnitude)), str(result_quantity.units)
175
+ )
176
+ elif isinstance(other, (Decimal, float, int)):
177
+ if not isinstance(other, Decimal):
178
+ other = Decimal(str(other))
179
+ result_quantity = self.quantity / other
180
+ return Measurement(
181
+ Decimal(str(result_quantity.magnitude)), str(self.quantity.units)
182
+ )
183
+ else:
184
+ raise TypeError(
185
+ "Division is only allowed with Measurement or numeric values."
186
+ )
187
+
188
+ def __str__(self):
189
+ if not str(self.quantity.units) == "dimensionless":
190
+ return f"{self.quantity.magnitude} {self.quantity.units}"
191
+ return f"{self.quantity.magnitude}"
192
+
193
+ def __repr__(self):
194
+ return f"Measurement({self.quantity.magnitude}, '{self.quantity.units}')"
195
+
196
+ def _compare(self, other: Any, operation: Callable[..., bool]) -> bool:
197
+ if isinstance(other, str):
198
+ other = Measurement.from_string(other)
199
+
200
+ # Überprüfen, ob `other` ein Measurement-Objekt ist
201
+ if not isinstance(other, Measurement):
202
+ return NotImplemented
203
+ try:
204
+ # Convert `other` to the same units as `self`
205
+ other_converted: pint.Quantity = other.quantity.to(self.quantity.units) # type: ignore
206
+ # Apply the comparison operation
207
+ return operation(self.quantity.magnitude, other_converted.magnitude)
208
+ except pint.DimensionalityError:
209
+ raise ValueError("Cannot compare measurements with different dimensions.")
210
+
211
+ def __radd__(self, other: Any) -> Measurement:
212
+ if other == 0:
213
+ return self
214
+ return self.__add__(other)
215
+
216
+ # Comparison Operators
217
+ def __eq__(self, other: Any) -> bool:
218
+ return self._compare(other, eq)
219
+
220
+ def __ne__(self, other: Any) -> bool:
221
+ return self._compare(other, ne)
222
+
223
+ def __lt__(self, other: Any) -> bool:
224
+ return self._compare(other, lt)
225
+
226
+ def __le__(self, other: Any) -> bool:
227
+ return self._compare(other, le)
228
+
229
+ def __gt__(self, other: Any) -> bool:
230
+ return self._compare(other, gt)
231
+
232
+ def __ge__(self, other: Any) -> bool:
233
+ return self._compare(other, ge)
@@ -0,0 +1,152 @@
1
+ # fields.py
2
+ from __future__ import annotations
3
+ from django.db import models
4
+ from django.core.exceptions import ValidationError
5
+ from decimal import Decimal
6
+ from general_manager.measurement.measurement import Measurement, ureg, currency_units
7
+ import pint
8
+ from typing import Any
9
+
10
+
11
+ class MeasurementField(models.Field): # type: ignore
12
+ description = (
13
+ "A field that stores a measurement value, both in base unit and original unit"
14
+ )
15
+
16
+ def __init__(
17
+ self,
18
+ base_unit: str,
19
+ null: bool = False,
20
+ blank: bool = False,
21
+ editable: bool = True,
22
+ *args: list[Any],
23
+ **kwargs: dict[str, Any],
24
+ ):
25
+ self.base_unit = base_unit # E.g., 'meter' for length units
26
+ # Determine the dimensionality of the base unit
27
+ self.base_dimension = ureg.parse_expression(self.base_unit).dimensionality
28
+ # Internal fields
29
+ null_blank_kwargs = {}
30
+ if null is True:
31
+ null_blank_kwargs["null"] = True
32
+ if blank is True:
33
+ null_blank_kwargs["blank"] = True
34
+ self.editable = editable
35
+ self.value_field: models.DecimalField[Decimal] = models.DecimalField(
36
+ max_digits=30,
37
+ decimal_places=10,
38
+ db_index=True,
39
+ **null_blank_kwargs,
40
+ editable=editable,
41
+ )
42
+ self.unit_field: models.CharField[str] = models.CharField(
43
+ max_length=30, **null_blank_kwargs, editable=editable
44
+ )
45
+ super().__init__(null=null, blank=blank, *args, **kwargs)
46
+
47
+ def contribute_to_class(
48
+ self, cls: type, name: str, private_only: bool = False, **kwargs: dict[str, Any]
49
+ ) -> None:
50
+ self.name = name
51
+ self.value_attr = f"{name}_value"
52
+ self.unit_attr = f"{name}_unit"
53
+ self.value_field.attname = self.value_attr
54
+ self.unit_field.attname = self.unit_attr
55
+ self.value_field.name = self.value_attr
56
+ self.unit_field.name = self.unit_attr
57
+ self.value_field.column = self.value_attr
58
+ self.unit_field.column = self.unit_attr
59
+
60
+ self.value_field.model = cls
61
+ self.unit_field.model = cls
62
+
63
+ self.value_field.contribute_to_class(cls, self.value_attr)
64
+ self.unit_field.contribute_to_class(cls, self.unit_attr)
65
+
66
+ setattr(cls, self.name, self)
67
+
68
+ def __get__(self, instance: Any, owner: Any) -> Any:
69
+ if instance is None:
70
+ return self
71
+ value = getattr(instance, self.value_attr)
72
+ unit = getattr(instance, self.unit_attr)
73
+ if value is None or unit is None:
74
+ return None
75
+ # Create a Measurement object with the value in the original unit
76
+ quantity_in_base_unit = Decimal(value) * ureg(self.base_unit)
77
+ # Convert back to the original unit
78
+ try:
79
+ quantity_in_original_unit: pint.Quantity = quantity_in_base_unit.to(unit) # type: ignore
80
+ except pint.errors.DimensionalityError:
81
+ # If the unit is not compatible, return the value in base unit
82
+ quantity_in_original_unit = quantity_in_base_unit
83
+ return Measurement(
84
+ quantity_in_original_unit.magnitude, str(quantity_in_original_unit.units)
85
+ )
86
+
87
+ def __set__(self, instance: Any, value: Any) -> None:
88
+ if self.editable is False:
89
+ raise ValidationError(f"{self.name} is not editable.")
90
+ if value is None:
91
+ setattr(instance, self.value_attr, None)
92
+ setattr(instance, self.unit_attr, None)
93
+ return
94
+ elif isinstance(value, str):
95
+ try:
96
+ value = Measurement.from_string(value)
97
+ except ValueError:
98
+ raise ValidationError(
99
+ {self.name: ["Value must be a Measurement instance or None."]}
100
+ )
101
+ if isinstance(value, Measurement):
102
+ if str(self.base_unit) in currency_units:
103
+ # Base unit is a currency
104
+ if not value.is_currency():
105
+ raise ValidationError(
106
+ {
107
+ self.name: [
108
+ f"The unit must be a currency ({', '.join(currency_units)})."
109
+ ]
110
+ }
111
+ )
112
+ else:
113
+ # Physical unit
114
+ if value.is_currency():
115
+ raise ValidationError(
116
+ {self.name: ["The unit cannot be a currency."]}
117
+ )
118
+ elif value.quantity.dimensionality != self.base_dimension:
119
+ raise ValidationError(
120
+ {
121
+ self.name: [
122
+ f"The unit must be compatible with '{self.base_unit}'."
123
+ ]
124
+ }
125
+ )
126
+ # Store the value in the base unit
127
+ try:
128
+ value_in_base_unit: Any = value.quantity.to(self.base_unit).magnitude # type: ignore
129
+ except pint.errors.DimensionalityError:
130
+ raise ValidationError(
131
+ {
132
+ self.name: [
133
+ f"The unit must be compatible with '{self.base_unit}'."
134
+ ]
135
+ }
136
+ )
137
+ setattr(instance, self.value_attr, Decimal(str(value_in_base_unit)))
138
+ # Store the original unit
139
+ setattr(instance, self.unit_attr, str(value.quantity.units))
140
+ else:
141
+ raise ValidationError(
142
+ {self.name: ["Value must be a Measurement instance or None."]}
143
+ )
144
+
145
+ def get_prep_value(self, value: Any) -> Any:
146
+ # Not needed since we use internal fields
147
+ pass
148
+
149
+ def deconstruct(self):
150
+ name, path, args, kwargs = super().deconstruct()
151
+ kwargs["base_unit"] = self.base_unit
152
+ return name, path, args, kwargs
@@ -0,0 +1 @@
1
+ from .managerBasedPermission import ManagerBasedPermission
@@ -0,0 +1,178 @@
1
+ from __future__ import annotations
2
+ from abc import ABC, abstractmethod
3
+ from typing import TYPE_CHECKING, Any, Literal
4
+ from general_manager.permission.permissionChecks import (
5
+ permission_functions,
6
+ permission_filter,
7
+ )
8
+
9
+
10
+ if TYPE_CHECKING:
11
+ from django.contrib.auth.models import AbstractUser, AnonymousUser
12
+ from general_manager.permission.permissionDataManager import (
13
+ PermissionDataManager,
14
+ )
15
+ from general_manager.manager.generalManager import GeneralManager
16
+ from general_manager.manager.meta import GeneralManagerMeta
17
+
18
+
19
+ class BasePermission(ABC):
20
+
21
+ def __init__(
22
+ self,
23
+ instance: PermissionDataManager | GeneralManager | GeneralManagerMeta,
24
+ request_user: AbstractUser | AnonymousUser,
25
+ ) -> None:
26
+ self._instance = instance
27
+ self._request_user = request_user
28
+
29
+ @property
30
+ def instance(self) -> PermissionDataManager | GeneralManager | GeneralManagerMeta:
31
+ return self._instance
32
+
33
+ @property
34
+ def request_user(self) -> AbstractUser | AnonymousUser:
35
+ return self._request_user
36
+
37
+ @classmethod
38
+ def checkCreatePermission(
39
+ cls,
40
+ data: dict[str, Any],
41
+ manager: type[GeneralManager],
42
+ request_user: AbstractUser | AnonymousUser | Any,
43
+ ) -> None:
44
+ request_user = cls.getUserWithId(request_user)
45
+ errors = []
46
+ permission_data = PermissionDataManager(permission_data=data, manager=manager)
47
+ Permission = cls(permission_data, request_user)
48
+ for key in data.keys():
49
+ is_allowed = Permission.checkPermission("create", key)
50
+ if not is_allowed:
51
+ errors.append(
52
+ f"Permission denied for {key} with value {data[key]} for user {request_user}"
53
+ )
54
+ if errors:
55
+ raise PermissionError(
56
+ f"Permission denied for user {request_user} with errors: {errors}"
57
+ )
58
+
59
+ @classmethod
60
+ def checkUpdatePermission(
61
+ cls,
62
+ data: dict[str, Any],
63
+ old_manager_instance: GeneralManager,
64
+ request_user: AbstractUser | AnonymousUser | Any,
65
+ ) -> None:
66
+ request_user = cls.getUserWithId(request_user)
67
+
68
+ errors = []
69
+ permission_data = PermissionDataManager[GeneralManager].forUpdate(
70
+ base_data=old_manager_instance, update_data=data
71
+ )
72
+ Permission = cls(permission_data, request_user)
73
+ for key in data.keys():
74
+ is_allowed = Permission.checkPermission("update", key)
75
+ if not is_allowed:
76
+ errors.append(
77
+ f"Permission denied for {key} with value {data[key]} for user {request_user}"
78
+ )
79
+ if errors:
80
+ raise PermissionError(
81
+ f"Permission denied for user {request_user} with errors: {errors}"
82
+ )
83
+
84
+ @classmethod
85
+ def checkDeletePermission(
86
+ cls,
87
+ manager_instance: GeneralManager,
88
+ request_user: AbstractUser | AnonymousUser | Any,
89
+ ) -> None:
90
+ request_user = cls.getUserWithId(request_user)
91
+
92
+ errors = []
93
+ permission_data = PermissionDataManager[GeneralManager](manager_instance)
94
+ Permission = cls(permission_data, request_user)
95
+ for key in manager_instance.__dict__.keys():
96
+ is_allowed = Permission.checkPermission("delete", key)
97
+ if not is_allowed:
98
+ errors.append(
99
+ f"Permission denied for {key} with value {getattr(manager_instance, key)} for user {request_user}"
100
+ )
101
+ if errors:
102
+ raise PermissionError(
103
+ f"Permission denied for user {request_user} with errors: {errors}"
104
+ )
105
+
106
+ @staticmethod
107
+ def getUserWithId(
108
+ user: Any | AbstractUser | AnonymousUser,
109
+ ) -> AbstractUser | AnonymousUser:
110
+ """
111
+ Returns the user with the given id
112
+ """
113
+ from django.contrib.auth.models import User
114
+
115
+ if isinstance(user, (AbstractUser, AnonymousUser)):
116
+ return user
117
+ try:
118
+ return User.objects.get(id=user)
119
+ except User.DoesNotExist:
120
+ return AnonymousUser()
121
+
122
+ @abstractmethod
123
+ def checkPermission(
124
+ self,
125
+ action: Literal["create", "read", "update", "delete"],
126
+ attriubte: str,
127
+ ) -> bool:
128
+ raise NotImplementedError
129
+
130
+ def getPermissionFilter(
131
+ self,
132
+ ) -> list[dict[Literal["filter", "exclude"], dict[str, str]]]:
133
+ """
134
+ Returns the filter for the permission
135
+ """
136
+ raise NotImplementedError
137
+
138
+ def _getPermissionFilter(
139
+ self, permission: str
140
+ ) -> dict[Literal["filter", "exclude"], dict[str, str]]:
141
+ """
142
+ Returns the filter for the permission
143
+ """
144
+ permission_function, *config = permission.split(":")
145
+ if permission_function not in permission_functions:
146
+ raise ValueError(f"Permission {permission} not found")
147
+ permission_filter = permission_functions[permission_function][
148
+ "permission_filter"
149
+ ](self.request_user, config)
150
+ if permission_filter is None:
151
+ return {"filter": {}, "exclude": {}}
152
+ return permission_filter
153
+
154
+ def validatePermissionString(
155
+ self,
156
+ permission: str,
157
+ ) -> bool:
158
+ # permission can be a combination of multiple permissions
159
+ # separated by "&" (e.g. "isAuthenticated&isMatchingKeyAccount")
160
+ # this means that all sub_permissions must be true
161
+ return all(
162
+ [
163
+ self.__validateSinglePermission(sub_permission)
164
+ for sub_permission in permission.split("&")
165
+ ]
166
+ )
167
+
168
+ def __validateSinglePermission(
169
+ self,
170
+ permission: str,
171
+ ) -> bool:
172
+ permission_function, *config = permission.split(":")
173
+ if permission_function not in permission_functions:
174
+ raise ValueError(f"Permission {permission} not found")
175
+
176
+ return permission_functions[permission_function]["permission_method"](
177
+ self.instance, self.request_user, config
178
+ )
File without changes