haiway 0.26.0__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.
@@ -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.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=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=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=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
@@ -12,14 +12,14 @@ haiway/context/types.py,sha256=LoW8238TTdbUgmxyHDi0LVc8M8ZwTHLWKkAPttTsTeg,746
12
12
  haiway/helpers/__init__.py,sha256=gyKM1mWyuQwSx_2ajpI0UF1nA8l5D7wrzZOt9XUkWJA,780
13
13
  haiway/helpers/asynchrony.py,sha256=Ddj8UdXhVczAbAC-rLpyhWa4RJ_W2Eolo45Veorq7_4,5362
14
14
  haiway/helpers/caching.py,sha256=BqgcUGQSAmXsuLi5V8EwlZzuGyutHOn1V4k7BHsGKeg,14347
15
- haiway/helpers/concurrent.py,sha256=fU6je62XvbySylZKDgzC_AGKPC7Kimmd5SmkVpySBUo,13115
16
- haiway/helpers/files.py,sha256=L6vXd8gdgWx5jPL8azloU8IGoFq2xnxjMc4ufz-gdl4,11650
17
- haiway/helpers/observability.py,sha256=R4md41g7iTslzvtRaY5W9pgXqmuzJuGByjFb6vsO4W4,10994
15
+ haiway/helpers/concurrent.py,sha256=kTcm_wLAKqVQOKgTHIKwbkMX5hJ5GIXOAc5RRIUvbo4,13063
16
+ haiway/helpers/files.py,sha256=MzGR-GF8FpBzSihDeen7wdxmX2R-JjAT1MB9aMW-8-g,11626
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.0.dist-info/METADATA,sha256=IWQSIN2nqYPQETpjzyJYJ6dv2L--xrP-RG9J9qoQXoc,4919
44
- haiway-0.26.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
45
- haiway-0.26.0.dist-info/licenses/LICENSE,sha256=3phcpHVNBP8jsi77gOO0E7rgKeDeu99Pi7DSnK9YHoQ,1069
46
- haiway-0.26.0.dist-info/RECORD,,
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,,