GeneralManager 0.14.0__py3-none-any.whl → 0.15.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.
- general_manager/__init__.py +49 -0
- general_manager/api/__init__.py +36 -0
- general_manager/api/graphql.py +92 -43
- general_manager/api/mutation.py +35 -10
- general_manager/api/property.py +26 -3
- general_manager/apps.py +23 -16
- general_manager/bucket/__init__.py +32 -0
- general_manager/bucket/baseBucket.py +76 -64
- general_manager/bucket/calculationBucket.py +188 -108
- general_manager/bucket/databaseBucket.py +130 -49
- general_manager/bucket/groupBucket.py +113 -60
- general_manager/cache/__init__.py +38 -0
- general_manager/cache/cacheDecorator.py +29 -17
- general_manager/cache/cacheTracker.py +34 -15
- general_manager/cache/dependencyIndex.py +117 -33
- general_manager/cache/modelDependencyCollector.py +17 -8
- general_manager/cache/signals.py +17 -6
- general_manager/factory/__init__.py +34 -5
- general_manager/factory/autoFactory.py +57 -60
- general_manager/factory/factories.py +39 -14
- general_manager/factory/factoryMethods.py +38 -1
- general_manager/interface/__init__.py +36 -0
- general_manager/interface/baseInterface.py +71 -27
- general_manager/interface/calculationInterface.py +18 -10
- general_manager/interface/databaseBasedInterface.py +102 -71
- general_manager/interface/databaseInterface.py +66 -20
- general_manager/interface/models.py +10 -4
- general_manager/interface/readOnlyInterface.py +44 -30
- general_manager/manager/__init__.py +36 -3
- general_manager/manager/generalManager.py +73 -47
- general_manager/manager/groupManager.py +72 -17
- general_manager/manager/input.py +23 -15
- general_manager/manager/meta.py +53 -53
- general_manager/measurement/__init__.py +37 -2
- general_manager/measurement/measurement.py +135 -58
- general_manager/measurement/measurementField.py +161 -61
- general_manager/permission/__init__.py +32 -1
- general_manager/permission/basePermission.py +29 -12
- general_manager/permission/managerBasedPermission.py +32 -26
- general_manager/permission/mutationPermission.py +32 -3
- general_manager/permission/permissionChecks.py +9 -1
- general_manager/permission/permissionDataManager.py +49 -15
- general_manager/permission/utils.py +14 -3
- general_manager/rule/__init__.py +27 -1
- general_manager/rule/handler.py +90 -5
- general_manager/rule/rule.py +40 -27
- general_manager/utils/__init__.py +44 -2
- general_manager/utils/argsToKwargs.py +17 -9
- general_manager/utils/filterParser.py +29 -30
- general_manager/utils/formatString.py +2 -0
- general_manager/utils/jsonEncoder.py +14 -1
- general_manager/utils/makeCacheKey.py +18 -12
- general_manager/utils/noneToZero.py +8 -6
- general_manager/utils/pathMapping.py +92 -29
- general_manager/utils/public_api.py +49 -0
- general_manager/utils/testing.py +135 -69
- {generalmanager-0.14.0.dist-info → generalmanager-0.15.0.dist-info}/METADATA +38 -4
- generalmanager-0.15.0.dist-info/RECORD +62 -0
- generalmanager-0.15.0.dist-info/licenses/LICENSE +21 -0
- generalmanager-0.14.0.dist-info/RECORD +0 -58
- generalmanager-0.14.0.dist-info/licenses/LICENSE +0 -29
- {generalmanager-0.14.0.dist-info → generalmanager-0.15.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.14.0.dist-info → generalmanager-0.15.0.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,5 @@
|
|
1
|
+
"""Dependency index management for cached GeneralManager query results."""
|
2
|
+
|
1
3
|
from __future__ import annotations
|
2
4
|
import time
|
3
5
|
import ast
|
@@ -7,7 +9,7 @@ import logging
|
|
7
9
|
from django.core.cache import cache
|
8
10
|
from general_manager.cache.signals import post_data_change, pre_data_change
|
9
11
|
from django.dispatch import receiver
|
10
|
-
from typing import Literal, Any, Iterable, TYPE_CHECKING, Type, Tuple
|
12
|
+
from typing import Literal, Any, Iterable, TYPE_CHECKING, Type, Tuple, cast
|
11
13
|
|
12
14
|
if TYPE_CHECKING:
|
13
15
|
from general_manager.manager.generalManager import GeneralManager
|
@@ -33,22 +35,35 @@ logger = logging.getLogger(__name__)
|
|
33
35
|
# -----------------------------------------------------------------------------
|
34
36
|
# CONFIG
|
35
37
|
# -----------------------------------------------------------------------------
|
36
|
-
INDEX_KEY = "dependency_index" #
|
37
|
-
LOCK_KEY = "dependency_index_lock" #
|
38
|
-
LOCK_TIMEOUT = 5 #
|
39
|
-
UNDEFINED = object() #
|
38
|
+
INDEX_KEY = "dependency_index" # Cache key storing the complete dependency index
|
39
|
+
LOCK_KEY = "dependency_index_lock" # Cache key used for the dependency lock
|
40
|
+
LOCK_TIMEOUT = 5 # Lock TTL in seconds
|
41
|
+
UNDEFINED = object() # Sentinel for undefined values
|
40
42
|
|
41
43
|
|
42
44
|
# -----------------------------------------------------------------------------
|
43
45
|
# LOCKING HELPERS
|
44
46
|
# -----------------------------------------------------------------------------
|
45
47
|
def acquire_lock(timeout: int = LOCK_TIMEOUT) -> bool:
|
46
|
-
"""
|
48
|
+
"""
|
49
|
+
Attempt to acquire the cache-backed lock guarding dependency writes.
|
50
|
+
|
51
|
+
Parameters:
|
52
|
+
timeout (int): Expiration time for the lock entry in seconds.
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
bool: True if the lock was acquired; otherwise, False.
|
56
|
+
"""
|
47
57
|
return cache.add(LOCK_KEY, "1", timeout)
|
48
58
|
|
49
59
|
|
50
60
|
def release_lock() -> None:
|
51
|
-
"""
|
61
|
+
"""
|
62
|
+
Release the cache-backed lock guarding dependency writes.
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
None
|
66
|
+
"""
|
52
67
|
cache.delete(LOCK_KEY)
|
53
68
|
|
54
69
|
|
@@ -56,16 +71,30 @@ def release_lock() -> None:
|
|
56
71
|
# INDEX ACCESS
|
57
72
|
# -----------------------------------------------------------------------------
|
58
73
|
def get_full_index() -> dependency_index:
|
59
|
-
"""
|
60
|
-
|
61
|
-
|
74
|
+
"""
|
75
|
+
Fetch the dependency index from cache, initialising it on first access.
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
dependency_index: Mapping of tracked filters and excludes keyed by manager name.
|
79
|
+
"""
|
80
|
+
cached_index = cache.get(INDEX_KEY, None)
|
81
|
+
if cached_index is None:
|
62
82
|
idx: dependency_index = {"filter": {}, "exclude": {}}
|
63
83
|
cache.set(INDEX_KEY, idx, None)
|
64
|
-
|
84
|
+
return idx
|
85
|
+
return cast(dependency_index, cached_index)
|
65
86
|
|
66
87
|
|
67
88
|
def set_full_index(idx: dependency_index) -> None:
|
68
|
-
"""
|
89
|
+
"""
|
90
|
+
Persist the dependency index to cache.
|
91
|
+
|
92
|
+
Parameters:
|
93
|
+
idx (dependency_index): Updated index that should replace the cached value.
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
None
|
97
|
+
"""
|
69
98
|
cache.set(INDEX_KEY, idx, None)
|
70
99
|
|
71
100
|
|
@@ -82,6 +111,20 @@ def record_dependencies(
|
|
82
111
|
]
|
83
112
|
],
|
84
113
|
) -> None:
|
114
|
+
"""
|
115
|
+
Register cache keys against the filters and exclusions they depend on.
|
116
|
+
|
117
|
+
Parameters:
|
118
|
+
cache_key (str): Cache key produced for the cached queryset.
|
119
|
+
dependencies (Iterable[tuple[str, Literal["filter", "exclude", "identification"], str]]):
|
120
|
+
Collection describing manager name, dependency type, and identifying data.
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
None
|
124
|
+
|
125
|
+
Raises:
|
126
|
+
TimeoutError: If the dependency lock cannot be acquired within `LOCK_TIMEOUT`.
|
127
|
+
"""
|
85
128
|
start = time.time()
|
86
129
|
while not acquire_lock():
|
87
130
|
if time.time() - start > LOCK_TIMEOUT:
|
@@ -92,15 +135,16 @@ def record_dependencies(
|
|
92
135
|
idx = get_full_index()
|
93
136
|
for model_name, action, identifier in dependencies:
|
94
137
|
if action in ("filter", "exclude"):
|
138
|
+
action_key = cast(Literal["filter", "exclude"], action)
|
95
139
|
params = ast.literal_eval(identifier)
|
96
|
-
section = idx[
|
140
|
+
section = idx[action_key].setdefault(model_name, {})
|
97
141
|
for lookup, val in params.items():
|
98
142
|
lookup_map = section.setdefault(lookup, {})
|
99
143
|
val_key = repr(val)
|
100
144
|
lookup_map.setdefault(val_key, set()).add(cache_key)
|
101
145
|
|
102
146
|
else:
|
103
|
-
#
|
147
|
+
# Treat identification lookups as a simple filter on `id`
|
104
148
|
section = idx["filter"].setdefault(model_name, {})
|
105
149
|
lookup_map = section.setdefault("identification", {})
|
106
150
|
val_key = identifier
|
@@ -116,7 +160,18 @@ def record_dependencies(
|
|
116
160
|
# INDEX CLEANUP
|
117
161
|
# -----------------------------------------------------------------------------
|
118
162
|
def remove_cache_key_from_index(cache_key: str) -> None:
|
119
|
-
"""
|
163
|
+
"""
|
164
|
+
Remove a cache entry from all dependency mappings.
|
165
|
+
|
166
|
+
Parameters:
|
167
|
+
cache_key (str): Cache key that should be expunged from the index.
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
None
|
171
|
+
|
172
|
+
Raises:
|
173
|
+
TimeoutError: If the dependency lock cannot be acquired within `LOCK_TIMEOUT`.
|
174
|
+
"""
|
120
175
|
start = time.time()
|
121
176
|
while not acquire_lock():
|
122
177
|
if time.time() - start > LOCK_TIMEOUT:
|
@@ -147,13 +202,35 @@ def remove_cache_key_from_index(cache_key: str) -> None:
|
|
147
202
|
# CACHE INVALIDATION
|
148
203
|
# -----------------------------------------------------------------------------
|
149
204
|
def invalidate_cache_key(cache_key: str) -> None:
|
205
|
+
"""
|
206
|
+
Delete the cached result associated with the provided key.
|
207
|
+
|
208
|
+
Parameters:
|
209
|
+
cache_key (str): Key referencing the cached queryset.
|
210
|
+
|
211
|
+
Returns:
|
212
|
+
None
|
213
|
+
"""
|
150
214
|
cache.delete(cache_key)
|
151
215
|
|
152
216
|
|
153
217
|
@receiver(pre_data_change)
|
154
218
|
def capture_old_values(
|
155
|
-
sender: Type[GeneralManager],
|
219
|
+
sender: Type[GeneralManager],
|
220
|
+
instance: GeneralManager | None,
|
221
|
+
**kwargs: object,
|
156
222
|
) -> None:
|
223
|
+
"""
|
224
|
+
Cache the field values referenced by tracked filters before an update.
|
225
|
+
|
226
|
+
Parameters:
|
227
|
+
sender (type[GeneralManager]): Manager class dispatching the signal.
|
228
|
+
instance (GeneralManager | None): Manager instance about to change.
|
229
|
+
**kwargs: Additional signal metadata.
|
230
|
+
|
231
|
+
Returns:
|
232
|
+
None
|
233
|
+
"""
|
157
234
|
if instance is None:
|
158
235
|
return
|
159
236
|
manager_name = sender.__name__
|
@@ -164,16 +241,16 @@ def capture_old_values(
|
|
164
241
|
lookups |= set(idx.get(action, {}).get(manager_name, {}))
|
165
242
|
if lookups and instance.identification:
|
166
243
|
# save old values for later comparison
|
167
|
-
vals = {}
|
244
|
+
vals: dict[str, object] = {}
|
168
245
|
for lookup in lookups:
|
169
246
|
attr_path = lookup.split("__")
|
170
|
-
|
247
|
+
current: object = instance
|
171
248
|
for i, attr in enumerate(attr_path):
|
172
|
-
if getattr(
|
249
|
+
if getattr(current, attr, UNDEFINED) is UNDEFINED:
|
173
250
|
lookup = "__".join(attr_path[:i])
|
174
251
|
break
|
175
|
-
|
176
|
-
vals[lookup] =
|
252
|
+
current = getattr(current, attr, None)
|
253
|
+
vals[lookup] = current
|
177
254
|
setattr(instance, "_old_values", vals)
|
178
255
|
|
179
256
|
|
@@ -182,12 +259,19 @@ def generic_cache_invalidation(
|
|
182
259
|
sender: type[GeneralManager],
|
183
260
|
instance: GeneralManager,
|
184
261
|
old_relevant_values: dict[str, Any],
|
185
|
-
**kwargs,
|
186
|
-
):
|
262
|
+
**kwargs: object,
|
263
|
+
) -> None:
|
187
264
|
"""
|
188
|
-
|
189
|
-
|
190
|
-
|
265
|
+
Invalidate cached query results affected by a data change.
|
266
|
+
|
267
|
+
Parameters:
|
268
|
+
sender (type[GeneralManager]): Manager class that triggered the signal.
|
269
|
+
instance (GeneralManager): Updated manager instance.
|
270
|
+
old_relevant_values (dict[str, Any]): Previously captured values for tracked lookups.
|
271
|
+
**kwargs: Additional signal metadata.
|
272
|
+
|
273
|
+
Returns:
|
274
|
+
None
|
191
275
|
"""
|
192
276
|
manager_name = sender.__name__
|
193
277
|
idx = get_full_index()
|
@@ -208,7 +292,7 @@ def generic_cache_invalidation(
|
|
208
292
|
except:
|
209
293
|
return False
|
210
294
|
|
211
|
-
# range
|
295
|
+
# range comparisons
|
212
296
|
if op in ("gt", "gte", "lt", "lte"):
|
213
297
|
try:
|
214
298
|
thr = type(value)(ast.literal_eval(val_key))
|
@@ -223,7 +307,7 @@ def generic_cache_invalidation(
|
|
223
307
|
if op == "lte":
|
224
308
|
return value <= thr
|
225
309
|
|
226
|
-
# wildcard / regex
|
310
|
+
# wildcard / regex comparisons
|
227
311
|
if op in ("contains", "startswith", "endswith", "regex"):
|
228
312
|
try:
|
229
313
|
literal = ast.literal_eval(val_key)
|
@@ -238,7 +322,7 @@ def generic_cache_invalidation(
|
|
238
322
|
return text.startswith(literal)
|
239
323
|
if op == "endswith":
|
240
324
|
return text.endswith(literal)
|
241
|
-
# regex:
|
325
|
+
# regex: treat the stored key as the regex pattern
|
242
326
|
if op == "regex":
|
243
327
|
try:
|
244
328
|
pattern = re.compile(val_key)
|
@@ -273,12 +357,12 @@ def generic_cache_invalidation(
|
|
273
357
|
# 2) get old & new value
|
274
358
|
old_val = old_relevant_values.get("__".join(attr_path))
|
275
359
|
|
276
|
-
|
360
|
+
current: object = instance
|
277
361
|
for attr in attr_path:
|
278
|
-
|
279
|
-
if
|
362
|
+
current = getattr(current, attr, None)
|
363
|
+
if current is None:
|
280
364
|
break
|
281
|
-
new_val =
|
365
|
+
new_val = current
|
282
366
|
|
283
367
|
# 3) check against all cache_keys
|
284
368
|
for val_key, cache_keys in list(lookup_map.items()):
|
@@ -1,3 +1,5 @@
|
|
1
|
+
"""Helpers that derive cache dependency metadata from GeneralManager objects."""
|
2
|
+
|
1
3
|
from typing import Generator
|
2
4
|
from general_manager.manager.generalManager import GeneralManager
|
3
5
|
from general_manager.bucket.baseBucket import Bucket
|
@@ -9,21 +11,20 @@ from general_manager.cache.dependencyIndex import (
|
|
9
11
|
|
10
12
|
|
11
13
|
class ModelDependencyCollector:
|
14
|
+
"""Collect dependency tuples from cached arguments."""
|
12
15
|
|
13
16
|
@staticmethod
|
14
17
|
def collect(
|
15
|
-
obj,
|
18
|
+
obj: object,
|
16
19
|
) -> Generator[tuple[general_manager_name, filter_type, str], None, None]:
|
17
20
|
"""
|
18
|
-
|
19
|
-
|
20
|
-
Inspects the input object and its nested structures to identify instances of GeneralManager and Bucket, yielding a tuple for each dependency found. Each tuple contains the manager class name, the dependency type ("identification", "filter", or "exclude"), and the string representation of the dependency value.
|
21
|
+
Traverse arbitrary objects and yield cache dependency tuples.
|
21
22
|
|
22
|
-
|
23
|
-
obj:
|
23
|
+
Parameters:
|
24
|
+
obj (object): Object that may contain GeneralManager instances, buckets, or nested collections.
|
24
25
|
|
25
26
|
Yields:
|
26
|
-
|
27
|
+
tuple[str, filter_type, str]: Dependency descriptors combining manager name, dependency type, and lookup data.
|
27
28
|
"""
|
28
29
|
if isinstance(obj, GeneralManager):
|
29
30
|
yield (
|
@@ -44,7 +45,15 @@ class ModelDependencyCollector:
|
|
44
45
|
@staticmethod
|
45
46
|
def addArgs(dependencies: set[Dependency], args: tuple, kwargs: dict) -> None:
|
46
47
|
"""
|
47
|
-
|
48
|
+
Enrich the dependency set with values discovered in positional and keyword arguments.
|
49
|
+
|
50
|
+
Parameters:
|
51
|
+
dependencies (set[Dependency]): Target collection that accumulates dependency tuples.
|
52
|
+
args (tuple): Positional arguments from the cached function.
|
53
|
+
kwargs (dict): Keyword arguments from the cached function.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
None
|
48
57
|
"""
|
49
58
|
if args and isinstance(args[0], GeneralManager):
|
50
59
|
inner_self = args[0]
|
general_manager/cache/signals.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
"""Signals and decorators for tracking GeneralManager data changes."""
|
2
|
+
|
1
3
|
from django.dispatch import Signal
|
2
4
|
from typing import Callable, TypeVar, ParamSpec, cast
|
3
5
|
|
@@ -13,17 +15,26 @@ R = TypeVar("R")
|
|
13
15
|
|
14
16
|
def dataChange(func: Callable[P, R]) -> Callable[P, R]:
|
15
17
|
"""
|
16
|
-
|
17
|
-
|
18
|
-
|
18
|
+
Wrap a data-modifying function with pre- and post-change signal dispatching.
|
19
|
+
|
20
|
+
Parameters:
|
21
|
+
func (Callable[P, R]): Function that performs a data mutation.
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
Callable[P, R]: Wrapped function that sends `pre_data_change` and `post_data_change` signals.
|
19
25
|
"""
|
20
26
|
|
21
27
|
@wraps(func)
|
22
28
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
23
29
|
"""
|
24
|
-
|
25
|
-
|
26
|
-
|
30
|
+
Execute the wrapped function while emitting data change signals.
|
31
|
+
|
32
|
+
Parameters:
|
33
|
+
*args: Positional arguments forwarded to the wrapped function.
|
34
|
+
**kwargs: Keyword arguments forwarded to the wrapped function.
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
R: Result produced by the wrapped function.
|
27
38
|
"""
|
28
39
|
action = func.__name__
|
29
40
|
if func.__name__ == "create":
|
@@ -1,5 +1,34 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
"""Factory helpers for generating GeneralManager test data."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from general_manager.utils.public_api import build_module_dir, resolve_export
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"AutoFactory",
|
11
|
+
"LazyMeasurement",
|
12
|
+
"LazyDeltaDate",
|
13
|
+
"LazyProjectName",
|
14
|
+
]
|
15
|
+
|
16
|
+
_MODULE_MAP = {
|
17
|
+
"AutoFactory": ("general_manager.factory.autoFactory", "AutoFactory"),
|
18
|
+
"LazyMeasurement": ("general_manager.factory.factoryMethods", "LazyMeasurement"),
|
19
|
+
"LazyDeltaDate": ("general_manager.factory.factoryMethods", "LazyDeltaDate"),
|
20
|
+
"LazyProjectName": ("general_manager.factory.factoryMethods", "LazyProjectName"),
|
21
|
+
}
|
22
|
+
|
23
|
+
|
24
|
+
def __getattr__(name: str) -> Any:
|
25
|
+
return resolve_export(
|
26
|
+
name,
|
27
|
+
module_all=__all__,
|
28
|
+
module_map=_MODULE_MAP,
|
29
|
+
module_globals=globals(),
|
30
|
+
)
|
31
|
+
|
32
|
+
|
33
|
+
def __dir__() -> list[str]:
|
34
|
+
return build_module_dir(module_all=__all__, module_globals=globals())
|
@@ -1,3 +1,5 @@
|
|
1
|
+
"""Auto-generating factory utilities for GeneralManager models."""
|
2
|
+
|
1
3
|
from __future__ import annotations
|
2
4
|
from typing import TYPE_CHECKING, Type, Callable, Union, Any, TypeVar, Literal
|
3
5
|
from django.db import models
|
@@ -14,10 +16,7 @@ modelsModel = TypeVar("modelsModel", bound=models.Model)
|
|
14
16
|
|
15
17
|
|
16
18
|
class AutoFactory(DjangoModelFactory[modelsModel]):
|
17
|
-
"""
|
18
|
-
A factory class that automatically generates values for model fields,
|
19
|
-
including handling of unique fields and constraints.
|
20
|
-
"""
|
19
|
+
"""Factory that auto-populates model fields based on interface metadata."""
|
21
20
|
|
22
21
|
interface: Type[DBBasedInterface]
|
23
22
|
_adjustmentMethod: (
|
@@ -29,18 +28,15 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
29
28
|
cls, strategy: Literal["build", "create"], params: dict[str, Any]
|
30
29
|
) -> models.Model | list[models.Model]:
|
31
30
|
"""
|
32
|
-
|
33
|
-
|
34
|
-
Automatically fills unset model fields, excluding generic foreign keys and auto-created fields, and handles custom and special fields as defined by the interface. After instance creation or building, processes many-to-many relationships. Raises a ValueError if the model is not a subclass of Django's Model.
|
35
|
-
|
31
|
+
Generate and populate model instances with automatically derived field values.
|
32
|
+
|
36
33
|
Parameters:
|
37
|
-
strategy (Literal["build", "create"]):
|
38
|
-
params (dict[str, Any]): Field values
|
39
|
-
|
34
|
+
strategy (Literal["build", "create"]): Whether to persist the generated instances.
|
35
|
+
params (dict[str, Any]): Field values supplied by the caller.
|
36
|
+
|
40
37
|
Returns:
|
41
|
-
models.Model
|
38
|
+
models.Model | list[models.Model]: Generated instance(s) matching the requested strategy.
|
42
39
|
"""
|
43
|
-
cls._original_params = params
|
44
40
|
model = cls._meta.model
|
45
41
|
if not issubclass(model, models.Model):
|
46
42
|
raise ValueError("Model must be a type")
|
@@ -86,9 +82,11 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
86
82
|
cls, obj: models.Model, attrs: dict[str, Any]
|
87
83
|
) -> None:
|
88
84
|
"""
|
89
|
-
|
90
|
-
|
91
|
-
|
85
|
+
Assign related objects to many-to-many fields after creation/building.
|
86
|
+
|
87
|
+
Parameters:
|
88
|
+
obj (models.Model): Instance whose many-to-many relations should be populated.
|
89
|
+
attrs (dict[str, Any]): Original attributes passed to the factory.
|
92
90
|
"""
|
93
91
|
for field in obj._meta.many_to_many:
|
94
92
|
if field.name in attrs:
|
@@ -99,13 +97,15 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
99
97
|
getattr(obj, field.name).set(m2m_values)
|
100
98
|
|
101
99
|
@classmethod
|
102
|
-
def _adjust_kwargs(cls, **kwargs:
|
103
|
-
# Remove ManyToMany fields from kwargs before object creation
|
100
|
+
def _adjust_kwargs(cls, **kwargs: Any) -> dict[str, Any]:
|
104
101
|
"""
|
105
|
-
|
106
|
-
|
102
|
+
Remove many-to-many keys from kwargs prior to model instantiation.
|
103
|
+
|
104
|
+
Parameters:
|
105
|
+
**kwargs (dict[str, Any]): Field values supplied by the caller.
|
106
|
+
|
107
107
|
Returns:
|
108
|
-
dict[str, Any]:
|
108
|
+
dict[str, Any]: Keyword arguments with many-to-many entries stripped.
|
109
109
|
"""
|
110
110
|
model: Type[models.Model] = cls._meta.model
|
111
111
|
m2m_fields = {field.name for field in model._meta.many_to_many}
|
@@ -115,15 +115,18 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
115
115
|
|
116
116
|
@classmethod
|
117
117
|
def _create(
|
118
|
-
cls, model_class: Type[models.Model], *args:
|
118
|
+
cls, model_class: Type[models.Model], *args: Any, **kwargs: Any
|
119
119
|
) -> models.Model | list[models.Model]:
|
120
120
|
"""
|
121
|
-
Create and save
|
122
|
-
|
123
|
-
|
124
|
-
|
121
|
+
Create and save model instance(s), applying adjustment hooks when defined.
|
122
|
+
|
123
|
+
Parameters:
|
124
|
+
model_class (type[models.Model]): Django model class to instantiate.
|
125
|
+
*args: Unused positional arguments (required by factory_boy).
|
126
|
+
**kwargs (dict[str, Any]): Field values supplied by the caller.
|
127
|
+
|
125
128
|
Returns:
|
126
|
-
|
129
|
+
models.Model | list[models.Model]: Saved instance(s).
|
127
130
|
"""
|
128
131
|
kwargs = cls._adjust_kwargs(**kwargs)
|
129
132
|
if cls._adjustmentMethod is not None:
|
@@ -132,15 +135,18 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
132
135
|
|
133
136
|
@classmethod
|
134
137
|
def _build(
|
135
|
-
cls, model_class: Type[models.Model], *args:
|
138
|
+
cls, model_class: Type[models.Model], *args: Any, **kwargs: Any
|
136
139
|
) -> models.Model | list[models.Model]:
|
137
140
|
"""
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
141
|
+
Build (without saving) model instance(s), applying adjustment hooks when defined.
|
142
|
+
|
143
|
+
Parameters:
|
144
|
+
model_class (type[models.Model]): Django model class to instantiate.
|
145
|
+
*args: Unused positional arguments (required by factory_boy).
|
146
|
+
**kwargs (dict[str, Any]): Field values supplied by the caller.
|
147
|
+
|
142
148
|
Returns:
|
143
|
-
models.Model
|
149
|
+
models.Model | list[models.Model]: Unsaved instance(s).
|
144
150
|
"""
|
145
151
|
kwargs = cls._adjust_kwargs(**kwargs)
|
146
152
|
if cls._adjustmentMethod is not None:
|
@@ -151,15 +157,17 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
151
157
|
|
152
158
|
@classmethod
|
153
159
|
def _modelCreation(
|
154
|
-
cls, model_class: Type[models.Model], **kwargs:
|
160
|
+
cls, model_class: Type[models.Model], **kwargs: Any
|
155
161
|
) -> models.Model:
|
156
162
|
"""
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
163
|
+
Instantiate, validate, and save a model instance.
|
164
|
+
|
165
|
+
Parameters:
|
166
|
+
model_class (type[models.Model]): Model class to instantiate.
|
167
|
+
**kwargs (dict[str, Any]): Field assignments applied prior to saving.
|
168
|
+
|
161
169
|
Returns:
|
162
|
-
|
170
|
+
models.Model: Saved instance.
|
163
171
|
"""
|
164
172
|
obj = model_class()
|
165
173
|
for field, value in kwargs.items():
|
@@ -170,18 +178,9 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
170
178
|
|
171
179
|
@classmethod
|
172
180
|
def _modelBuilding(
|
173
|
-
cls, model_class: Type[models.Model], **kwargs:
|
181
|
+
cls, model_class: Type[models.Model], **kwargs: Any
|
174
182
|
) -> models.Model:
|
175
|
-
"""
|
176
|
-
Constructs an unsaved Django model instance with the specified field values.
|
177
|
-
|
178
|
-
Parameters:
|
179
|
-
model_class (Type[models.Model]): The Django model class to instantiate.
|
180
|
-
**kwargs: Field values to assign to the model instance.
|
181
|
-
|
182
|
-
Returns:
|
183
|
-
models.Model: An unsaved instance of the specified model with attributes set from kwargs.
|
184
|
-
"""
|
183
|
+
"""Construct an unsaved model instance with the provided field values."""
|
185
184
|
obj = model_class()
|
186
185
|
for field, value in kwargs.items():
|
187
186
|
setattr(obj, field, value)
|
@@ -192,19 +191,17 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
192
191
|
cls, use_creation_method: bool, params: dict[str, Any]
|
193
192
|
) -> models.Model | list[models.Model]:
|
194
193
|
"""
|
195
|
-
|
196
|
-
|
197
|
-
If the adjustment method returns a single dictionary, a single instance is created or built. If it returns a list of dictionaries, multiple instances are created or built accordingly.
|
198
|
-
|
194
|
+
Create or build instance(s) using the configured adjustment method.
|
195
|
+
|
199
196
|
Parameters:
|
200
|
-
use_creation_method (bool):
|
201
|
-
params (dict[str, Any]): Arguments
|
202
|
-
|
197
|
+
use_creation_method (bool): Whether generated objects should be saved.
|
198
|
+
params (dict[str, Any]): Arguments forwarded to the adjustment callback.
|
199
|
+
|
203
200
|
Returns:
|
204
|
-
models.Model
|
205
|
-
|
201
|
+
models.Model | list[models.Model]: Created or built instance(s).
|
202
|
+
|
206
203
|
Raises:
|
207
|
-
ValueError: If
|
204
|
+
ValueError: If no adjustment method has been configured.
|
208
205
|
"""
|
209
206
|
model_cls = cls._meta.model
|
210
207
|
if cls._adjustmentMethod is None:
|