haiway 0.7.1__py3-none-any.whl → 0.8.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 +14 -2
- haiway/context/__init__.py +14 -2
- haiway/context/access.py +114 -57
- haiway/context/identifier.py +75 -0
- haiway/context/logging.py +176 -0
- haiway/context/metrics.py +61 -329
- haiway/context/state.py +6 -6
- haiway/context/tasks.py +4 -5
- haiway/helpers/__init__.py +2 -0
- haiway/helpers/metrics.py +307 -0
- haiway/state/structure.py +5 -5
- {haiway-0.7.1.dist-info → haiway-0.8.0.dist-info}/METADATA +1 -1
- {haiway-0.7.1.dist-info → haiway-0.8.0.dist-info}/RECORD +16 -13
- {haiway-0.7.1.dist-info → haiway-0.8.0.dist-info}/LICENSE +0 -0
- {haiway-0.7.1.dist-info → haiway-0.8.0.dist-info}/WHEEL +0 -0
- {haiway-0.7.1.dist-info → haiway-0.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,307 @@
|
|
1
|
+
from collections.abc import Sequence
|
2
|
+
from itertools import chain
|
3
|
+
from time import monotonic
|
4
|
+
from typing import Any, Self, cast, final
|
5
|
+
|
6
|
+
from haiway.context import MetricsHandler, ScopeIdentifier, ctx
|
7
|
+
from haiway.state import State
|
8
|
+
from haiway.types import MISSING, Missing
|
9
|
+
|
10
|
+
__all_ = [
|
11
|
+
"MetricsLogger",
|
12
|
+
]
|
13
|
+
|
14
|
+
|
15
|
+
class MetricsScopeStore:
|
16
|
+
def __init__(
|
17
|
+
self,
|
18
|
+
identifier: ScopeIdentifier,
|
19
|
+
/,
|
20
|
+
) -> None:
|
21
|
+
self.identifier: ScopeIdentifier = identifier
|
22
|
+
self.entered: float = monotonic()
|
23
|
+
self.metrics: dict[type[State], State] = {}
|
24
|
+
self.exited: float | None = None
|
25
|
+
self.nested: list[MetricsScopeStore] = []
|
26
|
+
|
27
|
+
@property
|
28
|
+
def time(self) -> float:
|
29
|
+
return (self.exited or monotonic()) - self.entered
|
30
|
+
|
31
|
+
@property
|
32
|
+
def finished(self) -> float:
|
33
|
+
return self.exited is not None and all(nested.finished for nested in self.nested)
|
34
|
+
|
35
|
+
def merged(self) -> Sequence[State]:
|
36
|
+
merged_metrics: dict[type[State], State] = dict(self.metrics)
|
37
|
+
for element in chain.from_iterable(nested.merged() for nested in self.nested):
|
38
|
+
metric_type: type[State] = type(element)
|
39
|
+
current: State | Missing = merged_metrics.get(
|
40
|
+
metric_type,
|
41
|
+
MISSING,
|
42
|
+
)
|
43
|
+
|
44
|
+
if current is MISSING:
|
45
|
+
continue # do not merge to missing
|
46
|
+
|
47
|
+
elif hasattr(current, "__add__"):
|
48
|
+
merged_metrics[metric_type] = current.__add__(element) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
|
49
|
+
|
50
|
+
else:
|
51
|
+
merged_metrics[metric_type] = element
|
52
|
+
|
53
|
+
return tuple(merged_metrics.values())
|
54
|
+
|
55
|
+
|
56
|
+
@final
|
57
|
+
class MetricsLogger:
|
58
|
+
@classmethod
|
59
|
+
def handler(
|
60
|
+
cls,
|
61
|
+
items_limit: int | None = None,
|
62
|
+
item_character_limit: int | None = None,
|
63
|
+
) -> MetricsHandler:
|
64
|
+
logger_handler: Self = cls(
|
65
|
+
items_limit=items_limit,
|
66
|
+
item_character_limit=item_character_limit,
|
67
|
+
)
|
68
|
+
return MetricsHandler(
|
69
|
+
record=logger_handler.record,
|
70
|
+
enter_scope=logger_handler.enter_scope,
|
71
|
+
exit_scope=logger_handler.exit_scope,
|
72
|
+
)
|
73
|
+
|
74
|
+
def __init__(
|
75
|
+
self,
|
76
|
+
items_limit: int | None = None,
|
77
|
+
item_character_limit: int | None = None,
|
78
|
+
) -> None:
|
79
|
+
self.items_limit: int | None = items_limit
|
80
|
+
self.item_character_limit: int | None = item_character_limit
|
81
|
+
self.scopes: dict[ScopeIdentifier, MetricsScopeStore] = {}
|
82
|
+
|
83
|
+
def record(
|
84
|
+
self,
|
85
|
+
scope: ScopeIdentifier,
|
86
|
+
/,
|
87
|
+
metric: State,
|
88
|
+
) -> None:
|
89
|
+
assert scope in self.scopes # nosec: B101
|
90
|
+
metric_type: type[State] = type(metric)
|
91
|
+
metrics: dict[type[State], State] = self.scopes[scope].metrics
|
92
|
+
if (current := metrics.get(metric_type)) and hasattr(current, "__add__"):
|
93
|
+
metrics[type(metric)] = current.__add__(metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
|
94
|
+
|
95
|
+
metrics[type(metric)] = metric
|
96
|
+
if __debug__:
|
97
|
+
if log := _state_log(
|
98
|
+
metric,
|
99
|
+
list_items_limit=self.items_limit,
|
100
|
+
item_character_limit=self.item_character_limit,
|
101
|
+
):
|
102
|
+
ctx.log_info(f"Recorded:\n• {type(metric).__qualname__}:{log}")
|
103
|
+
|
104
|
+
def enter_scope[Metric: State](
|
105
|
+
self,
|
106
|
+
scope: ScopeIdentifier,
|
107
|
+
/,
|
108
|
+
) -> None:
|
109
|
+
assert scope not in self.scopes # nosec: B101
|
110
|
+
self.scopes[scope] = MetricsScopeStore(scope)
|
111
|
+
|
112
|
+
def exit_scope[Metric: State](
|
113
|
+
self,
|
114
|
+
scope: ScopeIdentifier,
|
115
|
+
/,
|
116
|
+
) -> None:
|
117
|
+
assert scope in self.scopes # nosec: B101
|
118
|
+
self.scopes[scope].exited = monotonic()
|
119
|
+
|
120
|
+
if __debug__:
|
121
|
+
if scope.is_root and self.scopes[scope].finished:
|
122
|
+
if log := _tree_log(
|
123
|
+
self.scopes[scope],
|
124
|
+
list_items_limit=self.items_limit,
|
125
|
+
item_character_limit=self.item_character_limit,
|
126
|
+
):
|
127
|
+
ctx.log_info(log)
|
128
|
+
|
129
|
+
|
130
|
+
def _tree_log(
|
131
|
+
metrics: MetricsScopeStore,
|
132
|
+
list_items_limit: int | None,
|
133
|
+
item_character_limit: int | None,
|
134
|
+
) -> str:
|
135
|
+
log: str = f"@{metrics.identifier}({metrics.time:.2f}s):"
|
136
|
+
|
137
|
+
for metric in metrics.merged():
|
138
|
+
metric_log: str = ""
|
139
|
+
for key, value in vars(metric).items():
|
140
|
+
if value_log := _value_log(
|
141
|
+
value,
|
142
|
+
list_items_limit=list_items_limit,
|
143
|
+
item_character_limit=item_character_limit,
|
144
|
+
):
|
145
|
+
metric_log += f"\n| + {key}: {value_log}"
|
146
|
+
|
147
|
+
else:
|
148
|
+
continue # skip missing values
|
149
|
+
|
150
|
+
if not metric_log:
|
151
|
+
continue # skip empty logs
|
152
|
+
|
153
|
+
log += f"\n• {type(metric).__qualname__}:{metric_log}"
|
154
|
+
|
155
|
+
for nested in metrics.nested:
|
156
|
+
nested_log: str = _tree_log(
|
157
|
+
nested,
|
158
|
+
list_items_limit=list_items_limit,
|
159
|
+
item_character_limit=item_character_limit,
|
160
|
+
).replace("\n", "\n| ")
|
161
|
+
|
162
|
+
log += f"\n{nested_log}"
|
163
|
+
|
164
|
+
return log.strip()
|
165
|
+
|
166
|
+
|
167
|
+
def _state_log(
|
168
|
+
value: State,
|
169
|
+
/,
|
170
|
+
list_items_limit: int | None,
|
171
|
+
item_character_limit: int | None,
|
172
|
+
) -> str | None:
|
173
|
+
state_log: str = ""
|
174
|
+
for key, element in vars(value).items():
|
175
|
+
element_log: str | None = _value_log(
|
176
|
+
element,
|
177
|
+
list_items_limit=list_items_limit,
|
178
|
+
item_character_limit=item_character_limit,
|
179
|
+
)
|
180
|
+
|
181
|
+
if element_log:
|
182
|
+
state_log += f"\n| + {key}: {element_log}"
|
183
|
+
|
184
|
+
else:
|
185
|
+
continue # skip empty logs
|
186
|
+
|
187
|
+
if state_log:
|
188
|
+
return state_log.replace("\n", "\n| ")
|
189
|
+
|
190
|
+
else:
|
191
|
+
return None # skip empty logs
|
192
|
+
|
193
|
+
|
194
|
+
def _dict_log(
|
195
|
+
value: dict[Any, Any],
|
196
|
+
/,
|
197
|
+
list_items_limit: int | None,
|
198
|
+
item_character_limit: int | None,
|
199
|
+
) -> str | None:
|
200
|
+
dict_log: str = ""
|
201
|
+
for key, element in value.items():
|
202
|
+
element_log: str | None = _value_log(
|
203
|
+
element,
|
204
|
+
list_items_limit=list_items_limit,
|
205
|
+
item_character_limit=item_character_limit,
|
206
|
+
)
|
207
|
+
if element_log:
|
208
|
+
dict_log += f"\n| + {key}: {element_log}"
|
209
|
+
|
210
|
+
else:
|
211
|
+
continue # skip empty logs
|
212
|
+
|
213
|
+
if dict_log:
|
214
|
+
return dict_log.replace("\n", "\n| ")
|
215
|
+
|
216
|
+
else:
|
217
|
+
return None # skip empty logs
|
218
|
+
|
219
|
+
|
220
|
+
def _list_log(
|
221
|
+
value: list[Any],
|
222
|
+
/,
|
223
|
+
list_items_limit: int | None,
|
224
|
+
item_character_limit: int | None,
|
225
|
+
) -> str | None:
|
226
|
+
list_log: str = ""
|
227
|
+
enumerated: list[tuple[int, Any]] = list(enumerate(value))
|
228
|
+
if list_items_limit:
|
229
|
+
if list_items_limit > 0:
|
230
|
+
enumerated = enumerated[:list_items_limit]
|
231
|
+
|
232
|
+
else:
|
233
|
+
enumerated = enumerated[list_items_limit:]
|
234
|
+
|
235
|
+
for idx, element in enumerated:
|
236
|
+
element_log: str | None = _value_log(
|
237
|
+
element,
|
238
|
+
list_items_limit=list_items_limit,
|
239
|
+
item_character_limit=item_character_limit,
|
240
|
+
)
|
241
|
+
if element_log:
|
242
|
+
list_log += f"\n| [{idx}] {element_log}"
|
243
|
+
|
244
|
+
else:
|
245
|
+
continue # skip empty logs
|
246
|
+
|
247
|
+
if list_log:
|
248
|
+
return list_log.replace("\n", "\n| ")
|
249
|
+
|
250
|
+
else:
|
251
|
+
return None # skip empty logs
|
252
|
+
|
253
|
+
|
254
|
+
def _raw_value_log(
|
255
|
+
value: Any,
|
256
|
+
/,
|
257
|
+
item_character_limit: int | None,
|
258
|
+
) -> str | None:
|
259
|
+
if value is MISSING:
|
260
|
+
return None # skip missing
|
261
|
+
|
262
|
+
value_log = str(value)
|
263
|
+
if not value_log:
|
264
|
+
return None # skip empty logs
|
265
|
+
|
266
|
+
if (item_character_limit := item_character_limit) and len(value_log) > item_character_limit:
|
267
|
+
return value_log.replace("\n", " ")[:item_character_limit] + "..."
|
268
|
+
|
269
|
+
else:
|
270
|
+
return value_log.replace("\n", "\n| ")
|
271
|
+
|
272
|
+
|
273
|
+
def _value_log(
|
274
|
+
value: Any,
|
275
|
+
/,
|
276
|
+
list_items_limit: int | None,
|
277
|
+
item_character_limit: int | None,
|
278
|
+
) -> str | None:
|
279
|
+
# try unpack dicts
|
280
|
+
if isinstance(value, dict):
|
281
|
+
return _dict_log(
|
282
|
+
cast(dict[Any, Any], value),
|
283
|
+
list_items_limit=list_items_limit,
|
284
|
+
item_character_limit=item_character_limit,
|
285
|
+
)
|
286
|
+
|
287
|
+
# try unpack lists
|
288
|
+
elif isinstance(value, list):
|
289
|
+
return _list_log(
|
290
|
+
cast(list[Any], value),
|
291
|
+
list_items_limit=list_items_limit,
|
292
|
+
item_character_limit=item_character_limit,
|
293
|
+
)
|
294
|
+
|
295
|
+
# try unpack state
|
296
|
+
elif isinstance(value, State):
|
297
|
+
return _state_log(
|
298
|
+
value,
|
299
|
+
list_items_limit=list_items_limit,
|
300
|
+
item_character_limit=item_character_limit,
|
301
|
+
)
|
302
|
+
|
303
|
+
else:
|
304
|
+
return _raw_value_log(
|
305
|
+
value,
|
306
|
+
item_character_limit=item_character_limit,
|
307
|
+
)
|
haiway/state/structure.py
CHANGED
@@ -91,7 +91,7 @@ class StateMeta(type):
|
|
91
91
|
),
|
92
92
|
)
|
93
93
|
|
94
|
-
state_type.
|
94
|
+
state_type.__TYPE_PARAMETERS__ = type_parameters # pyright: ignore[reportAttributeAccessIssue]
|
95
95
|
state_type.__ATTRIBUTES__ = attributes # pyright: ignore[reportAttributeAccessIssue]
|
96
96
|
state_type.__slots__ = frozenset(attributes.keys()) # pyright: ignore[reportAttributeAccessIssue]
|
97
97
|
state_type.__match_args__ = state_type.__slots__ # pyright: ignore[reportAttributeAccessIssue]
|
@@ -139,7 +139,7 @@ class StateMeta(type):
|
|
139
139
|
# then check if we are parametrized
|
140
140
|
checked_parameters: Mapping[str, Any] | None = getattr(
|
141
141
|
self,
|
142
|
-
"
|
142
|
+
"__TYPE_PARAMETERS__",
|
143
143
|
None,
|
144
144
|
)
|
145
145
|
if checked_parameters is None:
|
@@ -152,7 +152,7 @@ class StateMeta(type):
|
|
152
152
|
# we can verify all of the attributes to check if we have common base
|
153
153
|
available_parameters: Mapping[str, Any] | None = getattr(
|
154
154
|
subclass,
|
155
|
-
"
|
155
|
+
"__TYPE_PARAMETERS__",
|
156
156
|
None,
|
157
157
|
)
|
158
158
|
|
@@ -204,7 +204,7 @@ class State(metaclass=StateMeta):
|
|
204
204
|
|
205
205
|
_: ClassVar[Self]
|
206
206
|
__IMMUTABLE__: ClassVar[EllipsisType] = ...
|
207
|
-
|
207
|
+
__TYPE_PARAMETERS__: ClassVar[Mapping[str, Any] | None] = None
|
208
208
|
__ATTRIBUTES__: ClassVar[dict[str, StateAttribute[Any]]]
|
209
209
|
|
210
210
|
@classmethod
|
@@ -213,7 +213,7 @@ class State(metaclass=StateMeta):
|
|
213
213
|
type_argument: tuple[type[Any], ...] | type[Any],
|
214
214
|
) -> type[Self]:
|
215
215
|
assert Generic in cls.__bases__, "Can't specialize non generic type!" # nosec: B101
|
216
|
-
assert cls.
|
216
|
+
assert cls.__TYPE_PARAMETERS__ is None, "Can't specialize already specialized type!" # nosec: B101
|
217
217
|
|
218
218
|
type_arguments: tuple[type[Any], ...]
|
219
219
|
match type_argument:
|
@@ -1,15 +1,18 @@
|
|
1
|
-
haiway/__init__.py,sha256=
|
1
|
+
haiway/__init__.py,sha256=W11mHfW3WnYhVAuNOHeEHVc-BQkOodHM9j4_o36j4dA,1669
|
2
2
|
haiway/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
haiway/context/__init__.py,sha256=
|
4
|
-
haiway/context/access.py,sha256=
|
3
|
+
haiway/context/__init__.py,sha256=FDPD92cVaZKbQoyXl8lO1GzrscfTSrvST_gtfxOxxcM,620
|
4
|
+
haiway/context/access.py,sha256=7UfhtDCjkVtdLfdWc5nR58KwKI3r693LcjFAlRtYHa0,15653
|
5
5
|
haiway/context/disposables.py,sha256=DZjnMp-wMfF-em2Wjhbm1MvXubNpuzFBT70BQNIxC7M,2019
|
6
|
-
haiway/context/
|
7
|
-
haiway/context/
|
8
|
-
haiway/context/
|
6
|
+
haiway/context/identifier.py,sha256=f5-vZOnhKZmYj__emS51WcgsJqfD_yz6ilr3GUo9z1k,2034
|
7
|
+
haiway/context/logging.py,sha256=ptwgENuyw-WFgokVsYx9OXZGhJENuO_wgfVjcBryUKM,4251
|
8
|
+
haiway/context/metrics.py,sha256=m3fFem_zukiWejmb9KPqgX1lyR8GxB_AgPSDRjfUmFk,3234
|
9
|
+
haiway/context/state.py,sha256=LCcFxXqDBu6prvPyPicN-ecONSNHyR56PfQ5u5jNFCU,3000
|
10
|
+
haiway/context/tasks.py,sha256=h6OxLxHHqkw0LfQi81NtbsCKfACKxYRZAkDhlJTpqCM,1904
|
9
11
|
haiway/context/types.py,sha256=VvJA7wAPZ3ISpgyThVguioYUXqhHf0XkPfRd0M1ERiQ,142
|
10
|
-
haiway/helpers/__init__.py,sha256=
|
12
|
+
haiway/helpers/__init__.py,sha256=a2x-geUNGKe8HqqmCCZO-6GNUic-bARSnKeUNJeWQpY,543
|
11
13
|
haiway/helpers/asynchrony.py,sha256=rh_Hwo0MQHfKnw5dLUCFTAm3Fk3SVS8Or8cTcQFdPA8,6042
|
12
14
|
haiway/helpers/caching.py,sha256=Ok_WE5Whe7XqnIuLZo4rNNBFeWap-aUWX799s4b1JAQ,9536
|
15
|
+
haiway/helpers/metrics.py,sha256=ERTpyryPqTnkLdXdDfP91jojUrloMVL-AAeoGa0Pjls,8711
|
13
16
|
haiway/helpers/retries.py,sha256=gIkyUlqJLDYaxIZd3qzeqGFY9y5Gp8dgZLlZ6hs8hoc,7538
|
14
17
|
haiway/helpers/throttling.py,sha256=zo0OwFq64si5KUwhd58cFHLmGAmYwRbFRJMbv9suhPs,3844
|
15
18
|
haiway/helpers/timeouted.py,sha256=1xU09hQnFdj6p48BwZl5xUvtIr3zC0ZUXehkdrduCjs,3074
|
@@ -18,7 +21,7 @@ haiway/state/__init__.py,sha256=emTuwGFn7HyjyTJ_ass69J5jQIA7_WHO4teZz_dR05Y,355
|
|
18
21
|
haiway/state/attributes.py,sha256=iQ7TJHnT3hlcYwKcxchXE56zU8WbOTJZhsVn_HocXBc,22903
|
19
22
|
haiway/state/path.py,sha256=4vh-fYQv8_xRWjS0ErMQslKDWRI6-KVECAr8JhYk0UY,17503
|
20
23
|
haiway/state/requirement.py,sha256=3iQqzp5Q7w6y5uClamJGH7S5Hib9pciuTAV27PP5lS8,6161
|
21
|
-
haiway/state/structure.py,sha256=
|
24
|
+
haiway/state/structure.py,sha256=KvWC9_gE9pjtyUAzcFnQ12K8SyBqwbdPK_z8D2xzqDs,11862
|
22
25
|
haiway/state/validation.py,sha256=n5cHcJTbv3Zf-qs05yzuLJIMBReV_4yYVwcH6IL58N0,13836
|
23
26
|
haiway/types/__init__.py,sha256=00Ulp2BxcIWm9vWXKQPodpFEwE8hpqj6OYgrNxelp5s,252
|
24
27
|
haiway/types/frozen.py,sha256=CZhFCXnWAKEhuWSfILxA8smfdpMd5Ku694ycfLh98R8,76
|
@@ -31,8 +34,8 @@ haiway/utils/logs.py,sha256=oDsc1ZdqKDjlTlctLbDcp9iX98Acr-1tdw-Pyg3DElo,1577
|
|
31
34
|
haiway/utils/mimic.py,sha256=BkVjTVP2TxxC8GChPGyDV6UXVwJmiRiSWeOYZNZFHxs,1828
|
32
35
|
haiway/utils/noop.py,sha256=qgbZlOKWY6_23Zs43OLukK2HagIQKRyR04zrFVm5rWI,344
|
33
36
|
haiway/utils/queue.py,sha256=oQ3GXCJ-PGNtMEr6EPdgqAvYZoj8lAa7Z2drBKBEoBM,2345
|
34
|
-
haiway-0.
|
35
|
-
haiway-0.
|
36
|
-
haiway-0.
|
37
|
-
haiway-0.
|
38
|
-
haiway-0.
|
37
|
+
haiway-0.8.0.dist-info/LICENSE,sha256=GehQEW_I1pkmxkkj3NEa7rCTQKYBn7vTPabpDYJlRuo,1063
|
38
|
+
haiway-0.8.0.dist-info/METADATA,sha256=Oe38AT-2rCTqdwbfogI_ji8zWHPW1DGaE62tLV46yVY,3898
|
39
|
+
haiway-0.8.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
40
|
+
haiway-0.8.0.dist-info/top_level.txt,sha256=_LdXVLzUzgkvAGQnQJj5kQfoFhpPW6EF4Kj9NapniLg,7
|
41
|
+
haiway-0.8.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|