GeneralManager 0.10.3__py3-none-any.whl → 0.10.5__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.
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
- from typing import Any, cast
2
+ from typing import Any, cast, TYPE_CHECKING
3
3
  from factory.declarations import LazyFunction
4
4
  from factory.faker import Faker
5
- import exrex # type: ignore
5
+ import exrex
6
6
  from django.db import models
7
7
  from django.core.validators import RegexValidator
8
8
  import random
@@ -11,12 +11,17 @@ from general_manager.measurement.measurement import Measurement
11
11
  from general_manager.measurement.measurementField import MeasurementField
12
12
  from datetime import date, datetime, time, timezone
13
13
 
14
+ if TYPE_CHECKING:
15
+ from general_manager.factory.autoFactory import AutoFactory
14
16
 
15
- def getFieldValue(field: models.Field[Any, Any] | models.ForeignObjectRel) -> object:
17
+
18
+ def getFieldValue(
19
+ field: models.Field[Any, Any] | models.ForeignObjectRel,
20
+ ) -> object:
16
21
  """
17
- Generates an appropriate value for a given Django model field for use in testing or data factories.
22
+ Generate a suitable fake or factory value for a given Django model field, for use in tests or data factories.
18
23
 
19
- If the field allows null values, there is a 10% chance of returning None. Handles a wide range of Django field types, including measurement, text, numeric, date/time, boolean, relational, and specialized fields, returning a suitable fake or factory-generated value for each. For relational fields (OneToOneField and ForeignKey), attempts to use a factory if available or selects a random existing instance; raises ValueError if neither is possible. Returns None for unsupported field types.
24
+ Returns a value appropriate for the field type, including support for measurement, text, numeric, date/time, boolean, email, URL, IP address, UUID, duration, and character fields (with regex support). For relational fields (OneToOneField and ForeignKey), attempts to use a related model factory or select a random existing instance; raises ValueError if neither is available. Returns None for unsupported field types or with a 10% chance if the field allows null values.
20
25
  """
21
26
  if field.null:
22
27
  if random.choice([True] + 9 * [False]):
@@ -62,78 +67,103 @@ def getFieldValue(field: models.Field[Any, Any] | models.ForeignObjectRel) -> ob
62
67
  return cast(date, Faker("date_between", start_date="-1y", end_date="today"))
63
68
  elif isinstance(field, models.BooleanField):
64
69
  return cast(bool, Faker("pybool"))
70
+ elif isinstance(field, models.EmailField):
71
+ return cast(str, Faker("email"))
72
+ elif isinstance(field, models.URLField):
73
+ return cast(str, Faker("url"))
74
+ elif isinstance(field, models.GenericIPAddressField):
75
+ return cast(str, Faker("ipv4"))
76
+ elif isinstance(field, models.UUIDField):
77
+ return cast(str, Faker("uuid4"))
78
+ elif isinstance(field, models.DurationField):
79
+ return cast(time, Faker("time_delta"))
80
+ elif isinstance(field, models.CharField):
81
+ max_length = field.max_length or 100
82
+ # Check for RegexValidator
83
+ regex = None
84
+ for validator in field.validators:
85
+ if isinstance(validator, RegexValidator):
86
+ regex = getattr(validator.regex, "pattern", None)
87
+ break
88
+ if regex:
89
+ # Use exrex to generate a string matching the regex
90
+ return LazyFunction(lambda: exrex.getone(regex))
91
+ else:
92
+ return cast(str, Faker("text", max_nb_chars=max_length))
65
93
  elif isinstance(field, models.OneToOneField):
66
- if hasattr(field.related_model, "_general_manager_class"):
67
- related_factory = field.related_model._general_manager_class.Factory
94
+ related_model = getRelatedModel(field)
95
+ if hasattr(related_model, "_general_manager_class"):
96
+ related_factory = related_model._general_manager_class.Factory # type: ignore
68
97
  return related_factory()
69
98
  else:
70
99
  # If no factory exists, pick a random existing instance
