haiway 0.21.4__py3-none-any.whl → 0.22.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 -0
- haiway/context/access.py +113 -6
- haiway/context/disposables.py +230 -37
- haiway/context/state.py +86 -12
- haiway/helpers/__init__.py +3 -0
- haiway/helpers/asynchrony.py +4 -6
- haiway/helpers/files.py +421 -0
- haiway/state/structure.py +8 -0
- haiway/types/missing.py +3 -0
- {haiway-0.21.4.dist-info → haiway-0.22.0.dist-info}/METADATA +2 -2
- {haiway-0.21.4.dist-info → haiway-0.22.0.dist-info}/RECORD +13 -12
- {haiway-0.21.4.dist-info → haiway-0.22.0.dist-info}/WHEEL +0 -0
- {haiway-0.21.4.dist-info → haiway-0.22.0.dist-info}/licenses/LICENSE +0 -0
haiway/__init__.py
CHANGED
@@ -13,6 +13,8 @@ from haiway.context import (
|
|
13
13
|
ctx,
|
14
14
|
)
|
15
15
|
from haiway.helpers import (
|
16
|
+
File,
|
17
|
+
FileAccess,
|
16
18
|
LoggerObservability,
|
17
19
|
asynchronous,
|
18
20
|
cache,
|
@@ -63,6 +65,8 @@ __all__ = (
|
|
63
65
|
"DefaultValue",
|
64
66
|
"Disposable",
|
65
67
|
"Disposables",
|
68
|
+
"File",
|
69
|
+
"FileAccess",
|
66
70
|
"LoggerObservability",
|
67
71
|
"Missing",
|
68
72
|
"MissingContext",
|
haiway/context/access.py
CHANGED
@@ -169,8 +169,8 @@ class ScopeContext:
|
|
169
169
|
StateContext(
|
170
170
|
state=ScopeState(
|
171
171
|
(
|
172
|
-
*await disposables.__aenter__(),
|
173
172
|
*self._state_context._state._state.values(),
|
173
|
+
*await disposables.prepare(),
|
174
174
|
)
|
175
175
|
),
|
176
176
|
),
|
@@ -187,7 +187,7 @@ class ScopeContext:
|
|
187
187
|
exc_tb: TracebackType | None,
|
188
188
|
) -> None:
|
189
189
|
if disposables := self._disposables:
|
190
|
-
await disposables.
|
190
|
+
await disposables.dispose(
|
191
191
|
exc_type=exc_type,
|
192
192
|
exc_val=exc_val,
|
193
193
|
exc_tb=exc_tb,
|
@@ -382,6 +382,48 @@ class ctx:
|
|
382
382
|
|
383
383
|
return StateContext.updated(element for element in state if element is not None)
|
384
384
|
|
385
|
+
@staticmethod
|
386
|
+
def disposables(
|
387
|
+
*disposables: Disposable | None,
|
388
|
+
) -> Disposables:
|
389
|
+
"""
|
390
|
+
Create a container for managing multiple disposable resources.
|
391
|
+
|
392
|
+
Disposables are async context managers that can provide state objects and
|
393
|
+
require proper cleanup. This method creates a Disposables container that
|
394
|
+
manages multiple disposable resources as a single unit, handling their
|
395
|
+
lifecycle and state propagation.
|
396
|
+
|
397
|
+
Parameters
|
398
|
+
----------
|
399
|
+
*disposables: Disposable | None
|
400
|
+
Variable number of disposable resources to be managed together.
|
401
|
+
None values are filtered out automatically.
|
402
|
+
|
403
|
+
Returns
|
404
|
+
-------
|
405
|
+
Disposables
|
406
|
+
A container that manages the lifecycle of all provided disposables
|
407
|
+
and propagates their state to the context when used with ctx.scope()
|
408
|
+
|
409
|
+
Examples
|
410
|
+
--------
|
411
|
+
Using disposables with database connections:
|
412
|
+
|
413
|
+
>>> from haiway import ctx
|
414
|
+
>>> async def main():
|
415
|
+
...
|
416
|
+
... async with ctx.scope(
|
417
|
+
... "database_work",
|
418
|
+
... disposables=(database_connection(),)
|
419
|
+
... ):
|
420
|
+
... # ConnectionState is now available in context
|
421
|
+
... conn_state = ctx.state(ConnectionState)
|
422
|
+
... await conn_state.connection.execute("SELECT 1")
|
423
|
+
"""
|
424
|
+
|
425
|
+
return Disposables(*(disposable for disposable in disposables if disposable is not None))
|
426
|
+
|
385
427
|
@staticmethod
|
386
428
|
def spawn[Result, **Arguments](
|
387
429
|
function: Callable[Arguments, Coroutine[Any, Any, Result]],
|
@@ -493,6 +535,29 @@ class ctx:
|
|
493
535
|
else:
|
494
536
|
raise RuntimeError("Attempting to cancel context out of asyncio task")
|
495
537
|
|
538
|
+
@staticmethod
|
539
|
+
def check_state[StateType: State](
|
540
|
+
state: type[StateType],
|
541
|
+
/,
|
542
|
+
) -> bool:
|
543
|
+
"""
|
544
|
+
Check if state object is available in the current context.
|
545
|
+
|
546
|
+
Verifies if state object of the specified type is available the current context.
|
547
|
+
Instantiates requested state if needed and possible.
|
548
|
+
|
549
|
+
Parameters
|
550
|
+
----------
|
551
|
+
state: type[StateType]
|
552
|
+
The type of state to check
|
553
|
+
|
554
|
+
Returns
|
555
|
+
-------
|
556
|
+
bool
|
557
|
+
True if state is available, otherwise False.
|
558
|
+
"""
|
559
|
+
return StateContext.check_state(state)
|
560
|
+
|
496
561
|
@staticmethod
|
497
562
|
def state[StateType: State](
|
498
563
|
state: type[StateType],
|
@@ -500,18 +565,60 @@ class ctx:
|
|
500
565
|
default: StateType | None = None,
|
501
566
|
) -> StateType:
|
502
567
|
"""
|
503
|
-
Access current scope context
|
504
|
-
|
568
|
+
Access state from the current scope context by its type.
|
569
|
+
|
570
|
+
Retrieves state objects that have been propagated within the current execution context.
|
571
|
+
State objects are automatically made available through context scopes and disposables.
|
572
|
+
If no matching state is found, creates a default instance if possible.
|
505
573
|
|
506
574
|
Parameters
|
507
575
|
----------
|
508
576
|
state: type[StateType]
|
509
|
-
type
|
577
|
+
The State class type to retrieve from the current context
|
578
|
+
default: StateType | None, default=None
|
579
|
+
Optional default instance to return if state is not found in context.
|
580
|
+
If None and no state is found, a new instance will be created if possible.
|
510
581
|
|
511
582
|
Returns
|
512
583
|
-------
|
513
584
|
StateType
|
514
|
-
|
585
|
+
The state instance from the current context or a default/new instance
|
586
|
+
|
587
|
+
Raises
|
588
|
+
------
|
589
|
+
RuntimeError
|
590
|
+
If called outside of any scope context
|
591
|
+
TypeError
|
592
|
+
If no state is found and no default can be created
|
593
|
+
|
594
|
+
Examples
|
595
|
+
--------
|
596
|
+
Accessing configuration state:
|
597
|
+
|
598
|
+
>>> from haiway import ctx, State
|
599
|
+
>>>
|
600
|
+
>>> class ApiConfig(State):
|
601
|
+
... base_url: str = "https://api.example.com"
|
602
|
+
... timeout: int = 30
|
603
|
+
>>>
|
604
|
+
>>> async def fetch_data():
|
605
|
+
... config = ctx.state(ApiConfig)
|
606
|
+
... # Use config.base_url and config.timeout
|
607
|
+
>>>
|
608
|
+
>>> async with ctx.scope("api", ApiConfig(base_url="https://custom.api.com")):
|
609
|
+
... await fetch_data() # Uses custom config
|
610
|
+
|
611
|
+
Accessing state with default:
|
612
|
+
|
613
|
+
>>> cache_config = ctx.state(CacheConfig, default=CacheConfig(ttl=3600))
|
614
|
+
|
615
|
+
Within service classes:
|
616
|
+
|
617
|
+
>>> class UserService(State):
|
618
|
+
... @classmethod
|
619
|
+
... async def get_user(cls, user_id: str) -> User:
|
620
|
+
... config = ctx.state(DatabaseConfig)
|
621
|
+
... # Use config to connect to database
|
515
622
|
"""
|
516
623
|
return StateContext.state(
|
517
624
|
state,
|
haiway/context/disposables.py
CHANGED
@@ -1,11 +1,20 @@
|
|
1
|
-
from asyncio import
|
2
|
-
|
1
|
+
from asyncio import (
|
2
|
+
AbstractEventLoop,
|
3
|
+
gather,
|
4
|
+
get_running_loop,
|
5
|
+
iscoroutinefunction,
|
6
|
+
run_coroutine_threadsafe,
|
7
|
+
wrap_future,
|
8
|
+
)
|
9
|
+
from collections.abc import Callable, Coroutine, Iterable
|
3
10
|
from contextlib import AbstractAsyncContextManager
|
4
11
|
from itertools import chain
|
5
12
|
from types import TracebackType
|
6
13
|
from typing import Any, final
|
7
14
|
|
15
|
+
from haiway.context.state import ScopeState, StateContext
|
8
16
|
from haiway.state import State
|
17
|
+
from haiway.utils.mimic import mimic_function
|
9
18
|
|
10
19
|
__all__ = (
|
11
20
|
"Disposable",
|
@@ -14,10 +23,37 @@ __all__ = (
|
|
14
23
|
|
15
24
|
type Disposable = AbstractAsyncContextManager[Iterable[State] | State | None]
|
16
25
|
"""
|
17
|
-
A type alias for asynchronous context managers that
|
26
|
+
A type alias for asynchronous context managers that provide disposable resources.
|
18
27
|
|
19
28
|
Represents an asynchronous resource that needs proper cleanup when no longer needed.
|
20
|
-
When entered, it may return State instances that will be propagated
|
29
|
+
When entered, it may return State instances that will be automatically propagated
|
30
|
+
to the current context. The resource is guaranteed to be properly disposed of
|
31
|
+
when the context exits, even if exceptions occur.
|
32
|
+
|
33
|
+
Type Details
|
34
|
+
------------
|
35
|
+
- Must be an async context manager (implements __aenter__ and __aexit__)
|
36
|
+
- Can return None, a single State instance, or multiple State instances
|
37
|
+
- State instances are automatically added to the context scope
|
38
|
+
- Cleanup is handled automatically when the context exits
|
39
|
+
|
40
|
+
Examples
|
41
|
+
--------
|
42
|
+
Creating a disposable database connection:
|
43
|
+
|
44
|
+
>>> import contextlib
|
45
|
+
>>> from haiway import State
|
46
|
+
>>>
|
47
|
+
>>> class DatabaseState(State):
|
48
|
+
... connection: DatabaseConnection
|
49
|
+
...
|
50
|
+
>>> @contextlib.asynccontextmanager
|
51
|
+
>>> async def database_disposable():
|
52
|
+
... connection = await create_database_connection()
|
53
|
+
... try:
|
54
|
+
... yield DatabaseState(connection=connection)
|
55
|
+
... finally:
|
56
|
+
... await connection.close()
|
21
57
|
"""
|
22
58
|
|
23
59
|
|
@@ -27,14 +63,60 @@ class Disposables:
|
|
27
63
|
A container for multiple Disposable resources that manages their lifecycle.
|
28
64
|
|
29
65
|
This class provides a way to handle multiple disposable resources as a single unit,
|
30
|
-
entering all of them when the container is entered and exiting all of
|
31
|
-
the container is exited. Any states returned by the disposables are
|
32
|
-
and
|
66
|
+
entering all of them in parallel when the container is entered and exiting all of
|
67
|
+
them when the container is exited. Any states returned by the disposables are
|
68
|
+
collected and automatically propagated to the context.
|
69
|
+
|
70
|
+
Key Features
|
71
|
+
------------
|
72
|
+
- Parallel setup and cleanup of all contained disposables
|
73
|
+
- Automatic state collection and context propagation
|
74
|
+
- Thread-safe cross-event-loop disposal
|
75
|
+
- Exception handling with BaseExceptionGroup for multiple failures
|
76
|
+
- Immutable after initialization
|
77
|
+
|
78
|
+
The class is designed to work seamlessly with ctx.scope() and ensures proper
|
79
|
+
resource cleanup even when exceptions occur during setup or teardown.
|
33
80
|
|
34
|
-
|
81
|
+
Examples
|
82
|
+
--------
|
83
|
+
Creating and using multiple disposables:
|
84
|
+
|
85
|
+
>>> from haiway import ctx
|
86
|
+
>>> async def main():
|
87
|
+
... disposables = Disposables(
|
88
|
+
... database_disposable(),
|
89
|
+
... cache_disposable()
|
90
|
+
... )
|
91
|
+
...
|
92
|
+
... async with ctx.scope("app", disposables=disposables):
|
93
|
+
... # Both DatabaseState and CacheState are available
|
94
|
+
... db = ctx.state(DatabaseState)
|
95
|
+
... cache = ctx.state(CacheState)
|
96
|
+
|
97
|
+
Direct context manager usage:
|
98
|
+
|
99
|
+
>>> async def process_data():
|
100
|
+
... disposables = Disposables(
|
101
|
+
... create_temp_file_disposable(),
|
102
|
+
... create_network_connection_disposable()
|
103
|
+
... )
|
104
|
+
...
|
105
|
+
... async with disposables:
|
106
|
+
... # Resources are set up in parallel
|
107
|
+
... temp_file = ctx.state(TempFileState)
|
108
|
+
... network = ctx.state(NetworkState)
|
109
|
+
...
|
110
|
+
... # Process data using both resources
|
111
|
+
...
|
112
|
+
... # All resources cleaned up automatically
|
35
113
|
"""
|
36
114
|
|
37
|
-
__slots__ = (
|
115
|
+
__slots__ = (
|
116
|
+
"_disposables",
|
117
|
+
"_loop",
|
118
|
+
"_state_context",
|
119
|
+
)
|
38
120
|
|
39
121
|
def __init__(
|
40
122
|
self,
|
@@ -54,6 +136,18 @@ class Disposables:
|
|
54
136
|
"_disposables",
|
55
137
|
disposables,
|
56
138
|
)
|
139
|
+
self._state_context: StateContext | None
|
140
|
+
object.__setattr__(
|
141
|
+
self,
|
142
|
+
"_state_context",
|
143
|
+
None,
|
144
|
+
)
|
145
|
+
self._loop: AbstractEventLoop | None
|
146
|
+
object.__setattr__(
|
147
|
+
self,
|
148
|
+
"_loop",
|
149
|
+
None,
|
150
|
+
)
|
57
151
|
|
58
152
|
def __setattr__(
|
59
153
|
self,
|
@@ -85,7 +179,7 @@ class Disposables:
|
|
85
179
|
"""
|
86
180
|
return len(self._disposables) > 0
|
87
181
|
|
88
|
-
async def
|
182
|
+
async def _setup(
|
89
183
|
self,
|
90
184
|
disposable: Disposable,
|
91
185
|
/,
|
@@ -100,25 +194,106 @@ class Disposables:
|
|
100
194
|
case multiple:
|
101
195
|
return multiple
|
102
196
|
|
103
|
-
async def
|
197
|
+
async def prepare(self) -> Iterable[State]:
|
198
|
+
assert self._loop is None # nosec: B101
|
199
|
+
object.__setattr__(
|
200
|
+
self,
|
201
|
+
"_loop",
|
202
|
+
get_running_loop(),
|
203
|
+
)
|
204
|
+
return [
|
205
|
+
*chain.from_iterable(
|
206
|
+
await gather(
|
207
|
+
*[self._setup(disposable) for disposable in self._disposables],
|
208
|
+
)
|
209
|
+
)
|
210
|
+
]
|
211
|
+
|
212
|
+
async def __aenter__(self) -> None:
|
104
213
|
"""
|
105
214
|
Enter all contained disposables asynchronously.
|
106
215
|
|
107
|
-
Enters all disposables in parallel and collects any State objects they return
|
108
|
-
|
109
|
-
Returns
|
110
|
-
-------
|
111
|
-
Iterable[State]
|
112
|
-
Collection of State objects from all disposables.
|
216
|
+
Enters all disposables in parallel and collects any State objects they return updating
|
217
|
+
current state context.
|
113
218
|
"""
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
219
|
+
|
220
|
+
assert self._state_context is None, "Context reentrance is not allowed" # nosec: B101
|
221
|
+
state_context = StateContext(state=ScopeState(await self.prepare()))
|
222
|
+
state_context.__enter__()
|
223
|
+
object.__setattr__(
|
224
|
+
self,
|
225
|
+
"_state_context",
|
226
|
+
state_context,
|
227
|
+
)
|
228
|
+
|
229
|
+
async def _cleanup(
|
230
|
+
self,
|
231
|
+
/,
|
232
|
+
exc_type: type[BaseException] | None,
|
233
|
+
exc_val: BaseException | None,
|
234
|
+
exc_tb: TracebackType | None,
|
235
|
+
) -> list[bool | BaseException | None]:
|
236
|
+
return await gather(
|
237
|
+
*[
|
238
|
+
disposable.__aexit__(
|
239
|
+
exc_type,
|
240
|
+
exc_val,
|
241
|
+
exc_tb,
|
119
242
|
)
|
243
|
+
for disposable in self._disposables
|
244
|
+
],
|
245
|
+
return_exceptions=True,
|
246
|
+
)
|
247
|
+
|
248
|
+
async def dispose(
|
249
|
+
self,
|
250
|
+
/,
|
251
|
+
exc_type: type[BaseException] | None = None,
|
252
|
+
exc_val: BaseException | None = None,
|
253
|
+
exc_tb: TracebackType | None = None,
|
254
|
+
) -> None:
|
255
|
+
assert self._loop is not None # nosec: B101
|
256
|
+
results: list[bool | BaseException | None]
|
257
|
+
|
258
|
+
try:
|
259
|
+
current_loop: AbstractEventLoop = get_running_loop()
|
260
|
+
if self._loop != current_loop:
|
261
|
+
results = await wrap_future(
|
262
|
+
run_coroutine_threadsafe(
|
263
|
+
self._cleanup(
|
264
|
+
exc_type,
|
265
|
+
exc_val,
|
266
|
+
exc_tb,
|
267
|
+
),
|
268
|
+
loop=self._loop,
|
269
|
+
)
|
270
|
+
)
|
271
|
+
|
272
|
+
else:
|
273
|
+
results = await self._cleanup(
|
274
|
+
exc_type,
|
275
|
+
exc_val,
|
276
|
+
exc_tb,
|
277
|
+
)
|
278
|
+
|
279
|
+
finally:
|
280
|
+
object.__setattr__(
|
281
|
+
self,
|
282
|
+
"_loop",
|
283
|
+
None,
|
120
284
|
)
|
121
|
-
|
285
|
+
|
286
|
+
exceptions: list[BaseException] = [exc for exc in results if isinstance(exc, BaseException)]
|
287
|
+
|
288
|
+
match len(exceptions):
|
289
|
+
case 0:
|
290
|
+
return
|
291
|
+
|
292
|
+
case 1:
|
293
|
+
raise exceptions[0]
|
294
|
+
|
295
|
+
case _:
|
296
|
+
raise BaseExceptionGroup("Disposables cleanup errors", exceptions)
|
122
297
|
|
123
298
|
async def __aexit__(
|
124
299
|
self,
|
@@ -146,19 +321,37 @@ class Disposables:
|
|
146
321
|
BaseExceptionGroup
|
147
322
|
If multiple disposables raise exceptions during exit
|
148
323
|
"""
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
324
|
+
assert self._state_context is not None, "Unbalanced context enter/exit" # nosec: B101
|
325
|
+
try:
|
326
|
+
self._state_context.__exit__(
|
327
|
+
exc_type,
|
328
|
+
exc_val,
|
329
|
+
exc_tb,
|
330
|
+
)
|
331
|
+
object.__setattr__(
|
332
|
+
self,
|
333
|
+
"_state_context",
|
334
|
+
None,
|
335
|
+
)
|
160
336
|
|
161
|
-
|
337
|
+
finally:
|
338
|
+
await self.dispose(
|
339
|
+
exc_type,
|
340
|
+
exc_val,
|
341
|
+
exc_tb,
|
342
|
+
)
|
343
|
+
|
344
|
+
def __call__[Result, **Arguments](
|
345
|
+
self,
|
346
|
+
function: Callable[Arguments, Coroutine[Any, Any, Result]],
|
347
|
+
) -> Callable[Arguments, Coroutine[Any, Any, Result]]:
|
348
|
+
assert iscoroutinefunction(function) # nosec: B101
|
349
|
+
|
350
|
+
async def async_context(
|
351
|
+
*args: Arguments.args,
|
352
|
+
**kwargs: Arguments.kwargs,
|
353
|
+
) -> Result:
|
354
|
+
async with self:
|
355
|
+
return await function(*args, **kwargs)
|
162
356
|
|
163
|
-
|
164
|
-
raise BaseExceptionGroup("Disposing errors", exceptions)
|
357
|
+
return mimic_function(function, within=async_context)
|
haiway/context/state.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
from asyncio import iscoroutinefunction
|
2
2
|
from collections.abc import Callable, Coroutine, Iterable, MutableMapping
|
3
3
|
from contextvars import ContextVar, Token
|
4
|
+
from threading import Lock
|
4
5
|
from types import TracebackType
|
5
6
|
from typing import Any, Self, cast, final, overload
|
6
7
|
|
@@ -24,7 +25,7 @@ class ScopeState:
|
|
24
25
|
This class is immutable after initialization.
|
25
26
|
"""
|
26
27
|
|
27
|
-
__slots__ = ("_state"
|
28
|
+
__slots__ = ("_lock", "_state")
|
28
29
|
|
29
30
|
def __init__(
|
30
31
|
self,
|
@@ -36,6 +37,12 @@ class ScopeState:
|
|
36
37
|
"_state",
|
37
38
|
{type(element): element for element in state},
|
38
39
|
)
|
40
|
+
self._lock: Lock
|
41
|
+
object.__setattr__(
|
42
|
+
self,
|
43
|
+
"_lock",
|
44
|
+
Lock(),
|
45
|
+
)
|
39
46
|
|
40
47
|
def __setattr__(
|
41
48
|
self,
|
@@ -56,6 +63,43 @@ class ScopeState:
|
|
56
63
|
f" attribute - '{name}' cannot be deleted"
|
57
64
|
)
|
58
65
|
|
66
|
+
def check_state[StateType: State](
|
67
|
+
self,
|
68
|
+
state: type[StateType],
|
69
|
+
/,
|
70
|
+
) -> bool:
|
71
|
+
"""
|
72
|
+
Check state object availability by its type.
|
73
|
+
|
74
|
+
If the state type is not found, attempts to instantiate a new instance of\
|
75
|
+
the type if possible.
|
76
|
+
|
77
|
+
Parameters
|
78
|
+
----------
|
79
|
+
state: type[StateType]
|
80
|
+
The type of state to check
|
81
|
+
|
82
|
+
Returns
|
83
|
+
-------
|
84
|
+
bool
|
85
|
+
True if state is available, otherwise False.
|
86
|
+
"""
|
87
|
+
if state in self._state:
|
88
|
+
return True
|
89
|
+
|
90
|
+
else:
|
91
|
+
with self._lock:
|
92
|
+
if state in self._state:
|
93
|
+
return True
|
94
|
+
|
95
|
+
try:
|
96
|
+
initialized: StateType = state()
|
97
|
+
self._state[state] = initialized
|
98
|
+
return True
|
99
|
+
|
100
|
+
except BaseException:
|
101
|
+
return False # unavailable, we don't care the exception
|
102
|
+
|
59
103
|
def state[StateType: State](
|
60
104
|
self,
|
61
105
|
state: type[StateType],
|
@@ -93,17 +137,20 @@ class ScopeState:
|
|
93
137
|
return default
|
94
138
|
|
95
139
|
else:
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
140
|
+
with self._lock:
|
141
|
+
if state in self._state:
|
142
|
+
return cast(StateType, self._state[state])
|
143
|
+
|
144
|
+
try:
|
145
|
+
initialized: StateType = state()
|
146
|
+
self._state[state] = initialized
|
147
|
+
return initialized
|
148
|
+
|
149
|
+
except Exception as exc:
|
150
|
+
raise MissingState(
|
151
|
+
f"{state.__qualname__} is not defined in current scope"
|
152
|
+
" and failed to provide a default value"
|
153
|
+
) from exc
|
107
154
|
|
108
155
|
def updated(
|
109
156
|
self,
|
@@ -149,6 +196,33 @@ class StateContext:
|
|
149
196
|
|
150
197
|
_context = ContextVar[ScopeState]("StateContext")
|
151
198
|
|
199
|
+
@classmethod
|
200
|
+
def check_state[StateType: State](
|
201
|
+
cls,
|
202
|
+
state: type[StateType],
|
203
|
+
/,
|
204
|
+
) -> bool:
|
205
|
+
"""
|
206
|
+
Check if state object is available in the current context.
|
207
|
+
|
208
|
+
Verifies if state object of the specified type is available the current context.
|
209
|
+
|
210
|
+
Parameters
|
211
|
+
----------
|
212
|
+
state: type[StateType]
|
213
|
+
The type of state to check
|
214
|
+
|
215
|
+
Returns
|
216
|
+
-------
|
217
|
+
bool
|
218
|
+
True if state is available, otherwise False.
|
219
|
+
"""
|
220
|
+
try:
|
221
|
+
return cls._context.get().check_state(state)
|
222
|
+
|
223
|
+
except LookupError:
|
224
|
+
return False # no context no state
|
225
|
+
|
152
226
|
@classmethod
|
153
227
|
def state[StateType: State](
|
154
228
|
cls,
|
haiway/helpers/__init__.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
from haiway.helpers.asynchrony import asynchronous
|
2
2
|
from haiway.helpers.caching import CacheMakeKey, CacheRead, CacheWrite, cache
|
3
3
|
from haiway.helpers.concurrent import process_concurrently
|
4
|
+
from haiway.helpers.files import File, FileAccess
|
4
5
|
from haiway.helpers.observability import LoggerObservability
|
5
6
|
from haiway.helpers.retries import retry
|
6
7
|
from haiway.helpers.throttling import throttle
|
@@ -11,6 +12,8 @@ __all__ = (
|
|
11
12
|
"CacheMakeKey",
|
12
13
|
"CacheRead",
|
13
14
|
"CacheWrite",
|
15
|
+
"File",
|
16
|
+
"FileAccess",
|
14
17
|
"LoggerObservability",
|
15
18
|
"asynchronous",
|
16
19
|
"cache",
|
haiway/helpers/asynchrony.py
CHANGED
@@ -11,12 +11,10 @@ __all__ = ("asynchronous",)
|
|
11
11
|
|
12
12
|
|
13
13
|
@overload
|
14
|
-
def asynchronous[**Args, Result]() ->
|
15
|
-
Callable[
|
16
|
-
|
17
|
-
|
18
|
-
]
|
19
|
-
): ...
|
14
|
+
def asynchronous[**Args, Result]() -> Callable[
|
15
|
+
[Callable[Args, Result]],
|
16
|
+
Callable[Args, Coroutine[Any, Any, Result]],
|
17
|
+
]: ...
|
20
18
|
|
21
19
|
|
22
20
|
@overload
|
haiway/helpers/files.py
ADDED
@@ -0,0 +1,421 @@
|
|
1
|
+
try:
|
2
|
+
import fcntl
|
3
|
+
|
4
|
+
except ModuleNotFoundError: # Windows does not supprt fcntl
|
5
|
+
fcntl = None
|
6
|
+
|
7
|
+
import os
|
8
|
+
from asyncio import Lock
|
9
|
+
from pathlib import Path
|
10
|
+
from types import TracebackType
|
11
|
+
from typing import Protocol, runtime_checkable
|
12
|
+
|
13
|
+
from haiway.context import ctx
|
14
|
+
from haiway.helpers.asynchrony import asynchronous
|
15
|
+
from haiway.state import State
|
16
|
+
|
17
|
+
__all__ = (
|
18
|
+
"File",
|
19
|
+
"FileAccess",
|
20
|
+
"FileAccessing",
|
21
|
+
"FileContext",
|
22
|
+
"FileException",
|
23
|
+
"FileReading",
|
24
|
+
"FileWriting",
|
25
|
+
)
|
26
|
+
|
27
|
+
|
28
|
+
class FileException(Exception):
|
29
|
+
"""
|
30
|
+
Exception raised for file operation errors.
|
31
|
+
|
32
|
+
Raised when file operations fail, such as attempting to access
|
33
|
+
a non-existent file without the create flag, or when file I/O
|
34
|
+
operations encounter errors.
|
35
|
+
"""
|
36
|
+
|
37
|
+
|
38
|
+
@runtime_checkable
|
39
|
+
class FileReading(Protocol):
|
40
|
+
"""
|
41
|
+
Protocol for asynchronous file reading operations.
|
42
|
+
|
43
|
+
Implementations read the entire file contents and return them as bytes.
|
44
|
+
The file position is managed internally and reading always returns the
|
45
|
+
complete file contents from the beginning.
|
46
|
+
"""
|
47
|
+
|
48
|
+
async def __call__(
|
49
|
+
self,
|
50
|
+
) -> bytes: ...
|
51
|
+
|
52
|
+
|
53
|
+
@runtime_checkable
|
54
|
+
class FileWriting(Protocol):
|
55
|
+
"""
|
56
|
+
Protocol for asynchronous file writing operations.
|
57
|
+
|
58
|
+
Implementations write the provided content to the file, completely
|
59
|
+
replacing any existing content. The write operation is atomic and
|
60
|
+
includes proper synchronization to ensure data is written to disk.
|
61
|
+
"""
|
62
|
+
|
63
|
+
async def __call__(
|
64
|
+
self,
|
65
|
+
content: bytes,
|
66
|
+
) -> None: ...
|
67
|
+
|
68
|
+
|
69
|
+
class File(State):
|
70
|
+
"""
|
71
|
+
State container for file operations within a context scope.
|
72
|
+
|
73
|
+
Provides access to file operations after a file has been opened using
|
74
|
+
FileAccess within a context scope. Follows Haiway's pattern of accessing
|
75
|
+
functionality through class methods that retrieve state from the current context.
|
76
|
+
|
77
|
+
The file operations are provided through the reading and writing protocol
|
78
|
+
implementations, which are injected when the file is opened.
|
79
|
+
"""
|
80
|
+
|
81
|
+
@classmethod
|
82
|
+
async def read(
|
83
|
+
cls,
|
84
|
+
) -> bytes:
|
85
|
+
"""
|
86
|
+
Read the complete contents of the file.
|
87
|
+
|
88
|
+
Returns
|
89
|
+
-------
|
90
|
+
bytes
|
91
|
+
The complete file contents as bytes
|
92
|
+
|
93
|
+
Raises
|
94
|
+
------
|
95
|
+
FileException
|
96
|
+
If no file is currently open in the context
|
97
|
+
"""
|
98
|
+
return await ctx.state(cls).reading()
|
99
|
+
|
100
|
+
@classmethod
|
101
|
+
async def write(
|
102
|
+
cls,
|
103
|
+
content: bytes,
|
104
|
+
/,
|
105
|
+
) -> None:
|
106
|
+
"""
|
107
|
+
Write content to the file, replacing existing content.
|
108
|
+
|
109
|
+
Parameters
|
110
|
+
----------
|
111
|
+
content : bytes
|
112
|
+
The bytes content to write to the file
|
113
|
+
|
114
|
+
Raises
|
115
|
+
------
|
116
|
+
FileException
|
117
|
+
If no file is currently open in the context
|
118
|
+
"""
|
119
|
+
await ctx.state(cls).writing(content)
|
120
|
+
|
121
|
+
reading: FileReading
|
122
|
+
writing: FileWriting
|
123
|
+
|
124
|
+
|
125
|
+
@runtime_checkable
|
126
|
+
class FileContext(Protocol):
|
127
|
+
"""
|
128
|
+
Protocol for file context managers.
|
129
|
+
|
130
|
+
Defines the interface for file context managers that handle the opening,
|
131
|
+
access, and cleanup of file resources. Implementations ensure proper
|
132
|
+
resource management and make file operations available through the File
|
133
|
+
state class.
|
134
|
+
|
135
|
+
The context manager pattern ensures that file handles are properly closed
|
136
|
+
and locks are released even if exceptions occur during file operations.
|
137
|
+
"""
|
138
|
+
|
139
|
+
async def __aenter__(self) -> File:
|
140
|
+
"""
|
141
|
+
Enter the file context and return file operations.
|
142
|
+
|
143
|
+
Returns
|
144
|
+
-------
|
145
|
+
File
|
146
|
+
A File state instance configured for the opened file
|
147
|
+
|
148
|
+
Raises
|
149
|
+
------
|
150
|
+
FileException
|
151
|
+
If the file cannot be opened
|
152
|
+
"""
|
153
|
+
...
|
154
|
+
|
155
|
+
async def __aexit__(
|
156
|
+
self,
|
157
|
+
exc_type: type[BaseException] | None,
|
158
|
+
exc_val: BaseException | None,
|
159
|
+
exc_tb: TracebackType | None,
|
160
|
+
) -> bool | None:
|
161
|
+
"""
|
162
|
+
Exit the file context and clean up resources.
|
163
|
+
|
164
|
+
Parameters
|
165
|
+
----------
|
166
|
+
exc_type : type[BaseException] | None
|
167
|
+
The exception type if an exception occurred
|
168
|
+
exc_val : BaseException | None
|
169
|
+
The exception value if an exception occurred
|
170
|
+
exc_tb : TracebackType | None
|
171
|
+
The exception traceback if an exception occurred
|
172
|
+
|
173
|
+
Returns
|
174
|
+
-------
|
175
|
+
bool | None
|
176
|
+
None to allow exceptions to propagate
|
177
|
+
"""
|
178
|
+
...
|
179
|
+
|
180
|
+
|
181
|
+
@runtime_checkable
|
182
|
+
class FileAccessing(Protocol):
|
183
|
+
"""
|
184
|
+
Protocol for file access implementations.
|
185
|
+
|
186
|
+
Defines the interface for creating file context managers with specific
|
187
|
+
access patterns. Implementations handle the details of file opening,
|
188
|
+
locking, and resource management.
|
189
|
+
"""
|
190
|
+
|
191
|
+
async def __call__(
|
192
|
+
self,
|
193
|
+
path: Path | str,
|
194
|
+
create: bool,
|
195
|
+
exclusive: bool,
|
196
|
+
) -> FileContext: ...
|
197
|
+
|
198
|
+
|
199
|
+
@asynchronous
|
200
|
+
def _open_file_handle(
|
201
|
+
path: Path,
|
202
|
+
*,
|
203
|
+
create: bool,
|
204
|
+
exclusive: bool,
|
205
|
+
) -> int:
|
206
|
+
file_handle: int
|
207
|
+
if path.exists():
|
208
|
+
file_handle = os.open(path, os.O_RDWR)
|
209
|
+
|
210
|
+
elif create:
|
211
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
212
|
+
file_handle = os.open(path, os.O_RDWR | os.O_CREAT)
|
213
|
+
|
214
|
+
else:
|
215
|
+
raise FileException(f"File does not exist: {path}")
|
216
|
+
|
217
|
+
if exclusive and fcntl is not None:
|
218
|
+
fcntl.flock(file_handle, fcntl.LOCK_EX) # pyright: ignore[reportAttributeAccessIssue] # windows
|
219
|
+
|
220
|
+
return file_handle
|
221
|
+
|
222
|
+
|
223
|
+
@asynchronous
|
224
|
+
def _read_file_contents(
|
225
|
+
file_handle: int,
|
226
|
+
*,
|
227
|
+
path: Path,
|
228
|
+
) -> bytes:
|
229
|
+
os.lseek(file_handle, 0, os.SEEK_SET)
|
230
|
+
bytes_remaining: int = path.stat().st_size
|
231
|
+
|
232
|
+
# Read all bytes, handling partial reads
|
233
|
+
chunks: list[bytes] = []
|
234
|
+
while bytes_remaining > 0:
|
235
|
+
chunk: bytes = os.read(file_handle, bytes_remaining)
|
236
|
+
if not chunk:
|
237
|
+
raise FileException("Unexpected end of file during read")
|
238
|
+
|
239
|
+
bytes_remaining -= len(chunk)
|
240
|
+
chunks.append(chunk)
|
241
|
+
|
242
|
+
return b"".join(chunks)
|
243
|
+
|
244
|
+
|
245
|
+
@asynchronous
|
246
|
+
def _write_file_contents(
|
247
|
+
file_handle: int,
|
248
|
+
*,
|
249
|
+
content: bytes,
|
250
|
+
) -> None:
|
251
|
+
os.lseek(file_handle, 0, os.SEEK_SET)
|
252
|
+
|
253
|
+
# Write all bytes, handling partial writes
|
254
|
+
offset: int = 0
|
255
|
+
while offset < len(content):
|
256
|
+
bytes_written: int = os.write(file_handle, content[offset:])
|
257
|
+
if bytes_written == 0:
|
258
|
+
raise FileException("Failed to write file content")
|
259
|
+
|
260
|
+
offset += bytes_written
|
261
|
+
|
262
|
+
os.ftruncate(file_handle, len(content))
|
263
|
+
os.fsync(file_handle)
|
264
|
+
|
265
|
+
|
266
|
+
@asynchronous
|
267
|
+
def _close_file_handle(
|
268
|
+
file_handle: int,
|
269
|
+
*,
|
270
|
+
exclusive: bool,
|
271
|
+
) -> None:
|
272
|
+
if exclusive and fcntl is not None:
|
273
|
+
fcntl.flock(file_handle, fcntl.LOCK_UN) # pyright: ignore[reportAttributeAccessIssue] # windows
|
274
|
+
|
275
|
+
os.close(file_handle)
|
276
|
+
|
277
|
+
|
278
|
+
async def _file_access_context(
|
279
|
+
path: Path | str,
|
280
|
+
create: bool,
|
281
|
+
exclusive: bool,
|
282
|
+
) -> FileContext:
|
283
|
+
file_path: Path = Path(path)
|
284
|
+
|
285
|
+
class FileAccessContext:
|
286
|
+
__slots__ = (
|
287
|
+
"_file_handle",
|
288
|
+
"_file_path",
|
289
|
+
"_lock",
|
290
|
+
)
|
291
|
+
|
292
|
+
def __init__(
|
293
|
+
self,
|
294
|
+
path: Path | str,
|
295
|
+
) -> None:
|
296
|
+
self._file_handle: int | None = None
|
297
|
+
self._file_path: Path = Path(path)
|
298
|
+
self._lock: Lock = Lock()
|
299
|
+
|
300
|
+
async def __aenter__(self) -> File:
|
301
|
+
assert self._file_handle is None # nosec: B101
|
302
|
+
|
303
|
+
self._file_handle = await _open_file_handle(
|
304
|
+
file_path,
|
305
|
+
create=create,
|
306
|
+
exclusive=exclusive,
|
307
|
+
)
|
308
|
+
|
309
|
+
async def read_file() -> bytes:
|
310
|
+
assert self._file_handle is not None # nosec: B101
|
311
|
+
async with self._lock:
|
312
|
+
return await _read_file_contents(
|
313
|
+
self._file_handle,
|
314
|
+
path=file_path,
|
315
|
+
)
|
316
|
+
|
317
|
+
async def write_file(content: bytes) -> None:
|
318
|
+
assert self._file_handle is not None # nosec: B101
|
319
|
+
async with self._lock:
|
320
|
+
await _write_file_contents(
|
321
|
+
self._file_handle,
|
322
|
+
content=content,
|
323
|
+
)
|
324
|
+
|
325
|
+
return File(
|
326
|
+
reading=read_file,
|
327
|
+
writing=write_file,
|
328
|
+
)
|
329
|
+
|
330
|
+
async def __aexit__(
|
331
|
+
self,
|
332
|
+
exc_type: type[BaseException] | None,
|
333
|
+
exc_val: BaseException | None,
|
334
|
+
exc_tb: TracebackType | None,
|
335
|
+
) -> bool | None:
|
336
|
+
assert self._file_handle is not None # nosec: B101
|
337
|
+
await _close_file_handle(
|
338
|
+
self._file_handle,
|
339
|
+
exclusive=exclusive,
|
340
|
+
)
|
341
|
+
|
342
|
+
return FileAccessContext(path=path)
|
343
|
+
|
344
|
+
|
345
|
+
class FileAccess(State):
|
346
|
+
"""
|
347
|
+
State container for file access configuration within a context scope.
|
348
|
+
|
349
|
+
Provides the entry point for file operations within Haiway's context system.
|
350
|
+
Follows the framework's pattern of using state classes to configure behavior
|
351
|
+
that can be injected and replaced for testing.
|
352
|
+
|
353
|
+
File access is scoped to the context, meaning only one file can be open
|
354
|
+
at a time within a given context scope. This design ensures predictable
|
355
|
+
resource usage and simplifies error handling.
|
356
|
+
|
357
|
+
The default implementation provides standard filesystem access with
|
358
|
+
optional file creation and exclusive locking. Custom implementations
|
359
|
+
can be injected by replacing the accessing function.
|
360
|
+
|
361
|
+
Examples
|
362
|
+
--------
|
363
|
+
Basic file operations:
|
364
|
+
|
365
|
+
>>> async with ctx.scope("app", disposables=(FileAccess.open("config.json", create=True),)):
|
366
|
+
... data = await File.read()
|
367
|
+
... await File.write(b'{"setting": "value"}')
|
368
|
+
|
369
|
+
Exclusive file access for critical operations:
|
370
|
+
|
371
|
+
>>> async with ctx.scope("app", disposables=(FileAccess.open("config.json", exclusive=True),)):
|
372
|
+
... content = await File.read()
|
373
|
+
... processed = process_data(content)
|
374
|
+
... await File.write(processed)
|
375
|
+
"""
|
376
|
+
|
377
|
+
@classmethod
|
378
|
+
async def open(
|
379
|
+
cls,
|
380
|
+
path: Path | str,
|
381
|
+
create: bool = False,
|
382
|
+
exclusive: bool = False,
|
383
|
+
) -> FileContext:
|
384
|
+
"""
|
385
|
+
Open a file for reading and writing.
|
386
|
+
|
387
|
+
Opens a file using the configured file access implementation. The file
|
388
|
+
is opened with read and write permissions, and the entire file content
|
389
|
+
is available through the File.read() and File.write() methods.
|
390
|
+
|
391
|
+
Parameters
|
392
|
+
----------
|
393
|
+
path : Path | str
|
394
|
+
The file path to open, as a Path object or string
|
395
|
+
create : bool, optional
|
396
|
+
If True, create the file and parent directories if they don't exist.
|
397
|
+
If False, raise FileException for missing files. Default is False
|
398
|
+
exclusive : bool, optional
|
399
|
+
If True, acquire an exclusive lock on the file for the duration of
|
400
|
+
the context. This prevents other processes from accessing the file
|
401
|
+
concurrently. Default is False
|
402
|
+
|
403
|
+
Returns
|
404
|
+
-------
|
405
|
+
FileContext
|
406
|
+
A FileContext that manages the file lifecycle and provides access
|
407
|
+
to file operations through the File state class
|
408
|
+
|
409
|
+
Raises
|
410
|
+
------
|
411
|
+
FileException
|
412
|
+
If the file cannot be opened with the specified parameters, or if
|
413
|
+
a file is already open in the current context scope
|
414
|
+
"""
|
415
|
+
return await ctx.state(cls).accessing(
|
416
|
+
path,
|
417
|
+
create=create,
|
418
|
+
exclusive=exclusive,
|
419
|
+
)
|
420
|
+
|
421
|
+
accessing: FileAccessing = _file_access_context
|
haiway/state/structure.py
CHANGED
@@ -728,6 +728,14 @@ class State(metaclass=StateMeta):
|
|
728
728
|
for key in self.__ATTRIBUTES__.keys()
|
729
729
|
)
|
730
730
|
|
731
|
+
def __hash__(self) -> int:
|
732
|
+
return hash(
|
733
|
+
(
|
734
|
+
self.__class__,
|
735
|
+
*tuple(getattr(self, key, MISSING) for key in sorted(self.__ATTRIBUTES__.keys())),
|
736
|
+
)
|
737
|
+
)
|
738
|
+
|
731
739
|
def __setattr__(
|
732
740
|
self,
|
733
741
|
name: str,
|
haiway/types/missing.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: haiway
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.22.0
|
4
4
|
Summary: Framework for dependency injection and state management within structured concurrency model.
|
5
5
|
Project-URL: Homepage, https://miquido.com
|
6
6
|
Project-URL: Repository, https://github.com/miquido/haiway.git
|
@@ -40,7 +40,7 @@ Requires-Dist: pyright~=1.1; extra == 'dev'
|
|
40
40
|
Requires-Dist: pytest-asyncio~=0.26; extra == 'dev'
|
41
41
|
Requires-Dist: pytest-cov~=6.1; extra == 'dev'
|
42
42
|
Requires-Dist: pytest~=8.3; extra == 'dev'
|
43
|
-
Requires-Dist: ruff~=0.
|
43
|
+
Requires-Dist: ruff~=0.12; extra == 'dev'
|
44
44
|
Provides-Extra: opentelemetry
|
45
45
|
Requires-Dist: opentelemetry-api~=1.33; extra == 'opentelemetry'
|
46
46
|
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc~=1.33; extra == 'opentelemetry'
|
@@ -1,17 +1,18 @@
|
|
1
|
-
haiway/__init__.py,sha256=
|
1
|
+
haiway/__init__.py,sha256=FiOAMHHawyGk9FfZU-1UflT8nmwu9J0CrG2QwrJGccw,1917
|
2
2
|
haiway/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
haiway/context/__init__.py,sha256=1N_SvdPkTfIZDZybm3y0rY2dGrDLWTm0ryzUz2XD4f8,1174
|
4
|
-
haiway/context/access.py,sha256=
|
5
|
-
haiway/context/disposables.py,sha256=
|
4
|
+
haiway/context/access.py,sha256=j5wC0kluFEb1NpTsUG7fgFVklfeNCqb0dygK1VIdLuo,25485
|
5
|
+
haiway/context/disposables.py,sha256=yRABVbUJIMP6r5oqO-Yl4y20ulhlDuQ2lLnI25QCltg,10495
|
6
6
|
haiway/context/identifier.py,sha256=dCCwLneXJzH__ZWFlGRUHvoCmbT4lM0QVbyokYIbUHg,5255
|
7
7
|
haiway/context/observability.py,sha256=gLKbMPNvt5ozrfyc4TGahN8A_dFFtyCjUIMZu9_wZHA,23722
|
8
|
-
haiway/context/state.py,sha256=
|
8
|
+
haiway/context/state.py,sha256=Ni6NUbRYKmgi7HhvosrVH62IzfalLDUH7VYRwpUiQ78,11426
|
9
9
|
haiway/context/tasks.py,sha256=pScFgeiyrXSJRDFZiYbBLi3k_DHkSlhB8rgAnYtgyrU,4925
|
10
10
|
haiway/context/types.py,sha256=VDWXJySihfvSSPzY09PaGk6j5S9HgmAUboBGCZ8o_4k,766
|
11
|
-
haiway/helpers/__init__.py,sha256=
|
12
|
-
haiway/helpers/asynchrony.py,sha256=
|
11
|
+
haiway/helpers/__init__.py,sha256=uEJATJOiqRWeOPMjrGdn_CmWvwrlUQ1gmrau4XsBgUk,720
|
12
|
+
haiway/helpers/asynchrony.py,sha256=Ddj8UdXhVczAbAC-rLpyhWa4RJ_W2Eolo45Veorq7_4,5362
|
13
13
|
haiway/helpers/caching.py,sha256=BqgcUGQSAmXsuLi5V8EwlZzuGyutHOn1V4k7BHsGKeg,14347
|
14
14
|
haiway/helpers/concurrent.py,sha256=xGMcan_tiETAHQs1YFmgYpA4YMFo6rIbFKvNeMlRFG8,2551
|
15
|
+
haiway/helpers/files.py,sha256=L6vXd8gdgWx5jPL8azloU8IGoFq2xnxjMc4ufz-gdl4,11650
|
15
16
|
haiway/helpers/observability.py,sha256=3G0eRE1WYTGRujS0mxzYbLR4MlKnoYllE8cu2Eb_23w,11073
|
16
17
|
haiway/helpers/retries.py,sha256=52LA85HejTiSmCmTMAA9c8oUqD_VnhbTn1b3kwlU52c,9032
|
17
18
|
haiway/helpers/throttling.py,sha256=KBWUSHdKVMC5_nRMmmoPNwfp-3AcerQ6OczJa9gNLM0,5796
|
@@ -23,11 +24,11 @@ haiway/state/__init__.py,sha256=AaMqlMhO4zKS_XNevy3A7BHh5PxmguA-Sk_FnaNDY1Q,355
|
|
23
24
|
haiway/state/attributes.py,sha256=sububiFP23aBB8RGk6OvTUp7BEY6S0kER_uHC09yins,26733
|
24
25
|
haiway/state/path.py,sha256=bv5MI3HmUyku78k0Sz5lc7Q_Bay53iom1l3AL5KZs-4,32143
|
25
26
|
haiway/state/requirement.py,sha256=zNTx7s8FiMZKu9EV3T6f1SOJpR4SC9X5hhL--PVWPCY,15641
|
26
|
-
haiway/state/structure.py,sha256=
|
27
|
+
haiway/state/structure.py,sha256=XzPDCrFUSy-PKVzI9G6TqechRW3nXSbTK7f_AhxCzq4,23481
|
27
28
|
haiway/state/validation.py,sha256=eDOZKRrfd-dmdbqoHcLacdCVKmVCEpwt239EG6ljNF8,23557
|
28
29
|
haiway/types/__init__.py,sha256=jFr5kf36SvVGdgngvik6_HzG8YNa3NVsdDDSqxVuGm4,281
|
29
30
|
haiway/types/default.py,sha256=59chcOaoGqI2to08RamCCLluimfYbJp5xbYl3fWaLrM,4153
|
30
|
-
haiway/types/missing.py,sha256=
|
31
|
+
haiway/types/missing.py,sha256=OfiyYUnzTk3arKWu8S6ORCEYGvcRu_mdL4j1ExdSvgI,4256
|
31
32
|
haiway/utils/__init__.py,sha256=HOylRgBEa0uNxEuPBupaJ28l4wEQiy98cGJi2Gtirr4,972
|
32
33
|
haiway/utils/always.py,sha256=dd6jDQ1j4DpJjTKO1J2Tv5xS8X1LnMC4kQ0D7DtKUvw,1230
|
33
34
|
haiway/utils/collections.py,sha256=gF5tC1EaEzBfPpXrHqR0mZh8e4pRwEPSVactvfN-30M,4737
|
@@ -39,7 +40,7 @@ haiway/utils/mimic.py,sha256=xaZiUKp096QFfdSw7cNIKEWt2UIS7vf880KF54gny38,1831
|
|
39
40
|
haiway/utils/noop.py,sha256=U8ocfoCgt-pY0owJDPtrRrj53cabeIXH9qCKWMQnoRk,1336
|
40
41
|
haiway/utils/queue.py,sha256=6v2u3pA6A44IuCCTOjmCt3yLyOcm7PCRnrIGo25j-1o,6402
|
41
42
|
haiway/utils/stream.py,sha256=lXaeveTY0-AYG5xVzcQYaiC6SUD5fUtHoMXiQcrQAAM,5723
|
42
|
-
haiway-0.
|
43
|
-
haiway-0.
|
44
|
-
haiway-0.
|
45
|
-
haiway-0.
|
43
|
+
haiway-0.22.0.dist-info/METADATA,sha256=6__3ZCA0Klv8j7coosQhvQY9GWNqyVlUKSIRbh1f7Jw,4919
|
44
|
+
haiway-0.22.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
45
|
+
haiway-0.22.0.dist-info/licenses/LICENSE,sha256=3phcpHVNBP8jsi77gOO0E7rgKeDeu99Pi7DSnK9YHoQ,1069
|
46
|
+
haiway-0.22.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|