GeneralManager 0.17.0__py3-none-any.whl → 0.19.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 (68) 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/cacheDecorator.py +3 -0
  25. general_manager/cache/dependencyIndex.py +143 -45
  26. general_manager/cache/signals.py +9 -2
  27. general_manager/factory/__init__.py +10 -1
  28. general_manager/factory/autoFactory.py +55 -13
  29. general_manager/factory/factories.py +110 -40
  30. general_manager/factory/factoryMethods.py +122 -34
  31. general_manager/interface/__init__.py +10 -1
  32. general_manager/interface/baseInterface.py +129 -36
  33. general_manager/interface/calculationInterface.py +35 -18
  34. general_manager/interface/databaseBasedInterface.py +71 -45
  35. general_manager/interface/databaseInterface.py +96 -38
  36. general_manager/interface/models.py +5 -5
  37. general_manager/interface/readOnlyInterface.py +94 -20
  38. general_manager/manager/__init__.py +10 -1
  39. general_manager/manager/generalManager.py +25 -16
  40. general_manager/manager/groupManager.py +20 -6
  41. general_manager/manager/meta.py +84 -16
  42. general_manager/measurement/__init__.py +10 -1
  43. general_manager/measurement/measurement.py +289 -95
  44. general_manager/measurement/measurementField.py +42 -31
  45. general_manager/permission/__init__.py +10 -1
  46. general_manager/permission/basePermission.py +120 -38
  47. general_manager/permission/managerBasedPermission.py +72 -21
  48. general_manager/permission/mutationPermission.py +14 -9
  49. general_manager/permission/permissionChecks.py +14 -12
  50. general_manager/permission/permissionDataManager.py +24 -11
  51. general_manager/permission/utils.py +34 -6
  52. general_manager/public_api_registry.py +36 -10
  53. general_manager/rule/__init__.py +10 -1
  54. general_manager/rule/handler.py +133 -44
  55. general_manager/rule/rule.py +178 -39
  56. general_manager/utils/__init__.py +10 -1
  57. general_manager/utils/argsToKwargs.py +34 -9
  58. general_manager/utils/filterParser.py +22 -7
  59. general_manager/utils/formatString.py +1 -0
  60. general_manager/utils/pathMapping.py +23 -15
  61. general_manager/utils/public_api.py +33 -2
  62. general_manager/utils/testing.py +31 -33
  63. {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/METADATA +3 -1
  64. generalmanager-0.19.0.dist-info/RECORD +77 -0
  65. {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/licenses/LICENSE +1 -1
  66. generalmanager-0.17.0.dist-info/RECORD +0 -77
  67. {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/WHEEL +0 -0
  68. {generalmanager-0.17.0.dist-info → generalmanager-0.19.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, TYPE_CHECKING
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 random
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
- ) -> object:
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): Field definition to generate a value for.
79
+ field (models.Field | models.ForeignObjectRel): The Django field or relation to generate a value for.
28
80
 
29
81
  Returns:
30
- object: Value appropriate for the field type.
82
+ object: A value suitable for assignment to the given field (or `None`).
31
83
 
32
84
  Raises:
33
- ValueError: If a related model lacks both a factory and existing instances.
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 random.choice([True] + 9 * [False]):
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
- value = Decimal(random.randrange(0, 10_000_000)) / Decimal("100") # two dp
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: random.choice(related_instances))
184
+ return LazyFunction(lambda: _RNG.choice(related_instances))
112
185
  else:
113
- raise ValueError(
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 = random.choice([True, True, False])
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: random.choice(existing_instances))
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: random.choice(related_instances))
205
+ return LazyFunction(lambda: _RNG.choice(related_instances))
135
206
  else:
136
- raise ValueError(
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
- Return the related model class for the given relational field.
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.Field | models.ForeignObjectRel): Relational field to inspect.
221
+ field (models.ForeignObjectRel | models.Field): Relational field or relation descriptor to inspect.
151
222
 
152
223
  Returns:
153
- type[models.Model]: Related model class.
224
+ type[models.Model]: The related Django model class.
154
225
 
155
226
  Raises:
156
- ValueError: If the field does not declare a related model.
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 ValueError(f"Field {field.name} does not have a related model defined.")
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 TypeError(
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
- Produce sample related instances for a ManyToMany field.
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): Field definition whose values should be generated.
251
+ field (models.ManyToManyField): The ManyToMany field to generate values for.
180
252
 
181
253
  Returns:
182
- list[models.Model]: List of related instances to assign to the field.
254
+ list[models.Model]: A list of related model instances to assign to the field.
183
255
 
184
256
  Raises:
185
- ValueError: If neither factories nor existing instances are available.
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 = random.randint(min_required, 10)
266
+ number_of_instances = _RNG.randint(min_required, 10)
195
267
  if related_factory and related_instances:
196
- number_to_create = random.randint(min_required, number_of_instances)
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 = random.sample(related_instances, number_to_pick)
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 = random.sample(related_instances, number_to_pick)
284
+ existing_instances = _RNG.sample(related_instances, number_to_pick)
213
285
  return existing_instances
214
286
  else:
215
- raise ValueError(
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
- from factory.declarations import LazyFunction, LazyAttribute, LazyAttributeSequence
5
- import random
6
- from general_manager.measurement.measurement import Measurement
7
- from datetime import timedelta, date, datetime
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
- Return a lazy factory producing ``Measurement`` values in the given range.
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): Minimum magnitude.
23
- max_value (int | float): Maximum magnitude.
24
- unit (str): Measurement unit.
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"{random.uniform(min_value, max_value):.6f}", unit)
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
- """Return a lazy attribute that offsets a base date by a random delta.
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 to offset.
36
- base_attribute (str): Name of the attribute providing the base date.
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 obj: (getattr(obj, base_attribute) or date.today())
40
- + timedelta(days=random.randint(avg_delta_days // 2, avg_delta_days * 3 // 2))
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
- """Return a lazy attribute producing dates within the supplied range."""
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
- """Return a lazy attribute producing datetimes within the supplied range."""
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 obj: start + timedelta(seconds=random.randint(0, int(span)))
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
- """Return a lazy factory yielding random integers within the bounds."""
85
- return LazyFunction(lambda: random.randint(min_value, max_value))
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
- """Return a lazy factory yielding Decimal values within the bounds."""
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
- """Return a lazy factory selecting a random element from the options."""
98
- return LazyFunction(lambda: random.choice(options))
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
- """Return a lazy attribute sequence starting at ``start`` with ``step`` increments."""
103
- return LazyAttributeSequence(lambda obj, n: start + n * step)
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
- """Return a lazy factory yielding booleans with the given true ratio."""
108
- return LazyFunction(lambda: random.random() < trues_ratio)
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
- """Return a lazy factory producing UUID4 strings."""
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: F401,F403
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__,