pico-ioc 2.1.0__py3-none-any.whl → 2.1.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.
- pico_ioc/__init__.py +2 -1
- pico_ioc/_version.py +1 -1
- pico_ioc/aop.py +18 -7
- pico_ioc/api.py +50 -18
- pico_ioc/config_builder.py +1 -5
- pico_ioc/config_registrar.py +21 -4
- pico_ioc/config_runtime.py +39 -13
- pico_ioc/container.py +41 -29
- pico_ioc/decorators.py +2 -1
- pico_ioc/event_bus.py +24 -25
- pico_ioc/locator.py +0 -4
- pico_ioc/registrar.py +20 -1
- pico_ioc/scope.py +13 -4
- {pico_ioc-2.1.0.dist-info → pico_ioc-2.1.1.dist-info}/METADATA +2 -2
- pico_ioc-2.1.1.dist-info/RECORD +25 -0
- pico_ioc-2.1.0.dist-info/RECORD +0 -25
- {pico_ioc-2.1.0.dist-info → pico_ioc-2.1.1.dist-info}/WHEEL +0 -0
- {pico_ioc-2.1.0.dist-info → pico_ioc-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-2.1.0.dist-info → pico_ioc-2.1.1.dist-info}/top_level.txt +0 -0
pico_ioc/__init__.py
CHANGED
|
@@ -29,7 +29,7 @@ from .factory import ComponentFactory, ProviderMetadata, DeferredProvider
|
|
|
29
29
|
from .aop import MethodCtx, MethodInterceptor, intercepted_by, UnifiedComponentProxy, health, ContainerObserver
|
|
30
30
|
from .container import PicoContainer
|
|
31
31
|
from .event_bus import EventBus, ExecPolicy, ErrorPolicy, Event, subscribe, AutoSubscriberMixin
|
|
32
|
-
from .config_runtime import JsonTreeSource, YamlTreeSource, DictSource, Discriminator
|
|
32
|
+
from .config_runtime import JsonTreeSource, YamlTreeSource, DictSource, Discriminator, Value
|
|
33
33
|
from .analysis import DependencyRequest, analyze_callable_dependencies
|
|
34
34
|
|
|
35
35
|
__all__ = [
|
|
@@ -88,6 +88,7 @@ __all__ = [
|
|
|
88
88
|
"YamlTreeSource",
|
|
89
89
|
"DictSource",
|
|
90
90
|
"Discriminator",
|
|
91
|
+
"Value",
|
|
91
92
|
"DependencyRequest",
|
|
92
93
|
"analyze_callable_dependencies",
|
|
93
94
|
]
|
pico_ioc/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '2.1.
|
|
1
|
+
__version__ = '2.1.1'
|
pico_ioc/aop.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# src/pico_ioc/aop.py
|
|
2
|
+
|
|
1
3
|
import inspect
|
|
2
4
|
import pickle
|
|
3
5
|
import threading
|
|
4
6
|
from typing import Any, Callable, Dict, List, Tuple, Protocol, Union
|
|
5
|
-
from .exceptions import SerializationError
|
|
7
|
+
from .exceptions import SerializationError, AsyncResolutionError
|
|
6
8
|
|
|
7
9
|
KeyT = Union[str, type]
|
|
8
10
|
|
|
@@ -119,9 +121,19 @@ class UnifiedComponentProxy:
|
|
|
119
121
|
tgt = creator()
|
|
120
122
|
if tgt is None:
|
|
121
123
|
raise RuntimeError("UnifiedComponentProxy object_creator returned None")
|
|
124
|
+
|
|
125
|
+
container = object.__getattribute__(self, "_container")
|
|
126
|
+
if container and hasattr(container, "_run_configure_methods"):
|
|
127
|
+
res = container._run_configure_methods(tgt)
|
|
128
|
+
if inspect.isawaitable(res):
|
|
129
|
+
raise AsyncResolutionError(
|
|
130
|
+
f"Lazy component {type(tgt).__name__} requires async "
|
|
131
|
+
"@configure but was resolved via sync get()"
|
|
132
|
+
)
|
|
133
|
+
|
|
122
134
|
object.__setattr__(self, "_target", tgt)
|
|
123
135
|
return tgt
|
|
124
|
-
|
|
136
|
+
|
|
125
137
|
def _scope_signature(self) -> Tuple[Any, ...]:
|
|
126
138
|
container = object.__getattribute__(self, "_container")
|
|
127
139
|
target = object.__getattribute__(self, "_target")
|
|
@@ -138,7 +150,7 @@ class UnifiedComponentProxy:
|
|
|
138
150
|
return ()
|
|
139
151
|
return (container.scopes.get_id(sc),)
|
|
140
152
|
return ()
|
|
141
|
-
|
|
153
|
+
|
|
142
154
|
def _build_wrapped(self, name: str, bound: Callable[..., Any], interceptors_cls: Tuple[type, ...]):
|
|
143
155
|
container = object.__getattribute__(self, "_container")
|
|
144
156
|
interceptors = [container.get(cls) for cls in interceptors_cls]
|
|
@@ -182,11 +194,11 @@ class UnifiedComponentProxy:
|
|
|
182
194
|
raise RuntimeError(f"Async interceptor returned awaitable on sync method: {name}")
|
|
183
195
|
return res
|
|
184
196
|
return sig, sw, interceptors_cls
|
|
185
|
-
|
|
197
|
+
|
|
186
198
|
@property
|
|
187
199
|
def __class__(self):
|
|
188
200
|
return self._get_real_object().__class__
|
|
189
|
-
|
|
201
|
+
|
|
190
202
|
def __getattr__(self, name: str) -> Any:
|
|
191
203
|
target = self._get_real_object()
|
|
192
204
|
attr = getattr(target, name)
|
|
@@ -211,7 +223,7 @@ class UnifiedComponentProxy:
|
|
|
211
223
|
sig, wrapped, cls_tuple = self._build_wrapped(name, attr, interceptors_cls)
|
|
212
224
|
cache[name] = (sig, wrapped, cls_tuple)
|
|
213
225
|
return wrapped
|
|
214
|
-
|
|
226
|
+
|
|
215
227
|
def __setattr__(self, name, value): setattr(self._get_real_object(), name, value)
|
|
216
228
|
def __delattr__(self, name): delattr(self._get_real_object(), name)
|
|
217
229
|
def __str__(self): return str(self._get_real_object())
|
|
@@ -275,4 +287,3 @@ class UnifiedComponentProxy:
|
|
|
275
287
|
return (pickle.loads, (data,))
|
|
276
288
|
except Exception as e:
|
|
277
289
|
raise SerializationError(f"Proxy target is not serializable: {e}")
|
|
278
|
-
|
pico_ioc/api.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# src/pico_ioc/api.py
|
|
2
|
+
|
|
1
3
|
import importlib
|
|
2
4
|
import pkgutil
|
|
3
5
|
import logging
|
|
4
6
|
import inspect
|
|
5
|
-
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
|
|
7
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
|
|
6
8
|
from .exceptions import ConfigurationError, InvalidBindingError
|
|
7
9
|
from .factory import ComponentFactory, ProviderMetadata
|
|
8
10
|
from .locator import ComponentLocator
|
|
@@ -10,6 +12,7 @@ from .scope import ScopeManager, ScopedCaches
|
|
|
10
12
|
from .container import PicoContainer
|
|
11
13
|
from .decorators import component, factory, provides, Qualifier, configure, cleanup, configured
|
|
12
14
|
from .config_builder import ContextConfig, configuration
|
|
15
|
+
from .registrar import Registrar
|
|
13
16
|
|
|
14
17
|
KeyT = Union[str, type]
|
|
15
18
|
Provider = Callable[[], Any]
|
|
@@ -26,7 +29,6 @@ def _iter_input_modules(inputs: Union[Any, Iterable[Any]]) -> Iterable[Any]:
|
|
|
26
29
|
mod = importlib.import_module(it)
|
|
27
30
|
else:
|
|
28
31
|
mod = it
|
|
29
|
-
|
|
30
32
|
if hasattr(mod, "__path__"):
|
|
31
33
|
for sub in _scan_package(mod):
|
|
32
34
|
name = getattr(sub, "__name__", None)
|
|
@@ -49,28 +51,37 @@ def _normalize_override_provider(v: Any):
|
|
|
49
51
|
return (lambda f=v: f()), False
|
|
50
52
|
return (lambda inst=v: inst), False
|
|
51
53
|
|
|
52
|
-
def init(
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
def init(
|
|
55
|
+
modules: Union[Any, Iterable[Any]],
|
|
56
|
+
*,
|
|
57
|
+
profiles: Tuple[str, ...] = (),
|
|
58
|
+
allowed_profiles: Optional[Iterable[str]] = None,
|
|
59
|
+
environ: Optional[Dict[str, str]] = None,
|
|
60
|
+
overrides: Optional[Dict[KeyT, Any]] = None,
|
|
61
|
+
logger: Optional[logging.Logger] = None,
|
|
62
|
+
config: Optional[ContextConfig] = None,
|
|
63
|
+
custom_scopes: Optional[Iterable[str]] = None,
|
|
64
|
+
validate_only: bool = False,
|
|
65
|
+
container_id: Optional[str] = None,
|
|
66
|
+
observers: Optional[List["ContainerObserver"]] = None,
|
|
67
|
+
) -> PicoContainer:
|
|
55
68
|
active = tuple(p.strip() for p in profiles if p)
|
|
56
|
-
allowed_set = set(a.strip() for a in allowed_profiles) if allowed_profiles is not None else None
|
|
57
69
|
|
|
70
|
+
allowed_set = set(a.strip() for a in allowed_profiles) if allowed_profiles is not None else None
|
|
58
71
|
if allowed_set is not None:
|
|
59
72
|
unknown = set(active) - allowed_set
|
|
60
73
|
if unknown:
|
|
61
74
|
raise ConfigurationError(f"Unknown profiles: {sorted(unknown)}; allowed: {sorted(allowed_set)}")
|
|
62
|
-
|
|
75
|
+
|
|
63
76
|
factory = ComponentFactory()
|
|
64
77
|
caches = ScopedCaches()
|
|
65
78
|
scopes = ScopeManager()
|
|
66
|
-
|
|
67
79
|
if custom_scopes:
|
|
68
|
-
for
|
|
69
|
-
scopes.register_scope(
|
|
80
|
+
for name in custom_scopes:
|
|
81
|
+
scopes.register_scope(name)
|
|
70
82
|
|
|
71
83
|
pico = PicoContainer(factory, caches, scopes, container_id=container_id, profiles=active, observers=observers or [])
|
|
72
84
|
registrar = Registrar(factory, profiles=active, environ=environ, logger=logger, config=config)
|
|
73
|
-
|
|
74
85
|
for m in _iter_input_modules(modules):
|
|
75
86
|
registrar.register_module(m)
|
|
76
87
|
|
|
@@ -79,8 +90,7 @@ def init(modules: Union[Any, Iterable[Any]], *, profiles: Tuple[str, ...] = (),
|
|
|
79
90
|
prov, _ = _normalize_override_provider(v)
|
|
80
91
|
factory.bind(k, prov)
|
|
81
92
|
|
|
82
|
-
registrar.finalize(overrides)
|
|
83
|
-
|
|
93
|
+
registrar.finalize(overrides, pico_instance=pico)
|
|
84
94
|
if validate_only:
|
|
85
95
|
locator = registrar.locator()
|
|
86
96
|
pico.attach_locator(locator)
|
|
@@ -91,13 +101,39 @@ def init(modules: Union[Any, Iterable[Any]], *, profiles: Tuple[str, ...] = (),
|
|
|
91
101
|
registrar.attach_runtime(pico, locator)
|
|
92
102
|
pico.attach_locator(locator)
|
|
93
103
|
_fail_fast_cycle_check(pico)
|
|
104
|
+
|
|
105
|
+
if not validate_only:
|
|
106
|
+
eager_singletons = []
|
|
107
|
+
for key, md in locator._metadata.items():
|
|
108
|
+
if md.scope == "singleton" and not md.lazy:
|
|
109
|
+
cache = pico._cache_for(key)
|
|
110
|
+
instance = cache.get(key)
|
|
111
|
+
if instance is None:
|
|
112
|
+
instance = pico.get(key)
|
|
113
|
+
eager_singletons.append(instance)
|
|
114
|
+
else:
|
|
115
|
+
eager_singletons.append(instance)
|
|
116
|
+
|
|
117
|
+
configure_awaitables = []
|
|
118
|
+
for instance in eager_singletons:
|
|
119
|
+
res = pico._run_configure_methods(instance)
|
|
120
|
+
if inspect.isawaitable(res):
|
|
121
|
+
configure_awaitables.append(res)
|
|
122
|
+
|
|
123
|
+
if configure_awaitables:
|
|
124
|
+
raise ConfigurationError(
|
|
125
|
+
"Sync init() found eagerly loaded singletons with async @configure methods. "
|
|
126
|
+
"This can be caused by an async __ainit__ or async @configure. "
|
|
127
|
+
"Use an async main function and await pico.aget() for those components, "
|
|
128
|
+
"or mark them as lazy=True."
|
|
129
|
+
)
|
|
130
|
+
|
|
94
131
|
return pico
|
|
95
132
|
|
|
96
133
|
def _find_cycle(graph: Dict[KeyT, Tuple[KeyT, ...]]) -> Optional[Tuple[KeyT, ...]]:
|
|
97
134
|
temp: Set[KeyT] = set()
|
|
98
135
|
perm: Set[KeyT] = set()
|
|
99
136
|
stack: List[KeyT] = []
|
|
100
|
-
|
|
101
137
|
def visit(n: KeyT) -> Optional[Tuple[KeyT, ...]]:
|
|
102
138
|
if n in perm:
|
|
103
139
|
return None
|
|
@@ -107,20 +143,16 @@ def _find_cycle(graph: Dict[KeyT, Tuple[KeyT, ...]]) -> Optional[Tuple[KeyT, ...
|
|
|
107
143
|
return tuple(stack[idx:] + [n])
|
|
108
144
|
except ValueError:
|
|
109
145
|
return tuple([n, n])
|
|
110
|
-
|
|
111
146
|
temp.add(n)
|
|
112
147
|
stack.append(n)
|
|
113
|
-
|
|
114
148
|
for m in graph.get(n, ()):
|
|
115
149
|
c = visit(m)
|
|
116
150
|
if c:
|
|
117
151
|
return c
|
|
118
|
-
|
|
119
152
|
stack.pop()
|
|
120
153
|
temp.remove(n)
|
|
121
154
|
perm.add(n)
|
|
122
155
|
return None
|
|
123
|
-
|
|
124
156
|
for node in graph.keys():
|
|
125
157
|
c = visit(node)
|
|
126
158
|
if c:
|
pico_ioc/config_builder.py
CHANGED
|
@@ -3,13 +3,9 @@ import json
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from typing import Any, Optional, Protocol, Mapping, List, Tuple, Dict, Union
|
|
5
5
|
|
|
6
|
-
from .config_runtime import TreeSource, DictSource, JsonTreeSource, YamlTreeSource
|
|
6
|
+
from .config_runtime import TreeSource, DictSource, JsonTreeSource, YamlTreeSource, Value
|
|
7
7
|
from .exceptions import ConfigurationError
|
|
8
8
|
|
|
9
|
-
class Value:
|
|
10
|
-
def __init__(self, value: Any):
|
|
11
|
-
self.value = value
|
|
12
|
-
|
|
13
9
|
class ConfigSource(Protocol):
|
|
14
10
|
pass
|
|
15
11
|
|
pico_ioc/config_registrar.py
CHANGED
|
@@ -4,7 +4,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union, get_args,
|
|
|
4
4
|
from .constants import PICO_INFRA, PICO_NAME, PICO_META
|
|
5
5
|
from .exceptions import ConfigurationError
|
|
6
6
|
from .factory import ProviderMetadata, DeferredProvider
|
|
7
|
-
from .config_builder import ContextConfig, ConfigSource, FlatDictSource
|
|
7
|
+
from .config_builder import ContextConfig, ConfigSource, FlatDictSource, Value
|
|
8
8
|
from .config_runtime import ConfigResolver, TypeAdapterRegistry, ObjectGraphBuilder, TreeSource
|
|
9
9
|
from .analysis import analyze_callable_dependencies, DependencyRequest
|
|
10
10
|
|
|
@@ -68,6 +68,22 @@ class ConfigurationManager:
|
|
|
68
68
|
raise ConfigurationError(f"Configuration class {getattr(cls, '__name__', str(cls))} must be a dataclass")
|
|
69
69
|
values: Dict[str, Any] = {}
|
|
70
70
|
for f in fields(cls):
|
|
71
|
+
field_type = f.type
|
|
72
|
+
value_override = None
|
|
73
|
+
|
|
74
|
+
if get_origin(field_type) is Annotated:
|
|
75
|
+
args = get_args(field_type)
|
|
76
|
+
field_type = args[0] if args else Any
|
|
77
|
+
metas = args[1:] if len(args) > 1 else ()
|
|
78
|
+
for m in metas:
|
|
79
|
+
if isinstance(m, Value):
|
|
80
|
+
value_override = m.value
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
if value_override is not None:
|
|
84
|
+
values[f.name] = value_override
|
|
85
|
+
continue
|
|
86
|
+
|
|
71
87
|
base_key = _upper_key(f.name)
|
|
72
88
|
keys_to_try = []
|
|
73
89
|
if prefix:
|
|
@@ -84,7 +100,7 @@ class ConfigurationManager:
|
|
|
84
100
|
if f.default is not MISSING or f.default_factory is not MISSING:
|
|
85
101
|
continue
|
|
86
102
|
raise ConfigurationError(f"Missing configuration key: {(prefix or '') + base_key}")
|
|
87
|
-
values[f.name] = _coerce(raw,
|
|
103
|
+
values[f.name] = _coerce(raw, field_type if isinstance(field_type, type) or get_origin(field_type) else str)
|
|
88
104
|
return cls(**values)
|
|
89
105
|
|
|
90
106
|
def _auto_detect_mapping(self, target_type: type) -> str:
|
|
@@ -159,7 +175,7 @@ class ConfigurationManager:
|
|
|
159
175
|
factory_method=None,
|
|
160
176
|
qualifiers=qset,
|
|
161
177
|
primary=True,
|
|
162
|
-
lazy=False,
|
|
178
|
+
lazy=bool(meta.get("lazy", False)),
|
|
163
179
|
infra="configured",
|
|
164
180
|
pico_name=prefix,
|
|
165
181
|
scope=sc,
|
|
@@ -180,7 +196,7 @@ class ConfigurationManager:
|
|
|
180
196
|
factory_method=None,
|
|
181
197
|
qualifiers=qset,
|
|
182
198
|
primary=True,
|
|
183
|
-
lazy=False,
|
|
199
|
+
lazy=bool(meta.get("lazy", False)),
|
|
184
200
|
infra="configured",
|
|
185
201
|
pico_name=prefix,
|
|
186
202
|
scope=sc,
|
|
@@ -217,3 +233,4 @@ class ConfigurationManager:
|
|
|
217
233
|
prefix = md.pico_name or ""
|
|
218
234
|
keys = [_upper_key(f.name) for f in fields(target_type)]
|
|
219
235
|
return any(self._lookup_flat(prefix + k) is not None for k in keys)
|
|
236
|
+
|
pico_ioc/config_runtime.py
CHANGED
|
@@ -8,6 +8,10 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, Union, g
|
|
|
8
8
|
from .exceptions import ConfigurationError
|
|
9
9
|
from .constants import PICO_META
|
|
10
10
|
|
|
11
|
+
class Value:
|
|
12
|
+
def __init__(self, value: Any):
|
|
13
|
+
self.value = value
|
|
14
|
+
|
|
11
15
|
class Discriminator:
|
|
12
16
|
def __init__(self, name: str):
|
|
13
17
|
self.name = name
|
|
@@ -160,8 +164,8 @@ class ObjectGraphBuilder:
|
|
|
160
164
|
org = get_origin(t)
|
|
161
165
|
|
|
162
166
|
if org is Annotated:
|
|
163
|
-
base,
|
|
164
|
-
return self._build_discriminated(node, base,
|
|
167
|
+
base, metas = self._split_annotated(t)
|
|
168
|
+
return self._build_discriminated(node, base, metas, path)
|
|
165
169
|
|
|
166
170
|
if org in (list, List):
|
|
167
171
|
elem_t = get_args(t)[0] if get_args(t) else Any
|
|
@@ -255,20 +259,42 @@ class ObjectGraphBuilder:
|
|
|
255
259
|
|
|
256
260
|
def _build_discriminated(self, node: Any, base: Any, metas: Tuple[Any, ...], path: Tuple[str, ...]) -> Any:
|
|
257
261
|
disc_name = None
|
|
262
|
+
disc_value = None
|
|
263
|
+
has_value = False
|
|
264
|
+
|
|
258
265
|
for m in metas:
|
|
259
266
|
if isinstance(m, Discriminator):
|
|
260
267
|
disc_name = m.name
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
268
|
+
if isinstance(m, Value):
|
|
269
|
+
disc_value = m.value
|
|
270
|
+
has_value = True
|
|
271
|
+
|
|
272
|
+
tn: Optional[str] = None
|
|
273
|
+
|
|
274
|
+
if disc_name and has_value:
|
|
275
|
+
tn = str(disc_value)
|
|
276
|
+
elif disc_name and isinstance(node, dict) and disc_name in node:
|
|
277
|
+
tn = str(node[disc_name])
|
|
278
|
+
|
|
279
|
+
if tn is not None and get_origin(base) is Union:
|
|
280
|
+
for cand in get_args(base):
|
|
281
|
+
if isinstance(cand, type) and getattr(cand, "__name__", "") == tn:
|
|
282
|
+
|
|
283
|
+
cleaned_node = {k: v for k, v in node.items() if k != disc_name}
|
|
284
|
+
|
|
285
|
+
if has_value:
|
|
286
|
+
cleaned_node[disc_name] = tn
|
|
287
|
+
|
|
288
|
+
return self._build(cleaned_node, cand, path)
|
|
289
|
+
|
|
290
|
+
raise ConfigurationError(
|
|
291
|
+
f"Discriminator value '{tn}' for field '{disc_name}' "
|
|
292
|
+
f"did not match any type in Union {base} at {'.'.join(path)}"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if has_value and not disc_name:
|
|
296
|
+
return disc_value
|
|
297
|
+
|
|
272
298
|
return self._build(node, base, path)
|
|
273
299
|
|
|
274
300
|
def _coerce_prim(self, node: Any, t: type, path: Tuple[str, ...]) -> Any:
|
pico_ioc/container.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
# src/pico_ioc/container.py
|
|
2
|
+
|
|
1
3
|
import inspect
|
|
2
4
|
import contextvars
|
|
3
5
|
import functools
|
|
4
6
|
from typing import Any, Dict, List, Optional, Tuple, overload, Union, Callable, Iterable, Set, get_args, get_origin, Annotated, Protocol, Mapping
|
|
5
7
|
from contextlib import contextmanager
|
|
6
8
|
from .constants import LOGGER, PICO_META
|
|
7
|
-
from .exceptions import ComponentCreationError, ProviderNotFoundError, AsyncResolutionError
|
|
9
|
+
from .exceptions import ComponentCreationError, ProviderNotFoundError, AsyncResolutionError, ConfigurationError
|
|
8
10
|
from .factory import ComponentFactory, ProviderMetadata
|
|
9
11
|
from .locator import ComponentLocator
|
|
10
12
|
from .scope import ScopedCaches, ScopeManager
|
|
@@ -195,6 +197,26 @@ class PicoContainer:
|
|
|
195
197
|
finally:
|
|
196
198
|
self.deactivate(token_container)
|
|
197
199
|
|
|
200
|
+
def _run_configure_methods(self, instance: Any) -> Any:
|
|
201
|
+
if not _needs_async_configure(instance):
|
|
202
|
+
for m in _iter_configure_methods(instance):
|
|
203
|
+
configure_deps = analyze_callable_dependencies(m)
|
|
204
|
+
args = self._resolve_args(configure_deps)
|
|
205
|
+
res = m(**args)
|
|
206
|
+
if inspect.isawaitable(res):
|
|
207
|
+
LOGGER.warning(f"Async configure method {m} called during sync get. Awaitable ignored.")
|
|
208
|
+
return instance
|
|
209
|
+
|
|
210
|
+
async def runner():
|
|
211
|
+
for m in _iter_configure_methods(instance):
|
|
212
|
+
configure_deps = analyze_callable_dependencies(m)
|
|
213
|
+
args = self._resolve_args(configure_deps)
|
|
214
|
+
r = m(**args)
|
|
215
|
+
if inspect.isawaitable(r):
|
|
216
|
+
await r
|
|
217
|
+
return instance
|
|
218
|
+
return runner()
|
|
219
|
+
|
|
198
220
|
@overload
|
|
199
221
|
def get(self, key: type) -> Any: ...
|
|
200
222
|
@overload
|
|
@@ -210,6 +232,14 @@ class PicoContainer:
|
|
|
210
232
|
key_name = getattr(key, '__name__', str(key))
|
|
211
233
|
raise AsyncResolutionError(key)
|
|
212
234
|
|
|
235
|
+
md = self._locator._metadata.get(key) if self._locator else None
|
|
236
|
+
scope = (md.scope if md else "singleton")
|
|
237
|
+
if scope != "singleton":
|
|
238
|
+
instance_or_awaitable_configured = self._run_configure_methods(instance)
|
|
239
|
+
if inspect.isawaitable(instance_or_awaitable_configured):
|
|
240
|
+
raise AsyncResolutionError(key)
|
|
241
|
+
instance = instance_or_awaitable_configured
|
|
242
|
+
|
|
213
243
|
final_instance = self._maybe_wrap_with_aspects(key, instance)
|
|
214
244
|
cache = self._cache_for(key)
|
|
215
245
|
cache.put(key, final_instance)
|
|
@@ -228,6 +258,15 @@ class PicoContainer:
|
|
|
228
258
|
if inspect.isawaitable(instance_or_awaitable):
|
|
229
259
|
instance = await instance_or_awaitable
|
|
230
260
|
|
|
261
|
+
md = self._locator._metadata.get(key) if self._locator else None
|
|
262
|
+
scope = (md.scope if md else "singleton")
|
|
263
|
+
if scope != "singleton":
|
|
264
|
+
instance_or_awaitable_configured = self._run_configure_methods(instance)
|
|
265
|
+
if inspect.isawaitable(instance_or_awaitable_configured):
|
|
266
|
+
instance = await instance_or_awaitable_configured
|
|
267
|
+
else:
|
|
268
|
+
instance = instance_or_awaitable_configured
|
|
269
|
+
|
|
231
270
|
final_instance = self._maybe_wrap_with_aspects(key, instance)
|
|
232
271
|
cache = self._cache_for(key)
|
|
233
272
|
cache.put(key, final_instance)
|
|
@@ -432,7 +471,7 @@ class PicoContainer:
|
|
|
432
471
|
inst = cls(**deps)
|
|
433
472
|
|
|
434
473
|
ainit = getattr(inst, "__ainit__", None)
|
|
435
|
-
has_async = (callable(ainit) and inspect.iscoroutinefunction(ainit))
|
|
474
|
+
has_async = (callable(ainit) and inspect.iscoroutinefunction(ainit))
|
|
436
475
|
|
|
437
476
|
if has_async:
|
|
438
477
|
async def runner():
|
|
@@ -446,39 +485,12 @@ class PicoContainer:
|
|
|
446
485
|
res = ainit(**kwargs)
|
|
447
486
|
if inspect.isawaitable(res):
|
|
448
487
|
await res
|
|
449
|
-
for m in _iter_configure_methods(inst):
|
|
450
|
-
configure_deps = analyze_callable_dependencies(m)
|
|
451
|
-
args = self._resolve_args(configure_deps)
|
|
452
|
-
r = m(**args)
|
|
453
|
-
if inspect.isawaitable(r):
|
|
454
|
-
await r
|
|
455
488
|
return inst
|
|
456
489
|
return runner()
|
|
457
490
|
|
|
458
|
-
for m in _iter_configure_methods(inst):
|
|
459
|
-
configure_deps = analyze_callable_dependencies(m)
|
|
460
|
-
args = self._resolve_args(configure_deps)
|
|
461
|
-
m(**args)
|
|
462
491
|
return inst
|
|
463
492
|
|
|
464
493
|
def build_method(self, fn: Callable[..., Any], locator: ComponentLocator, dependencies: Tuple[DependencyRequest, ...]) -> Any:
|
|
465
494
|
deps = self._resolve_args(dependencies)
|
|
466
495
|
obj = fn(**deps)
|
|
467
|
-
|
|
468
|
-
has_async = _needs_async_configure(obj)
|
|
469
|
-
if has_async:
|
|
470
|
-
async def runner():
|
|
471
|
-
for m in _iter_configure_methods(obj):
|
|
472
|
-
configure_deps = analyze_callable_dependencies(m)
|
|
473
|
-
args = self._resolve_args(configure_deps)
|
|
474
|
-
r = m(**args)
|
|
475
|
-
if inspect.isawaitable(r):
|
|
476
|
-
await r
|
|
477
|
-
return obj
|
|
478
|
-
return runner()
|
|
479
|
-
|
|
480
|
-
for m in _iter_configure_methods(obj):
|
|
481
|
-
configure_deps = analyze_callable_dependencies(m)
|
|
482
|
-
args = self._resolve_args(configure_deps)
|
|
483
|
-
m(**args)
|
|
484
496
|
return obj
|
pico_ioc/decorators.py
CHANGED
|
@@ -172,13 +172,14 @@ def cleanup(fn):
|
|
|
172
172
|
m["cleanup"] = True
|
|
173
173
|
return fn
|
|
174
174
|
|
|
175
|
-
def configured(target: Any, *, prefix: str = "", mapping: str = "auto"):
|
|
175
|
+
def configured(target: Any = "self", *, prefix: str = "", mapping: str = "auto", **kwargs):
|
|
176
176
|
if mapping not in ("auto", "flat", "tree"):
|
|
177
177
|
raise ValueError("mapping must be one of 'auto', 'flat', or 'tree'")
|
|
178
178
|
def dec(cls):
|
|
179
179
|
setattr(cls, PICO_INFRA, "configured")
|
|
180
180
|
m = _meta_get(cls)
|
|
181
181
|
m["configured"] = {"target": target, "prefix": prefix, "mapping": mapping}
|
|
182
|
+
_apply_common_metadata(cls, **kwargs)
|
|
182
183
|
return cls
|
|
183
184
|
return dec
|
|
184
185
|
|
pico_ioc/event_bus.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# src/pico_ioc/event_bus.py
|
|
2
1
|
import asyncio
|
|
3
2
|
import inspect
|
|
4
3
|
import logging
|
|
@@ -151,29 +150,30 @@ class EventBus:
|
|
|
151
150
|
self._worker_loop = None
|
|
152
151
|
|
|
153
152
|
def post(self, event: Event) -> None:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
153
|
+
with self._lock:
|
|
154
|
+
if self._closed:
|
|
155
|
+
raise EventBusClosedError()
|
|
156
|
+
if self._queue is None:
|
|
157
|
+
raise EventBusError("Worker queue not initialized. Call start_worker().")
|
|
158
|
+
loop = self._worker_loop
|
|
159
|
+
if loop and loop.is_running():
|
|
160
|
+
try:
|
|
161
|
+
current_loop = asyncio.get_running_loop()
|
|
162
|
+
if current_loop is loop:
|
|
163
|
+
try:
|
|
164
|
+
self._queue.put_nowait(event)
|
|
165
|
+
return
|
|
166
|
+
except asyncio.QueueFull:
|
|
167
|
+
raise EventBusQueueFullError()
|
|
168
|
+
except RuntimeError:
|
|
169
|
+
pass
|
|
170
|
+
try:
|
|
171
|
+
loop.call_soon_threadsafe(self._queue.put_nowait, event)
|
|
172
|
+
return
|
|
173
|
+
except asyncio.QueueFull:
|
|
174
|
+
raise EventBusQueueFullError()
|
|
175
|
+
else:
|
|
176
|
+
raise EventBusError("Worker queue not initialized or loop not running. Call start_worker().")
|
|
177
177
|
|
|
178
178
|
async def aclose(self) -> None:
|
|
179
179
|
await self.stop_worker()
|
|
@@ -220,4 +220,3 @@ class PicoEventBusProvider:
|
|
|
220
220
|
loop.create_task(event_bus.aclose())
|
|
221
221
|
else:
|
|
222
222
|
asyncio.run(event_bus.aclose())
|
|
223
|
-
|
pico_ioc/locator.py
CHANGED
|
@@ -115,10 +115,6 @@ class ComponentLocator:
|
|
|
115
115
|
return k
|
|
116
116
|
return None
|
|
117
117
|
|
|
118
|
-
def _compile_argplan_static(self, callable_obj):
|
|
119
|
-
raise NotImplementedError("This method is obsolete and replaced by analysis module")
|
|
120
|
-
|
|
121
|
-
|
|
122
118
|
def dependency_keys_for_static(self, md: ProviderMetadata):
|
|
123
119
|
deps: List[KeyT] = []
|
|
124
120
|
for dep in md.dependencies:
|
pico_ioc/registrar.py
CHANGED
|
@@ -17,6 +17,7 @@ from .provider_selector import ProviderSelector
|
|
|
17
17
|
from .dependency_validator import DependencyValidator
|
|
18
18
|
from .component_scanner import ComponentScanner
|
|
19
19
|
from .analysis import analyze_callable_dependencies, DependencyRequest
|
|
20
|
+
from .container import PicoContainer
|
|
20
21
|
|
|
21
22
|
KeyT = Union[str, type]
|
|
22
23
|
Provider = Callable[[], Any]
|
|
@@ -133,7 +134,7 @@ class Registrar:
|
|
|
133
134
|
add("pico_name", md.pico_name, k)
|
|
134
135
|
|
|
135
136
|
|
|
136
|
-
def finalize(self, overrides: Optional[Dict[KeyT, Any]]) -> None:
|
|
137
|
+
def finalize(self, overrides: Optional[Dict[KeyT, Any]], *, pico_instance: PicoContainer) -> None:
|
|
137
138
|
candidates, on_missing, deferred_providers, provides_functions = self._scanner.get_scan_results()
|
|
138
139
|
self._deferred = deferred_providers
|
|
139
140
|
self._provides_functions = provides_functions
|
|
@@ -142,6 +143,24 @@ class Registrar:
|
|
|
142
143
|
for key, (provider, md) in winners.items():
|
|
143
144
|
self._bind_if_absent(key, provider)
|
|
144
145
|
self._metadata[key] = md
|
|
146
|
+
|
|
147
|
+
if PicoContainer not in self._metadata:
|
|
148
|
+
self._factory.bind(PicoContainer, lambda: pico_instance)
|
|
149
|
+
self._metadata[PicoContainer] = ProviderMetadata(
|
|
150
|
+
key=PicoContainer,
|
|
151
|
+
provided_type=PicoContainer,
|
|
152
|
+
concrete_class=PicoContainer,
|
|
153
|
+
factory_class=None,
|
|
154
|
+
factory_method=None,
|
|
155
|
+
qualifiers=set(),
|
|
156
|
+
primary=True,
|
|
157
|
+
lazy=False,
|
|
158
|
+
infra="component",
|
|
159
|
+
pico_name="PicoContainer",
|
|
160
|
+
override=True,
|
|
161
|
+
scope="singleton",
|
|
162
|
+
dependencies=()
|
|
163
|
+
)
|
|
145
164
|
|
|
146
165
|
self._promote_scopes()
|
|
147
166
|
self._rebuild_indexes()
|
pico_ioc/scope.py
CHANGED
|
@@ -3,6 +3,7 @@ import contextvars
|
|
|
3
3
|
import inspect
|
|
4
4
|
from typing import Any, Dict, Optional, Tuple
|
|
5
5
|
from collections import OrderedDict
|
|
6
|
+
from .exceptions import ScopeError
|
|
6
7
|
|
|
7
8
|
class ScopeProtocol:
|
|
8
9
|
def get_id(self) -> Any | None: ...
|
|
@@ -42,16 +43,24 @@ class ScopeManager:
|
|
|
42
43
|
self._scopes: Dict[str, ScopeProtocol] = {
|
|
43
44
|
"request": ContextVarScope(contextvars.ContextVar("pico_request_id", default=None)),
|
|
44
45
|
"session": ContextVarScope(contextvars.ContextVar("pico_session_id", default=None)),
|
|
46
|
+
"websocket": ContextVarScope(contextvars.ContextVar("pico_websocket_id", default=None)),
|
|
45
47
|
"transaction": ContextVarScope(contextvars.ContextVar("pico_tx_id", default=None)),
|
|
46
48
|
}
|
|
47
|
-
|
|
49
|
+
|
|
50
|
+
def register_scope(self, name: str) -> None:
|
|
48
51
|
if not isinstance(name, str) or not name:
|
|
49
|
-
from .exceptions import ScopeError
|
|
50
52
|
raise ScopeError("Scope name must be a non-empty string")
|
|
51
53
|
if name in ("singleton", "prototype"):
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
raise ScopeError(f"Cannot register reserved scope: '{name}'")
|
|
55
|
+
if name in self._scopes:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
var_name = f"pico_{name}_id"
|
|
59
|
+
context_var = contextvars.ContextVar(var_name, default=None)
|
|
60
|
+
implementation = ContextVarScope(context_var)
|
|
54
61
|
self._scopes[name] = implementation
|
|
62
|
+
|
|
63
|
+
|
|
55
64
|
def get_id(self, name: str) -> Any | None:
|
|
56
65
|
if name in ("singleton", "prototype"):
|
|
57
66
|
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-ioc
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: A minimalist, zero-dependency Inversion of Control (IoC) container for Python.
|
|
5
5
|
Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -39,7 +39,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
39
39
|
Classifier: Programming Language :: Python :: 3.14
|
|
40
40
|
Classifier: License :: OSI Approved :: MIT License
|
|
41
41
|
Classifier: Operating System :: OS Independent
|
|
42
|
-
Requires-Python: >=3.
|
|
42
|
+
Requires-Python: >=3.10
|
|
43
43
|
Description-Content-Type: text/markdown
|
|
44
44
|
License-File: LICENSE
|
|
45
45
|
Provides-Extra: yaml
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
pico_ioc/__init__.py,sha256=i25Obx7aH_Oy5b6yjjnCswDgni7InIjrGEcG6vLAw6I,2414
|
|
2
|
+
pico_ioc/_version.py,sha256=Aht2295j8FswZ-nPYofCYr3fBZ6Uyf0thTfl5Oc2mWA,22
|
|
3
|
+
pico_ioc/analysis.py,sha256=k49R-HcDyvpSNid8mxv7Fc6fPHnDu1C_b4HxrGLNF2g,2780
|
|
4
|
+
pico_ioc/aop.py,sha256=XcyzsuKPrVPk1_Jad7Mn-qwoL1y0ZuVWwRZBA-CslJk,13301
|
|
5
|
+
pico_ioc/api.py,sha256=0pcRFHzhDcX8ijd67xAsVrTejwXuJKz7kTKRUrIuX2s,6161
|
|
6
|
+
pico_ioc/component_scanner.py,sha256=S-9XNxrgyq_JFdc4Uqn2bEb-HxafSgIWylIurxyN_UA,7955
|
|
7
|
+
pico_ioc/config_builder.py,sha256=7kcYIq1Yrb46Tic7uLeaCDvLA-Sa_p1PIoGF00mivso,2848
|
|
8
|
+
pico_ioc/config_registrar.py,sha256=34iNQY1TUEPTXbb-QV1T-c5VKAn18hBcNt5MLhzDSfY,8456
|
|
9
|
+
pico_ioc/config_runtime.py,sha256=hiL1kCxhpjbfOdUaH71jMGNESDpWsaJkQXh7q1T71bg,12781
|
|
10
|
+
pico_ioc/constants.py,sha256=AhIt0ieDZ9Turxb_YbNzj11wUbBbzjKfWh1BDlSx2Nw,183
|
|
11
|
+
pico_ioc/container.py,sha256=gXgQT12ChpexbZALUfb0YYohlcRbUUeJ8-ltdR7xitc,18956
|
|
12
|
+
pico_ioc/decorators.py,sha256=ru_YeqyJ3gbfb6M8WeJZlBxfcBBEuGDvxpHJGzU6FIs,6412
|
|
13
|
+
pico_ioc/dependency_validator.py,sha256=BIR6pKntACiabF6CjNZ3m00RMnet9BPK1_9y1iCJ5KQ,4144
|
|
14
|
+
pico_ioc/event_bus.py,sha256=nOL91JLYxap9kbb-HBGEhOVwtXN_bfI4q0mtSRZFlHk,8434
|
|
15
|
+
pico_ioc/exceptions.py,sha256=FBuajj5g29hAGODt2tAWuy2sG5mQojdSddaqFzim-aY,2383
|
|
16
|
+
pico_ioc/factory.py,sha256=oJXx_BYJuvV8oxYzs5I3gx9WM6uLYZ8GCc43gukNanc,1671
|
|
17
|
+
pico_ioc/locator.py,sha256=4WN1qvXXW_LRInB2XJR8pTgIuJ8RyWBSpVo28HwtlL0,4737
|
|
18
|
+
pico_ioc/provider_selector.py,sha256=pU7NbI5vifvUlJEjlRJmvveQUZVD47T24QmiP0CHRw0,1213
|
|
19
|
+
pico_ioc/registrar.py,sha256=hIk48nXghTdA3WBljCbw2q8J_6F_hCk1ljSi4Pb8P3A,8368
|
|
20
|
+
pico_ioc/scope.py,sha256=hOdTmjjfrRt8APXoS3lbTbSPxILi7flBXz_qpIkpoKw,6137
|
|
21
|
+
pico_ioc-2.1.1.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
|
|
22
|
+
pico_ioc-2.1.1.dist-info/METADATA,sha256=deaBUX6MOzClrMVDDhk5siAh2W0QrtQI-w4n2ZI-gGI,10673
|
|
23
|
+
pico_ioc-2.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
+
pico_ioc-2.1.1.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
25
|
+
pico_ioc-2.1.1.dist-info/RECORD,,
|
pico_ioc-2.1.0.dist-info/RECORD
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
pico_ioc/__init__.py,sha256=pxYvrunT9cFAXxGbow5TRlzRXDVvm4E7L6PT1UkPeUw,2394
|
|
2
|
-
pico_ioc/_version.py,sha256=LbU43-7hsLmdXWI0wTAl3y6D3Tr7CbJiDOLXwl7clj8,22
|
|
3
|
-
pico_ioc/analysis.py,sha256=k49R-HcDyvpSNid8mxv7Fc6fPHnDu1C_b4HxrGLNF2g,2780
|
|
4
|
-
pico_ioc/aop.py,sha256=r8JCPsTTF1hmrQMtnj-_Lj99fIy5J97RAJjiR2LjVPQ,12769
|
|
5
|
-
pico_ioc/api.py,sha256=tR0pm6YEnDTA62EIT_Cpw1EaPfdnBFY9Dvpc38ypP-o,5061
|
|
6
|
-
pico_ioc/component_scanner.py,sha256=S-9XNxrgyq_JFdc4Uqn2bEb-HxafSgIWylIurxyN_UA,7955
|
|
7
|
-
pico_ioc/config_builder.py,sha256=ROBvbnm2Zv8apRdtZJHtebp-2cOMiuENFyiqaX1T2Ik,2918
|
|
8
|
-
pico_ioc/config_registrar.py,sha256=8Xpl-CNRFny3EIRlcNx6oMJQldXkj_jdaPbC17qk2Ec,7803
|
|
9
|
-
pico_ioc/config_runtime.py,sha256=Q8jVFQ1b6h1ZOkdacm70u0Q7TlEiUGENFkG-YWRkBxA,12133
|
|
10
|
-
pico_ioc/constants.py,sha256=AhIt0ieDZ9Turxb_YbNzj11wUbBbzjKfWh1BDlSx2Nw,183
|
|
11
|
-
pico_ioc/container.py,sha256=1gRjHH5kq_lOlnGUh5QYA4vUHF3ScQpwXoT9PWiyJ2g,18352
|
|
12
|
-
pico_ioc/decorators.py,sha256=qrx4oMjRmmgwqT4cdp6FxBoqex3L1yNGlCUxO865z14,6347
|
|
13
|
-
pico_ioc/dependency_validator.py,sha256=BIR6pKntACiabF6CjNZ3m00RMnet9BPK1_9y1iCJ5KQ,4144
|
|
14
|
-
pico_ioc/event_bus.py,sha256=E8Qb8KZ6K1CuXSbMlG0MNPHkGoWlssLLPzHq1QYdADQ,8346
|
|
15
|
-
pico_ioc/exceptions.py,sha256=FBuajj5g29hAGODt2tAWuy2sG5mQojdSddaqFzim-aY,2383
|
|
16
|
-
pico_ioc/factory.py,sha256=oJXx_BYJuvV8oxYzs5I3gx9WM6uLYZ8GCc43gukNanc,1671
|
|
17
|
-
pico_ioc/locator.py,sha256=UDyXl3oSK-sZ8ouOlhsDM91shMP0atMxyMoQlaSYvfE,4885
|
|
18
|
-
pico_ioc/provider_selector.py,sha256=pU7NbI5vifvUlJEjlRJmvveQUZVD47T24QmiP0CHRw0,1213
|
|
19
|
-
pico_ioc/registrar.py,sha256=cexXFDsuxBwM8FvW7mwmuyqcTcOkSP6ELecwklcs0F4,7625
|
|
20
|
-
pico_ioc/scope.py,sha256=GDsDJWw7e5Vpiys-M4vQfKMJWSCiorRsT5cPo6z34Mk,5924
|
|
21
|
-
pico_ioc-2.1.0.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
|
|
22
|
-
pico_ioc-2.1.0.dist-info/METADATA,sha256=lYR54Lhdwmn5tDXol2M5ReJxBsXpdZ9bH0KrcYOlq2s,10672
|
|
23
|
-
pico_ioc-2.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
-
pico_ioc-2.1.0.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
25
|
-
pico_ioc-2.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|