krons 0.1.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.
Files changed (101) hide show
  1. kronos/__init__.py +0 -0
  2. kronos/core/__init__.py +145 -0
  3. kronos/core/broadcaster.py +116 -0
  4. kronos/core/element.py +225 -0
  5. kronos/core/event.py +316 -0
  6. kronos/core/eventbus.py +116 -0
  7. kronos/core/flow.py +356 -0
  8. kronos/core/graph.py +442 -0
  9. kronos/core/node.py +982 -0
  10. kronos/core/pile.py +575 -0
  11. kronos/core/processor.py +494 -0
  12. kronos/core/progression.py +296 -0
  13. kronos/enforcement/__init__.py +57 -0
  14. kronos/enforcement/common/__init__.py +34 -0
  15. kronos/enforcement/common/boolean.py +85 -0
  16. kronos/enforcement/common/choice.py +97 -0
  17. kronos/enforcement/common/mapping.py +118 -0
  18. kronos/enforcement/common/model.py +102 -0
  19. kronos/enforcement/common/number.py +98 -0
  20. kronos/enforcement/common/string.py +140 -0
  21. kronos/enforcement/context.py +129 -0
  22. kronos/enforcement/policy.py +80 -0
  23. kronos/enforcement/registry.py +153 -0
  24. kronos/enforcement/rule.py +312 -0
  25. kronos/enforcement/service.py +370 -0
  26. kronos/enforcement/validator.py +198 -0
  27. kronos/errors.py +146 -0
  28. kronos/operations/__init__.py +32 -0
  29. kronos/operations/builder.py +228 -0
  30. kronos/operations/flow.py +398 -0
  31. kronos/operations/node.py +101 -0
  32. kronos/operations/registry.py +92 -0
  33. kronos/protocols.py +414 -0
  34. kronos/py.typed +0 -0
  35. kronos/services/__init__.py +81 -0
  36. kronos/services/backend.py +286 -0
  37. kronos/services/endpoint.py +608 -0
  38. kronos/services/hook.py +471 -0
  39. kronos/services/imodel.py +465 -0
  40. kronos/services/registry.py +115 -0
  41. kronos/services/utilities/__init__.py +36 -0
  42. kronos/services/utilities/header_factory.py +87 -0
  43. kronos/services/utilities/rate_limited_executor.py +271 -0
  44. kronos/services/utilities/rate_limiter.py +180 -0
  45. kronos/services/utilities/resilience.py +414 -0
  46. kronos/session/__init__.py +41 -0
  47. kronos/session/exchange.py +258 -0
  48. kronos/session/message.py +60 -0
  49. kronos/session/session.py +411 -0
  50. kronos/specs/__init__.py +25 -0
  51. kronos/specs/adapters/__init__.py +0 -0
  52. kronos/specs/adapters/_utils.py +45 -0
  53. kronos/specs/adapters/dataclass_field.py +246 -0
  54. kronos/specs/adapters/factory.py +56 -0
  55. kronos/specs/adapters/pydantic_adapter.py +309 -0
  56. kronos/specs/adapters/sql_ddl.py +946 -0
  57. kronos/specs/catalog/__init__.py +36 -0
  58. kronos/specs/catalog/_audit.py +39 -0
  59. kronos/specs/catalog/_common.py +43 -0
  60. kronos/specs/catalog/_content.py +59 -0
  61. kronos/specs/catalog/_enforcement.py +70 -0
  62. kronos/specs/factory.py +120 -0
  63. kronos/specs/operable.py +314 -0
  64. kronos/specs/phrase.py +405 -0
  65. kronos/specs/protocol.py +140 -0
  66. kronos/specs/spec.py +506 -0
  67. kronos/types/__init__.py +60 -0
  68. kronos/types/_sentinel.py +311 -0
  69. kronos/types/base.py +369 -0
  70. kronos/types/db_types.py +260 -0
  71. kronos/types/identity.py +66 -0
  72. kronos/utils/__init__.py +40 -0
  73. kronos/utils/_hash.py +234 -0
  74. kronos/utils/_json_dump.py +392 -0
  75. kronos/utils/_lazy_init.py +63 -0
  76. kronos/utils/_to_list.py +165 -0
  77. kronos/utils/_to_num.py +85 -0
  78. kronos/utils/_utils.py +375 -0
  79. kronos/utils/concurrency/__init__.py +205 -0
  80. kronos/utils/concurrency/_async_call.py +333 -0
  81. kronos/utils/concurrency/_cancel.py +122 -0
  82. kronos/utils/concurrency/_errors.py +96 -0
  83. kronos/utils/concurrency/_patterns.py +363 -0
  84. kronos/utils/concurrency/_primitives.py +328 -0
  85. kronos/utils/concurrency/_priority_queue.py +135 -0
  86. kronos/utils/concurrency/_resource_tracker.py +110 -0
  87. kronos/utils/concurrency/_run_async.py +67 -0
  88. kronos/utils/concurrency/_task.py +95 -0
  89. kronos/utils/concurrency/_utils.py +79 -0
  90. kronos/utils/fuzzy/__init__.py +14 -0
  91. kronos/utils/fuzzy/_extract_json.py +90 -0
  92. kronos/utils/fuzzy/_fuzzy_json.py +288 -0
  93. kronos/utils/fuzzy/_fuzzy_match.py +149 -0
  94. kronos/utils/fuzzy/_string_similarity.py +187 -0
  95. kronos/utils/fuzzy/_to_dict.py +396 -0
  96. kronos/utils/sql/__init__.py +13 -0
  97. kronos/utils/sql/_sql_validation.py +142 -0
  98. krons-0.1.0.dist-info/METADATA +70 -0
  99. krons-0.1.0.dist-info/RECORD +101 -0
  100. krons-0.1.0.dist-info/WHEEL +4 -0
  101. krons-0.1.0.dist-info/licenses/LICENSE +201 -0
