GeneralManager 0.14.1__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.
Files changed (62) hide show
  1. general_manager/__init__.py +49 -0
  2. general_manager/api/__init__.py +36 -0
  3. general_manager/api/graphql.py +92 -43
  4. general_manager/api/mutation.py +35 -10
  5. general_manager/api/property.py +26 -3
  6. general_manager/apps.py +23 -16
  7. general_manager/bucket/__init__.py +32 -0
  8. general_manager/bucket/baseBucket.py +76 -64
  9. general_manager/bucket/calculationBucket.py +188 -108
  10. general_manager/bucket/databaseBucket.py +130 -49
  11. general_manager/bucket/groupBucket.py +113 -60
  12. general_manager/cache/__init__.py +38 -0
  13. general_manager/cache/cacheDecorator.py +29 -17
  14. general_manager/cache/cacheTracker.py +34 -15
  15. general_manager/cache/dependencyIndex.py +117 -33
  16. general_manager/cache/modelDependencyCollector.py +17 -8
  17. general_manager/cache/signals.py +17 -6
  18. general_manager/factory/__init__.py +34 -5
  19. general_manager/factory/autoFactory.py +57 -60
  20. general_manager/factory/factories.py +39 -14
  21. general_manager/factory/factoryMethods.py +38 -1
  22. general_manager/interface/__init__.py +36 -0
  23. general_manager/interface/baseInterface.py +71 -27
  24. general_manager/interface/calculationInterface.py +18 -10
  25. general_manager/interface/databaseBasedInterface.py +102 -71
  26. general_manager/interface/databaseInterface.py +66 -20
  27. general_manager/interface/models.py +10 -4
  28. general_manager/interface/readOnlyInterface.py +44 -30
  29. general_manager/manager/__init__.py +36 -3
  30. general_manager/manager/generalManager.py +73 -47
  31. general_manager/manager/groupManager.py +72 -17
  32. general_manager/manager/input.py +23 -15
  33. general_manager/manager/meta.py +53 -53
  34. general_manager/measurement/__init__.py +37 -2
  35. general_manager/measurement/measurement.py +135 -58
  36. general_manager/measurement/measurementField.py +161 -61
  37. general_manager/permission/__init__.py +32 -1
  38. general_manager/permission/basePermission.py +29 -12
  39. general_manager/permission/managerBasedPermission.py +32 -26
  40. general_manager/permission/mutationPermission.py +32 -3
  41. general_manager/permission/permissionChecks.py +9 -1
  42. general_manager/permission/permissionDataManager.py +49 -15
  43. general_manager/permission/utils.py +14 -3
  44. general_manager/rule/__init__.py +27 -1
  45. general_manager/rule/handler.py +90 -5
  46. general_manager/rule/rule.py +40 -27
  47. general_manager/utils/__init__.py +44 -2
  48. general_manager/utils/argsToKwargs.py +17 -9
  49. general_manager/utils/filterParser.py +29 -30
  50. general_manager/utils/formatString.py +2 -0
  51. general_manager/utils/jsonEncoder.py +14 -1
  52. general_manager/utils/makeCacheKey.py +18 -12
  53. general_manager/utils/noneToZero.py +8 -6
  54. general_manager/utils/pathMapping.py +92 -29
  55. general_manager/utils/public_api.py +49 -0
  56. general_manager/utils/testing.py +135 -69
  57. {generalmanager-0.14.1.dist-info → generalmanager-0.15.0.dist-info}/METADATA +10 -2
  58. generalmanager-0.15.0.dist-info/RECORD +62 -0
  59. generalmanager-0.14.1.dist-info/RECORD +0 -58
  60. {generalmanager-0.14.1.dist-info → generalmanager-0.15.0.dist-info}/WHEEL +0 -0
  61. {generalmanager-0.14.1.dist-info → generalmanager-0.15.0.dist-info}/licenses/LICENSE +0 -0
  62. {generalmanager-0.14.1.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" # Key unter dem der gesamte Index liegt
37
- LOCK_KEY = "dependency_index_lock" # Key für das Sperr‑Mutex
38
- LOCK_TIMEOUT = 5 # Sekunden TTL für den Lock
39
- UNDEFINED = object() # Dummy für nicht definierte Werte
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
- """Atomar: create Lock key if it doesn't exist."""
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
- """Release Lock key."""
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
- """Load or initialize the full index."""
60
- idx = cache.get(INDEX_KEY, None)
61
- if idx is None:
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
- return idx
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
- """Write the complete index back to the cache."""
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[action].setdefault(model_name, {})
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
- # director ID Lookup as simple filter on 'id'
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
- """Remove a cache key from the index."""
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], instance: GeneralManager | None, **kwargs
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
- obj = instance
247
+ current: object = instance
171
248
  for i, attr in enumerate(attr_path):
