GeneralManager 0.3.1__tar.gz → 0.4.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 (73) hide show
  1. {generalmanager-0.3.1 → generalmanager-0.4.0}/GeneralManager.egg-info/PKG-INFO +1 -1
  2. {generalmanager-0.3.1 → generalmanager-0.4.0}/GeneralManager.egg-info/SOURCES.txt +6 -2
  3. {generalmanager-0.3.1 → generalmanager-0.4.0}/PKG-INFO +1 -1
  4. {generalmanager-0.3.1 → generalmanager-0.4.0}/pyproject.toml +1 -1
  5. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/cache/dependencyIndex.py +41 -45
  6. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/factory/__init__.py +1 -1
  7. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/factory/factories.py +71 -54
  8. generalmanager-0.4.0/src/general_manager/factory/factoryMethods.py +109 -0
  9. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/measurement/measurement.py +8 -0
  10. generalmanager-0.4.0/tests/test_dependencyIndex.py +967 -0
  11. generalmanager-0.4.0/tests/test_factories.py +350 -0
  12. generalmanager-0.4.0/tests/test_factoryMethods.py +228 -0
  13. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_noneToZero.py +2 -2
  14. generalmanager-0.4.0/tests/test_signals.py +109 -0
  15. generalmanager-0.3.1/src/general_manager/factory/lazy_methods.py +0 -38
  16. {generalmanager-0.3.1 → generalmanager-0.4.0}/GeneralManager.egg-info/dependency_links.txt +0 -0
  17. {generalmanager-0.3.1 → generalmanager-0.4.0}/GeneralManager.egg-info/requires.txt +0 -0
  18. {generalmanager-0.3.1 → generalmanager-0.4.0}/GeneralManager.egg-info/top_level.txt +0 -0
  19. {generalmanager-0.3.1 → generalmanager-0.4.0}/LICENSE +0 -0
  20. {generalmanager-0.3.1 → generalmanager-0.4.0}/README.md +0 -0
  21. {generalmanager-0.3.1 → generalmanager-0.4.0}/setup.cfg +0 -0
  22. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/__init__.py +0 -0
  23. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/api/graphql.py +0 -0
  24. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/api/mutation.py +0 -0
  25. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/api/property.py +0 -0
  26. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/apps.py +0 -0
  27. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/auxiliary/__init__.py +0 -0
  28. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/auxiliary/argsToKwargs.py +0 -0
  29. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/auxiliary/filterParser.py +0 -0
  30. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/auxiliary/jsonEncoder.py +0 -0
  31. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/auxiliary/makeCacheKey.py +0 -0
  32. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/auxiliary/noneToZero.py +0 -0
  33. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/auxiliary/pathMapping.py +0 -0
  34. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/cache/cacheDecorator.py +0 -0
  35. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/cache/cacheTracker.py +0 -0
  36. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/cache/modelDependencyCollector.py +0 -0
  37. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/cache/signals.py +0 -0
  38. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/interface/__init__.py +0 -0
  39. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/interface/baseInterface.py +0 -0
  40. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/interface/calculationInterface.py +0 -0
  41. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/interface/databaseInterface.py +0 -0
  42. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/manager/__init__.py +0 -0
  43. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/manager/generalManager.py +0 -0
  44. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/manager/groupManager.py +0 -0
  45. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/manager/input.py +0 -0
  46. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/manager/meta.py +0 -0
  47. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/measurement/__init__.py +0 -0
  48. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/measurement/measurementField.py +0 -0
  49. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/permission/__init__.py +0 -0
  50. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/permission/basePermission.py +0 -0
  51. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/permission/fileBasedPermission.py +0 -0
  52. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/permission/managerBasedPermission.py +0 -0
  53. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/permission/permissionChecks.py +0 -0
  54. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/permission/permissionDataManager.py +0 -0
  55. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/rule/__init__.py +0 -0
  56. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/rule/handler.py +0 -0
  57. {generalmanager-0.3.1 → generalmanager-0.4.0}/src/general_manager/rule/rule.py +0 -0
  58. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_argsToKwargs.py +0 -0
  59. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_basePermission.py +0 -0
  60. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_cacheDecorator.py +0 -0
  61. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_cacheTracker.py +0 -0
  62. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_filterParser.py +0 -0
  63. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_graph_ql.py +0 -0
  64. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_input.py +0 -0
  65. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_jsonEncoder.py +0 -0
  66. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_makeCacheKey.py +0 -0
  67. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_managerBasedPermission.py +0 -0
  68. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_measurement.py +0 -0
  69. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_measurement_field.py +0 -0
  70. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_modelDependencyCollector.py +0 -0
  71. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_rule_handler.py +0 -0
  72. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_rules.py +0 -0
  73. {generalmanager-0.3.1 → generalmanager-0.4.0}/tests/test_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Kurzbeschreibung deines Pakets
