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.
@@ -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
@@ -0,0 +1,8 @@
1
+ from haiway.state.attributes import AttributeAnnotation, attribute_annotations
2
+ from haiway.state.structure import Structure
3
+
4
+ __all__ = [
5
+ "attribute_annotations",
6
+ "AttributeAnnotation",
7
+ "Structure",
8
+ ]