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/__init__.py
ADDED
|
File without changes
|
kronos/core/__init__.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Core primitives with lazy loading for fast import."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
# Lazy import mapping
|
|
11
|
+
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
|
12
|
+
# broadcaster
|
|
13
|
+
"Broadcaster": ("kronos.core.broadcaster", "Broadcaster"),
|
|
14
|
+
# element
|
|
15
|
+
"Element": ("kronos.core.element", "Element"),
|
|
16
|
+
# event
|
|
17
|
+
"Event": ("kronos.core.event", "Event"),
|
|
18
|
+
"EventStatus": ("kronos.core.event", "EventStatus"),
|
|
19
|
+
"Execution": ("kronos.core.event", "Execution"),
|
|
20
|
+
# eventbus
|
|
21
|
+
"EventBus": ("kronos.core.eventbus", "EventBus"),
|
|
22
|
+
"Handler": ("kronos.core.eventbus", "Handler"),
|
|
23
|
+
# flow
|
|
24
|
+
"Flow": ("kronos.core.flow", "Flow"),
|
|
25
|
+
# graph
|
|
26
|
+
"Edge": ("kronos.core.graph", "Edge"),
|
|
27
|
+
"EdgeCondition": ("kronos.core.graph", "EdgeCondition"),
|
|
28
|
+
"Graph": ("kronos.core.graph", "Graph"),
|
|
29
|
+
# node
|
|
30
|
+
"DEFAULT_NODE_CONFIG": ("kronos.core.node", "DEFAULT_NODE_CONFIG"),
|
|
31
|
+
"NODE_REGISTRY": ("kronos.core.node", "NODE_REGISTRY"),
|
|
32
|
+
"PERSISTABLE_NODE_REGISTRY": ("kronos.core.node", "PERSISTABLE_NODE_REGISTRY"),
|
|
33
|
+
"Node": ("kronos.core.node", "Node"),
|
|
34
|
+
"NodeConfig": ("kronos.core.node", "NodeConfig"),
|
|
35
|
+
"create_node": ("kronos.core.node", "create_node"),
|
|
36
|
+
"generate_ddl": ("kronos.core.node", "generate_ddl"),
|
|
37
|
+
# phrase
|
|
38
|
+
"PHRASE_REGISTRY": ("kronos.core.phrase", "PHRASE_REGISTRY"),
|
|
39
|
+
"Phrase": ("kronos.core.phrase", "Phrase"),
|
|
40
|
+
"PhraseConfig": ("kronos.core.phrase", "PhraseConfig"),
|
|
41
|
+
"PhraseError": ("kronos.core.phrase", "PhraseError"),
|
|
42
|
+
"RequirementNotMet": ("kronos.core.phrase", "RequirementNotMet"),
|
|
43
|
+
"create_phrase": ("kronos.core.phrase", "create_phrase"),
|
|
44
|
+
"get_phrase": ("kronos.core.phrase", "get_phrase"),
|
|
45
|
+
"list_phrases": ("kronos.core.phrase", "list_phrases"),
|
|
46
|
+
# pile
|
|
47
|
+
"Pile": ("kronos.core.pile", "Pile"),
|
|
48
|
+
# processor
|
|
49
|
+
"Executor": ("kronos.core.processor", "Executor"),
|
|
50
|
+
"Processor": ("kronos.core.processor", "Processor"),
|
|
51
|
+
# progression
|
|
52
|
+
"Progression": ("kronos.core.progression", "Progression"),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_LOADED: dict[str, object] = {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def __getattr__(name: str) -> object:
|
|
59
|
+
"""Lazy import attributes on first access."""
|
|
60
|
+
if name in _LOADED:
|
|
61
|
+
return _LOADED[name]
|
|
62
|
+
|
|
63
|
+
if name in _LAZY_IMPORTS:
|
|
64
|
+
from importlib import import_module
|
|
65
|
+
|
|
66
|
+
module_name, attr_name = _LAZY_IMPORTS[name]
|
|
67
|
+
module = import_module(module_name)
|
|
68
|
+
value = getattr(module, attr_name)
|
|
69
|
+
_LOADED[name] = value
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
raise AttributeError(f"module 'kronos.core' has no attribute {name!r}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def __dir__() -> list[str]:
|
|
76
|
+
"""Return all available attributes for autocomplete."""
|
|
77
|
+
return list(__all__)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# TYPE_CHECKING block for static analysis
|
|
81
|
+
if TYPE_CHECKING:
|
|
82
|
+
from .broadcaster import Broadcaster
|
|
83
|
+
from .element import Element
|
|
84
|
+
from .event import Event, EventStatus, Execution
|
|
85
|
+
from .eventbus import EventBus, Handler
|
|
86
|
+
from .flow import Flow
|
|
87
|
+
from .graph import Edge, EdgeCondition, Graph
|
|
88
|
+
from .node import (
|
|
89
|
+
DEFAULT_NODE_CONFIG,
|
|
90
|
+
NODE_REGISTRY,
|
|
91
|
+
PERSISTABLE_NODE_REGISTRY,
|
|
92
|
+
Node,
|
|
93
|
+
NodeConfig,
|
|
94
|
+
create_node,
|
|
95
|
+
generate_ddl,
|
|
96
|
+
)
|
|
97
|
+
from .phrase import (
|
|
98
|
+
PHRASE_REGISTRY,
|
|
99
|
+
Phrase,
|
|
100
|
+
PhraseConfig,
|
|
101
|
+
PhraseError,
|
|
102
|
+
RequirementNotMet,
|
|
103
|
+
create_phrase,
|
|
104
|
+
get_phrase,
|
|
105
|
+
list_phrases,
|
|
106
|
+
)
|
|
107
|
+
from .pile import Pile
|
|
108
|
+
from .processor import Executor, Processor
|
|
109
|
+
from .progression import Progression
|
|
110
|
+
|
|
111
|
+
__all__ = [
|
|
112
|
+
# constants
|
|
113
|
+
"DEFAULT_NODE_CONFIG",
|
|
114
|
+
"NODE_REGISTRY",
|
|
115
|
+
"PERSISTABLE_NODE_REGISTRY",
|
|
116
|
+
"PHRASE_REGISTRY",
|
|
117
|
+
# classes
|
|
118
|
+
"Broadcaster",
|
|
119
|
+
"Edge",
|
|
120
|
+
"EdgeCondition",
|
|
121
|
+
"Element",
|
|
122
|
+
"Event",
|
|
123
|
+
"EventBus",
|
|
124
|
+
"EventStatus",
|
|
125
|
+
"Execution",
|
|
126
|
+
"Executor",
|
|
127
|
+
"Flow",
|
|
128
|
+
"Graph",
|
|
129
|
+
"Handler",
|
|
130
|
+
"Node",
|
|
131
|
+
"NodeConfig",
|
|
132
|
+
"Phrase",
|
|
133
|
+
"PhraseConfig",
|
|
134
|
+
"PhraseError",
|
|
135
|
+
"Pile",
|
|
136
|
+
"Processor",
|
|
137
|
+
"Progression",
|
|
138
|
+
"RequirementNotMet",
|
|
139
|
+
# functions
|
|
140
|
+
"create_node",
|
|
141
|
+
"create_phrase",
|
|
142
|
+
"generate_ddl",
|
|
143
|
+
"get_phrase",
|
|
144
|
+
"list_phrases",
|
|
145
|
+
]
|
|
@@ -0,0 +1,116 @@
|
|
|
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 logging
|
|
7
|
+
import weakref
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
9
|
+
from typing import Any, ClassVar
|
|
10
|
+
|
|
11
|
+
from kronos.utils import is_coro_func
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
__all__ = ["Broadcaster"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Broadcaster:
|
|
19
|
+
"""Singleton pub/sub with weakref-based automatic subscriber cleanup.
|
|
20
|
+
|
|
21
|
+
Subclass and set `_event_type` to define typed broadcasters. Subscribers
|
|
22
|
+
stored as weakrefs (WeakMethod for bound methods) for automatic cleanup.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
class UserBroadcaster(Broadcaster):
|
|
26
|
+
_event_type = UserEvent
|
|
27
|
+
|
|
28
|
+
UserBroadcaster.subscribe(my_handler)
|
|
29
|
+
await UserBroadcaster.broadcast(UserEvent(...))
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
_instance: ClassVar[Broadcaster | None] = None
|
|
33
|
+
_subscribers: ClassVar[
|
|
34
|
+
list[weakref.ref[Callable[[Any], None] | Callable[[Any], Awaitable[None]]]]
|
|
35
|
+
] = []
|
|
36
|
+
_event_type: ClassVar[type] #: Override in subclass to restrict event types.
|
|
37
|
+
|
|
38
|
+
def __new__(cls):
|
|
39
|
+
if cls._instance is None:
|
|
40
|
+
cls._instance = super().__new__(cls)
|
|
41
|
+
return cls._instance
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def subscribe(cls, callback: Callable[[Any], None] | Callable[[Any], Awaitable[None]]) -> None:
|
|
45
|
+
"""Add subscriber callback (idempotent, stored as weakref).
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
callback: Sync or async callable receiving the event.
|
|
49
|
+
Bound methods use WeakMethod; functions use weakref.
|
|
50
|
+
"""
|
|
51
|
+
for weak_ref in cls._subscribers:
|
|
52
|
+
if weak_ref() is callback:
|
|
53
|
+
return
|
|
54
|
+
weak_callback: weakref.ref[Callable[[Any], None] | Callable[[Any], Awaitable[None]]]
|
|
55
|
+
if hasattr(callback, "__self__"):
|
|
56
|
+
weak_callback = weakref.WeakMethod(callback) # type: ignore[assignment]
|
|
57
|
+
else:
|
|
58
|
+
weak_callback = weakref.ref(callback)
|
|
59
|
+
cls._subscribers.append(weak_callback)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def unsubscribe(
|
|
63
|
+
cls, callback: Callable[[Any], None] | Callable[[Any], Awaitable[None]]
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Remove subscriber callback.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
callback: Previously subscribed callback to remove.
|
|
69
|
+
"""
|
|
70
|
+
for weak_ref in list(cls._subscribers):
|
|
71
|
+
if weak_ref() is callback:
|
|
72
|
+
cls._subscribers.remove(weak_ref)
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def _cleanup_dead_refs(
|
|
77
|
+
cls,
|
|
78
|
+
) -> list[Callable[[Any], None] | Callable[[Any], Awaitable[None]]]:
|
|
79
|
+
"""Prune dead weakrefs, return live callbacks (in-place update)."""
|
|
80
|
+
callbacks, alive_refs = [], []
|
|
81
|
+
for weak_ref in cls._subscribers:
|
|
82
|
+
if (cb := weak_ref()) is not None:
|
|
83
|
+
callbacks.append(cb)
|
|
84
|
+
alive_refs.append(weak_ref)
|
|
85
|
+
cls._subscribers[:] = alive_refs
|
|
86
|
+
return callbacks
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
async def broadcast(cls, event: Any) -> None:
|
|
90
|
+
"""Broadcast event to all subscribers sequentially.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
event: Event instance (must match _event_type).
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
ValueError: If event type doesn't match _event_type.
|
|
97
|
+
|
|
98
|
+
Note:
|
|
99
|
+
Callback exceptions are logged and suppressed.
|
|
100
|
+
"""
|
|
101
|
+
if not isinstance(event, cls._event_type):
|
|
102
|
+
raise ValueError(f"Event must be of type {cls._event_type.__name__}")
|
|
103
|
+
for callback in cls._cleanup_dead_refs():
|
|
104
|
+
try:
|
|
105
|
+
if is_coro_func(callback):
|
|
106
|
+
if (result := callback(event)) is not None:
|
|
107
|
+
await result
|
|
108
|
+
else:
|
|
109
|
+
callback(event)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.error(f"Error in subscriber callback: {e}", exc_info=True)
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def get_subscriber_count(cls) -> int:
|
|
115
|
+
"""Count live subscribers (triggers dead ref cleanup)."""
|
|
116
|
+
return len(cls._cleanup_dead_refs())
|
kronos/core/element.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
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 datetime as dt
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
from uuid import UUID, uuid4
|
|
10
|
+
|
|
11
|
+
import orjson
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
13
|
+
|
|
14
|
+
from kronos.protocols import (
|
|
15
|
+
Deserializable,
|
|
16
|
+
Hashable,
|
|
17
|
+
Observable,
|
|
18
|
+
Serializable,
|
|
19
|
+
implements,
|
|
20
|
+
)
|
|
21
|
+
from kronos.types import MaybeSentinel, Unset, UnsetType, is_sentinel, is_unset
|
|
22
|
+
from kronos.utils import (
|
|
23
|
+
coerce_created_at,
|
|
24
|
+
json_dump,
|
|
25
|
+
load_type_from_string,
|
|
26
|
+
now_utc,
|
|
27
|
+
to_uuid,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = ("LN_ELEMENT_FIELDS", "Element")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@implements(Observable, Serializable, Deserializable, Hashable)
|
|
34
|
+
class Element(BaseModel):
|
|
35
|
+
"""Base element with UUID identity, timestamps, polymorphic serialization.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
id: UUID identifier (frozen, auto-generated)
|
|
39
|
+
created_at: UTC datetime (frozen, auto-generated)
|
|
40
|
+
metadata: Arbitrary metadata dict
|
|
41
|
+
|
|
42
|
+
Serialization injects kron_class for polymorphic deserialization.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
model_config = ConfigDict(
|
|
46
|
+
populate_by_name=True,
|
|
47
|
+
arbitrary_types_allowed=True,
|
|
48
|
+
validate_assignment=True,
|
|
49
|
+
use_enum_values=True,
|
|
50
|
+
extra="forbid",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
id: UUID = Field(default_factory=uuid4, frozen=True)
|
|
54
|
+
created_at: dt.datetime = Field(default_factory=now_utc, frozen=True)
|
|
55
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
56
|
+
|
|
57
|
+
@field_validator("id", mode="before")
|
|
58
|
+
@classmethod
|
|
59
|
+
def _coerce_id(cls, v) -> UUID:
|
|
60
|
+
"""Coerce to UUID4."""
|
|
61
|
+
return to_uuid(v)
|
|
62
|
+
|
|
63
|
+
@field_validator("created_at", mode="before")
|
|
64
|
+
@classmethod
|
|
65
|
+
def _coerce_created_at(cls, v) -> dt.datetime:
|
|
66
|
+
"""Coerce to UTC datetime."""
|
|
67
|
+
return coerce_created_at(v)
|
|
68
|
+
|
|
69
|
+
@field_validator("metadata", mode="before")
|
|
70
|
+
@classmethod
|
|
71
|
+
def _validate_meta_integrity(cls, val: dict[str, Any] | MaybeSentinel) -> dict[str, Any]:
|
|
72
|
+
"""Validate and coerce metadata to dict. Raises ValueError if conversion fails."""
|
|
73
|
+
if is_sentinel(val, {"none"}):
|
|
74
|
+
return {}
|
|
75
|
+
|
|
76
|
+
with contextlib.suppress(orjson.JSONDecodeError, TypeError):
|
|
77
|
+
val = json_dump(val, decode=True, as_loaded=True)
|
|
78
|
+
|
|
79
|
+
if not isinstance(val, dict):
|
|
80
|
+
raise ValueError("Invalid metadata: must be a dictionary")
|
|
81
|
+
|
|
82
|
+
return val
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def class_name(cls, full: bool = False) -> str:
|
|
86
|
+
"""Get class name, stripping generic parameters (Flow[E,P] -> Flow).
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
full: If True, returns module.Class; otherwise Class only.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Class name string.
|
|
93
|
+
"""
|
|
94
|
+
name = cls.__qualname__ if full else cls.__name__
|
|
95
|
+
if "[" in name:
|
|
96
|
+
name = name.split("[")[0]
|
|
97
|
+
if full:
|
|
98
|
+
return f"{cls.__module__}.{name}"
|
|
99
|
+
return name
|
|
100
|
+
|
|
101
|
+
def _to_dict(self, **kwargs: Any) -> dict[str, Any]:
|
|
102
|
+
"""Serialize to dict with kron_class injected in metadata."""
|
|
103
|
+
data = self.model_dump(**kwargs)
|
|
104
|
+
data["metadata"]["kron_class"] = self.__class__.class_name(full=True)
|
|
105
|
+
|
|
106
|
+
return data
|
|
107
|
+
|
|
108
|
+
def to_dict(
|
|
109
|
+
self,
|
|
110
|
+
mode: Literal["python", "json", "db"] = "python",
|
|
111
|
+
created_at_format: (Literal["datetime", "isoformat", "timestamp"] | UnsetType) = Unset,
|
|
112
|
+
meta_key: str | UnsetType = Unset,
|
|
113
|
+
**kwargs: Any,
|
|
114
|
+
) -> dict[str, Any]:
|
|
115
|
+
"""Serialize to dict with kron_class metadata injected.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
mode: python/json/db (db auto-renames metadata to node_metadata).
|
|
119
|
+
created_at_format: datetime/isoformat/timestamp (auto-selected by mode).
|
|
120
|
+
meta_key: Rename metadata field (overrides db default).
|
|
121
|
+
**kwargs: Passed to model_dump().
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Serialized dict with kron_class in metadata for polymorphic restore.
|
|
125
|
+
"""
|
|
126
|
+
if is_unset(created_at_format):
|
|
127
|
+
created_at_format = "isoformat" if mode == "json" else "datetime"
|
|
128
|
+
|
|
129
|
+
if is_unset(meta_key) and mode == "db":
|
|
130
|
+
meta_key = "node_metadata"
|
|
131
|
+
|
|
132
|
+
data = self._to_dict(**kwargs)
|
|
133
|
+
if mode in ("json", "db"):
|
|
134
|
+
data = json_dump(data, decode=True, as_loaded=True)
|
|
135
|
+
|
|
136
|
+
if "created_at" in data:
|
|
137
|
+
if created_at_format == "isoformat":
|
|
138
|
+
if mode == "python":
|
|
139
|
+
data["created_at"] = self.created_at.isoformat()
|
|
140
|
+
elif created_at_format == "timestamp":
|
|
141
|
+
data["created_at"] = self.created_at.timestamp()
|
|
142
|
+
elif created_at_format == "datetime":
|
|
143
|
+
if mode == "json":
|
|
144
|
+
raise ValueError(
|
|
145
|
+
"created_at_format='datetime' not valid for mode='json'. "
|
|
146
|
+
"Use 'isoformat' or 'timestamp' for JSON serialization."
|
|
147
|
+
)
|
|
148
|
+
if mode == "db" and isinstance(data["created_at"], str):
|
|
149
|
+
data["created_at"] = self.created_at
|
|
150
|
+
|
|
151
|
+
if not is_unset(meta_key) and "metadata" in data:
|
|
152
|
+
data[meta_key] = data.pop("metadata")
|
|
153
|
+
|
|
154
|
+
return data
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def from_dict(
|
|
158
|
+
cls, data: dict[str, Any], meta_key: str | UnsetType = Unset, **kwargs: Any
|
|
159
|
+
) -> Element:
|
|
160
|
+
"""Deserialize from dict with polymorphic type restoration via kron_class.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
data: Serialized element dict.
|
|
164
|
+
meta_key: Restore metadata from this key (db mode compatibility).
|
|
165
|
+
**kwargs: Passed to model_validate().
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Element instance (actual type determined by kron_class if present).
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
ValueError: If kron_class invalid or not Element subclass.
|
|
172
|
+
"""
|
|
173
|
+
data = data.copy()
|
|
174
|
+
|
|
175
|
+
if not is_unset(meta_key) and meta_key in data:
|
|
176
|
+
data["metadata"] = data.pop(meta_key)
|
|
177
|
+
|
|
178
|
+
metadata = data.get("metadata", {})
|
|
179
|
+
kron_class = Unset
|
|
180
|
+
|
|
181
|
+
if isinstance(metadata, dict):
|
|
182
|
+
metadata = metadata.copy()
|
|
183
|
+
data["metadata"] = metadata
|
|
184
|
+
kron_class = metadata.pop("kron_class", Unset)
|
|
185
|
+
|
|
186
|
+
if not is_unset(kron_class) and kron_class != cls.class_name(full=True):
|
|
187
|
+
try:
|
|
188
|
+
target_cls = load_type_from_string(kron_class)
|
|
189
|
+
except ValueError as e:
|
|
190
|
+
raise ValueError(f"Failed to deserialize class '{kron_class}': {e}") from e
|
|
191
|
+
|
|
192
|
+
if not issubclass(target_cls, Element):
|
|
193
|
+
raise ValueError(
|
|
194
|
+
f"'{kron_class}' is not an Element subclass. "
|
|
195
|
+
f"Cannot deserialize into {cls.__name__}"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
target_func = getattr(target_cls.from_dict, "__func__", target_cls.from_dict)
|
|
199
|
+
cls_func = getattr(cls.from_dict, "__func__", cls.from_dict)
|
|
200
|
+
if target_func is cls_func:
|
|
201
|
+
return target_cls.model_validate(data, **kwargs)
|
|
202
|
+
|
|
203
|
+
return target_cls.from_dict(data, **kwargs)
|
|
204
|
+
|
|
205
|
+
return cls.model_validate(data, **kwargs)
|
|
206
|
+
|
|
207
|
+
def __eq__(self, other: Any) -> bool:
|
|
208
|
+
"""Equality by ID."""
|
|
209
|
+
if not isinstance(other, Element):
|
|
210
|
+
return NotImplemented
|
|
211
|
+
return self.id == other.id
|
|
212
|
+
|
|
213
|
+
def __hash__(self) -> int:
|
|
214
|
+
"""Hash by ID."""
|
|
215
|
+
return hash(self.id)
|
|
216
|
+
|
|
217
|
+
def __bool__(self) -> bool:
|
|
218
|
+
"""Always truthy."""
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
def __repr__(self) -> str:
|
|
222
|
+
return f"{self.__class__.__name__}(id={self.id})"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
LN_ELEMENT_FIELDS = ["id", "created_at", "metadata"]
|