5
5
  Author-email: Tim Kleindick <tkleindick@yahoo.de>
6
6
  License: Non-Commercial MIT License
@@ -25,7 +25,7 @@ src/general_manager/cache/modelDependencyCollector.py
25
25
  src/general_manager/cache/signals.py
26
26
  src/general_manager/factory/__init__.py
27
27
  src/general_manager/factory/factories.py
28
- src/general_manager/factory/lazy_methods.py
28
+ src/general_manager/factory/factoryMethods.py
29
29
  src/general_manager/interface/__init__.py
30
30
  src/general_manager/interface/baseInterface.py
31
31
  src/general_manager/interface/calculationInterface.py
@@ -51,6 +51,9 @@ tests/test_argsToKwargs.py
51
51
  tests/test_basePermission.py
52
52
  tests/test_cacheDecorator.py
53
53
  tests/test_cacheTracker.py
54
+ tests/test_dependencyIndex.py
55
+ tests/test_factories.py
56
+ tests/test_factoryMethods.py
54
57
  tests/test_filterParser.py
55
58
  tests/test_graph_ql.py
56
59
  tests/test_input.py
@@ -63,4 +66,5 @@ tests/test_modelDependencyCollector.py
63
66
  tests/test_noneToZero.py
64
67
  tests/test_rule_handler.py
65
68
  tests/test_rules.py
66
- tests/test_settings.py
69
+ tests/test_settings.py
70
+ tests/test_signals.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Kurzbeschreibung deines Pakets
5
5
  Author-email: Tim Kleindick <tkleindick@yahoo.de>
6
6
  License: Non-Commercial MIT License
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "GeneralManager"
10
- version = "0.3.1"
10
+ version = "0.4.0"
11
11
  description = "Kurzbeschreibung deines Pakets"
12
12
  readme = "README.md"
13
13
  authors = [
@@ -33,18 +33,19 @@ type Dependency = Tuple[general_manager_name, filter_type, str]
33
33
  INDEX_KEY = "dependency_index" # Key unter dem der gesamte Index liegt
34
34
  LOCK_KEY = "dependency_index_lock" # Key für das Sperr‑Mutex
35
35
  LOCK_TIMEOUT = 5 # Sekunden TTL für den Lock
36
+ UNDEFINED = object() # Dummy für nicht definierte Werte
36
37
 
37
38
 
38
39
  # -----------------------------------------------------------------------------
39
40
  # LOCKING HELPERS
40
41
  # -----------------------------------------------------------------------------
41
42
  def acquire_lock(timeout: int = LOCK_TIMEOUT) -> bool:
42
- """Atomar: legt den LOCK_KEY an, wenn noch frei."""
43
+ """Atomar: create Lock key if it doesn't exist."""
43
44
  return cache.add(LOCK_KEY, "1", timeout)
44
45
 
45
46
 
46
47
  def release_lock() -> None:
47
- """Gibt den Lock frei."""
48
+ """Release Lock key."""
48
49
  cache.delete(LOCK_KEY)
49
50
 
50
51
 
@@ -52,7 +53,7 @@ def release_lock() -> None:
52
53
  # INDEX ACCESS
53
54
  # -----------------------------------------------------------------------------
54
55
  def get_full_index() -> dependency_index:
55
- """Lädt oder initialisiert den kompletten Index."""
56
+ """Load or initialize the full index."""
56
57
  idx = cache.get(INDEX_KEY, None)
57
58
  if idx is None:
58
59
  idx: dependency_index = {"filter": {}, "exclude": {}}
@@ -61,7 +62,7 @@ def get_full_index() -> dependency_index:
61
62
 
62
63
 
63
64
  def set_full_index(idx: dependency_index) -> None:
64
- """Schreibt den kompletten Index zurück in den Cache."""
65
+ """Write the complete index back to the cache."""
65
66
  cache.set(INDEX_KEY, idx, None)
66
67
 
67
68
 
@@ -78,15 +79,6 @@ def record_dependencies(
78
79
  ]
79
80
  ],
