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.
- kronos/__init__.py +0 -0
- kronos/core/__init__.py +145 -0
- kronos/core/broadcaster.py +116 -0
- kronos/core/element.py +225 -0
- kronos/core/event.py +316 -0
- kronos/core/eventbus.py +116 -0
- kronos/core/flow.py +356 -0
- kronos/core/graph.py +442 -0
- kronos/core/node.py +982 -0
- kronos/core/pile.py +575 -0
- kronos/core/processor.py +494 -0
- kronos/core/progression.py +296 -0
- kronos/enforcement/__init__.py +57 -0
- kronos/enforcement/common/__init__.py +34 -0
- kronos/enforcement/common/boolean.py +85 -0
- kronos/enforcement/common/choice.py +97 -0
- kronos/enforcement/common/mapping.py +118 -0
- kronos/enforcement/common/model.py +102 -0
- kronos/enforcement/common/number.py +98 -0
- kronos/enforcement/common/string.py +140 -0
- kronos/enforcement/context.py +129 -0
- kronos/enforcement/policy.py +80 -0
- kronos/enforcement/registry.py +153 -0
- kronos/enforcement/rule.py +312 -0
- kronos/enforcement/service.py +370 -0
- kronos/enforcement/validator.py +198 -0
- kronos/errors.py +146 -0
- kronos/operations/__init__.py +32 -0
- kronos/operations/builder.py +228 -0
- kronos/operations/flow.py +398 -0
- kronos/operations/node.py +101 -0
- kronos/operations/registry.py +92 -0
- kronos/protocols.py +414 -0
- kronos/py.typed +0 -0
- kronos/services/__init__.py +81 -0
- kronos/services/backend.py +286 -0
- kronos/services/endpoint.py +608 -0
- kronos/services/hook.py +471 -0
- kronos/services/imodel.py +465 -0
- kronos/services/registry.py +115 -0
- kronos/services/utilities/__init__.py +36 -0
- kronos/services/utilities/header_factory.py +87 -0
- kronos/services/utilities/rate_limited_executor.py +271 -0
- kronos/services/utilities/rate_limiter.py +180 -0
- kronos/services/utilities/resilience.py +414 -0
- kronos/session/__init__.py +41 -0
- kronos/session/exchange.py +258 -0
- kronos/session/message.py +60 -0
- kronos/session/session.py +411 -0
- kronos/specs/__init__.py +25 -0
- kronos/specs/adapters/__init__.py +0 -0
- kronos/specs/adapters/_utils.py +45 -0
- kronos/specs/adapters/dataclass_field.py +246 -0
- kronos/specs/adapters/factory.py +56 -0
- kronos/specs/adapters/pydantic_adapter.py +309 -0
- kronos/specs/adapters/sql_ddl.py +946 -0
- kronos/specs/catalog/__init__.py +36 -0
- kronos/specs/catalog/_audit.py +39 -0
- kronos/specs/catalog/_common.py +43 -0
- kronos/specs/catalog/_content.py +59 -0
- kronos/specs/catalog/_enforcement.py +70 -0
- kronos/specs/factory.py +120 -0
- kronos/specs/operable.py +314 -0
- kronos/specs/phrase.py +405 -0
- kronos/specs/protocol.py +140 -0
- kronos/specs/spec.py +506 -0
- kronos/types/__init__.py +60 -0
- kronos/types/_sentinel.py +311 -0
- kronos/types/base.py +369 -0
- kronos/types/db_types.py +260 -0
- kronos/types/identity.py +66 -0
- kronos/utils/__init__.py +40 -0
- kronos/utils/_hash.py +234 -0
- kronos/utils/_json_dump.py +392 -0
- kronos/utils/_lazy_init.py +63 -0
- kronos/utils/_to_list.py +165 -0
- kronos/utils/_to_num.py +85 -0
- kronos/utils/_utils.py +375 -0
- kronos/utils/concurrency/__init__.py +205 -0
- kronos/utils/concurrency/_async_call.py +333 -0
- kronos/utils/concurrency/_cancel.py +122 -0
- kronos/utils/concurrency/_errors.py +96 -0
- kronos/utils/concurrency/_patterns.py +363 -0
- kronos/utils/concurrency/_primitives.py +328 -0
- kronos/utils/concurrency/_priority_queue.py +135 -0
- kronos/utils/concurrency/_resource_tracker.py +110 -0
- kronos/utils/concurrency/_run_async.py +67 -0
- kronos/utils/concurrency/_task.py +95 -0
- kronos/utils/concurrency/_utils.py +79 -0
- kronos/utils/fuzzy/__init__.py +14 -0
- kronos/utils/fuzzy/_extract_json.py +90 -0
- kronos/utils/fuzzy/_fuzzy_json.py +288 -0
- kronos/utils/fuzzy/_fuzzy_match.py +149 -0
- kronos/utils/fuzzy/_string_similarity.py +187 -0
- kronos/utils/fuzzy/_to_dict.py +396 -0
- kronos/utils/sql/__init__.py +13 -0
- kronos/utils/sql/_sql_validation.py +142 -0
- krons-0.1.0.dist-info/METADATA +70 -0
- krons-0.1.0.dist-info/RECORD +101 -0
- krons-0.1.0.dist-info/WHEEL +4 -0
- 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)})"
|