kronos/core/pile.py ADDED
@@ -0,0 +1,575 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import contextlib
7
+ import threading
8
+ from collections.abc import Callable, Iterator
9
+ from typing import Any, Generic, Literal, TypeVar, overload
10
+ from uuid import UUID
11
+
12
+ from pydantic import Field, PrivateAttr, field_serializer, field_validator
13
+ from typing_extensions import override
14
+
15
+ from kronos.errors import ExistsError, NotFoundError
16
+ from kronos.protocols import Containable, Deserializable, Serializable, implements
17
+ from kronos.types import Unset, UnsetType, is_unset
18
+ from kronos.utils import extract_types, load_type_from_string, synchronized
19
+ from kronos.utils.concurrency import Lock as AsyncLock
20
+
21
+ from .element import Element
22
+ from .progression import Progression
23
+
24
+ __all__ = ("Pile",)
25
+
26
+ T = TypeVar("T", bound=Element)
27
+
28
+
29
+ @implements(
30
+ Containable,
31
+ Serializable,
32
+ Deserializable,
33
+ )
34
+ class Pile(Element, Generic[T]):
35
+ """Thread-safe typed collection with rich query interface.
36
+
37
+ A Pile is an ordered, type-validated container for Element subclasses.
38
+ Maintains insertion order via Progression, supports concurrent access.
39
+
40
+ Type-dispatched __getitem__:
41
+ pile[uuid|str] -> T (single item by ID)
42
+ pile[int] -> T (single item by index)
43
+ pile[slice] -> Pile[T] (range)
44
+ pile[list|tuple] -> Pile[T] (indices or UUIDs)
45
+ pile[Progression] -> Pile[T] (ordered subset)
46
+ pile[callable] -> Pile[T] (filter function)
47
+
48
+ Example:
49
+ pile = Pile[Node](items=[n1, n2], item_type=Node)
50
+ pile.add(n3)
51
+ filtered = pile[lambda x: x.metadata.get("active")]
52
+ """
53
+
54
+ _items: dict[UUID, T] = PrivateAttr(default_factory=dict)
55
+ _progression: Progression = PrivateAttr(default_factory=Progression)
56
+ _lock: threading.RLock = PrivateAttr(default_factory=threading.RLock)
57
+ _async_lock: AsyncLock = PrivateAttr(default_factory=AsyncLock)
58
+
59
+ @property
60
+ def progression(self) -> Progression:
61
+ """Read-only copy of progression order."""
62
+ return Progression(order=list(self._progression.order), name=self._progression.name)
63
+
64
+ item_type: set[type] | None = Field(
65
+ default=None,
66
+ frozen=True,
67
+ description="Allowed types for validation (None = any Element subclass)",
68
+ )
69
+ strict_type: bool = Field(
70
+ default=False,
71
+ frozen=True,
72
+ description="Enforce exact type match (disallow subclasses)",
73
+ )
74
+
75
+ @field_validator("item_type", mode="before")
76
+ @classmethod
77
+ def _normalize_item_type(cls, v: Any) -> set[type] | None:
78
+ """Normalize item_type to set[type] from various input formats."""
79
+ if v is None:
80
+ return None
81
+ if isinstance(v, list) and v and isinstance(v[0], str):
82
+ return {load_type_from_string(type_str) for type_str in v}
83
+ return extract_types(v)
84
+
85
+ @override
86
+ def __init__(
87
+ self,
88
+ items: list[T] | None = None,
89
+ item_type: type[T] | set[type] | list[type] | None = None,
90
+ order: list[UUID] | Progression | None = None,
91
+ strict_type: bool = False,
92
+ **kwargs,
93
+ ):
94
+ """Initialize Pile with optional items.
95
+
96
+ Args:
97
+ items: Initial items to add
98
+ item_type: Type(s) for validation (type, set, list, or Union)
99
+ order: Custom order (list of UUIDs or Progression)
100
+ strict_type: Enforce exact type match (no subclasses)
101
+ **kwargs: Element fields (id, created_at, metadata)
102
+
103
+ Raises:
104
+ NotFoundError: If order contains UUID not in items
105
+ TypeError: If item type validation fails
106
+ """
107
+ super().__init__(**{"item_type": item_type, "strict_type": strict_type, **kwargs})
108
+
109
+ if items:
110
+ for item in items:
111
+ self.add(item)
112
+
113
+ if order:
114
+ order_list = list(order.order) if isinstance(order, Progression) else order
115
+ for uid in order_list:
116
+ if uid not in self._items:
117
+ raise NotFoundError(f"UUID {uid} in order not found in items")
118
+ self._progression = Progression(order=order_list)
119
+
120
+ @field_serializer("item_type")
121
+ def _serialize_item_type(self, v: set[type] | None) -> list[str] | None:
122
+ """Serialize item_type to list of fully-qualified type names."""
123
+ if v is None:
124
+ return None
125
+ return [f"{t.__module__}.{t.__name__}" for t in v]
126
+
127
+ @override
128
+ def to_dict(
129
+ self,
130
+ mode: Literal["python", "json", "db"] = "python",
131
+ created_at_format: (Literal["datetime", "isoformat", "timestamp"] | UnsetType) = Unset,
132
+ meta_key: str | UnsetType = Unset,
133
+ item_meta_key: str | UnsetType = Unset,
134
+ item_created_at_format: (Literal["datetime", "isoformat", "timestamp"] | UnsetType) = Unset,
135
+ **kwargs: Any,
136
+ ) -> dict[str, Any]:
137
+ """Serialize pile with items in progression order.
138
+
139
+ Args:
140
+ mode: Serialization mode (python/json/db)
141
+ created_at_format: Timestamp format for Pile itself
142
+ meta_key: Rename Pile metadata field
143
+ item_meta_key: Metadata key name for items
144
+ item_created_at_format: Timestamp format for items
145
+ **kwargs: Passed to model_dump()
146
+
147
+ Returns:
148
+ Dict with pile fields and serialized items in order
149
+ """
150
+ data = super().to_dict(
151
+ mode=mode, created_at_format=created_at_format, meta_key=meta_key, **kwargs
152
+ )
153
+
154
+ actual_meta_key = (
155
+ meta_key
156
+ if not is_unset(meta_key)
157
+ else ("node_metadata" if mode == "db" else "metadata")
158
+ )
159
+
160
+ if self._progression.name and actual_meta_key in data:
161
+ data[actual_meta_key]["progression_name"] = self._progression.name
162
+
163
+ item_mode = "python" if mode == "python" else "json"
164
+ data["items"] = [
165
+ i.to_dict(
166
+ mode=item_mode,
167
+ meta_key=item_meta_key,
168
+ created_at_format=item_created_at_format,
169
+ )
170
+ for i in self
171
+ ]
172
+
173
+ return data
174
+
175
+ # ==================== Core Operations ====================
176
+
177
+ @synchronized
178
+ def add(self, item: T) -> None:
179
+ """Add item to pile. Raises ExistsError if duplicate, TypeError if invalid type."""
180
+ self._validate_type(item)
181
+
182
+ if item.id in self._items:
183
+ raise ExistsError(f"Item {item.id} already exists in pile")
184
+
185
+ self._items[item.id] = item
186
+ self._progression.append(item.id)
187
+
188
+ @synchronized
189
+ def remove(self, item_id: UUID | str | Element) -> T:
190
+ """Remove and return item. Raises NotFoundError if not found."""
191
+ uid = self._coerce_id(item_id)
192
+
193
+ try:
194
+ item = self._items.pop(uid)
195
+ except KeyError:
196
+ raise NotFoundError(f"Item {uid} not found in pile") from None
197
+
198
+ self._progression.remove(uid)
199
+ return item
200
+
201
+ @synchronized
202
+ def pop(self, item_id: UUID | str | Element, default: Any = ...) -> T | Any:
203
+ """Remove and return item, or default if not found. Raises NotFoundError if no default."""
204
+ uid = self._coerce_id(item_id)
205
+
206
+ try:
207
+ item = self._items.pop(uid)
208
+ self._progression.remove(uid)
209
+ return item
210
+ except KeyError:
211
+ if default is ...:
212
+ raise NotFoundError(f"Item {uid} not found in pile") from None
213
+ return default
214
+
215
+ @synchronized
216
+ def get(self, item_id: UUID | str | Element, default: Any = ...) -> T | Any:
217
+ """Get item by ID, or default if not found. Raises NotFoundError if no default."""
218
+ uid = self._coerce_id(item_id)
219
+
220
+ try:
221
+ return self._items[uid]
222
+ except KeyError:
223
+ if default is ...:
224
+ raise NotFoundError(f"Item {uid} not found in pile") from None
225
+ return default
226
+
227
+ @synchronized
228
+ def update(self, item: T) -> None:
229
+ """Update existing item in-place. Raises NotFoundError if not found, TypeError if invalid."""
230
+ self._validate_type(item)
231
+
232
+ if item.id not in self._items:
233
+ raise NotFoundError(f"Item {item.id} not found in pile")
234
+
235
+ self._items[item.id] = item
236
+
237
+ @synchronized
238
+ def clear(self) -> None:
239
+ """Remove all items from pile."""
240
+ self._items.clear()
241
+ self._progression.clear()
242
+
243
+ # ==================== Set-like Operations ====================
244
+
245
+ @synchronized
246
+ def include(self, item: T) -> bool:
247
+ """Idempotent add: returns True if item is in pile after call, False on validation failure."""
248
+ if item.id in self._items:
249
+ return True
250
+ try:
251
+ self._validate_type(item)
252
+ self._items[item.id] = item
253
+ self._progression.append(item.id)
254
+ return True
255
+ except Exception:
256
+ return False
257
+
258
+ @synchronized
259
+ def exclude(self, item: UUID | str | Element) -> bool:
260
+ """Idempotent remove: returns True if item is not in pile after call, False on coercion failure."""
261
+ try:
262
+ uid = self._coerce_id(item)
263
+ except Exception:
264
+ return False
265
+ if uid not in self._items:
266
+ return True
267
+ self._items.pop(uid, None)
268
+ try:
269
+ self._progression.remove(uid)
270
+ except ValueError:
271
+ pass
272
+ return True
273
+
274
+ # ==================== Rich __getitem__ (Type Dispatch) ====================
275
+
276
+ @overload
277
+ def __getitem__(self, key: UUID | str) -> T: ...
278
+ @overload
279
+ def __getitem__(self, key: Progression) -> Pile[T]: ...
280
+ @overload
281
+ def __getitem__(self, key: int) -> T: ...
282
+ @overload
283
+ def __getitem__(self, key: slice) -> Pile[T]: ...
284
+ @overload
285
+ def __getitem__(self, key: list[int] | tuple[int, ...]) -> Pile[T]: ...
286
+ @overload
287
+ def __getitem__(self, key: list[UUID] | tuple[UUID, ...]) -> Pile[T]: ...
288
+ @overload
289
+ def __getitem__(self, key: Callable[[T], bool]) -> Pile[T]: ...
290
+
291
+ def __getitem__(self, key: Any) -> T | Pile[T]:
292
+ """Type-dispatched query: UUID/str/int -> T; slice/list/tuple/Progression/callable -> Pile[T]."""
293
+ if isinstance(key, (UUID, str)):
294
+ return self.get(key)
295
+ elif isinstance(key, int):
296
+ return self._get_by_index(key)
297
+ elif isinstance(key, Progression):
298
+ return self._filter_by_progression(key)
299
+ elif isinstance(key, slice):
300
+ return self._get_by_slice(key)
301
+ elif isinstance(key, (list, tuple)):
302
+ return self._get_by_list(key)
303
+ elif callable(key):
304
+ return self._filter_by_function(key)
305
+ else:
306
+ raise TypeError(
307
+ f"Invalid key type: {type(key)}. Expected UUID, str, int, slice, list, tuple, Progression, or callable"
308
+ )
309
+
310
+ def _filter_by_progression(self, prog: Progression) -> Pile[T]:
311
+ """Return new Pile with items in progression order. Raises NotFoundError if UUID missing."""
312
+ if any(uid not in self._items for uid in prog):
313
+ raise NotFoundError("Some items from progression not found in pile")
314
+
315
+ return Pile(
316
+ items=[self._items[uid] for uid in prog],
317
+ item_type=self.item_type,
318
+ strict_type=self.strict_type,
319
+ )
320
+
321
+ @synchronized
322
+ def _get_by_index(self, index: int) -> T:
323
+ """Get item by position in progression order."""
324
+ uid: UUID = self._progression[index]
325
+ return self._items[uid]
326
+
327
+ @synchronized
328
+ def _get_by_slice(self, s: slice) -> Pile[T]:
329
+ """Return new Pile with items from slice range."""
330
+ uids: list[UUID] = self._progression[s]
331
+ return Pile(
332
+ items=[self._items[uid] for uid in uids],
333
+ item_type=self.item_type,
334
+ strict_type=self.strict_type,
335
+ )
336
+
337
+ @synchronized
338
+ def _get_by_list(self, keys: list | tuple) -> Pile[T]:
339
+ """Return new Pile from list of indices or UUIDs. No mixing allowed."""
340
+ if not keys:
341
+ raise ValueError("Cannot get items with empty list/tuple")
342
+
343
+ first = keys[0]
344
+ if isinstance(first, int):
345
+ if not all(isinstance(k, int) for k in keys):
346
+ raise TypeError("Cannot mix int and UUID in list/tuple indexing")
347
+ items = [self._get_by_index(idx) for idx in keys]
348
+ elif isinstance(first, (UUID, str)):
349
+ if not all(isinstance(k, (UUID, str)) for k in keys):
350
+ raise TypeError("Cannot mix int and UUID in list/tuple indexing")
351
+ items = [self.get(uid) for uid in keys]
352
+ else:
353
+ raise TypeError(f"list/tuple must contain only int or UUID, got {type(first)}")
354
+
355
+ return Pile(
356
+ items=items,
357
+ item_type=self.item_type,
358
+ strict_type=self.strict_type,
359
+ )
360
+
361
+ def _filter_by_function(self, func: Callable[[T], bool]) -> Pile[T]:
362
+ """Return new Pile with items matching filter function."""
363
+ return Pile(
364
+ items=[item for item in self if func(item)],
365
+ item_type=self.item_type,
366
+ strict_type=self.strict_type,
367
+ )
368
+
369
+ def filter_by_type(self, item_type: type[T] | set[type] | list[type]) -> Pile[T]:
370
+ """Return new Pile with items matching specified type(s).
371
+
372
+ Args:
373
+ item_type: Type(s) to filter by
374
+
375
+ Returns:
376
+ New Pile containing only items of requested type(s)
377
+
378
+ Raises:
379
+ TypeError: If requested type incompatible with pile's item_type
380
+ NotFoundError: If no items match
381
+ """
382
+ types_to_filter = extract_types(item_type)
383
+
384
+ if self.item_type is not None:
385
+ if self.strict_type:
386
+ invalid_types = types_to_filter - self.item_type
387
+ if invalid_types:
388
+ raise TypeError(
389
+ f"Types {invalid_types} not allowed in pile (allowed: {self.item_type})"
390
+ )
391
+ else:
392
+ for t in types_to_filter:
393
+ is_compatible = any(
394
+ issubclass(t, allowed) or issubclass(allowed, t)
395
+ for allowed in self.item_type
396
+ )
397
+ if not is_compatible:
398
+ raise TypeError(
399
+ f"Type {t} not compatible with allowed types {self.item_type}"
400
+ )
401
+
402
+ filtered_items = [
403
+ item for item in self if any(isinstance(item, t) for t in types_to_filter)
404
+ ]
405
+
406
+ if not filtered_items:
407
+ raise NotFoundError(f"No items of type(s) {types_to_filter} found in pile")
408
+
409
+ return Pile(
410
+ items=filtered_items,
411
+ item_type=self.item_type,
412
+ strict_type=self.strict_type,
413
+ )
414
+
415
+ # ==================== Context Managers ====================
416
+
417
+ async def __aenter__(self) -> Pile[T]:
418
+ """Acquire async lock for context manager."""
419
+ await self._async_lock.acquire()
420
+ return self
421
+
422
+ async def __aexit__(self, _exc_type, _exc_val, _exc_tb) -> None:
423
+ """Release async lock."""
424
+ self._async_lock.release()
425
+
426
+ # ==================== Query Operations ====================
427
+
428
+ @synchronized
429
+ def __contains__(self, item: UUID | str | Element) -> bool:
430
+ """Check if item exists in pile by ID."""
431
+ with contextlib.suppress(Exception):
432
+ uid = self._coerce_id(item)
433
+ return uid in self._items
434
+ return False
435
+
436
+ @synchronized
437
+ def __len__(self) -> int:
438
+ return len(self._items)
439
+
440
+ def __bool__(self) -> bool:
441
+ return len(self._items) > 0
442
+
443
+ @synchronized
444
+ def __iter__(self) -> Iterator[T]: # type: ignore[override]
445
+ """Iterate items in progression order. LSP override: yields T, not field tuples."""
446
+ for uid in self._progression:
447
+ yield self._items[uid]
448
+
449
+ def keys(self) -> Iterator[UUID]:
450
+ """Iterate UUIDs in progression order."""
451
+ return iter(self._progression)
452
+
453
+ def items(self) -> Iterator[tuple[UUID, T]]:
454
+ """Iterate (UUID, item) pairs in progression order."""
455
+ for i in self:
456
+ yield (i.id, i)
457
+
458
+ def __list__(self) -> list[T]:
459
+ """Return items as list in progression order."""
460
+ return [i for i in self]
461
+
462
+ def is_empty(self) -> bool:
463
+ """Check if pile contains no items."""
464
+ return len(self._items) == 0
465
+
466
+ # ==================== Validation ====================
467
+
468
+ def _validate_type(self, item: T) -> None:
469
+ """Validate item against pile's type constraints. Raises TypeError on failure."""
470
+ if not isinstance(item, Element):
471
+ raise TypeError(f"Item must be Element subclass, got {type(item)}")
472
+
473
+ if self.item_type is not None:
474
+ item_type_actual = type(item)
475
+
476
+ if self.strict_type:
477
+ if item_type_actual not in self.item_type:
478
+ raise TypeError(
479
+ f"Item type {item_type_actual} not in allowed types {self.item_type} "
480
+ "(strict_type=True, no subclasses allowed)"
481
+ )
482
+ else:
483
+ if not any(issubclass(item_type_actual, t) for t in self.item_type):
484
+ raise TypeError(
485
+ f"Item type {item_type_actual} is not a subclass of any allowed type {self.item_type}"
486
+ )
487
+
488
+ # ==================== Deserialization ====================
489
+
490
+ @classmethod
491
+ @override
492
+ def from_dict(
493
+ cls,
494
+ data: dict[str, Any],
495
+ meta_key: str | UnsetType = Unset,
496
+ item_meta_key: str | UnsetType = Unset,
497
+ **kwargs: Any,
498
+ ) -> Pile[T]:
499
+ """Deserialize Pile from dict. Validates all item types before deserializing.
500
+
501
+ Args:
502
+ data: Serialized pile data
503
+ meta_key: Restore metadata from this key (db mode compatibility)
504
+ item_meta_key: Metadata key for item deserialization
505
+ **kwargs: Additional arguments (e.g., item_type override)
506
+
507
+ Returns:
508
+ Reconstructed Pile with all items
509
+
510
+ Raises:
511
+ TypeError: If any item type incompatible with pile's constraints
512
+ """
513
+ from .element import Element
514
+
515
+ data = data.copy()
516
+
517
+ if not is_unset(meta_key) and meta_key in data:
518
+ data["metadata"] = data.pop(meta_key)
519
+ elif "node_metadata" in data and "metadata" not in data:
520
+ data["metadata"] = data.pop("node_metadata")
521
+ data.pop("node_metadata", None)
522
+
523
+ item_type_data = data.get("item_type") or kwargs.get("item_type")
524
+ strict_type = data.get("strict_type", False)
525
+
526
+ items_data = data.get("items", [])
527
+ if item_type_data is not None and items_data:
528
+ if (
529
+ isinstance(item_type_data, list)
530
+ and item_type_data
531
+ and isinstance(item_type_data[0], str)
532
+ ):
533
+ allowed_types = {load_type_from_string(type_str) for type_str in item_type_data}
534
+ else:
535
+ allowed_types = extract_types(item_type_data)
536
+
537
+ for item_dict in items_data:
538
+ kron_class = item_dict.get("metadata", {}).get("kron_class")
539
+ if kron_class:
540
+ try:
541
+ item_type_actual = load_type_from_string(kron_class)
542
+ except ValueError:
543
+ continue
544
+
545
+ if strict_type:
546
+ if item_type_actual not in allowed_types:
547
+ raise TypeError(
548
+ f"Item type {kron_class} not in allowed types {allowed_types} "
549
+ "(strict_type=True)"
550
+ )
551
+ else:
552
+ if not any(issubclass(item_type_actual, t) for t in allowed_types):
553
+ raise TypeError(
554
+ f"Item type {kron_class} is not a subclass of any allowed type {allowed_types}"
555
+ )
556
+
557
+ pile_data = data.copy()
558
+ pile_data.pop("items", None)
559
+ pile_data.pop("item_type", None)
560
+ pile_data.pop("strict_type", None)
561
+ pile = cls(item_type=item_type_data, strict_type=strict_type, **pile_data)
562
+
563
+ metadata = data.get("metadata", {})
564
+ progression_name = metadata.get("progression_name")
565
+ if progression_name:
566
+ pile._progression.name = progression_name
567
+
568
+ for item_dict in items_data:
569
+ item = Element.from_dict(item_dict, meta_key=item_meta_key)
570
+ pile.add(item) # type: ignore[arg-type]
571
+
572
+ return pile
573
+
574
+ def __repr__(self) -> str:
575
+ return f"Pile(len={len(self)})"