haiway 0.26.1__py3-none-any.whl → 0.27.1__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 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",
@@ -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
@@ -978,6 +979,7 @@ class ctx:
978
979
  metric: str,
979
980
  value: float | int,
980
981
  unit: str | None = None,
982
+ kind: ObservabilityMetricKind,
981
983
  attributes: Mapping[str, ObservabilityAttribute] | None = None,
982
984
  ) -> None: ...
983
985
 
@@ -990,6 +992,7 @@ class ctx:
990
992
  metric: str | None = None,
991
993
  value: float | int | None = None,
992
994
  unit: str | None = None,
995
+ kind: ObservabilityMetricKind | None = None,
993
996
  attributes: Mapping[str, ObservabilityAttribute] | None = None,
994
997
  ) -> None:
995
998
  if event is not None:
@@ -1003,11 +1006,13 @@ class ctx:
1003
1006
  elif metric is not None:
1004
1007
  assert event is None # nosec: B101
1005
1008
  assert value is not None # nosec: B101
1009
+ assert kind is not None # nosec: B101
1006
1010
  ObservabilityContext.record_metric(
1007
1011
  level,
1008
1012
  metric,
1009
1013
  value=value,
1010
1014
  unit=unit,
1015
+ kind=kind,
1011
1016
  attributes=attributes or {},
1012
1017
  )
1013
1018
 
@@ -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
 
@@ -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 Observability, ObservabilityLevel, ScopeIdentifier
8
- from haiway.context.observability import ObservabilityAttribute
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
- if name not in self._counters:
274
- self._counters[name] = self.meter.create_counter(
275
- name=name,
276
- unit=unit or "",
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
- self._counters[name].add(
280
- value,
281
- attributes={
282
- key: cast(Any, value)
283
- for key, value in attributes.items()
284
- if value is not None and value is not MISSING
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",
@@ -14,20 +14,20 @@ __all__ = (
14
14
 
15
15
  @overload
16
16
  def as_list[T](
17
- collection: Iterable[T],
17
+ iterable: Iterable[T],
18
18
  /,
19
19
  ) -> list[T]: ...
20
20
 
21
21
 
22
22
  @overload
23
23
  def as_list[T](
24
- collection: Iterable[T] | None,
24
+ iterable: Iterable[T] | None,
25
25
  /,
26
26
  ) -> list[T] | None: ...
27
27
 
28
28
 
29
29
  def as_list[T](
30
- collection: Iterable[T] | None,
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
- collection : Iterable[T] | None
39
- The input collection to be converted to a list.
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 collection,
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 collection is None:
50
+ if iterable is None:
51
51
  return None
52
52
 
53
- elif isinstance(collection, list):
54
- return collection
53
+ elif isinstance(iterable, list):
54
+ return iterable
55
55
 
56
56
  else:
57
- return list(collection)
57
+ return list(iterable)
58
58
 
59
59
 
60
60
  @overload
61
61
  def as_tuple[T](
62
- collection: Iterable[T],
62
+ iterable: Iterable[T],
63
63
  /,
64
64
  ) -> tuple[T, ...]: ...
65
65
 
66
66
 
67
67
  @overload
68
68
  def as_tuple[T](
69
- collection: Iterable[T] | None,
69
+ iterable: Iterable[T] | None,
70
70
  /,
71
71
  ) -> tuple[T, ...] | None: ...
72
72
 
73
73
 
74
74
  def as_tuple[T](
75
- collection: Iterable[T] | None,
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
- collection : Iterable[T] | None
84
- The input collection to be converted to a tuple.
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 collection,
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 collection is None:
95
+ if iterable is None:
96
96
  return None
97
97
 
98
- elif isinstance(collection, tuple):
99
- return collection
98
+ elif isinstance(iterable, tuple):
99
+ return iterable
100
100
 
101
101
  else:
102
- return tuple(collection)
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
- collection: Mapping[K, V],
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
- collection: Mapping[K, V] | None,
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
- collection: Mapping[K, V] | None,
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
- collection : Mapping[K, V] | None
174
- The input collection to be converted to a dict.
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 collection,
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 collection is None:
185
+ if mapping is None:
186
186
  return None
187
187
 
188
- elif isinstance(collection, dict):
189
- return collection
188
+ elif isinstance(mapping, dict):
189
+ return mapping
190
190
 
191
191
  else:
192
- return dict(collection)
192
+ return dict(mapping)
193
193
 
194
194
 
195
195
  @overload
196
- def without_missing[T: Mapping[str, Any]](
196
+ def without_missing(
197
197
  mapping: Mapping[str, Any],
198
198
  /,
199
199
  ) -> Mapping[str, Any]: ...
@@ -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
- # check for string
41
- if isinstance(value, str):
44
+ if value is None:
45
+ return "None"
46
+
47
+ elif isinstance(value, str):
42
48
  if "\n" in value:
43
- return f'"""\n{value.replace("\n", "\n ")}\n"""'
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
- # check for bytes
49
- elif isinstance(value, bytes):
50
- return f"b'{value!r}'"
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(value)
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(value)
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(value)
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
- formatted_value: str = value.replace("\n", "\n| ")
74
- return f"┝ {key}:\n{formatted_value}"
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: Any,
117
+ value: str,
118
+ indent: int,
84
119
  ) -> str:
120
+ indent_str = " " * indent
85
121
  if "\n" in value:
86
- formatted_value: str = value.replace("\n", "\n ")
87
- return f"[{key}]:\n{formatted_value}"
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 str(other)
137
+ return f"{indent_str}{other}"
99
138
 
100
139
  variables: ItemsView[str, Any] = vars(other).items()
101
-
102
- parts: list[str] = [f"┍━ {type(other).__name__}:"]
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(value)
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
- if parts:
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(value)
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=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
- return "{\n" + "\n".join(parts) + "\n}"
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(element)
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
- return "[\n" + "\n".join(parts) + "\n]"
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}[]"
@@ -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.26.1
3
+ Version: 0.27.1
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=tJpU6TzK-o-Pt8joGrJah5eC08STVoRzvhXeLn3oTKo,2095
1
+ haiway/__init__.py,sha256=qzLQvEsgGKFNHp9A34sTg_MrBSvPUq4UtmQS172TfaI,2247
2
2
  haiway/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- haiway/context/__init__.py,sha256=DKcf1AHGEqLWF8Kki30YKQ07GjonUjpAA8I51P4AxAg,1296
4
- haiway/context/access.py,sha256=g8WdrKNvg6sO3X7Km_9X9syafxS0Shm_EP-JKqRLyxI,31367
3
+ haiway/context/__init__.py,sha256=fjWNPXgBsW_nBsT8aR0SuiRehxDylRHHXA-nU2Rvy_Y,1356
4
+ haiway/context/access.py,sha256=93IvbUoBCMy-oBAVHoRuZDzU_N7zm_LoDj5br6-cIG0,31566
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=rZoZT7g4ZM5_OKIFV0uA0rJodHxondw8X2bMSf_W6_s,20244
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=R4md41g7iTslzvtRaY5W9pgXqmuzJuGByjFb6vsO4W4,10994
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=uFgSuvwOgW7IbffROY6Kc4ZRJGoQV6rEWqIQltU_Iho,27365
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=FkY6EUwkZmb2Z8Z5UpMW3i9J0l9JoowgrULy-s_6X5M,943
33
+ haiway/utils/__init__.py,sha256=ZmRloJ3IUqotOjN1_ejS3NYb4A30LKR78YTaY5b3nao,1133
34
34
  haiway/utils/always.py,sha256=dd6jDQ1j4DpJjTKO1J2Tv5xS8X1LnMC4kQ0D7DtKUvw,1230
35
- haiway/utils/collections.py,sha256=W2K5haxogHdngEw2JF_qEUr0O28dhirdy2kzSbeW4wE,4745
35
+ haiway/utils/collections.py,sha256=LExyiwpJX7nc0FKhrK12Yim90IcHzHBWRXvnjLjJrz8,4653
36
36
  haiway/utils/env.py,sha256=mCMveOWwOphgp8Ir5NEpZQFENyG7MBOoLlUeHzzIYEQ,11262
37
- haiway/utils/formatting.py,sha256=SQ-gjBa2nxg_UhIP0AhNXIRwcDRei2ZZUiCLMiYLYUo,4041
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.26.1.dist-info/METADATA,sha256=IEiaciUkopUmCqO7MxJZdfjUrELAI_tDRGBvFL8jUCg,4919
44
- haiway-0.26.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
45
- haiway-0.26.1.dist-info/licenses/LICENSE,sha256=3phcpHVNBP8jsi77gOO0E7rgKeDeu99Pi7DSnK9YHoQ,1069
46
- haiway-0.26.1.dist-info/RECORD,,
44
+ haiway-0.27.1.dist-info/METADATA,sha256=W4rwS9P4_1f50cENKaEeXRtP1SIJFlYL408ndVGZIzo,4919
45
+ haiway-0.27.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
46
+ haiway-0.27.1.dist-info/licenses/LICENSE,sha256=3phcpHVNBP8jsi77gOO0E7rgKeDeu99Pi7DSnK9YHoQ,1069
47
+ haiway-0.27.1.dist-info/RECORD,,