haiway 0.26.1__py3-none-any.whl → 0.27.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 +10 -0
- haiway/context/__init__.py +2 -0
- haiway/context/access.py +4 -0
- haiway/context/observability.py +11 -1
- haiway/helpers/observability.py +8 -2
- haiway/opentelemetry/observability.py +60 -14
- haiway/utils/__init__.py +12 -0
- haiway/utils/collections.py +31 -31
- haiway/utils/formatting.py +126 -36
- haiway/utils/metadata.py +480 -0
- {haiway-0.26.1.dist-info → haiway-0.27.0.dist-info}/METADATA +1 -1
- {haiway-0.26.1.dist-info → haiway-0.27.0.dist-info}/RECORD +14 -13
- {haiway-0.26.1.dist-info → haiway-0.27.0.dist-info}/WHEEL +0 -0
- {haiway-0.26.1.dist-info → haiway-0.27.0.dist-info}/licenses/LICENSE +0 -0
haiway/__init__.py
CHANGED
@@ -37,8 +37,13 @@ from haiway.types import (
|
|
37
37
|
unwrap_missing,
|
38
38
|
)
|
39
39
|
from haiway.utils import (
|
40
|
+
META_EMPTY,
|
40
41
|
AsyncQueue,
|
41
42
|
AsyncStream,
|
43
|
+
Meta,
|
44
|
+
MetaTags,
|
45
|
+
MetaValue,
|
46
|
+
MetaValues,
|
42
47
|
always,
|
43
48
|
as_dict,
|
44
49
|
as_list,
|
@@ -59,6 +64,7 @@ from haiway.utils import (
|
|
59
64
|
)
|
60
65
|
|
61
66
|
__all__ = (
|
67
|
+
"META_EMPTY",
|
62
68
|
"MISSING",
|
63
69
|
"AsyncQueue",
|
64
70
|
"AsyncStream",
|
@@ -73,6 +79,10 @@ __all__ = (
|
|
73
79
|
"FileAccess",
|
74
80
|
"Immutable",
|
75
81
|
"LoggerObservability",
|
82
|
+
"Meta",
|
83
|
+
"MetaTags",
|
84
|
+
"MetaValue",
|
85
|
+
"MetaValues",
|
76
86
|
"Missing",
|
77
87
|
"MissingContext",
|
78
88
|
"MissingState",
|
haiway/context/__init__.py
CHANGED
@@ -9,6 +9,7 @@ from haiway.context.observability import (
|
|
9
9
|
ObservabilityEventRecording,
|
10
10
|
ObservabilityLevel,
|
11
11
|
ObservabilityLogRecording,
|
12
|
+
ObservabilityMetricKind,
|
12
13
|
ObservabilityMetricRecording,
|
13
14
|
ObservabilityScopeEntering,
|
14
15
|
ObservabilityScopeExiting,
|
@@ -33,6 +34,7 @@ __all__ = (
|
|
33
34
|
"ObservabilityEventRecording",
|
34
35
|
"ObservabilityLevel",
|
35
36
|
"ObservabilityLogRecording",
|
37
|
+
"ObservabilityMetricKind",
|
36
38
|
"ObservabilityMetricRecording",
|
37
39
|
"ObservabilityScopeEntering",
|
38
40
|
"ObservabilityScopeExiting",
|
haiway/context/access.py
CHANGED
@@ -25,6 +25,7 @@ from haiway.context.observability import (
|
|
25
25
|
ObservabilityAttribute,
|
26
26
|
ObservabilityContext,
|
27
27
|
ObservabilityLevel,
|
28
|
+
ObservabilityMetricKind,
|
28
29
|
)
|
29
30
|
|
30
31
|
# Import after other imports to avoid circular dependencies
|
@@ -990,6 +991,7 @@ class ctx:
|
|
990
991
|
metric: str | None = None,
|
991
992
|
value: float | int | None = None,
|
992
993
|
unit: str | None = None,
|
994
|
+
kind: ObservabilityMetricKind | None = None,
|
993
995
|
attributes: Mapping[str, ObservabilityAttribute] | None = None,
|
994
996
|
) -> None:
|
995
997
|
if event is not None:
|
@@ -1003,11 +1005,13 @@ class ctx:
|
|
1003
1005
|
elif metric is not None:
|
1004
1006
|
assert event is None # nosec: B101
|
1005
1007
|
assert value is not None # nosec: B101
|
1008
|
+
assert kind is not None # nosec: B101
|
1006
1009
|
ObservabilityContext.record_metric(
|
1007
1010
|
level,
|
1008
1011
|
metric,
|
1009
1012
|
value=value,
|
1010
1013
|
unit=unit,
|
1014
|
+
kind=kind,
|
1011
1015
|
attributes=attributes or {},
|
1012
1016
|
)
|
1013
1017
|
|
haiway/context/observability.py
CHANGED
@@ -7,7 +7,7 @@ from logging import INFO as INFO_LOGGING
|
|
7
7
|
from logging import WARNING as WARNING_LOGGING
|
8
8
|
from logging import Logger, getLogger
|
9
9
|
from types import TracebackType
|
10
|
-
from typing import Any, ClassVar, Protocol, Self, runtime_checkable
|
10
|
+
from typing import Any, ClassVar, Literal, Protocol, Self, runtime_checkable
|
11
11
|
from uuid import UUID, uuid4
|
12
12
|
|
13
13
|
from haiway.context.identifier import ScopeIdentifier
|
@@ -23,6 +23,7 @@ __all__ = (
|
|
23
23
|
"ObservabilityEventRecording",
|
24
24
|
"ObservabilityLevel",
|
25
25
|
"ObservabilityLogRecording",
|
26
|
+
"ObservabilityMetricKind",
|
26
27
|
"ObservabilityMetricRecording",
|
27
28
|
"ObservabilityScopeEntering",
|
28
29
|
"ObservabilityScopeExiting",
|
@@ -118,6 +119,9 @@ class ObservabilityEventRecording(Protocol):
|
|
118
119
|
) -> None: ...
|
119
120
|
|
120
121
|
|
122
|
+
type ObservabilityMetricKind = Literal["counter", "histogram", "gauge"]
|
123
|
+
|
124
|
+
|
121
125
|
@runtime_checkable
|
122
126
|
class ObservabilityMetricRecording(Protocol):
|
123
127
|
"""
|
@@ -136,6 +140,7 @@ class ObservabilityMetricRecording(Protocol):
|
|
136
140
|
metric: str,
|
137
141
|
value: float | int,
|
138
142
|
unit: str | None,
|
143
|
+
kind: ObservabilityMetricKind,
|
139
144
|
attributes: Mapping[str, ObservabilityAttribute],
|
140
145
|
) -> None: ...
|
141
146
|
|
@@ -278,6 +283,7 @@ def _logger_observability(
|
|
278
283
|
metric: str,
|
279
284
|
value: float | int,
|
280
285
|
unit: str | None,
|
286
|
+
kind: ObservabilityMetricKind,
|
281
287
|
attributes: Mapping[str, ObservabilityAttribute],
|
282
288
|
) -> None:
|
283
289
|
if attributes:
|
@@ -553,6 +559,7 @@ class ObservabilityContext(Immutable):
|
|
553
559
|
*,
|
554
560
|
value: float | int,
|
555
561
|
unit: str | None,
|
562
|
+
kind: ObservabilityMetricKind,
|
556
563
|
attributes: Mapping[str, ObservabilityAttribute],
|
557
564
|
) -> None:
|
558
565
|
"""
|
@@ -571,6 +578,8 @@ class ObservabilityContext(Immutable):
|
|
571
578
|
The numeric value of the metric
|
572
579
|
unit: str | None
|
573
580
|
Optional unit for the metric (e.g., "ms", "bytes")
|
581
|
+
kind: ObservabilityMetricKind
|
582
|
+
The metric kind defining its value handling.
|
574
583
|
attributes: Mapping[str, ObservabilityAttribute]
|
575
584
|
Key-value attributes associated with the metric
|
576
585
|
"""
|
@@ -584,6 +593,7 @@ class ObservabilityContext(Immutable):
|
|
584
593
|
metric=metric,
|
585
594
|
value=value,
|
586
595
|
unit=unit,
|
596
|
+
kind=kind,
|
587
597
|
attributes=attributes,
|
588
598
|
)
|
589
599
|
|
haiway/helpers/observability.py
CHANGED
@@ -4,8 +4,13 @@ from time import monotonic
|
|
4
4
|
from typing import Any
|
5
5
|
from uuid import UUID, uuid4
|
6
6
|
|
7
|
-
from haiway.context import
|
8
|
-
|
7
|
+
from haiway.context import (
|
8
|
+
Observability,
|
9
|
+
ObservabilityAttribute,
|
10
|
+
ObservabilityLevel,
|
11
|
+
ObservabilityMetricKind,
|
12
|
+
ScopeIdentifier,
|
13
|
+
)
|
9
14
|
from haiway.utils.formatting import format_str
|
10
15
|
|
11
16
|
__all__ = ("LoggerObservability",)
|
@@ -189,6 +194,7 @@ def LoggerObservability( # noqa: C901, PLR0915
|
|
189
194
|
metric: str,
|
190
195
|
value: float | int,
|
191
196
|
unit: str | None,
|
197
|
+
kind: ObservabilityMetricKind,
|
192
198
|
attributes: Mapping[str, ObservabilityAttribute],
|
193
199
|
) -> None:
|
194
200
|
assert root_scope is not None # nosec: B101
|
@@ -12,7 +12,7 @@ from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
|
|
12
12
|
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
|
13
13
|
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
14
14
|
from opentelemetry.metrics._internal import Meter
|
15
|
-
from opentelemetry.metrics._internal.instrument import Counter
|
15
|
+
from opentelemetry.metrics._internal.instrument import Counter, Gauge, Histogram
|
16
16
|
from opentelemetry.sdk._logs import LoggerProvider
|
17
17
|
from opentelemetry.sdk._logs._internal import LogRecord
|
18
18
|
from opentelemetry.sdk._logs._internal.export import (
|
@@ -33,6 +33,7 @@ from haiway.context import (
|
|
33
33
|
Observability,
|
34
34
|
ObservabilityAttribute,
|
35
35
|
ObservabilityLevel,
|
36
|
+
ObservabilityMetricKind,
|
36
37
|
ScopeIdentifier,
|
37
38
|
ctx,
|
38
39
|
)
|
@@ -54,6 +55,8 @@ class ScopeStore:
|
|
54
55
|
"_completed",
|
55
56
|
"_counters",
|
56
57
|
"_exited",
|
58
|
+
"_gauges",
|
59
|
+
"_histograms",
|
57
60
|
"_token",
|
58
61
|
"context",
|
59
62
|
"identifier",
|
@@ -91,6 +94,8 @@ class ScopeStore:
|
|
91
94
|
self.identifier: ScopeIdentifier = identifier
|
92
95
|
self.nested: list[ScopeStore] = []
|
93
96
|
self._counters: dict[str, Counter] = {}
|
97
|
+
self._histograms: dict[str, Histogram] = {}
|
98
|
+
self._gauges: dict[str, Gauge] = {}
|
94
99
|
self._exited: bool = False
|
95
100
|
self._completed: bool = False
|
96
101
|
self.span: Span = span
|
@@ -251,6 +256,7 @@ class ScopeStore:
|
|
251
256
|
*,
|
252
257
|
value: float | int,
|
253
258
|
unit: str | None,
|
259
|
+
kind: ObservabilityMetricKind,
|
254
260
|
attributes: Mapping[str, ObservabilityAttribute],
|
255
261
|
) -> None:
|
256
262
|
"""
|
@@ -267,23 +273,59 @@ class ScopeStore:
|
|
267
273
|
The value to add to the metric
|
268
274
|
unit : str | None
|
269
275
|
The unit of the metric (if any)
|
276
|
+
kind: ObservabilityMetricKind
|
277
|
+
The metric kind defining its value handling.
|
270
278
|
attributes : Mapping[str, ObservabilityAttribute]
|
271
279
|
Attributes to attach to the metric
|
272
280
|
"""
|
273
|
-
|
274
|
-
|
275
|
-
name
|
276
|
-
|
277
|
-
|
281
|
+
match kind:
|
282
|
+
case "counter":
|
283
|
+
if name not in self._counters:
|
284
|
+
self._counters[name] = self.meter.create_counter(
|
285
|
+
name=name,
|
286
|
+
unit=unit or "",
|
287
|
+
)
|
288
|
+
|
289
|
+
self._counters[name].add(
|
290
|
+
value,
|
291
|
+
attributes={
|
292
|
+
key: cast(Any, value)
|
293
|
+
for key, value in attributes.items()
|
294
|
+
if value is not None and value is not MISSING
|
295
|
+
},
|
296
|
+
)
|
278
297
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
298
|
+
case "histogram":
|
299
|
+
if name not in self._histograms:
|
300
|
+
self._histograms[name] = self.meter.create_histogram(
|
301
|
+
name=name,
|
302
|
+
unit=unit or "",
|
303
|
+
)
|
304
|
+
|
305
|
+
self._histograms[name].record(
|
306
|
+
value,
|
307
|
+
attributes={
|
308
|
+
key: cast(Any, value)
|
309
|
+
for key, value in attributes.items()
|
310
|
+
if value is not None and value is not MISSING
|
311
|
+
},
|
312
|
+
)
|
313
|
+
|
314
|
+
case "gauge":
|
315
|
+
if name not in self._gauges:
|
316
|
+
self._gauges[name] = self.meter.create_gauge(
|
317
|
+
name=name,
|
318
|
+
unit=unit or "",
|
319
|
+
)
|
320
|
+
|
321
|
+
self._gauges[name].set(
|
322
|
+
value,
|
323
|
+
attributes={
|
324
|
+
key: cast(Any, value)
|
325
|
+
for key, value in attributes.items()
|
326
|
+
if value is not None and value is not MISSING
|
327
|
+
},
|
328
|
+
)
|
287
329
|
|
288
330
|
def record_attributes(
|
289
331
|
self,
|
@@ -589,6 +631,7 @@ class OpenTelemetry:
|
|
589
631
|
metric: str,
|
590
632
|
value: float | int,
|
591
633
|
unit: str | None,
|
634
|
+
kind: ObservabilityMetricKind,
|
592
635
|
attributes: Mapping[str, ObservabilityAttribute],
|
593
636
|
) -> None:
|
594
637
|
"""
|
@@ -609,6 +652,8 @@ class OpenTelemetry:
|
|
609
652
|
The numeric value of the metric
|
610
653
|
unit: str | None
|
611
654
|
Optional unit for the metric (e.g., "ms", "bytes")
|
655
|
+
kind: ObservabilityMetricKind
|
656
|
+
The metric kind defining its value handling.
|
612
657
|
attributes: Mapping[str, ObservabilityAttribute]
|
613
658
|
Key-value attributes associated with the metric
|
614
659
|
"""
|
@@ -622,6 +667,7 @@ class OpenTelemetry:
|
|
622
667
|
metric,
|
623
668
|
value=value,
|
624
669
|
unit=unit,
|
670
|
+
kind=kind,
|
625
671
|
attributes=attributes,
|
626
672
|
)
|
627
673
|
|
haiway/utils/__init__.py
CHANGED
@@ -11,14 +11,26 @@ from haiway.utils.env import (
|
|
11
11
|
)
|
12
12
|
from haiway.utils.formatting import format_str
|
13
13
|
from haiway.utils.logs import setup_logging
|
14
|
+
from haiway.utils.metadata import (
|
15
|
+
META_EMPTY,
|
16
|
+
Meta,
|
17
|
+
MetaTags,
|
18
|
+
MetaValue,
|
19
|
+
MetaValues,
|
20
|
+
)
|
14
21
|
from haiway.utils.mimic import mimic_function
|
15
22
|
from haiway.utils.noop import async_noop, noop
|
16
23
|
from haiway.utils.queue import AsyncQueue
|
17
24
|
from haiway.utils.stream import AsyncStream
|
18
25
|
|
19
26
|
__all__ = (
|
27
|
+
"META_EMPTY",
|
20
28
|
"AsyncQueue",
|
21
29
|
"AsyncStream",
|
30
|
+
"Meta",
|
31
|
+
"MetaTags",
|
32
|
+
"MetaValue",
|
33
|
+
"MetaValues",
|
22
34
|
"always",
|
23
35
|
"as_dict",
|
24
36
|
"as_list",
|
haiway/utils/collections.py
CHANGED
@@ -14,20 +14,20 @@ __all__ = (
|
|
14
14
|
|
15
15
|
@overload
|
16
16
|
def as_list[T](
|
17
|
-
|
17
|
+
iterable: Iterable[T],
|
18
18
|
/,
|
19
19
|
) -> list[T]: ...
|
20
20
|
|
21
21
|
|
22
22
|
@overload
|
23
23
|
def as_list[T](
|
24
|
-
|
24
|
+
iterable: Iterable[T] | None,
|
25
25
|
/,
|
26
26
|
) -> list[T] | None: ...
|
27
27
|
|
28
28
|
|
29
29
|
def as_list[T](
|
30
|
-
|
30
|
+
iterable: Iterable[T] | None,
|
31
31
|
/,
|
32
32
|
) -> list[T] | None:
|
33
33
|
"""
|
@@ -35,44 +35,44 @@ def as_list[T](
|
|
35
35
|
|
36
36
|
Parameters
|
37
37
|
----------
|
38
|
-
|
39
|
-
The input
|
38
|
+
iterable : Iterable[T] | None
|
39
|
+
The input iterable to be converted to a list.
|
40
40
|
If None is provided, None is returned.
|
41
41
|
|
42
42
|
Returns
|
43
43
|
-------
|
44
44
|
list[T] | None
|
45
|
-
A new list containing all elements of the input
|
45
|
+
A new list containing all elements of the input iterable,
|
46
46
|
or the original list if it was already one.
|
47
47
|
Returns None if None was provided.
|
48
48
|
"""
|
49
49
|
|
50
|
-
if
|
50
|
+
if iterable is None:
|
51
51
|
return None
|
52
52
|
|
53
|
-
elif isinstance(
|
54
|
-
return
|
53
|
+
elif isinstance(iterable, list):
|
54
|
+
return iterable
|
55
55
|
|
56
56
|
else:
|
57
|
-
return list(
|
57
|
+
return list(iterable)
|
58
58
|
|
59
59
|
|
60
60
|
@overload
|
61
61
|
def as_tuple[T](
|
62
|
-
|
62
|
+
iterable: Iterable[T],
|
63
63
|
/,
|
64
64
|
) -> tuple[T, ...]: ...
|
65
65
|
|
66
66
|
|
67
67
|
@overload
|
68
68
|
def as_tuple[T](
|
69
|
-
|
69
|
+
iterable: Iterable[T] | None,
|
70
70
|
/,
|
71
71
|
) -> tuple[T, ...] | None: ...
|
72
72
|
|
73
73
|
|
74
74
|
def as_tuple[T](
|
75
|
-
|
75
|
+
iterable: Iterable[T] | None,
|
76
76
|
/,
|
77
77
|
) -> tuple[T, ...] | None:
|
78
78
|
"""
|
@@ -80,26 +80,26 @@ def as_tuple[T](
|
|
80
80
|
|
81
81
|
Parameters
|
82
82
|
----------
|
83
|
-
|
84
|
-
The input
|
83
|
+
iterable : Iterable[T] | None
|
84
|
+
The input iterable to be converted to a tuple.
|
85
85
|
If None is provided, None is returned.
|
86
86
|
|
87
87
|
Returns
|
88
88
|
-------
|
89
89
|
tuple[T, ...] | None
|
90
|
-
A new tuple containing all elements of the input
|
90
|
+
A new tuple containing all elements of the input iterable,
|
91
91
|
or the original tuple if it was already one.
|
92
92
|
Returns None if None was provided.
|
93
93
|
"""
|
94
94
|
|
95
|
-
if
|
95
|
+
if iterable is None:
|
96
96
|
return None
|
97
97
|
|
98
|
-
elif isinstance(
|
99
|
-
return
|
98
|
+
elif isinstance(iterable, tuple):
|
99
|
+
return iterable
|
100
100
|
|
101
101
|
else:
|
102
|
-
return tuple(
|
102
|
+
return tuple(iterable)
|
103
103
|
|
104
104
|
|
105
105
|
@overload
|
@@ -149,20 +149,20 @@ def as_set[T](
|
|
149
149
|
|
150
150
|
@overload
|
151
151
|
def as_dict[K, V](
|
152
|
-
|
152
|
+
mapping: Mapping[K, V],
|
153
153
|
/,
|
154
154
|
) -> dict[K, V]: ...
|
155
155
|
|
156
156
|
|
157
157
|
@overload
|
158
158
|
def as_dict[K, V](
|
159
|
-
|
159
|
+
mapping: Mapping[K, V] | None,
|
160
160
|
/,
|
161
161
|
) -> dict[K, V] | None: ...
|
162
162
|
|
163
163
|
|
164
164
|
def as_dict[K, V](
|
165
|
-
|
165
|
+
mapping: Mapping[K, V] | None,
|
166
166
|
/,
|
167
167
|
) -> dict[K, V] | None:
|
168
168
|
"""
|
@@ -170,30 +170,30 @@ def as_dict[K, V](
|
|
170
170
|
|
171
171
|
Parameters
|
172
172
|
----------
|
173
|
-
|
174
|
-
The input
|
173
|
+
mapping : Mapping[K, V] | None
|
174
|
+
The input mapping to be converted to a dict.
|
175
175
|
If None is provided, None is returned.
|
176
176
|
|
177
177
|
Returns
|
178
178
|
-------
|
179
179
|
dict[K, V] | None
|
180
|
-
A new dict containing all elements of the input
|
180
|
+
A new dict containing all elements of the input mapping,
|
181
181
|
or the original dict if it was already one.
|
182
182
|
Returns None if None was provided.
|
183
183
|
"""
|
184
184
|
|
185
|
-
if
|
185
|
+
if mapping is None:
|
186
186
|
return None
|
187
187
|
|
188
|
-
elif isinstance(
|
189
|
-
return
|
188
|
+
elif isinstance(mapping, dict):
|
189
|
+
return mapping
|
190
190
|
|
191
191
|
else:
|
192
|
-
return dict(
|
192
|
+
return dict(mapping)
|
193
193
|
|
194
194
|
|
195
195
|
@overload
|
196
|
-
def without_missing
|
196
|
+
def without_missing(
|
197
197
|
mapping: Mapping[str, Any],
|
198
198
|
/,
|
199
199
|
) -> Mapping[str, Any]: ...
|
haiway/utils/formatting.py
CHANGED
@@ -1,14 +1,18 @@
|
|
1
|
-
from collections.abc import ItemsView, Mapping, Sequence
|
1
|
+
from collections.abc import ItemsView, Mapping, Sequence, Set
|
2
|
+
from datetime import datetime
|
2
3
|
from typing import Any
|
4
|
+
from uuid import UUID
|
3
5
|
|
4
6
|
from haiway.types.missing import MISSING
|
5
7
|
|
6
8
|
__all__ = ("format_str",)
|
7
9
|
|
8
10
|
|
9
|
-
def format_str( # noqa: PLR0911
|
11
|
+
def format_str( # noqa: PLR0911 PLR0912 C901
|
10
12
|
value: Any,
|
11
13
|
/,
|
14
|
+
*,
|
15
|
+
indent: int = 0,
|
12
16
|
) -> str:
|
13
17
|
"""
|
14
18
|
Format any Python value into a readable string representation.
|
@@ -37,108 +41,153 @@ def format_str( # noqa: PLR0911
|
|
37
41
|
- MISSING values are converted to empty strings
|
38
42
|
- Nested structures maintain proper indentation
|
39
43
|
"""
|
40
|
-
|
41
|
-
|
44
|
+
if value is None:
|
45
|
+
return "None"
|
46
|
+
|
47
|
+
elif isinstance(value, str):
|
42
48
|
if "\n" in value:
|
43
|
-
|
49
|
+
indent_str = " " * (indent + 2)
|
50
|
+
indented_value = value.replace("\n", f"\n{indent_str}")
|
51
|
+
return f'"""\n{indent_str}{indented_value}\n{" " * indent}"""'
|
44
52
|
|
45
53
|
else:
|
46
54
|
return f'"{value}"'
|
47
55
|
|
48
|
-
|
49
|
-
|
50
|
-
|
56
|
+
elif isinstance(value, int | float | complex):
|
57
|
+
return str(value)
|
58
|
+
|
59
|
+
elif isinstance(value, bool):
|
60
|
+
return str(value)
|
61
|
+
|
62
|
+
elif isinstance(value, set | frozenset | Set):
|
63
|
+
return _set_str(
|
64
|
+
value,
|
65
|
+
indent=indent,
|
66
|
+
)
|
51
67
|
|
52
|
-
# try unpack mapping
|
53
68
|
elif isinstance(value, Mapping):
|
54
|
-
return _mapping_str(
|
69
|
+
return _mapping_str(
|
70
|
+
value,
|
71
|
+
indent=indent,
|
72
|
+
)
|
55
73
|
|
56
|
-
# try unpack sequence
|
57
74
|
elif isinstance(value, Sequence):
|
58
|
-
return _sequence_str(
|
75
|
+
return _sequence_str(
|
76
|
+
value,
|
77
|
+
indent=indent,
|
78
|
+
)
|
59
79
|
|
60
80
|
elif value is MISSING:
|
61
81
|
return ""
|
62
82
|
|
83
|
+
elif isinstance(value, UUID):
|
84
|
+
return str(value)
|
85
|
+
|
86
|
+
elif isinstance(value, datetime):
|
87
|
+
return value.isoformat()
|
88
|
+
|
89
|
+
elif isinstance(value, bytes):
|
90
|
+
return repr(value)
|
91
|
+
|
63
92
|
else: # fallback to object
|
64
|
-
return _object_str(
|
93
|
+
return _object_str(
|
94
|
+
value,
|
95
|
+
indent=indent,
|
96
|
+
)
|
65
97
|
|
66
98
|
|
67
99
|
def _attribute_str(
|
68
100
|
*,
|
69
101
|
key: str,
|
70
102
|
value: str,
|
103
|
+
indent: int,
|
71
104
|
) -> str:
|
105
|
+
indent_str = " " * indent
|
72
106
|
if "\n" in value:
|
73
|
-
|
74
|
-
return f"┝ {key}:\n{
|
107
|
+
# Don't add extra indentation - value should already handle it
|
108
|
+
return f"{indent_str}┝ {key}:\n{value}"
|
75
109
|
|
76
110
|
else:
|
77
|
-
return f"┝ {key}: {value}"
|
111
|
+
return f"{indent_str}┝ {key}: {value}"
|
78
112
|
|
79
113
|
|
80
114
|
def _element_str(
|
81
115
|
*,
|
82
116
|
key: Any,
|
83
|
-
value:
|
117
|
+
value: str,
|
118
|
+
indent: int,
|
84
119
|
) -> str:
|
120
|
+
indent_str = " " * indent
|
85
121
|
if "\n" in value:
|
86
|
-
|
87
|
-
return f"[{key}]:\n{
|
122
|
+
# Don't add extra indentation - value should already handle it
|
123
|
+
return f"{indent_str}[{key}]:\n{value}"
|
88
124
|
|
89
125
|
else:
|
90
|
-
return f"[{key}]: {value}"
|
126
|
+
return f"{indent_str}[{key}]: {value}"
|
91
127
|
|
92
128
|
|
93
129
|
def _object_str(
|
94
130
|
other: object,
|
95
131
|
/,
|
132
|
+
*,
|
133
|
+
indent: int,
|
96
134
|
) -> str:
|
135
|
+
indent_str: str = " " * indent
|
97
136
|
if not hasattr(other, "__dict__"):
|
98
|
-
return
|
137
|
+
return f"{indent_str}{other}"
|
99
138
|
|
100
139
|
variables: ItemsView[str, Any] = vars(other).items()
|
101
|
-
|
102
|
-
parts: list[str] = [
|
140
|
+
header = f"{indent_str}┍━ {type(other).__name__}:"
|
141
|
+
parts: list[str] = [header]
|
103
142
|
for key, value in variables:
|
104
143
|
if key.startswith("_"):
|
105
144
|
continue # skip private and dunder
|
106
145
|
|
107
|
-
value_string: str = format_str(
|
146
|
+
value_string: str = format_str(
|
147
|
+
value,
|
148
|
+
indent=indent + 2,
|
149
|
+
)
|
108
150
|
|
109
151
|
if value_string:
|
110
152
|
parts.append(
|
111
153
|
_attribute_str(
|
112
154
|
key=key,
|
113
155
|
value=value_string,
|
156
|
+
indent=indent,
|
114
157
|
)
|
115
158
|
)
|
116
159
|
|
117
160
|
else:
|
118
161
|
continue # skip empty elements
|
119
162
|
|
120
|
-
|
121
|
-
return "\n".join(parts) + "\n┕━"
|
122
|
-
|
123
|
-
else:
|
124
|
-
return ""
|
163
|
+
return "\n".join(parts) + f"\n{indent_str}┕━"
|
125
164
|
|
126
165
|
|
127
166
|
def _mapping_str(
|
128
167
|
mapping: Mapping[Any, Any],
|
129
168
|
/,
|
169
|
+
*,
|
170
|
+
indent: int,
|
130
171
|
) -> str:
|
131
172
|
items: ItemsView[Any, Any] = mapping.items()
|
132
173
|
|
174
|
+
indent_str = " " * indent
|
133
175
|
parts: list[str] = []
|
134
176
|
for key, value in items:
|
135
|
-
value_string: str = format_str(
|
177
|
+
value_string: str = format_str(
|
178
|
+
value,
|
179
|
+
indent=indent + 2,
|
180
|
+
)
|
136
181
|
|
137
182
|
if value_string:
|
138
183
|
parts.append(
|
139
184
|
_element_str(
|
140
|
-
key=
|
185
|
+
key=format_str(
|
186
|
+
key,
|
187
|
+
indent=indent + 2,
|
188
|
+
),
|
141
189
|
value=value_string,
|
190
|
+
indent=indent + 2,
|
142
191
|
)
|
143
192
|
)
|
144
193
|
|
@@ -146,25 +195,64 @@ def _mapping_str(
|
|
146
195
|
continue # skip empty items
|
147
196
|
|
148
197
|
if parts:
|
149
|
-
|
198
|
+
open_brace = "{\n" if indent == 0 else f"{indent_str}{{\n"
|
199
|
+
close_brace = "\n}" if indent == 0 else f"\n{indent_str}}}"
|
200
|
+
return open_brace + "\n".join(parts) + close_brace
|
150
201
|
|
151
202
|
else:
|
152
|
-
return "{}"
|
203
|
+
return "{}" if indent == 0 else f"{indent_str}{{}}"
|
204
|
+
|
205
|
+
|
206
|
+
def _set_str(
|
207
|
+
set_value: Set[Any] | set[Any] | frozenset[Any],
|
208
|
+
/,
|
209
|
+
*,
|
210
|
+
indent: int,
|
211
|
+
) -> str:
|
212
|
+
indent_str: str = " " * indent
|
213
|
+
element_indent_str: str = " " * (indent + 2)
|
214
|
+
parts: list[str] = []
|
215
|
+
for element in set_value:
|
216
|
+
element_string: str = format_str(
|
217
|
+
element,
|
218
|
+
indent=indent + 2,
|
219
|
+
)
|
220
|
+
|
221
|
+
if element_string:
|
222
|
+
parts.append(f"{element_indent_str}{element_string}")
|
223
|
+
|
224
|
+
else:
|
225
|
+
continue # skip empty elements
|
226
|
+
|
227
|
+
if parts:
|
228
|
+
open_brace: str = f"{indent_str}{{\n"
|
229
|
+
close_brace: str = f"\n{indent_str}}}"
|
230
|
+
return open_brace + ",\n".join(parts) + close_brace
|
231
|
+
|
232
|
+
else:
|
233
|
+
return f"{indent_str}{{}}"
|
153
234
|
|
154
235
|
|
155
236
|
def _sequence_str(
|
156
237
|
sequence: Sequence[Any],
|
157
238
|
/,
|
239
|
+
*,
|
240
|
+
indent: int,
|
158
241
|
) -> str:
|
242
|
+
indent_str: str = " " * indent
|
159
243
|
parts: list[str] = []
|
160
244
|
for idx, element in enumerate(sequence):
|
161
|
-
element_string: str = format_str(
|
245
|
+
element_string: str = format_str(
|
246
|
+
element,
|
247
|
+
indent=indent + 2,
|
248
|
+
)
|
162
249
|
|
163
250
|
if element_string:
|
164
251
|
parts.append(
|
165
252
|
_element_str(
|
166
253
|
key=idx,
|
167
254
|
value=element_string,
|
255
|
+
indent=indent + 2,
|
168
256
|
)
|
169
257
|
)
|
170
258
|
|
@@ -172,7 +260,9 @@ def _sequence_str(
|
|
172
260
|
continue # skip empty elements
|
173
261
|
|
174
262
|
if parts:
|
175
|
-
|
263
|
+
open_bracket: str = f"{indent_str}[\n"
|
264
|
+
close_bracket: str = f"\n{indent_str}]"
|
265
|
+
return open_bracket + "\n".join(parts) + close_bracket
|
176
266
|
|
177
267
|
else:
|
178
|
-
return "[]"
|
268
|
+
return f"{indent_str}[]"
|
haiway/utils/metadata.py
ADDED
@@ -0,0 +1,480 @@
|
|
1
|
+
import json
|
2
|
+
from collections.abc import Collection, Iterator, Mapping
|
3
|
+
from datetime import datetime
|
4
|
+
from types import EllipsisType
|
5
|
+
from typing import Any, ClassVar, Final, Self, cast, final
|
6
|
+
from uuid import UUID
|
7
|
+
|
8
|
+
__all__ = (
|
9
|
+
"META_EMPTY",
|
10
|
+
"Meta",
|
11
|
+
"MetaTags",
|
12
|
+
"MetaValue",
|
13
|
+
"MetaValues",
|
14
|
+
)
|
15
|
+
|
16
|
+
type MetaValue = Mapping[str, MetaValue] | Collection[MetaValue] | str | float | int | bool | None
|
17
|
+
type MetaValues = Mapping[str, MetaValue]
|
18
|
+
type MetaTags = Collection[str]
|
19
|
+
|
20
|
+
|
21
|
+
@final
|
22
|
+
class Meta(Mapping[str, MetaValue]):
|
23
|
+
"""
|
24
|
+
Immutable metadata container with type-safe access to common fields.
|
25
|
+
|
26
|
+
Meta provides a structured way to store and access metadata as key-value pairs
|
27
|
+
with built-in support for common metadata fields like identifiers, names,
|
28
|
+
descriptions, tags, and timestamps. All values are validated to ensure they
|
29
|
+
conform to the MetaValue type constraints.
|
30
|
+
|
31
|
+
The class implements the Mapping protocol, allowing dict-like access while
|
32
|
+
maintaining immutability. It provides builder-style methods (with_*) for
|
33
|
+
creating modified copies and convenience properties for accessing standard
|
34
|
+
metadata fields.
|
35
|
+
|
36
|
+
Examples
|
37
|
+
--------
|
38
|
+
>>> meta = Meta.of({"kind": "user", "name": "John"})
|
39
|
+
>>> meta = meta.with_tags(["active", "verified"])
|
40
|
+
>>> print(meta.kind) # "user"
|
41
|
+
>>> print(meta.tags) # ("active", "verified")
|
42
|
+
"""
|
43
|
+
|
44
|
+
__IMMUTABLE__: ClassVar[EllipsisType] = ...
|
45
|
+
|
46
|
+
__slots__ = ("_values",)
|
47
|
+
|
48
|
+
def __init__(
|
49
|
+
self,
|
50
|
+
values: MetaValues,
|
51
|
+
/,
|
52
|
+
):
|
53
|
+
assert isinstance(values, Mapping) # nosec: B101
|
54
|
+
self._values: MetaValues
|
55
|
+
object.__setattr__(
|
56
|
+
self,
|
57
|
+
"_values",
|
58
|
+
values,
|
59
|
+
)
|
60
|
+
|
61
|
+
@classmethod
|
62
|
+
def of(
|
63
|
+
cls,
|
64
|
+
meta: Self | MetaValues | None,
|
65
|
+
) -> Self:
|
66
|
+
"""
|
67
|
+
Create a Meta instance from various input types.
|
68
|
+
|
69
|
+
This factory method provides a flexible way to create Meta instances,
|
70
|
+
handling None values, existing Meta instances, and raw mappings.
|
71
|
+
|
72
|
+
Parameters
|
73
|
+
----------
|
74
|
+
meta : Self | MetaValues | None
|
75
|
+
The metadata to wrap. Can be None (returns META_EMPTY),
|
76
|
+
an existing Meta instance (returns as-is), or a mapping
|
77
|
+
of values to validate and wrap.
|
78
|
+
|
79
|
+
Returns
|
80
|
+
-------
|
81
|
+
Self
|
82
|
+
A Meta instance containing the provided metadata.
|
83
|
+
"""
|
84
|
+
if meta is None:
|
85
|
+
return cast(Self, META_EMPTY)
|
86
|
+
|
87
|
+
elif isinstance(meta, Meta):
|
88
|
+
return cast(Self, meta)
|
89
|
+
|
90
|
+
else:
|
91
|
+
assert isinstance(meta, Mapping) # nosec: B101
|
92
|
+
return cls({key: _validated_meta_value(value) for key, value in meta.items()})
|
93
|
+
|
94
|
+
@classmethod
|
95
|
+
def from_mapping(
|
96
|
+
cls,
|
97
|
+
mapping: Mapping[str, Any],
|
98
|
+
/,
|
99
|
+
) -> Self:
|
100
|
+
return cls({key: _validated_meta_value(value) for key, value in mapping.items()})
|
101
|
+
|
102
|
+
@classmethod
|
103
|
+
def from_json(
|
104
|
+
cls,
|
105
|
+
value: str | bytes,
|
106
|
+
/,
|
107
|
+
) -> Self:
|
108
|
+
match json.loads(value):
|
109
|
+
case {**values}:
|
110
|
+
return cls({key: _validated_meta_value(val) for key, val in values.items()})
|
111
|
+
|
112
|
+
case other:
|
113
|
+
raise ValueError(f"Invalid json: {other}")
|
114
|
+
|
115
|
+
def to_str(self) -> str:
|
116
|
+
return self.__str__()
|
117
|
+
|
118
|
+
def to_mapping(
|
119
|
+
self,
|
120
|
+
) -> Mapping[str, Any]:
|
121
|
+
return self._values
|
122
|
+
|
123
|
+
def to_json(
|
124
|
+
self,
|
125
|
+
) -> str:
|
126
|
+
return json.dumps(self._values)
|
127
|
+
|
128
|
+
@property
|
129
|
+
def kind(self) -> str | None:
|
130
|
+
match self._values.get("kind"):
|
131
|
+
case str() as kind:
|
132
|
+
return kind
|
133
|
+
|
134
|
+
case _:
|
135
|
+
return None
|
136
|
+
|
137
|
+
def with_kind(
|
138
|
+
self,
|
139
|
+
kind: str,
|
140
|
+
/,
|
141
|
+
) -> Self:
|
142
|
+
return self.__class__(
|
143
|
+
{
|
144
|
+
**self._values,
|
145
|
+
"kind": kind,
|
146
|
+
}
|
147
|
+
)
|
148
|
+
|
149
|
+
def _get_uuid(
|
150
|
+
self,
|
151
|
+
key: str,
|
152
|
+
/,
|
153
|
+
) -> UUID | None:
|
154
|
+
match self._values.get(key):
|
155
|
+
case str() as identifier:
|
156
|
+
try:
|
157
|
+
return UUID(identifier)
|
158
|
+
except ValueError:
|
159
|
+
return None
|
160
|
+
case _:
|
161
|
+
return None
|
162
|
+
|
163
|
+
def _with_uuid(
|
164
|
+
self,
|
165
|
+
key: str,
|
166
|
+
/,
|
167
|
+
*,
|
168
|
+
value: UUID,
|
169
|
+
) -> Self:
|
170
|
+
return self.__class__(
|
171
|
+
{
|
172
|
+
**self._values,
|
173
|
+
key: str(value),
|
174
|
+
}
|
175
|
+
)
|
176
|
+
|
177
|
+
@property
|
178
|
+
def identifier(self) -> UUID | None:
|
179
|
+
return self._get_uuid("identifier")
|
180
|
+
|
181
|
+
def with_identifier(
|
182
|
+
self,
|
183
|
+
identifier: UUID,
|
184
|
+
/,
|
185
|
+
) -> Self:
|
186
|
+
return self._with_uuid(
|
187
|
+
"identifier",
|
188
|
+
value=identifier,
|
189
|
+
)
|
190
|
+
|
191
|
+
@property
|
192
|
+
def origin_identifier(self) -> UUID | None:
|
193
|
+
return self._get_uuid("origin_identifier")
|
194
|
+
|
195
|
+
def with_origin_identifier(
|
196
|
+
self,
|
197
|
+
identifier: UUID,
|
198
|
+
/,
|
199
|
+
) -> Self:
|
200
|
+
return self._with_uuid(
|
201
|
+
"origin_identifier",
|
202
|
+
value=identifier,
|
203
|
+
)
|
204
|
+
|
205
|
+
@property
|
206
|
+
def predecessor_identifier(self) -> UUID | None:
|
207
|
+
return self._get_uuid("predecessor_identifier")
|
208
|
+
|
209
|
+
def with_predecessor_identifier(
|
210
|
+
self,
|
211
|
+
identifier: UUID,
|
212
|
+
/,
|
213
|
+
) -> Self:
|
214
|
+
return self._with_uuid(
|
215
|
+
"predecessor_identifier",
|
216
|
+
value=identifier,
|
217
|
+
)
|
218
|
+
|
219
|
+
@property
|
220
|
+
def successor_identifier(self) -> UUID | None:
|
221
|
+
return self._get_uuid("successor_identifier")
|
222
|
+
|
223
|
+
def with_successor_identifier(
|
224
|
+
self,
|
225
|
+
identifier: UUID,
|
226
|
+
/,
|
227
|
+
) -> Self:
|
228
|
+
return self._with_uuid(
|
229
|
+
"successor_identifier",
|
230
|
+
value=identifier,
|
231
|
+
)
|
232
|
+
|
233
|
+
@property
|
234
|
+
def name(self) -> str | None:
|
235
|
+
match self._values.get("name"):
|
236
|
+
case str() as name:
|
237
|
+
return name
|
238
|
+
|
239
|
+
case _:
|
240
|
+
return None
|
241
|
+
|
242
|
+
def with_name(
|
243
|
+
self,
|
244
|
+
name: str,
|
245
|
+
/,
|
246
|
+
) -> Self:
|
247
|
+
return self.__class__(
|
248
|
+
{
|
249
|
+
**self._values,
|
250
|
+
"name": name,
|
251
|
+
}
|
252
|
+
)
|
253
|
+
|
254
|
+
@property
|
255
|
+
def description(self) -> str | None:
|
256
|
+
match self._values.get("description"):
|
257
|
+
case str() as description:
|
258
|
+
return description
|
259
|
+
|
260
|
+
case _:
|
261
|
+
return None
|
262
|
+
|
263
|
+
def with_description(
|
264
|
+
self,
|
265
|
+
description: str,
|
266
|
+
/,
|
267
|
+
) -> Self:
|
268
|
+
return self.__class__(
|
269
|
+
{
|
270
|
+
**self._values,
|
271
|
+
"description": description,
|
272
|
+
}
|
273
|
+
)
|
274
|
+
|
275
|
+
@property
|
276
|
+
def tags(self) -> MetaTags:
|
277
|
+
match self._values.get("tags"):
|
278
|
+
case [*tags]:
|
279
|
+
return tuple(tag for tag in tags if isinstance(tag, str))
|
280
|
+
|
281
|
+
case _:
|
282
|
+
return ()
|
283
|
+
|
284
|
+
def with_tags(
|
285
|
+
self,
|
286
|
+
tags: MetaTags,
|
287
|
+
/,
|
288
|
+
) -> Self:
|
289
|
+
match self._values.get("tags"):
|
290
|
+
case [*current_tags]:
|
291
|
+
return self.__class__(
|
292
|
+
{
|
293
|
+
**self._values,
|
294
|
+
"tags": (
|
295
|
+
*current_tags,
|
296
|
+
*(
|
297
|
+
_validated_meta_value(tag)
|
298
|
+
for tag in tags
|
299
|
+
if tag not in current_tags
|
300
|
+
),
|
301
|
+
),
|
302
|
+
}
|
303
|
+
)
|
304
|
+
|
305
|
+
case _:
|
306
|
+
return self.__class__({**self._values, "tags": tags})
|
307
|
+
|
308
|
+
def has_tags(
|
309
|
+
self,
|
310
|
+
tags: MetaTags,
|
311
|
+
/,
|
312
|
+
) -> bool:
|
313
|
+
match self._values.get("tags"):
|
314
|
+
case [*meta_tags]:
|
315
|
+
return all(tag in meta_tags for tag in tags)
|
316
|
+
|
317
|
+
case _:
|
318
|
+
return False
|
319
|
+
|
320
|
+
@property
|
321
|
+
def creation(self) -> datetime | None:
|
322
|
+
match self._values.get("creation"):
|
323
|
+
case str() as iso_value:
|
324
|
+
try:
|
325
|
+
return datetime.fromisoformat(iso_value)
|
326
|
+
|
327
|
+
except ValueError:
|
328
|
+
return None
|
329
|
+
|
330
|
+
case _:
|
331
|
+
return None
|
332
|
+
|
333
|
+
def with_creation(
|
334
|
+
self,
|
335
|
+
creation: datetime,
|
336
|
+
/,
|
337
|
+
) -> Self:
|
338
|
+
return self.__class__(
|
339
|
+
{
|
340
|
+
**self._values,
|
341
|
+
"creation": creation.isoformat(),
|
342
|
+
}
|
343
|
+
)
|
344
|
+
|
345
|
+
def merged_with(
|
346
|
+
self,
|
347
|
+
values: Self | MetaValues | None,
|
348
|
+
/,
|
349
|
+
) -> Self:
|
350
|
+
if not values:
|
351
|
+
return self # do not make a copy when nothing will be updated
|
352
|
+
|
353
|
+
return self.__class__(
|
354
|
+
{
|
355
|
+
**self._values, # already validated
|
356
|
+
**{key: _validated_meta_value(value) for key, value in values.items()},
|
357
|
+
}
|
358
|
+
)
|
359
|
+
|
360
|
+
def excluding(
|
361
|
+
self,
|
362
|
+
*excluded: str,
|
363
|
+
) -> Self:
|
364
|
+
if not excluded:
|
365
|
+
return self
|
366
|
+
|
367
|
+
excluded_set: set[str] = set(excluded)
|
368
|
+
return self.__class__(
|
369
|
+
{key: value for key, value in self._values.items() if key not in excluded_set}
|
370
|
+
)
|
371
|
+
|
372
|
+
def updated(
|
373
|
+
self,
|
374
|
+
**values: MetaValue,
|
375
|
+
) -> Self:
|
376
|
+
return self.__replace__(**values)
|
377
|
+
|
378
|
+
def __replace__(
|
379
|
+
self,
|
380
|
+
**values: Any,
|
381
|
+
) -> Self:
|
382
|
+
return self.merged_with(values)
|
383
|
+
|
384
|
+
def __bool__(self) -> bool:
|
385
|
+
return bool(self._values)
|
386
|
+
|
387
|
+
def __contains__(
|
388
|
+
self,
|
389
|
+
element: Any,
|
390
|
+
) -> bool:
|
391
|
+
return element in self._values
|
392
|
+
|
393
|
+
def __setattr__(
|
394
|
+
self,
|
395
|
+
name: str,
|
396
|
+
value: Any,
|
397
|
+
) -> Any:
|
398
|
+
raise AttributeError(
|
399
|
+
f"Can't modify immutable {self.__class__.__qualname__},"
|
400
|
+
f" attribute - '{name}' cannot be modified"
|
401
|
+
)
|
402
|
+
|
403
|
+
def __delattr__(
|
404
|
+
self,
|
405
|
+
name: str,
|
406
|
+
) -> None:
|
407
|
+
raise AttributeError(
|
408
|
+
f"Can't modify immutable {self.__class__.__qualname__},"
|
409
|
+
f" attribute - '{name}' cannot be deleted"
|
410
|
+
)
|
411
|
+
|
412
|
+
def __setitem__(
|
413
|
+
self,
|
414
|
+
key: str,
|
415
|
+
value: Any,
|
416
|
+
) -> MetaValue:
|
417
|
+
raise AttributeError(
|
418
|
+
f"Can't modify immutable {self.__class__.__qualname__},"
|
419
|
+
f" item - '{key}' cannot be modified"
|
420
|
+
)
|
421
|
+
|
422
|
+
def __delitem__(
|
423
|
+
self,
|
424
|
+
key: str,
|
425
|
+
) -> MetaValue:
|
426
|
+
raise AttributeError(
|
427
|
+
f"Can't modify immutable {self.__class__.__qualname__},"
|
428
|
+
f" item - '{key}' cannot be deleted"
|
429
|
+
)
|
430
|
+
|
431
|
+
def __getitem__(
|
432
|
+
self,
|
433
|
+
key: str,
|
434
|
+
) -> MetaValue:
|
435
|
+
return self._values[key]
|
436
|
+
|
437
|
+
def __iter__(self) -> Iterator[str]:
|
438
|
+
return iter(self._values)
|
439
|
+
|
440
|
+
def __len__(self) -> int:
|
441
|
+
return len(self._values)
|
442
|
+
|
443
|
+
def __copy__(self) -> Self:
|
444
|
+
return self # Metadata is immutable, no need to provide an actual copy
|
445
|
+
|
446
|
+
def __deepcopy__(
|
447
|
+
self,
|
448
|
+
memo: dict[int, Any] | None,
|
449
|
+
) -> Self:
|
450
|
+
return self # Metadata is immutable, no need to provide an actual copy
|
451
|
+
|
452
|
+
|
453
|
+
def _validated_meta_value(value: Any) -> MetaValue: # noqa: PLR0911
|
454
|
+
match value:
|
455
|
+
case None:
|
456
|
+
return value
|
457
|
+
|
458
|
+
case str():
|
459
|
+
return value
|
460
|
+
|
461
|
+
case int():
|
462
|
+
return value
|
463
|
+
|
464
|
+
case float():
|
465
|
+
return value
|
466
|
+
|
467
|
+
case bool():
|
468
|
+
return value
|
469
|
+
|
470
|
+
case [*values]:
|
471
|
+
return tuple(_validated_meta_value(value) for value in values)
|
472
|
+
|
473
|
+
case {**values}:
|
474
|
+
return {key: _validated_meta_value(value) for key, value in values.items()}
|
475
|
+
|
476
|
+
case other:
|
477
|
+
raise TypeError(f"Invalid Meta value: {type(other)}")
|
478
|
+
|
479
|
+
|
480
|
+
META_EMPTY: Final[Meta] = Meta({})
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: haiway
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.27.0
|
4
4
|
Summary: Framework for dependency injection and state management within structured concurrency model.
|
5
5
|
Project-URL: Homepage, https://miquido.com
|
6
6
|
Project-URL: Repository, https://github.com/miquido/haiway.git
|
@@ -1,10 +1,10 @@
|
|
1
|
-
haiway/__init__.py,sha256=
|
1
|
+
haiway/__init__.py,sha256=qzLQvEsgGKFNHp9A34sTg_MrBSvPUq4UtmQS172TfaI,2247
|
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=fjWNPXgBsW_nBsT8aR0SuiRehxDylRHHXA-nU2Rvy_Y,1356
|
4
|
+
haiway/context/access.py,sha256=Itpweo9sRFuf_Gu601v6_lWXmAJa4qYsmz2dQ7m7pYA,31527
|
5
5
|
haiway/context/disposables.py,sha256=7Jo-5qzS3UQvZUf4yOqUgfnueMg8I65jwHDp-4g6w54,7998
|
6
6
|
haiway/context/identifier.py,sha256=ps7YM1ZnUrj66SPVyxqMhTRMaYOMNSb82J3FfMRVHm4,4690
|
7
|
-
haiway/context/observability.py,sha256
|
7
|
+
haiway/context/observability.py,sha256=-Q-uzGdtYV1TYaSbhZLMQSTJEMRKBMKTiIv6RoyZXTM,20601
|
8
8
|
haiway/context/presets.py,sha256=NVRv-PzuzonxdzIEJmt8SRMRO0lUgKb-jR-2ixUtkzI,10760
|
9
9
|
haiway/context/state.py,sha256=1oeON23_vaX7IgyckPcA5jWMXije93TNuSh0l9dQqNE,9920
|
10
10
|
haiway/context/tasks.py,sha256=0LdoxkQW0op4-QhAA-IDQO0PQr6Q3Vp4mO5ssEFbclU,4930
|
@@ -14,12 +14,12 @@ haiway/helpers/asynchrony.py,sha256=Ddj8UdXhVczAbAC-rLpyhWa4RJ_W2Eolo45Veorq7_4,
|
|
14
14
|
haiway/helpers/caching.py,sha256=BqgcUGQSAmXsuLi5V8EwlZzuGyutHOn1V4k7BHsGKeg,14347
|
15
15
|
haiway/helpers/concurrent.py,sha256=kTcm_wLAKqVQOKgTHIKwbkMX5hJ5GIXOAc5RRIUvbo4,13063
|
16
16
|
haiway/helpers/files.py,sha256=MzGR-GF8FpBzSihDeen7wdxmX2R-JjAT1MB9aMW-8-g,11626
|
17
|
-
haiway/helpers/observability.py,sha256=
|
17
|
+
haiway/helpers/observability.py,sha256=ylDilsseZwTsPBmLO6GSUrbiTV593fwNqi3nL8wk814,11043
|
18
18
|
haiway/helpers/retries.py,sha256=OH__I9e-PUFxcSwuQLIzJ9F1MwXgbz1Ur4jEjJiOmjQ,8974
|
19
19
|
haiway/helpers/throttling.py,sha256=KBWUSHdKVMC5_nRMmmoPNwfp-3AcerQ6OczJa9gNLM0,5796
|
20
20
|
haiway/helpers/timeouting.py,sha256=GQ8-btb36f0Jq7TnorAPYXyKScNmf0nxHXCYxqGl-o8,3949
|
21
21
|
haiway/opentelemetry/__init__.py,sha256=TV-1C14mDAtcHhFZ29ActFQdrGH6x5KuGV9w-JlKYJg,91
|
22
|
-
haiway/opentelemetry/observability.py,sha256=
|
22
|
+
haiway/opentelemetry/observability.py,sha256=7VugyXLxGA75VUTSS9fBoVx4SvqZebFpGkOVV70q4-s,29142
|
23
23
|
haiway/state/__init__.py,sha256=mtYgg2TojOBNjFsfoRjYkfZPDhKV5sPJXxDGFBvB8-0,417
|
24
24
|
haiway/state/attributes.py,sha256=sububiFP23aBB8RGk6OvTUp7BEY6S0kER_uHC09yins,26733
|
25
25
|
haiway/state/immutable.py,sha256=YStOo1RQs8JoyLuumOmFXp9IrwOMzIHExVvDmcqj4lE,3693
|
@@ -30,17 +30,18 @@ haiway/state/validation.py,sha256=8IKm9-rJDmP0xJhqRCPc1mm7J1nnjbZDdInmCHZxaeg,24
|
|
30
30
|
haiway/types/__init__.py,sha256=BQKjbPZQej4DQsD_y4linn4rQMWdfaahKW-t-gapSjs,285
|
31
31
|
haiway/types/default.py,sha256=59chcOaoGqI2to08RamCCLluimfYbJp5xbYl3fWaLrM,4153
|
32
32
|
haiway/types/missing.py,sha256=V9FWUgAWUsmFuSXc57MORQOVh2wO2vlF1qYopmcEA2A,5760
|
33
|
-
haiway/utils/__init__.py,sha256=
|
33
|
+
haiway/utils/__init__.py,sha256=ZmRloJ3IUqotOjN1_ejS3NYb4A30LKR78YTaY5b3nao,1133
|
34
34
|
haiway/utils/always.py,sha256=dd6jDQ1j4DpJjTKO1J2Tv5xS8X1LnMC4kQ0D7DtKUvw,1230
|
35
|
-
haiway/utils/collections.py,sha256=
|
35
|
+
haiway/utils/collections.py,sha256=LExyiwpJX7nc0FKhrK12Yim90IcHzHBWRXvnjLjJrz8,4653
|
36
36
|
haiway/utils/env.py,sha256=mCMveOWwOphgp8Ir5NEpZQFENyG7MBOoLlUeHzzIYEQ,11262
|
37
|
-
haiway/utils/formatting.py,sha256=
|
37
|
+
haiway/utils/formatting.py,sha256=cqZHlRc9E4ybNq0Uo826JPJ7WuOWOOY995mCe_2URQw,6499
|
38
38
|
haiway/utils/logs.py,sha256=-MVyxVGU892yJKFh0bkshW_NEg1aiJt9wv2cUY2w98o,1847
|
39
|
+
haiway/utils/metadata.py,sha256=I_e9D5rhHLm7fpHyZFjChm1FnNxPPkeTulD8uhDn_a4,11754
|
39
40
|
haiway/utils/mimic.py,sha256=xaZiUKp096QFfdSw7cNIKEWt2UIS7vf880KF54gny38,1831
|
40
41
|
haiway/utils/noop.py,sha256=U8ocfoCgt-pY0owJDPtrRrj53cabeIXH9qCKWMQnoRk,1336
|
41
42
|
haiway/utils/queue.py,sha256=6v2u3pA6A44IuCCTOjmCt3yLyOcm7PCRnrIGo25j-1o,6402
|
42
43
|
haiway/utils/stream.py,sha256=lXaeveTY0-AYG5xVzcQYaiC6SUD5fUtHoMXiQcrQAAM,5723
|
43
|
-
haiway-0.
|
44
|
-
haiway-0.
|
45
|
-
haiway-0.
|
46
|
-
haiway-0.
|
44
|
+
haiway-0.27.0.dist-info/METADATA,sha256=0dw_DzT7Ui0OY25vJ8Xem5Wo52jDepyfYIg45BnuroI,4919
|
45
|
+
haiway-0.27.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
46
|
+
haiway-0.27.0.dist-info/licenses/LICENSE,sha256=3phcpHVNBP8jsi77gOO0E7rgKeDeu99Pi7DSnK9YHoQ,1069
|
47
|
+
haiway-0.27.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|