haiway 0.10.14__py3-none-any.whl → 0.10.16__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 +111 -0
- haiway/context/__init__.py +27 -0
- haiway/context/access.py +615 -0
- haiway/context/disposables.py +78 -0
- haiway/context/identifier.py +92 -0
- haiway/context/logging.py +176 -0
- haiway/context/metrics.py +165 -0
- haiway/context/state.py +113 -0
- haiway/context/tasks.py +64 -0
- haiway/context/types.py +12 -0
- haiway/helpers/__init__.py +21 -0
- haiway/helpers/asynchrony.py +225 -0
- haiway/helpers/caching.py +326 -0
- haiway/helpers/metrics.py +459 -0
- haiway/helpers/retries.py +223 -0
- haiway/helpers/throttling.py +133 -0
- haiway/helpers/timeouted.py +112 -0
- haiway/helpers/tracing.py +137 -0
- haiway/py.typed +0 -0
- haiway/state/__init__.py +12 -0
- haiway/state/attributes.py +747 -0
- haiway/state/path.py +524 -0
- haiway/state/requirement.py +229 -0
- haiway/state/structure.py +414 -0
- haiway/state/validation.py +468 -0
- haiway/types/__init__.py +14 -0
- haiway/types/default.py +108 -0
- haiway/types/frozen.py +5 -0
- haiway/types/missing.py +95 -0
- haiway/utils/__init__.py +28 -0
- haiway/utils/always.py +61 -0
- haiway/utils/collections.py +185 -0
- haiway/utils/env.py +230 -0
- haiway/utils/freezing.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 +82 -0
- {haiway-0.10.14.dist-info → haiway-0.10.16.dist-info}/METADATA +1 -1
- haiway-0.10.16.dist-info/RECORD +42 -0
- haiway-0.10.14.dist-info/RECORD +0 -4
- {haiway-0.10.14.dist-info → haiway-0.10.16.dist-info}/WHEEL +0 -0
- {haiway-0.10.14.dist-info → haiway-0.10.16.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,133 @@
|
|
1
|
+
from asyncio import (
|
2
|
+
Lock,
|
3
|
+
iscoroutinefunction,
|
4
|
+
sleep,
|
5
|
+
)
|
6
|
+
from collections import deque
|
7
|
+
from collections.abc import Callable, Coroutine
|
8
|
+
from datetime import timedelta
|
9
|
+
from time import monotonic
|
10
|
+
from typing import cast, overload
|
11
|
+
|
12
|
+
from haiway.utils.mimic import mimic_function
|
13
|
+
|
14
|
+
__all__ = [
|
15
|
+
"throttle",
|
16
|
+
]
|
17
|
+
|
18
|
+
|
19
|
+
@overload
|
20
|
+
def throttle[**Args, Result](
|
21
|
+
function: Callable[Args, Coroutine[None, None, Result]],
|
22
|
+
/,
|
23
|
+
) -> Callable[Args, Coroutine[None, None, Result]]: ...
|
24
|
+
|
25
|
+
|
26
|
+
@overload
|
27
|
+
def throttle[**Args, Result](
|
28
|
+
*,
|
29
|
+
limit: int = 1,
|
30
|
+
period: timedelta | float = 1,
|
31
|
+
) -> Callable[
|
32
|
+
[Callable[Args, Coroutine[None, None, Result]]], Callable[Args, Coroutine[None, None, Result]]
|
33
|
+
]: ...
|
34
|
+
|
35
|
+
|
36
|
+
def throttle[**Args, Result](
|
37
|
+
function: Callable[Args, Coroutine[None, None, Result]] | None = None,
|
38
|
+
*,
|
39
|
+
limit: int = 1,
|
40
|
+
period: timedelta | float = 1,
|
41
|
+
) -> (
|
42
|
+
Callable[
|
43
|
+
[Callable[Args, Coroutine[None, None, Result]]],
|
44
|
+
Callable[Args, Coroutine[None, None, Result]],
|
45
|
+
]
|
46
|
+
| Callable[Args, Coroutine[None, None, Result]]
|
47
|
+
):
|
48
|
+
"""\
|
49
|
+
Throttle for function calls with custom limit and period time. \
|
50
|
+
Works only for async functions by waiting desired time before execution. \
|
51
|
+
It is not allowed to be used on class or instance methods. \
|
52
|
+
This wrapper is not thread safe.
|
53
|
+
|
54
|
+
Parameters
|
55
|
+
----------
|
56
|
+
function: Callable[Args, Coroutine[None, None, Result]]
|
57
|
+
function to wrap in throttle
|
58
|
+
limit: int
|
59
|
+
limit of executions in given period, if no period was specified
|
60
|
+
it is number of concurrent executions instead, default is 1
|
61
|
+
period: timedelta | float | None
|
62
|
+
period time (in seconds by default) during which the limit resets, default is 1 second
|
63
|
+
|
64
|
+
Returns
|
65
|
+
-------
|
66
|
+
Callable[[Callable[Args, Coroutine[None, None, Result]]], Callable[Args, Coroutine[None, None, Result]]] \
|
67
|
+
| Callable[Args, Coroutine[None, None, Result]]
|
68
|
+
provided function wrapped in throttle
|
69
|
+
""" # noqa: E501
|
70
|
+
|
71
|
+
def _wrap(
|
72
|
+
function: Callable[Args, Coroutine[None, None, Result]],
|
73
|
+
) -> Callable[Args, Coroutine[None, None, Result]]:
|
74
|
+
assert iscoroutinefunction(function) # nosec: B101
|
75
|
+
return cast(
|
76
|
+
Callable[Args, Coroutine[None, None, Result]],
|
77
|
+
_AsyncThrottle(
|
78
|
+
function,
|
79
|
+
limit=limit,
|
80
|
+
period=period,
|
81
|
+
),
|
82
|
+
)
|
83
|
+
|
84
|
+
if function := function:
|
85
|
+
return _wrap(function)
|
86
|
+
|
87
|
+
else:
|
88
|
+
return _wrap
|
89
|
+
|
90
|
+
|
91
|
+
class _AsyncThrottle[**Args, Result]:
|
92
|
+
def __init__(
|
93
|
+
self,
|
94
|
+
function: Callable[Args, Coroutine[None, None, Result]],
|
95
|
+
/,
|
96
|
+
limit: int,
|
97
|
+
period: timedelta | float,
|
98
|
+
) -> None:
|
99
|
+
self._function: Callable[Args, Coroutine[None, None, Result]] = function
|
100
|
+
self._entries: deque[float] = deque()
|
101
|
+
self._lock: Lock = Lock()
|
102
|
+
self._limit: int = limit
|
103
|
+
self._period: float
|
104
|
+
match period:
|
105
|
+
case timedelta() as delta:
|
106
|
+
self._period = delta.total_seconds()
|
107
|
+
|
108
|
+
case period_seconds:
|
109
|
+
self._period = period_seconds
|
110
|
+
|
111
|
+
# mimic function attributes if able
|
112
|
+
mimic_function(function, within=self)
|
113
|
+
|
114
|
+
async def __call__(
|
115
|
+
self,
|
116
|
+
*args: Args.args,
|
117
|
+
**kwargs: Args.kwargs,
|
118
|
+
) -> Result:
|
119
|
+
async with self._lock:
|
120
|
+
time_now: float = monotonic()
|
121
|
+
while self._entries: # cleanup old entries
|
122
|
+
if self._entries[0] + self._period <= time_now:
|
123
|
+
self._entries.popleft()
|
124
|
+
|
125
|
+
else:
|
126
|
+
break
|
127
|
+
|
128
|
+
if len(self._entries) >= self._limit:
|
129
|
+
await sleep(self._entries[0] - time_now)
|
130
|
+
|
131
|
+
self._entries.append(monotonic())
|
132
|
+
|
133
|
+
return await self._function(*args, **kwargs)
|
@@ -0,0 +1,112 @@
|
|
1
|
+
from asyncio import AbstractEventLoop, Future, Task, TimerHandle, get_running_loop
|
2
|
+
from collections.abc import Callable, Coroutine
|
3
|
+
|
4
|
+
from haiway.utils.mimic import mimic_function
|
5
|
+
|
6
|
+
__all__ = [
|
7
|
+
"timeout",
|
8
|
+
]
|
9
|
+
|
10
|
+
|
11
|
+
def timeout[**Args, Result](
|
12
|
+
timeout: float,
|
13
|
+
/,
|
14
|
+
) -> Callable[
|
15
|
+
[Callable[Args, Coroutine[None, None, Result]]],
|
16
|
+
Callable[Args, Coroutine[None, None, Result]],
|
17
|
+
]:
|
18
|
+
"""\
|
19
|
+
Timeout wrapper for a function call. \
|
20
|
+
When the timeout time will pass before function returns function execution will be \
|
21
|
+
cancelled and TimeoutError exception will raise. Make sure that wrapped \
|
22
|
+
function handles cancellation properly.
|
23
|
+
This wrapper is not thread safe.
|
24
|
+
|
25
|
+
Parameters
|
26
|
+
----------
|
27
|
+
timeout: float
|
28
|
+
timeout time in seconds
|
29
|
+
|
30
|
+
Returns
|
31
|
+
-------
|
32
|
+
Callable[[Callable[_Args, _Result]], Callable[_Args, _Result]] | Callable[_Args, _Result]
|
33
|
+
function wrapper adding timeout
|
34
|
+
"""
|
35
|
+
|
36
|
+
def _wrap(
|
37
|
+
function: Callable[Args, Coroutine[None, None, Result]],
|
38
|
+
) -> Callable[Args, Coroutine[None, None, Result]]:
|
39
|
+
return _AsyncTimeout(
|
40
|
+
function,
|
41
|
+
timeout=timeout,
|
42
|
+
)
|
43
|
+
|
44
|
+
return _wrap
|
45
|
+
|
46
|
+
|
47
|
+
class _AsyncTimeout[**Args, Result]:
|
48
|
+
def __init__(
|
49
|
+
self,
|
50
|
+
function: Callable[Args, Coroutine[None, None, Result]],
|
51
|
+
/,
|
52
|
+
timeout: float,
|
53
|
+
) -> None:
|
54
|
+
self._function: Callable[Args, Coroutine[None, None, Result]] = function
|
55
|
+
self._timeout: float = timeout
|
56
|
+
|
57
|
+
# mimic function attributes if able
|
58
|
+
mimic_function(function, within=self)
|
59
|
+
|
60
|
+
async def __call__(
|
61
|
+
self,
|
62
|
+
*args: Args.args,
|
63
|
+
**kwargs: Args.kwargs,
|
64
|
+
) -> Result:
|
65
|
+
loop: AbstractEventLoop = get_running_loop()
|
66
|
+
future: Future[Result] = loop.create_future()
|
67
|
+
task: Task[Result] = loop.create_task(
|
68
|
+
self._function(
|
69
|
+
*args,
|
70
|
+
**kwargs,
|
71
|
+
),
|
72
|
+
)
|
73
|
+
|
74
|
+
def on_timeout(
|
75
|
+
future: Future[Result],
|
76
|
+
) -> None:
|
77
|
+
if future.done():
|
78
|
+
return # ignore if already finished
|
79
|
+
|
80
|
+
# result future on its completion will ensure that task will complete
|
81
|
+
future.set_exception(TimeoutError())
|
82
|
+
|
83
|
+
timeout_handle: TimerHandle = loop.call_later(
|
84
|
+
self._timeout,
|
85
|
+
on_timeout,
|
86
|
+
future,
|
87
|
+
)
|
88
|
+
|
89
|
+
def on_completion(
|
90
|
+
task: Task[Result],
|
91
|
+
) -> None:
|
92
|
+
timeout_handle.cancel() # at this stage we no longer need timeout to trigger
|
93
|
+
|
94
|
+
if future.done():
|
95
|
+
return # ignore if already finished
|
96
|
+
|
97
|
+
try:
|
98
|
+
future.set_result(task.result())
|
99
|
+
|
100
|
+
except Exception as exc:
|
101
|
+
future.set_exception(exc)
|
102
|
+
|
103
|
+
task.add_done_callback(on_completion)
|
104
|
+
|
105
|
+
def on_result(
|
106
|
+
future: Future[Result],
|
107
|
+
) -> None:
|
108
|
+
task.cancel() # when result future completes make sure that task also completes
|
109
|
+
|
110
|
+
future.add_done_callback(on_result)
|
111
|
+
|
112
|
+
return await future
|
@@ -0,0 +1,137 @@
|
|
1
|
+
from asyncio import iscoroutinefunction
|
2
|
+
from collections.abc import Callable, Coroutine, Mapping, Sequence
|
3
|
+
from typing import Any, Self, cast
|
4
|
+
|
5
|
+
from haiway.context import ctx
|
6
|
+
from haiway.state import State
|
7
|
+
from haiway.types import MISSING, Missing
|
8
|
+
from haiway.utils import mimic_function
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"ArgumentsTrace",
|
12
|
+
"ResultTrace",
|
13
|
+
"traced",
|
14
|
+
]
|
15
|
+
|
16
|
+
|
17
|
+
class ArgumentsTrace(State):
|
18
|
+
if __debug__:
|
19
|
+
|
20
|
+
@classmethod
|
21
|
+
def of(cls, *args: Any, **kwargs: Any) -> Self:
|
22
|
+
return cls(
|
23
|
+
args=args if args else MISSING,
|
24
|
+
kwargs=kwargs if kwargs else MISSING,
|
25
|
+
)
|
26
|
+
|
27
|
+
else: # remove tracing for non debug runs to prevent accidental secret leaks
|
28
|
+
|
29
|
+
@classmethod
|
30
|
+
def of(cls, *args: Any, **kwargs: Any) -> Self:
|
31
|
+
return cls(
|
32
|
+
args=MISSING,
|
33
|
+
kwargs=MISSING,
|
34
|
+
)
|
35
|
+
|
36
|
+
args: Sequence[Any] | Missing
|
37
|
+
kwargs: Mapping[str, Any] | Missing
|
38
|
+
|
39
|
+
|
40
|
+
class ResultTrace(State):
|
41
|
+
if __debug__:
|
42
|
+
|
43
|
+
@classmethod
|
44
|
+
def of(
|
45
|
+
cls,
|
46
|
+
value: Any,
|
47
|
+
/,
|
48
|
+
) -> Self:
|
49
|
+
return cls(result=value)
|
50
|
+
|
51
|
+
else: # remove tracing for non debug runs to prevent accidental secret leaks
|
52
|
+
|
53
|
+
@classmethod
|
54
|
+
def of(
|
55
|
+
cls,
|
56
|
+
value: Any,
|
57
|
+
/,
|
58
|
+
) -> Self:
|
59
|
+
return cls(result=MISSING)
|
60
|
+
|
61
|
+
result: Any | Missing
|
62
|
+
|
63
|
+
|
64
|
+
def traced[**Args, Result](
|
65
|
+
function: Callable[Args, Result],
|
66
|
+
/,
|
67
|
+
) -> Callable[Args, Result]:
|
68
|
+
if __debug__:
|
69
|
+
if iscoroutinefunction(function):
|
70
|
+
return cast(
|
71
|
+
Callable[Args, Result],
|
72
|
+
_traced_async(
|
73
|
+
function,
|
74
|
+
label=function.__name__,
|
75
|
+
),
|
76
|
+
)
|
77
|
+
|
78
|
+
else:
|
79
|
+
return _traced_sync(
|
80
|
+
function,
|
81
|
+
label=function.__name__,
|
82
|
+
)
|
83
|
+
|
84
|
+
else: # do not trace on non debug runs
|
85
|
+
return function
|
86
|
+
|
87
|
+
|
88
|
+
def _traced_sync[**Args, Result](
|
89
|
+
function: Callable[Args, Result],
|
90
|
+
/,
|
91
|
+
label: str,
|
92
|
+
) -> Callable[Args, Result]:
|
93
|
+
def traced(
|
94
|
+
*args: Args.args,
|
95
|
+
**kwargs: Args.kwargs,
|
96
|
+
) -> Result:
|
97
|
+
with ctx.scope(label):
|
98
|
+
ctx.record(ArgumentsTrace.of(*args, **kwargs))
|
99
|
+
try:
|
100
|
+
result: Result = function(*args, **kwargs)
|
101
|
+
ctx.record(ResultTrace.of(result))
|
102
|
+
return result
|
103
|
+
|
104
|
+
except BaseException as exc:
|
105
|
+
ctx.record(ResultTrace.of(f"{type(exc)}: {exc}"))
|
106
|
+
raise exc
|
107
|
+
|
108
|
+
return mimic_function(
|
109
|
+
function,
|
110
|
+
within=traced,
|
111
|
+
)
|
112
|
+
|
113
|
+
|
114
|
+
def _traced_async[**Args, Result](
|
115
|
+
function: Callable[Args, Coroutine[Any, Any, Result]],
|
116
|
+
/,
|
117
|
+
label: str,
|
118
|
+
) -> Callable[Args, Coroutine[Any, Any, Result]]:
|
119
|
+
async def traced(
|
120
|
+
*args: Args.args,
|
121
|
+
**kwargs: Args.kwargs,
|
122
|
+
) -> Result:
|
123
|
+
with ctx.scope(label):
|
124
|
+
ctx.record(ArgumentsTrace.of(*args, **kwargs))
|
125
|
+
try:
|
126
|
+
result: Result = await function(*args, **kwargs)
|
127
|
+
ctx.record(ResultTrace.of(result))
|
128
|
+
return result
|
129
|
+
|
130
|
+
except BaseException as exc:
|
131
|
+
ctx.record(ResultTrace.of(f"{type(exc)}: {exc}"))
|
132
|
+
raise exc
|
133
|
+
|
134
|
+
return mimic_function(
|
135
|
+
function,
|
136
|
+
within=traced,
|
137
|
+
)
|
haiway/py.typed
ADDED
File without changes
|
haiway/state/__init__.py
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
from haiway.state.attributes import AttributeAnnotation, attribute_annotations
|
2
|
+
from haiway.state.path import AttributePath
|
3
|
+
from haiway.state.requirement import AttributeRequirement
|
4
|
+
from haiway.state.structure import State
|
5
|
+
|
6
|
+
__all__ = [
|
7
|
+
"AttributeAnnotation",
|
8
|
+
"AttributePath",
|
9
|
+
"AttributeRequirement",
|
10
|
+
"State",
|
11
|
+
"attribute_annotations",
|
12
|
+
]
|