haiway 0.24.2__py3-none-any.whl → 0.25.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.
- haiway/__init__.py +4 -1
- haiway/context/__init__.py +4 -0
- haiway/context/access.py +214 -63
- haiway/context/disposables.py +5 -119
- haiway/context/identifier.py +19 -44
- haiway/context/observability.py +22 -142
- haiway/context/presets.py +337 -0
- haiway/context/state.py +38 -84
- haiway/context/tasks.py +7 -10
- haiway/helpers/concurrent.py +12 -12
- haiway/helpers/observability.py +4 -6
- haiway/opentelemetry/observability.py +5 -5
- haiway/state/__init__.py +2 -0
- haiway/state/immutable.py +127 -0
- haiway/state/structure.py +63 -117
- haiway/state/validation.py +95 -60
- {haiway-0.24.2.dist-info → haiway-0.25.0.dist-info}/METADATA +1 -1
- {haiway-0.24.2.dist-info → haiway-0.25.0.dist-info}/RECORD +20 -18
- {haiway-0.24.2.dist-info → haiway-0.25.0.dist-info}/WHEEL +0 -0
- {haiway-0.24.2.dist-info → haiway-0.25.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,337 @@
|
|
1
|
+
from collections.abc import Collection, Iterable, Mapping
|
2
|
+
from contextvars import ContextVar, Token
|
3
|
+
from types import TracebackType
|
4
|
+
from typing import ClassVar, Protocol, Self, cast
|
5
|
+
|
6
|
+
from haiway.context.disposables import Disposable, Disposables
|
7
|
+
from haiway.state import Immutable, State
|
8
|
+
from haiway.types.default import Default
|
9
|
+
|
10
|
+
__all__ = (
|
11
|
+
"ContextPresets",
|
12
|
+
"ContextPresetsRegistry",
|
13
|
+
"ContextPresetsRegistryContext",
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class ContextPresetsStatePreparing(Protocol):
|
18
|
+
async def __call__(self) -> Iterable[State] | State: ...
|
19
|
+
|
20
|
+
|
21
|
+
class ContextPresetsDisposablesPreparing(Protocol):
|
22
|
+
async def __call__(self) -> Iterable[Disposable] | Disposable: ...
|
23
|
+
|
24
|
+
|
25
|
+
class ContextPresets(Immutable):
|
26
|
+
"""
|
27
|
+
A configuration preset for context scopes.
|
28
|
+
|
29
|
+
ContextPresets allow you to define reusable combinations of state and disposables
|
30
|
+
that can be applied to scopes by name. This provides a convenient way to manage
|
31
|
+
complex application configurations and resource setups.
|
32
|
+
|
33
|
+
State Priority
|
34
|
+
--------------
|
35
|
+
When used with ctx.scope(), preset state has lower priority than explicit state:
|
36
|
+
1. Explicit state (passed to ctx.scope()) - **highest priority**
|
37
|
+
2. Explicit disposables (passed to ctx.scope()) - medium priority
|
38
|
+
3. Preset state (from presets) - low priority
|
39
|
+
4. Contextual state (from parent contexts) - **lowest priority**
|
40
|
+
|
41
|
+
Examples
|
42
|
+
--------
|
43
|
+
Basic preset with static state:
|
44
|
+
|
45
|
+
>>> from haiway import State
|
46
|
+
>>> from haiway.context import ContextPresets
|
47
|
+
>>>
|
48
|
+
>>> class DatabaseConfig(State):
|
49
|
+
... connection_string: str
|
50
|
+
... pool_size: int = 10
|
51
|
+
>>>
|
52
|
+
>>> db_preset = ContextPresets(
|
53
|
+
... name="database",
|
54
|
+
... _state=[DatabaseConfig(connection_string="postgresql://localhost/app")]
|
55
|
+
... )
|
56
|
+
|
57
|
+
Preset with dynamic state factory:
|
58
|
+
|
59
|
+
>>> async def load_config() -> DatabaseConfig:
|
60
|
+
... # Load configuration from environment or config file
|
61
|
+
... return DatabaseConfig(connection_string=os.getenv("DB_URL"))
|
62
|
+
>>>
|
63
|
+
>>> dynamic_preset = ContextPresets(
|
64
|
+
... name="dynamic_db",
|
65
|
+
... _state=[load_config]
|
66
|
+
... )
|
67
|
+
|
68
|
+
Preset with disposables:
|
69
|
+
|
70
|
+
>>> from contextlib import asynccontextmanager
|
71
|
+
>>>
|
72
|
+
>>> @asynccontextmanager
|
73
|
+
>>> async def database_connection():
|
74
|
+
... conn = await create_connection()
|
75
|
+
... try:
|
76
|
+
... yield ConnectionState(connection=conn)
|
77
|
+
... finally:
|
78
|
+
... await conn.close()
|
79
|
+
>>>
|
80
|
+
>>> async def connection_factory():
|
81
|
+
... return database_connection()
|
82
|
+
>>>
|
83
|
+
>>> db_preset = ContextPresets(
|
84
|
+
... name="database",
|
85
|
+
... _state=[DatabaseConfig(connection_string="...")],
|
86
|
+
... _disposables=[connection_factory]
|
87
|
+
... )
|
88
|
+
|
89
|
+
Using presets:
|
90
|
+
|
91
|
+
>>> from haiway import ctx
|
92
|
+
>>>
|
93
|
+
>>> with ctx.presets(db_preset):
|
94
|
+
... async with ctx.scope("database"):
|
95
|
+
... config = ctx.state(DatabaseConfig)
|
96
|
+
... # Use the preset configuration
|
97
|
+
"""
|
98
|
+
|
99
|
+
name: str
|
100
|
+
_state: Collection[ContextPresetsStatePreparing | State] = Default(())
|
101
|
+
_disposables: Collection[ContextPresetsDisposablesPreparing] = Default(())
|
102
|
+
|
103
|
+
def extended(
|
104
|
+
self,
|
105
|
+
other: Self,
|
106
|
+
) -> Self:
|
107
|
+
"""
|
108
|
+
Create a new preset by extending this preset with another.
|
109
|
+
|
110
|
+
Combines the state and disposables from both presets, keeping the name
|
111
|
+
of the current preset. The other preset's state and disposables are
|
112
|
+
appended to this preset's collections.
|
113
|
+
|
114
|
+
Parameters
|
115
|
+
----------
|
116
|
+
other : Self
|
117
|
+
Another ContextPresets instance to merge with this one.
|
118
|
+
|
119
|
+
Returns
|
120
|
+
-------
|
121
|
+
Self
|
122
|
+
A new ContextPresets instance with combined state and disposables.
|
123
|
+
"""
|
124
|
+
return self.__class__(
|
125
|
+
name=self.name,
|
126
|
+
_state=(*self._state, *other._state),
|
127
|
+
_disposables=(*self._disposables, *other._disposables),
|
128
|
+
)
|
129
|
+
|
130
|
+
def with_state(
|
131
|
+
self,
|
132
|
+
*state: ContextPresetsStatePreparing | State,
|
133
|
+
) -> Self:
|
134
|
+
"""
|
135
|
+
Create a new preset with additional state.
|
136
|
+
|
137
|
+
Returns a new ContextPresets instance with the provided state objects
|
138
|
+
or state factories added to the existing state collection.
|
139
|
+
|
140
|
+
Parameters
|
141
|
+
----------
|
142
|
+
*state : ContextPresetsStatePreparing | State
|
143
|
+
Additional state objects or state factory functions to include.
|
144
|
+
|
145
|
+
Returns
|
146
|
+
-------
|
147
|
+
Self
|
148
|
+
A new ContextPresets instance with the additional state, or the
|
149
|
+
same instance if no state was provided.
|
150
|
+
"""
|
151
|
+
if not state:
|
152
|
+
return self
|
153
|
+
|
154
|
+
return self.__class__(
|
155
|
+
name=self.name,
|
156
|
+
_state=(*self._state, *state),
|
157
|
+
_disposables=self._disposables,
|
158
|
+
)
|
159
|
+
|
160
|
+
def with_disposable(
|
161
|
+
self,
|
162
|
+
*disposable: ContextPresetsDisposablesPreparing,
|
163
|
+
) -> Self:
|
164
|
+
"""
|
165
|
+
Create a new preset with additional disposables.
|
166
|
+
|
167
|
+
Returns a new ContextPresets instance with the provided disposable
|
168
|
+
factory functions added to the existing disposables collection.
|
169
|
+
|
170
|
+
Parameters
|
171
|
+
----------
|
172
|
+
*disposable : ContextPresetsDisposablesPreparing
|
173
|
+
Additional disposable factory functions to include.
|
174
|
+
|
175
|
+
Returns
|
176
|
+
-------
|
177
|
+
Self
|
178
|
+
A new ContextPresets instance with the additional disposables, or the
|
179
|
+
same instance if no disposables were provided.
|
180
|
+
"""
|
181
|
+
if not disposable:
|
182
|
+
return self
|
183
|
+
|
184
|
+
return self.__class__(
|
185
|
+
name=self.name,
|
186
|
+
_state=self._state,
|
187
|
+
_disposables=(*self._disposables, *disposable),
|
188
|
+
)
|
189
|
+
|
190
|
+
async def prepare(self) -> Disposables:
|
191
|
+
"""
|
192
|
+
Prepare the preset for use by resolving all state and disposables.
|
193
|
+
|
194
|
+
This method evaluates all state factories and disposable factories to create
|
195
|
+
concrete instances. State objects are wrapped in a DisposableState to unify
|
196
|
+
the handling of state and disposable resources.
|
197
|
+
|
198
|
+
The method ensures concurrent safety by creating fresh instances each time
|
199
|
+
it's called, making it safe to use the same preset across multiple concurrent
|
200
|
+
scopes.
|
201
|
+
|
202
|
+
Returns
|
203
|
+
-------
|
204
|
+
Disposables
|
205
|
+
A Disposables container holding all resolved state (wrapped in DisposableState)
|
206
|
+
and disposable resources from this preset.
|
207
|
+
|
208
|
+
Note
|
209
|
+
----
|
210
|
+
This method is called automatically when using presets with ctx.scope(),
|
211
|
+
so you typically don't need to call it directly.
|
212
|
+
"""
|
213
|
+
# Collect states directly
|
214
|
+
collected_states: list[State] = []
|
215
|
+
for state in self._state:
|
216
|
+
if isinstance(state, State):
|
217
|
+
collected_states.append(state)
|
218
|
+
else:
|
219
|
+
resolved_state: Iterable[State] | State = await state()
|
220
|
+
if isinstance(resolved_state, State):
|
221
|
+
collected_states.append(resolved_state)
|
222
|
+
|
223
|
+
else:
|
224
|
+
collected_states.extend(resolved_state)
|
225
|
+
|
226
|
+
collected_disposables: list[Disposable]
|
227
|
+
if collected_states:
|
228
|
+
collected_disposables = [DisposableState(_state=collected_states)]
|
229
|
+
|
230
|
+
else:
|
231
|
+
collected_disposables = []
|
232
|
+
|
233
|
+
for disposable in self._disposables:
|
234
|
+
resolved_disposable: Iterable[Disposable] | Disposable = await disposable()
|
235
|
+
if hasattr(resolved_disposable, "__aenter__") and hasattr(
|
236
|
+
resolved_disposable, "__aexit__"
|
237
|
+
):
|
238
|
+
collected_disposables.append(cast(Disposable, resolved_disposable))
|
239
|
+
|
240
|
+
else:
|
241
|
+
collected_disposables.extend(cast(Iterable[Disposable], resolved_disposable))
|
242
|
+
|
243
|
+
return Disposables(*collected_disposables)
|
244
|
+
|
245
|
+
|
246
|
+
class DisposableState(Immutable):
|
247
|
+
_state: Iterable[State]
|
248
|
+
|
249
|
+
async def __aenter__(self) -> Iterable[State]:
|
250
|
+
return self._state
|
251
|
+
|
252
|
+
async def __aexit__(
|
253
|
+
self,
|
254
|
+
exc_type: type[BaseException] | None,
|
255
|
+
exc_val: BaseException | None,
|
256
|
+
exc_tb: TracebackType | None,
|
257
|
+
) -> None:
|
258
|
+
pass
|
259
|
+
|
260
|
+
|
261
|
+
class ContextPresetsRegistry(Immutable):
|
262
|
+
_presets: Mapping[str, ContextPresets]
|
263
|
+
|
264
|
+
def __init__(
|
265
|
+
self,
|
266
|
+
presets: Collection[ContextPresets],
|
267
|
+
) -> None:
|
268
|
+
object.__setattr__(
|
269
|
+
self,
|
270
|
+
"_presets",
|
271
|
+
{preset.name: preset for preset in presets},
|
272
|
+
)
|
273
|
+
|
274
|
+
def select(
|
275
|
+
self,
|
276
|
+
name: str,
|
277
|
+
/,
|
278
|
+
) -> ContextPresets | None:
|
279
|
+
return self._presets.get(name)
|
280
|
+
|
281
|
+
|
282
|
+
class ContextPresetsRegistryContext(Immutable):
|
283
|
+
_context: ClassVar[ContextVar[ContextPresetsRegistry]] = ContextVar[ContextPresetsRegistry](
|
284
|
+
"ContextPresetsRegistryContext"
|
285
|
+
)
|
286
|
+
|
287
|
+
@classmethod
|
288
|
+
def select(
|
289
|
+
cls,
|
290
|
+
name: str,
|
291
|
+
/,
|
292
|
+
) -> ContextPresets | None:
|
293
|
+
try:
|
294
|
+
return cls._context.get().select(name)
|
295
|
+
|
296
|
+
except LookupError:
|
297
|
+
return None # no presets
|
298
|
+
|
299
|
+
_registry: ContextPresetsRegistry
|
300
|
+
_token: Token[ContextPresetsRegistry] | None = None
|
301
|
+
|
302
|
+
def __init__(
|
303
|
+
self,
|
304
|
+
registry: ContextPresetsRegistry,
|
305
|
+
) -> None:
|
306
|
+
object.__setattr__(
|
307
|
+
self,
|
308
|
+
"_registry",
|
309
|
+
registry,
|
310
|
+
)
|
311
|
+
object.__setattr__(
|
312
|
+
self,
|
313
|
+
"_token",
|
314
|
+
None,
|
315
|
+
)
|
316
|
+
|
317
|
+
def __enter__(self) -> None:
|
318
|
+
assert self._token is None, "Context reentrance is not allowed" # nosec: B101
|
319
|
+
object.__setattr__(
|
320
|
+
self,
|
321
|
+
"_token",
|
322
|
+
ContextPresetsRegistryContext._context.set(self._registry),
|
323
|
+
)
|
324
|
+
|
325
|
+
def __exit__(
|
326
|
+
self,
|
327
|
+
exc_type: type[BaseException] | None,
|
328
|
+
exc_val: BaseException | None,
|
329
|
+
exc_tb: TracebackType | None,
|
330
|
+
) -> None:
|
331
|
+
assert self._token is not None, "Unbalanced context enter/exit" # nosec: B101
|
332
|
+
ContextPresetsRegistryContext._context.reset(self._token)
|
333
|
+
object.__setattr__(
|
334
|
+
self,
|
335
|
+
"_token",
|
336
|
+
None,
|
337
|
+
)
|
haiway/context/state.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
from asyncio import iscoroutinefunction
|
2
|
-
from collections.abc import Callable, Coroutine, Iterable, MutableMapping
|
2
|
+
from collections.abc import Callable, Collection, Coroutine, Iterable, MutableMapping
|
3
3
|
from contextvars import ContextVar, Token
|
4
4
|
from threading import Lock
|
5
5
|
from types import TracebackType
|
6
|
-
from typing import Any, Self, cast,
|
6
|
+
from typing import Any, ClassVar, Self, cast, overload
|
7
7
|
|
8
8
|
from haiway.context.types import MissingContext, MissingState
|
9
|
-
from haiway.state import State
|
9
|
+
from haiway.state import Immutable, State
|
10
10
|
from haiway.utils.mimic import mimic_function
|
11
11
|
|
12
12
|
__all__ = (
|
@@ -15,8 +15,7 @@ __all__ = (
|
|
15
15
|
)
|
16
16
|
|
17
17
|
|
18
|
-
|
19
|
-
class ScopeState:
|
18
|
+
class ScopeState(Immutable):
|
20
19
|
"""
|
21
20
|
Container for state objects within a scope.
|
22
21
|
|
@@ -25,47 +24,24 @@ class ScopeState:
|
|
25
24
|
This class is immutable after initialization.
|
26
25
|
"""
|
27
26
|
|
28
|
-
|
29
|
-
|
30
|
-
"_state",
|
31
|
-
)
|
27
|
+
_state: MutableMapping[type[State], State]
|
28
|
+
_lock: Lock
|
32
29
|
|
33
30
|
def __init__(
|
34
31
|
self,
|
35
32
|
state: Iterable[State],
|
36
33
|
) -> None:
|
37
|
-
self._state: MutableMapping[type[State], State]
|
38
34
|
object.__setattr__(
|
39
35
|
self,
|
40
36
|
"_state",
|
41
37
|
{type(element): element for element in state},
|
42
38
|
)
|
43
|
-
self._lock: Lock
|
44
39
|
object.__setattr__(
|
45
40
|
self,
|
46
41
|
"_lock",
|
47
42
|
Lock(),
|
48
43
|
)
|
49
44
|
|
50
|
-
def __setattr__(
|
51
|
-
self,
|
52
|
-
name: str,
|
53
|
-
value: Any,
|
54
|
-
) -> Any:
|
55
|
-
raise AttributeError(
|
56
|
-
f"Can't modify immutable {self.__class__.__qualname__},"
|
57
|
-
f" attribute - '{name}' cannot be modified"
|
58
|
-
)
|
59
|
-
|
60
|
-
def __delattr__(
|
61
|
-
self,
|
62
|
-
name: str,
|
63
|
-
) -> None:
|
64
|
-
raise AttributeError(
|
65
|
-
f"Can't modify immutable {self.__class__.__qualname__},"
|
66
|
-
f" attribute - '{name}' cannot be deleted"
|
67
|
-
)
|
68
|
-
|
69
45
|
def check_state[StateType: State](
|
70
46
|
self,
|
71
47
|
state: type[StateType],
|
@@ -183,20 +159,17 @@ class ScopeState:
|
|
183
159
|
Self
|
184
160
|
A new ScopeState with the combined state
|
185
161
|
"""
|
186
|
-
if state
|
187
|
-
|
188
|
-
|
189
|
-
*self._state.values(),
|
190
|
-
*state,
|
191
|
-
]
|
192
|
-
)
|
193
|
-
|
194
|
-
else:
|
162
|
+
# Fast path: if no new state, return self
|
163
|
+
state_list = list(state)
|
164
|
+
if not state_list:
|
195
165
|
return self
|
196
166
|
|
167
|
+
# Optimize: pre-allocate with known size and use dict comprehension
|
168
|
+
combined_state = {**self._state, **{type(s): s for s in state_list}}
|
169
|
+
return self.__class__(combined_state.values())
|
170
|
+
|
197
171
|
|
198
|
-
|
199
|
-
class StateContext:
|
172
|
+
class StateContext(Immutable):
|
200
173
|
"""
|
201
174
|
Context manager for state within a scope.
|
202
175
|
|
@@ -205,7 +178,26 @@ class StateContext:
|
|
205
178
|
This class is immutable after initialization.
|
206
179
|
"""
|
207
180
|
|
208
|
-
_context = ContextVar[ScopeState]("StateContext")
|
181
|
+
_context: ClassVar[ContextVar[ScopeState]] = ContextVar[ScopeState]("StateContext")
|
182
|
+
|
183
|
+
@classmethod
|
184
|
+
def current_state(cls) -> Collection[State]:
|
185
|
+
"""
|
186
|
+
Return an immutable snapshot of the current state.
|
187
|
+
|
188
|
+
Returns
|
189
|
+
-------
|
190
|
+
Collection[State]
|
191
|
+
State objects present in the current context,
|
192
|
+
or an empty tuple if no context is active.
|
193
|
+
"""
|
194
|
+
try:
|
195
|
+
scope_state: ScopeState = cls._context.get()
|
196
|
+
with scope_state._lock:
|
197
|
+
return tuple(scope_state._state.values())
|
198
|
+
|
199
|
+
except LookupError:
|
200
|
+
return () # return empty as default
|
209
201
|
|
210
202
|
@classmethod
|
211
203
|
def check_state[StateType: State](
|
@@ -306,51 +298,13 @@ class StateContext:
|
|
306
298
|
"""
|
307
299
|
try:
|
308
300
|
# update current scope context
|
309
|
-
return cls(
|
301
|
+
return cls(_state=cls._context.get().updated(state=state))
|
310
302
|
|
311
303
|
except LookupError: # create root scope when missing
|
312
|
-
return cls(
|
304
|
+
return cls(_state=ScopeState(state))
|
313
305
|
|
314
|
-
|
315
|
-
|
316
|
-
"_token",
|
317
|
-
)
|
318
|
-
|
319
|
-
def __init__(
|
320
|
-
self,
|
321
|
-
state: ScopeState,
|
322
|
-
) -> None:
|
323
|
-
self._state: ScopeState
|
324
|
-
object.__setattr__(
|
325
|
-
self,
|
326
|
-
"_state",
|
327
|
-
state,
|
328
|
-
)
|
329
|
-
self._token: Token[ScopeState] | None
|
330
|
-
object.__setattr__(
|
331
|
-
self,
|
332
|
-
"_token",
|
333
|
-
None,
|
334
|
-
)
|
335
|
-
|
336
|
-
def __setattr__(
|
337
|
-
self,
|
338
|
-
name: str,
|
339
|
-
value: Any,
|
340
|
-
) -> Any:
|
341
|
-
raise AttributeError(
|
342
|
-
f"Can't modify immutable {self.__class__.__qualname__},"
|
343
|
-
f" attribute - '{name}' cannot be modified"
|
344
|
-
)
|
345
|
-
|
346
|
-
def __delattr__(
|
347
|
-
self,
|
348
|
-
name: str,
|
349
|
-
) -> None:
|
350
|
-
raise AttributeError(
|
351
|
-
f"Can't modify immutable {self.__class__.__qualname__},"
|
352
|
-
f" attribute - '{name}' cannot be deleted"
|
353
|
-
)
|
306
|
+
_state: ScopeState
|
307
|
+
_token: Token[ScopeState] | None = None
|
354
308
|
|
355
309
|
def __enter__(self) -> None:
|
356
310
|
"""
|
haiway/context/tasks.py
CHANGED
@@ -2,13 +2,14 @@ from asyncio import Task, TaskGroup, get_event_loop
|
|
2
2
|
from collections.abc import Callable, Coroutine
|
3
3
|
from contextvars import ContextVar, Token, copy_context
|
4
4
|
from types import TracebackType
|
5
|
-
from typing import Any,
|
5
|
+
from typing import Any, ClassVar
|
6
|
+
|
7
|
+
from haiway.state import Immutable
|
6
8
|
|
7
9
|
__all__ = ("TaskGroupContext",)
|
8
10
|
|
9
11
|
|
10
|
-
|
11
|
-
class TaskGroupContext:
|
12
|
+
class TaskGroupContext(Immutable):
|
12
13
|
"""
|
13
14
|
Context manager for managing task groups within a scope.
|
14
15
|
|
@@ -17,7 +18,7 @@ class TaskGroupContext:
|
|
17
18
|
This class is immutable after initialization.
|
18
19
|
"""
|
19
20
|
|
20
|
-
_context = ContextVar[TaskGroup]("TaskGroupContext")
|
21
|
+
_context: ClassVar[ContextVar[TaskGroup]] = ContextVar[TaskGroup]("TaskGroupContext")
|
21
22
|
|
22
23
|
@classmethod
|
23
24
|
def run[Result, **Arguments](
|
@@ -59,10 +60,8 @@ class TaskGroupContext:
|
|
59
60
|
context=copy_context(),
|
60
61
|
)
|
61
62
|
|
62
|
-
|
63
|
-
|
64
|
-
"_token",
|
65
|
-
)
|
63
|
+
_group: TaskGroup
|
64
|
+
_token: Token[TaskGroup] | None = None
|
66
65
|
|
67
66
|
def __init__(
|
68
67
|
self,
|
@@ -76,13 +75,11 @@ class TaskGroupContext:
|
|
76
75
|
task_group: TaskGroup | None
|
77
76
|
The task group to use, or None to create a new one
|
78
77
|
"""
|
79
|
-
self._group: TaskGroup
|
80
78
|
object.__setattr__(
|
81
79
|
self,
|
82
80
|
"_group",
|
83
81
|
task_group if task_group is not None else TaskGroup(),
|
84
82
|
)
|
85
|
-
self._token: Token[TaskGroup] | None
|
86
83
|
object.__setattr__(
|
87
84
|
self,
|
88
85
|
"_token",
|
haiway/helpers/concurrent.py
CHANGED
@@ -130,9 +130,9 @@ async def process_concurrently[Element]( # noqa: C901, PLR0912
|
|
130
130
|
|
131
131
|
@overload
|
132
132
|
async def execute_concurrently[Element, Result](
|
133
|
-
source: Collection[Element],
|
134
|
-
/,
|
135
133
|
handler: Callable[[Element], Coroutine[Any, Any, Result]],
|
134
|
+
/,
|
135
|
+
elements: Collection[Element],
|
136
136
|
*,
|
137
137
|
concurrent_tasks: int = 2,
|
138
138
|
) -> Sequence[Result]: ...
|
@@ -140,9 +140,9 @@ async def execute_concurrently[Element, Result](
|
|
140
140
|
|
141
141
|
@overload
|
142
142
|
async def execute_concurrently[Element, Result](
|
143
|
-
source: Collection[Element],
|
144
|
-
/,
|
145
143
|
handler: Callable[[Element], Coroutine[Any, Any, Result]],
|
144
|
+
/,
|
145
|
+
elements: Collection[Element],
|
146
146
|
*,
|
147
147
|
concurrent_tasks: int = 2,
|
148
148
|
return_exceptions: Literal[True],
|
@@ -150,9 +150,9 @@ async def execute_concurrently[Element, Result](
|
|
150
150
|
|
151
151
|
|
152
152
|
async def execute_concurrently[Element, Result]( # noqa: C901
|
153
|
-
source: Collection[Element],
|
154
|
-
/,
|
155
153
|
handler: Callable[[Element], Coroutine[Any, Any, Result]],
|
154
|
+
/,
|
155
|
+
elements: Collection[Element],
|
156
156
|
*,
|
157
157
|
concurrent_tasks: int = 2,
|
158
158
|
return_exceptions: bool = False,
|
@@ -173,11 +173,11 @@ async def execute_concurrently[Element, Result]( # noqa: C901
|
|
173
173
|
|
174
174
|
Parameters
|
175
175
|
----------
|
176
|
-
source : Collection[Element]
|
177
|
-
A collection of elements to process. The collection size determines
|
178
|
-
the result sequence length.
|
179
176
|
handler : Callable[[Element], Coroutine[Any, Any, Result]]
|
180
177
|
A coroutine function that processes each element and returns a result.
|
178
|
+
elements : Collection[Element]
|
179
|
+
A collection of elements to process. The collection size determines
|
180
|
+
the result sequence length.
|
181
181
|
concurrent_tasks : int, default=2
|
182
182
|
Maximum number of concurrent tasks. Must be greater than 0. Higher
|
183
183
|
values allow more parallelism but consume more resources.
|
@@ -206,16 +206,16 @@ async def execute_concurrently[Element, Result]( # noqa: C901
|
|
206
206
|
...
|
207
207
|
>>> urls = ["http://api.example.com/1", "http://api.example.com/2"]
|
208
208
|
>>> results = await execute_concurrently(
|
209
|
-
... urls,
|
210
209
|
... fetch_data,
|
210
|
+
... urls,
|
211
211
|
... concurrent_tasks=10
|
212
212
|
... )
|
213
213
|
>>> # results[0] corresponds to urls[0], results[1] to urls[1], etc.
|
214
214
|
|
215
215
|
>>> # With exception handling
|
216
216
|
>>> results = await execute_concurrently(
|
217
|
-
... urls,
|
218
217
|
... fetch_data,
|
218
|
+
... urls,
|
219
219
|
... concurrent_tasks=10,
|
220
220
|
... return_exceptions=True
|
221
221
|
... )
|
@@ -230,7 +230,7 @@ async def execute_concurrently[Element, Result]( # noqa: C901
|
|
230
230
|
running: set[Task[Result]] = set()
|
231
231
|
results: MutableSequence[Task[Result]] = []
|
232
232
|
try:
|
233
|
-
for element in
|
233
|
+
for element in elements:
|
234
234
|
task: Task[Result] = ctx.spawn(handler, element)
|
235
235
|
results.append(task)
|
236
236
|
running.add(task)
|
haiway/helpers/observability.py
CHANGED
@@ -243,7 +243,7 @@ def LoggerObservability( # noqa: C901, PLR0915
|
|
243
243
|
nonlocal root_logger
|
244
244
|
if root_scope is None:
|
245
245
|
root_scope = scope
|
246
|
-
root_logger = logger or getLogger(scope.
|
246
|
+
root_logger = logger or getLogger(scope.name)
|
247
247
|
|
248
248
|
else:
|
249
249
|
scopes[scope.parent_id].nested.append(scope_store)
|
@@ -251,7 +251,7 @@ def LoggerObservability( # noqa: C901, PLR0915
|
|
251
251
|
assert root_logger is not None # nosec: B101
|
252
252
|
root_logger.log(
|
253
253
|
ObservabilityLevel.INFO,
|
254
|
-
f"[{trace_id_hex}] {scope.unique_name} Entering scope: {scope.
|
254
|
+
f"[{trace_id_hex}] {scope.unique_name} Entering scope: {scope.name}",
|
255
255
|
)
|
256
256
|
|
257
257
|
def scope_exiting(
|
@@ -274,7 +274,7 @@ def LoggerObservability( # noqa: C901, PLR0915
|
|
274
274
|
|
275
275
|
root_logger.log(
|
276
276
|
ObservabilityLevel.INFO,
|
277
|
-
f"[{trace_id_hex}] {scope.unique_name} Exiting scope: {scope.
|
277
|
+
f"[{trace_id_hex}] {scope.unique_name} Exiting scope: {scope.name}",
|
278
278
|
)
|
279
279
|
metric_str: str = f"Metric - scope_time:{scopes[scope.scope_id].time:.3f}s"
|
280
280
|
if debug_context: # store only for summary
|
@@ -332,9 +332,7 @@ def _tree_summary(scope_store: ScopeStore) -> str:
|
|
332
332
|
str
|
333
333
|
A formatted string representation of the scope hierarchy with recorded events
|
334
334
|
"""
|
335
|
-
elements: list[str] = [
|
336
|
-
f"┍━ {scope_store.identifier.label} [{scope_store.identifier.scope_id}]:"
|
337
|
-
]
|
335
|
+
elements: list[str] = [f"┍━ {scope_store.identifier.name} [{scope_store.identifier.scope_id}]:"]
|
338
336
|
for element in scope_store.store:
|
339
337
|
if not element:
|
340
338
|
continue # skip empty
|