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,15 +1,15 @@
1
1
  """Database-backed bucket implementation for GeneralManager collections."""
2
2
 
3
3
  from __future__ import annotations
4
- from typing import Type, Any, Generator, TypeVar, TYPE_CHECKING
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
- Merge two database buckets (or bucket and instance) into a single result.
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): Bucket or manager instance to merge.
167
+ other (Bucket[GeneralManagerType] | GeneralManagerType): The bucket or manager instance to merge with this bucket.
66
168
 
67
169
  Returns:
68
- DatabaseBucket[GeneralManagerType]: New bucket containing the combined queryset.
170
+ DatabaseBucket[GeneralManagerType]: A new bucket containing the combined items from both operands.
69
171
 
70
172
  Raises:
71
- ValueError: If the operand is incompatible or uses a different manager class.
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 ValueError("Cannot combine different bucket types")
181
+ raise DatabaseBucketTypeMismatchError(self.__class__, type(other))
79
182
  if self._manager_class != other._manager_class:
80
- raise ValueError("Cannot combine different bucket managers")
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, list[Any]], list[tuple[str, Any, str]]]:
217
+ ) -> tuple[dict[str, Any], dict[str, Any], list[tuple[str, Any, str]]]:
113
218
  """
114
- Separate ORM-compatible filters from Python-side property filters.
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[dict[str, Any], dict[str, Any], list[tuple[str, Any, str]]]:
121
- Query annotations, ORM-compatible lookups, and Python-evaluated filter specifications.
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, list[Any]] = {}
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 ValueError(
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
- Produce a bucket filtered by the supplied lookups in addition to existing state.
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 applied to the underlying queryset.
286
+ **kwargs (Any): Django-style lookup expressions to apply to the underlying queryset.
179
287
 
180
288
  Returns:
181
- DatabaseBucket[GeneralManagerType]: New bucket representing the refined queryset.
289
+ DatabaseBucket[GeneralManagerType]: New bucket containing items matching the existing state combined with the provided lookups.
182
290
 
183
291
  Raises:
184
- ValueError: If the ORM rejects the filter arguments.
185
- TypeError: If a query annotation callback does not return a queryset.
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 TypeError("Query annotation must return a Django QuerySet")
308
+ raise InvalidQueryAnnotationTypeError()
200
309
  qs = qs.annotate(**other_annotations)
201
310
  try:
202
311
  qs = qs.filter(**orm_kwargs)
203
- except Exception as e:
204
- raise ValueError(f"Error filtering queryset: {e}")
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 supplied lookups.
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 identifying records to omit.
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]: New bucket representing the filtered queryset.
332
+ DatabaseBucket[GeneralManagerType]: A new bucket whose queryset omits rows matching the provided lookups.
222
333
 
223
334
  Raises:
224
- TypeError: If a query annotation callback does not return a queryset.
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 TypeError("Query annotation must return a Django QuerySet")
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 bucket ordered by the specified fields.
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, ...]): Field name(s) used for ordering.
377
- reverse (bool): Whether to sort in descending order.
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: Bucket whose queryset is ordered accordingly.
493
+ DatabaseBucket: A new bucket whose underlying queryset is ordered according to the requested keys.
381
494
 
382
495
  Raises:
383
- ValueError: If sorting by a non-sortable property or when the ORM rejects the ordering.
384
- TypeError: If a property annotation callback does not return a queryset.
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 ValueError(
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 TypeError("Query annotation must return a Django QuerySet")
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 Exception as e:
439
- raise ValueError(f"Error ordering queryset: {e}")
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
- Bucket,
12
- GeneralManagerType,
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 the supplied group-by keys.
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 requested for grouping.
72
-
73
- Returns:
74
- None
165
+ group_by_keys (tuple[str, ...]): Attribute names to use for grouping.
75
166
 
76
167
  Raises:
77
- TypeError: If a key is not a string.
78
- ValueError: If a key is not an attribute exposed by the manager interface.
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 TypeError("groupBy() arguments must be a strings")
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 ValueError(
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
- Construct grouped manager objects for every unique combination of key values.
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 that will be partitioned by the configured keys.
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]]: Group managers covering all key combinations.
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
- Combine two grouping buckets produced from the same manager class.
210
+ Return a new GroupBucket representing the union of this bucket and another compatible GroupBucket.
122
211
 
123
212
  Parameters:
124
- other (object): Another grouping bucket to merge.
213
+ other (GroupBucket): The grouping bucket to merge with this one.
125
214
 
126
215
  Returns:
127
- GroupBucket[GeneralManagerType]: Bucket representing the union of both inputs.
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
- ValueError: If `other` is not a compatible GroupBucket instance.
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 ValueError("Cannot combine different bucket types")
223
+ raise GroupBucketTypeMismatchError(self.__class__, type(other))
134
224
  if self._manager_class != other._manager_class:
135
- raise ValueError("Cannot combine different manager classes")
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 grouped manager matching the supplied filters.
321
+ Retrieve the first GroupManager matching the provided lookups.
230
322
 
231
323
  Parameters:
232
- **kwargs: Field lookups applied to the grouped data.
324
+ **kwargs: Field lookups used to filter the grouped managers.
233
325
 
234
326
  Returns:
235
- GroupManager[GeneralManagerType]: Matching grouped manager.
327
+ The first matching GroupManager.
236
328
 
237
329
  Raises:
238
- ValueError: If no grouped manager matches the filters.
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 ValueError(
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
- Access a specific group or a slice of groups.
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): Index or slice describing the desired groups.
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] | GroupBucket[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
- ValueError: If the requested slice contains no groups.
262
- TypeError: If the argument is not an integer or slice.
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 ValueError("Cannot slice an empty GroupBucket")
364
+ raise EmptyGroupBucketSliceError()
276
365
  return GroupBucket(self._manager_class, self._group_by_keys, new_base_data)
277
- raise TypeError(f"Invalid argument type: {type(item)}. Expected int or slice.")
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: F401,F403
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__,
@@ -88,6 +88,9 @@ def cached(
88
88
 
89
89
  return result
90
90
 
91
+ # fix for python 3.14:
92
+ wrapper.__annotations__ = func.__annotations__
93
+
91
94
  return cast(FuncT, wrapper)
92
95
 
93
96
  return decorator