GeneralManager 0.14.1__py3-none-any.whl → 0.15.1__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.
- general_manager/__init__.py +49 -0
- general_manager/api/__init__.py +36 -0
- general_manager/api/graphql.py +92 -43
- general_manager/api/mutation.py +35 -10
- general_manager/api/property.py +26 -3
- general_manager/apps.py +23 -16
- general_manager/bucket/__init__.py +32 -0
- general_manager/bucket/baseBucket.py +76 -64
- general_manager/bucket/calculationBucket.py +188 -108
- general_manager/bucket/databaseBucket.py +130 -49
- general_manager/bucket/groupBucket.py +113 -60
- general_manager/cache/__init__.py +38 -0
- general_manager/cache/cacheDecorator.py +29 -17
- general_manager/cache/cacheTracker.py +34 -15
- general_manager/cache/dependencyIndex.py +117 -33
- general_manager/cache/modelDependencyCollector.py +17 -8
- general_manager/cache/signals.py +17 -6
- general_manager/factory/__init__.py +34 -5
- general_manager/factory/autoFactory.py +57 -60
- general_manager/factory/factories.py +39 -14
- general_manager/factory/factoryMethods.py +38 -1
- general_manager/interface/__init__.py +36 -0
- general_manager/interface/baseInterface.py +71 -27
- general_manager/interface/calculationInterface.py +18 -10
- general_manager/interface/databaseBasedInterface.py +102 -71
- general_manager/interface/databaseInterface.py +66 -20
- general_manager/interface/models.py +10 -4
- general_manager/interface/readOnlyInterface.py +44 -30
- general_manager/manager/__init__.py +36 -3
- general_manager/manager/generalManager.py +73 -47
- general_manager/manager/groupManager.py +72 -17
- general_manager/manager/input.py +23 -15
- general_manager/manager/meta.py +53 -53
- general_manager/measurement/__init__.py +37 -2
- general_manager/measurement/measurement.py +135 -58
- general_manager/measurement/measurementField.py +161 -61
- general_manager/permission/__init__.py +32 -1
- general_manager/permission/basePermission.py +29 -12
- general_manager/permission/managerBasedPermission.py +32 -26
- general_manager/permission/mutationPermission.py +32 -3
- general_manager/permission/permissionChecks.py +9 -1
- general_manager/permission/permissionDataManager.py +49 -15
- general_manager/permission/utils.py +14 -3
- general_manager/rule/__init__.py +27 -1
- general_manager/rule/handler.py +90 -5
- general_manager/rule/rule.py +40 -27
- general_manager/utils/__init__.py +44 -2
- general_manager/utils/argsToKwargs.py +17 -9
- general_manager/utils/filterParser.py +29 -30
- general_manager/utils/formatString.py +2 -0
- general_manager/utils/jsonEncoder.py +14 -1
- general_manager/utils/makeCacheKey.py +18 -12
- general_manager/utils/noneToZero.py +8 -6
- general_manager/utils/pathMapping.py +92 -29
- general_manager/utils/public_api.py +49 -0
- general_manager/utils/testing.py +135 -69
- {generalmanager-0.14.1.dist-info → generalmanager-0.15.1.dist-info}/METADATA +10 -2
- generalmanager-0.15.1.dist-info/RECORD +62 -0
- generalmanager-0.14.1.dist-info/RECORD +0 -58
- {generalmanager-0.14.1.dist-info → generalmanager-0.15.1.dist-info}/WHEEL +0 -0
- {generalmanager-0.14.1.dist-info → generalmanager-0.15.1.dist-info}/licenses/LICENSE +0 -0
- {generalmanager-0.14.1.dist-info → generalmanager-0.15.1.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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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
|
111
|
+
bool: True if the comparison succeeds; otherwise, False.
|
113
112
|
"""
|
114
113
|
try:
|
115
114
|
if lookup == "exact":
|
@@ -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
|
-
|
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
|
-
|
12
|
-
|
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
|
-
|
15
|
-
func: The
|
16
|
-
args: Positional arguments
|
17
|
-
kwargs: Keyword arguments
|
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
|
-
|
25
|
+
str: Hexadecimal SHA-256 digest representing the call signature.
|
21
26
|
"""
|
22
27
|
sig = inspect.signature(func)
|
23
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
value:
|
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
|
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
|
-
|
41
|
+
Populate the path mapping with tracers for every distinct pair of managed classes.
|
29
42
|
|
30
|
-
|
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__(
|
56
|
+
def __init__(
|
57
|
+
self,
|
58
|
+
path_start: PathStart | GeneralManager | type[GeneralManager],
|
59
|
+
) -> None:
|
41
60
|
"""
|
42
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
174
|
+
Recursively compute the traversal path from `current_manager` to the destination class.
|
113
175
|
|
114
|
-
|
115
|
-
current_manager:
|
116
|
-
path:
|
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
|
-
|
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
|
-
|
216
|
+
Traverse the stored path starting from the provided manager or bucket instance.
|
155
217
|
|
156
|
-
|
157
|
-
start_instance
|
218
|
+
Parameters:
|
219
|
+
start_instance (GeneralManager | Bucket): Object used as the traversal root.
|
158
220
|
|
159
221
|
Returns:
|
160
|
-
|
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
|
-
|
172
|
-
|
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 |
|
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))
|