haiway 0.10.15__py3-none-any.whl → 0.10.17__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 +542 -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.15.dist-info → haiway-0.10.17.dist-info}/METADATA +1 -1
  40. haiway-0.10.17.dist-info/RECORD +42 -0
  41. haiway-0.10.15.dist-info/RECORD +0 -4
  42. {haiway-0.10.15.dist-info → haiway-0.10.17.dist-info}/WHEEL +0 -0
  43. {haiway-0.10.15.dist-info → haiway-0.10.17.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
@@ -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
+ ]