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.
Files changed (43) hide show
  1. haiway/__init__.py +111 -0
  2. haiway/context/__init__.py +27 -0
  3. haiway/context/access.py +615 -0
  4. haiway/context/disposables.py +78 -0
  5. haiway/context/identifier.py +92 -0
  6. haiway/context/logging.py +176 -0
  7. haiway/context/metrics.py +165 -0
  8. haiway/context/state.py +113 -0
  9. haiway/context/tasks.py +64 -0
  10. haiway/context/types.py +12 -0
  11. haiway/helpers/__init__.py +21 -0
  12. haiway/helpers/asynchrony.py +225 -0
  13. haiway/helpers/caching.py +326 -0
  14. haiway/helpers/metrics.py +459 -0
  15. haiway/helpers/retries.py +223 -0
  16. haiway/helpers/throttling.py +133 -0
  17. haiway/helpers/timeouted.py +112 -0
  18. haiway/helpers/tracing.py +137 -0
  19. haiway/py.typed +0 -0
  20. haiway/state/__init__.py +12 -0
  21. haiway/state/attributes.py +747 -0
  22. haiway/state/path.py +524 -0
  23. haiway/state/requirement.py +229 -0
  24. haiway/state/structure.py +414 -0
  25. haiway/state/validation.py +468 -0
  26. haiway/types/__init__.py +14 -0
  27. haiway/types/default.py +108 -0
  28. haiway/types/frozen.py +5 -0
  29. haiway/types/missing.py +95 -0
  30. haiway/utils/__init__.py +28 -0
  31. haiway/utils/always.py +61 -0
  32. haiway/utils/collections.py +185 -0
  33. haiway/utils/env.py +230 -0
  34. haiway/utils/freezing.py +28 -0
  35. haiway/utils/logs.py +57 -0
  36. haiway/utils/mimic.py +77 -0
  37. haiway/utils/noop.py +24 -0
  38. haiway/utils/queue.py +82 -0
  39. {haiway-0.10.14.dist-info → haiway-0.10.16.dist-info}/METADATA +1 -1
  40. haiway-0.10.16.dist-info/RECORD +42 -0
  41. haiway-0.10.14.dist-info/RECORD +0 -4
  42. {haiway-0.10.14.dist-info → haiway-0.10.16.dist-info}/WHEEL +0 -0
  43. {haiway-0.10.14.dist-info → haiway-0.10.16.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,225 @@
1
+ from asyncio import AbstractEventLoop, get_running_loop, iscoroutinefunction
2
+ from collections.abc import Callable, Coroutine
3
+ from concurrent.futures import Executor
4
+ from contextvars import Context, copy_context
5
+ from functools import partial
6
+ from typing import Any, cast, overload
7
+
8
+ from haiway.types.missing import MISSING, Missing
9
+
10
+ __all__ = [
11
+ "asynchronous",
12
+ "wrap_async",
13
+ ]
14
+
15
+
16
+ def wrap_async[**Args, Result](
17
+ function: Callable[Args, Coroutine[None, None, Result]] | Callable[Args, Result],
18
+ /,
19
+ ) -> Callable[Args, Coroutine[None, None, Result]]:
20
+ if iscoroutinefunction(function):
21
+ return function
22
+
23
+ else:
24
+
25
+ async def async_function(*args: Args.args, **kwargs: Args.kwargs) -> Result:
26
+ return cast(Callable[Args, Result], function)(*args, **kwargs)
27
+
28
+ _mimic_async(function, within=async_function)
29
+ return async_function
30
+
31
+
32
+ @overload
33
+ def asynchronous[**Args, Result]() -> Callable[
34
+ [Callable[Args, Result]],
35
+ Callable[Args, Coroutine[None, None, Result]],
36
+ ]: ...
37
+
38
+
39
+ @overload
40
+ def asynchronous[**Args, Result](
41
+ *,
42
+ loop: AbstractEventLoop | None = None,
43
+ executor: Executor,
44
+ ) -> Callable[
45
+ [Callable[Args, Result]],
46
+ Callable[Args, Coroutine[None, None, Result]],
47
+ ]: ...
48
+
49
+
50
+ @overload
51
+ def asynchronous[**Args, Result](
52
+ function: Callable[Args, Result],
53
+ /,
54
+ ) -> Callable[Args, Coroutine[None, None, Result]]: ...
55
+
56
+
57
+ def asynchronous[**Args, Result](
58
+ function: Callable[Args, Result] | None = None,
59
+ /,
60
+ loop: AbstractEventLoop | None = None,
61
+ executor: Executor | Missing = MISSING,
62
+ ) -> (
63
+ Callable[
64
+ [Callable[Args, Result]],
65
+ Callable[Args, Coroutine[None, None, Result]],
66
+ ]
67
+ | Callable[Args, Coroutine[None, None, Result]]
68
+ ):
69
+ """\
70
+ Wrapper for a sync function to convert it to an async function. \
71
+ When specified an executor, it can be used to wrap long running or blocking synchronous \
72
+ operations within coroutines system.
73
+
74
+ Parameters
75
+ ----------
76
+ function: Callable[Args, Result]
77
+ function to be wrapped as running in loop executor.
78
+ loop: AbstractEventLoop | None
79
+ loop used to call the function. When None was provided the loop currently running while \
80
+ executing the function will be used. Default is None.
81
+ executor: Executor | Missing
82
+ executor used to run the function. When not provided (Missing) default loop executor\
83
+ will be used.
84
+
85
+ Returns
86
+ -------
87
+ Callable[_Args, _Result]
88
+ function wrapped to async using loop executor.
89
+ """
90
+
91
+ def wrap(
92
+ wrapped: Callable[Args, Result],
93
+ ) -> Callable[Args, Coroutine[None, None, Result]]:
94
+ assert not iscoroutinefunction(wrapped), "Cannot wrap async function in executor" # nosec: B101
95
+
96
+ return _ExecutorWrapper(
97
+ wrapped,
98
+ loop=loop,
99
+ executor=cast(Executor | None, None if executor is MISSING else executor),
100
+ )
101
+
102
+ if function := function:
103
+ return wrap(wrapped=function)
104
+
105
+ else:
106
+ return wrap
107
+
108
+
109
+ class _ExecutorWrapper[**Args, Result]:
110
+ def __init__(
111
+ self,
112
+ function: Callable[Args, Result],
113
+ /,
114
+ loop: AbstractEventLoop | None,
115
+ executor: Executor | None,
116
+ ) -> None:
117
+ self._function: Callable[Args, Result] = function
118
+ self._loop: AbstractEventLoop | None = loop
119
+ self._executor: Executor | None = executor
120
+
121
+ # mimic function attributes if able
122
+ _mimic_async(function, within=self)
123
+
124
+ async def __call__(
125
+ self,
126
+ *args: Args.args,
127
+ **kwargs: Args.kwargs,
128
+ ) -> Result:
129
+ context: Context = copy_context()
130
+ return await (self._loop or get_running_loop()).run_in_executor(
131
+ self._executor,
132
+ context.run,
133
+ partial(self._function, *args, **kwargs),
134
+ )
135
+
136
+ def __get__(
137
+ self,
138
+ instance: object,
139
+ owner: type | None = None,
140
+ /,
141
+ ) -> Callable[Args, Coroutine[None, None, Result]]:
142
+ if owner is None:
143
+ return self
144
+
145
+ else:
146
+ return _mimic_async(
147
+ self._function,
148
+ within=partial(
149
+ self.__method_call__,
150
+ instance,
151
+ ),
152
+ )
153
+
154
+ async def __method_call__(
155
+ self,
156
+ __method_self: object,
157
+ *args: Args.args,
158
+ **kwargs: Args.kwargs,
159
+ ) -> Result:
160
+ return await (self._loop or get_running_loop()).run_in_executor(
161
+ self._executor,
162
+ partial(self._function, __method_self, *args, **kwargs),
163
+ )
164
+
165
+
166
+ def _mimic_async[**Args, Result](
167
+ function: Callable[Args, Result],
168
+ /,
169
+ within: Callable[..., Coroutine[None, None, Result]],
170
+ ) -> Callable[Args, Coroutine[None, None, Result]]:
171
+ try:
172
+ annotations: Any = getattr( # noqa: B009
173
+ function,
174
+ "__annotations__",
175
+ )
176
+ setattr( # noqa: B010
177
+ within,
178
+ "__annotations__",
179
+ {
180
+ **annotations,
181
+ "return": Coroutine[None, None, annotations.get("return", Any)],
182
+ },
183
+ )
184
+
185
+ except AttributeError:
186
+ pass
187
+
188
+ for attribute in (
189
+ "__module__",
190
+ "__name__",
191
+ "__qualname__",
192
+ "__doc__",
193
+ "__type_params__",
194
+ "__defaults__",
195
+ "__kwdefaults__",
196
+ "__globals__",
197
+ ):
198
+ try:
199
+ setattr(
200
+ within,
201
+ attribute,
202
+ getattr(
203
+ function,
204
+ attribute,
205
+ ),
206
+ )
207
+
208
+ except AttributeError:
209
+ pass
210
+ try:
211
+ within.__dict__.update(function.__dict__)
212
+
213
+ except AttributeError:
214
+ pass
215
+
216
+ setattr( # noqa: B010 - mimic functools.wraps behavior for correct signature checks
217
+ within,
218
+ "__wrapped__",
219
+ function,
220
+ )
221
+
222
+ return cast(
223
+ Callable[Args, Coroutine[None, None, Result]],
224
+ within,
225
+ )
@@ -0,0 +1,326 @@
1
+ from asyncio import AbstractEventLoop, Task, get_running_loop, iscoroutinefunction, shield
2
+ from collections import OrderedDict
3
+ from collections.abc import Callable, Coroutine, Hashable
4
+ from functools import _make_key, partial # pyright: ignore[reportPrivateUsage]
5
+ from time import monotonic
6
+ from typing import NamedTuple, cast, overload
7
+ from weakref import ref
8
+
9
+ from haiway.utils.mimic import mimic_function
10
+
11
+ __all__ = [
12
+ "cache",
13
+ ]
14
+
15
+
16
+ @overload
17
+ def cache[**Args, Result](
18
+ function: Callable[Args, Result],
19
+ /,
20
+ ) -> Callable[Args, Result]: ...
21
+
22
+
23
+ @overload
24
+ def cache[**Args, Result](
25
+ *,
26
+ limit: int = 1,
27
+ expiration: float | None = None,
28
+ ) -> Callable[[Callable[Args, Result]], Callable[Args, Result]]: ...
29
+
30
+
31
+ def cache[**Args, Result](
32
+ function: Callable[Args, Result] | None = None,
33
+ *,
34
+ limit: int = 1,
35
+ expiration: float | None = None,
36
+ ) -> Callable[[Callable[Args, Result]], Callable[Args, Result]] | Callable[Args, Result]:
37
+ """\
38
+ Simple lru function result cache with optional expire time. \
39
+ Works for both sync and async functions. \
40
+ It is not allowed to be used on class methods. \
41
+ This wrapper is not thread safe.
42
+
43
+ Parameters
44
+ ----------
45
+ function: Callable[_Args, _Result]
46
+ function to wrap in cache, either sync or async
47
+ limit: int
48
+ limit of cache entries to keep, default is 1
49
+ expiration: float | None
50
+ entries expiration time in seconds, default is None (not expiring)
51
+
52
+ Returns
53
+ -------
54
+ Callable[[Callable[_Args, _Result]], Callable[_Args, _Result]] | Callable[_Args, _Result]
55
+ provided function wrapped in cache
56
+ """
57
+
58
+ def _wrap(function: Callable[Args, Result]) -> Callable[Args, Result]:
59
+ if iscoroutinefunction(function):
60
+ return cast(
61
+ Callable[Args, Result],
62
+ _AsyncCache(
63
+ function,
64
+ limit=limit,
65
+ expiration=expiration,
66
+ ),
67
+ )
68
+
69
+ else:
70
+ return cast(
71
+ Callable[Args, Result],
72
+ _SyncCache(
73
+ function,
74
+ limit=limit,
75
+ expiration=expiration,
76
+ ),
77
+ )
78
+
79
+ if function := function:
80
+ return _wrap(function)
81
+
82
+ else:
83
+ return _wrap
84
+
85
+
86
+ class _CacheEntry[Entry](NamedTuple):
87
+ value: Entry
88
+ expire: float | None
89
+
90
+
91
+ class _SyncCache[**Args, Result]:
92
+ def __init__(
93
+ self,
94
+ function: Callable[Args, Result],
95
+ /,
96
+ limit: int,
97
+ expiration: float | None,
98
+ ) -> None:
99
+ self._function: Callable[Args, Result] = function
100
+ self._cached: OrderedDict[Hashable, _CacheEntry[Result]] = OrderedDict()
101
+ self._limit: int = limit
102
+ if expiration := expiration:
103
+
104
+ def next_expire_time() -> float | None:
105
+ return monotonic() + expiration
106
+
107
+ else:
108
+
109
+ def next_expire_time() -> float | None:
110
+ return None
111
+
112
+ self._next_expire_time: Callable[[], float | None] = next_expire_time
113
+
114
+ # mimic function attributes if able
115
+ mimic_function(function, within=self)
116
+
117
+ def __get__(
118
+ self,
119
+ instance: object | None,
120
+ owner: type | None = None,
121
+ /,
122
+ ) -> Callable[Args, Result]:
123
+ if owner is None or instance is None:
124
+ return self
125
+
126
+ else:
127
+ return mimic_function(
128
+ self._function,
129
+ within=partial(
130
+ self.__method_call__,
131
+ instance,
132
+ ),
133
+ )
134
+
135
+ def __call__(
136
+ self,
137
+ *args: Args.args,
138
+ **kwargs: Args.kwargs,
139
+ ) -> Result:
140
+ key: Hashable = _make_key(
141
+ args=args,
142
+ kwds=kwargs,
143
+ typed=True,
144
+ )
145
+
146
+ match self._cached.get(key):
147
+ case None:
148
+ pass
149
+
150
+ case entry:
151
+ if (expire := entry[1]) and expire < monotonic():
152
+ # if still running let it complete if able
153
+ del self._cached[key] # continue the same way as if empty
154
+
155
+ else:
156
+ self._cached.move_to_end(key)
157
+ return entry[0]
158
+
159
+ result: Result = self._function(*args, **kwargs)
160
+ self._cached[key] = _CacheEntry(
161
+ value=result,
162
+ expire=self._next_expire_time(),
163
+ )
164
+
165
+ if len(self._cached) > self._limit:
166
+ # if still running let it complete if able
167
+ self._cached.popitem(last=False)
168
+
169
+ return result
170
+
171
+ def __method_call__(
172
+ self,
173
+ __method_self: object,
174
+ *args: Args.args,
175
+ **kwargs: Args.kwargs,
176
+ ) -> Result:
177
+ key: Hashable = _make_key(
178
+ args=(ref(__method_self), *args),
179
+ kwds=kwargs,
180
+ typed=True,
181
+ )
182
+
183
+ match self._cached.get(key):
184
+ case None:
185
+ pass
186
+
187
+ case entry:
188
+ if (expire := entry[1]) and expire < monotonic():
189
+ # if still running let it complete if able
190
+ del self._cached[key] # continue the same way as if empty
191
+
192
+ else:
193
+ self._cached.move_to_end(key)
194
+ return entry[0]
195
+
196
+ result: Result = self._function(__method_self, *args, **kwargs) # pyright: ignore[reportUnknownVariableType, reportCallIssue]
197
+ self._cached[key] = _CacheEntry(
198
+ value=result, # pyright: ignore[reportUnknownArgumentType]
199
+ expire=self._next_expire_time(),
200
+ )
201
+ if len(self._cached) > self._limit:
202
+ # if still running let it complete if able
203
+ self._cached.popitem(last=False)
204
+
205
+ return result # pyright: ignore[reportUnknownArgumentType, reportUnknownVariableType]
206
+
207
+
208
+ class _AsyncCache[**Args, Result]:
209
+ def __init__(
210
+ self,
211
+ function: Callable[Args, Coroutine[None, None, Result]],
212
+ /,
213
+ limit: int,
214
+ expiration: float | None,
215
+ ) -> None:
216
+ self._function: Callable[Args, Coroutine[None, None, Result]] = function
217
+ self._cached: OrderedDict[Hashable, _CacheEntry[Task[Result]]] = OrderedDict()
218
+ self._limit: int = limit
219
+ if expiration := expiration:
220
+
221
+ def next_expire_time() -> float | None:
222
+ return monotonic() + expiration
223
+
224
+ else:
225
+
226
+ def next_expire_time() -> float | None:
227
+ return None
228
+
229
+ self._next_expire_time: Callable[[], float | None] = next_expire_time
230
+
231
+ # mimic function attributes if able
232
+ mimic_function(function, within=self)
233
+
234
+ def __get__(
235
+ self,
236
+ instance: object | None,
237
+ owner: type | None = None,
238
+ /,
239
+ ) -> Callable[Args, Coroutine[None, None, Result]]:
240
+ if owner is None or instance is None:
241
+ return self
242
+
243
+ else:
244
+ return mimic_function(
245
+ self._function,
246
+ within=partial(
247
+ self.__method_call__,
248
+ instance,
249
+ ),
250
+ )
251
+
252
+ async def __call__(
253
+ self,
254
+ *args: Args.args,
255
+ **kwargs: Args.kwargs,
256
+ ) -> Result:
257
+ loop: AbstractEventLoop = get_running_loop()
258
+ key: Hashable = _make_key(
259
+ args=args,
260
+ kwds=kwargs,
261
+ typed=True,
262
+ )
263
+
264
+ match self._cached.get(key):
265
+ case None:
266
+ pass
267
+
268
+ case entry:
269
+ if (expire := entry[1]) and expire < monotonic():
270
+ # if still running let it complete if able
271
+ del self._cached[key] # continue the same way as if empty
272
+
273
+ else:
274
+ self._cached.move_to_end(key)
275
+ return await shield(entry[0])
276
+
277
+ task: Task[Result] = loop.create_task(self._function(*args, **kwargs)) # pyright: ignore[reportCallIssue]
278
+ self._cached[key] = _CacheEntry(
279
+ value=task,
280
+ expire=self._next_expire_time(),
281
+ )
282
+ if len(self._cached) > self._limit:
283
+ # if still running let it complete if able
284
+ self._cached.popitem(last=False)
285
+
286
+ return await shield(task)
287
+
288
+ async def __method_call__(
289
+ self,
290
+ __method_self: object,
291
+ *args: Args.args,
292
+ **kwargs: Args.kwargs,
293
+ ) -> Result:
294
+ loop: AbstractEventLoop = get_running_loop()
295
+ key: Hashable = _make_key(
296
+ args=(ref(__method_self), *args),
297
+ kwds=kwargs,
298
+ typed=True,
299
+ )
300
+
301
+ match self._cached.get(key):
302
+ case None:
303
+ pass
304
+
305
+ case entry:
306
+ if (expire := entry[1]) and expire < monotonic():
307
+ # if still running let it complete if able
308
+ del self._cached[key] # continue the same way as if empty
309
+
310
+ else:
311
+ self._cached.move_to_end(key)
312
+ return await shield(entry[0])
313
+
314
+ task: Task[Result] = loop.create_task(
315
+ self._function(__method_self, *args, **kwargs), # pyright: ignore[reportCallIssue, reportUnknownArgumentType]
316
+ )
317
+ self._cached[key] = _CacheEntry(
318
+ value=task,
319
+ expire=self._next_expire_time(),
320
+ )
321
+
322
+ if len(self._cached) > self._limit:
323
+ # if still running let it complete if able
324
+ self._cached.popitem(last=False)
325
+
326
+ return await shield(task)