GeneralManager 0.14.0__py3-none-any.whl → 0.15.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 (63) hide show
  1. general_manager/__init__.py +49 -0
  2. general_manager/api/__init__.py +36 -0
  3. general_manager/api/graphql.py +92 -43
  4. general_manager/api/mutation.py +35 -10
  5. general_manager/api/property.py +26 -3
  6. general_manager/apps.py +23 -16
  7. general_manager/bucket/__init__.py +32 -0
  8. general_manager/bucket/baseBucket.py +76 -64
  9. general_manager/bucket/calculationBucket.py +188 -108
  10. general_manager/bucket/databaseBucket.py +130 -49
  11. general_manager/bucket/groupBucket.py +113 -60
  12. general_manager/cache/__init__.py +38 -0
  13. general_manager/cache/cacheDecorator.py +29 -17
  14. general_manager/cache/cacheTracker.py +34 -15
  15. general_manager/cache/dependencyIndex.py +117 -33
  16. general_manager/cache/modelDependencyCollector.py +17 -8
  17. general_manager/cache/signals.py +17 -6
  18. general_manager/factory/__init__.py +34 -5
  19. general_manager/factory/autoFactory.py +57 -60
  20. general_manager/factory/factories.py +39 -14
  21. general_manager/factory/factoryMethods.py +38 -1
  22. general_manager/interface/__init__.py +36 -0
  23. general_manager/interface/baseInterface.py +71 -27
  24. general_manager/interface/calculationInterface.py +18 -10
  25. general_manager/interface/databaseBasedInterface.py +102 -71
  26. general_manager/interface/databaseInterface.py +66 -20
  27. general_manager/interface/models.py +10 -4
  28. general_manager/interface/readOnlyInterface.py +44 -30
  29. general_manager/manager/__init__.py +36 -3
  30. general_manager/manager/generalManager.py +73 -47
  31. general_manager/manager/groupManager.py +72 -17
  32. general_manager/manager/input.py +23 -15
  33. general_manager/manager/meta.py +53 -53
  34. general_manager/measurement/__init__.py +37 -2
  35. general_manager/measurement/measurement.py +135 -58
  36. general_manager/measurement/measurementField.py +161 -61
  37. general_manager/permission/__init__.py +32 -1
  38. general_manager/permission/basePermission.py +29 -12
  39. general_manager/permission/managerBasedPermission.py +32 -26
  40. general_manager/permission/mutationPermission.py +32 -3
  41. general_manager/permission/permissionChecks.py +9 -1
  42. general_manager/permission/permissionDataManager.py +49 -15
  43. general_manager/permission/utils.py +14 -3
  44. general_manager/rule/__init__.py +27 -1
  45. general_manager/rule/handler.py +90 -5
  46. general_manager/rule/rule.py +40 -27
  47. general_manager/utils/__init__.py +44 -2
  48. general_manager/utils/argsToKwargs.py +17 -9
  49. general_manager/utils/filterParser.py +29 -30
  50. general_manager/utils/formatString.py +2 -0
  51. general_manager/utils/jsonEncoder.py +14 -1
  52. general_manager/utils/makeCacheKey.py +18 -12
  53. general_manager/utils/noneToZero.py +8 -6
  54. general_manager/utils/pathMapping.py +92 -29
  55. general_manager/utils/public_api.py +49 -0
  56. general_manager/utils/testing.py +135 -69
  57. {generalmanager-0.14.0.dist-info → generalmanager-0.15.0.dist-info}/METADATA +38 -4
  58. generalmanager-0.15.0.dist-info/RECORD +62 -0
  59. generalmanager-0.15.0.dist-info/licenses/LICENSE +21 -0
  60. generalmanager-0.14.0.dist-info/RECORD +0 -58
  61. generalmanager-0.14.0.dist-info/licenses/LICENSE +0 -29
  62. {generalmanager-0.14.0.dist-info → generalmanager-0.15.0.dist-info}/WHEEL +0 -0
  63. {generalmanager-0.14.0.dist-info → generalmanager-0.15.0.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,5 @@
1
+ """Utilities for parsing filter keyword arguments into structured callables."""
2
+
1
3
  from __future__ import annotations
2
4
  from typing import Any, Callable
3
5
  from general_manager.manager.input import Input
@@ -7,20 +9,21 @@ def parse_filters(
7
9
  filter_kwargs: dict[str, Any], possible_values: dict[str, Input]
8
10
  ) -> dict[str, dict]:
9
11
  """
10
- Parses filter keyword arguments and constructs filter criteria for input fields.
11
-
12
- For each filter key-value pair, determines the target field and lookup type, validates the field, and generates either filter keyword arguments or filter functions depending on the field's type. Returns a dictionary mapping field names to filter criteria, supporting both direct lookups and dynamic filter functions.
13
-
14
- Args:
15
- filter_kwargs: Dictionary of filter keys and their corresponding values.
16
- possible_values: Mapping of field names to Input definitions used for validation and casting.
17
-
12
+ Parse raw filter keyword arguments into structured criteria for the configured input fields.
13
+
14
+ Parameters:
15
+ filter_kwargs (dict[str, Any]): Filter expressions keyed by `<field>[__lookup]` strings.
16
+ possible_values (dict[str, Input]): Input definitions that validate, cast, and describe dependencies for each field.
17
+
18
18
  Returns:
19
- A dictionary where each key is a field name and each value is a dictionary containing either 'filter_kwargs' for direct lookups or 'filter_funcs' for dynamic filtering.
19
+ dict[str, dict[str, Any]]: Mapping of input field names to dictionaries containing either `filter_kwargs` or `filter_funcs` entries used when evaluating filters.
20
+
21
+ Raises:
22
+ ValueError: If a filter references an input field that is not defined in `possible_values`.
20
23
  """
21
24
  from general_manager.manager.generalManager import GeneralManager
22
25
 
23
- filters = {}
26
+ filters: dict[str, dict[str, Any]] = {}
24
27
  for kwarg, value in filter_kwargs.items():
25
28
  parts = kwarg.split("__")
26
29
  field_name = parts[0]
@@ -57,16 +60,14 @@ def parse_filters(
57
60
 
58
61
  def create_filter_function(lookup_str: str, value: Any) -> Callable[[Any], bool]:
59
62
  """
60
- Creates a filter function based on an attribute path and lookup operation.
61
-
62
- The returned function checks whether an object's nested attribute(s) satisfy a specified comparison or matching operation against a given value.
63
-
64
- Args:
65
- lookup_str: Attribute path and lookup operation, separated by double underscores (e.g., "age__gte", "name__contains").
66
- value: The value to compare against.
67
-
63
+ Build a callable that evaluates whether an object's attribute satisfies a lookup expression.
64
+
65
+ Parameters:
66
+ lookup_str (str): Attribute path and lookup operator separated by double underscores (for example, `age__gte`).
67
+ value (Any): Reference value used when applying the lookup comparison.
68
+
68
69
  Returns:
69
- A function that takes an object and returns True if the object's attribute(s) match the filter condition, otherwise False.
70
+ Callable[[Any], bool]: Function returning True when the target object's attribute value passes the lookup test.
70
71
  """
71
72
  parts = lookup_str.split("__") if lookup_str else []
72
73
  if parts and parts[-1] in [
@@ -86,7 +87,7 @@ def create_filter_function(lookup_str: str, value: Any) -> Callable[[Any], bool]
86
87
  lookup = "exact"
87
88
  attr_path = parts
88
89
 
89
- def filter_func(x):
90
+ def filter_func(x: object) -> bool:
90
91
  for attr in attr_path:
91
92
  if hasattr(x, attr):
92
93
  x = getattr(x, attr)
@@ -99,17 +100,15 @@ def create_filter_function(lookup_str: str, value: Any) -> Callable[[Any], bool]
99
100
 
100
101
  def apply_lookup(value_to_check: Any, lookup: str, filter_value: Any) -> bool:
101
102
  """
102
- Evaluates whether a value satisfies a specified lookup condition against a filter value.
103
-
104
- Supports comparison and string operations such as "exact", "lt", "lte", "gt", "gte", "contains", "startswith", "endswith", and "in". Returns False for unsupported lookups or if a TypeError occurs.
105
-
106
- Args:
107
- value_to_check: The value to be compared or checked.
108
- lookup: The lookup operation to perform.
109
- filter_value: The value to compare against.
110
-
103
+ Evaluate a lookup operation against a candidate value.
104
+
105
+ Parameters:
106
+ value_to_check (Any): Value that will be compared using the lookup expression.
107
+ lookup (str): Name of the comparison operation (for example, `exact`, `gte`, or `contains`).
108
+ filter_value (Any): Reference value supplied by the filter expression.
109
+
111
110
  Returns:
112
- True if the lookup condition is satisfied; otherwise, False.
111
+ bool: True if the comparison succeeds; otherwise, False.
113
112
  """
114
113
  try:
115
114
  if lookup == "exact":
@@ -1,3 +1,5 @@
1
+ """Utility helpers for converting between common string casing styles."""
2
+
1
3
  def snake_to_pascal(s: str) -> str:
2
4
  """
3
5
  Convert a snake_case string to PascalCase.
@@ -1,10 +1,23 @@
1
+ """Custom JSON encoder capable of rendering GeneralManager objects."""
2
+
1
3
  from datetime import datetime, date, time
2
4
  import json
3
5
  from general_manager.manager.generalManager import GeneralManager
4
6
 
5
7
 
6
8
  class CustomJSONEncoder(json.JSONEncoder):
7
- def default(self, o):
9
+ """Serialise complex objects that appear within GeneralManager payloads."""
10
+
11
+ def default(self, o: object) -> object:
12
+ """
13
+ Convert unsupported objects into JSON-friendly representations.
14
+
15
+ Parameters:
16
+ o (Any): Object to encode.
17
+
18
+ Returns:
19
+ Any: JSON-serialisable representation of the object.
20
+ """
8
21
 
9
22
  # Serialize datetime objects as ISO strings
10
23
  if isinstance(o, (datetime, date, time)):
@@ -1,26 +1,32 @@
1
+ """Utilities for building deterministic cache keys from function calls."""
2
+
1
3
  import inspect
2
4
  import json
3
- from general_manager.utils.jsonEncoder import CustomJSONEncoder
4
5
  from hashlib import sha256
6
+ from typing import Callable, Mapping
5
7
 
8
+ from general_manager.utils.jsonEncoder import CustomJSONEncoder
6
9
 
7
- def make_cache_key(func, args, kwargs):
8
- """
9
- Generates a unique, deterministic cache key for a specific function call.
10
10
 
11
- The key is derived from the function's module, qualified name, and bound arguments,
12
- serialized to JSON and hashed with SHA-256 to ensure uniqueness for each call signature.
11
+ def make_cache_key(
12
+ func: Callable[..., object],
13
+ args: tuple[object, ...],
14
+ kwargs: Mapping[str, object] | None,
15
+ ) -> str:
16
+ """
17
+ Build a deterministic cache key that uniquely identifies a function invocation.
13
18
 
14
- Args:
15
- func: The target function to be identified.
16
- args: Positional arguments for the function call.
17
- kwargs: Keyword arguments for the function call.
19
+ Parameters:
20
+ func (Callable[..., Any]): The function whose invocation should be cached.
21
+ args (tuple[Any, ...]): Positional arguments supplied to the function.
22
+ kwargs (dict[str, Any]): Keyword arguments supplied to the function.
18
23
 
19
24
  Returns:
20
- A hexadecimal SHA-256 hash string uniquely representing the function call.
25
+ str: Hexadecimal SHA-256 digest representing the call signature.
21
26
  """
22
27
  sig = inspect.signature(func)
23
- bound = sig.bind_partial(*args, **kwargs)
28
+ kwargs_dict = dict(kwargs or {})
29
+ bound = sig.bind_partial(*args, **kwargs_dict)
24
30
  bound.apply_defaults()
25
31
  payload = {
26
32
  "module": func.__module__,
@@ -1,3 +1,5 @@
1
+ """Convenience helpers for normalising optional numeric inputs."""
2
+
1
3
  from typing import Optional, TypeVar, Literal
2
4
  from general_manager.measurement import Measurement
3
5
 
@@ -8,13 +10,13 @@ def noneToZero(
8
10
  value: Optional[NUMBERVALUE],
9
11
  ) -> NUMBERVALUE | Literal[0]:
10
12
  """
11
- Returns zero if the input is None; otherwise, returns the original value.
12
-
13
- Args:
14
- value: An integer, float, or Measurement, or None.
15
-
13
+ Replace None with zero while preserving existing numeric values.
14
+
15
+ Parameters:
16
+ value (Optional[NUMBERVALUE]): Numeric value or Measurement instance that may be None.
17
+
16
18
  Returns:
17
- The original value if not None, otherwise 0.
19
+ NUMBERVALUE | Literal[0]: The input value if it is not None; otherwise, zero.
18
20
  """
19
21
  if value is None:
20
22
  return 0
@@ -1,5 +1,7 @@
1
+ """Utilities for tracing relationships between GeneralManager classes."""
2
+
1
3
  from __future__ import annotations
2
- from typing import TYPE_CHECKING, cast, get_args
4
+ from typing import TYPE_CHECKING, Any, cast, get_args
3
5
  from general_manager.manager.meta import GeneralManagerMeta
4
6
  from general_manager.api.property import GraphQLProperty
5
7
 
@@ -12,22 +14,36 @@ type PathDestination = str
12
14
 
13
15
 
14
16
  class PathMap:
17
+ """Maintain cached traversal paths between GeneralManager classes."""
15
18
 
16
19
  instance: PathMap
17
20
  mapping: dict[tuple[PathStart, PathDestination], PathTracer] = {}
18
21
 
19
- def __new__(cls, *args, **kwargs):
22
+ def __new__(cls, *args: object, **kwargs: object) -> PathMap:
23
+ """
24
+ Create or return the singleton PathMap instance and ensure the path mapping is initialised.
25
+
26
+ Parameters:
27
+ args (tuple): Positional arguments ignored by the constructor.
28
+ kwargs (dict): Keyword arguments ignored by the constructor.
29
+
30
+ Returns:
31
+ PathMap: The singleton PathMap instance.
32
+ """
20
33
  if not hasattr(cls, "instance"):
21
34
  cls.instance = super().__new__(cls)
22
35
  cls.createPathMapping()
23
36
  return cls.instance
24
37
 
25
38
  @classmethod
26
- def createPathMapping(cls):
39
+ def createPathMapping(cls) -> None:
27
40
  """
28
- Builds the mapping of paths between all pairs of distinct managed classes.
41
+ Populate the path mapping with tracers for every distinct pair of managed classes.
29
42
 
30
- Iterates over all registered managed classes and creates a PathTracer for each unique start and destination class pair, storing them in the mapping dictionary.
43
+ The generated tracers capture the attribute sequence needed to navigate from the start class to the destination class and are cached on the singleton instance.
44
+
45
+ Returns:
46
+ None
31
47
  """
32
48
  all_managed_classes = GeneralManagerMeta.all_classes
33
49
  for start_class in all_managed_classes:
@@ -37,19 +53,29 @@ class PathMap:
37
53
  (start_class.__name__, destination_class.__name__)
38
54
  ] = PathTracer(start_class, destination_class)
39
55
 
40
- def __init__(self, path_start: PathStart | GeneralManager | type[GeneralManager]):
56
+ def __init__(
57
+ self,
58
+ path_start: PathStart | GeneralManager | type[GeneralManager],
59
+ ) -> None:
41
60
  """
42
- Initializes a PathMap with a specified starting point.
61
+ Create a new traversal context rooted at the provided manager class or instance.
62
+
63
+ Parameters:
64
+ path_start (PathStart | GeneralManager | type[GeneralManager]): Name, instance, or class that serves as the origin for future path lookups. The value determines both the stored starting instance and the class metadata used for path resolution.
43
65
 
44
- The starting point can be a class name (string), a GeneralManager instance, or a GeneralManager subclass. Sets internal attributes for the start instance, class, and class name based on the input.
66
+ Returns:
67
+ None
45
68
  """
69
+ self.start_instance: GeneralManager | None
70
+ self.start_class: type[GeneralManager] | None
71
+ self.start_class_name: str
46
72
  if isinstance(path_start, GeneralManager):
47
73
  self.start_instance = path_start
48
74
  self.start_class = path_start.__class__
49
75
  self.start_class_name = path_start.__class__.__name__
50
76
  elif isinstance(path_start, type):
51
77
  self.start_instance = None
52
- self.start_class = path_start
78
+ self.start_class = cast(type[GeneralManager], path_start)
53
79
  self.start_class_name = path_start.__name__
54
80
  else:
55
81
  self.start_instance = None
@@ -59,6 +85,15 @@ class PathMap:
59
85
  def to(
60
86
  self, path_destination: PathDestination | type[GeneralManager] | str
61
87
  ) -> PathTracer | None:
88
+ """
89
+ Retrieve the cached path tracer from the start class to the desired destination.
90
+
91
+ Parameters:
92
+ path_destination (PathDestination | type[GeneralManager] | str): Target manager identifier, either as a class, instance name, or class name.
93
+
94
+ Returns:
95
+ PathTracer | None: The tracer describing how to traverse to the destination, or None if no path is known.
96
+ """
62
97
  if isinstance(path_destination, type):
63
98
  path_destination = path_destination.__name__
64
99
 
@@ -70,19 +105,34 @@ class PathMap:
70
105
  def goTo(
71
106
  self, path_destination: PathDestination | type[GeneralManager] | str
72
107
  ) -> GeneralManager | Bucket | None:
108
+ """
109
+ Follow the cached path from the starting point to the requested destination.
110
+
111
+ Parameters:
112
+ path_destination (PathDestination | type[GeneralManager] | str): Target manager identifier, either as a class, instance, or class name.
113
+
114
+ Returns:
115
+ GeneralManager | Bucket | None: The resolved manager instance, a bucket of instances, or None when no path exists.
116
+
117
+ Raises:
118
+ ValueError: If no starting instance was supplied when constructing the PathMap.
119
+ """
73
120
  if isinstance(path_destination, type):
74
121
  path_destination = path_destination.__name__
75
122
 
76
123
  tracer = self.mapping.get((self.start_class_name, path_destination), None)
77
124
  if not tracer:
78
125
  return None
79
- if not self.start_instance:
126
+ if self.start_instance is None:
80
127
  raise ValueError("Cannot call goTo on a PathMap without a start instance.")
81
128
  return tracer.traversePath(self.start_instance)
82
129
 
83
130
  def getAllConnected(self) -> set[str]:
84
131
  """
85
- Returns a list of all classes that are connected to the start class.
132
+ List the class names that are reachable from the configured starting point.
133
+
134
+ Returns:
135
+ set[str]: Collection of destination class names that have a valid traversal path.
86
136
  """
87
137
  connected_classes: set[str] = set()
88
138
  for path_tuple, path_obj in self.mapping.items():
@@ -95,13 +145,25 @@ class PathMap:
95
145
 
96
146
 
97
147
  class PathTracer:
148
+ """Resolve attribute paths linking one manager class to another."""
149
+
98
150
  def __init__(
99
151
  self, start_class: type[GeneralManager], destination_class: type[GeneralManager]
100
- ):
152
+ ) -> None:
153
+ """
154
+ Initialise a path tracer between two manager classes.
155
+
156
+ Parameters:
157
+ start_class (type[GeneralManager]): Origin manager class where traversal begins.
158
+ destination_class (type[GeneralManager]): Target manager class to reach.
159
+
160
+ Returns:
161
+ None
162
+ """
101
163
  self.start_class = start_class
102
164
  self.destination_class = destination_class
103
165
  if self.start_class == self.destination_class:
104
- self.path = []
166
+ self.path: list[str] | None = []
105
167
  else:
106
168
  self.path = self.createPath(start_class, [])
107
169
 
@@ -109,14 +171,14 @@ class PathTracer:
109
171
  self, current_manager: type[GeneralManager], path: list[str]
110
172
  ) -> list[str] | None:
111
173
  """
112
- Recursively constructs a path of attribute names from the current manager class to the destination class.
174
+ Recursively compute the traversal path from `current_manager` to the destination class.
113
175
 
114
- Args:
115
- current_manager: The current GeneralManager subclass being inspected.
116
- path: The list of attribute names traversed so far.
176
+ Parameters:
177
+ current_manager (type[GeneralManager]): Manager class used as the current traversal node.
178
+ path (list[str]): Sequence of attribute names accumulated along the traversal.
117
179
 
118
180
  Returns:
119
- A list of attribute names representing the path to the destination class, or None if no path exists.
181
+ list[str] | None: Updated list of attribute names leading to the destination, or None if no route exists.
120
182
  """
121
183
  current_connections = {
122
184
  attr_name: attr_value["type"]
@@ -135,7 +197,7 @@ class PathTracer:
135
197
  for attr, attr_type in current_connections.items():
136
198
  if attr in path or attr_type == self.start_class:
137
199
  continue
138
- if attr_type is None:
200
+ if attr_type is None or not isinstance(attr_type, type):
139
201
  continue
140
202
  if not issubclass(attr_type, GeneralManager):
141
203
  continue
@@ -151,27 +213,28 @@ class PathTracer:
151
213
  self, start_instance: GeneralManager | Bucket
152
214
  ) -> GeneralManager | Bucket | None:
153
215
  """
154
- Traverses the stored path from a starting instance to reach the destination instance or bucket.
216
+ Traverse the stored path starting from the provided manager or bucket instance.
155
217
 
156
- Args:
157
- start_instance: The initial GeneralManager or Bucket instance from which to begin traversal.
218
+ Parameters:
219
+ start_instance (GeneralManager | Bucket): Object used as the traversal root.
158
220
 
159
221
  Returns:
160
- The resulting GeneralManager or Bucket instance at the end of the path, or None if the path is empty.
222
+ GeneralManager | Bucket | None: The resolved destination object, a merged bucket, or None when no traversal is required.
161
223
  """
162
- current_instance = start_instance
224
+ current_instance: Any = start_instance
163
225
  if not self.path:
164
226
  return None
165
227
  for attr in self.path:
166
228
  if not isinstance(current_instance, Bucket):
167
229
  current_instance = getattr(current_instance, attr)
168
230
  continue
169
- new_instance = None
231
+ new_instance: Any = None
170
232
  for entry in current_instance:
171
- if not new_instance:
172
- new_instance = getattr(entry, attr)
233
+ attr_value = getattr(entry, attr)
234
+ if new_instance is None:
235
+ new_instance = attr_value
173
236
  else:
174
- new_instance = new_instance | getattr(entry, attr)
237
+ new_instance = new_instance | attr_value # type: ignore[operator]
175
238
  current_instance = new_instance
176
239
 
177
- return current_instance
240
+ return cast(GeneralManager | Bucket[Any] | None, current_instance)
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ """Utility helpers for building lazy-loading public package APIs."""
4
+
5
+ from importlib import import_module
6
+ from typing import Any, Iterable, Mapping, MutableMapping, overload
7
+
8
+ ModuleTarget = tuple[str, str]
9
+ ModuleMap = Mapping[str, str | ModuleTarget]
10
+
11
+
12
+ @overload
13
+ def _normalize_target(name: str, target: str) -> ModuleTarget: ...
14
+
15
+
16
+ @overload
17
+ def _normalize_target(name: str, target: ModuleTarget) -> ModuleTarget: ...
18
+
19
+
20
+ def _normalize_target(name: str, target: str | ModuleTarget) -> ModuleTarget:
21
+ if isinstance(target, tuple):
22
+ return target
23
+ return target, name
24
+
25
+
26
+ def resolve_export(
27
+ name: str,
28
+ *,
29
+ module_all: Iterable[str],
30
+ module_map: ModuleMap,
31
+ module_globals: MutableMapping[str, Any],
32
+ ) -> Any:
33
+ """Resolve a lazily-loaded export for a package __init__ module."""
34
+ if name not in module_all:
35
+ raise AttributeError(f"module {module_globals['__name__']!r} has no attribute {name!r}")
36
+ module_path, attr_name = _normalize_target(name, module_map[name])
37
+ module = import_module(module_path)
38
+ value = getattr(module, attr_name)
39
+ module_globals[name] = value
40
+ return value
41
+
42
+
43
+ def build_module_dir(
44
+ *,
45
+ module_all: Iterable[str],
46
+ module_globals: MutableMapping[str, Any],
47
+ ) -> list[str]:
48
+ """Return a sorted directory listing for a package __init__ module."""
49
+ return sorted(list(module_globals.keys()) + list(module_all))