GeneralManager 0.17.0__py3-none-any.whl → 0.18.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of GeneralManager might be problematic. Click here for more details.
- general_manager/__init__.py +11 -1
- general_manager/_types/api.py +0 -1
- general_manager/_types/bucket.py +0 -1
- general_manager/_types/cache.py +0 -1
- general_manager/_types/factory.py +0 -1
- general_manager/_types/general_manager.py +0 -1
- general_manager/_types/interface.py +0 -1
- general_manager/_types/manager.py +0 -1
- general_manager/_types/measurement.py +0 -1
- general_manager/_types/permission.py +0 -1
- general_manager/_types/rule.py +0 -1
- general_manager/_types/utils.py +0 -1
- general_manager/api/__init__.py +13 -1
- general_manager/api/graphql.py +356 -221
- general_manager/api/graphql_subscription_consumer.py +81 -78
- general_manager/api/mutation.py +85 -23
- general_manager/api/property.py +39 -13
- general_manager/apps.py +188 -47
- general_manager/bucket/__init__.py +10 -1
- general_manager/bucket/calculationBucket.py +155 -53
- general_manager/bucket/databaseBucket.py +157 -45
- general_manager/bucket/groupBucket.py +133 -44
- general_manager/cache/__init__.py +10 -1
- general_manager/cache/dependencyIndex.py +143 -45
- general_manager/cache/signals.py +9 -2
- general_manager/factory/__init__.py +10 -1
- general_manager/factory/autoFactory.py +55 -13
- general_manager/factory/factories.py +110 -40
- general_manager/factory/factoryMethods.py +122 -34
- general_manager/interface/__init__.py +10 -1
- general_manager/interface/baseInterface.py +129 -36
- general_manager/interface/calculationInterface.py +35 -18
- general_manager/interface/databaseBasedInterface.py +71 -45
- general_manager/interface/databaseInterface.py +96 -38
- general_manager/interface/models.py +5 -5
- general_manager/interface/readOnlyInterface.py +94 -20
- general_manager/manager/__init__.py +10 -1
- general_manager/manager/generalManager.py +25 -16
- general_manager/manager/groupManager.py +20 -6
- general_manager/manager/meta.py +84 -16
- general_manager/measurement/__init__.py +10 -1
- general_manager/measurement/measurement.py +289 -95
- general_manager/measurement/measurementField.py +42 -31
- general_manager/permission/__init__.py +10 -1
- general_manager/permission/basePermission.py +120 -38
- general_manager/permission/managerBasedPermission.py +72 -21
- general_manager/permission/mutationPermission.py +14 -9
- general_manager/permission/permissionChecks.py +14 -12
- general_manager/permission/permissionDataManager.py +24 -11
- general_manager/permission/utils.py +34 -6
- general_manager/public_api_registry.py +36 -10
- general_manager/rule/__init__.py +10 -1
- general_manager/rule/handler.py +133 -44
- general_manager/rule/rule.py +178 -39
- general_manager/utils/__init__.py +10 -1
- general_manager/utils/argsToKwargs.py +34 -9
- general_manager/utils/filterParser.py +22 -7
- general_manager/utils/formatString.py +1 -0
- general_manager/utils/pathMapping.py +23 -15
- general_manager/utils/public_api.py +33 -2
- general_manager/utils/testing.py +31 -33
- {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/METADATA +2 -1
- generalmanager-0.18.0.dist-info/RECORD +77 -0
- {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/licenses/LICENSE +1 -1
- generalmanager-0.17.0.dist-info/RECORD +0 -77
- {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/top_level.txt +0 -0
|
@@ -1,48 +1,121 @@
|
|
|
1
1
|
"""Helpers for generating realistic factory values for Django models."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
from typing import Any, cast
|
|
4
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
5
|
+
|
|
5
6
|
from factory.declarations import LazyFunction
|
|
6
7
|
from factory.faker import Faker
|
|
7
8
|
import exrex # type: ignore[import-untyped]
|
|
8
|
-
from django.db import models
|
|
9
9
|
from django.core.validators import RegexValidator
|
|
10
|
-
import
|
|
10
|
+
from django.db import models
|
|
11
|
+
from datetime import date, datetime, time, timezone
|
|
11
12
|
from decimal import Decimal
|
|
13
|
+
from random import SystemRandom
|
|
12
14
|
from general_manager.measurement.measurement import Measurement
|
|
13
15
|
from general_manager.measurement.measurementField import MeasurementField
|
|
14
|
-
from datetime import date, datetime, time, timezone
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
17
18
|
from general_manager.factory.autoFactory import AutoFactory
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
_RNG = SystemRandom()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MissingFactoryOrInstancesError(ValueError):
|
|
25
|
+
"""Raised when a related model offers neither a factory nor existing instances."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, related_model: type[models.Model]) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Exception raised when a related model has neither a registered factory nor any existing instances.
|
|
30
|
+
|
|
31
|
+
Parameters:
|
|
32
|
+
related_model (type[models.Model]): The Django model class that lacks both a factory and existing instances.
|
|
33
|
+
"""
|
|
34
|
+
super().__init__(
|
|
35
|
+
f"No factory found for {related_model.__name__} and no instances found."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MissingRelatedModelError(ValueError):
|
|
40
|
+
"""Raised when a relational field lacks a related model definition."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, field_name: str) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Initialize the exception for a field that does not declare a related model.
|
|
45
|
+
|
|
46
|
+
Parameters:
|
|
47
|
+
field_name (str): The name of the field missing a related model; included in the exception message.
|
|
48
|
+
"""
|
|
49
|
+
super().__init__(f"Field {field_name} does not have a related model defined.")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class InvalidRelatedModelTypeError(TypeError):
|
|
53
|
+
"""Raised when a relational field references an incompatible model type."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, field_name: str, related: object) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Initialize the exception indicating a relational field references a non-model type.
|
|
58
|
+
|
|
59
|
+
Parameters:
|
|
60
|
+
field_name (str): Name of the relational field that declared an invalid related model.
|
|
61
|
+
related (object): The value provided as the related model; its repr is included in the exception message.
|
|
62
|
+
"""
|
|
63
|
+
super().__init__(
|
|
64
|
+
f"Related model for {field_name} must be a Django model class, got {related!r}."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
20
68
|
def getFieldValue(
|
|
21
69
|
field: models.Field[Any, Any] | models.ForeignObjectRel,
|
|
22
|
-
) ->
|
|
70
|
+
) -> Any:
|
|
23
71
|
"""
|
|
24
|
-
Generate a realistic value for a Django model field.
|
|
72
|
+
Generate a realistic sample value for a Django model field or relation.
|
|
73
|
+
|
|
74
|
+
This returns a value appropriate for the field's type (e.g., text for TextField, Decimal for DecimalField,
|
|
75
|
+
datetime for DateTimeField, a LazyFunction that creates or selects a related instance for relational fields).
|
|
76
|
+
If the field is nullable there is a 10% chance this will return `None`.
|
|
25
77
|
|
|
26
78
|
Parameters:
|
|
27
|
-
field (models.Field | models.ForeignObjectRel):
|
|
79
|
+
field (models.Field | models.ForeignObjectRel): The Django field or relation to generate a value for.
|
|
28
80
|
|
|
29
81
|
Returns:
|
|
30
|
-
object:
|
|
82
|
+
object: A value suitable for assignment to the given field (or `None`).
|
|
31
83
|
|
|
32
84
|
Raises:
|
|
33
|
-
|
|
85
|
+
MissingFactoryOrInstancesError: When a related field's model has neither a factory nor any existing instances.
|
|
86
|
+
MissingRelatedModelError: When a relational field does not declare a related model.
|
|
87
|
+
InvalidRelatedModelTypeError: When a relational field's related value is not a Django model class.
|
|
34
88
|
"""
|
|
35
89
|
if field.null:
|
|
36
|
-
if
|
|
90
|
+
if _RNG.choice([True] + 9 * [False]):
|
|
37
91
|
return None
|
|
38
92
|
|
|
39
93
|
if isinstance(field, MeasurementField):
|
|
40
94
|
|
|
41
95
|
def _measurement() -> Measurement:
|
|
42
|
-
|
|
96
|
+
"""
|
|
97
|
+
Create a Measurement using the field's base unit and a randomly chosen value.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
measurement (Measurement): A Measurement whose value is a Decimal with two decimal places between 0.00 and 100000.00 and whose unit is the enclosing field's `base_unit`.
|
|
101
|
+
"""
|
|
102
|
+
value = Decimal(_RNG.randrange(0, 10_000_000)) / Decimal("100") # two dp
|
|
43
103
|
return Measurement(value, field.base_unit)
|
|
44
104
|
|
|
45
105
|
return LazyFunction(_measurement)
|
|
106
|
+
elif (
|
|
107
|
+
getattr(field, "choices", None)
|
|
108
|
+
and not getattr(field, "many_to_one", False)
|
|
109
|
+
and not getattr(field, "many_to_many", False)
|
|
110
|
+
):
|
|
111
|
+
# Use any declared choices directly to keep generated values valid.
|
|
112
|
+
flat_choices = [
|
|
113
|
+
choice[0] if isinstance(choice, (list, tuple)) and choice else choice
|
|
114
|
+
for choice in list(getattr(field, "flatchoices", ()))
|
|
115
|
+
]
|
|
116
|
+
if flat_choices:
|
|
117
|
+
return LazyFunction(lambda: _RNG.choice(flat_choices))
|
|
118
|
+
# Fall through to default behaviour when no usable choices were discovered.
|
|
46
119
|
elif isinstance(field, models.TextField):
|
|
47
120
|
return cast(str, Faker("paragraph"))
|
|
48
121
|
elif isinstance(field, models.IntegerField):
|
|
@@ -108,21 +181,19 @@ def getFieldValue(
|
|
|
108
181
|
# If no factory exists, pick a random existing instance
|
|
109
182
|
related_instances = list(related_model.objects.all())
|
|
110
183
|
if related_instances:
|
|
111
|
-
return LazyFunction(lambda:
|
|
184
|
+
return LazyFunction(lambda: _RNG.choice(related_instances))
|
|
112
185
|
else:
|
|
113
|
-
raise
|
|
114
|
-
f"No factory found for {related_model.__name__} and no instances found"
|
|
115
|
-
)
|
|
186
|
+
raise MissingFactoryOrInstancesError(related_model)
|
|
116
187
|
elif isinstance(field, models.ForeignKey):
|
|
117
188
|
related_model = getRelatedModel(field)
|
|
118
189
|
# Create or get an instance of the related model
|
|
119
190
|
if hasattr(related_model, "_general_manager_class"):
|
|
120
|
-
create_a_new_instance =
|
|
191
|
+
create_a_new_instance = _RNG.choice([True, True, False])
|
|
121
192
|
if not create_a_new_instance:
|
|
122
193
|
existing_instances = list(related_model.objects.all())
|
|
123
194
|
if existing_instances:
|
|
124
195
|
# Pick a random existing instance
|
|
125
|
-
return LazyFunction(lambda:
|
|
196
|
+
return LazyFunction(lambda: _RNG.choice(existing_instances))
|
|
126
197
|
|
|
127
198
|
related_factory = related_model._general_manager_class.Factory # type: ignore
|
|
128
199
|
return related_factory()
|
|
@@ -131,11 +202,9 @@ def getFieldValue(
|
|
|
131
202
|
# If no factory exists, pick a random existing instance
|
|
132
203
|
related_instances = list(related_model.objects.all())
|
|
133
204
|
if related_instances:
|
|
134
|
-
return LazyFunction(lambda:
|
|
205
|
+
return LazyFunction(lambda: _RNG.choice(related_instances))
|
|
135
206
|
else:
|
|
136
|
-
raise
|
|
137
|
-
f"No factory found for {related_model.__name__} and no instances found"
|
|
138
|
-
)
|
|
207
|
+
raise MissingFactoryOrInstancesError(related_model)
|
|
139
208
|
else:
|
|
140
209
|
return None
|
|
141
210
|
|
|
@@ -144,28 +213,29 @@ def getRelatedModel(
|
|
|
144
213
|
field: models.ForeignObjectRel | models.Field[Any, Any],
|
|
145
214
|
) -> type[models.Model]:
|
|
146
215
|
"""
|
|
147
|
-
|
|
216
|
+
Resolve and return the Django model class referenced by a relational field.
|
|
217
|
+
|
|
218
|
+
If the field's declared related model is the string "self", this resolves it to the field's model before validation.
|
|
148
219
|
|
|
149
220
|
Parameters:
|
|
150
|
-
field (models.
|
|
221
|
+
field (models.ForeignObjectRel | models.Field): Relational field or relation descriptor to inspect.
|
|
151
222
|
|
|
152
223
|
Returns:
|
|
153
|
-
type[models.Model]:
|
|
224
|
+
type[models.Model]: The related Django model class.
|
|
154
225
|
|
|
155
226
|
Raises:
|
|
156
|
-
|
|
227
|
+
MissingRelatedModelError: If the field does not declare a related model.
|
|
228
|
+
InvalidRelatedModelTypeError: If the resolved related model is not a Django model class.
|
|
157
229
|
"""
|
|
158
230
|
related_model = field.related_model
|
|
159
231
|
if related_model is None:
|
|
160
|
-
raise
|
|
232
|
+
raise MissingRelatedModelError(field.name)
|
|
161
233
|
if related_model == "self":
|
|
162
234
|
related_model = field.model
|
|
163
235
|
if not isinstance(related_model, type) or not issubclass(
|
|
164
236
|
related_model, models.Model
|
|
165
237
|
):
|
|
166
|
-
raise
|
|
167
|
-
f"Related model for {field.name} must be a Django model class, got {related_model!r}"
|
|
168
|
-
)
|
|
238
|
+
raise InvalidRelatedModelTypeError(field.name, related_model)
|
|
169
239
|
return cast(type[models.Model], related_model)
|
|
170
240
|
|
|
171
241
|
|
|
@@ -173,16 +243,18 @@ def getManyToManyFieldValue(
|
|
|
173
243
|
field: models.ManyToManyField,
|
|
174
244
|
) -> list[models.Model]:
|
|
175
245
|
"""
|
|
176
|
-
|
|
246
|
+
Generate a list of related model instances suitable for assigning to a ManyToManyField.
|
|
247
|
+
|
|
248
|
+
The function selects a random number of related objects (at least one when the field is not blank, up to 10). It will use the related model's factory to create new instances when available, prefer a mix of created and existing instances if both are present, or return existing instances when no factory is available.
|
|
177
249
|
|
|
178
250
|
Parameters:
|
|
179
|
-
field (models.ManyToManyField):
|
|
251
|
+
field (models.ManyToManyField): The ManyToMany field to generate values for.
|
|
180
252
|
|
|
181
253
|
Returns:
|
|
182
|
-
list[models.Model]:
|
|
254
|
+
list[models.Model]: A list of related model instances to assign to the field.
|
|
183
255
|
|
|
184
256
|
Raises:
|
|
185
|
-
|
|
257
|
+
MissingFactoryOrInstancesError: If the related model provides neither a factory nor any existing instances.
|
|
186
258
|
"""
|
|
187
259
|
related_factory = None
|
|
188
260
|
related_model = getRelatedModel(field)
|
|
@@ -191,13 +263,13 @@ def getManyToManyFieldValue(
|
|
|
191
263
|
related_factory = related_model._general_manager_class.Factory # type: ignore
|
|
192
264
|
|
|
193
265
|
min_required = 0 if field.blank else 1
|
|
194
|
-
number_of_instances =
|
|
266
|
+
number_of_instances = _RNG.randint(min_required, 10)
|
|
195
267
|
if related_factory and related_instances:
|
|
196
|
-
number_to_create =
|
|
268
|
+
number_to_create = _RNG.randint(min_required, number_of_instances)
|
|
197
269
|
number_to_pick = number_of_instances - number_to_create
|
|
198
270
|
if number_to_pick > len(related_instances):
|
|
199
271
|
number_to_pick = len(related_instances)
|
|
200
|
-
existing_instances =
|
|
272
|
+
existing_instances = _RNG.sample(related_instances, number_to_pick)
|
|
201
273
|
new_instances = [related_factory() for _ in range(number_to_create)]
|
|
202
274
|
return existing_instances + new_instances
|
|
203
275
|
elif related_factory:
|
|
@@ -209,9 +281,7 @@ def getManyToManyFieldValue(
|
|
|
209
281
|
number_to_pick = number_of_instances
|
|
210
282
|
if number_to_pick > len(related_instances):
|
|
211
283
|
number_to_pick = len(related_instances)
|
|
212
|
-
existing_instances =
|
|
284
|
+
existing_instances = _RNG.sample(related_instances, number_to_pick)
|
|
213
285
|
return existing_instances
|
|
214
286
|
else:
|
|
215
|
-
raise
|
|
216
|
-
f"No factory found for {related_model.__name__} and no instances found"
|
|
217
|
-
)
|
|
287
|
+
raise MissingFactoryOrInstancesError(related_model)
|
|
@@ -1,43 +1,62 @@
|
|
|
1
1
|
"""Convenience helpers for defining factory_boy lazy attributes."""
|
|
2
2
|
|
|
3
3
|
from typing import Any, Optional
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
from
|
|
7
|
-
from
|
|
4
|
+
|
|
5
|
+
from factory.declarations import LazyAttribute, LazyAttributeSequence, LazyFunction
|
|
6
|
+
from datetime import date, datetime, timedelta
|
|
7
|
+
from decimal import Decimal
|
|
8
8
|
from faker import Faker
|
|
9
|
+
from general_manager.measurement.measurement import Measurement
|
|
10
|
+
from random import SystemRandom
|
|
9
11
|
import uuid
|
|
10
|
-
from decimal import Decimal
|
|
11
12
|
|
|
12
13
|
fake = Faker()
|
|
14
|
+
_RNG = SystemRandom()
|
|
15
|
+
|
|
16
|
+
_AVG_DELTA_DAYS_ERROR = "avg_delta_days must be >= 0"
|
|
17
|
+
_EMPTY_OPTIONS_ERROR = "options must be a non-empty list"
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
def LazyMeasurement(
|
|
16
21
|
min_value: int | float, max_value: int | float, unit: str
|
|
17
22
|
) -> LazyFunction:
|
|
18
23
|
"""
|
|
19
|
-
|
|
24
|
+
Create a lazy factory that produces Measurement values with a numeric magnitude sampled between the given bounds and the specified unit.
|
|
20
25
|
|
|
21
26
|
Parameters:
|
|
22
|
-
min_value (int | float):
|
|
23
|
-
max_value (int | float):
|
|
24
|
-
unit (str): Measurement
|
|
27
|
+
min_value (int | float): Lower bound (inclusive) for the sampled magnitude.
|
|
28
|
+
max_value (int | float): Upper bound (inclusive) for the sampled magnitude.
|
|
29
|
+
unit (str): Unit string to attach to the Measurement.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
LazyFunction: A factory that yields a Measurement whose numeric value is drawn uniformly between min_value and max_value (formatted to six decimal places) and uses the provided unit.
|
|
25
33
|
"""
|
|
26
34
|
return LazyFunction(
|
|
27
|
-
lambda: Measurement(f"{
|
|
35
|
+
lambda: Measurement(f"{_RNG.uniform(min_value, max_value):.6f}", unit)
|
|
28
36
|
)
|
|
29
37
|
|
|
30
38
|
|
|
31
39
|
def LazyDeltaDate(avg_delta_days: int, base_attribute: str) -> LazyAttribute:
|
|
32
|
-
"""
|
|
40
|
+
"""
|
|
41
|
+
Compute a date by offsetting an instance's base date attribute by a randomized number of days.
|
|
33
42
|
|
|
34
43
|
Parameters:
|
|
35
|
-
avg_delta_days (int): Average number of days
|
|
36
|
-
|
|
44
|
+
avg_delta_days (int): Average number of days for the offset; the actual offset is randomly chosen
|
|
45
|
+
between floor(avg_delta_days / 2) and floor(avg_delta_days * 3 / 2), inclusive.
|
|
46
|
+
base_attribute (str): Name of the attribute on the instance that provides the base date. If that
|
|
47
|
+
attribute is missing or evaluates to false, today's date is used as the base.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
date: The base date shifted by the randomly chosen number of days.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: If avg_delta_days is negative.
|
|
37
54
|
"""
|
|
55
|
+
if avg_delta_days < 0:
|
|
56
|
+
raise ValueError(_AVG_DELTA_DAYS_ERROR)
|
|
38
57
|
return LazyAttribute(
|
|
39
|
-
lambda
|
|
40
|
-
+ timedelta(days=
|
|
58
|
+
lambda instance: (getattr(instance, base_attribute) or date.today())
|
|
59
|
+
+ timedelta(days=_RNG.randint(avg_delta_days // 2, avg_delta_days * 3 // 2))
|
|
41
60
|
)
|
|
42
61
|
|
|
43
62
|
|
|
@@ -59,57 +78,126 @@ def LazyDateToday() -> LazyFunction:
|
|
|
59
78
|
|
|
60
79
|
|
|
61
80
|
def LazyDateBetween(start_date: date, end_date: date) -> LazyAttribute:
|
|
62
|
-
"""
|
|
81
|
+
"""
|
|
82
|
+
Produce a lazy attribute that yields a date between two given dates (inclusive).
|
|
83
|
+
|
|
84
|
+
Parameters:
|
|
85
|
+
start_date (date): The start of the date range. If later than end_date, the range will be corrected.
|
|
86
|
+
end_date (date): The end of the date range. If earlier than start_date, the range will be corrected.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
date: A date between start_date and end_date, inclusive.
|
|
90
|
+
"""
|
|
63
91
|
delta = (end_date - start_date).days
|
|
64
92
|
if delta < 0:
|
|
65
93
|
start_date, end_date = end_date, start_date
|
|
66
94
|
delta = -delta
|
|
67
|
-
return LazyAttribute(
|
|
68
|
-
lambda obj: start_date + timedelta(days=random.randint(0, delta))
|
|
69
|
-
)
|
|
95
|
+
return LazyAttribute(lambda _: start_date + timedelta(days=_RNG.randint(0, delta)))
|
|
70
96
|
|
|
71
97
|
|
|
72
98
|
def LazyDateTimeBetween(start: datetime, end: datetime) -> LazyAttribute:
|
|
73
|
-
"""
|
|
99
|
+
"""
|
|
100
|
+
Produce a lazy attribute that yields a datetime within the inclusive range defined by `start` and `end`.
|
|
101
|
+
|
|
102
|
+
If `start` is after `end`, the two endpoints are swapped before selecting a value.
|
|
103
|
+
|
|
104
|
+
Parameters:
|
|
105
|
+
start (datetime): The start of the datetime range.
|
|
106
|
+
end (datetime): The end of the datetime range.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
LazyAttribute: A lazy attribute that produces a `datetime` between `start` and `end` (inclusive).
|
|
110
|
+
"""
|
|
74
111
|
span = (end - start).total_seconds()
|
|
75
112
|
if span < 0:
|
|
76
113
|
start, end = end, start
|
|
77
114
|
span = -span
|
|
78
115
|
return LazyAttribute(
|
|
79
|
-
lambda
|
|
116
|
+
lambda _: start + timedelta(seconds=_RNG.randint(0, int(span)))
|
|
80
117
|
)
|
|
81
118
|
|
|
82
119
|
|
|
83
120
|
def LazyInteger(min_value: int, max_value: int) -> LazyFunction:
|
|
84
|
-
"""
|
|
85
|
-
|
|
121
|
+
"""
|
|
122
|
+
Return a lazy factory that produces an integer within the provided bounds.
|
|
123
|
+
|
|
124
|
+
Parameters:
|
|
125
|
+
min_value (int): Lower bound (inclusive) for generated integers.
|
|
126
|
+
max_value (int): Upper bound (inclusive) for generated integers.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
int: A random integer greater than or equal to min_value and less than or equal to max_value.
|
|
130
|
+
"""
|
|
131
|
+
return LazyFunction(lambda: _RNG.randint(min_value, max_value))
|
|
86
132
|
|
|
87
133
|
|
|
88
134
|
def LazyDecimal(min_value: float, max_value: float, precision: int = 2) -> LazyFunction:
|
|
89
|
-
"""
|
|
135
|
+
"""
|
|
136
|
+
Create a lazy factory that produces Decimal values between min_value and max_value, rounded to the specified precision.
|
|
137
|
+
|
|
138
|
+
Parameters:
|
|
139
|
+
min_value (float): Lower bound of the generated value.
|
|
140
|
+
max_value (float): Upper bound of the generated value.
|
|
141
|
+
precision (int): Number of decimal places to round the generated value to.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Decimal: A Decimal value between min_value and max_value (inclusive), rounded to `precision` decimal places.
|
|
145
|
+
"""
|
|
90
146
|
fmt = f"{{:.{precision}f}}"
|
|
91
|
-
return LazyFunction(
|
|
92
|
-
lambda: Decimal(fmt.format(random.uniform(min_value, max_value)))
|
|
93
|
-
)
|
|
147
|
+
return LazyFunction(lambda: Decimal(fmt.format(_RNG.uniform(min_value, max_value))))
|
|
94
148
|
|
|
95
149
|
|
|
96
150
|
def LazyChoice(options: list[Any]) -> LazyFunction:
|
|
97
|
-
"""
|
|
98
|
-
|
|
151
|
+
"""
|
|
152
|
+
Create a lazy factory that selects a random element from the provided options.
|
|
153
|
+
|
|
154
|
+
Parameters:
|
|
155
|
+
options (list[Any]): Candidate values to choose from.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Any: One element randomly chosen from `options`.
|
|
159
|
+
"""
|
|
160
|
+
if not options:
|
|
161
|
+
raise ValueError(_EMPTY_OPTIONS_ERROR)
|
|
162
|
+
return LazyFunction(lambda: _RNG.choice(options))
|
|
99
163
|
|
|
100
164
|
|
|
101
165
|
def LazySequence(start: int = 0, step: int = 1) -> LazyAttributeSequence:
|
|
102
|
-
"""
|
|
103
|
-
|
|
166
|
+
"""
|
|
167
|
+
Produce a sequence attribute that yields successive integer values.
|
|
168
|
+
|
|
169
|
+
Each produced value equals start + index * step where index is the zero-based position in the sequence.
|
|
170
|
+
|
|
171
|
+
Parameters:
|
|
172
|
+
start (int): Initial value of the sequence.
|
|
173
|
+
step (int): Increment between successive values.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
LazyAttributeSequence: An attribute sequence that yields integers as described.
|
|
177
|
+
"""
|
|
178
|
+
return LazyAttributeSequence(lambda _instance, index: start + index * step)
|
|
104
179
|
|
|
105
180
|
|
|
106
181
|
def LazyBoolean(trues_ratio: float = 0.5) -> LazyFunction:
|
|
107
|
-
"""
|
|
108
|
-
|
|
182
|
+
"""
|
|
183
|
+
Return booleans where each value is True with the specified probability.
|
|
184
|
+
|
|
185
|
+
Parameters:
|
|
186
|
+
trues_ratio (float): Probability that the generated value is True; expected between 0 and 1.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
bool: `True` with probability `trues_ratio`, `False` otherwise.
|
|
190
|
+
"""
|
|
191
|
+
return LazyFunction(lambda: _RNG.random() < trues_ratio)
|
|
109
192
|
|
|
110
193
|
|
|
111
194
|
def LazyUUID() -> LazyFunction:
|
|
112
|
-
"""
|
|
195
|
+
"""
|
|
196
|
+
Create a lazy factory that yields RFC 4122 version 4 UUID strings.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
uuid_str (str): A UUID4 string in standard 36-character representation.
|
|
200
|
+
"""
|
|
113
201
|
return LazyFunction(lambda: str(uuid.uuid4()))
|
|
114
202
|
|
|
115
203
|
|
|
@@ -12,10 +12,19 @@ __all__ = list(INTERFACE_EXPORTS)
|
|
|
12
12
|
_MODULE_MAP = INTERFACE_EXPORTS
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
|
-
from general_manager._types.interface import * # noqa:
|
|
15
|
+
from general_manager._types.interface import * # noqa: F403
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def __getattr__(name: str) -> Any:
|
|
19
|
+
"""
|
|
20
|
+
Lazily resolve a public API export and return the object for the given attribute name.
|
|
21
|
+
|
|
22
|
+
Parameters:
|
|
23
|
+
name (str): Name of the attribute to resolve from the module's public exports.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Any: The resolved export object associated with `name`.
|
|
27
|
+
"""
|
|
19
28
|
return resolve_export(
|
|
20
29
|
name,
|
|
21
30
|
module_all=__all__,
|