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.
- 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 +542 -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.15.dist-info → haiway-0.10.17.dist-info}/METADATA +1 -1
- haiway-0.10.17.dist-info/RECORD +42 -0
- haiway-0.10.15.dist-info/RECORD +0 -4
- {haiway-0.10.15.dist-info → haiway-0.10.17.dist-info}/WHEEL +0 -0
- {haiway-0.10.15.dist-info → haiway-0.10.17.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
from contextvars import ContextVar, Token
|
2
|
+
from types import TracebackType
|
3
|
+
from typing import Any, Self, final
|
4
|
+
from uuid import uuid4
|
5
|
+
|
6
|
+
__all__ = [
|
7
|
+
"ScopeIdentifier",
|
8
|
+
]
|
9
|
+
|
10
|
+
|
11
|
+
@final
|
12
|
+
class ScopeIdentifier:
|
13
|
+
_context = ContextVar[Self]("ScopeIdentifier")
|
14
|
+
|
15
|
+
@classmethod
|
16
|
+
def current_trace_id(cls) -> str:
|
17
|
+
try:
|
18
|
+
return ScopeIdentifier._context.get().trace_id
|
19
|
+
|
20
|
+
except LookupError as exc:
|
21
|
+
raise RuntimeError("Attempting to access scope identifier outside of scope") from exc
|
22
|
+
|
23
|
+
@classmethod
|
24
|
+
def scope(
|
25
|
+
cls,
|
26
|
+
label: str,
|
27
|
+
/,
|
28
|
+
) -> Self:
|
29
|
+
current: Self
|
30
|
+
try: # check for current scope
|
31
|
+
current = cls._context.get()
|
32
|
+
|
33
|
+
except LookupError:
|
34
|
+
# create root scope when missing
|
35
|
+
trace_id: str = uuid4().hex
|
36
|
+
return cls(
|
37
|
+
label=label,
|
38
|
+
scope_id=uuid4().hex,
|
39
|
+
parent_id=trace_id, # trace_id is parent_id for root
|
40
|
+
trace_id=trace_id,
|
41
|
+
)
|
42
|
+
|
43
|
+
# create nested scope otherwise
|
44
|
+
return cls(
|
45
|
+
label=label,
|
46
|
+
scope_id=uuid4().hex,
|
47
|
+
parent_id=current.scope_id,
|
48
|
+
trace_id=current.trace_id,
|
49
|
+
)
|
50
|
+
|
51
|
+
def __init__(
|
52
|
+
self,
|
53
|
+
trace_id: str,
|
54
|
+
parent_id: str,
|
55
|
+
scope_id: str,
|
56
|
+
label: str,
|
57
|
+
) -> None:
|
58
|
+
self.trace_id: str = trace_id
|
59
|
+
self.parent_id: str = parent_id
|
60
|
+
self.scope_id: str = scope_id
|
61
|
+
self.label: str = label
|
62
|
+
self.unique_name: str = f"[{trace_id}] [{label}] [{scope_id}]"
|
63
|
+
|
64
|
+
@property
|
65
|
+
def is_root(self) -> bool:
|
66
|
+
return self.trace_id == self.parent_id
|
67
|
+
|
68
|
+
def __str__(self) -> str:
|
69
|
+
return self.unique_name
|
70
|
+
|
71
|
+
def __eq__(self, other: Any) -> bool:
|
72
|
+
if not isinstance(other, self.__class__):
|
73
|
+
return False
|
74
|
+
|
75
|
+
return self.scope_id == other.scope_id and self.trace_id == other.trace_id
|
76
|
+
|
77
|
+
def __hash__(self) -> int:
|
78
|
+
return hash(self.scope_id)
|
79
|
+
|
80
|
+
def __enter__(self) -> None:
|
81
|
+
assert not hasattr(self, "_token"), "Context reentrance is not allowed" # nosec: B101
|
82
|
+
self._token: Token[ScopeIdentifier] = ScopeIdentifier._context.set(self)
|
83
|
+
|
84
|
+
def __exit__(
|
85
|
+
self,
|
86
|
+
exc_type: type[BaseException] | None,
|
87
|
+
exc_val: BaseException | None,
|
88
|
+
exc_tb: TracebackType | None,
|
89
|
+
) -> None:
|
90
|
+
assert hasattr(self, "_token"), "Unbalanced context enter/exit" # nosec: B101
|
91
|
+
ScopeIdentifier._context.reset(self._token)
|
92
|
+
del self._token
|
@@ -0,0 +1,176 @@
|
|
1
|
+
from contextvars import ContextVar, Token
|
2
|
+
from logging import DEBUG, ERROR, INFO, WARNING, Logger, getLogger
|
3
|
+
from time import monotonic
|
4
|
+
from types import TracebackType
|
5
|
+
from typing import Any, Self, final
|
6
|
+
|
7
|
+
from haiway.context.identifier import ScopeIdentifier
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"LoggerContext",
|
11
|
+
]
|
12
|
+
|
13
|
+
|
14
|
+
@final
|
15
|
+
class LoggerContext:
|
16
|
+
_context = ContextVar[Self]("LoggerContext")
|
17
|
+
|
18
|
+
@classmethod
|
19
|
+
def scope(
|
20
|
+
cls,
|
21
|
+
scope: ScopeIdentifier,
|
22
|
+
/,
|
23
|
+
*,
|
24
|
+
logger: Logger | None,
|
25
|
+
) -> Self:
|
26
|
+
current: Self
|
27
|
+
try: # check for current scope
|
28
|
+
current = cls._context.get()
|
29
|
+
|
30
|
+
except LookupError:
|
31
|
+
# create root scope when missing
|
32
|
+
return cls(
|
33
|
+
scope=scope,
|
34
|
+
logger=logger,
|
35
|
+
)
|
36
|
+
|
37
|
+
# create nested scope otherwise
|
38
|
+
return cls(
|
39
|
+
scope=scope,
|
40
|
+
logger=logger or current._logger,
|
41
|
+
)
|
42
|
+
|
43
|
+
@classmethod
|
44
|
+
def log_error(
|
45
|
+
cls,
|
46
|
+
message: str,
|
47
|
+
/,
|
48
|
+
*args: Any,
|
49
|
+
exception: BaseException | None = None,
|
50
|
+
) -> None:
|
51
|
+
try:
|
52
|
+
cls._context.get().log(
|
53
|
+
ERROR,
|
54
|
+
message,
|
55
|
+
*args,
|
56
|
+
exception=exception,
|
57
|
+
)
|
58
|
+
|
59
|
+
except LookupError:
|
60
|
+
getLogger().log(
|
61
|
+
ERROR,
|
62
|
+
message,
|
63
|
+
*args,
|
64
|
+
exc_info=exception,
|
65
|
+
)
|
66
|
+
|
67
|
+
@classmethod
|
68
|
+
def log_warning(
|
69
|
+
cls,
|
70
|
+
message: str,
|
71
|
+
/,
|
72
|
+
*args: Any,
|
73
|
+
exception: Exception | None = None,
|
74
|
+
) -> None:
|
75
|
+
try:
|
76
|
+
cls._context.get().log(
|
77
|
+
WARNING,
|
78
|
+
message,
|
79
|
+
*args,
|
80
|
+
exception=exception,
|
81
|
+
)
|
82
|
+
|
83
|
+
except LookupError:
|
84
|
+
getLogger().log(
|
85
|
+
WARNING,
|
86
|
+
message,
|
87
|
+
*args,
|
88
|
+
exc_info=exception,
|
89
|
+
)
|
90
|
+
|
91
|
+
@classmethod
|
92
|
+
def log_info(
|
93
|
+
cls,
|
94
|
+
message: str,
|
95
|
+
/,
|
96
|
+
*args: Any,
|
97
|
+
) -> None:
|
98
|
+
try:
|
99
|
+
cls._context.get().log(
|
100
|
+
INFO,
|
101
|
+
message,
|
102
|
+
*args,
|
103
|
+
)
|
104
|
+
|
105
|
+
except LookupError:
|
106
|
+
getLogger().log(
|
107
|
+
INFO,
|
108
|
+
message,
|
109
|
+
*args,
|
110
|
+
)
|
111
|
+
|
112
|
+
@classmethod
|
113
|
+
def log_debug(
|
114
|
+
cls,
|
115
|
+
message: str,
|
116
|
+
/,
|
117
|
+
*args: Any,
|
118
|
+
exception: Exception | None = None,
|
119
|
+
) -> None:
|
120
|
+
try:
|
121
|
+
cls._context.get().log(
|
122
|
+
DEBUG,
|
123
|
+
message,
|
124
|
+
*args,
|
125
|
+
exception=exception,
|
126
|
+
)
|
127
|
+
|
128
|
+
except LookupError:
|
129
|
+
getLogger().log(
|
130
|
+
DEBUG,
|
131
|
+
message,
|
132
|
+
*args,
|
133
|
+
exc_info=exception,
|
134
|
+
)
|
135
|
+
|
136
|
+
def __init__(
|
137
|
+
self,
|
138
|
+
scope: ScopeIdentifier,
|
139
|
+
logger: Logger | None,
|
140
|
+
) -> None:
|
141
|
+
self._prefix: str = scope.unique_name
|
142
|
+
self._logger: Logger = logger or getLogger(name=scope.label)
|
143
|
+
|
144
|
+
def log(
|
145
|
+
self,
|
146
|
+
level: int,
|
147
|
+
message: str,
|
148
|
+
/,
|
149
|
+
*args: Any,
|
150
|
+
exception: BaseException | None = None,
|
151
|
+
) -> None:
|
152
|
+
self._logger.log(
|
153
|
+
level,
|
154
|
+
f"{self._prefix} {message}",
|
155
|
+
*args,
|
156
|
+
exc_info=exception,
|
157
|
+
)
|
158
|
+
|
159
|
+
def __enter__(self) -> None:
|
160
|
+
assert not hasattr(self, "_token"), "Context reentrance is not allowed" # nosec: B101
|
161
|
+
assert not hasattr(self, "_entered"), "Context reentrance is not allowed" # nosec: B101
|
162
|
+
self._entered: float = monotonic()
|
163
|
+
self._token: Token[LoggerContext] = LoggerContext._context.set(self)
|
164
|
+
self.log(DEBUG, "Entering context...")
|
165
|
+
|
166
|
+
def __exit__(
|
167
|
+
self,
|
168
|
+
exc_type: type[BaseException] | None,
|
169
|
+
exc_val: BaseException | None,
|
170
|
+
exc_tb: TracebackType | None,
|
171
|
+
) -> None:
|
172
|
+
assert hasattr(self, "_token"), "Unbalanced context enter/exit" # nosec: B101
|
173
|
+
LoggerContext._context.reset(self._token)
|
174
|
+
del self._token
|
175
|
+
self.log(DEBUG, f"...exiting context after {monotonic() - self._entered:.2f}s")
|
176
|
+
del self._entered
|
@@ -0,0 +1,165 @@
|
|
1
|
+
from contextvars import ContextVar, Token
|
2
|
+
from types import TracebackType
|
3
|
+
from typing import Protocol, Self, final, runtime_checkable
|
4
|
+
|
5
|
+
from haiway.context.identifier import ScopeIdentifier
|
6
|
+
from haiway.context.logging import LoggerContext
|
7
|
+
from haiway.state import State
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"MetricsContext",
|
11
|
+
"MetricsHandler",
|
12
|
+
"MetricsReading",
|
13
|
+
"MetricsRecording",
|
14
|
+
"MetricsScopeEntering",
|
15
|
+
"MetricsScopeExiting",
|
16
|
+
]
|
17
|
+
|
18
|
+
|
19
|
+
@runtime_checkable
|
20
|
+
class MetricsRecording(Protocol):
|
21
|
+
def __call__(
|
22
|
+
self,
|
23
|
+
scope: ScopeIdentifier,
|
24
|
+
/,
|
25
|
+
metric: State,
|
26
|
+
) -> None: ...
|
27
|
+
|
28
|
+
|
29
|
+
@runtime_checkable
|
30
|
+
class MetricsReading(Protocol):
|
31
|
+
async def __call__[Metric: State](
|
32
|
+
self,
|
33
|
+
scope: ScopeIdentifier,
|
34
|
+
/,
|
35
|
+
*,
|
36
|
+
metric: type[Metric],
|
37
|
+
merged: bool,
|
38
|
+
) -> Metric | None: ...
|
39
|
+
|
40
|
+
|
41
|
+
@runtime_checkable
|
42
|
+
class MetricsScopeEntering(Protocol):
|
43
|
+
def __call__[Metric: State](
|
44
|
+
self,
|
45
|
+
scope: ScopeIdentifier,
|
46
|
+
/,
|
47
|
+
) -> None: ...
|
48
|
+
|
49
|
+
|
50
|
+
@runtime_checkable
|
51
|
+
class MetricsScopeExiting(Protocol):
|
52
|
+
def __call__[Metric: State](
|
53
|
+
self,
|
54
|
+
scope: ScopeIdentifier,
|
55
|
+
/,
|
56
|
+
) -> None: ...
|
57
|
+
|
58
|
+
|
59
|
+
class MetricsHandler(State):
|
60
|
+
record: MetricsRecording
|
61
|
+
read: MetricsReading
|
62
|
+
enter_scope: MetricsScopeEntering
|
63
|
+
exit_scope: MetricsScopeExiting
|
64
|
+
|
65
|
+
|
66
|
+
@final
|
67
|
+
class MetricsContext:
|
68
|
+
_context = ContextVar[Self]("MetricsContext")
|
69
|
+
|
70
|
+
@classmethod
|
71
|
+
def scope(
|
72
|
+
cls,
|
73
|
+
scope: ScopeIdentifier,
|
74
|
+
/,
|
75
|
+
*,
|
76
|
+
metrics: MetricsHandler | None,
|
77
|
+
) -> Self:
|
78
|
+
current: Self
|
79
|
+
try: # check for current scope
|
80
|
+
current = cls._context.get()
|
81
|
+
|
82
|
+
except LookupError:
|
83
|
+
# create root scope when missing
|
84
|
+
return cls(
|
85
|
+
scope=scope,
|
86
|
+
metrics=metrics,
|
87
|
+
)
|
88
|
+
|
89
|
+
# create nested scope otherwise
|
90
|
+
return cls(
|
91
|
+
scope=scope,
|
92
|
+
metrics=metrics or current._metrics,
|
93
|
+
)
|
94
|
+
|
95
|
+
@classmethod
|
96
|
+
def record(
|
97
|
+
cls,
|
98
|
+
metric: State,
|
99
|
+
/,
|
100
|
+
) -> None:
|
101
|
+
try: # catch exceptions - we don't wan't to blow up on metrics
|
102
|
+
metrics: Self = cls._context.get()
|
103
|
+
|
104
|
+
if metrics._metrics is not None:
|
105
|
+
metrics._metrics.record(
|
106
|
+
metrics._scope,
|
107
|
+
metric,
|
108
|
+
)
|
109
|
+
|
110
|
+
except Exception as exc:
|
111
|
+
LoggerContext.log_error(
|
112
|
+
"Failed to record metric: %s",
|
113
|
+
type(metric).__qualname__,
|
114
|
+
exception=exc,
|
115
|
+
)
|
116
|
+
|
117
|
+
@classmethod
|
118
|
+
async def read[Metric: State](
|
119
|
+
cls,
|
120
|
+
metric: type[Metric],
|
121
|
+
/,
|
122
|
+
merged: bool,
|
123
|
+
) -> Metric | None:
|
124
|
+
try: # catch exceptions - we don't wan't to blow up on metrics
|
125
|
+
metrics: Self = cls._context.get()
|
126
|
+
|
127
|
+
if metrics._metrics is not None:
|
128
|
+
return await metrics._metrics.read(
|
129
|
+
metrics._scope,
|
130
|
+
metric=metric,
|
131
|
+
merged=merged,
|
132
|
+
)
|
133
|
+
|
134
|
+
except Exception as exc:
|
135
|
+
LoggerContext.log_error(
|
136
|
+
"Failed to read metric: %s",
|
137
|
+
metric.__qualname__,
|
138
|
+
exception=exc,
|
139
|
+
)
|
140
|
+
|
141
|
+
def __init__(
|
142
|
+
self,
|
143
|
+
scope: ScopeIdentifier,
|
144
|
+
metrics: MetricsHandler | None,
|
145
|
+
) -> None:
|
146
|
+
self._scope: ScopeIdentifier = scope
|
147
|
+
self._metrics: MetricsHandler | None = metrics
|
148
|
+
|
149
|
+
def __enter__(self) -> None:
|
150
|
+
assert not hasattr(self, "_token"), "Context reentrance is not allowed" # nosec: B101
|
151
|
+
self._token: Token[MetricsContext] = MetricsContext._context.set(self)
|
152
|
+
if self._metrics is not None:
|
153
|
+
self._metrics.enter_scope(self._scope)
|
154
|
+
|
155
|
+
def __exit__(
|
156
|
+
self,
|
157
|
+
exc_type: type[BaseException] | None,
|
158
|
+
exc_val: BaseException | None,
|
159
|
+
exc_tb: TracebackType | None,
|
160
|
+
) -> None:
|
161
|
+
assert hasattr(self, "_token"), "Unbalanced context enter/exit" # nosec: B101
|
162
|
+
MetricsContext._context.reset(self._token)
|
163
|
+
del self._token
|
164
|
+
if self._metrics is not None:
|
165
|
+
self._metrics.exit_scope(self._scope)
|
haiway/context/state.py
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
from collections.abc import Iterable
|
2
|
+
from contextvars import ContextVar, Token
|
3
|
+
from types import TracebackType
|
4
|
+
from typing import Self, cast, final
|
5
|
+
|
6
|
+
from haiway.context.types import MissingContext, MissingState
|
7
|
+
from haiway.state import State
|
8
|
+
from haiway.utils import freeze
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"ScopeState",
|
12
|
+
"StateContext",
|
13
|
+
]
|
14
|
+
|
15
|
+
|
16
|
+
@final
|
17
|
+
class ScopeState:
|
18
|
+
def __init__(
|
19
|
+
self,
|
20
|
+
state: Iterable[State],
|
21
|
+
) -> None:
|
22
|
+
self._state: dict[type[State], State] = {type(element): element for element in state}
|
23
|
+
freeze(self)
|
24
|
+
|
25
|
+
def state[StateType: State](
|
26
|
+
self,
|
27
|
+
state: type[StateType],
|
28
|
+
/,
|
29
|
+
default: StateType | None = None,
|
30
|
+
) -> StateType:
|
31
|
+
if state in self._state:
|
32
|
+
return cast(StateType, self._state[state])
|
33
|
+
|
34
|
+
elif default is not None:
|
35
|
+
return default
|
36
|
+
|
37
|
+
else:
|
38
|
+
try:
|
39
|
+
initialized: StateType = state()
|
40
|
+
self._state[state] = initialized
|
41
|
+
return initialized
|
42
|
+
|
43
|
+
except Exception as exc:
|
44
|
+
raise MissingState(
|
45
|
+
f"{state.__qualname__} is not defined in current scope"
|
46
|
+
" and failed to provide a default value"
|
47
|
+
) from exc
|
48
|
+
|
49
|
+
def updated(
|
50
|
+
self,
|
51
|
+
state: Iterable[State],
|
52
|
+
) -> Self:
|
53
|
+
if state:
|
54
|
+
return self.__class__(
|
55
|
+
[
|
56
|
+
*self._state.values(),
|
57
|
+
*state,
|
58
|
+
]
|
59
|
+
)
|
60
|
+
|
61
|
+
else:
|
62
|
+
return self
|
63
|
+
|
64
|
+
|
65
|
+
@final
|
66
|
+
class StateContext:
|
67
|
+
_context = ContextVar[ScopeState]("StateContext")
|
68
|
+
|
69
|
+
@classmethod
|
70
|
+
def current[StateType: State](
|
71
|
+
cls,
|
72
|
+
state: type[StateType],
|
73
|
+
/,
|
74
|
+
default: StateType | None = None,
|
75
|
+
) -> StateType:
|
76
|
+
try:
|
77
|
+
return cls._context.get().state(state, default=default)
|
78
|
+
|
79
|
+
except LookupError as exc:
|
80
|
+
raise MissingContext("StateContext requested but not defined!") from exc
|
81
|
+
|
82
|
+
@classmethod
|
83
|
+
def updated(
|
84
|
+
cls,
|
85
|
+
state: Iterable[State],
|
86
|
+
/,
|
87
|
+
) -> Self:
|
88
|
+
try:
|
89
|
+
# update current scope context
|
90
|
+
return cls(state=cls._context.get().updated(state=state))
|
91
|
+
|
92
|
+
except LookupError: # create root scope when missing
|
93
|
+
return cls(state=ScopeState(state))
|
94
|
+
|
95
|
+
def __init__(
|
96
|
+
self,
|
97
|
+
state: ScopeState,
|
98
|
+
) -> None:
|
99
|
+
self._state: ScopeState = state
|
100
|
+
|
101
|
+
def __enter__(self) -> None:
|
102
|
+
assert not hasattr(self, "_token"), "Context reentrance is not allowed" # nosec: B101
|
103
|
+
self._token: Token[ScopeState] = StateContext._context.set(self._state)
|
104
|
+
|
105
|
+
def __exit__(
|
106
|
+
self,
|
107
|
+
exc_type: type[BaseException] | None,
|
108
|
+
exc_val: BaseException | None,
|
109
|
+
exc_tb: TracebackType | None,
|
110
|
+
) -> None:
|
111
|
+
assert hasattr(self, "_token"), "Unbalanced context enter/exit" # nosec: B101
|
112
|
+
StateContext._context.reset(self._token)
|
113
|
+
del self._token
|
haiway/context/tasks.py
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
from asyncio import Task, TaskGroup, get_event_loop
|
2
|
+
from collections.abc import Callable, Coroutine
|
3
|
+
from contextvars import ContextVar, copy_context
|
4
|
+
from types import TracebackType
|
5
|
+
from typing import final
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
"TaskGroupContext",
|
9
|
+
]
|
10
|
+
|
11
|
+
|
12
|
+
@final
|
13
|
+
class TaskGroupContext:
|
14
|
+
_context = ContextVar[TaskGroup]("TaskGroupContext")
|
15
|
+
|
16
|
+
@classmethod
|
17
|
+
def run[Result, **Arguments](
|
18
|
+
cls,
|
19
|
+
function: Callable[Arguments, Coroutine[None, None, Result]],
|
20
|
+
/,
|
21
|
+
*args: Arguments.args,
|
22
|
+
**kwargs: Arguments.kwargs,
|
23
|
+
) -> Task[Result]:
|
24
|
+
try:
|
25
|
+
return cls._context.get().create_task(
|
26
|
+
function(*args, **kwargs),
|
27
|
+
context=copy_context(),
|
28
|
+
)
|
29
|
+
|
30
|
+
except LookupError: # spawn task out of group as a fallback
|
31
|
+
return get_event_loop().create_task(
|
32
|
+
function(*args, **kwargs),
|
33
|
+
context=copy_context(),
|
34
|
+
)
|
35
|
+
|
36
|
+
def __init__(
|
37
|
+
self,
|
38
|
+
) -> None:
|
39
|
+
self._group: TaskGroup = TaskGroup()
|
40
|
+
|
41
|
+
async def __aenter__(self) -> None:
|
42
|
+
assert not hasattr(self, "_token"), "Context reentrance is not allowed" # nosec: B101
|
43
|
+
await self._group.__aenter__()
|
44
|
+
self._token = TaskGroupContext._context.set(self._group)
|
45
|
+
|
46
|
+
async def __aexit__(
|
47
|
+
self,
|
48
|
+
exc_type: type[BaseException] | None,
|
49
|
+
exc_val: BaseException | None,
|
50
|
+
exc_tb: TracebackType | None,
|
51
|
+
) -> None:
|
52
|
+
assert hasattr(self, "_token"), "Unbalanced context enter/exit" # nosec: B101
|
53
|
+
TaskGroupContext._context.reset(self._token)
|
54
|
+
del self._token
|
55
|
+
|
56
|
+
try:
|
57
|
+
await self._group.__aexit__(
|
58
|
+
et=exc_type,
|
59
|
+
exc=exc_val,
|
60
|
+
tb=exc_tb,
|
61
|
+
)
|
62
|
+
|
63
|
+
except BaseException:
|
64
|
+
pass # silence TaskGroup exceptions, if there was exception already we will get it
|
haiway/context/types.py
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
from haiway.helpers.asynchrony import asynchronous, wrap_async
|
2
|
+
from haiway.helpers.caching import cache
|
3
|
+
from haiway.helpers.metrics import MetricsHolder, MetricsLogger
|
4
|
+
from haiway.helpers.retries import retry
|
5
|
+
from haiway.helpers.throttling import throttle
|
6
|
+
from haiway.helpers.timeouted import timeout
|
7
|
+
from haiway.helpers.tracing import ArgumentsTrace, ResultTrace, traced
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"ArgumentsTrace",
|
11
|
+
"MetricsHolder",
|
12
|
+
"MetricsLogger",
|
13
|
+
"ResultTrace",
|
14
|
+
"asynchronous",
|
15
|
+
"cache",
|
16
|
+
"retry",
|
17
|
+
"throttle",
|
18
|
+
"timeout",
|
19
|
+
"traced",
|
20
|
+
"wrap_async",
|
21
|
+
]
|