71
- related_instances = list(field.related_model.objects.all())
100
+ related_instances = list(related_model.objects.all())
72
101
  if related_instances:
73
102
  return LazyFunction(lambda: random.choice(related_instances))
74
103
  else:
75
104
  raise ValueError(
76
- f"No factory found for {field.related_model.__name__} and no instances found"
105
+ f"No factory found for {related_model.__name__} and no instances found"
77
106
  )
78
107
  elif isinstance(field, models.ForeignKey):
108
+ related_model = getRelatedModel(field)
79
109
  # Create or get an instance of the related model
80
- if hasattr(field.related_model, "_general_manager_class"):
110
+ if hasattr(related_model, "_general_manager_class"):
81
111
  create_a_new_instance = random.choice([True, True, False])
82
112
  if not create_a_new_instance:
83
- existing_instances = list(field.related_model.objects.all())
113
+ existing_instances = list(related_model.objects.all())
84
114
  if existing_instances:
85
115
  # Pick a random existing instance
86
116
  return LazyFunction(lambda: random.choice(existing_instances))
87
117
 
88
- related_factory = field.related_model._general_manager_class.Factory
118
+ related_factory = related_model._general_manager_class.Factory # type: ignore
89
119
  return related_factory()
90
120
 
91
121
  else:
92
122
  # If no factory exists, pick a random existing instance
93
- related_instances = list(field.related_model.objects.all())
123
+ related_instances = list(related_model.objects.all())
94
124
  if related_instances:
95
125
  return LazyFunction(lambda: random.choice(related_instances))
96
126
  else:
97
127
  raise ValueError(
98
- f"No factory found for {field.related_model.__name__} and no instances found"
128
+ f"No factory found for {related_model.__name__} and no instances found"
99
129
  )
100
- elif isinstance(field, models.EmailField):
101
- return cast(str, Faker("email"))
102
- elif isinstance(field, models.URLField):
103
- return cast(str, Faker("url"))
104
- elif isinstance(field, models.GenericIPAddressField):
105
- return cast(str, Faker("ipv4"))
106
- elif isinstance(field, models.UUIDField):
107
- return cast(str, Faker("uuid4"))
108
- elif isinstance(field, models.DurationField):
109
- return cast(time, Faker("time_delta"))
110
- elif isinstance(field, models.CharField):
111
- max_length = field.max_length or 100
112
- # Check for RegexValidator
113
- regex = None
114
- for validator in field.validators:
115
- if isinstance(validator, RegexValidator):
116
- regex = getattr(validator.regex, "pattern", None)
117
- break
118
- if regex:
119
- # Use exrex to generate a string matching the regex
120
- return LazyFunction(lambda: exrex.getone(regex)) # type: ignore
121
- else:
122
- return cast(str, Faker("text", max_nb_chars=max_length))
123
130
  else:
124
- return None # For unsupported field types
131
+ return None
132
+
133
+
134
+ def getRelatedModel(
135
+ field: models.ForeignObjectRel | models.Field[Any, Any],
136
+ ) -> type[models.Model]:
137
+ """
138
+ Return the related model class for a given Django relational field.
139
+
140
+ Raises:
141
+ ValueError: If the field does not have a related model defined.
142
+ """
143
+ related_model = field.related_model
144
+ if related_model is None:
145
+ raise ValueError(f"Field {field.name} does not have a related model defined.")
146
+ if related_model == "self":
147
+ related_model = field.model
148
+ return related_model # For unsupported field types
125
149
 
126
150
 
127
151
  def getManyToManyFieldValue(
128
152
  field: models.ManyToManyField,
129
153
  ) -> list[models.Model]:
130
154
  """
131
- Returns a list of instances for a ManyToMany field.
155
+ Generate a list of model instances to associate with a ManyToMany field for testing or factory purposes.
156
+
157
+ If a related model factory is available, creates new instances as needed. Otherwise, selects from existing instances. Raises a ValueError if neither a factory nor existing instances are available.
158
+
159
+ Returns:
160
+ list[models.Model]: A list of related model instances for the ManyToMany field.
132
161
  """
133
162
  related_factory = None
134
- related_instances = list(field.related_model.objects.all())
135
- if hasattr(field.related_model, "_general_manager_class"):
136
- related_factory = field.related_model._general_manager_class.Factory
163
+ related_model = getRelatedModel(field)
164
+ related_instances = list(related_model.objects.all())
165
+ if hasattr(related_model, "_general_manager_class"):
166
+ related_factory = related_model._general_manager_class.Factory # type: ignore
137
167
 
138
168
  min_required = 0 if field.blank else 1
139
169
  number_of_instances = random.randint(min_required, 10)
@@ -158,5 +188,5 @@ def getManyToManyFieldValue(
158
188
  return existing_instances
159
189
  else:
160
190
  raise ValueError(
161
- f"No factory found for {field.related_model.__name__} and no instances found"
191
+ f"No factory found for {related_model.__name__} and no instances found"
162
192
  )
@@ -69,12 +69,12 @@ class InterfaceBase(ABC):
69
69
  **kwargs: dict[str, Any],
70
70
  ) -> dict[str, Any]:
71
71
  """
72
- Parses and validates input arguments into a structured identification dictionary.
72
+ Parse and validate input arguments into a dictionary of input field values.
73
73
 
74
- Converts positional and keyword arguments into a dictionary keyed by input field names, normalizing argument names and ensuring all required fields are present. Processes input fields in dependency order, casting and validating each value. Raises a `TypeError` for unexpected or missing arguments and a `ValueError` if circular dependencies among input fields are detected.
74
+ Positional and keyword arguments are mapped to input field names, with normalization of argument names (e.g., removing trailing `_id`). Ensures all required fields are present and no unexpected arguments are provided. Processes input fields in dependency order, casting and validating each value. Raises a `TypeError` for missing or unexpected arguments and a `ValueError` if circular dependencies are detected.
75
75
 
76
76
  Returns:
77
- A dictionary mapping input field names to their validated and cast values.
77
+ dict[str, Any]: A dictionary mapping input field names to their validated and cast values.
78
78
  """
79
79
  identification = {}
80
80
  kwargs = args_to_kwargs(args, self.input_fields.keys(), kwargs)
@@ -26,12 +26,18 @@ class CalculationInterface(InterfaceBase):
26
26
 
27
27
  @classmethod
28
28
  def getAttributeTypes(cls) -> dict[str, AttributeTypedDict]:
29
+ """
30
+ Return a dictionary describing the type and metadata for each input field in the calculation interface.
31
+
32
+ Each entry includes the field's type, default value (`None`), and flags indicating that the field is not editable, is required, and is not derived.
33
+ """
29
34
  return {
30
35
  name: {
31
36
  "type": field.type,
32
37
  "default": None,
33
38
  "is_editable": False,
34
39
  "is_required": True,
40
+ "is_derived": False,
35
41
  }
36
42
  for name, field in cls.input_fields.items()
37
43
  }
@@ -61,14 +67,13 @@ class CalculationInterface(InterfaceBase):
61
67
  def _preCreate(
62
68
  name: generalManagerClassName, attrs: attributes, interface: interfaceBaseClass
63
69
  ) -> tuple[attributes, interfaceBaseClass, None]:
64
-
65
70
  """
66
- Prepares attributes and a new interface class before creating a GeneralManager class.
67
-
68
- Collects all `Input` instances from the provided interface class, sets the interface type in the attributes, dynamically creates a new interface class with these input fields, and adds it to the attributes.
69
-
71
+ Prepare and return updated attributes and a new interface class for GeneralManager creation.
72
+
73
+ Collects all `Input` instances from the provided interface class, sets the interface type in the attributes, dynamically creates a new interface class with an `input_fields` attribute, and adds this class to the attributes dictionary.
74
+
70
75
  Returns:
71
- A tuple containing the updated attributes dictionary, the new interface class, and None.
76
+ tuple: A tuple containing the updated attributes dictionary, the new interface class, and None.
72
77
  """
73
78
  input_fields: dict[str, Input[Any]] = {}
74
79
  for key, value in vars(interface).items():
@@ -109,12 +114,18 @@ class CalculationInterface(InterfaceBase):
109
114
  @classmethod
110
115
  def getFieldType(cls, field_name: str) -> type:
111
116
  """
112
- Returns the Python type of a specified input field.
113
-
117
+ Return the Python type of the specified input field.
118
+
119
+ Parameters:
120
+ field_name (str): The name of the input field.
121
+
122
+ Returns:
123
+ type: The Python type associated with the input field.
124
+
114
125
  Raises:
115
- KeyError: If the field name does not exist in input_fields.
126
+ KeyError: If the specified field name does not exist in input_fields.
116
127
  """
117
- input = cls.input_fields.get(field_name)
118
- if input is None:
128
+ field = cls.input_fields.get(field_name)
129
+ if field is None:
119
130
  raise KeyError(f"Field '{field_name}' not found in input fields.")
120
- return input.type
131
+ return field.type
@@ -1,5 +1,5 @@
1
1
  from __future__ import annotations
2
- from typing import Type, Any, Callable, TYPE_CHECKING, TypeVar, Generic
2
+ from typing import Type, Any, Callable, TYPE_CHECKING, TypeVar, Generic, cast
3
3
  from django.db import models
4
4
  from datetime import datetime, timedelta
5
5
  from general_manager.measurement.measurement import Measurement
@@ -25,13 +25,13 @@ from general_manager.interface.models import (
25
25
  GeneralManagerModel,
26
26
  getFullCleanMethode,
27
27
  )
28
+ from django.contrib.contenttypes.fields import GenericForeignKey
28
29
 
29
30
  if TYPE_CHECKING:
30
31
  from general_manager.rule.rule import Rule
31
32
 
32
33
  modelsModel = TypeVar("modelsModel", bound=models.Model)
33
34
 
34
-
35
35
  MODEL_TYPE = TypeVar("MODEL_TYPE", bound=GeneralManagerBasisModel)
36
36
 
37
37
 
@@ -43,12 +43,12 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
43
43
  self,
44
44
  *args: list[Any],
45
45
  search_date: datetime | None = None,
46
- **kwargs: dict[str, Any],
46
+ **kwargs: Any,
47
47
  ):
48
48
  """
49
- Initialize the interface and load the associated model instance.
50
-
51
- If `search_date` is provided, loads the historical record as of that date; otherwise, loads the current record.
49
+ Initialize the interface and load the associated model instance by primary key.
50
+
51
+ If a `search_date` is provided, retrieves the historical record as of that date; otherwise, loads the current record.
52
52
  """
53
53
  super().__init__(*args, **kwargs)
54
54
  self.pk = self.identification["id"]
@@ -135,10 +135,10 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
135
135
  @classmethod
136
136
  def getAttributeTypes(cls) -> dict[str, AttributeTypedDict]:
137
137
  """
138
- Return a dictionary mapping attribute names to metadata describing their types and properties.
139
-
140
- The dictionary includes all model fields, custom fields, foreign keys, many-to-many, and reverse relation fields. For each attribute, the metadata specifies its Python type (translated from Django field types when possible), whether it is required, editable, derived, and its default value. For related models with a general manager class, the type is set to that class.
141
-
138
+ Return a dictionary mapping each attribute name of the model to its type information and metadata.
139
+
140
+ The returned dictionary includes all standard model fields, custom fields, foreign keys, many-to-many, and reverse relation fields, excluding any GenericForeignKey fields. For each attribute, the metadata specifies its Python type (translated from Django field types when possible), whether it is required, editable, derived, and its default value. For related models with a general manager class, the type is set to that class.
141
+
142
142
  Returns:
143
143
  dict[str, AttributeTypedDict]: Mapping of attribute names to their type information and metadata.
144
144
  """
@@ -163,7 +163,7 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
163
163
  fields: dict[str, AttributeTypedDict] = {}
164
164
  field_name_list, to_ignore_list = cls.handleCustomFields(cls._model)
165
165
  for field_name in field_name_list:
166
- field: models.Field = getattr(cls._model, field_name)
166
+ field = cast(models.Field, getattr(cls._model, field_name))
167
167
  fields[field_name] = {
168
168
  "type": type(field),
169
169
  "is_derived": False,
@@ -174,7 +174,7 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
174
174
 
175
175
  for field_name in cls.__getModelFields():
176
176
  if field_name not in to_ignore_list:
177
- field: models.Field = getattr(cls._model, field_name).field
177
+ field = cast(models.Field, getattr(cls._model, field_name).field)
178
178
  fields[field_name] = {
179
179
  "type": type(field),
180
180
  "is_derived": False,
@@ -185,20 +185,27 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
185
185
 
186
186
  for field_name in cls.__getForeignKeyFields():
187
187
  field = cls._model._meta.get_field(field_name)
188
+ if isinstance(field, GenericForeignKey):
189
+ continue
188
190
  related_model = field.related_model
191
+ if related_model == "self":
192
+ related_model = cls._model
189
193
  if related_model and hasattr(
190
194
  related_model,
191
195
  "_general_manager_class",
192
196
  ):
193
- related_model = related_model._general_manager_class
197
+ related_model = related_model._general_manager_class # type: ignore
194
198
 
195
199
  elif related_model is not None:
200
+ default = None
201
+ if hasattr(field, "default"):
202
+ default = field.default # type: ignore
196
203
  fields[field_name] = {
197
204
  "type": related_model,
198
205
  "is_derived": False,
199
206
  "is_required": not field.null,
200
207
  "is_editable": field.editable,
201
- "default": field.default,
208
+ "default": default,
202
209
  }
203
210
 
204
211
  for field_name, field_call in [
@@ -212,11 +219,17 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
212
219
  raise ValueError("Field name already exists.")
213
220
  field = cls._model._meta.get_field(field_name)
214
221
  related_model = cls._model._meta.get_field(field_name).related_model
222
+ if related_model == "self":
223
+ related_model = cls._model
224
+ if isinstance(field, GenericForeignKey):
225
+ continue
226
+
215
227
  if related_model and hasattr(
216
228
  related_model,
217
229
  "_general_manager_class",
218
230
  ):
219
- related_model = related_model._general_manager_class
231
+ related_model = related_model._general_manager_class # type: ignore
232
+
220
233
  if related_model is not None:
221
234
  fields[f"{field_name}_list"] = {
222
235
  "type": related_model,
@@ -234,9 +247,12 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
234
247
  @classmethod
235
248
  def getAttributes(cls) -> dict[str, Callable[[DBBasedInterface], Any]]:
236
249
  """
237
- Returns a mapping of attribute names to callables that retrieve their values from a DBBasedInterface instance.
238
-
239
- The returned dictionary includes accessors for custom fields, model fields, foreign keys (optionally returning related interface instances), many-to-many relations, and reverse relations. For related models that have a general manager class, the accessor returns an instance of that class; otherwise, it returns the related object or queryset directly. Raises a ValueError if a field name conflict occurs.
250
+ Return a dictionary mapping attribute names to callables that retrieve values from a DBBasedInterface instance.
251
+
252
+ The mapping includes accessors for custom fields, standard model fields, foreign keys, many-to-many relations, and reverse relations. For related models with a general manager class, the accessor returns an instance of that class; otherwise, it returns the related object or queryset. Raises a ValueError if a field name conflict is detected.
253
+
254
+ Returns:
255
+ dict: A dictionary where keys are attribute names and values are callables that extract the corresponding value from a DBBasedInterface instance.
240
256
  """
241
257
  field_values: dict[str, Any] = {}
242
258
 
@@ -258,7 +274,7 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
258
274
  related_model,
259
275
  "_general_manager_class",
260
276
  ):
261
- generalManagerClass = related_model._general_manager_class
277
+ generalManagerClass = related_model._general_manager_class # type: ignore
262
278
  field_values[f"{field_name}"] = (
263
279
  lambda self, field_name=field_name, manager_class=generalManagerClass: manager_class(
264
280
  getattr(self._instance, field_name).pk
@@ -322,13 +338,13 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
322
338
  @staticmethod
323
339
  def _getCustomFields(model: Type[models.Model] | models.Model) -> list[str]:
324
340
  """
325
- Returns a list of custom field names defined directly on the model class.
326
-
327
- Args:
341
+ Return a list of custom field names defined directly as class attributes on the given Django model.
342
+
343
+ Parameters:
328
344
  model: The Django model class or instance to inspect.
329
-
345
+
330
346
  Returns:
331
- A list of field names that are defined as class attributes on the model, not via Django's meta system.
347
+ A list of field names for fields declared directly on the model class, excluding those defined via Django's meta system.
332
348
  """
333
349
  return [
334
350
  field.name
@@ -337,11 +353,11 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
337
353
  ]
338
354
 
339
355
  @classmethod
340
- def __getModelFields(cls):
356
+ def __getModelFields(cls) -> list[str]:
341
357
  """
342
- Returns a list of model field names that are not many-to-many or related fields.
343
-
344
- Excludes fields representing many-to-many relationships and related models.
358
+ Return a list of field names for the model that are neither many-to-many nor related fields.
359
+
360
+ Fields representing many-to-many relationships or relations to other models are excluded from the result.
345
361
  """
346
362
  return [
347
363
  field.name
@@ -350,9 +366,9 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
350
366
  ]
351
367
 
352
368
  @classmethod
353
- def __getForeignKeyFields(cls):
369
+ def __getForeignKeyFields(cls) -> list[str]:
354
370
  """
355
- Returns a list of field names for all foreign key and one-to-one relations on the model.
371
+ Return a list of field names for all foreign key and one-to-one relations on the model, excluding generic foreign keys.
356
372
  """
357
373
  return [
358
374
  field.name
@@ -361,11 +377,11 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
361
377
  ]
362
378
 
363
379
  @classmethod
364
- def __getManyToManyFields(cls):
380
+ def __getManyToManyFields(cls) -> list[tuple[str, str]]:
365
381
  """
366
- Returns a list of tuples containing the names of all many-to-many fields on the model.
367
-
368
- Each tuple consists of the field name repeated twice.
382
+ Return a list of tuples representing all many-to-many fields on the model.
383
+
384
+ Each tuple contains the field name twice. Fields that are generic foreign keys are excluded.
369
385
  """
370
386
  return [
371
387
  (field.name, field.name)
@@ -374,11 +390,11 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
374
390
  ]
375
391
 
376
392
  @classmethod
377
- def __getReverseRelations(cls):
393
+ def __getReverseRelations(cls) -> list[tuple[str, str]]:
378
394
  """
379
- Returns a list of tuples representing reverse one-to-many relations for the model.
380
-
381
- Each tuple contains the related field's name and its default related accessor name.
395
+ Return a list of reverse one-to-many relations for the model, excluding generic foreign keys.
396
+
397
+ Each tuple contains the related field's name and its default related accessor name (e.g., `fieldname_set`).
382
398
  """
383
399
  return [
384
400
  (field.name, f"{field.name}_set")
@@ -485,11 +501,15 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
485
501
  @classmethod
486
502
  def getFieldType(cls, field_name: str) -> type:
487
503
  """
488
- Returns the type associated with the specified field name.
489
-
490
- If the field is a relation and its related model defines a `_general_manager_class`, that class is returned; otherwise, returns the Django field type.
504
+ Return the type associated with a given model field name.
505
+
506
+ If the field is a relation and its related model has a `_general_manager_class` attribute, that class is returned; otherwise, returns the Django field type.
491
507
  """
492
508
  field = cls._model._meta.get_field(field_name)
493
- if field.is_relation and field.related_model:
494
- return field.related_model._general_manager_class
509
+ if (
510
+ field.is_relation
511
+ and field.related_model
512
+ and hasattr(field.related_model, "_general_manager_class")
513
+ ):
514
+ return field.related_model._general_manager_class # type: ignore
495
515
  return type(field)