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/helpers/retry.py
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
from asyncio import CancelledError, iscoroutinefunction, sleep
|
2
|
+
from collections.abc import Callable, Coroutine
|
3
|
+
from typing import cast, overload
|
4
|
+
|
5
|
+
from haiway.context import ctx
|
6
|
+
from haiway.utils import mimic_function
|
7
|
+
|
8
|
+
__all__ = [
|
9
|
+
"auto_retry",
|
10
|
+
]
|
11
|
+
|
12
|
+
|
13
|
+
@overload
|
14
|
+
def auto_retry[**Args, Result](
|
15
|
+
function: Callable[Args, Result],
|
16
|
+
/,
|
17
|
+
) -> Callable[Args, Result]:
|
18
|
+
"""\
|
19
|
+
Function wrapper retrying the wrapped function again on fail. \
|
20
|
+
Works for both sync and async functions. \
|
21
|
+
It is not allowed to be used on class methods. \
|
22
|
+
This wrapper is not thread safe.
|
23
|
+
|
24
|
+
Parameters
|
25
|
+
----------
|
26
|
+
function: Callable[_Args_T, _Result_T]
|
27
|
+
function to wrap in auto retry, either sync or async.
|
28
|
+
|
29
|
+
Returns
|
30
|
+
-------
|
31
|
+
Callable[_Args_T, _Result_T]
|
32
|
+
provided function wrapped in auto retry with default configuration.
|
33
|
+
"""
|
34
|
+
|
35
|
+
|
36
|
+
@overload
|
37
|
+
def auto_retry[**Args, Result](
|
38
|
+
*,
|
39
|
+
limit: int = 1,
|
40
|
+
delay: Callable[[int, Exception], float] | float | None = None,
|
41
|
+
catching: set[type[Exception]] | tuple[type[Exception], ...] | type[Exception] = Exception,
|
42
|
+
) -> Callable[[Callable[Args, Result]], Callable[Args, Result]]:
|
43
|
+
"""\
|
44
|
+
Function wrapper retrying the wrapped function again on fail. \
|
45
|
+
Works for both sync and async functions. \
|
46
|
+
It is not allowed to be used on class methods. \
|
47
|
+
This wrapper is not thread safe.
|
48
|
+
|
49
|
+
Parameters
|
50
|
+
----------
|
51
|
+
limit: int
|
52
|
+
limit of retries, default is 1
|
53
|
+
delay: Callable[[int, Exception], float] | float | None
|
54
|
+
retry delay time in seconds, either concrete value or a function producing it, \
|
55
|
+
default is None (no delay)
|
56
|
+
catching: set[type[Exception]] | type[Exception] | None
|
57
|
+
Exception types that are triggering auto retry. Retry will trigger only when \
|
58
|
+
exceptions of matching types (including subclasses) will occur. CancelledError \
|
59
|
+
will be always propagated even if specified explicitly.
|
60
|
+
Default is Exception - all subclasses of Exception will be handled.
|
61
|
+
|
62
|
+
Returns
|
63
|
+
-------
|
64
|
+
Callable[[Callable[_Args_T, _Result_T]], Callable[_Args_T, _Result_T]]
|
65
|
+
function wrapper for adding auto retry
|
66
|
+
"""
|
67
|
+
|
68
|
+
|
69
|
+
def auto_retry[**Args, Result](
|
70
|
+
function: Callable[Args, Result] | None = None,
|
71
|
+
*,
|
72
|
+
limit: int = 1,
|
73
|
+
delay: Callable[[int, Exception], float] | float | None = None,
|
74
|
+
catching: set[type[Exception]] | tuple[type[Exception], ...] | type[Exception] = Exception,
|
75
|
+
) -> Callable[[Callable[Args, Result]], Callable[Args, Result]] | Callable[Args, Result]:
|
76
|
+
"""\
|
77
|
+
Function wrapper retrying the wrapped function again on fail. \
|
78
|
+
Works for both sync and async functions. \
|
79
|
+
It is not allowed to be used on class methods. \
|
80
|
+
This wrapper is not thread safe.
|
81
|
+
|
82
|
+
Parameters
|
83
|
+
----------
|
84
|
+
function: Callable[_Args_T, _Result_T]
|
85
|
+
function to wrap in auto retry, either sync or async.
|
86
|
+
limit: int
|
87
|
+
limit of retries, default is 1
|
88
|
+
delay: Callable[[int, Exception], float] | float | None
|
89
|
+
retry delay time in seconds, either concrete value or a function producing it, \
|
90
|
+
default is None (no delay)
|
91
|
+
catching: set[type[Exception]] | type[Exception] | None
|
92
|
+
Exception types that are triggering auto retry. Retry will trigger only when \
|
93
|
+
exceptions of matching types (including subclasses) will occur. CancelledError \
|
94
|
+
will be always propagated even if specified explicitly.
|
95
|
+
Default is Exception - all subclasses of Exception will be handled.
|
96
|
+
|
97
|
+
Returns
|
98
|
+
-------
|
99
|
+
Callable[[Callable[_Args_T, _Result_T]], Callable[_Args_T, _Result_T]] | \
|
100
|
+
Callable[_Args_T, _Result_T]
|
101
|
+
function wrapper for adding auto retry or a wrapped function
|
102
|
+
"""
|
103
|
+
|
104
|
+
def _wrap(
|
105
|
+
function: Callable[Args, Result],
|
106
|
+
/,
|
107
|
+
) -> Callable[Args, Result]:
|
108
|
+
if iscoroutinefunction(function):
|
109
|
+
return cast(
|
110
|
+
Callable[Args, Result],
|
111
|
+
_wrap_async(
|
112
|
+
function,
|
113
|
+
limit=limit,
|
114
|
+
delay=delay,
|
115
|
+
catching=catching if isinstance(catching, set | tuple) else {catching},
|
116
|
+
),
|
117
|
+
)
|
118
|
+
else:
|
119
|
+
assert delay is None, "Delay is not supported in sync wrapper" # nosec: B101
|
120
|
+
return _wrap_sync(
|
121
|
+
function,
|
122
|
+
limit=limit,
|
123
|
+
catching=catching if isinstance(catching, set | tuple) else {catching},
|
124
|
+
)
|
125
|
+
|
126
|
+
if function := function:
|
127
|
+
return _wrap(function)
|
128
|
+
else:
|
129
|
+
return _wrap
|
130
|
+
|
131
|
+
|
132
|
+
def _wrap_sync[**Args, Result](
|
133
|
+
function: Callable[Args, Result],
|
134
|
+
*,
|
135
|
+
limit: int,
|
136
|
+
catching: set[type[Exception]] | tuple[type[Exception], ...],
|
137
|
+
) -> Callable[Args, Result]:
|
138
|
+
assert limit > 0, "Limit has to be greater than zero" # nosec: B101
|
139
|
+
|
140
|
+
@mimic_function(function)
|
141
|
+
def wrapped(
|
142
|
+
*args: Args.args,
|
143
|
+
**kwargs: Args.kwargs,
|
144
|
+
) -> Result:
|
145
|
+
attempt: int = 0
|
146
|
+
while True:
|
147
|
+
try:
|
148
|
+
return function(*args, **kwargs)
|
149
|
+
except CancelledError as exc:
|
150
|
+
raise exc
|
151
|
+
|
152
|
+
except Exception as exc:
|
153
|
+
if attempt < limit and any(isinstance(exc, exception) for exception in catching):
|
154
|
+
attempt += 1
|
155
|
+
ctx.log_error(
|
156
|
+
"Attempting to retry %s which failed due to an error: %s",
|
157
|
+
function.__name__,
|
158
|
+
exc,
|
159
|
+
)
|
160
|
+
|
161
|
+
else:
|
162
|
+
raise exc
|
163
|
+
|
164
|
+
return wrapped
|
165
|
+
|
166
|
+
|
167
|
+
def _wrap_async[**Args, Result](
|
168
|
+
function: Callable[Args, Coroutine[None, None, Result]],
|
169
|
+
*,
|
170
|
+
limit: int,
|
171
|
+
delay: Callable[[int, Exception], float] | float | None,
|
172
|
+
catching: set[type[Exception]] | tuple[type[Exception], ...],
|
173
|
+
) -> Callable[Args, Coroutine[None, None, Result]]:
|
174
|
+
assert limit > 0, "Limit has to be greater than zero" # nosec: B101
|
175
|
+
|
176
|
+
@mimic_function(function)
|
177
|
+
async def wrapped(
|
178
|
+
*args: Args.args,
|
179
|
+
**kwargs: Args.kwargs,
|
180
|
+
) -> Result:
|
181
|
+
attempt: int = 0
|
182
|
+
while True:
|
183
|
+
try:
|
184
|
+
return await function(*args, **kwargs)
|
185
|
+
except CancelledError as exc:
|
186
|
+
raise exc
|
187
|
+
|
188
|
+
except Exception as exc:
|
189
|
+
if attempt < limit and any(isinstance(exc, exception) for exception in catching):
|
190
|
+
attempt += 1
|
191
|
+
ctx.log_error(
|
192
|
+
"Attempting to retry %s which failed due to an error",
|
193
|
+
function.__name__,
|
194
|
+
exception=exc,
|
195
|
+
)
|
196
|
+
|
197
|
+
match delay:
|
198
|
+
case None:
|
199
|
+
continue
|
200
|
+
|
201
|
+
case float(strict):
|
202
|
+
await sleep(delay=strict)
|
203
|
+
|
204
|
+
case make_delay: # type: Callable[[], float]
|
205
|
+
await sleep(delay=make_delay(attempt, exc)) # pyright: ignore[reportCallIssue, reportUnknownArgumentType]
|
206
|
+
|
207
|
+
else:
|
208
|
+
raise exc
|
209
|
+
|
210
|
+
return wrapped
|
@@ -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
|
+
"with_timeout",
|
8
|
+
]
|
9
|
+
|
10
|
+
|
11
|
+
def with_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
|
haiway/py.typed
ADDED
File without changes
|