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.

Files changed (67) hide show
  1. general_manager/__init__.py +11 -1
  2. general_manager/_types/api.py +0 -1
  3. general_manager/_types/bucket.py +0 -1
  4. general_manager/_types/cache.py +0 -1
  5. general_manager/_types/factory.py +0 -1
  6. general_manager/_types/general_manager.py +0 -1
  7. general_manager/_types/interface.py +0 -1
  8. general_manager/_types/manager.py +0 -1
  9. general_manager/_types/measurement.py +0 -1
  10. general_manager/_types/permission.py +0 -1
  11. general_manager/_types/rule.py +0 -1
  12. general_manager/_types/utils.py +0 -1
  13. general_manager/api/__init__.py +13 -1
  14. general_manager/api/graphql.py +356 -221
  15. general_manager/api/graphql_subscription_consumer.py +81 -78
  16. general_manager/api/mutation.py +85 -23
  17. general_manager/api/property.py +39 -13
  18. general_manager/apps.py +188 -47
  19. general_manager/bucket/__init__.py +10 -1
  20. general_manager/bucket/calculationBucket.py +155 -53
  21. general_manager/bucket/databaseBucket.py +157 -45
  22. general_manager/bucket/groupBucket.py +133 -44
  23. general_manager/cache/__init__.py +10 -1
  24. general_manager/cache/dependencyIndex.py +143 -45
  25. general_manager/cache/signals.py +9 -2
  26. general_manager/factory/__init__.py +10 -1
  27. general_manager/factory/autoFactory.py +55 -13
  28. general_manager/factory/factories.py +110 -40
  29. general_manager/factory/factoryMethods.py +122 -34
  30. general_manager/interface/__init__.py +10 -1
  31. general_manager/interface/baseInterface.py +129 -36
  32. general_manager/interface/calculationInterface.py +35 -18
  33. general_manager/interface/databaseBasedInterface.py +71 -45
  34. general_manager/interface/databaseInterface.py +96 -38
  35. general_manager/interface/models.py +5 -5
  36. general_manager/interface/readOnlyInterface.py +94 -20
  37. general_manager/manager/__init__.py +10 -1
  38. general_manager/manager/generalManager.py +25 -16
  39. general_manager/manager/groupManager.py +20 -6
  40. general_manager/manager/meta.py +84 -16
  41. general_manager/measurement/__init__.py +10 -1
  42. general_manager/measurement/measurement.py +289 -95
  43. general_manager/measurement/measurementField.py +42 -31
  44. general_manager/permission/__init__.py +10 -1
  45. general_manager/permission/basePermission.py +120 -38
  46. general_manager/permission/managerBasedPermission.py +72 -21
  47. general_manager/permission/mutationPermission.py +14 -9
  48. general_manager/permission/permissionChecks.py +14 -12
  49. general_manager/permission/permissionDataManager.py +24 -11
  50. general_manager/permission/utils.py +34 -6
  51. general_manager/public_api_registry.py +36 -10
  52. general_manager/rule/__init__.py +10 -1
  53. general_manager/rule/handler.py +133 -44
  54. general_manager/rule/rule.py +178 -39
  55. general_manager/utils/__init__.py +10 -1
  56. general_manager/utils/argsToKwargs.py +34 -9
  57. general_manager/utils/filterParser.py +22 -7
  58. general_manager/utils/formatString.py +1 -0
  59. general_manager/utils/pathMapping.py +23 -15
  60. general_manager/utils/public_api.py +33 -2
  61. general_manager/utils/testing.py +31 -33
  62. {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/METADATA +2 -1
  63. generalmanager-0.18.0.dist-info/RECORD +77 -0
  64. {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/licenses/LICENSE +1 -1
  65. generalmanager-0.17.0.dist-info/RECORD +0 -77
  66. {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/WHEEL +0 -0
  67. {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 keys against the filters and exclusions they depend on.
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): Cache key produced for the cached queryset.
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
- Collection describing manager name, dependency type, and identifying data.
122
-
123
- Returns:
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
- TimeoutError: If the dependency lock cannot be acquired within `LOCK_TIMEOUT`.
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 TimeoutError("Could not aquire lock for record_dependencies")
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("__cache_dependencies__", {})
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 entry from all dependency mappings.
186
+ Remove a cache key from all dependency mappings in the stored dependency index.
169
187
 
170
- Parameters:
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
- Returns:
174
- None
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
- TimeoutError: If the dependency lock cannot be acquired within `LOCK_TIMEOUT`.
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 TimeoutError("Could not aquire lock for remove_cache_key_from_index")
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 ("filter", "exclude"):
188
- action_section = idx.get(action, {})
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
- Cache the field values referenced by tracked filters before an update.
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
- sender (type[GeneralManager]): Manager class dispatching the signal.
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 ("filter", "exclude"):
252
- model_section = idx.get(action, {}).get(manager_name, {})
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
- setattr(instance, "_old_values", vals)
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 cached query results affected by a data change.
296
+ Invalidate cache entries whose recorded dependencies are affected by changes to a GeneralManager instance.
285
297
 
286
- Parameters:
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
- Returns:
293
- None
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) # type: ignore
341
- except Exception:
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 = literal if isinstance(literal, str) else str(literal)
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, lookup_key: str, action: 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 ("filter", "exclude"):
472
- model_section = idx.get(action, {}).get(manager_name, {})
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(ck, lookup, action)
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(ck, lookup, action)
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
@@ -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
- Execute the wrapped function while emitting data change signals.
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: Result produced by the wrapped function.
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: F401,F403
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 with automatically derived field values.
65
+ Generate and populate model instances using automatically derived field values.
32
66
 
33
67
  Parameters:
34
- strategy (Literal["build", "create"]): Whether to persist the generated instances.
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]: Generated instance(s) matching the requested strategy.
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
- if not issubclass(model, models.Model):
42
- raise ValueError("Model must be a type")
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 ValueError("Model must be a type")
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): Whether generated objects should be saved.
198
- params (dict[str, Any]): Arguments forwarded to the adjustment callback.
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]: Created or built instance(s).
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
- ValueError: If no adjustment method has been configured.
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 ValueError("generate_func is not defined")
250
+ raise UndefinedAdjustmentMethodError()
209
251
  records = cls._adjustmentMethod(**params)
210
252
  if isinstance(records, dict):
211
253
  if use_creation_method: