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/__init__.py ADDED
File without changes
@@ -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"]