haiway 0.1.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 +75 -0
- haiway/context/__init__.py +14 -0
- haiway/context/access.py +416 -0
- haiway/context/dependencies.py +61 -0
- haiway/context/metrics.py +329 -0
- haiway/context/state.py +115 -0
- haiway/context/tasks.py +65 -0
- haiway/context/types.py +17 -0
- haiway/helpers/__init__.py +13 -0
- haiway/helpers/asynchronous.py +226 -0
- haiway/helpers/cache.py +326 -0
- haiway/helpers/retry.py +210 -0
- haiway/helpers/throttling.py +133 -0
- haiway/helpers/timeout.py +112 -0
- haiway/py.typed +0 -0
- haiway/state/__init__.py +8 -0
- haiway/state/attributes.py +360 -0
- haiway/state/structure.py +254 -0
- haiway/state/validation.py +125 -0
- haiway/types/__init__.py +11 -0
- haiway/types/frozen.py +5 -0
- haiway/types/missing.py +91 -0
- haiway/utils/__init__.py +23 -0
- haiway/utils/always.py +61 -0
- haiway/utils/env.py +164 -0
- haiway/utils/immutable.py +28 -0
- haiway/utils/logs.py +57 -0
- haiway/utils/mimic.py +77 -0
- haiway/utils/noop.py +24 -0
- haiway/utils/queue.py +89 -0
- haiway-0.1.0.dist-info/LICENSE +21 -0
- haiway-0.1.0.dist-info/METADATA +86 -0
- haiway-0.1.0.dist-info/RECORD +35 -0
- haiway-0.1.0.dist-info/WHEEL +5 -0
- haiway-0.1.0.dist-info/top_level.txt +1 -0
haiway/__init__.py
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
from haiway.context import (
|
2
|
+
Dependencies,
|
3
|
+
Dependency,
|
4
|
+
MissingContext,
|
5
|
+
MissingDependency,
|
6
|
+
MissingState,
|
7
|
+
ScopeMetrics,
|
8
|
+
ctx,
|
9
|
+
)
|
10
|
+
from haiway.helpers import (
|
11
|
+
asynchronous,
|
12
|
+
auto_retry,
|
13
|
+
cached,
|
14
|
+
throttle,
|
15
|
+
with_timeout,
|
16
|
+
)
|
17
|
+
from haiway.state import Structure
|
18
|
+
from haiway.types import (
|
19
|
+
MISSING,
|
20
|
+
Missing,
|
21
|
+
frozenlist,
|
22
|
+
is_missing,
|
23
|
+
not_missing,
|
24
|
+
when_missing,
|
25
|
+
)
|
26
|
+
from haiway.utils import (
|
27
|
+
AsyncQueue,
|
28
|
+
always,
|
29
|
+
async_always,
|
30
|
+
async_noop,
|
31
|
+
freeze,
|
32
|
+
getenv_bool,
|
33
|
+
getenv_float,
|
34
|
+
getenv_int,
|
35
|
+
getenv_str,
|
36
|
+
load_env,
|
37
|
+
mimic_function,
|
38
|
+
noop,
|
39
|
+
setup_logging,
|
40
|
+
)
|
41
|
+
|
42
|
+
__all__ = [
|
43
|
+
"always",
|
44
|
+
"async_always",
|
45
|
+
"async_noop",
|
46
|
+
"asynchronous",
|
47
|
+
"AsyncQueue",
|
48
|
+
"auto_retry",
|
49
|
+
"cached",
|
50
|
+
"ctx",
|
51
|
+
"Dependencies",
|
52
|
+
"Dependency",
|
53
|
+
"freeze",
|
54
|
+
"frozenlist",
|
55
|
+
"getenv_bool",
|
56
|
+
"getenv_float",
|
57
|
+
"getenv_int",
|
58
|
+
"getenv_str",
|
59
|
+
"is_missing",
|
60
|
+
"load_env",
|
61
|
+
"mimic_function",
|
62
|
+
"Missing",
|
63
|
+
"MISSING",
|
64
|
+
"MissingContext",
|
65
|
+
"MissingDependency",
|
66
|
+
"MissingState",
|
67
|
+
"noop",
|
68
|
+
"not_missing",
|
69
|
+
"ScopeMetrics",
|
70
|
+
"setup_logging",
|
71
|
+
"Structure",
|
72
|
+
"throttle",
|
73
|
+
"when_missing",
|
74
|
+
"with_timeout",
|
75
|
+
]
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from haiway.context.access import ctx
|
2
|
+
from haiway.context.dependencies import Dependencies, Dependency
|
3
|
+
from haiway.context.metrics import ScopeMetrics
|
4
|
+
from haiway.context.types import MissingContext, MissingDependency, MissingState
|
5
|
+
|
6
|
+
__all__ = [
|
7
|
+
"ctx",
|
8
|
+
"Dependencies",
|
9
|
+
"Dependency",
|
10
|
+
"MissingContext",
|
11
|
+
"MissingDependency",
|
12
|
+
"MissingState",
|
13
|
+
"ScopeMetrics",
|
14
|
+
]
|
haiway/context/access.py
ADDED
@@ -0,0 +1,416 @@
|
|
1
|
+
from asyncio import (
|
2
|
+
Task,
|
3
|
+
current_task,
|
4
|
+
)
|
5
|
+
from collections.abc import (
|
6
|
+
Callable,
|
7
|
+
Coroutine,
|
8
|
+
)
|
9
|
+
from logging import Logger
|
10
|
+
from types import TracebackType
|
11
|
+
from typing import Any, final
|
12
|
+
|
13
|
+
from haiway.context.dependencies import Dependencies, Dependency
|
14
|
+
from haiway.context.metrics import MetricsContext, ScopeMetrics
|
15
|
+
from haiway.context.state import StateContext
|
16
|
+
from haiway.context.tasks import TaskGroupContext
|
17
|
+
from haiway.state import Structure
|
18
|
+
from haiway.utils import freeze
|
19
|
+
|
20
|
+
__all__ = [
|
21
|
+
"ctx",
|
22
|
+
]
|
23
|
+
|
24
|
+
|
25
|
+
@final
|
26
|
+
class ScopeContext:
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
task_group: TaskGroupContext,
|
30
|
+
state: StateContext,
|
31
|
+
metrics: MetricsContext,
|
32
|
+
completion: Callable[[ScopeMetrics], Coroutine[None, None, None]] | None,
|
33
|
+
) -> None:
|
34
|
+
self._task_group: TaskGroupContext = task_group
|
35
|
+
self._state: StateContext = state
|
36
|
+
self._metrics: MetricsContext = metrics
|
37
|
+
self._completion: Callable[[ScopeMetrics], Coroutine[None, None, None]] | None = completion
|
38
|
+
|
39
|
+
freeze(self)
|
40
|
+
|
41
|
+
def __enter__(self) -> None:
|
42
|
+
assert self._completion is None, "Can't enter synchronous context with completion" # nosec: B101
|
43
|
+
|
44
|
+
self._state.__enter__()
|
45
|
+
self._metrics.__enter__()
|
46
|
+
|
47
|
+
def __exit__(
|
48
|
+
self,
|
49
|
+
exc_type: type[BaseException] | None,
|
50
|
+
exc_val: BaseException | None,
|
51
|
+
exc_tb: TracebackType | None,
|
52
|
+
) -> None:
|
53
|
+
self._metrics.__exit__(
|
54
|
+
exc_type=exc_type,
|
55
|
+
exc_val=exc_val,
|
56
|
+
exc_tb=exc_tb,
|
57
|
+
)
|
58
|
+
|
59
|
+
self._state.__exit__(
|
60
|
+
exc_type=exc_type,
|
61
|
+
exc_val=exc_val,
|
62
|
+
exc_tb=exc_tb,
|
63
|
+
)
|
64
|
+
|
65
|
+
async def __aenter__(self) -> None:
|
66
|
+
self._state.__enter__()
|
67
|
+
self._metrics.__enter__()
|
68
|
+
await self._task_group.__aenter__()
|
69
|
+
|
70
|
+
async def __aexit__(
|
71
|
+
self,
|
72
|
+
exc_type: type[BaseException] | None,
|
73
|
+
exc_val: BaseException | None,
|
74
|
+
exc_tb: TracebackType | None,
|
75
|
+
) -> None:
|
76
|
+
await self._task_group.__aexit__(
|
77
|
+
exc_type=exc_type,
|
78
|
+
exc_val=exc_val,
|
79
|
+
exc_tb=exc_tb,
|
80
|
+
)
|
81
|
+
|
82
|
+
self._metrics.__exit__(
|
83
|
+
exc_type=exc_type,
|
84
|
+
exc_val=exc_val,
|
85
|
+
exc_tb=exc_tb,
|
86
|
+
)
|
87
|
+
|
88
|
+
self._state.__exit__(
|
89
|
+
exc_type=exc_type,
|
90
|
+
exc_val=exc_val,
|
91
|
+
exc_tb=exc_tb,
|
92
|
+
)
|
93
|
+
|
94
|
+
if completion := self._completion:
|
95
|
+
await completion(self._metrics._metrics) # pyright: ignore[reportPrivateUsage]
|
96
|
+
|
97
|
+
|
98
|
+
@final
|
99
|
+
class ctx:
|
100
|
+
@staticmethod
|
101
|
+
def scope(
|
102
|
+
name: str,
|
103
|
+
/,
|
104
|
+
*state: Structure,
|
105
|
+
logger: Logger | None = None,
|
106
|
+
trace_id: str | None = None,
|
107
|
+
completion: Callable[[ScopeMetrics], Coroutine[None, None, None]] | None = None,
|
108
|
+
) -> ScopeContext:
|
109
|
+
"""
|
110
|
+
Access scope context with given parameters. When called within an existing context\
|
111
|
+
it becomes nested with current context as its predecessor.
|
112
|
+
|
113
|
+
Parameters
|
114
|
+
----------
|
115
|
+
name: Value
|
116
|
+
name of the scope context
|
117
|
+
|
118
|
+
*state: Structure
|
119
|
+
state propagated within the scope context, will be merged with current if any\
|
120
|
+
by replacing current with provided on conflict
|
121
|
+
|
122
|
+
logger: Logger | None
|
123
|
+
logger used within the scope context, when not provided current logger will be used\
|
124
|
+
if any, otherwise the logger with the scope name will be requested.
|
125
|
+
|
126
|
+
trace_id: str | None = None
|
127
|
+
tracing identifier included in logs produced within the scope context, when not\
|
128
|
+
provided current identifier will be used if any, otherwise it random id will\
|
129
|
+
be generated
|
130
|
+
|
131
|
+
completion: Callable[[ScopeMetrics], Coroutine[None, None, None]] | None = None
|
132
|
+
completion callback called on exit from the scope granting access to finished\
|
133
|
+
scope metrics. Completion is called outside of the context when its metrics is\
|
134
|
+
already finished. Make sure to avoid any long operations within the completion.
|
135
|
+
|
136
|
+
Returns
|
137
|
+
-------
|
138
|
+
ScopeContext
|
139
|
+
context object intended to enter context manager with it
|
140
|
+
"""
|
141
|
+
|
142
|
+
return ScopeContext(
|
143
|
+
task_group=TaskGroupContext(),
|
144
|
+
metrics=MetricsContext.scope(
|
145
|
+
name,
|
146
|
+
logger=logger,
|
147
|
+
trace_id=trace_id,
|
148
|
+
),
|
149
|
+
state=StateContext.updated(state),
|
150
|
+
completion=completion,
|
151
|
+
)
|
152
|
+
|
153
|
+
@staticmethod
|
154
|
+
def updated(
|
155
|
+
*state: Structure,
|
156
|
+
) -> StateContext:
|
157
|
+
"""
|
158
|
+
Update scope context with given state. When called within an existing context\
|
159
|
+
it becomes nested with current context as its predecessor.
|
160
|
+
|
161
|
+
Parameters
|
162
|
+
----------
|
163
|
+
*state: Structure
|
164
|
+
state propagated within the updated scope context, will be merged with current if any\
|
165
|
+
by replacing current with provided on conflict
|
166
|
+
|
167
|
+
Returns
|
168
|
+
-------
|
169
|
+
StateContext
|
170
|
+
state part of context object intended to enter context manager with it
|
171
|
+
"""
|
172
|
+
|
173
|
+
return StateContext.updated(state)
|
174
|
+
|
175
|
+
@staticmethod
|
176
|
+
def spawn[Result, **Arguments](
|
177
|
+
function: Callable[Arguments, Coroutine[None, None, Result]],
|
178
|
+
/,
|
179
|
+
*args: Arguments.args,
|
180
|
+
**kwargs: Arguments.kwargs,
|
181
|
+
) -> Task[Result]:
|
182
|
+
"""
|
183
|
+
Spawn an async task within current scope context task group. When called outside of context\
|
184
|
+
it will spawn detached task instead.
|
185
|
+
|
186
|
+
Parameters
|
187
|
+
----------
|
188
|
+
function: Callable[Arguments, Coroutine[None, None, Result]]
|
189
|
+
function to be called within the task group
|
190
|
+
|
191
|
+
*args: Arguments.args
|
192
|
+
positional arguments passed to function call
|
193
|
+
|
194
|
+
**kwargs: Arguments.kwargs
|
195
|
+
keyword arguments passed to function call
|
196
|
+
|
197
|
+
Returns
|
198
|
+
-------
|
199
|
+
Task[Result]
|
200
|
+
task for tracking function execution and result
|
201
|
+
"""
|
202
|
+
|
203
|
+
return TaskGroupContext.run(function, *args, **kwargs)
|
204
|
+
|
205
|
+
@staticmethod
|
206
|
+
def cancel() -> None:
|
207
|
+
"""
|
208
|
+
Cancel current asyncio task
|
209
|
+
"""
|
210
|
+
|
211
|
+
if task := current_task():
|
212
|
+
task.cancel()
|
213
|
+
|
214
|
+
else:
|
215
|
+
raise RuntimeError("Attempting to cancel context out of asyncio task")
|
216
|
+
|
217
|
+
@staticmethod
|
218
|
+
async def dependency[DependencyType: Dependency](
|
219
|
+
dependency: type[DependencyType],
|
220
|
+
/,
|
221
|
+
) -> DependencyType:
|
222
|
+
"""
|
223
|
+
Access current dependency by its type.
|
224
|
+
|
225
|
+
Parameters
|
226
|
+
----------
|
227
|
+
dependency: type[DependencyType]
|
228
|
+
type of requested dependency
|
229
|
+
|
230
|
+
Returns
|
231
|
+
-------
|
232
|
+
DependencyType
|
233
|
+
resolved dependency instance
|
234
|
+
"""
|
235
|
+
|
236
|
+
return await Dependencies.dependency(dependency)
|
237
|
+
|
238
|
+
@staticmethod
|
239
|
+
def state[StateType: Structure](
|
240
|
+
state: type[StateType],
|
241
|
+
/,
|
242
|
+
default: StateType | None = None,
|
243
|
+
) -> StateType:
|
244
|
+
"""
|
245
|
+
Access current scope context state by its type. If there is no matching state defined\
|
246
|
+
default value will be created if able, an exception will raise otherwise.
|
247
|
+
|
248
|
+
Parameters
|
249
|
+
----------
|
250
|
+
state: type[StateType]
|
251
|
+
type of requested state
|
252
|
+
|
253
|
+
Returns
|
254
|
+
-------
|
255
|
+
StateType
|
256
|
+
resolved state instance
|
257
|
+
"""
|
258
|
+
return StateContext.current(
|
259
|
+
state,
|
260
|
+
default=default,
|
261
|
+
)
|
262
|
+
|
263
|
+
@staticmethod
|
264
|
+
def record[Metric: Structure](
|
265
|
+
metric: Metric,
|
266
|
+
/,
|
267
|
+
merge: Callable[[Metric, Metric], Metric] = lambda lhs, rhs: rhs,
|
268
|
+
) -> None:
|
269
|
+
"""
|
270
|
+
Record metric within current scope context.
|
271
|
+
|
272
|
+
Parameters
|
273
|
+
----------
|
274
|
+
metric: MetricType
|
275
|
+
value of metric to be recorded
|
276
|
+
|
277
|
+
merge: Callable[[MetricType, MetricType], MetricType] = lambda lhs, rhs: rhs
|
278
|
+
merge method used on to resolve conflicts when a metric of the same type\
|
279
|
+
was already recorded. When not provided value will be override current if any.
|
280
|
+
|
281
|
+
Returns
|
282
|
+
-------
|
283
|
+
None
|
284
|
+
"""
|
285
|
+
|
286
|
+
MetricsContext.record(
|
287
|
+
metric,
|
288
|
+
merge=merge,
|
289
|
+
)
|
290
|
+
|
291
|
+
@staticmethod
|
292
|
+
def log_error(
|
293
|
+
message: str,
|
294
|
+
/,
|
295
|
+
*args: Any,
|
296
|
+
exception: BaseException | None = None,
|
297
|
+
) -> None:
|
298
|
+
"""
|
299
|
+
Log using ERROR level within current scope context. When there is no current scope\
|
300
|
+
root logger will be used without additional details.
|
301
|
+
|
302
|
+
Parameters
|
303
|
+
----------
|
304
|
+
message: str
|
305
|
+
message to be written to log
|
306
|
+
|
307
|
+
*args: Any
|
308
|
+
message format arguments
|
309
|
+
|
310
|
+
exception: BaseException | None = None
|
311
|
+
exception associated with log, when provided full stack trace will be recorded
|
312
|
+
|
313
|
+
Returns
|
314
|
+
-------
|
315
|
+
None
|
316
|
+
"""
|
317
|
+
|
318
|
+
MetricsContext.log_error(
|
319
|
+
message,
|
320
|
+
*args,
|
321
|
+
exception=exception,
|
322
|
+
)
|
323
|
+
|
324
|
+
@staticmethod
|
325
|
+
def log_warning(
|
326
|
+
message: str,
|
327
|
+
/,
|
328
|
+
*args: Any,
|
329
|
+
exception: Exception | None = None,
|
330
|
+
) -> None:
|
331
|
+
"""
|
332
|
+
Log using WARNING level within current scope context. When there is no current scope\
|
333
|
+
root logger will be used without additional details.
|
334
|
+
|
335
|
+
Parameters
|
336
|
+
----------
|
337
|
+
message: str
|
338
|
+
message to be written to log
|
339
|
+
|
340
|
+
*args: Any
|
341
|
+
message format arguments
|
342
|
+
|
343
|
+
exception: BaseException | None = None
|
344
|
+
exception associated with log, when provided full stack trace will be recorded
|
345
|
+
|
346
|
+
Returns
|
347
|
+
-------
|
348
|
+
None
|
349
|
+
"""
|
350
|
+
|
351
|
+
MetricsContext.log_warning(
|
352
|
+
message,
|
353
|
+
*args,
|
354
|
+
exception=exception,
|
355
|
+
)
|
356
|
+
|
357
|
+
@staticmethod
|
358
|
+
def log_info(
|
359
|
+
message: str,
|
360
|
+
/,
|
361
|
+
*args: Any,
|
362
|
+
) -> None:
|
363
|
+
"""
|
364
|
+
Log using INFO level within current scope context. When there is no current scope\
|
365
|
+
root logger will be used without additional details.
|
366
|
+
|
367
|
+
Parameters
|
368
|
+
----------
|
369
|
+
message: str
|
370
|
+
message to be written to log
|
371
|
+
|
372
|
+
*args: Any
|
373
|
+
message format arguments
|
374
|
+
|
375
|
+
Returns
|
376
|
+
-------
|
377
|
+
None
|
378
|
+
"""
|
379
|
+
|
380
|
+
MetricsContext.log_info(
|
381
|
+
message,
|
382
|
+
*args,
|
383
|
+
)
|
384
|
+
|
385
|
+
@staticmethod
|
386
|
+
def log_debug(
|
387
|
+
message: str,
|
388
|
+
/,
|
389
|
+
*args: Any,
|
390
|
+
exception: Exception | None = None,
|
391
|
+
) -> None:
|
392
|
+
"""
|
393
|
+
Log using DEBUG level within current scope context. When there is no current scope\
|
394
|
+
root logger will be used without additional details.
|
395
|
+
|
396
|
+
Parameters
|
397
|
+
----------
|
398
|
+
message: str
|
399
|
+
message to be written to log
|
400
|
+
|
401
|
+
*args: Any
|
402
|
+
message format arguments
|
403
|
+
|
404
|
+
exception: BaseException | None = None
|
405
|
+
exception associated with log, when provided full stack trace will be recorded
|
406
|
+
|
407
|
+
Returns
|
408
|
+
-------
|
409
|
+
None
|
410
|
+
"""
|
411
|
+
|
412
|
+
MetricsContext.log_debug(
|
413
|
+
message,
|
414
|
+
*args,
|
415
|
+
exception=exception,
|
416
|
+
)
|
@@ -0,0 +1,61 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from asyncio import Lock, gather, shield
|
3
|
+
from typing import ClassVar, Self, cast, final
|
4
|
+
|
5
|
+
__all__ = [
|
6
|
+
"Dependencies",
|
7
|
+
"Dependency",
|
8
|
+
]
|
9
|
+
|
10
|
+
|
11
|
+
class Dependency(ABC):
|
12
|
+
@classmethod
|
13
|
+
@abstractmethod
|
14
|
+
async def prepare(cls) -> Self: ...
|
15
|
+
|
16
|
+
async def dispose(self) -> None: # noqa: B027
|
17
|
+
pass
|
18
|
+
|
19
|
+
|
20
|
+
@final
|
21
|
+
class Dependencies:
|
22
|
+
_lock: ClassVar[Lock] = Lock()
|
23
|
+
_dependencies: ClassVar[dict[type[Dependency], Dependency]] = {}
|
24
|
+
|
25
|
+
def __init__(self) -> None:
|
26
|
+
raise NotImplementedError("Can't instantiate Dependencies")
|
27
|
+
|
28
|
+
@classmethod
|
29
|
+
async def dependency[Requested: Dependency](
|
30
|
+
cls,
|
31
|
+
dependency: type[Requested],
|
32
|
+
/,
|
33
|
+
) -> Requested:
|
34
|
+
async with cls._lock:
|
35
|
+
if dependency not in cls._dependencies:
|
36
|
+
cls._dependencies[dependency] = await dependency.prepare()
|
37
|
+
|
38
|
+
return cast(Requested, cls._dependencies[dependency])
|
39
|
+
|
40
|
+
@classmethod
|
41
|
+
async def register(
|
42
|
+
cls,
|
43
|
+
dependency: Dependency,
|
44
|
+
/,
|
45
|
+
) -> None:
|
46
|
+
async with cls._lock:
|
47
|
+
if current := cls._dependencies.get(dependency.__class__):
|
48
|
+
await current.dispose()
|
49
|
+
|
50
|
+
cls._dependencies[dependency.__class__] = dependency
|
51
|
+
|
52
|
+
@classmethod
|
53
|
+
async def dispose(cls) -> None:
|
54
|
+
async with cls._lock:
|
55
|
+
await shield(
|
56
|
+
gather(
|
57
|
+
*[dependency.dispose() for dependency in cls._dependencies.values()],
|
58
|
+
return_exceptions=False,
|
59
|
+
)
|
60
|
+
)
|
61
|
+
cls._dependencies.clear()
|