GeneralManager 0.17.0__py3-none-any.whl → 0.18.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.
Potentially problematic release.
This version of GeneralManager might be problematic. Click here for more details.
- general_manager/__init__.py +11 -1
- general_manager/_types/api.py +0 -1
- general_manager/_types/bucket.py +0 -1
- general_manager/_types/cache.py +0 -1
- general_manager/_types/factory.py +0 -1
- general_manager/_types/general_manager.py +0 -1
- general_manager/_types/interface.py +0 -1
- general_manager/_types/manager.py +0 -1
- general_manager/_types/measurement.py +0 -1
- general_manager/_types/permission.py +0 -1
- general_manager/_types/rule.py +0 -1
- general_manager/_types/utils.py +0 -1
- general_manager/api/__init__.py +13 -1
- general_manager/api/graphql.py +356 -221
- general_manager/api/graphql_subscription_consumer.py +81 -78
- general_manager/api/mutation.py +85 -23
- general_manager/api/property.py +39 -13
- general_manager/apps.py +188 -47
- general_manager/bucket/__init__.py +10 -1
- general_manager/bucket/calculationBucket.py +155 -53
- general_manager/bucket/databaseBucket.py +157 -45
- general_manager/bucket/groupBucket.py +133 -44
- general_manager/cache/__init__.py +10 -1
- general_manager/cache/dependencyIndex.py +143 -45
- general_manager/cache/signals.py +9 -2
- general_manager/factory/__init__.py +10 -1
- general_manager/factory/autoFactory.py +55 -13
- general_manager/factory/factories.py +110 -40
- general_manager/factory/factoryMethods.py +122 -34
- general_manager/interface/__init__.py +10 -1
- general_manager/interface/baseInterface.py +129 -36
- general_manager/interface/calculationInterface.py +35 -18
- general_manager/interface/databaseBasedInterface.py +71 -45
- general_manager/interface/databaseInterface.py +96 -38
- general_manager/interface/models.py +5 -5
- general_manager/interface/readOnlyInterface.py +94 -20
- general_manager/manager/__init__.py +10 -1
- general_manager/manager/generalManager.py +25 -16
- general_manager/manager/groupManager.py +20 -6
- general_manager/manager/meta.py +84 -16
- general_manager/measurement/__init__.py +10 -1
- general_manager/measurement/measurement.py +289 -95
- general_manager/measurement/measurementField.py +42 -31
- general_manager/permission/__init__.py +10 -1
- general_manager/permission/basePermission.py +120 -38
- general_manager/permission/managerBasedPermission.py +72 -21
- general_manager/permission/mutationPermission.py +14 -9
- general_manager/permission/permissionChecks.py +14 -12
- general_manager/permission/permissionDataManager.py +24 -11
- general_manager/permission/utils.py +34 -6
- general_manager/public_api_registry.py +36 -10
- general_manager/rule/__init__.py +10 -1
- general_manager/rule/handler.py +133 -44
- general_manager/rule/rule.py +178 -39
- general_manager/utils/__init__.py +10 -1
- general_manager/utils/argsToKwargs.py +34 -9
- general_manager/utils/filterParser.py +22 -7
- general_manager/utils/formatString.py +1 -0
- general_manager/utils/pathMapping.py +23 -15
- general_manager/utils/public_api.py +33 -2
- general_manager/utils/testing.py +31 -33
- {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/METADATA +2 -1
- generalmanager-0.18.0.dist-info/RECORD +77 -0
- {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/licenses/LICENSE +1 -1
- generalmanager-0.17.0.dist-info/RECORD +0 -77
- {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/top_level.txt +0 -0
|
@@ -33,6 +33,22 @@ type Dependency = Tuple[general_manager_name, filter_type, str]
|
|
|
33
33
|
|
|
34
34
|
logger = logging.getLogger(__name__)
|
|
35
35
|
|
|
36
|
+
|
|
37
|
+
class DependencyLockTimeoutError(TimeoutError):
|
|
38
|
+
"""Raised when the dependency index lock cannot be acquired within the timeout."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, operation: str) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Error raised when acquiring the dependency index lock times out.
|
|
43
|
+
|
|
44
|
+
Parameters:
|
|
45
|
+
operation (str): Name or description of the operation during which lock acquisition timed out.
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(
|
|
48
|
+
f"Timed out acquiring dependency index lock during {operation}."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
36
52
|
# -----------------------------------------------------------------------------
|
|
37
53
|
# CONFIG
|
|
38
54
|
# -----------------------------------------------------------------------------
|
|
@@ -40,6 +56,7 @@ INDEX_KEY = "dependency_index" # Cache key storing the complete dependency inde
|
|
|
40
56
|
LOCK_KEY = "dependency_index_lock" # Cache key used for the dependency lock
|
|
41
57
|
LOCK_TIMEOUT = 5 # Lock TTL in seconds
|
|
42
58
|
UNDEFINED = object() # Sentinel for undefined values
|
|
59
|
+
ACTIONS: tuple[Literal["filter"], Literal["exclude"]] = ("filter", "exclude")
|
|
43
60
|
|
|
44
61
|
|
|
45
62
|
# -----------------------------------------------------------------------------
|
|
@@ -113,23 +130,22 @@ def record_dependencies(
|
|
|
113
130
|
],
|
|
114
131
|
) -> None:
|
|
115
132
|
"""
|
|
116
|
-
Register cache
|
|
133
|
+
Register a cache key as dependent on the given manager-level filters, exclusions, or identifications.
|
|
117
134
|
|
|
118
135
|
Parameters:
|
|
119
|
-
cache_key (str):
|
|
136
|
+
cache_key (str): The cache key to associate with the declared dependencies.
|
|
120
137
|
dependencies (Iterable[tuple[str, Literal["filter", "exclude", "identification"], str]]):
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
None
|
|
138
|
+
Iterable of tuples describing each dependency as (manager_name, action, identifier).
|
|
139
|
+
- For `filter` and `exclude`, `identifier` is a string representation of a mapping of lookups to values.
|
|
140
|
+
- For `identification`, `identifier` is the identifying value (treated as a lookup on `id`).
|
|
125
141
|
|
|
126
142
|
Raises:
|
|
127
|
-
|
|
143
|
+
DependencyLockTimeoutError: If a lock cannot be acquired within the configured timeout while updating the index.
|
|
128
144
|
"""
|
|
129
145
|
start = time.time()
|
|
130
146
|
while not acquire_lock():
|
|
131
147
|
if time.time() - start > LOCK_TIMEOUT:
|
|
132
|
-
raise
|
|
148
|
+
raise DependencyLockTimeoutError("record_dependencies")
|
|
133
149
|
time.sleep(0.05)
|
|
134
150
|
|
|
135
151
|
try:
|
|
@@ -140,7 +156,9 @@ def record_dependencies(
|
|
|
140
156
|
params = ast.literal_eval(identifier)
|
|
141
157
|
section = idx[action_key].setdefault(model_name, {})
|
|
142
158
|
if len(params) > 1:
|
|
143
|
-
cache_dependencies = section.setdefault(
|
|
159
|
+
cache_dependencies = section.setdefault(
|
|
160
|
+
"__cache_dependencies__", {}
|
|
161
|
+
)
|
|
144
162
|
cache_dependencies.setdefault(cache_key, set()).add(identifier)
|
|
145
163
|
for lookup, val in params.items():
|
|
146
164
|
lookup_map = section.setdefault(lookup, {})
|
|
@@ -165,27 +183,26 @@ def record_dependencies(
|
|
|
165
183
|
# -----------------------------------------------------------------------------
|
|
166
184
|
def remove_cache_key_from_index(cache_key: str) -> None:
|
|
167
185
|
"""
|
|
168
|
-
Remove a cache
|
|
186
|
+
Remove a cache key from all dependency mappings in the stored dependency index.
|
|
169
187
|
|
|
170
|
-
|
|
171
|
-
cache_key (str): Cache key that should be expunged from the index.
|
|
188
|
+
Acquires the dependency lock to update and persist the index; if the lock cannot be obtained within LOCK_TIMEOUT the operation fails.
|
|
172
189
|
|
|
173
|
-
|
|
174
|
-
|
|
190
|
+
Parameters:
|
|
191
|
+
cache_key (str): The cache key to expunge from all recorded filter, exclude, and identification mappings.
|
|
175
192
|
|
|
176
193
|
Raises:
|
|
177
|
-
|
|
194
|
+
DependencyLockTimeoutError: If the dependency lock cannot be acquired within LOCK_TIMEOUT.
|
|
178
195
|
"""
|
|
179
196
|
start = time.time()
|
|
180
197
|
while not acquire_lock():
|
|
181
198
|
if time.time() - start > LOCK_TIMEOUT:
|
|
182
|
-
raise
|
|
199
|
+
raise DependencyLockTimeoutError("remove_cache_key_from_index")
|
|
183
200
|
time.sleep(0.05)
|
|
184
201
|
|
|
185
202
|
try:
|
|
186
203
|
idx = get_full_index()
|
|
187
|
-
for action in
|
|
188
|
-
action_section = idx
|
|
204
|
+
for action in ACTIONS:
|
|
205
|
+
action_section = idx[action]
|
|
189
206
|
for mname, model_section in list(action_section.items()):
|
|
190
207
|
cache_dependencies = model_section.get("__cache_dependencies__", {})
|
|
191
208
|
for lookup, lookup_map in list(model_section.items()):
|
|
@@ -232,15 +249,10 @@ def capture_old_values(
|
|
|
232
249
|
**kwargs: object,
|
|
233
250
|
) -> None:
|
|
234
251
|
"""
|
|
235
|
-
|
|
252
|
+
Record the current values of fields referenced by tracked filters on the given manager instance before it changes.
|
|
236
253
|
|
|
237
254
|
Parameters:
|
|
238
|
-
|
|
239
|
-
instance (GeneralManager | None): Manager instance about to change.
|
|
240
|
-
**kwargs: Additional signal metadata.
|
|
241
|
-
|
|
242
|
-
Returns:
|
|
243
|
-
None
|
|
255
|
+
instance (GeneralManager | None): Manager instance about to change; if provided, this function sets instance._old_values to a mapping of lookup keys to their current values for use by post-change invalidation logic.
|
|
244
256
|
"""
|
|
245
257
|
if instance is None:
|
|
246
258
|
return
|
|
@@ -248,8 +260,8 @@ def capture_old_values(
|
|
|
248
260
|
idx = get_full_index()
|
|
249
261
|
# get all lookups for this model
|
|
250
262
|
lookups = set()
|
|
251
|
-
for action in
|
|
252
|
-
model_section = idx
|
|
263
|
+
for action in ACTIONS:
|
|
264
|
+
model_section = idx[action].get(manager_name)
|
|
253
265
|
if isinstance(model_section, dict):
|
|
254
266
|
lookups |= {
|
|
255
267
|
lookup
|
|
@@ -270,7 +282,7 @@ def capture_old_values(
|
|
|
270
282
|
break
|
|
271
283
|
current = getattr(current, attr, None)
|
|
272
284
|
vals[lookup] = current
|
|
273
|
-
|
|
285
|
+
instance._old_values = vals
|
|
274
286
|
|
|
275
287
|
|
|
276
288
|
@receiver(post_data_change)
|
|
@@ -281,16 +293,14 @@ def generic_cache_invalidation(
|
|
|
281
293
|
**kwargs: object,
|
|
282
294
|
) -> None:
|
|
283
295
|
"""
|
|
284
|
-
Invalidate
|
|
296
|
+
Invalidate cache entries whose recorded dependencies are affected by changes to a GeneralManager instance.
|
|
285
297
|
|
|
286
|
-
|
|
287
|
-
sender (type[GeneralManager]): Manager class that triggered the signal.
|
|
288
|
-
instance (GeneralManager): Updated manager instance.
|
|
289
|
-
old_relevant_values (dict[str, Any]): Previously captured values for tracked lookups.
|
|
290
|
-
**kwargs: Additional signal metadata.
|
|
298
|
+
Uses the dependency index to compare previously captured values against the instance's current values for tracked lookups, evaluates both simple and composite dependency conditions for "filter" and "exclude" actions, and for any dependency that warrants invalidation it deletes the corresponding cache entry and removes its references from the index.
|
|
291
299
|
|
|
292
|
-
|
|
293
|
-
|
|
300
|
+
Parameters:
|
|
301
|
+
sender (type[GeneralManager]): Manager class that emitted the signal.
|
|
302
|
+
instance (GeneralManager): The manager instance that was changed.
|
|
303
|
+
old_relevant_values (dict[str, Any]): Mapping of lookup paths (joined by "__") to their values as captured before the change; used to compare old vs. new values for invalidation decisions.
|
|
294
304
|
"""
|
|
295
305
|
manager_name = sender.__name__
|
|
296
306
|
idx = get_full_index()
|
|
@@ -304,6 +314,22 @@ def generic_cache_invalidation(
|
|
|
304
314
|
return value
|
|
305
315
|
|
|
306
316
|
def _coerce_to_type(sample: Any, raw: Any) -> Any | None:
|
|
317
|
+
"""
|
|
318
|
+
Coerces a raw value to match the type and semantics of a sample value.
|
|
319
|
+
|
|
320
|
+
Attempts to convert `raw` into the same type as `sample`. Handles:
|
|
321
|
+
- datetimes: parses ISO-like strings, preserves or aligns timezone info with `sample`,
|
|
322
|
+
- dates: parses ISO date strings,
|
|
323
|
+
- booleans: recognizes common textual and numeric boolean representations,
|
|
324
|
+
- other types: attempts to call the sample's type on `raw`.
|
|
325
|
+
|
|
326
|
+
Parameters:
|
|
327
|
+
sample: A value whose type and semantics should be used as the target.
|
|
328
|
+
raw: The input value to coerce.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
The coerced value of the same type as `sample`, or `None` if `raw` cannot be sensibly converted.
|
|
332
|
+
"""
|
|
307
333
|
if sample is None:
|
|
308
334
|
return None
|
|
309
335
|
|
|
@@ -336,14 +362,50 @@ def generic_cache_invalidation(
|
|
|
336
362
|
return None
|
|
337
363
|
return None
|
|
338
364
|
|
|
365
|
+
# Booleans: avoid bool("False") == True
|
|
366
|
+
if isinstance(sample, bool):
|
|
367
|
+
if isinstance(raw, bool):
|
|
368
|
+
return raw
|
|
369
|
+
if isinstance(raw, (int,)):
|
|
370
|
+
return bool(raw)
|
|
371
|
+
if isinstance(raw, str):
|
|
372
|
+
s = raw.strip().lower()
|
|
373
|
+
if s in {"true", "1", "yes", "y", "t"}:
|
|
374
|
+
return True
|
|
375
|
+
if s in {"false", "0", "no", "n", "f"}:
|
|
376
|
+
return False
|
|
377
|
+
return None
|
|
339
378
|
try:
|
|
340
|
-
return type(sample)(raw)
|
|
341
|
-
except
|
|
379
|
+
return type(sample)(raw) # type: ignore
|
|
380
|
+
except (TypeError, ValueError):
|
|
342
381
|
if isinstance(raw, type(sample)):
|
|
343
382
|
return raw
|
|
344
383
|
return None
|
|
345
384
|
|
|
346
385
|
def matches(op: str, value: Any, val_key: Any) -> bool:
|
|
386
|
+
"""
|
|
387
|
+
Evaluate whether a given value satisfies a lookup operation described by `op` and `val_key`.
|
|
388
|
+
|
|
389
|
+
Supports operators:
|
|
390
|
+
- "eq": equality; attempts to interpret `val_key` as a literal and coerce it to `value`'s type before comparing.
|
|
391
|
+
- "in": membership; expects `val_key` to be a literal iterable and checks if any coerced element equals `value`.
|
|
392
|
+
- "gt", "gte", "lt", "lte": numeric/date comparisons; `val_key` is interpreted as a literal and coerced to `value`'s type.
|
|
393
|
+
- "contains", "startswith", "endswith": string containment/prefix/suffix checks; both sides are compared as strings.
|
|
394
|
+
- "regex": treats `val_key` as a regular expression pattern and tests it against the string form of `value`.
|
|
395
|
+
|
|
396
|
+
Behavior notes:
|
|
397
|
+
- If `value` is None the function returns `False`.
|
|
398
|
+
- Literal parsing of `val_key` is attempted via AST literal evaluation; if parsing or coercion fails, the function falls back to conservative comparisons or returns `False` where appropriate.
|
|
399
|
+
- Regex patterns that fail to compile are treated as non-matching.
|
|
400
|
+
|
|
401
|
+
Parameters:
|
|
402
|
+
op (str): The lookup operator name (one of the supported operators above).
|
|
403
|
+
value (Any): The runtime value to test.
|
|
404
|
+
val_key (Any): The stored comparison key (often a string representation) to interpret for the comparison.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
bool: `True` if the comparison defined by `op` and `val_key` matches `value`, `False` otherwise.
|
|
408
|
+
"""
|
|
347
409
|
if value is None:
|
|
348
410
|
return False
|
|
349
411
|
|
|
@@ -403,7 +465,9 @@ def generic_cache_invalidation(
|
|
|
403
465
|
# regex: treat the stored key as the regex pattern
|
|
404
466
|
if op == "regex":
|
|
405
467
|
try:
|
|
406
|
-
pattern_source =
|
|
468
|
+
pattern_source = (
|
|
469
|
+
literal if isinstance(literal, str) else str(literal)
|
|
470
|
+
)
|
|
407
471
|
pattern = re.compile(pattern_source)
|
|
408
472
|
except re.error:
|
|
409
473
|
return False
|
|
@@ -412,6 +476,15 @@ def generic_cache_invalidation(
|
|
|
412
476
|
return False
|
|
413
477
|
|
|
414
478
|
def current_value_for_path(path: list[str]) -> Any:
|
|
479
|
+
"""
|
|
480
|
+
Fetches the current value from the captured `instance` by following a sequence of attribute names.
|
|
481
|
+
|
|
482
|
+
Parameters:
|
|
483
|
+
path (list[str]): Ordered attribute names to traverse on the instance (e.g., ["user", "profile", "email"]).
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
The value found at the end of the attribute path, or `None` if any attribute along the path is missing.
|
|
487
|
+
"""
|
|
415
488
|
current: object = instance
|
|
416
489
|
for attr in path:
|
|
417
490
|
current = getattr(current, attr, UNDEFINED)
|
|
@@ -420,8 +493,29 @@ def generic_cache_invalidation(
|
|
|
420
493
|
return current
|
|
421
494
|
|
|
422
495
|
def evaluate_composite(
|
|
423
|
-
cache_key: str,
|
|
496
|
+
cache_key: str,
|
|
497
|
+
lookup_key: str,
|
|
498
|
+
action: Literal["filter", "exclude"],
|
|
499
|
+
model_section: dict[str, dict[str, set[str]]],
|
|
424
500
|
) -> bool | None:
|
|
501
|
+
"""
|
|
502
|
+
Determine whether a composite dependency (multiple lookup params grouped under a single identifier)
|
|
503
|
+
for a given cache key and lookup should cause cache invalidation.
|
|
504
|
+
|
|
505
|
+
Parameters:
|
|
506
|
+
cache_key (str): The cache key being evaluated.
|
|
507
|
+
lookup_key (str): The specific lookup (operator and attribute path joined by `"__"`) that prompted evaluation.
|
|
508
|
+
action (Literal["filter", "exclude"]): The dependency action context; "filter" treats a match as cause for invalidation,
|
|
509
|
+
"exclude" treats a change in match membership as cause for invalidation.
|
|
510
|
+
model_section (dict[str, dict[str, set[str]]]): The index section for the model containing lookup maps and an
|
|
511
|
+
optional "__cache_dependencies__" mapping from cache keys to sets of identifier strings (each identifier
|
|
512
|
+
encodes multiple lookup parameters).
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
bool | None: `True` if the composite dependency indicates the cache entry should be invalidated,
|
|
516
|
+
`False` if it indicates no invalidation is required, or `None` if there are no composite identifiers
|
|
517
|
+
registered for `cache_key`.
|
|
518
|
+
"""
|
|
425
519
|
cache_dependencies = model_section.get("__cache_dependencies__", {})
|
|
426
520
|
identifiers = cache_dependencies.get(cache_key) if cache_dependencies else None
|
|
427
521
|
if not identifiers:
|
|
@@ -468,9 +562,9 @@ def generic_cache_invalidation(
|
|
|
468
562
|
return True
|
|
469
563
|
return False
|
|
470
564
|
|
|
471
|
-
for action in
|
|
472
|
-
model_section = idx
|
|
473
|
-
if not model_section:
|
|
565
|
+
for action in ACTIONS:
|
|
566
|
+
model_section = idx[action].get(manager_name)
|
|
567
|
+
if not isinstance(model_section, dict):
|
|
474
568
|
continue
|
|
475
569
|
for lookup, lookup_map in model_section.items():
|
|
476
570
|
if lookup.startswith("__"):
|
|
@@ -512,7 +606,9 @@ def generic_cache_invalidation(
|
|
|
512
606
|
if action == "filter":
|
|
513
607
|
# Filter: invalidate if new match or old match
|
|
514
608
|
for ck in list(cache_keys):
|
|
515
|
-
composite_decision = evaluate_composite(
|
|
609
|
+
composite_decision = evaluate_composite(
|
|
610
|
+
ck, lookup, action, model_section
|
|
611
|
+
)
|
|
516
612
|
should_invalidate = (
|
|
517
613
|
composite_decision
|
|
518
614
|
if composite_decision is not None
|
|
@@ -528,7 +624,9 @@ def generic_cache_invalidation(
|
|
|
528
624
|
else: # action == 'exclude'
|
|
529
625
|
# Excludes: invalidate only if matches changed
|
|
530
626
|
for ck in list(cache_keys):
|
|
531
|
-
composite_decision = evaluate_composite(
|
|
627
|
+
composite_decision = evaluate_composite(
|
|
628
|
+
ck, lookup, action, model_section
|
|
629
|
+
)
|
|
532
630
|
should_invalidate = (
|
|
533
631
|
composite_decision
|
|
534
632
|
if composite_decision is not None
|
general_manager/cache/signals.py
CHANGED
|
@@ -27,14 +27,16 @@ def dataChange(func: Callable[P, R]) -> Callable[P, R]:
|
|
|
27
27
|
@wraps(func)
|
|
28
28
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
29
29
|
"""
|
|
30
|
-
|
|
30
|
+
Emit pre_data_change and post_data_change signals around the wrapped function call.
|
|
31
|
+
|
|
32
|
+
Emits a pre_data_change signal before invoking the wrapped function and a post_data_change signal afterwards. Signals are sent with `sender`, `instance`, and `action`; the post-change signal also includes `old_relevant_values`. After signaling, the wrapper removes the `_old_values` attribute from the pre-change instance if it exists.
|
|
31
33
|
|
|
32
34
|
Parameters:
|
|
33
35
|
*args: Positional arguments forwarded to the wrapped function.
|
|
34
36
|
**kwargs: Keyword arguments forwarded to the wrapped function.
|
|
35
37
|
|
|
36
38
|
Returns:
|
|
37
|
-
R:
|
|
39
|
+
R: The result returned by the wrapped function.
|
|
38
40
|
"""
|
|
39
41
|
action = func.__name__
|
|
40
42
|
if func.__name__ == "create":
|
|
@@ -66,6 +68,11 @@ def dataChange(func: Callable[P, R]) -> Callable[P, R]:
|
|
|
66
68
|
old_relevant_values=old_relevant_values,
|
|
67
69
|
**kwargs,
|
|
68
70
|
)
|
|
71
|
+
if instance_before is not None:
|
|
72
|
+
try:
|
|
73
|
+
delattr(instance_before, "_old_values")
|
|
74
|
+
except AttributeError:
|
|
75
|
+
pass
|
|
69
76
|
return result
|
|
70
77
|
|
|
71
78
|
return wrapper
|
|
@@ -12,10 +12,19 @@ __all__ = list(FACTORY_EXPORTS)
|
|
|
12
12
|
_MODULE_MAP = FACTORY_EXPORTS
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
|
-
from general_manager._types.factory import * # noqa:
|
|
15
|
+
from general_manager._types.factory import * # noqa: F403
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def __getattr__(name: str) -> Any:
|
|
19
|
+
"""
|
|
20
|
+
Dynamically resolve and return a named export from this module.
|
|
21
|
+
|
|
22
|
+
Parameters:
|
|
23
|
+
name (str): The attribute name to resolve.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Any: The resolved attribute object corresponding to `name`.
|
|
27
|
+
"""
|
|
19
28
|
return resolve_export(
|
|
20
29
|
name,
|
|
21
30
|
module_all=__all__,
|
|
@@ -15,6 +15,40 @@ if TYPE_CHECKING:
|
|
|
15
15
|
modelsModel = TypeVar("modelsModel", bound=models.Model)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
class InvalidGeneratedObjectError(TypeError):
|
|
19
|
+
"""Raised when factory generation produces non-model instances."""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Initialize the exception indicating a generated object is not a Django model instance.
|
|
24
|
+
|
|
25
|
+
Sets a default error message explaining that the generated object is not a Django model instance.
|
|
26
|
+
"""
|
|
27
|
+
super().__init__("Generated object is not a Django model instance.")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InvalidAutoFactoryModelError(TypeError):
|
|
31
|
+
"""Raised when the factory metadata does not reference a Django model class."""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Raised when an AutoFactory target model is not a Django model class.
|
|
36
|
+
|
|
37
|
+
The exception carries a default message explaining that `_meta.model` must be a Django model class.
|
|
38
|
+
"""
|
|
39
|
+
super().__init__("AutoFactory requires _meta.model to be a Django model class.")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class UndefinedAdjustmentMethodError(ValueError):
|
|
43
|
+
"""Raised when an adjustment method is required but not configured."""
|
|
44
|
+
|
|
45
|
+
def __init__(self) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Initialize the UndefinedAdjustmentMethodError with the default message indicating that a generate/adjustment function is not configured.
|
|
48
|
+
"""
|
|
49
|
+
super().__init__("_adjustmentMethod is not defined.")
|
|
50
|
+
|
|
51
|
+
|
|
18
52
|
class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
19
53
|
"""Factory that auto-populates model fields based on interface metadata."""
|
|
20
54
|
|
|
@@ -28,18 +62,26 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
|
28
62
|
cls, strategy: Literal["build", "create"], params: dict[str, Any]
|
|
29
63
|
) -> models.Model | list[models.Model]:
|
|
30
64
|
"""
|
|
31
|
-
Generate and populate model instances
|
|
65
|
+
Generate and populate model instances using automatically derived field values.
|
|
32
66
|
|
|
33
67
|
Parameters:
|
|
34
|
-
strategy (Literal["build", "create"]):
|
|
35
|
-
params (dict[str, Any]): Field values supplied by the caller.
|
|
68
|
+
strategy (Literal["build", "create"]): Either "build" (unsaved instance) or "create" (saved instance).
|
|
69
|
+
params (dict[str, Any]): Field values supplied by the caller; any missing non-auto fields will be populated automatically.
|
|
36
70
|
|
|
37
71
|
Returns:
|
|
38
|
-
models.Model | list[models.Model]:
|
|
72
|
+
models.Model | list[models.Model]: A generated model instance or a list of generated model instances.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
InvalidAutoFactoryModelError: If the factory target `_meta.model` is not a Django model class.
|
|
76
|
+
InvalidGeneratedObjectError: If an element of a generated list is not a Django model instance.
|
|
39
77
|
"""
|
|
40
78
|
model = cls._meta.model
|
|
41
|
-
|
|
42
|
-
|
|
79
|
+
try:
|
|
80
|
+
is_model = isinstance(model, type) and issubclass(model, models.Model)
|
|
81
|
+
except TypeError:
|
|
82
|
+
is_model = False
|
|
83
|
+
if not is_model:
|
|
84
|
+
raise InvalidAutoFactoryModelError
|
|
43
85
|
field_name_list, to_ignore_list = cls.interface.handleCustomFields(model)
|
|
44
86
|
|
|
45
87
|
fields = [
|
|
@@ -71,7 +113,7 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
|
71
113
|
if isinstance(obj, list):
|
|
72
114
|
for item in obj:
|
|
73
115
|
if not isinstance(item, models.Model):
|
|
74
|
-
raise
|
|
116
|
+
raise InvalidGeneratedObjectError()
|
|
75
117
|
cls._handleManyToManyFieldsAfterCreation(item, params)
|
|
76
118
|
else:
|
|
77
119
|
cls._handleManyToManyFieldsAfterCreation(obj, params)
|
|
@@ -191,21 +233,21 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
|
191
233
|
cls, use_creation_method: bool, params: dict[str, Any]
|
|
192
234
|
) -> models.Model | list[models.Model]:
|
|
193
235
|
"""
|
|
194
|
-
Create or build instance(s) using the configured adjustment method.
|
|
236
|
+
Create or build model instance(s) using the configured adjustment method.
|
|
195
237
|
|
|
196
238
|
Parameters:
|
|
197
|
-
use_creation_method (bool):
|
|
198
|
-
params (dict[str, Any]):
|
|
239
|
+
use_creation_method (bool): If True, created records are validated and saved; if False, unsaved instances are returned.
|
|
240
|
+
params (dict[str, Any]): Keyword arguments forwarded to the adjustment method to produce record dict(s).
|
|
199
241
|
|
|
200
242
|
Returns:
|
|
201
|
-
models.Model | list[models.Model]:
|
|
243
|
+
models.Model | list[models.Model]: A single model instance or a list of instances — saved instances when `use_creation_method` is True, unsaved otherwise.
|
|
202
244
|
|
|
203
245
|
Raises:
|
|
204
|
-
|
|
246
|
+
UndefinedAdjustmentMethodError: If no adjustment method has been configured on the factory.
|
|
205
247
|
"""
|
|
206
248
|
model_cls = cls._meta.model
|
|
207
249
|
if cls._adjustmentMethod is None:
|
|
208
|
-
raise
|
|
250
|
+
raise UndefinedAdjustmentMethodError()
|
|
209
251
|
records = cls._adjustmentMethod(**params)
|
|
210
252
|
if isinstance(records, dict):
|
|
211
253
|
if use_creation_method:
|