80
81
  ) -> None:
81
- """
82
- Speichert die Abhängigkeiten eines Cache Eintrags.
83
- :param cache_key: der Key unter dem das Ergebnis im cache steht
84
- :param dependencies: Iterable von Tuplen (model_name, action, identifier)
85
- action ∈ {'filter','exclude'} oder sonstige → 'id'
86
- identifier = für filter/exclude: Dict String,
87
- sonst: Primärschlüssel als String
88
- """
89
- # 1) Lock holen (Spin‑Lock mit Timeout)
90
82
  start = time.time()
91
83
  while not acquire_lock():
92
84
  if time.time() - start > LOCK_TIMEOUT:
@@ -105,7 +97,7 @@ def record_dependencies(
105
97
  lookup_map.setdefault(val_key, set()).add(cache_key)
106
98
 
107
99
  else:
108
- # Direkter IDLookup als simpler filter auf 'id'
100
+ # director ID Lookup as simple filter on 'id'
109
101
  section = idx["filter"].setdefault(model_name, {})
110
102
  lookup_map = section.setdefault("identification", {})
111
103
  val_key = identifier
@@ -120,15 +112,12 @@ def record_dependencies(
120
112
  # -----------------------------------------------------------------------------
121
113
  # INDEX CLEANUP
122
114
  # -----------------------------------------------------------------------------
123
- def remove_cache_key_from_index(cache_key: str) -> bool:
124
- """
125
- Entfernt einen cache_key aus allen Einträgen in filter/​exclude.
126
- Nützlich, sobald Du den Cache gelöscht hast.
127
- """
115
+ def remove_cache_key_from_index(cache_key: str) -> None:
116
+ """Remove a cache key from the index."""
128
117
  start = time.time()
129
118
  while not acquire_lock():
130
119
  if time.time() - start > LOCK_TIMEOUT:
131
- return False
120
+ raise TimeoutError("Could not aquire lock for remove_cache_key_from_index")
132
121
  time.sleep(0.05)
133
122
 
134
123
  try:
@@ -149,14 +138,12 @@ def remove_cache_key_from_index(cache_key: str) -> bool:
149
138
  set_full_index(idx)
150
139
  finally:
151
140
  release_lock()
152
- return True
153
141
 
154
142
 
155
143
  # -----------------------------------------------------------------------------
156
144
  # CACHE INVALIDATION
157
145
  # -----------------------------------------------------------------------------
158
146
  def invalidate_cache_key(cache_key: str) -> None:
159
- """Löscht den CacheEintrag – hier nutzt du deinen CacheBackend."""
160
147
  cache.delete(cache_key)
161
148
 
162
149
 
@@ -165,33 +152,28 @@ def capture_old_values(
165
152
  sender: Type[GeneralManager], instance: GeneralManager | None, **kwargs
166
153
  ) -> None:
167
154
  if instance is None:
168
- # Wenn es kein Modell ist, gibt es nichts zu tun
169
155
  return
170
156
  manager_name = sender.__name__
171
157
  idx = get_full_index()
172
- # Welche Lookups interessieren uns für diesen Model?
158
+ # get all lookups for this model
173
159
  lookups = set()
174
160
  for action in ("filter", "exclude"):
175
161
  lookups |= set(idx.get(action, {}).get(manager_name, {}))
176
162
  if lookups and instance.identification:
177
- # Speichere alle relevanten Attribute für später
163
+ # save old values for later comparison
178
164
  vals = {}
179
165
  for lookup in lookups:
180
166
  attr_path = lookup.split("__")
181
167
  obj = instance
182
- for attr in attr_path:
183
- obj = getattr(obj, attr, None)
184
- if obj is None:
168
+ for i, attr in enumerate(attr_path):
169
+ if getattr(obj, attr, UNDEFINED) is UNDEFINED:
170
+ lookup = "__".join(attr_path[:i])
185
171
  break
172
+ obj = getattr(obj, attr, None)
186
173
  vals[lookup] = obj
187
174
  setattr(instance, "_old_values", vals)
188
175
 
189
176
 
190
- # -----------------------------------------------------------------------------
191
- # GENERIC CACHE INVALIDATION: vergleicht alt vs. neu und invalidiert nur bei Übergang
192
- # -----------------------------------------------------------------------------
193
-
194
-
195
177
  @receiver(post_data_change)
196
178
  def generic_cache_invalidation(
197
179
  sender: type[GeneralManager],
@@ -236,18 +218,32 @@ def generic_cache_invalidation(
236
218
  # wildcard / regex
237
219
  if op in ("contains", "startswith", "endswith", "regex"):
238
220
  try:
239
- pattern = re.compile(val_key)
240
- except:
241
- return False
242
- text = value or ""
243
- return bool(pattern.search(text))
221
+ literal = ast.literal_eval(val_key)
222
+ except Exception:
223
+ literal = val_key
224
+
225
+ # ensure we always work with strings to avoid TypeErrors
226
+ text = "" if value is None else str(value)
227
+ if op == "contains":
228
+ return literal in text
229
+ if op == "startswith":
230
+ return text.startswith(literal)
231
+ if op == "endswith":
232
+ return text.endswith(literal)
233
+ # regex: val_key selbst als Pattern benutzen
234
+ if op == "regex":
235
+ try:
236
+ pattern = re.compile(val_key)
237
+ except re.error:
238
+ return False
239
+ return bool(pattern.search(text))
244
240
 
245
241
  return False
246
242
 
247
243
  for action in ("filter", "exclude"):
248
244
  model_section = idx.get(action, {}).get(manager_name, {})
249
245
  for lookup, lookup_map in model_section.items():
250
- # 1) Operator und Attributpfad ermitteln
246
+ # 1) get operator and attribute path
251
247
  parts = lookup.split("__")
252
248
  if parts[-1] in (
253
249
  "gt",
@@ -266,8 +262,8 @@ def generic_cache_invalidation(
266
262
  op = "eq"
267
263
  attr_path = parts
268
264
 
269
- # 2) Alten und neuen Wert holen
270
- old_val = old_relevant_values.get(lookup)
265
+ # 2) get old & new value
266
+ old_val = old_relevant_values.get("__".join(attr_path))
271
267
 
272
268
  obj = instance
273
269
  for attr in attr_path:
@@ -276,14 +272,14 @@ def generic_cache_invalidation(
276
272
  break
277
273
  new_val = obj
278
274
 
279
- # 3) Für jedes val_key prüfen
275
+ # 3) check against all cache_keys
280
276
  for val_key, cache_keys in list(lookup_map.items()):
281
277
  old_match = matches(op, old_val, val_key)
282
278
  new_match = matches(op, new_val, val_key)
283
279
 
284
280
  if action == "filter":
285
- # Direkte & alle Filter-Abhängigkeiten: immer invalidieren, wenn neu matcht
286
- if new_match:
281
+ # Filter: invalidate if new match or old match
282
+ if new_match or old_match:
287
283
  print(
288
284
  f"Invalidate cache key {cache_keys} for filter {lookup} with value {val_key}"
289
285
  )
@@ -292,7 +288,7 @@ def generic_cache_invalidation(
292
288
  remove_cache_key_from_index(ck)
293
289
 
294
290
  else: # action == 'exclude'
295
- # Excludes: nur invalidieren, wenn sich der Match-Status ändert
291
+ # Excludes: invalidate only if matches changed
296
292
  if old_match != new_match:
297
293
  print(
298
294
  f"Invalidate cache key {cache_keys} for exclude {lookup} with value {val_key}"
@@ -1,4 +1,4 @@
1
- from .lazy_methods import (
1
+ from .factoryMethods import (
2
2
  LazyMeasurement,
3
3
  LazyDeltaDate,
4
4
  LazyProjectName,
@@ -1,17 +1,16 @@
1
1
  from __future__ import annotations
2
2
  from typing import TYPE_CHECKING, Type, Callable, Union, Any, TypeVar, Literal, cast
3
- from factory.declarations import LazyFunction, LazyAttribute, LazyAttributeSequence
3
+ from factory.declarations import LazyFunction
4
4
  from factory.faker import Faker
5
5
  import exrex # type: ignore
6
6
  from django.db import models
7
7
  from django.core.validators import RegexValidator
8
8
  from factory.django import DjangoModelFactory
9
- from django.utils import timezone
10
9
  import random
11
10
  from decimal import Decimal
12
11
  from general_manager.measurement.measurement import Measurement
13
12
  from general_manager.measurement.measurementField import MeasurementField
14
- from datetime import date, datetime, time
13
+ from datetime import date, datetime, time, timezone
15
14
 
16
15
  if TYPE_CHECKING:
17
16
  from general_manager.interface.databaseInterface import (
@@ -33,7 +32,7 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
33
32
  ) = None
34
33
 
35
34
  @classmethod
36
- def _generate( # type: ignore
35
+ def _generate(
37
36
  cls, strategy: Literal["build", "create"], params: dict[str, Any]
38
37
  ) -> models.Model | list[models.Model]:
39
38
  cls._original_params = params
@@ -64,7 +63,7 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
64
63
  continue # Skip fields that are already set
65
64
  if isinstance(field, models.AutoField) or field.auto_created:
66
65
  continue # Skip auto fields
67
- params[field.name] = get_field_value(field)
66
+ params[field.name] = getFieldValue(field)
68
67
 
69
68
  obj: list[models.Model] | models.Model = super()._generate(strategy, params)
70
69
  if isinstance(obj, list):
@@ -84,7 +83,7 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
84
83
  if field.name in attrs:
85
84
  m2m_values = attrs[field.name]
86
85
  else:
87
- m2m_values = get_m2m_field_value(field)
86
+ m2m_values = getManyToManyFieldValue(field)
88
87
  if m2m_values:
89
88
  getattr(obj, field.name).set(m2m_values)
90
89
 
@@ -98,7 +97,7 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
98
97
  return kwargs
99
98
 
100
99
  @classmethod
101
- def _create( # type: ignore
100
+ def _create(
102
101
  cls, model_class: Type[models.Model], *args: list[Any], **kwargs: dict[str, Any]
103
102
  ) -> models.Model | list[models.Model]:
104
103
  kwargs = cls._adjust_kwargs(**kwargs)
@@ -107,7 +106,7 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
107
106
  return cls._modelCreation(model_class, **kwargs)
108
107
 
109
108
  @classmethod
110
- def _build( # type: ignore
109
+ def _build(
111
110
  cls, model_class: Type[models.Model], *args: list[Any], **kwargs: dict[str, Any]
112
111
  ) -> models.Model | list[models.Model]:
113
112
  kwargs = cls._adjust_kwargs(**kwargs)
@@ -157,7 +156,7 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
157
156
  return created_objects
158
157
 
159
158
 
160
- def get_field_value(field: models.Field[Any, Any] | models.ForeignObjectRel) -> object:
159
+ def getFieldValue(field: models.Field[Any, Any] | models.ForeignObjectRel) -> object:
161
160
  """
162
161
  Returns a suitable value for a given Django model field.
163
162
  """
@@ -166,22 +165,12 @@ def get_field_value(field: models.Field[Any, Any] | models.ForeignObjectRel) ->
166
165
  return None
167
166
 
168
167
  if isinstance(field, MeasurementField):
169
- base_unit = field.base_unit
170
- value = Decimal(str(random.uniform(0, 10_000))[:10])
171
- return LazyFunction(lambda: Measurement(value, base_unit))
172
- elif isinstance(field, models.CharField):
173
- max_length = field.max_length or 100
174
- # Check for RegexValidator
175
- regex = None
176
- for validator in field.validators:
177
- if isinstance(validator, RegexValidator):
178
- regex = getattr(validator.regex, "pattern", None)
179
- break
180
- if regex:
181
- # Use exrex to generate a string matching the regex
182
- return LazyFunction(lambda: exrex.getone(regex)) # type: ignore
183
- else:
184
- return cast(str, Faker("text", max_nb_chars=max_length))
168
+
169
+ def _measurement():
170
+ value = Decimal(random.randrange(0, 10_000_000)) / Decimal("100") # two dp
171
+ return Measurement(value, field.base_unit)
172
+
173
+ return LazyFunction(_measurement)
185
174
  elif isinstance(field, models.TextField):
186
175
  return cast(str, Faker("paragraph"))
187
176
  elif isinstance(field, models.IntegerField):
@@ -201,8 +190,6 @@ def get_field_value(field: models.Field[Any, Any] | models.ForeignObjectRel) ->
201
190
  )
202
191
  elif isinstance(field, models.FloatField):
203
192
  return cast(float, Faker("pyfloat", positive=True))
204
- elif isinstance(field, models.DateField):
205
- return cast(date, Faker("date_between", start_date="-1y", end_date="today"))
206
193
  elif isinstance(field, models.DateTimeField):
207
194
  return cast(
208
195
  datetime,
@@ -213,10 +200,11 @@ def get_field_value(field: models.Field[Any, Any] | models.ForeignObjectRel) ->
213
200
  tzinfo=timezone.utc,
214
201
  ),
215
202
  )
203
+ elif isinstance(field, models.DateField):
204
+ return cast(date, Faker("date_between", start_date="-1y", end_date="today"))
216
205
  elif isinstance(field, models.BooleanField):
217
206
  return cast(bool, Faker("pybool"))
218
- elif isinstance(field, models.ForeignKey):
219
- # Create or get an instance of the related model
207
+ elif isinstance(field, models.OneToOneField):
220
208
  if hasattr(field.related_model, "_general_manager_class"):
221
209
  related_factory = field.related_model._general_manager_class.Factory
222
210
  return related_factory()
@@ -229,11 +217,19 @@ def get_field_value(field: models.Field[Any, Any] | models.ForeignObjectRel) ->
229
217
  raise ValueError(
230
218
  f"No factory found for {field.related_model.__name__} and no instances found"
231
219
  )
232
- elif isinstance(field, models.OneToOneField):
233
- # Similar to ForeignKey
220
+ elif isinstance(field, models.ForeignKey):
221
+ # Create or get an instance of the related model
234
222
  if hasattr(field.related_model, "_general_manager_class"):
223
+ create_a_new_instance = random.choice([True, True, False])
224
+ if not create_a_new_instance:
225
+ existing_instances = list(field.related_model.objects.all())
226
+ if existing_instances:
227
+ # Pick a random existing instance
228
+ return LazyFunction(lambda: random.choice(existing_instances))
229
+
235
230
  related_factory = field.related_model._general_manager_class.Factory
236
231
  return related_factory()
232
+
237
233
  else:
238
234
  # If no factory exists, pick a random existing instance
239
235
  related_instances = list(field.related_model.objects.all())
@@ -253,35 +249,56 @@ def get_field_value(field: models.Field[Any, Any] | models.ForeignObjectRel) ->
253
249
  return cast(str, Faker("uuid4"))
254
250
  elif isinstance(field, models.DurationField):
255
251
  return cast(time, Faker("time_delta"))
252
+ elif isinstance(field, models.CharField):
253
+ max_length = field.max_length or 100
254
+ # Check for RegexValidator
255
+ regex = None
256
+ for validator in field.validators:
257
+ if isinstance(validator, RegexValidator):
258
+ regex = getattr(validator.regex, "pattern", None)
259
+ break
260
+ if regex:
261
+ # Use exrex to generate a string matching the regex
262
+ return LazyFunction(lambda: exrex.getone(regex)) # type: ignore
263
+ else:
264
+ return cast(str, Faker("text", max_nb_chars=max_length))
256
265
  else:
257
266
  return None # For unsupported field types
258
267
 
259
268
 
260
- def get_m2m_field_value(field: models.ManyToManyField[Any, Any]) -> list[models.Model]:
269
+ def getManyToManyFieldValue(
270
+ field: models.ManyToManyField,
271
+ ) -> list[models.Model]:
261
272
  """
262
273
  Returns a list of instances for a ManyToMany field.
263
274
  """
264
- related_factory = globals().get(f"{field.related_model.__name__}Factory")
265
- existing_instances = list(field.related_model.objects.all())
275
+ related_factory = None
276
+ related_instances = list(field.related_model.objects.all())
277
+ if hasattr(field.related_model, "_general_manager_class"):
278
+ related_factory = field.related_model._general_manager_class.Factory
266
279
 
267
- if related_factory:
268
- # Use existing instances if available, otherwise create new ones
269
- if existing_instances:
270
- max_instances = len(existing_instances)
271
- num_instances = random.randint(0, min(max_instances, 15))
272
- return random.sample(existing_instances, num_instances)
273
- else:
274
- # No existing instances, create a few
275
- num_to_create = random.randint(1, 3)
276
- new_instances = [related_factory() for _ in range(num_to_create)]
277
- return new_instances
280
+ min_required = 0 if field.blank else 1
281
+ number_of_instances = random.randint(min_required, 10)
282
+ if related_factory and related_instances:
283
+ number_to_create = random.randint(min_required, number_of_instances)
284
+ number_to_pick = number_of_instances - number_to_create
285
+ if number_to_pick > len(related_instances):
286
+ number_to_pick = len(related_instances)
287
+ existing_instances = random.sample(related_instances, number_to_pick)
288
+ new_instances = [related_factory() for _ in range(number_to_create)]
289
+ return existing_instances + new_instances
290
+ elif related_factory:
291
+ number_to_create = number_of_instances
292
+ new_instances = [related_factory() for _ in range(number_to_create)]
293
+ return new_instances
294
+ elif related_instances:
295
+ number_to_create = 0
296
+ number_to_pick = number_of_instances
297
+ if number_to_pick > len(related_instances):
298
+ number_to_pick = len(related_instances)
299
+ existing_instances = random.sample(related_instances, number_to_pick)
300
+ return existing_instances
278
301
  else:
279
- # No factory exists, use existing instances
280
- if existing_instances:
281
- max_instances = len(existing_instances)
282
- num_instances = random.randint(0, max_instances)
283
- return random.sample(existing_instances, num_instances)
284
- else:
285
- raise ValueError(
286
- f"No factory found for {field.related_model.__name__} and no instances found"
287
- )
302
+ raise ValueError(
303
+ f"No factory found for {field.related_model.__name__} and no instances found"
304
+ )
@@ -0,0 +1,109 @@
1
+ from typing import Any, Optional
2
+ from factory.declarations import LazyFunction, LazyAttribute, LazyAttributeSequence
3
+ import random
4
+ from general_manager.measurement.measurement import Measurement
5
+ from datetime import timedelta, date, datetime
6
+ from faker import Faker
7
+ import uuid
8
+ from decimal import Decimal
9
+
10
+ fake = Faker()
11
+
12
+
13
+ def LazyMeasurement(
14
+ min_value: int | float, max_value: int | float, unit: str
15
+ ) -> LazyFunction:
16
+ return LazyFunction(
17
+ lambda: Measurement(str(random.uniform(min_value, max_value))[:10], unit)
18
+ )
19
+
20
+
21
+ def LazyDeltaDate(avg_delta_days: int, base_attribute: str) -> LazyAttribute:
22
+ return LazyAttribute(
23
+ lambda obj: (getattr(obj, base_attribute) or date.today())
24
+ + timedelta(days=random.randint(avg_delta_days // 2, avg_delta_days * 3 // 2))
25
+ )
26
+
27
+
28
+ def LazyProjectName() -> LazyFunction:
29
+ return LazyFunction(
30
+ lambda: (
31
+ f"{fake.word().capitalize()} "
32
+ f"{fake.word().capitalize()} "
33
+ f"{fake.random_element(elements=('X', 'Z', 'G'))}"
34
+ f"-{fake.random_int(min=1, max=1000)}"
35
+ )
36
+ )
37
+
38
+
39
+ def LazyDateToday() -> LazyFunction:
40
+ return LazyFunction(lambda: date.today())
41
+
42
+
43
+ def LazyDateBetween(start_date: date, end_date: date) -> LazyAttribute:
44
+ delta = (end_date - start_date).days
45
+ return LazyAttribute(
46
+ lambda obj: start_date + timedelta(days=random.randint(0, delta))
47
+ )
48
+
49
+
50
+ def LazyDateTimeBetween(start: datetime, end: datetime) -> LazyAttribute:
51
+ span = (end - start).total_seconds()
52
+ return LazyAttribute(
53
+ lambda obj: start + timedelta(seconds=random.randint(0, int(span)))
54
+ )
55
+
56
+
57
+ def LazyInteger(min_value: int, max_value: int) -> LazyFunction:
58
+ return LazyFunction(lambda: random.randint(min_value, max_value))
59
+
60
+
61
+ def LazyDecimal(min_value: float, max_value: float, precision: int = 2) -> LazyFunction:
62
+ fmt = f"{{:.{precision}f}}"
63
+ return LazyFunction(
64
+ lambda: Decimal(fmt.format(random.uniform(min_value, max_value)))
65
+ )
66
+
67
+
68
+ def LazyChoice(options: list[Any]) -> LazyFunction:
69
+ return LazyFunction(lambda: random.choice(options))
70
+
71
+
72
+ def LazySequence(start: int = 0, step: int = 1) -> LazyAttributeSequence:
73
+ return LazyAttributeSequence(lambda obj, n: start + n * step)
74
+
75
+
76
+ def LazyBoolean(trues_ratio: float = 0.5) -> LazyFunction:
77
+ return LazyFunction(lambda: random.random() < trues_ratio)
78
+
79
+
80
+ def LazyUUID() -> LazyFunction:
81
+ return LazyFunction(lambda: str(uuid.uuid4()))
82
+
83
+
84
+ def LazyFakerName() -> LazyFunction:
85
+ return LazyFunction(lambda: fake.name())
86
+
87
+
88
+ def LazyFakerEmail(
89
+ name: Optional[str] = None, domain: Optional[str] = None
90
+ ) -> LazyFunction:
91
+ if not name and not domain:
92
+ return LazyFunction(lambda: fake.email(domain=domain))
93
+ if not name:
94
+ name = fake.name()
95
+ if not domain:
96
+ domain = fake.domain_name()
97
+ return LazyFunction(lambda: name.replace(" ", "_") + "@" + domain)
98
+
99
+
100
+ def LazyFakerSentence(number_of_words: int = 6) -> LazyFunction:
101
+ return LazyFunction(lambda: fake.sentence(nb_words=number_of_words))
102
+
103
+
104
+ def LazyFakerAddress() -> LazyFunction:
105
+ return LazyFunction(lambda: fake.address())
106
+
107
+
108
+ def LazyFakerUrl() -> LazyFunction:
109
+ return LazyFunction(lambda: fake.url())
@@ -46,6 +46,14 @@ class Measurement:
46
46
  def quantity(self) -> pint.Quantity:
47
47
  return self.__quantity
48
48
 
49
+ @property
50
+ def magnitude(self) -> Decimal:
51
+ return self.__quantity.magnitude
52
+
53
+ @property
54
+ def unit(self) -> str:
55
+ return str(self.__quantity.units)
56
+
49
57
  @classmethod
50
58
  def from_string(cls, value: str) -> Measurement:
51
59
  value, unit = value.split(" ")