GeneralManager 0.19.1__py3-none-any.whl → 0.20.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/_types/api.py +4 -4
- general_manager/_types/bucket.py +4 -4
- general_manager/_types/cache.py +6 -6
- general_manager/_types/factory.py +35 -35
- general_manager/_types/general_manager.py +11 -9
- general_manager/_types/interface.py +5 -5
- general_manager/_types/manager.py +4 -4
- general_manager/_types/measurement.py +1 -1
- general_manager/_types/permission.py +3 -3
- general_manager/_types/utils.py +12 -12
- general_manager/api/graphql.py +207 -98
- general_manager/api/mutation.py +9 -9
- general_manager/api/property.py +4 -4
- general_manager/apps.py +120 -65
- general_manager/bucket/{baseBucket.py → base_bucket.py} +5 -5
- general_manager/bucket/{calculationBucket.py → calculation_bucket.py} +10 -10
- general_manager/bucket/{databaseBucket.py → database_bucket.py} +16 -19
- general_manager/bucket/{groupBucket.py → group_bucket.py} +8 -8
- general_manager/cache/{cacheDecorator.py → cache_decorator.py} +27 -6
- general_manager/cache/{cacheTracker.py → cache_tracker.py} +1 -1
- general_manager/cache/{dependencyIndex.py → dependency_index.py} +24 -8
- general_manager/cache/{modelDependencyCollector.py → model_dependency_collector.py} +4 -4
- general_manager/cache/signals.py +1 -1
- general_manager/factory/{autoFactory.py → auto_factory.py} +24 -19
- general_manager/factory/factories.py +10 -13
- general_manager/factory/{factoryMethods.py → factory_methods.py} +19 -17
- general_manager/interface/{baseInterface.py → base_interface.py} +30 -22
- general_manager/interface/{calculationInterface.py → calculation_interface.py} +10 -10
- general_manager/interface/{databaseBasedInterface.py → database_based_interface.py} +42 -42
- general_manager/interface/{databaseInterface.py → database_interface.py} +21 -21
- general_manager/interface/models.py +3 -3
- general_manager/interface/{readOnlyInterface.py → read_only_interface.py} +34 -25
- general_manager/logging.py +133 -0
- general_manager/manager/{generalManager.py → general_manager.py} +75 -17
- general_manager/manager/{groupManager.py → group_manager.py} +6 -6
- general_manager/manager/input.py +1 -1
- general_manager/manager/meta.py +63 -17
- general_manager/measurement/measurement.py +3 -3
- general_manager/permission/{basePermission.py → base_permission.py} +55 -32
- general_manager/permission/{managerBasedPermission.py → manager_based_permission.py} +21 -21
- general_manager/permission/{mutationPermission.py → mutation_permission.py} +12 -12
- general_manager/permission/{permissionChecks.py → permission_checks.py} +2 -2
- general_manager/permission/{permissionDataManager.py → permission_data_manager.py} +6 -6
- general_manager/permission/utils.py +6 -6
- general_manager/public_api_registry.py +76 -66
- general_manager/rule/handler.py +2 -2
- general_manager/rule/rule.py +102 -11
- general_manager/utils/{filterParser.py → filter_parser.py} +3 -3
- general_manager/utils/{jsonEncoder.py → json_encoder.py} +1 -1
- general_manager/utils/{makeCacheKey.py → make_cache_key.py} +1 -1
- general_manager/utils/{noneToZero.py → none_to_zero.py} +1 -1
- general_manager/utils/{pathMapping.py → path_mapping.py} +14 -14
- general_manager/utils/public_api.py +19 -0
- general_manager/utils/testing.py +14 -14
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/METADATA +1 -1
- generalmanager-0.20.0.dist-info/RECORD +78 -0
- generalmanager-0.19.1.dist-info/RECORD +0 -77
- /general_manager/measurement/{measurementField.py → measurement_field.py} +0 -0
- /general_manager/permission/{fileBasedPermission.py → file_based_permission.py} +0 -0
- /general_manager/utils/{argsToKwargs.py → args_to_kwargs.py} +0 -0
- /general_manager/utils/{formatString.py → format_string.py} +0 -0
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/licenses/LICENSE +0 -0
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/top_level.txt +0 -0
|
@@ -6,15 +6,12 @@ from typing import TYPE_CHECKING, Any, Generator, Type, TypeVar
|
|
|
6
6
|
from django.core.exceptions import FieldError
|
|
7
7
|
from django.db import models
|
|
8
8
|
|
|
9
|
-
from general_manager.bucket.
|
|
10
|
-
from general_manager.
|
|
11
|
-
from general_manager.
|
|
12
|
-
from general_manager.utils.filterParser import create_filter_function
|
|
9
|
+
from general_manager.bucket.base_bucket import Bucket
|
|
10
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
11
|
+
from general_manager.utils.filter_parser import create_filter_function
|
|
13
12
|
|
|
14
13
|
modelsModel = TypeVar("modelsModel", bound=models.Model)
|
|
15
|
-
|
|
16
|
-
if TYPE_CHECKING:
|
|
17
|
-
from general_manager.interface.databaseInterface import DatabaseInterface
|
|
14
|
+
GeneralManagerType = TypeVar("GeneralManagerType", bound=GeneralManager)
|
|
18
15
|
|
|
19
16
|
|
|
20
17
|
class DatabaseBucketTypeMismatchError(TypeError):
|
|
@@ -189,7 +186,7 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
189
186
|
{},
|
|
190
187
|
)
|
|
191
188
|
|
|
192
|
-
def
|
|
189
|
+
def __merge_filter_definitions(
|
|
193
190
|
self, basis: dict[str, list[Any]], **kwargs: Any
|
|
194
191
|
) -> dict[str, list[Any]]:
|
|
195
192
|
"""
|
|
@@ -211,7 +208,7 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
211
208
|
kwarg_filter[key].append(value)
|
|
212
209
|
return kwarg_filter
|
|
213
210
|
|
|
214
|
-
def
|
|
211
|
+
def __parse_filter_definitions(
|
|
215
212
|
self,
|
|
216
213
|
**kwargs: Any,
|
|
217
214
|
) -> tuple[dict[str, Any], dict[str, Any], list[tuple[str, Any, str]]]:
|
|
@@ -233,7 +230,7 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
233
230
|
annotations: dict[str, Any] = {}
|
|
234
231
|
orm_kwargs: dict[str, Any] = {}
|
|
235
232
|
python_filters: list[tuple[str, Any, str]] = []
|
|
236
|
-
properties = self._manager_class.Interface.
|
|
233
|
+
properties = self._manager_class.Interface.get_graph_ql_properties()
|
|
237
234
|
|
|
238
235
|
for k, v in kwargs.items():
|
|
239
236
|
root = k.split("__")[0]
|
|
@@ -251,7 +248,7 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
251
248
|
|
|
252
249
|
return annotations, orm_kwargs, python_filters
|
|
253
250
|
|
|
254
|
-
def
|
|
251
|
+
def __parse_python_filters(
|
|
255
252
|
self, query_set: models.QuerySet, python_filters: list[tuple[str, Any, str]]
|
|
256
253
|
) -> list[int]:
|
|
257
254
|
"""
|
|
@@ -293,7 +290,7 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
293
290
|
InvalidQueryAnnotationTypeError: If a query-annotation callback returns a non-QuerySet.
|
|
294
291
|
QuerysetFilteringError: If the ORM rejects the filter arguments or filtering fails.
|
|
295
292
|
"""
|
|
296
|
-
annotations, orm_kwargs, python_filters = self.
|
|
293
|
+
annotations, orm_kwargs, python_filters = self.__parse_filter_definitions(
|
|
297
294
|
**kwargs
|
|
298
295
|
)
|
|
299
296
|
qs = self._data
|
|
@@ -313,10 +310,10 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
313
310
|
raise QuerysetFilteringError(error) from error
|
|
314
311
|
|
|
315
312
|
if python_filters:
|
|
316
|
-
ids = self.
|
|
313
|
+
ids = self.__parse_python_filters(qs, python_filters)
|
|
317
314
|
qs = qs.filter(pk__in=ids)
|
|
318
315
|
|
|
319
|
-
merged_filter = self.
|
|
316
|
+
merged_filter = self.__merge_filter_definitions(self.filters, **kwargs)
|
|
320
317
|
return self.__class__(qs, self._manager_class, merged_filter, self.excludes)
|
|
321
318
|
|
|
322
319
|
def exclude(self, **kwargs: Any) -> DatabaseBucket[GeneralManagerType]:
|
|
@@ -334,7 +331,7 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
334
331
|
Raises:
|
|
335
332
|
InvalidQueryAnnotationTypeError: If an annotation callable is applied and does not return a Django QuerySet.
|
|
336
333
|
"""
|
|
337
|
-
annotations, orm_kwargs, python_filters = self.
|
|
334
|
+
annotations, orm_kwargs, python_filters = self.__parse_filter_definitions(
|
|
338
335
|
**kwargs
|
|
339
336
|
)
|
|
340
337
|
qs = self._data
|
|
@@ -351,10 +348,10 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
351
348
|
qs = qs.exclude(**orm_kwargs)
|
|
352
349
|
|
|
353
350
|
if python_filters:
|
|
354
|
-
ids = self.
|
|
351
|
+
ids = self.__parse_python_filters(qs, python_filters)
|
|
355
352
|
qs = qs.exclude(pk__in=ids)
|
|
356
353
|
|
|
357
|
-
merged_exclude = self.
|
|
354
|
+
merged_exclude = self.__merge_filter_definitions(self.excludes, **kwargs)
|
|
358
355
|
return self.__class__(qs, self._manager_class, self.filters, merged_exclude)
|
|
359
356
|
|
|
360
357
|
def first(self) -> GeneralManagerType | None:
|
|
@@ -467,7 +464,7 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
467
464
|
Returns:
|
|
468
465
|
bool: True when the primary key exists in the queryset.
|
|
469
466
|
"""
|
|
470
|
-
from general_manager.manager.
|
|
467
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
471
468
|
|
|
472
469
|
if isinstance(item, GeneralManager):
|
|
473
470
|
return item.identification.get("id", None) in self._data.values_list(
|
|
@@ -499,7 +496,7 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
499
496
|
"""
|
|
500
497
|
if isinstance(key, str):
|
|
501
498
|
key = (key,)
|
|
502
|
-
properties = self._manager_class.Interface.
|
|
499
|
+
properties = self._manager_class.Interface.get_graph_ql_properties()
|
|
503
500
|
annotations: dict[str, Any] = {}
|
|
504
501
|
python_keys: list[str] = []
|
|
505
502
|
qs = self._data
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
from typing import Any, Generator, Type
|
|
5
|
-
from general_manager.manager.
|
|
6
|
-
from general_manager.bucket.
|
|
5
|
+
from general_manager.manager.group_manager import GroupManager
|
|
6
|
+
from general_manager.bucket.base_bucket import Bucket, GeneralManagerType
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class InvalidGroupByKeyTypeError(TypeError):
|
|
@@ -132,10 +132,10 @@ class GroupBucket(Bucket[GeneralManagerType]):
|
|
|
132
132
|
ValueError: If a group-by key is not a valid manager attribute.
|
|
133
133
|
"""
|
|
134
134
|
super().__init__(manager_class)
|
|
135
|
-
self.
|
|
135
|
+
self.__check_group_by_arguments(group_by_keys)
|
|
136
136
|
self._group_by_keys = group_by_keys
|
|
137
|
-
self._data: list[GroupManager[GeneralManagerType]] =
|
|
138
|
-
data
|
|
137
|
+
self._data: list[GroupManager[GeneralManagerType]] = (
|
|
138
|
+
self.__build_grouped_manager(data)
|
|
139
139
|
)
|
|
140
140
|
self._basis_data: Bucket[GeneralManagerType] = data
|
|
141
141
|
|
|
@@ -157,7 +157,7 @@ class GroupBucket(Bucket[GeneralManagerType]):
|
|
|
157
157
|
and self._group_by_keys == other._group_by_keys
|
|
158
158
|
)
|
|
159
159
|
|
|
160
|
-
def
|
|
160
|
+
def __check_group_by_arguments(self, group_by_keys: tuple[str, ...]) -> None:
|
|
161
161
|
"""
|
|
162
162
|
Validate that each provided group-by key is a string and is exposed by the manager interface.
|
|
163
163
|
|
|
@@ -171,12 +171,12 @@ class GroupBucket(Bucket[GeneralManagerType]):
|
|
|
171
171
|
if not all(isinstance(arg, str) for arg in group_by_keys):
|
|
172
172
|
raise InvalidGroupByKeyTypeError()
|
|
173
173
|
if not all(
|
|
174
|
-
arg in self._manager_class.Interface.
|
|
174
|
+
arg in self._manager_class.Interface.get_attributes()
|
|
175
175
|
for arg in group_by_keys
|
|
176
176
|
):
|
|
177
177
|
raise UnknownGroupByKeyError(self._manager_class.__name__)
|
|
178
178
|
|
|
179
|
-
def
|
|
179
|
+
def __build_grouped_manager(
|
|
180
180
|
self,
|
|
181
181
|
data: Bucket[GeneralManagerType],
|
|
182
182
|
) -> list[GroupManager[GeneralManagerType]]:
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"""Helpers for caching GeneralManager computations with dependency tracking."""
|
|
2
2
|
|
|
3
|
-
from typing import Any, Callable, Optional, Protocol, Set, TypeVar, cast
|
|
4
3
|
from functools import wraps
|
|
4
|
+
from typing import Any, Callable, Optional, Protocol, Set, TypeVar, cast
|
|
5
|
+
|
|
5
6
|
from django.core.cache import cache as django_cache
|
|
6
|
-
|
|
7
|
-
from general_manager.cache.
|
|
8
|
-
from general_manager.cache.
|
|
9
|
-
from general_manager.
|
|
7
|
+
|
|
8
|
+
from general_manager.cache.cache_tracker import DependencyTracker
|
|
9
|
+
from general_manager.cache.dependency_index import Dependency, record_dependencies
|
|
10
|
+
from general_manager.cache.model_dependency_collector import ModelDependencyCollector
|
|
11
|
+
from general_manager.logging import get_logger
|
|
12
|
+
from general_manager.utils.make_cache_key import make_cache_key
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
class CacheBackend(Protocol):
|
|
@@ -42,6 +45,7 @@ RecordFn = Callable[[str, Set[Dependency]], None]
|
|
|
42
45
|
FuncT = TypeVar("FuncT", bound=Callable[..., object])
|
|
43
46
|
|
|
44
47
|
_SENTINEL = object()
|
|
48
|
+
logger = get_logger("cache.decorator")
|
|
45
49
|
|
|
46
50
|
|
|
47
51
|
def cached(
|
|
@@ -74,11 +78,19 @@ def cached(
|
|
|
74
78
|
if cached_deps:
|
|
75
79
|
for class_name, operation, identifier in cached_deps:
|
|
76
80
|
DependencyTracker.track(class_name, operation, identifier)
|
|
81
|
+
logger.debug(
|
|
82
|
+
"cache hit",
|
|
83
|
+
context={
|
|
84
|
+
"function": func.__qualname__,
|
|
85
|
+
"key": key,
|
|
86
|
+
"dependency_count": len(cached_deps) if cached_deps else 0,
|
|
87
|
+
},
|
|
88
|
+
)
|
|
77
89
|
return cached_result
|
|
78
90
|
|
|
79
91
|
with DependencyTracker() as dependencies:
|
|
80
92
|
result = func(*args, **kwargs)
|
|
81
|
-
ModelDependencyCollector.
|
|
93
|
+
ModelDependencyCollector.add_args(dependencies, args, kwargs)
|
|
82
94
|
|
|
83
95
|
cache_backend.set(key, result, timeout)
|
|
84
96
|
cache_backend.set(deps_key, dependencies, timeout)
|
|
@@ -86,6 +98,15 @@ def cached(
|
|
|
86
98
|
if dependencies and timeout is None:
|
|
87
99
|
record_fn(key, dependencies)
|
|
88
100
|
|
|
101
|
+
logger.debug(
|
|
102
|
+
"cache miss recorded",
|
|
103
|
+
context={
|
|
104
|
+
"function": func.__qualname__,
|
|
105
|
+
"key": key,
|
|
106
|
+
"dependency_count": len(dependencies),
|
|
107
|
+
"timeout": timeout,
|
|
108
|
+
},
|
|
109
|
+
)
|
|
89
110
|
return result
|
|
90
111
|
|
|
91
112
|
# fix for python 3.14:
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
"""Dependency index management for cached GeneralManager query results."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
import ast
|
|
6
6
|
import re
|
|
7
|
-
import
|
|
7
|
+
import time
|
|
8
8
|
from datetime import date, datetime
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Iterable, Literal, Tuple, Type, cast
|
|
9
10
|
|
|
10
11
|
from django.core.cache import cache
|
|
11
|
-
from general_manager.cache.signals import post_data_change, pre_data_change
|
|
12
12
|
from django.dispatch import receiver
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
from general_manager.cache.signals import post_data_change, pre_data_change
|
|
15
|
+
from general_manager.logging import get_logger
|
|
14
16
|
|
|
15
17
|
if TYPE_CHECKING:
|
|
16
|
-
from general_manager.manager.
|
|
18
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
17
19
|
|
|
18
20
|
type general_manager_name = str # e.g. "Project", "Derivative", "User"
|
|
19
21
|
type attribute = str # e.g. "field", "name", "id"
|
|
@@ -31,7 +33,7 @@ type dependency_index = dict[
|
|
|
31
33
|
type filter_type = Literal["filter", "exclude", "identification"]
|
|
32
34
|
type Dependency = Tuple[general_manager_name, filter_type, str]
|
|
33
35
|
|
|
34
|
-
logger =
|
|
36
|
+
logger = get_logger("cache.dependency_index")
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
class DependencyLockTimeoutError(TimeoutError):
|
|
@@ -616,7 +618,14 @@ def generic_cache_invalidation(
|
|
|
616
618
|
)
|
|
617
619
|
if should_invalidate:
|
|
618
620
|
logger.info(
|
|
619
|
-
|
|
621
|
+
"invalidating cache key",
|
|
622
|
+
context={
|
|
623
|
+
"manager": manager_name,
|
|
624
|
+
"key": ck,
|
|
625
|
+
"lookup": lookup,
|
|
626
|
+
"action": action,
|
|
627
|
+
"value": val_key,
|
|
628
|
+
},
|
|
620
629
|
)
|
|
621
630
|
invalidate_cache_key(ck)
|
|
622
631
|
remove_cache_key_from_index(ck)
|
|
@@ -634,7 +643,14 @@ def generic_cache_invalidation(
|
|
|
634
643
|
)
|
|
635
644
|
if should_invalidate:
|
|
636
645
|
logger.info(
|
|
637
|
-
|
|
646
|
+
"invalidating cache key",
|
|
647
|
+
context={
|
|
648
|
+
"manager": manager_name,
|
|
649
|
+
"key": ck,
|
|
650
|
+
"lookup": lookup,
|
|
651
|
+
"action": action,
|
|
652
|
+
"value": val_key,
|
|
653
|
+
},
|
|
638
654
|
)
|
|
639
655
|
invalidate_cache_key(ck)
|
|
640
656
|
remove_cache_key_from_index(ck)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Helpers that derive cache dependency metadata from GeneralManager objects."""
|
|
2
2
|
|
|
3
3
|
from typing import Generator
|
|
4
|
-
from general_manager.manager.
|
|
5
|
-
from general_manager.bucket.
|
|
6
|
-
from general_manager.cache.
|
|
4
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
5
|
+
from general_manager.bucket.base_bucket import Bucket
|
|
6
|
+
from general_manager.cache.dependency_index import (
|
|
7
7
|
general_manager_name,
|
|
8
8
|
Dependency,
|
|
9
9
|
filter_type,
|
|
@@ -43,7 +43,7 @@ class ModelDependencyCollector:
|
|
|
43
43
|
yield from ModelDependencyCollector.collect(item)
|
|
44
44
|
|
|
45
45
|
@staticmethod
|
|
46
|
-
def
|
|
46
|
+
def add_args(dependencies: set[Dependency], args: tuple, kwargs: dict) -> None:
|
|
47
47
|
"""
|
|
48
48
|
Enrich the dependency set with values discovered in positional and keyword arguments.
|
|
49
49
|
|
general_manager/cache/signals.py
CHANGED
|
@@ -4,11 +4,14 @@ from __future__ import annotations
|
|
|
4
4
|
from typing import TYPE_CHECKING, Type, Callable, Union, Any, TypeVar, Literal
|
|
5
5
|
from django.db import models
|
|
6
6
|
from factory.django import DjangoModelFactory
|
|
7
|
-
from general_manager.factory.factories import
|
|
7
|
+
from general_manager.factory.factories import (
|
|
8
|
+
get_field_value,
|
|
9
|
+
get_many_to_many_field_value,
|
|
10
|
+
)
|
|
8
11
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
9
12
|
|
|
10
13
|
if TYPE_CHECKING:
|
|
11
|
-
from general_manager.interface.
|
|
14
|
+
from general_manager.interface.database_interface import (
|
|
12
15
|
DBBasedInterface,
|
|
13
16
|
)
|
|
14
17
|
|
|
@@ -82,7 +85,7 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
|
82
85
|
is_model = False
|
|
83
86
|
if not is_model:
|
|
84
87
|
raise InvalidAutoFactoryModelError
|
|
85
|
-
field_name_list, to_ignore_list = cls.interface.
|
|
88
|
+
field_name_list, to_ignore_list = cls.interface.handle_custom_fields(model)
|
|
86
89
|
|
|
87
90
|
fields = [
|
|
88
91
|
field
|
|
@@ -107,20 +110,20 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
|
107
110
|
continue # Skip fields that are already set
|
|
108
111
|
if isinstance(field, models.AutoField) or field.auto_created:
|
|
109
112
|
continue # Skip auto fields
|
|
110
|
-
params[field.name] =
|
|
113
|
+
params[field.name] = get_field_value(field)
|
|
111
114
|
|
|
112
115
|
obj: list[models.Model] | models.Model = super()._generate(strategy, params)
|
|
113
116
|
if isinstance(obj, list):
|
|
114
117
|
for item in obj:
|
|
115
118
|
if not isinstance(item, models.Model):
|
|
116
119
|
raise InvalidGeneratedObjectError()
|
|
117
|
-
cls.
|
|
120
|
+
cls._handle_many_to_many_fields_after_creation(item, params)
|
|
118
121
|
else:
|
|
119
|
-
cls.
|
|
122
|
+
cls._handle_many_to_many_fields_after_creation(obj, params)
|
|
120
123
|
return obj
|
|
121
124
|
|
|
122
125
|
@classmethod
|
|
123
|
-
def
|
|
126
|
+
def _handle_many_to_many_fields_after_creation(
|
|
124
127
|
cls, obj: models.Model, attrs: dict[str, Any]
|
|
125
128
|
) -> None:
|
|
126
129
|
"""
|
|
@@ -134,7 +137,7 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
|
134
137
|
if field.name in attrs:
|
|
135
138
|
m2m_values = attrs[field.name]
|
|
136
139
|
else:
|
|
137
|
-
m2m_values =
|
|
140
|
+
m2m_values = get_many_to_many_field_value(field)
|
|
138
141
|
if m2m_values:
|
|
139
142
|
getattr(obj, field.name).set(m2m_values)
|
|
140
143
|
|
|
@@ -172,8 +175,10 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
|
172
175
|
"""
|
|
173
176
|
kwargs = cls._adjust_kwargs(**kwargs)
|
|
174
177
|
if cls._adjustmentMethod is not None:
|
|
175
|
-
return cls.
|
|
176
|
-
|
|
178
|
+
return cls.__create_with_generate_func(
|
|
179
|
+
use_creation_method=True, params=kwargs
|
|
180
|
+
)
|
|
181
|
+
return cls._model_creation(model_class, **kwargs)
|
|
177
182
|
|
|
178
183
|
@classmethod
|
|
179
184
|
def _build(
|
|
@@ -192,13 +197,13 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
|
192
197
|
"""
|
|
193
198
|
kwargs = cls._adjust_kwargs(**kwargs)
|
|
194
199
|
if cls._adjustmentMethod is not None:
|
|
195
|
-
return cls.
|
|
200
|
+
return cls.__create_with_generate_func(
|
|
196
201
|
use_creation_method=False, params=kwargs
|
|
197
202
|
)
|
|
198
|
-
return cls.
|
|
203
|
+
return cls._model_building(model_class, **kwargs)
|
|
199
204
|
|
|
200
205
|
@classmethod
|
|
201
|
-
def
|
|
206
|
+
def _model_creation(
|
|
202
207
|
cls, model_class: Type[models.Model], **kwargs: Any
|
|
203
208
|
) -> models.Model:
|
|
204
209
|
"""
|
|
@@ -219,7 +224,7 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
|
219
224
|
return obj
|
|
220
225
|
|
|
221
226
|
@classmethod
|
|
222
|
-
def
|
|
227
|
+
def _model_building(
|
|
223
228
|
cls, model_class: Type[models.Model], **kwargs: Any
|
|
224
229
|
) -> models.Model:
|
|
225
230
|
"""Construct an unsaved model instance with the provided field values."""
|
|
@@ -229,7 +234,7 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
|
229
234
|
return obj
|
|
230
235
|
|
|
231
236
|
@classmethod
|
|
232
|
-
def
|
|
237
|
+
def __create_with_generate_func(
|
|
233
238
|
cls, use_creation_method: bool, params: dict[str, Any]
|
|
234
239
|
) -> models.Model | list[models.Model]:
|
|
235
240
|
"""
|
|
@@ -251,13 +256,13 @@ class AutoFactory(DjangoModelFactory[modelsModel]):
|
|
|
251
256
|
records = cls._adjustmentMethod(**params)
|
|
252
257
|
if isinstance(records, dict):
|
|
253
258
|
if use_creation_method:
|
|
254
|
-
return cls.
|
|
255
|
-
return cls.
|
|
259
|
+
return cls._model_creation(model_cls, **records)
|
|
260
|
+
return cls._model_building(model_cls, **records)
|
|
256
261
|
|
|
257
262
|
created_objects: list[models.Model] = []
|
|
258
263
|
for record in records:
|
|
259
264
|
if use_creation_method:
|
|
260
|
-
created_objects.append(cls.
|
|
265
|
+
created_objects.append(cls._model_creation(model_cls, **record))
|
|
261
266
|
else:
|
|
262
|
-
created_objects.append(cls.
|
|
267
|
+
created_objects.append(cls._model_building(model_cls, **record))
|
|
263
268
|
return created_objects
|
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
"""Helpers for generating realistic factory values for Django models."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Any, cast
|
|
5
5
|
|
|
6
6
|
from factory.declarations import LazyFunction
|
|
7
7
|
from factory.faker import Faker
|
|
8
8
|
import exrex # type: ignore[import-untyped]
|
|
9
9
|
from django.core.validators import RegexValidator
|
|
10
10
|
from django.db import models
|
|
11
|
-
from datetime import date, datetime, time, timezone
|
|
11
|
+
from datetime import date, datetime, time, timezone, timedelta
|
|
12
12
|
from decimal import Decimal
|
|
13
13
|
from random import SystemRandom
|
|
14
14
|
from general_manager.measurement.measurement import Measurement
|
|
15
|
-
from general_manager.measurement.
|
|
16
|
-
|
|
17
|
-
if TYPE_CHECKING:
|
|
18
|
-
from general_manager.factory.autoFactory import AutoFactory
|
|
15
|
+
from general_manager.measurement.measurement_field import MeasurementField
|
|
19
16
|
|
|
20
17
|
|
|
21
18
|
_RNG = SystemRandom()
|
|
@@ -65,7 +62,7 @@ class InvalidRelatedModelTypeError(TypeError):
|
|
|
65
62
|
)
|
|
66
63
|
|
|
67
64
|
|
|
68
|
-
def
|
|
65
|
+
def get_field_value(
|
|
69
66
|
field: models.Field[Any, Any] | models.ForeignObjectRel,
|
|
70
67
|
) -> Any:
|
|
71
68
|
"""
|
|
@@ -158,7 +155,7 @@ def getFieldValue(
|
|
|
158
155
|
elif isinstance(field, models.UUIDField):
|
|
159
156
|
return cast(str, Faker("uuid4"))
|
|
160
157
|
elif isinstance(field, models.DurationField):
|
|
161
|
-
return cast(
|
|
158
|
+
return cast(timedelta, Faker("time_delta"))
|
|
162
159
|
elif isinstance(field, models.CharField):
|
|
163
160
|
max_length = field.max_length or 100
|
|
164
161
|
# Check for RegexValidator
|
|
@@ -173,7 +170,7 @@ def getFieldValue(
|
|
|
173
170
|
else:
|
|
174
171
|
return cast(str, Faker("text", max_nb_chars=max_length))
|
|
175
172
|
elif isinstance(field, models.OneToOneField):
|
|
176
|
-
related_model =
|
|
173
|
+
related_model = get_related_model(field)
|
|
177
174
|
if hasattr(related_model, "_general_manager_class"):
|
|
178
175
|
related_factory = related_model._general_manager_class.Factory # type: ignore
|
|
179
176
|
return related_factory()
|
|
@@ -185,7 +182,7 @@ def getFieldValue(
|
|
|
185
182
|
else:
|
|
186
183
|
raise MissingFactoryOrInstancesError(related_model)
|
|
187
184
|
elif isinstance(field, models.ForeignKey):
|
|
188
|
-
related_model =
|
|
185
|
+
related_model = get_related_model(field)
|
|
189
186
|
# Create or get an instance of the related model
|
|
190
187
|
if hasattr(related_model, "_general_manager_class"):
|
|
191
188
|
create_a_new_instance = _RNG.choice([True, True, False])
|
|
@@ -209,7 +206,7 @@ def getFieldValue(
|
|
|
209
206
|
return None
|
|
210
207
|
|
|
211
208
|
|
|
212
|
-
def
|
|
209
|
+
def get_related_model(
|
|
213
210
|
field: models.ForeignObjectRel | models.Field[Any, Any],
|
|
214
211
|
) -> type[models.Model]:
|
|
215
212
|
"""
|
|
@@ -239,7 +236,7 @@ def getRelatedModel(
|
|
|
239
236
|
return cast(type[models.Model], related_model)
|
|
240
237
|
|
|
241
238
|
|
|
242
|
-
def
|
|
239
|
+
def get_many_to_many_field_value(
|
|
243
240
|
field: models.ManyToManyField,
|
|
244
241
|
) -> list[models.Model]:
|
|
245
242
|
"""
|
|
@@ -257,7 +254,7 @@ def getManyToManyFieldValue(
|
|
|
257
254
|
MissingFactoryOrInstancesError: If the related model provides neither a factory nor any existing instances.
|
|
258
255
|
"""
|
|
259
256
|
related_factory = None
|
|
260
|
-
related_model =
|
|
257
|
+
related_model = get_related_model(field)
|
|
261
258
|
related_instances = list(related_model.objects.all())
|
|
262
259
|
if hasattr(related_model, "_general_manager_class"):
|
|
263
260
|
related_factory = related_model._general_manager_class.Factory # type: ignore
|