lionherd-core 1.0.0a3__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 (64) hide show
  1. lionherd_core/__init__.py +84 -0
  2. lionherd_core/base/__init__.py +30 -0
  3. lionherd_core/base/_utils.py +295 -0
  4. lionherd_core/base/broadcaster.py +128 -0
  5. lionherd_core/base/element.py +300 -0
  6. lionherd_core/base/event.py +322 -0
  7. lionherd_core/base/eventbus.py +112 -0
  8. lionherd_core/base/flow.py +236 -0
  9. lionherd_core/base/graph.py +616 -0
  10. lionherd_core/base/node.py +212 -0
  11. lionherd_core/base/pile.py +811 -0
  12. lionherd_core/base/progression.py +261 -0
  13. lionherd_core/errors.py +104 -0
  14. lionherd_core/libs/__init__.py +2 -0
  15. lionherd_core/libs/concurrency/__init__.py +60 -0
  16. lionherd_core/libs/concurrency/_cancel.py +85 -0
  17. lionherd_core/libs/concurrency/_errors.py +80 -0
  18. lionherd_core/libs/concurrency/_patterns.py +238 -0
  19. lionherd_core/libs/concurrency/_primitives.py +253 -0
  20. lionherd_core/libs/concurrency/_priority_queue.py +135 -0
  21. lionherd_core/libs/concurrency/_resource_tracker.py +66 -0
  22. lionherd_core/libs/concurrency/_task.py +58 -0
  23. lionherd_core/libs/concurrency/_utils.py +61 -0
  24. lionherd_core/libs/schema_handlers/__init__.py +35 -0
  25. lionherd_core/libs/schema_handlers/_function_call_parser.py +122 -0
  26. lionherd_core/libs/schema_handlers/_minimal_yaml.py +88 -0
  27. lionherd_core/libs/schema_handlers/_schema_to_model.py +251 -0
  28. lionherd_core/libs/schema_handlers/_typescript.py +153 -0
  29. lionherd_core/libs/string_handlers/__init__.py +15 -0
  30. lionherd_core/libs/string_handlers/_extract_json.py +65 -0
  31. lionherd_core/libs/string_handlers/_fuzzy_json.py +103 -0
  32. lionherd_core/libs/string_handlers/_string_similarity.py +347 -0
  33. lionherd_core/libs/string_handlers/_to_num.py +63 -0
  34. lionherd_core/ln/__init__.py +45 -0
  35. lionherd_core/ln/_async_call.py +314 -0
  36. lionherd_core/ln/_fuzzy_match.py +166 -0
  37. lionherd_core/ln/_fuzzy_validate.py +151 -0
  38. lionherd_core/ln/_hash.py +141 -0
  39. lionherd_core/ln/_json_dump.py +347 -0
  40. lionherd_core/ln/_list_call.py +110 -0
  41. lionherd_core/ln/_to_dict.py +373 -0
  42. lionherd_core/ln/_to_list.py +190 -0
  43. lionherd_core/ln/_utils.py +156 -0
  44. lionherd_core/lndl/__init__.py +62 -0
  45. lionherd_core/lndl/errors.py +30 -0
  46. lionherd_core/lndl/fuzzy.py +321 -0
  47. lionherd_core/lndl/parser.py +427 -0
  48. lionherd_core/lndl/prompt.py +137 -0
  49. lionherd_core/lndl/resolver.py +323 -0
  50. lionherd_core/lndl/types.py +287 -0
  51. lionherd_core/protocols.py +181 -0
  52. lionherd_core/py.typed +0 -0
  53. lionherd_core/types/__init__.py +46 -0
  54. lionherd_core/types/_sentinel.py +131 -0
  55. lionherd_core/types/base.py +341 -0
  56. lionherd_core/types/operable.py +133 -0
  57. lionherd_core/types/spec.py +313 -0
  58. lionherd_core/types/spec_adapters/__init__.py +10 -0
  59. lionherd_core/types/spec_adapters/_protocol.py +125 -0
  60. lionherd_core/types/spec_adapters/pydantic_field.py +177 -0
  61. lionherd_core-1.0.0a3.dist-info/METADATA +502 -0
  62. lionherd_core-1.0.0a3.dist-info/RECORD +64 -0
  63. lionherd_core-1.0.0a3.dist-info/WHEEL +4 -0
  64. lionherd_core-1.0.0a3.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,181 @@