172
- if getattr(obj, attr, UNDEFINED) is UNDEFINED:
249
+ if getattr(current, attr, UNDEFINED) is UNDEFINED:
173
250
  lookup = "__".join(attr_path[:i])
174
251
  break
175
- obj = getattr(obj, attr, None)
176
- vals[lookup] = obj
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
- Invalidates cached query results related to a model instance when its data changes.
189
-
190
- This function is intended to be used as a Django signal handler. It compares old and new values of relevant fields on a model instance against registered cache dependencies (filters and excludes). If a change affects any cached queryset result, the corresponding cache keys are invalidated and removed from the dependency index.
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: val_key selbst als Pattern benutzen
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
- obj = instance
360
+ current: object = instance
277
361
  for attr in attr_path:
278
- obj = getattr(obj, attr, None)
279
- if obj is None:
362
+ current = getattr(current, attr, None)
363
+ if current is None:
280
364
  break
281
- new_val = obj
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
- Recursively extracts dependency information from Django model-related objects.
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
- Args:
23
- obj: The object or collection to inspect for model dependencies.
23
+ Parameters:
24
+ obj (object): Object that may contain GeneralManager instances, buckets, or nested collections.
24
25
 
25
26
  Yields:
26
- Tuples of (manager class name, dependency type, dependency value) for each dependency discovered.
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
- Add dependencies to the dependency set.
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]
@@ -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
- Decorator that emits pre- and post-data change signals around the execution of the decorated function.
17
-
18
- Sends the `pre_data_change` signal before the wrapped function is called and the `post_data_change` signal after it completes. The signals include information about the sender, action, and relevant instance state before and after the change. Handles both regular functions and classmethods. Intended for use with functions that modify data to enable signal-based hooks for data change events.
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
- Wraps a function to emit pre- and post-data change signals around its execution.
25
-
26
- Sends the `pre_data_change` signal before the wrapped function is called and the `post_data_change` signal after, providing context such as the sender, action name, and relevant instance data. Handles both regular functions and classmethods, and distinguishes the "create" action by omitting a pre-existing instance.
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
- from .factoryMethods import (
2
- LazyMeasurement,
3
- LazyDeltaDate,
4
- LazyProjectName,
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
- Generates and populates one or more Django model instances with automatic field value assignment.
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"]): Determines whether to build (unsaved) or create (saved) the instance(s).
38
- params (dict[str, Any]): Field values to use for instance generation; missing fields are auto-filled.
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 or list[models.Model]: The generated model instance or list of instances.
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
- Assigns related objects to all many-to-many fields of a Django model instance after it has been created.
90
-
91
- For each many-to-many field, sets the related objects from the provided attributes if available; otherwise, generates related objects automatically. Uses the Django ORM's `set` method to establish the relationships.
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: dict[str, Any]) -> dict[str, Any]:
103
- # Remove ManyToMany fields from kwargs before object creation
100
+ def _adjust_kwargs(cls, **kwargs: Any) -> dict[str, Any]:
104
101
  """
105
- Removes many-to-many fields from the provided keyword arguments before creating or building a model instance.
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]: The keyword arguments with any many-to-many fields excluded.
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: list[Any], **kwargs: dict[str, Any]
118
+ cls, model_class: Type[models.Model], *args: Any, **kwargs: Any
119
119
  ) -> models.Model | list[models.Model]:
120
120
  """
121
- Create and save a Django model instance or multiple instances, optionally applying custom adjustment logic to field values.
122
-
123
- If an adjustment method is defined, it is used to generate or modify field values before instance creation. Otherwise, the model is instantiated and saved with the provided attributes.
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
- A saved model instance or a list of saved instances.
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: list[Any], **kwargs: dict[str, Any]
138
+ cls, model_class: Type[models.Model], *args: Any, **kwargs: Any
136
139
  ) -> models.Model | list[models.Model]:
137
140
  """
138
- Builds an unsaved instance or list of instances of the specified Django model class.
139
-
140
- If an adjustment method is defined, it is used to generate or modify field values before building. Many-to-many fields are excluded from the keyword arguments prior to instantiation.
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 or list[models.Model]: The unsaved model instance or list of instances.
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: dict[str, Any]
160
+ cls, model_class: Type[models.Model], **kwargs: Any
155
161
  ) -> models.Model:
156
162
  """
157
- Create, validate, and save a Django model instance with the specified field values.
158
-
159
- Initializes the model, assigns attributes from keyword arguments, performs validation with `full_clean()`, and saves the instance to the database.
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
- The saved Django model instance.
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: dict[str, Any]
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
- Creates or builds one or more model instances using the adjustment method to generate field values.
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): If True, instances are saved to the database; if False, instances are built but not saved.
201
- params (dict[str, Any]): Arguments passed to the adjustment method for generating field values.
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 or list[models.Model]: The created or built model instance(s).
205
-
201
+ models.Model | list[models.Model]: Created or built instance(s).
202
+
206
203
  Raises:
207
- ValueError: If the adjustment method is not defined.
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: