GeneralManager 0.16.1__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 +897 -147
- general_manager/api/graphql_subscription_consumer.py +432 -0
- general_manager/api/mutation.py +85 -23
- general_manager/api/property.py +39 -13
- general_manager/apps.py +336 -40
- 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 +303 -53
- 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 +21 -7
- 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 +49 -42
- {generalmanager-0.16.1.dist-info → generalmanager-0.18.0.dist-info}/METADATA +3 -1
- generalmanager-0.18.0.dist-info/RECORD +77 -0
- {generalmanager-0.16.1.dist-info → generalmanager-0.18.0.dist-info}/licenses/LICENSE +1 -1
- generalmanager-0.16.1.dist-info/RECORD +0 -76
- {generalmanager-0.16.1.dist-info → generalmanager-0.18.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.16.1.dist-info → generalmanager-0.18.0.dist-info}/top_level.txt +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
"""Database-backed bucket implementation for GeneralManager collections."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Generator, Type, TypeVar
|
|
5
|
+
|
|
6
|
+
from django.core.exceptions import FieldError
|
|
5
7
|
from django.db import models
|
|
6
|
-
from general_manager.interface.baseInterface import (
|
|
7
|
-
GeneralManagerType,
|
|
8
|
-
)
|
|
9
|
-
from general_manager.utils.filterParser import create_filter_function
|
|
10
|
-
from general_manager.bucket.baseBucket import Bucket
|
|
11
8
|
|
|
9
|
+
from general_manager.bucket.baseBucket import Bucket
|
|
10
|
+
from general_manager.interface.baseInterface import GeneralManagerType
|
|
12
11
|
from general_manager.manager.generalManager import GeneralManager
|
|
12
|
+
from general_manager.utils.filterParser import create_filter_function
|
|
13
13
|
|
|
14
14
|
modelsModel = TypeVar("modelsModel", bound=models.Model)
|
|
15
15
|
|
|
@@ -17,6 +17,108 @@ if TYPE_CHECKING:
|
|
|
17
17
|
from general_manager.interface.databaseInterface import DatabaseInterface
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class DatabaseBucketTypeMismatchError(TypeError):
|
|
21
|
+
"""Raised when attempting to combine buckets of different types."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, bucket_type: type, other_type: type) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Initialize the error for attempting to combine two incompatible bucket types.
|
|
26
|
+
|
|
27
|
+
Parameters:
|
|
28
|
+
bucket_type (type): The bucket type used in the operation.
|
|
29
|
+
other_type (type): The other bucket type that is incompatible with `bucket_type`.
|
|
30
|
+
"""
|
|
31
|
+
super().__init__(
|
|
32
|
+
f"Cannot combine {bucket_type.__name__} with {other_type.__name__}."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DatabaseBucketManagerMismatchError(TypeError):
|
|
37
|
+
"""Raised when combining buckets backed by different manager classes."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, first_manager: type, second_manager: type) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Raised when attempting to combine buckets that are backed by different manager classes.
|
|
42
|
+
|
|
43
|
+
Parameters:
|
|
44
|
+
first_manager (type): The first manager class involved in the attempted combination.
|
|
45
|
+
second_manager (type): The second manager class involved in the attempted combination.
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(
|
|
48
|
+
f"Cannot combine buckets for {first_manager.__name__} and {second_manager.__name__}."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class NonFilterablePropertyError(ValueError):
|
|
53
|
+
"""Raised when attempting to filter on a property without filter support."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, property_name: str, manager_name: str) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Raised when a filter is requested for a GraphQL property that is not marked as filterable on the given manager.
|
|
58
|
+
|
|
59
|
+
Parameters:
|
|
60
|
+
property_name (str): The GraphQL property name that was used for filtering.
|
|
61
|
+
manager_name (str): The name of the manager (or manager class) where the property is not filterable.
|
|
62
|
+
"""
|
|
63
|
+
super().__init__(
|
|
64
|
+
f"Property '{property_name}' is not filterable in {manager_name}."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class InvalidQueryAnnotationTypeError(TypeError):
|
|
69
|
+
"""Raised when a query annotation callback returns a non-queryset value."""
|
|
70
|
+
|
|
71
|
+
def __init__(self) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Exception raised when a query annotation callback returns a non-QuerySet.
|
|
74
|
+
|
|
75
|
+
The exception carries a standardized message: "Query annotation must return a Django QuerySet."
|
|
76
|
+
"""
|
|
77
|
+
super().__init__("Query annotation must return a Django QuerySet.")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class QuerysetFilteringError(ValueError):
|
|
81
|
+
"""Raised when applying ORM filters fails."""
|
|
82
|
+
|
|
83
|
+
def __init__(self, original: Exception) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Initialize a QuerysetFilteringError that wraps an original exception raised during ORM filtering.
|
|
86
|
+
|
|
87
|
+
Parameters:
|
|
88
|
+
original (Exception): The original exception encountered while filtering the queryset; its message is included in this error's message.
|
|
89
|
+
"""
|
|
90
|
+
super().__init__(f"Error filtering queryset: {original}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class QuerysetOrderingError(ValueError):
|
|
94
|
+
"""Raised when applying ORM ordering fails."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, original: Exception) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Initialize the QuerysetOrderingError by wrapping the originating exception.
|
|
99
|
+
|
|
100
|
+
Parameters:
|
|
101
|
+
original (Exception): The original exception raised while ordering the queryset; retained as the wrapped cause.
|
|
102
|
+
"""
|
|
103
|
+
super().__init__(f"Error ordering queryset: {original}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class NonSortablePropertyError(ValueError):
|
|
107
|
+
"""Raised when attempting to sort on a property lacking sort support."""
|
|
108
|
+
|
|
109
|
+
def __init__(self, property_name: str, manager_name: str) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Initialize an error indicating a property cannot be used for sorting on a manager.
|
|
112
|
+
|
|
113
|
+
Parameters:
|
|
114
|
+
property_name (str): The name of the property that was requested for sorting.
|
|
115
|
+
manager_name (str): The name of the manager (or manager class) where the property was queried.
|
|
116
|
+
"""
|
|
117
|
+
super().__init__(
|
|
118
|
+
f"Property '{property_name}' is not sortable in {manager_name}."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
20
122
|
class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
21
123
|
"""Bucket implementation backed by Django ORM querysets."""
|
|
22
124
|
|
|
@@ -59,25 +161,28 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
59
161
|
other: Bucket[GeneralManagerType] | GeneralManagerType,
|
|
60
162
|
) -> DatabaseBucket[GeneralManagerType]:
|
|
61
163
|
"""
|
|
62
|
-
|
|
164
|
+
Produce a new DatabaseBucket representing the union of this bucket with another DatabaseBucket or a GeneralManager instance of the same manager class.
|
|
63
165
|
|
|
64
166
|
Parameters:
|
|
65
|
-
other (Bucket[GeneralManagerType] | GeneralManagerType):
|
|
167
|
+
other (Bucket[GeneralManagerType] | GeneralManagerType): The bucket or manager instance to merge with this bucket.
|
|
66
168
|
|
|
67
169
|
Returns:
|
|
68
|
-
DatabaseBucket[GeneralManagerType]:
|
|
170
|
+
DatabaseBucket[GeneralManagerType]: A new bucket containing the combined items from both operands.
|
|
69
171
|
|
|
70
172
|
Raises:
|
|
71
|
-
|
|
173
|
+
DatabaseBucketTypeMismatchError: If `other` is not a DatabaseBucket of the same class and not a compatible GeneralManager.
|
|
174
|
+
DatabaseBucketManagerMismatchError: If `other` is a DatabaseBucket but uses a different manager class.
|
|
72
175
|
"""
|
|
73
176
|
if isinstance(other, GeneralManager) and other.__class__ == self._manager_class:
|
|
74
177
|
return self.__or__(
|
|
75
178
|
self._manager_class.filter(id__in=[other.identification["id"]])
|
|
76
179
|
)
|
|
77
180
|
if not isinstance(other, self.__class__):
|
|
78
|
-
raise
|
|
181
|
+
raise DatabaseBucketTypeMismatchError(self.__class__, type(other))
|
|
79
182
|
if self._manager_class != other._manager_class:
|
|
80
|
-
raise
|
|
183
|
+
raise DatabaseBucketManagerMismatchError(
|
|
184
|
+
self._manager_class, other._manager_class
|
|
185
|
+
)
|
|
81
186
|
return self.__class__(
|
|
82
187
|
self._data | other._data,
|
|
83
188
|
self._manager_class,
|
|
@@ -109,19 +214,24 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
109
214
|
def __parseFilterDeifintions(
|
|
110
215
|
self,
|
|
111
216
|
**kwargs: Any,
|
|
112
|
-
) -> tuple[dict[str, Any], dict[str,
|
|
217
|
+
) -> tuple[dict[str, Any], dict[str, Any], list[tuple[str, Any, str]]]:
|
|
113
218
|
"""
|
|
114
|
-
|
|
219
|
+
Split provided filter kwargs into three parts: query annotations required by properties, ORM-compatible lookup mappings, and Python-evaluated filter specifications.
|
|
115
220
|
|
|
116
221
|
Parameters:
|
|
117
222
|
**kwargs: Filter lookups supplied to `filter` or `exclude`.
|
|
118
223
|
|
|
119
224
|
Returns:
|
|
120
|
-
tuple
|
|
121
|
-
|
|
225
|
+
tuple:
|
|
226
|
+
- annotations (dict[str, Any]): Mapping from property name to its `query_annotation` (callable or annotation object) for properties that require ORM annotations.
|
|
227
|
+
- orm_kwargs (dict[str, list[Any]]): Mapping of ORM lookup strings (e.g., "field__lookup") to their values to be passed to the queryset.
|
|
228
|
+
- python_filters (list[tuple[str, Any, str]]): List of tuples (lookup, value, root_property_name) for properties that must be evaluated in Python.
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
NonFilterablePropertyError: If a lookup targets a property that is not allowed to be filtered.
|
|
122
232
|
"""
|
|
123
233
|
annotations: dict[str, Any] = {}
|
|
124
|
-
orm_kwargs: dict[str,
|
|
234
|
+
orm_kwargs: dict[str, Any] = {}
|
|
125
235
|
python_filters: list[tuple[str, Any, str]] = []
|
|
126
236
|
properties = self._manager_class.Interface.getGraphQLProperties()
|
|
127
237
|
|
|
@@ -129,9 +239,7 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
129
239
|
root = k.split("__")[0]
|
|
130
240
|
if root in properties:
|
|
131
241
|
if not properties[root].filterable:
|
|
132
|
-
raise
|
|
133
|
-
f"Property '{root}' is not filterable in {self._manager_class.__name__}"
|
|
134
|
-
)
|
|
242
|
+
raise NonFilterablePropertyError(root, self._manager_class.__name__)
|
|
135
243
|
prop = properties[root]
|
|
136
244
|
if prop.query_annotation is not None:
|
|
137
245
|
annotations[root] = prop.query_annotation
|
|
@@ -172,17 +280,18 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
172
280
|
|
|
173
281
|
def filter(self, **kwargs: Any) -> DatabaseBucket[GeneralManagerType]:
|
|
174
282
|
"""
|
|
175
|
-
|
|
283
|
+
Return a new DatabaseBucket refined by the given Django-style lookup expressions.
|
|
176
284
|
|
|
177
285
|
Parameters:
|
|
178
|
-
**kwargs (Any): Django-style lookup expressions
|
|
286
|
+
**kwargs (Any): Django-style lookup expressions to apply to the underlying queryset.
|
|
179
287
|
|
|
180
288
|
Returns:
|
|
181
|
-
DatabaseBucket[GeneralManagerType]: New bucket
|
|
289
|
+
DatabaseBucket[GeneralManagerType]: New bucket containing items matching the existing state combined with the provided lookups.
|
|
182
290
|
|
|
183
291
|
Raises:
|
|
184
|
-
|
|
185
|
-
|
|
292
|
+
NonFilterablePropertyError: If a provided property is not filterable for this manager.
|
|
293
|
+
InvalidQueryAnnotationTypeError: If a query-annotation callback returns a non-QuerySet.
|
|
294
|
+
QuerysetFilteringError: If the ORM rejects the filter arguments or filtering fails.
|
|
186
295
|
"""
|
|
187
296
|
annotations, orm_kwargs, python_filters = self.__parseFilterDeifintions(
|
|
188
297
|
**kwargs
|
|
@@ -196,12 +305,12 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
196
305
|
continue
|
|
197
306
|
qs = value(qs)
|
|
198
307
|
if not isinstance(qs, models.QuerySet):
|
|
199
|
-
raise
|
|
308
|
+
raise InvalidQueryAnnotationTypeError()
|
|
200
309
|
qs = qs.annotate(**other_annotations)
|
|
201
310
|
try:
|
|
202
311
|
qs = qs.filter(**orm_kwargs)
|
|
203
|
-
except
|
|
204
|
-
raise
|
|
312
|
+
except (FieldError, TypeError, ValueError) as error:
|
|
313
|
+
raise QuerysetFilteringError(error) from error
|
|
205
314
|
|
|
206
315
|
if python_filters:
|
|
207
316
|
ids = self.__parsePythonFilters(qs, python_filters)
|
|
@@ -212,16 +321,18 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
212
321
|
|
|
213
322
|
def exclude(self, **kwargs: Any) -> DatabaseBucket[GeneralManagerType]:
|
|
214
323
|
"""
|
|
215
|
-
Produce a bucket that excludes rows matching the
|
|
324
|
+
Produce a bucket that excludes rows matching the provided Django-style lookup expressions.
|
|
325
|
+
|
|
326
|
+
Accepts ORM lookups, query annotation entries, and Python-only filters; annotation callables will be applied to the underlying queryset as needed.
|
|
216
327
|
|
|
217
328
|
Parameters:
|
|
218
|
-
**kwargs (Any): Django-style lookup expressions
|
|
329
|
+
**kwargs (Any): Django-style lookup expressions, annotation entries, or property-based filters used to identify records to exclude.
|
|
219
330
|
|
|
220
331
|
Returns:
|
|
221
|
-
DatabaseBucket[GeneralManagerType]:
|
|
332
|
+
DatabaseBucket[GeneralManagerType]: A new bucket whose queryset omits rows matching the provided lookups.
|
|
222
333
|
|
|
223
334
|
Raises:
|
|
224
|
-
|
|
335
|
+
InvalidQueryAnnotationTypeError: If an annotation callable is applied and does not return a Django QuerySet.
|
|
225
336
|
"""
|
|
226
337
|
annotations, orm_kwargs, python_filters = self.__parseFilterDeifintions(
|
|
227
338
|
**kwargs
|
|
@@ -235,7 +346,7 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
235
346
|
continue
|
|
236
347
|
qs = value(qs)
|
|
237
348
|
if not isinstance(qs, models.QuerySet):
|
|
238
|
-
raise
|
|
349
|
+
raise InvalidQueryAnnotationTypeError()
|
|
239
350
|
qs = qs.annotate(**other_annotations)
|
|
240
351
|
qs = qs.exclude(**orm_kwargs)
|
|
241
352
|
|
|
@@ -370,18 +481,21 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
370
481
|
reverse: bool = False,
|
|
371
482
|
) -> DatabaseBucket:
|
|
372
483
|
"""
|
|
373
|
-
Return a new
|
|
484
|
+
Return a new DatabaseBucket ordered by the given property name(s).
|
|
485
|
+
|
|
486
|
+
Accepts a single property name or a tuple of property names. Properties with ORM annotations are applied at the database level; properties without ORM annotations are evaluated in Python and the resulting records are re-ordered while preserving a queryset result. Stable ordering and preservation of manager wrapping are maintained.
|
|
374
487
|
|
|
375
488
|
Parameters:
|
|
376
|
-
key (str | tuple[str, ...]):
|
|
377
|
-
reverse (bool):
|
|
489
|
+
key (str | tuple[str, ...]): Property name or sequence of property names to sort by, applied in order of appearance.
|
|
490
|
+
reverse (bool): If True, sort each specified key in descending order.
|
|
378
491
|
|
|
379
492
|
Returns:
|
|
380
|
-
DatabaseBucket:
|
|
493
|
+
DatabaseBucket: A new bucket whose underlying queryset is ordered according to the requested keys.
|
|
381
494
|
|
|
382
495
|
Raises:
|
|
383
|
-
|
|
384
|
-
|
|
496
|
+
NonSortablePropertyError: If any requested property is not marked as sortable on the manager's GraphQL properties.
|
|
497
|
+
InvalidQueryAnnotationTypeError: If a property query annotation callable returns a non-QuerySet value.
|
|
498
|
+
QuerysetOrderingError: If the ORM rejects the constructed ordering (e.g., invalid field or incompatible ordering expression).
|
|
385
499
|
"""
|
|
386
500
|
if isinstance(key, str):
|
|
387
501
|
key = (key,)
|
|
@@ -393,9 +507,7 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
393
507
|
if k in properties:
|
|
394
508
|
prop = properties[k]
|
|
395
509
|
if not prop.sortable:
|
|
396
|
-
raise
|
|
397
|
-
f"Property '{k}' is not sortable in {self._manager_class.__name__}"
|
|
398
|
-
)
|
|
510
|
+
raise NonSortablePropertyError(k, self._manager_class.__name__)
|
|
399
511
|
if prop.query_annotation is not None:
|
|
400
512
|
if callable(prop.query_annotation):
|
|
401
513
|
qs = prop.query_annotation(qs)
|
|
@@ -404,7 +516,7 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
404
516
|
else:
|
|
405
517
|
python_keys.append(k)
|
|
406
518
|
if not isinstance(qs, models.QuerySet):
|
|
407
|
-
raise
|
|
519
|
+
raise InvalidQueryAnnotationTypeError()
|
|
408
520
|
if annotations:
|
|
409
521
|
qs = qs.annotate(**annotations)
|
|
410
522
|
|
|
@@ -435,8 +547,8 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
|
|
|
435
547
|
order_fields = [f"-{k}" if reverse else k for k in key]
|
|
436
548
|
try:
|
|
437
549
|
qs = qs.order_by(*order_fields)
|
|
438
|
-
except
|
|
439
|
-
raise
|
|
550
|
+
except (FieldError, TypeError, ValueError) as error:
|
|
551
|
+
raise QuerysetOrderingError(error) from error
|
|
440
552
|
|
|
441
553
|
return self.__class__(qs, self._manager_class)
|
|
442
554
|
|
|
@@ -1,16 +1,110 @@
|
|
|
1
1
|
"""Grouping bucket implementation for aggregating GeneralManager instances."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
from typing import
|
|
5
|
-
Type,
|
|
6
|
-
Generator,
|
|
7
|
-
Any,
|
|
8
|
-
)
|
|
4
|
+
from typing import Any, Generator, Type
|
|
9
5
|
from general_manager.manager.groupManager import GroupManager
|
|
10
|
-
from general_manager.bucket.baseBucket import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
)
|
|
6
|
+
from general_manager.bucket.baseBucket import Bucket, GeneralManagerType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InvalidGroupByKeyTypeError(TypeError):
|
|
10
|
+
"""Raised when a non-string value is provided as a group-by key."""
|
|
11
|
+
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Error raised when a non-string group-by key is provided.
|
|
15
|
+
|
|
16
|
+
Initializes the exception with the message "groupBy() arguments must be strings."
|
|
17
|
+
"""
|
|
18
|
+
super().__init__("groupBy() arguments must be strings.")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UnknownGroupByKeyError(ValueError):
|
|
22
|
+
"""Raised when a group-by key does not exist on the manager interface."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, manager_name: str) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Create an UnknownGroupByKeyError indicating a missing attribute on a manager.
|
|
27
|
+
|
|
28
|
+
Parameters:
|
|
29
|
+
manager_name (str): Name of the manager whose attributes were expected; used to format the error message.
|
|
30
|
+
"""
|
|
31
|
+
super().__init__(f"groupBy() arguments must be attributes of {manager_name}.")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class GroupBucketTypeMismatchError(TypeError):
|
|
35
|
+
"""Raised when attempting to merge grouping buckets of different types."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, first_type: type, second_type: type) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Initialize the error for attempting to combine two incompatible bucket types.
|
|
40
|
+
|
|
41
|
+
Parameters:
|
|
42
|
+
first_type (type): The first type involved in the attempted combination.
|
|
43
|
+
second_type (type): The second type involved in the attempted combination.
|
|
44
|
+
|
|
45
|
+
Notes:
|
|
46
|
+
The exception message is formatted as "Cannot combine {first_type.__name__} with {second_type.__name__}."
|
|
47
|
+
"""
|
|
48
|
+
super().__init__(
|
|
49
|
+
f"Cannot combine {first_type.__name__} with {second_type.__name__}."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GroupBucketManagerMismatchError(ValueError):
|
|
54
|
+
"""Raised when grouping buckets track different manager classes."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, first_manager: type, second_manager: type) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Initialize the exception indicating two group buckets track different manager classes.
|
|
59
|
+
|
|
60
|
+
Parameters:
|
|
61
|
+
first_manager (type): The first manager class involved in the mismatch.
|
|
62
|
+
second_manager (type): The second manager class involved in the mismatch.
|
|
63
|
+
"""
|
|
64
|
+
super().__init__(
|
|
65
|
+
f"Cannot combine buckets for {first_manager.__name__} and {second_manager.__name__}."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class GroupItemNotFoundError(ValueError):
|
|
70
|
+
"""Raised when a grouped manager matching the provided criteria cannot be found."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, manager_name: str, criteria: dict[str, Any]) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Initialize an error indicating a grouped manager matching the provided lookup criteria could not be found.
|
|
75
|
+
|
|
76
|
+
Parameters:
|
|
77
|
+
manager_name (str): Name of the manager type searched for.
|
|
78
|
+
criteria (dict[str, Any]): Lookup criteria used to locate the manager; included in the error message.
|
|
79
|
+
"""
|
|
80
|
+
super().__init__(f"Cannot find {manager_name} with {criteria}.")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class EmptyGroupBucketSliceError(ValueError):
|
|
84
|
+
"""Raised when slicing a group bucket yields no results."""
|
|
85
|
+
|
|
86
|
+
def __init__(self) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Initialize the EmptyGroupBucketSliceError indicating that slicing a GroupBucket produced no results.
|
|
89
|
+
|
|
90
|
+
The exception carries the message "Cannot slice an empty GroupBucket."
|
|
91
|
+
"""
|
|
92
|
+
super().__init__("Cannot slice an empty GroupBucket.")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class InvalidGroupBucketIndexError(TypeError):
|
|
96
|
+
"""Raised when a group bucket is indexed with an unsupported type."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, received_type: type) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Initialize the exception for an unsupported GroupBucket index argument type.
|
|
101
|
+
|
|
102
|
+
Parameters:
|
|
103
|
+
received_type (type): The actual type that was passed as the index; used to construct the error message.
|
|
104
|
+
"""
|
|
105
|
+
super().__init__(
|
|
106
|
+
f"Invalid argument type: {received_type}. Expected int or slice."
|
|
107
|
+
)
|
|
14
108
|
|
|
15
109
|
|
|
16
110
|
class GroupBucket(Bucket[GeneralManagerType]):
|
|
@@ -65,40 +159,35 @@ class GroupBucket(Bucket[GeneralManagerType]):
|
|
|
65
159
|
|
|
66
160
|
def __checkGroupByArguments(self, group_by_keys: tuple[str, ...]) -> None:
|
|
67
161
|
"""
|
|
68
|
-
Validate
|
|
162
|
+
Validate that each provided group-by key is a string and is exposed by the manager interface.
|
|
69
163
|
|
|
70
164
|
Parameters:
|
|
71
|
-
group_by_keys (tuple[str, ...]): Attribute names
|
|
72
|
-
|
|
73
|
-
Returns:
|
|
74
|
-
None
|
|
165
|
+
group_by_keys (tuple[str, ...]): Attribute names to use for grouping.
|
|
75
166
|
|
|
76
167
|
Raises:
|
|
77
|
-
|
|
78
|
-
|
|
168
|
+
InvalidGroupByKeyTypeError: If any element of `group_by_keys` is not a string.
|
|
169
|
+
UnknownGroupByKeyError: If any key is not listed in the manager class's interface attributes.
|
|
79
170
|
"""
|
|
80
171
|
if not all(isinstance(arg, str) for arg in group_by_keys):
|
|
81
|
-
raise
|
|
172
|
+
raise InvalidGroupByKeyTypeError()
|
|
82
173
|
if not all(
|
|
83
174
|
arg in self._manager_class.Interface.getAttributes()
|
|
84
175
|
for arg in group_by_keys
|
|
85
176
|
):
|
|
86
|
-
raise
|
|
87
|
-
f"groupBy() argument must be a valid attribute of {self._manager_class.__name__}"
|
|
88
|
-
)
|
|
177
|
+
raise UnknownGroupByKeyError(self._manager_class.__name__)
|
|
89
178
|
|
|
90
179
|
def __buildGroupedManager(
|
|
91
180
|
self,
|
|
92
181
|
data: Bucket[GeneralManagerType],
|
|
93
182
|
) -> list[GroupManager[GeneralManagerType]]:
|
|
94
183
|
"""
|
|
95
|
-
|
|
184
|
+
Builds a GroupManager for each distinct combination of configured group-by attribute values.
|
|
96
185
|
|
|
97
186
|
Parameters:
|
|
98
|
-
data (Bucket[GeneralManagerType]): Source bucket
|
|
187
|
+
data (Bucket[GeneralManagerType]): Source bucket whose entries are partitioned by the bucket's configured group-by keys.
|
|
99
188
|
|
|
100
189
|
Returns:
|
|
101
|
-
list[GroupManager[GeneralManagerType]]:
|
|
190
|
+
list[GroupManager[GeneralManagerType]]: A list of GroupManager objects, one per unique tuple of group-by key values; groups are produced in order sorted by the string representation of their key tuples.
|
|
102
191
|
"""
|
|
103
192
|
group_by_values: set[tuple[tuple[str, Any], ...]] = set()
|
|
104
193
|
for entry in data:
|
|
@@ -118,21 +207,24 @@ class GroupBucket(Bucket[GeneralManagerType]):
|
|
|
118
207
|
|
|
119
208
|
def __or__(self, other: object) -> GroupBucket[GeneralManagerType]:
|
|
120
209
|
"""
|
|
121
|
-
|
|
210
|
+
Return a new GroupBucket representing the union of this bucket and another compatible GroupBucket.
|
|
122
211
|
|
|
123
212
|
Parameters:
|
|
124
|
-
other (
|
|
213
|
+
other (GroupBucket): The grouping bucket to merge with this one.
|
|
125
214
|
|
|
126
215
|
Returns:
|
|
127
|
-
GroupBucket[GeneralManagerType]:
|
|
216
|
+
GroupBucket[GeneralManagerType]: A GroupBucket with the same manager class and grouping keys whose basis data is the union of both inputs.
|
|
128
217
|
|
|
129
218
|
Raises:
|
|
130
|
-
|
|
219
|
+
GroupBucketTypeMismatchError: If `other` is not a GroupBucket of the same class.
|
|
220
|
+
GroupBucketManagerMismatchError: If `other` tracks a different manager class.
|
|
131
221
|
"""
|
|
132
222
|
if not isinstance(other, self.__class__):
|
|
133
|
-
raise
|
|
223
|
+
raise GroupBucketTypeMismatchError(self.__class__, type(other))
|
|
134
224
|
if self._manager_class != other._manager_class:
|
|
135
|
-
raise
|
|
225
|
+
raise GroupBucketManagerMismatchError(
|
|
226
|
+
self._manager_class, other._manager_class
|
|
227
|
+
)
|
|
136
228
|
return GroupBucket(
|
|
137
229
|
self._manager_class,
|
|
138
230
|
self._group_by_keys,
|
|
@@ -226,40 +318,37 @@ class GroupBucket(Bucket[GeneralManagerType]):
|
|
|
226
318
|
|
|
227
319
|
def get(self, **kwargs: Any) -> GroupManager[GeneralManagerType]:
|
|
228
320
|
"""
|
|
229
|
-
Retrieve the first
|
|
321
|
+
Retrieve the first GroupManager matching the provided lookups.
|
|
230
322
|
|
|
231
323
|
Parameters:
|
|
232
|
-
**kwargs: Field lookups
|
|
324
|
+
**kwargs: Field lookups used to filter the grouped managers.
|
|
233
325
|
|
|
234
326
|
Returns:
|
|
235
|
-
|
|
327
|
+
The first matching GroupManager.
|
|
236
328
|
|
|
237
329
|
Raises:
|
|
238
|
-
|
|
330
|
+
GroupItemNotFoundError: If no grouped manager matches the filters.
|
|
239
331
|
"""
|
|
240
332
|
first_value = self.filter(**kwargs).first()
|
|
241
333
|
if first_value is None:
|
|
242
|
-
raise
|
|
243
|
-
f"Cannot find {self._manager_class.__name__} with {kwargs}"
|
|
244
|
-
)
|
|
334
|
+
raise GroupItemNotFoundError(self._manager_class.__name__, kwargs)
|
|
245
335
|
return first_value
|
|
246
336
|
|
|
247
337
|
def __getitem__(
|
|
248
338
|
self, item: int | slice
|
|
249
339
|
) -> GroupManager[GeneralManagerType] | GroupBucket[GeneralManagerType]:
|
|
250
340
|
"""
|
|
251
|
-
|
|
341
|
+
Retrieve a single grouped manager by index or construct a new GroupBucket from a slice of groups.
|
|
252
342
|
|
|
253
343
|
Parameters:
|
|
254
|
-
item (int | slice):
|
|
344
|
+
item (int | slice): Integer index to select a single GroupManager, or a slice to select a subsequence of groups.
|
|
255
345
|
|
|
256
346
|
Returns:
|
|
257
|
-
GroupManager[GeneralManagerType]
|
|
258
|
-
Group at the specified index or a new bucket built from the selected groups.
|
|
347
|
+
GroupManager[GeneralManagerType] if `item` is an int, otherwise a GroupBucket[GeneralManagerType] built from the selected groups.
|
|
259
348
|
|
|
260
349
|
Raises:
|
|
261
|
-
|
|
262
|
-
|
|
350
|
+
EmptyGroupBucketSliceError: If the slice selects no groups.
|
|
351
|
+
InvalidGroupBucketIndexError: If `item` is not an int or slice.
|
|
263
352
|
"""
|
|
264
353
|
if isinstance(item, int):
|
|
265
354
|
return self._data[item]
|
|
@@ -272,9 +361,9 @@ class GroupBucket(Bucket[GeneralManagerType]):
|
|
|
272
361
|
else:
|
|
273
362
|
new_base_data = new_base_data | manager._data
|
|
274
363
|
if new_base_data is None:
|
|
275
|
-
raise
|
|
364
|
+
raise EmptyGroupBucketSliceError()
|
|
276
365
|
return GroupBucket(self._manager_class, self._group_by_keys, new_base_data)
|
|
277
|
-
raise
|
|
366
|
+
raise InvalidGroupBucketIndexError(type(item))
|
|
278
367
|
|
|
279
368
|
def __len__(self) -> int:
|
|
280
369
|
"""
|
|
@@ -12,10 +12,19 @@ __all__ = list(CACHE_EXPORTS)
|
|
|
12
12
|
_MODULE_MAP = CACHE_EXPORTS
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
|
-
from general_manager._types.cache import * # noqa:
|
|
15
|
+
from general_manager._types.cache import * # noqa: F403
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def __getattr__(name: str) -> Any:
|
|
19
|
+
"""
|
|
20
|
+
Resolve a public API export by attribute name for module-level dynamic access.
|
|
21
|
+
|
|
22
|
+
Parameters:
|
|
23
|
+
name (str): The attribute name being accessed on the module.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Any: The object exported under `name` from the module's cached public API, or raises AttributeError if not found.
|
|
27
|
+
"""
|
|
19
28
|
return resolve_export(
|
|
20
29
|
name,
|
|
21
30
|
module_all=__all__,
|