1
+ # Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from typing import Any, Protocol, runtime_checkable
5
+ from uuid import UUID
6
+
7
+ __all__ = (
8
+ "Adaptable",
9
+ "Allowable",
10
+ "AsyncAdaptable",
11
+ "Containable",
12
+ "Deserializable",
13
+ "Hashable",
14
+ "Invocable",
15
+ "Observable",
16
+ "Serializable",
17
+ "implements",
18
+ )
19
+
20
+
21
+ @runtime_checkable
22
+ class ObservableProto(Protocol):
23
+ """Objects with unique UUID identifier. Check via isinstance()."""
24
+
25
+ @property
26
+ def id(self) -> UUID:
27
+ """Unique identifier."""
28
+ ...
29
+
30
+
31
+ @runtime_checkable
32
+ class Serializable(Protocol):
33
+ """Objects that can be serialized to dict via to_dict()."""
34
+
35
+ def to_dict(self, **kwargs: Any) -> dict[str, Any]:
36
+ """Serialize to dict. Args: serialization options (mode, format, etc.)."""
37
+ ...
38
+
39
+
40
+ @runtime_checkable
41
+ class Deserializable(Protocol):
42
+ """Objects that can be deserialized from dict via from_dict() classmethod."""
43
+
44
+ @classmethod
45
+ def from_dict(cls, data: dict[str, Any], **kwargs: Any) -> Any:
46
+ """Deserialize from dict. Args: data dict, deserialization options."""
47
+ ...
48
+
49
+
50
+ @runtime_checkable
51
+ class Adaptable(Protocol):
52
+ """Sync adapter protocol for format conversion (TOML/YAML/JSON/SQL). Use AsyncAdaptable for async."""
53
+
54
+ def adapt_to(self, obj_key: str, many: bool = False, **kwargs: Any) -> Any:
55
+ """Convert to external format. Args: adapter key, many flag, adapter kwargs."""
56
+ ...
57
+
58
+ @classmethod
59
+ def adapt_from(cls, obj: Any, obj_key: str, many: bool = False, **kwargs: Any) -> Any:
60
+ """Create from external format. Args: source object, adapter key, many flag."""
61
+ ...
62
+
63
+ @classmethod
64
+ def register_adapter(cls, adapter: Any) -> None:
65
+ """Register adapter for this class."""
66
+ ...
67
+
68
+
69
+ @runtime_checkable
70
+ class AsyncAdaptable(Protocol):
71
+ """Async adapter protocol for I/O-bound format conversion (DBs, network, files). Use Adaptable for sync."""
72
+
73
+ async def adapt_to_async(self, obj_key: str, many: bool = False, **kwargs: Any) -> Any:
74
+ """Async convert to external format. Args: adapter key, many flag, adapter kwargs."""
75
+ ...
76
+
77
+ @classmethod
78
+ async def adapt_from_async(
79
+ cls, obj: Any, obj_key: str, many: bool = False, **kwargs: Any
80
+ ) -> Any:
81
+ """Async create from external format. Args: source object, adapter key, many flag."""
82
+ ...
83
+
84
+ @classmethod
85
+ def register_async_adapter(cls, adapter: Any) -> None:
86
+ """Register async adapter for this class."""
87
+ ...
88
+
89
+
90
+ @runtime_checkable
91
+ class Containable(Protocol):
92
+ """Objects that support membership testing via 'in' operator (__contains__)."""
93
+
94
+ def __contains__(self, item: Any) -> bool:
95
+ """Check if item is in collection (by UUID or instance)."""
96
+ ...
97
+
98
+
99
+ @runtime_checkable
100
+ class Invocable(Protocol):
101
+ """Objects that can be invoked/executed via async invoke() method."""
102
+
103
+ async def invoke(self) -> Any:
104
+ """Invoke/execute the object. Returns: execution result (any value or None)."""
105
+ ...
106
+
107
+
108
+ @runtime_checkable
109
+ class Hashable(Protocol):
110
+ """Objects that can be hashed via __hash__() for use in sets/dicts."""
111
+
112
+ def __hash__(self) -> int:
113
+ """Return hash value for object (must be immutable or ID-based)."""
114
+ ...
115
+
116
+
117
+ @runtime_checkable
118
+ class Allowable(Protocol):
119
+ """Objects with defined set of allowed values/keys via allowed()."""
120
+
121
+ def allowed(self) -> set[str]:
122
+ """Return set of allowed keys/values."""
123
+ ...
124
+
125
+
126
+ Observable = ObservableProto
127
+
128
+
129
+ def implements(*protocols: type):
130
+ """Declare protocol implementations (Rust-like: MUST define in class body).
131
+
132
+ CRITICAL SEMANTICS (strictest interpretation):
133
+ @implements() means the class **LITERALLY** implements/overrides the method
134
+ or declares the attribute IN ITS OWN CLASS BODY. Inheritance does NOT count.
135
+
136
+ This is Rust-like trait implementation: you must provide the implementation
137
+ in the impl block, not rely on inheritance.
138
+
139
+ Rules:
140
+ - Method must be defined in class body (even if it calls super())
141
+ - Property must be declared in class body (cannot inherit from parent)
142
+ - Classmethod must be defined in class body
143
+ - NO inheritance: @implements means "I define this, not my parent"
144
+
145
+ Args:
146
+ *protocols: Protocol classes that the decorated class **literally** implements
147
+
148
+ Returns:
149
+ Class decorator that stores protocols on cls.__protocols__
150
+
151
+ Usage:
152
+ ✓ CORRECT: Literal implementation
153
+ @implements(Serializable, Deserializable)
154
+ class MyClass:
155
+ def to_dict(self, **kwargs): ... # Defined in this class
156
+ @classmethod
157
+ def from_dict(cls, data, **kwargs): ... # Defined in this class
158
+
159
+ ✗ WRONG: Relying on inheritance
160
+ @implements(Serializable) # VIOLATION!
161
+ class Child(Parent): # Parent has to_dict()
162
+ pass # No to_dict in Child body → not allowed!
163
+
164
+ ✓ CORRECT: Explicit override
165
+ @implements(Serializable)
166
+ class Child(Parent):
167
+ def to_dict(self, **kwargs): # Explicit in Child body
168
+ return super().to_dict(**kwargs) # Can call parent
169
+
170
+ Rationale:
171
+ - Explicit > Implicit (Rust philosophy)
172
+ - Clear ownership: each class declares what it implements
173
+ - No ambiguity about where implementation lives
174
+ - Prevents accidental protocol claims through inheritance
175
+ """
176
+
177
+ def decorator(cls):
178
+ cls.__protocols__ = protocols
179
+ return cls
180
+
181
+ return decorator
lionherd_core/py.typed ADDED
File without changes
@@ -0,0 +1,46 @@
1
+ # Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from ._sentinel import (
5
+ MaybeSentinel,
6
+ MaybeUndefined,
7
+ MaybeUnset,
8
+ SingletonType,
9
+ T,
10
+ Undefined,
11
+ UndefinedType,
12
+ Unset,
13
+ UnsetType,
14
+ is_sentinel,
15
+ not_sentinel,
16
+ )
17
+ from .base import DataClass, Enum, KeysDict, KeysLike, Meta, ModelConfig, Params
18
+ from .operable import Operable
19
+ from .spec import CommonMeta, Spec
20
+
21
+ __all__ = (
22
+ "CommonMeta",
23
+ "DataClass",
24
+ "Enum",
25
+ "KeysDict",
26
+ "KeysLike",
27
+ "MaybeSentinel",
28
+ "MaybeUndefined",
29
+ "MaybeUnset",
30
+ "Meta",
31
+ # Base classes
32
+ "ModelConfig",
33
+ "Operable",
34
+ "Params",
35
+ "SingletonType",
36
+ # Spec system
37
+ "Spec",
38
+ "T",
39
+ # Sentinel types
40
+ "Undefined",
41
+ "UndefinedType",
42
+ "Unset",
43
+ "UnsetType",
44
+ "is_sentinel",
45
+ "not_sentinel",
46
+ )
@@ -0,0 +1,131 @@
1
+ # Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, ClassVar, Final, Literal, Self, TypeAlias, TypeGuard, TypeVar
7
+
8
+ __all__ = (
9
+ "MaybeSentinel",
10
+ "MaybeUndefined",
11
+ "MaybeUnset",
12
+ "SingletonType",
13
+ "T",
14
+ "Undefined",
15
+ "UndefinedType",
16
+ "Unset",
17
+ "UnsetType",
18
+ "is_sentinel",
19
+ "not_sentinel",
20
+ )
21
+
22
+ T = TypeVar("T")
23
+
24
+
25
+ class _SingletonMeta(type):
26
+ """Metaclass: ensures one instance per subclass for safe 'is' checks."""
27
+
28
+ _cache: ClassVar[dict[type, SingletonType]] = {}
29
+
30
+ def __call__(cls, *a, **kw):
31
+ if cls not in cls._cache:
32
+ cls._cache[cls] = super().__call__(*a, **kw)
33
+ return cls._cache[cls]
34
+
35
+
36
+ class SingletonType(metaclass=_SingletonMeta):
37
+ """Base for singleton sentinels. Falsy, identity-preserving across copy/deepcopy."""
38
+
39
+ __slots__: tuple[str, ...] = ()
40
+
41
+ def __deepcopy__(self, memo: dict[int, Any]) -> Self:
42
+ """Preserve singleton identity across deepcopy."""
43
+ return self
44
+
45
+ def __copy__(self) -> Self:
46
+ """Preserve singleton identity across copy."""
47
+ return self
48
+
49
+ # concrete classes *must* override the two methods below
50
+ def __bool__(self) -> bool:
51
+ raise NotImplementedError
52
+
53
+ def __repr__(self) -> str:
54
+ raise NotImplementedError
55
+
56
+
57
+ class UndefinedType(SingletonType):
58
+ """Sentinel: field/key entirely missing from namespace. Use for missing keys, never-set fields."""
59
+
60
+ __slots__ = ()
61
+
62
+ def __bool__(self) -> Literal[False]:
63
+ return False
64
+
65
+ def __repr__(self) -> Literal["Undefined"]:
66
+ return "Undefined"
67
+
68
+ def __str__(self) -> Literal["Undefined"]:
69
+ return "Undefined"
70
+
71
+ def __reduce__(self) -> tuple[type[UndefinedType], tuple[()]]:
72
+ """Preserve singleton identity across pickle/unpickle."""
73
+ return (UndefinedType, ())
74
+
75
+
76
+ class UnsetType(SingletonType):
77
+ """Sentinel: key present but value not provided. Use to distinguish None from 'not provided'."""
78
+
79
+ __slots__ = ()
80
+
81
+ def __bool__(self) -> Literal[False]:
82
+ return False
83
+
84
+ def __repr__(self) -> Literal["Unset"]:
85
+ return "Unset"
86
+
87
+ def __str__(self) -> Literal["Unset"]:
88
+ return "Unset"
89
+
90
+ def __reduce__(self) -> tuple[type[UnsetType], tuple[()]]:
91
+ """Preserve singleton identity across pickle/unpickle."""
92
+ return (UnsetType, ())
93
+
94
+
95
+ Undefined: Final[UndefinedType] = UndefinedType()
96
+ """A key or field entirely missing from a namespace"""
97
+ Unset: Final[UnsetType] = UnsetType()
98
+ """A key present but value not yet provided."""
99
+
100
+ MaybeUndefined: TypeAlias = T | UndefinedType
101
+ MaybeUnset: TypeAlias = T | UnsetType
102
+ MaybeSentinel: TypeAlias = T | UndefinedType | UnsetType
103
+
104
+ _EMPTY_TUPLE: tuple[Any, ...] = (tuple(), set(), frozenset(), dict(), list(), "")
105
+
106
+
107
+ def is_sentinel(
108
+ value: Any,
109
+ *,
110
+ none_as_sentinel: bool = False,
111
+ empty_as_sentinel: bool = False,
112
+ ) -> bool:
113
+ """Check if a value is any sentinel (Undefined or Unset)."""
114
+ if none_as_sentinel and value is None:
115
+ return True
116
+ if empty_as_sentinel and value in _EMPTY_TUPLE:
117
+ return True
118
+ return value is Undefined or value is Unset
119
+
120
+
121
+ def not_sentinel(
122
+ value: T | UndefinedType | UnsetType,
123
+ none_as_sentinel: bool = False,
124
+ empty_as_sentinel: bool = False,
125
+ ) -> TypeGuard[T]:
126
+ """Type-narrowing check: NOT a sentinel. Narrows MaybeSentinel[T] to T for type checkers."""
127
+ return not is_sentinel(
128
+ value,
129
+ none_as_sentinel=none_as_sentinel,
130
+ empty_as_sentinel=empty_as_sentinel,
131
+ )
@@ -0,0 +1,341 @@
1
+ # Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import MutableMapping, MutableSequence, MutableSet, Sequence
7
+ from dataclasses import dataclass
8
+ from enum import (
9
+ Enum as _Enum,
10
+ StrEnum,
11
+ )
12
+ from typing import Any, ClassVar, Literal, Self, TypedDict
13
+
14
+ from typing_extensions import override
15
+
16
+ from ..protocols import Allowable, Hashable, Serializable, implements
17
+ from ._sentinel import Undefined, Unset, is_sentinel
18
+
19
+ __all__ = (
20
+ "DataClass",
21
+ "Enum",
22
+ "KeysDict",
23
+ "KeysLike",
24
+ "Meta",
25
+ "ModelConfig",
26
+ "Params",
27
+ )
28
+
29
+
30
+ @implements(Allowable)
31
+ class Enum(StrEnum):
32
+ """String-backed enum (Python 3.11+). Members are strings, support JSON serialization."""
33
+
34
+ @classmethod
35
+ def allowed(cls) -> tuple[str, ...]:
36
+ """Return tuple of all allowed string values."""
37
+ return tuple(e.value for e in cls)
38
+
39
+
40
+ class KeysDict(TypedDict, total=False):
41
+ """TypedDict for keys dictionary."""
42
+
43
+ key: Any # Represents any key-type pair
44
+
45
+
46
+ @dataclass(slots=True, frozen=True)
47
+ class ModelConfig:
48
+ """Config for Params/DataClass: sentinel handling, validation, serialization."""
49
+
50
+ # Sentinel handling (controls what gets excluded from to_dict)
51
+ none_as_sentinel: bool = False
52
+ empty_as_sentinel: bool = False
53
+
54
+ # Validation
55
+ strict: bool = False
56
+ prefill_unset: bool = True
57
+
58
+ # Serialization
59
+ use_enum_values: bool = False
60
+
61
+
62
+ @implements(Serializable, Allowable, Hashable)
63
+ @dataclass(slots=True, frozen=True, init=False)
64
+ class Params:
65
+ """Base for function parameters with sentinel handling. Configure via _config."""
66
+
67
+ _config: ClassVar[ModelConfig] = ModelConfig()
68
+ _allowed_keys: ClassVar[set[str]] = set()
69
+
70
+ def __init__(self, **kwargs: Any):
71
+ """Init from kwargs. Validates and sets attributes."""
72
+ # Set all attributes from kwargs, allowing for sentinel values
73
+ for k, v in kwargs.items():
74
+ if k in self.allowed():
75
+ object.__setattr__(self, k, v)
76
+ else:
77
+ raise ValueError(f"Invalid parameter: {k}")
78
+
79
+ # Validate after setting all attributes
80
+ self._validate()
81
+
82
+ @classmethod
83
+ def _is_sentinel(cls, value: Any) -> bool:
84
+ """Check if value is sentinel (respects config)."""
85
+ return is_sentinel(
86
+ value,
87
+ none_as_sentinel=cls._config.none_as_sentinel,
88
+ empty_as_sentinel=cls._config.empty_as_sentinel,
89
+ )
90
+
91
+ @classmethod
92
+ def _normalize_value(cls, value: Any) -> Any:
93
+ """Normalize value for serialization (enum handling, etc.)."""
94
+ if cls._config.use_enum_values and isinstance(value, _Enum):
95
+ return value.value
96
+ return value
97
+
98
+ @classmethod
99
+ def allowed(cls) -> set[str]:
100
+ """Return the keys of the parameters."""
101
+ if cls._allowed_keys:
102
+ return cls._allowed_keys
103
+ cls._allowed_keys = {i for i in cls.__dataclass_fields__ if not i.startswith("_")}
104
+ return cls._allowed_keys
105
+
106
+ def _validate(self) -> None:
107
+ """Validate params. Collects errors in ExceptionGroup. Prefills unset if configured."""
108
+ missing: list[Exception] = []
109
+ for k in self.allowed():
110
+ if self._config.strict and self._is_sentinel(getattr(self, k, Unset)):
111
+ missing.append(ValueError(f"Missing required parameter: {k}"))
112
+ if self._config.prefill_unset and getattr(self, k, Undefined) is Undefined:
113
+ object.__setattr__(self, k, Unset)
114
+ if missing:
115
+ raise ExceptionGroup("Missing required parameters", missing)
116
+
117
+ def default_kw(self) -> Any:
118
+ # create a partial function with the current parameters
119
+ dict_ = self.to_dict()
120
+
121
+ # handle kwargs if present, handle both 'kwargs' and 'kw'
122
+ kw_ = {}
123
+ kw_.update(dict_.pop("kwargs", {}))
124
+ kw_.update(dict_.pop("kw", {}))
125
+ dict_.update(kw_)
126
+ return dict_
127
+
128
+ def to_dict(self, exclude: set[str] | None = None) -> dict[str, Any]:
129
+ data = {}
130
+ exclude = exclude or set()
131
+ for k in self.allowed():
132
+ if k not in exclude:
133
+ v = getattr(self, k, Undefined)
134
+ if not self._is_sentinel(v):
135
+ data[k] = self._normalize_value(v)
136
+ return data
137
+
138
+ def __hash__(self) -> int:
139
+ from ..ln._hash import hash_dict
140
+
141
+ return hash_dict(self.to_dict())
142
+
143
+ def __eq__(self, other: object) -> bool:
144
+ """Equality via hash comparison. Returns NotImplemented for non-Params types."""
145
+ if not isinstance(other, Params):
146
+ return NotImplemented
147
+ return hash(self) == hash(other)
148
+
149
+ def with_updates(
150
+ self, copy_containers: Literal["shallow", "deep"] | None = None, **kwargs: Any
151
+ ) -> Self:
152
+ """Return new instance with updated fields.
153
+
154
+ Args:
155
+ copy_containers: None (no copy), "shallow" (top-level), or "deep" (recursive).
156
+ **kwargs: Field updates.
157
+ """
158
+ dict_ = self.to_dict()
159
+
160
+ def _out(d: dict):
161
+ d.update(kwargs)
162
+ return type(self)(**d)
163
+
164
+ if copy_containers is None:
165
+ return _out(dict_)
166
+
167
+ match copy_containers:
168
+ case "shallow":
169
+ for k, v in dict_.items():
170
+ if k not in kwargs and isinstance(
171
+ v, (MutableSequence, MutableMapping, MutableSet)
172
+ ):
173
+ dict_[k] = v.copy()
174
+ return _out(dict_)
175
+
176
+ case "deep":
177
+ import copy
178
+
179
+ for k, v in dict_.items():
180
+ if k not in kwargs and isinstance(
181
+ v, (MutableSequence, MutableMapping, MutableSet)
182
+ ):
183
+ dict_[k] = copy.deepcopy(v)
184
+ return _out(dict_)
185
+
186
+ raise ValueError(
187
+ f"Invalid copy_containers: {copy_containers!r}. Must be 'shallow', 'deep', or None."
188
+ )
189
+
190
+
191
+ @implements(Serializable, Allowable, Hashable)
192
+ @dataclass(slots=True)
193
+ class DataClass:
194
+ """Base for dataclasses with strict parameter handling. Configure via _config."""
195
+
196
+ _config: ClassVar[ModelConfig] = ModelConfig()
197
+ _allowed_keys: ClassVar[set[str]] = set()
198
+
199
+ def __post_init__(self):
200
+ """Post-init: validates all fields."""
201
+ self._validate()
202
+
203
+ @classmethod
204
+ def allowed(cls) -> set[str]:
205
+ """Return the keys of the parameters."""
206
+ if cls._allowed_keys:
207
+ return cls._allowed_keys
208
+ cls._allowed_keys = {i for i in cls.__dataclass_fields__ if not i.startswith("_")}
209
+ return cls._allowed_keys
210
+
211
+ def _validate(self) -> None:
212
+ """Validate params. Collects errors in ExceptionGroup. Prefills unset if configured."""
213
+ missing: list[Exception] = []
214
+ for k in self.allowed():
215
+ if self._config.strict and self._is_sentinel(getattr(self, k, Unset)):
216
+ missing.append(ValueError(f"Missing required parameter: {k}"))
217
+ if self._config.prefill_unset and getattr(self, k, Undefined) is Undefined:
218
+ self.__setattr__(k, Unset)
219
+ if missing:
220
+ raise ExceptionGroup("Missing required parameters", missing)
221
+
222
+ def to_dict(self, exclude: set[str] | None = None) -> dict[str, Any]:
223
+ data = {}
224
+ exclude = exclude or set()
225
+ for k in type(self).allowed():
226
+ if k not in exclude:
227
+ v = getattr(self, k)
228
+ if not self._is_sentinel(v):
229
+ data[k] = self._normalize_value(v)
230
+ return data
231
+
232
+ @classmethod
233
+ def _is_sentinel(cls, value: Any) -> bool:
234
+ """Check if value is sentinel (respects config)."""
235
+ return is_sentinel(
236
+ value,
237
+ none_as_sentinel=cls._config.none_as_sentinel,
238
+ empty_as_sentinel=cls._config.empty_as_sentinel,
239
+ )
240
+
241
+ @classmethod
242
+ def _normalize_value(cls, value: Any) -> Any:
243
+ """Normalize value for serialization (enum handling, etc.)."""
244
+ from enum import Enum as _Enum
245
+
246
+ if cls._config.use_enum_values and isinstance(value, _Enum):
247
+ return value.value
248
+ return value
249
+
250
+ def with_updates(
251
+ self, copy_containers: Literal["shallow", "deep"] | None = None, **kwargs: Any
252
+ ) -> Self:
253
+ """Return new instance with updated fields.
254
+
255
+ Args:
256
+ copy_containers: None (no copy), "shallow" (top-level), or "deep" (recursive).
257
+ **kwargs: Field updates.
258
+ """
259
+ dict_ = self.to_dict()
260
+
261
+ def _out(d: dict):
262
+ d.update(kwargs)
263
+ return type(self)(**d)
264
+
265
+ if copy_containers is None:
266
+ return _out(dict_)
267
+
268
+ match copy_containers:
269
+ case "shallow":
270
+ for k, v in dict_.items():
271
+ if k not in kwargs and isinstance(
272
+ v, (MutableSequence, MutableMapping, MutableSet)
273
+ ):
274
+ dict_[k] = v.copy()
275
+ return _out(dict_)
276
+
277
+ case "deep":
278
+ import copy
279
+
280
+ for k, v in dict_.items():
281
+ if k not in kwargs and isinstance(
282
+ v, (MutableSequence, MutableMapping, MutableSet)
283
+ ):
284
+ dict_[k] = copy.deepcopy(v)
285
+ return _out(dict_)
286
+
287
+ raise ValueError(
288
+ f"Invalid copy_containers: {copy_containers!r}. Must be 'shallow', 'deep', or None."
289
+ )
290
+
291
+ def __hash__(self) -> int:
292
+ from ..ln._hash import hash_dict
293
+
294
+ return hash_dict(self.to_dict())
295
+
296
+ def __eq__(self, other: object) -> bool:
297
+ """Equality via hash comparison. Returns NotImplemented for non-DataClass types."""
298
+ if not isinstance(other, DataClass):
299
+ return NotImplemented
300
+ return hash(self) == hash(other)
301
+
302
+
303
+ KeysLike = Sequence[str] | KeysDict
304
+
305
+
306
+ @implements(Hashable)
307
+ @dataclass(slots=True, frozen=True)
308
+ class Meta:
309
+ """Immutable metadata container. Hashable for caching (callables hashed by id)."""
310
+
311
+ key: str
312
+ value: Any
313
+
314
+ @override
315
+ def __hash__(self) -> int:
316
+ """Hash metadata. Callables use id() for identity semantics."""
317
+ # For callables, use their id
318
+ if callable(self.value):
319
+ return hash((self.key, id(self.value)))
320
+ # For other values, try to hash directly
321
+ try:
322
+ return hash((self.key, self.value))
323
+ except TypeError:
324
+ # Fallback for unhashable types
325
+ return hash((self.key, str(self.value)))
326
+
327
+ @override
328
+ def __eq__(self, other: object) -> bool:
329
+ """Equality: callables compared by id, others by standard equality."""
330
+ if not isinstance(other, Meta):
331
+ return NotImplemented
332
+
333
+ if self.key != other.key:
334
+ return False
335
+
336
+ # For callables, compare by identity
337
+ if callable(self.value) and callable(other.value):
338
+ return id(self.value) == id(other.value)
339
+
340
+ # For other values, use standard equality
341
+ return bool(self.value == other.value)