lionagi 0.17.9__py3-none-any.whl → 0.17.11__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.
- lionagi/__init__.py +1 -2
- lionagi/_class_registry.py +1 -2
- lionagi/_errors.py +1 -2
- lionagi/adapters/async_postgres_adapter.py +2 -10
- lionagi/config.py +1 -2
- lionagi/fields/action.py +1 -2
- lionagi/fields/base.py +3 -0
- lionagi/fields/code.py +3 -0
- lionagi/fields/file.py +3 -0
- lionagi/fields/instruct.py +1 -2
- lionagi/fields/reason.py +3 -2
- lionagi/fields/research.py +3 -0
- lionagi/libs/__init__.py +1 -2
- lionagi/libs/file/__init__.py +1 -2
- lionagi/libs/file/chunk.py +1 -2
- lionagi/libs/file/process.py +1 -2
- lionagi/libs/schema/__init__.py +1 -2
- lionagi/libs/schema/as_readable.py +1 -2
- lionagi/libs/schema/extract_code_block.py +1 -2
- lionagi/libs/schema/extract_docstring.py +1 -2
- lionagi/libs/schema/function_to_schema.py +1 -2
- lionagi/libs/schema/load_pydantic_model_from_schema.py +1 -2
- lionagi/libs/validate/__init__.py +1 -2
- lionagi/libs/validate/common_field_validators.py +1 -2
- lionagi/libs/validate/validate_boolean.py +1 -2
- lionagi/ln/fuzzy/_string_similarity.py +1 -2
- lionagi/ln/types.py +45 -0
- lionagi/models/__init__.py +1 -2
- lionagi/models/field_model.py +392 -286
- lionagi/models/hashable_model.py +98 -14
- lionagi/models/model_params.py +272 -271
- lionagi/models/operable_model.py +9 -10
- lionagi/models/schema_model.py +1 -2
- lionagi/operations/ReAct/ReAct.py +1 -2
- lionagi/operations/ReAct/__init__.py +1 -2
- lionagi/operations/ReAct/utils.py +1 -2
- lionagi/operations/__init__.py +1 -2
- lionagi/operations/_act/__init__.py +1 -2
- lionagi/operations/_act/act.py +1 -2
- lionagi/operations/brainstorm/__init__.py +1 -2
- lionagi/operations/brainstorm/brainstorm.py +1 -2
- lionagi/operations/brainstorm/prompt.py +1 -2
- lionagi/operations/builder.py +1 -2
- lionagi/operations/chat/__init__.py +1 -2
- lionagi/operations/chat/chat.py +1 -2
- lionagi/operations/communicate/communicate.py +1 -2
- lionagi/operations/flow.py +1 -2
- lionagi/operations/instruct/__init__.py +1 -2
- lionagi/operations/instruct/instruct.py +1 -2
- lionagi/operations/interpret/__init__.py +1 -2
- lionagi/operations/interpret/interpret.py +1 -2
- lionagi/operations/operate/__init__.py +1 -2
- lionagi/operations/operate/operate.py +1 -2
- lionagi/operations/parse/__init__.py +1 -2
- lionagi/operations/parse/parse.py +1 -2
- lionagi/operations/plan/__init__.py +1 -2
- lionagi/operations/plan/plan.py +1 -2
- lionagi/operations/plan/prompt.py +1 -2
- lionagi/operations/select/__init__.py +1 -2
- lionagi/operations/select/select.py +1 -2
- lionagi/operations/select/utils.py +1 -2
- lionagi/operations/types.py +1 -2
- lionagi/operations/utils.py +1 -2
- lionagi/protocols/__init__.py +1 -2
- lionagi/protocols/_concepts.py +1 -2
- lionagi/protocols/action/__init__.py +1 -2
- lionagi/protocols/action/function_calling.py +3 -20
- lionagi/protocols/action/manager.py +34 -4
- lionagi/protocols/action/tool.py +1 -2
- lionagi/protocols/contracts.py +1 -2
- lionagi/protocols/forms/__init__.py +1 -2
- lionagi/protocols/forms/base.py +1 -2
- lionagi/protocols/forms/flow.py +1 -2
- lionagi/protocols/forms/form.py +1 -2
- lionagi/protocols/forms/report.py +1 -2
- lionagi/protocols/generic/__init__.py +1 -2
- lionagi/protocols/generic/element.py +17 -65
- lionagi/protocols/generic/event.py +1 -2
- lionagi/protocols/generic/log.py +14 -12
- lionagi/protocols/generic/pile.py +6 -4
- lionagi/protocols/generic/processor.py +1 -2
- lionagi/protocols/generic/progression.py +1 -2
- lionagi/protocols/graph/__init__.py +1 -2
- lionagi/protocols/graph/edge.py +1 -2
- lionagi/protocols/graph/graph.py +1 -2
- lionagi/protocols/graph/node.py +1 -2
- lionagi/protocols/ids.py +1 -2
- lionagi/protocols/mail/__init__.py +1 -2
- lionagi/protocols/mail/exchange.py +1 -2
- lionagi/protocols/mail/mail.py +1 -2
- lionagi/protocols/mail/mailbox.py +1 -2
- lionagi/protocols/mail/manager.py +1 -2
- lionagi/protocols/mail/package.py +1 -2
- lionagi/protocols/messages/__init__.py +1 -2
- lionagi/protocols/messages/action_request.py +1 -2
- lionagi/protocols/messages/action_response.py +1 -2
- lionagi/protocols/messages/assistant_response.py +1 -2
- lionagi/protocols/messages/base.py +1 -2
- lionagi/protocols/messages/instruction.py +1 -2
- lionagi/protocols/messages/manager.py +1 -2
- lionagi/protocols/messages/message.py +1 -2
- lionagi/protocols/messages/system.py +1 -2
- lionagi/protocols/operatives/__init__.py +1 -2
- lionagi/protocols/operatives/operative.py +30 -8
- lionagi/protocols/operatives/step.py +1 -2
- lionagi/protocols/types.py +1 -2
- lionagi/service/connections/__init__.py +1 -2
- lionagi/service/connections/api_calling.py +1 -2
- lionagi/service/connections/endpoint.py +1 -2
- lionagi/service/connections/endpoint_config.py +1 -2
- lionagi/service/connections/header_factory.py +1 -2
- lionagi/service/connections/match_endpoint.py +1 -2
- lionagi/service/connections/mcp/__init__.py +1 -2
- lionagi/service/connections/mcp/wrapper.py +1 -2
- lionagi/service/connections/providers/__init__.py +1 -2
- lionagi/service/connections/providers/anthropic_.py +1 -2
- lionagi/service/connections/providers/claude_code_cli.py +1 -2
- lionagi/service/connections/providers/exa_.py +1 -2
- lionagi/service/connections/providers/nvidia_nim_.py +2 -27
- lionagi/service/connections/providers/oai_.py +1 -2
- lionagi/service/connections/providers/ollama_.py +1 -2
- lionagi/service/connections/providers/perplexity_.py +1 -2
- lionagi/service/hooks/__init__.py +1 -1
- lionagi/service/hooks/_types.py +1 -1
- lionagi/service/hooks/_utils.py +1 -1
- lionagi/service/hooks/hook_event.py +1 -1
- lionagi/service/hooks/hook_registry.py +1 -1
- lionagi/service/hooks/hooked_event.py +1 -2
- lionagi/service/imodel.py +1 -2
- lionagi/service/manager.py +1 -2
- lionagi/service/rate_limited_processor.py +1 -2
- lionagi/service/resilience.py +1 -2
- lionagi/service/third_party/anthropic_models.py +3 -5
- lionagi/service/third_party/claude_code.py +1 -2
- lionagi/service/token_calculator.py +1 -2
- lionagi/service/types.py +1 -2
- lionagi/session/__init__.py +1 -2
- lionagi/session/branch.py +1 -2
- lionagi/session/session.py +1 -2
- lionagi/tools/__init__.py +1 -2
- lionagi/tools/base.py +1 -2
- lionagi/tools/file/__init__.py +1 -2
- lionagi/tools/file/reader.py +1 -2
- lionagi/tools/types.py +1 -2
- lionagi/utils.py +1 -2
- lionagi/version.py +1 -1
- {lionagi-0.17.9.dist-info → lionagi-0.17.11.dist-info}/METADATA +2 -2
- lionagi-0.17.11.dist-info/RECORD +199 -0
- lionagi-0.17.9.dist-info/RECORD +0 -199
- {lionagi-0.17.9.dist-info → lionagi-0.17.11.dist-info}/WHEEL +0 -0
- {lionagi-0.17.9.dist-info → lionagi-0.17.11.dist-info}/licenses/LICENSE +0 -0
lionagi/models/field_model.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
"""Field model implementation for compositional field definitions.
|
2
2
|
|
3
|
-
This module provides FieldModel, a
|
3
|
+
This module provides FieldModel, a Params-based class that enables
|
4
4
|
compositional field definitions with lazy materialization and aggressive caching.
|
5
5
|
"""
|
6
6
|
|
@@ -10,12 +10,13 @@ import os
|
|
10
10
|
import threading
|
11
11
|
from collections import OrderedDict
|
12
12
|
from collections.abc import Callable
|
13
|
-
from dataclasses import dataclass
|
14
|
-
from typing import Annotated, Any
|
13
|
+
from dataclasses import dataclass
|
14
|
+
from typing import Annotated, Any, ClassVar
|
15
15
|
|
16
16
|
from typing_extensions import Self, override
|
17
17
|
|
18
18
|
from .._errors import ValidationError
|
19
|
+
from ..ln.types import Meta, Params
|
19
20
|
|
20
21
|
# Cache of valid Pydantic Field parameters
|
21
22
|
_PYDANTIC_FIELD_PARAMS: set[str] | None = None
|
@@ -38,7 +39,7 @@ def _get_pydantic_field_params() -> set[str]:
|
|
38
39
|
|
39
40
|
# Global cache for annotated types with bounded size
|
40
41
|
_MAX_CACHE_SIZE = int(os.environ.get("LIONAGI_FIELD_CACHE_SIZE", "10000"))
|
41
|
-
_annotated_cache: OrderedDict[tuple[type, tuple[
|
42
|
+
_annotated_cache: OrderedDict[tuple[type, tuple[Meta, ...]], type] = (
|
42
43
|
OrderedDict()
|
43
44
|
)
|
44
45
|
_cache_lock = threading.RLock() # Thread-safe access to cache
|
@@ -47,57 +48,20 @@ _cache_lock = threading.RLock() # Thread-safe access to cache
|
|
47
48
|
METADATA_LIMIT = int(os.environ.get("LIONAGI_FIELD_META_LIMIT", "10"))
|
48
49
|
|
49
50
|
|
50
|
-
@dataclass(slots=True, frozen=True)
|
51
|
-
class FieldMeta:
|
52
|
-
"""Immutable metadata container for field templates."""
|
53
|
-
|
54
|
-
key: str
|
55
|
-
value: Any
|
56
|
-
|
57
|
-
@override
|
58
|
-
def __hash__(self) -> int:
|
59
|
-
"""Make metadata hashable for caching.
|
60
|
-
|
61
|
-
Note: For callables, we hash by id to maintain identity semantics.
|
62
|
-
"""
|
63
|
-
# For callables, use their id
|
64
|
-
if callable(self.value):
|
65
|
-
return hash((self.key, id(self.value)))
|
66
|
-
# For other values, try to hash directly
|
67
|
-
try:
|
68
|
-
return hash((self.key, self.value))
|
69
|
-
except TypeError:
|
70
|
-
# Fallback for unhashable types
|
71
|
-
return hash((self.key, str(self.value)))
|
72
|
-
|
73
|
-
@override
|
74
|
-
def __eq__(self, other: object) -> bool:
|
75
|
-
"""Compare metadata for equality.
|
76
|
-
|
77
|
-
For callables, compare by id to increase cache hits when the same
|
78
|
-
validator instance is reused. For other values, use standard equality.
|
79
|
-
"""
|
80
|
-
if not isinstance(other, FieldMeta):
|
81
|
-
return NotImplemented
|
82
|
-
|
83
|
-
if self.key != other.key:
|
84
|
-
return False
|
85
|
-
|
86
|
-
# For callables, compare by identity
|
87
|
-
if callable(self.value) and callable(other.value):
|
88
|
-
return id(self.value) == id(other.value)
|
89
|
-
|
90
|
-
# For other values, use standard equality
|
91
|
-
return bool(self.value == other.value)
|
92
|
-
|
93
|
-
|
94
51
|
@dataclass(slots=True, frozen=True, init=False)
|
95
|
-
class FieldModel:
|
52
|
+
class FieldModel(Params):
|
96
53
|
"""Field model for compositional field definitions.
|
97
54
|
|
98
55
|
This class provides a way to define field models that can be composed
|
99
56
|
and materialized lazily with aggressive caching for performance.
|
100
57
|
|
58
|
+
Key features:
|
59
|
+
- All unspecified fields are explicitly Unset (not None or empty)
|
60
|
+
- No silent type conversions - fails fast on incorrect types
|
61
|
+
- Aggressive caching of materialized types with LRU eviction
|
62
|
+
- Thread-safe field creation and caching
|
63
|
+
- Not directly instantiable - requires keyword arguments
|
64
|
+
|
101
65
|
Attributes:
|
102
66
|
base_type: The base Python type for this field
|
103
67
|
metadata: Tuple of metadata to attach via Annotated
|
@@ -107,56 +71,123 @@ class FieldModel:
|
|
107
71
|
LIONAGI_FIELD_META_LIMIT: Maximum metadata items per template (default: 10)
|
108
72
|
|
109
73
|
Example:
|
110
|
-
>>> field = FieldModel(str)
|
74
|
+
>>> field = FieldModel(base_type=str, name="username")
|
111
75
|
>>> nullable_field = field.as_nullable()
|
112
76
|
>>> annotated_type = nullable_field.annotated()
|
113
77
|
"""
|
114
78
|
|
79
|
+
# Class configuration - let Params handle Unset population
|
80
|
+
_prefill_unset: ClassVar[bool] = True
|
81
|
+
_none_as_sentinel: ClassVar[bool] = True
|
82
|
+
|
83
|
+
# Public fields (all start as Unset when not provided)
|
115
84
|
base_type: type[Any]
|
116
|
-
metadata: tuple[
|
117
|
-
|
118
|
-
def __init__(
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
"""
|
85
|
+
metadata: tuple[Meta, ...]
|
86
|
+
|
87
|
+
def __init__(self, **kwargs: Any) -> None:
|
88
|
+
"""Initialize FieldModel with legacy compatibility.
|
89
|
+
|
90
|
+
Handles backward compatibility by converting old-style kwargs to the new
|
91
|
+
Params-based format.
|
92
|
+
|
93
|
+
Args:
|
94
|
+
**kwargs: Arbitrary keyword arguments, including legacy ones
|
95
|
+
"""
|
96
|
+
# Convert legacy kwargs to proper format
|
97
|
+
converted = self._convert_kwargs_to_params(**kwargs)
|
98
|
+
|
99
|
+
# Set fields directly and validate
|
100
|
+
for k, v in converted.items():
|
101
|
+
if k in self.allowed():
|
102
|
+
object.__setattr__(self, k, v)
|
103
|
+
else:
|
104
|
+
raise ValueError(f"Invalid parameter: {k}")
|
105
|
+
|
106
|
+
# Validate after setting all attributes
|
107
|
+
self._validate()
|
108
|
+
|
109
|
+
def _validate(self) -> None:
|
110
|
+
"""Validate field configuration and process metadata.
|
111
|
+
|
112
|
+
This method performs minimal domain-specific validation, then processes
|
113
|
+
and validates the metadata configuration.
|
114
|
+
|
115
|
+
Raises:
|
116
|
+
ValueError: If base_type is invalid or metadata is malformed
|
117
|
+
"""
|
118
|
+
# Let parent handle basic Unset population
|
119
|
+
Params._validate(self)
|
120
|
+
|
121
|
+
# Minimal domain validation - only check what matters
|
122
|
+
if not self._is_sentinel(self.base_type):
|
123
|
+
# Allow types, GenericAlias (like list[str]), and union types (like str | None)
|
124
|
+
# Check for type, generic types, or union types
|
125
|
+
import types
|
126
|
+
|
127
|
+
is_valid_type = (
|
128
|
+
isinstance(self.base_type, type)
|
129
|
+
or hasattr(self.base_type, "__origin__")
|
130
|
+
or isinstance(
|
131
|
+
self.base_type, types.UnionType
|
132
|
+
) # Python 3.10+ union types (str | None)
|
133
|
+
or str(type(self.base_type))
|
134
|
+
== "<class 'types.UnionType'>" # Fallback check
|
135
|
+
)
|
136
|
+
if not is_valid_type:
|
137
|
+
raise ValueError(
|
138
|
+
f"base_type must be a type or type annotation, got {self.base_type}"
|
139
|
+
)
|
140
|
+
|
141
|
+
# Validate metadata limit
|
142
|
+
if not self._is_sentinel(self.metadata):
|
143
|
+
if len(self.metadata) > METADATA_LIMIT:
|
144
|
+
import warnings
|
145
|
+
|
146
|
+
warnings.warn(
|
147
|
+
f"FieldModel has {len(self.metadata)} metadata items, "
|
148
|
+
f"exceeding recommended limit of {METADATA_LIMIT}. "
|
149
|
+
"Consider simplifying the field definition.",
|
150
|
+
stacklevel=3,
|
151
|
+
)
|
152
|
+
|
153
|
+
@classmethod
|
154
|
+
def _convert_kwargs_to_params(cls, **kwargs: Any) -> dict[str, Any]:
|
155
|
+
"""Convert legacy kwargs to Params-compatible format.
|
156
|
+
|
157
|
+
This handles backward compatibility with the old FieldModel API.
|
127
158
|
|
128
159
|
Args:
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
**kwargs: Additional metadata as keyword arguments that will be converted to FieldMeta
|
160
|
+
**kwargs: Legacy keyword arguments
|
161
|
+
|
162
|
+
Returns:
|
163
|
+
Dictionary of converted parameters
|
134
164
|
"""
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
base_type =
|
140
|
-
|
141
|
-
#
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
165
|
+
params = {}
|
166
|
+
|
167
|
+
# Handle annotation alias for base_type
|
168
|
+
if "annotation" in kwargs and "base_type" not in kwargs:
|
169
|
+
params["base_type"] = kwargs.pop("annotation")
|
170
|
+
|
171
|
+
# Handle name in metadata
|
172
|
+
if "name" in kwargs:
|
173
|
+
name = kwargs.pop("name")
|
174
|
+
if name != "field": # Only add if non-default
|
175
|
+
metadata = list(kwargs.get("metadata", ()))
|
176
|
+
metadata.append(Meta("name", name))
|
177
|
+
params["metadata"] = tuple(metadata)
|
178
|
+
|
179
|
+
# Handle special flags
|
180
|
+
if "nullable" in kwargs and kwargs.pop("nullable"):
|
181
|
+
metadata = list(params.get("metadata", ()))
|
182
|
+
metadata.append(Meta("nullable", True))
|
183
|
+
params["metadata"] = tuple(metadata)
|
184
|
+
|
185
|
+
if "listable" in kwargs and kwargs.pop("listable"):
|
186
|
+
metadata = list(params.get("metadata", ()))
|
187
|
+
metadata.append(Meta("listable", True))
|
188
|
+
params["metadata"] = tuple(metadata)
|
189
|
+
|
190
|
+
# Validate conflicting defaults
|
160
191
|
if "default" in kwargs and "default_factory" in kwargs:
|
161
192
|
raise ValueError("Cannot have both default and default_factory")
|
162
193
|
|
@@ -171,40 +202,32 @@ class FieldModel:
|
|
171
202
|
"Validators must be a list of functions or a function"
|
172
203
|
)
|
173
204
|
|
174
|
-
# Convert remaining kwargs to
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
object.__setattr__(self, "metadata", tuple(meta_list))
|
205
|
+
# Convert remaining kwargs to metadata
|
206
|
+
if kwargs:
|
207
|
+
metadata = list(params.get("metadata", ()))
|
208
|
+
for key, value in kwargs.items():
|
209
|
+
metadata.append(Meta(key, value))
|
210
|
+
params["metadata"] = tuple(metadata)
|
181
211
|
|
182
|
-
|
183
|
-
self.__post_init__()
|
212
|
+
return params
|
184
213
|
|
185
214
|
def __getattr__(self, name: str) -> Any:
|
186
215
|
"""Handle access to custom attributes stored in metadata."""
|
187
216
|
# Check if the attribute exists in metadata
|
188
|
-
|
189
|
-
|
190
|
-
|
217
|
+
if not self._is_sentinel(self.metadata):
|
218
|
+
for meta in self.metadata:
|
219
|
+
if meta.key == name:
|
220
|
+
return meta.value
|
221
|
+
|
222
|
+
# Special handling for common attributes with defaults
|
223
|
+
if name == "name":
|
224
|
+
return "field"
|
225
|
+
|
191
226
|
# If not found, raise AttributeError as usual
|
192
227
|
raise AttributeError(
|
193
228
|
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
194
229
|
)
|
195
230
|
|
196
|
-
def __post_init__(self) -> None:
|
197
|
-
"""Validate metadata limit after initialization."""
|
198
|
-
if len(self.metadata) > METADATA_LIMIT:
|
199
|
-
import warnings
|
200
|
-
|
201
|
-
warnings.warn(
|
202
|
-
f"FieldModel has {len(self.metadata)} metadata items, "
|
203
|
-
f"exceeding recommended limit of {METADATA_LIMIT}. "
|
204
|
-
"Consider simplifying the field definition.",
|
205
|
-
stacklevel=3, # Show user's call site, not __post_init__
|
206
|
-
)
|
207
|
-
|
208
231
|
# ---- factory helpers -------------------------------------------------- #
|
209
232
|
|
210
233
|
def as_nullable(self) -> Self:
|
@@ -214,8 +237,16 @@ class FieldModel:
|
|
214
237
|
New FieldModel with nullable metadata added
|
215
238
|
"""
|
216
239
|
# Add nullable marker to metadata
|
217
|
-
|
218
|
-
|
240
|
+
current_metadata = (
|
241
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
242
|
+
)
|
243
|
+
new_metadata = (*current_metadata, Meta("nullable", True))
|
244
|
+
# Create new instance directly without going through __init__
|
245
|
+
new_instance = object.__new__(type(self))
|
246
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
247
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
248
|
+
new_instance._validate()
|
249
|
+
return new_instance
|
219
250
|
|
220
251
|
def as_listable(self) -> Self:
|
221
252
|
"""Create a new field model that wraps the type in a list.
|
@@ -226,11 +257,23 @@ class FieldModel:
|
|
226
257
|
Returns:
|
227
258
|
New FieldModel with list wrapper
|
228
259
|
"""
|
260
|
+
# Get current base type
|
261
|
+
current_base = (
|
262
|
+
Any if self._is_sentinel(self.base_type) else self.base_type
|
263
|
+
)
|
229
264
|
# Change base type to list of current type
|
230
|
-
new_base = list[
|
265
|
+
new_base = list[current_base] # type: ignore
|
231
266
|
# Add listable marker to metadata
|
232
|
-
|
233
|
-
|
267
|
+
current_metadata = (
|
268
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
269
|
+
)
|
270
|
+
new_metadata = (*current_metadata, Meta("listable", True))
|
271
|
+
# Create new instance directly without going through __init__
|
272
|
+
new_instance = object.__new__(type(self))
|
273
|
+
object.__setattr__(new_instance, "base_type", new_base)
|
274
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
275
|
+
new_instance._validate()
|
276
|
+
return new_instance
|
234
277
|
|
235
278
|
def with_validator(self, f: Callable[[Any], bool]) -> Self:
|
236
279
|
"""Add a validator function to this field model.
|
@@ -242,8 +285,16 @@ class FieldModel:
|
|
242
285
|
New FieldModel with validator added
|
243
286
|
"""
|
244
287
|
# Add validator to metadata
|
245
|
-
|
246
|
-
|
288
|
+
current_metadata = (
|
289
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
290
|
+
)
|
291
|
+
new_metadata = (*current_metadata, Meta("validator", f))
|
292
|
+
# Create new instance directly without going through __init__
|
293
|
+
new_instance = object.__new__(type(self))
|
294
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
295
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
296
|
+
new_instance._validate()
|
297
|
+
return new_instance
|
247
298
|
|
248
299
|
def with_description(self, description: str) -> Self:
|
249
300
|
"""Add a description to this field model.
|
@@ -255,14 +306,23 @@ class FieldModel:
|
|
255
306
|
New FieldModel with description added
|
256
307
|
"""
|
257
308
|
# Remove any existing description
|
309
|
+
current_metadata = (
|
310
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
311
|
+
)
|
258
312
|
filtered_metadata = tuple(
|
259
|
-
m for m in
|
313
|
+
m for m in current_metadata if m.key != "description"
|
260
314
|
)
|
261
315
|
new_metadata = (
|
262
316
|
*filtered_metadata,
|
263
|
-
|
317
|
+
Meta("description", description),
|
264
318
|
)
|
265
|
-
|
319
|
+
|
320
|
+
# Create new instance directly without going through __init__
|
321
|
+
new_instance = object.__new__(type(self))
|
322
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
323
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
324
|
+
new_instance._validate()
|
325
|
+
return new_instance
|
266
326
|
|
267
327
|
def with_default(self, default: Any) -> Self:
|
268
328
|
"""Add a default value to this field model.
|
@@ -274,11 +334,19 @@ class FieldModel:
|
|
274
334
|
New FieldModel with default added
|
275
335
|
"""
|
276
336
|
# Remove any existing default metadata to avoid conflicts
|
337
|
+
current_metadata = (
|
338
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
339
|
+
)
|
277
340
|
filtered_metadata = tuple(
|
278
|
-
m for m in
|
341
|
+
m for m in current_metadata if m.key != "default"
|
279
342
|
)
|
280
|
-
new_metadata = (*filtered_metadata,
|
281
|
-
|
343
|
+
new_metadata = (*filtered_metadata, Meta("default", default))
|
344
|
+
# Create new instance directly without going through __init__
|
345
|
+
new_instance = object.__new__(type(self))
|
346
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
347
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
348
|
+
new_instance._validate()
|
349
|
+
return new_instance
|
282
350
|
|
283
351
|
def with_frozen(self, frozen: bool = True) -> Self:
|
284
352
|
"""Mark this field as frozen (immutable after creation).
|
@@ -290,11 +358,19 @@ class FieldModel:
|
|
290
358
|
New FieldModel with frozen setting
|
291
359
|
"""
|
292
360
|
# Remove any existing frozen metadata
|
361
|
+
current_metadata = (
|
362
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
363
|
+
)
|
293
364
|
filtered_metadata = tuple(
|
294
|
-
m for m in
|
365
|
+
m for m in current_metadata if m.key != "frozen"
|
295
366
|
)
|
296
|
-
new_metadata = (*filtered_metadata,
|
297
|
-
|
367
|
+
new_metadata = (*filtered_metadata, Meta("frozen", frozen))
|
368
|
+
# Create new instance directly without going through __init__
|
369
|
+
new_instance = object.__new__(type(self))
|
370
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
371
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
372
|
+
new_instance._validate()
|
373
|
+
return new_instance
|
298
374
|
|
299
375
|
def with_alias(self, alias: str) -> Self:
|
300
376
|
"""Add an alias to this field.
|
@@ -306,8 +382,13 @@ class FieldModel:
|
|
306
382
|
New FieldModel with alias
|
307
383
|
"""
|
308
384
|
filtered_metadata = tuple(m for m in self.metadata if m.key != "alias")
|
309
|
-
new_metadata = (*filtered_metadata,
|
310
|
-
|
385
|
+
new_metadata = (*filtered_metadata, Meta("alias", alias))
|
386
|
+
# Create new instance directly without going through __init__
|
387
|
+
new_instance = object.__new__(type(self))
|
388
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
389
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
390
|
+
new_instance._validate()
|
391
|
+
return new_instance
|
311
392
|
|
312
393
|
def with_title(self, title: str) -> Self:
|
313
394
|
"""Add a title to this field.
|
@@ -319,8 +400,13 @@ class FieldModel:
|
|
319
400
|
New FieldModel with title
|
320
401
|
"""
|
321
402
|
filtered_metadata = tuple(m for m in self.metadata if m.key != "title")
|
322
|
-
new_metadata = (*filtered_metadata,
|
323
|
-
|
403
|
+
new_metadata = (*filtered_metadata, Meta("title", title))
|
404
|
+
# Create new instance directly without going through __init__
|
405
|
+
new_instance = object.__new__(type(self))
|
406
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
407
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
408
|
+
new_instance._validate()
|
409
|
+
return new_instance
|
324
410
|
|
325
411
|
def with_exclude(self, exclude: bool = True) -> Self:
|
326
412
|
"""Mark this field to be excluded from serialization.
|
@@ -331,11 +417,19 @@ class FieldModel:
|
|
331
417
|
Returns:
|
332
418
|
New FieldModel with exclude setting
|
333
419
|
"""
|
420
|
+
current_metadata = (
|
421
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
422
|
+
)
|
334
423
|
filtered_metadata = tuple(
|
335
|
-
m for m in
|
424
|
+
m for m in current_metadata if m.key != "exclude"
|
336
425
|
)
|
337
|
-
new_metadata = (*filtered_metadata,
|
338
|
-
|
426
|
+
new_metadata = (*filtered_metadata, Meta("exclude", exclude))
|
427
|
+
# Create new instance directly without going through __init__
|
428
|
+
new_instance = object.__new__(type(self))
|
429
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
430
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
431
|
+
new_instance._validate()
|
432
|
+
return new_instance
|
339
433
|
|
340
434
|
def with_metadata(self, key: str, value: Any) -> Self:
|
341
435
|
"""Add custom metadata to this field.
|
@@ -349,8 +443,13 @@ class FieldModel:
|
|
349
443
|
"""
|
350
444
|
# Replace existing metadata with same key
|
351
445
|
filtered_metadata = tuple(m for m in self.metadata if m.key != key)
|
352
|
-
new_metadata = (*filtered_metadata,
|
353
|
-
|
446
|
+
new_metadata = (*filtered_metadata, Meta(key, value))
|
447
|
+
# Create new instance directly without going through __init__
|
448
|
+
new_instance = object.__new__(type(self))
|
449
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
450
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
451
|
+
new_instance._validate()
|
452
|
+
return new_instance
|
354
453
|
|
355
454
|
def with_json_schema_extra(self, **kwargs: Any) -> Self:
|
356
455
|
"""Add JSON schema extra information.
|
@@ -365,14 +464,22 @@ class FieldModel:
|
|
365
464
|
existing = self.extract_metadata("json_schema_extra") or {}
|
366
465
|
updated = {**existing, **kwargs}
|
367
466
|
|
467
|
+
current_metadata = (
|
468
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
469
|
+
)
|
368
470
|
filtered_metadata = tuple(
|
369
|
-
m for m in
|
471
|
+
m for m in current_metadata if m.key != "json_schema_extra"
|
370
472
|
)
|
371
473
|
new_metadata = (
|
372
474
|
*filtered_metadata,
|
373
|
-
|
475
|
+
Meta("json_schema_extra", updated),
|
374
476
|
)
|
375
|
-
|
477
|
+
# Create new instance directly without going through __init__
|
478
|
+
new_instance = object.__new__(type(self))
|
479
|
+
object.__setattr__(new_instance, "base_type", self.base_type)
|
480
|
+
object.__setattr__(new_instance, "metadata", new_metadata)
|
481
|
+
new_instance._validate()
|
482
|
+
return new_instance
|
376
483
|
|
377
484
|
def create_field(self) -> Any:
|
378
485
|
"""Create a Pydantic FieldInfo object from this template.
|
@@ -388,27 +495,28 @@ class FieldModel:
|
|
388
495
|
# Extract metadata for FieldInfo
|
389
496
|
field_kwargs = {}
|
390
497
|
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
498
|
+
if not self._is_sentinel(self.metadata):
|
499
|
+
for meta in self.metadata:
|
500
|
+
if meta.key == "default":
|
501
|
+
# Handle callable defaults as default_factory
|
502
|
+
if callable(meta.value):
|
503
|
+
field_kwargs["default_factory"] = meta.value
|
504
|
+
else:
|
505
|
+
field_kwargs["default"] = meta.value
|
506
|
+
elif meta.key == "validator":
|
507
|
+
# Validators are handled separately in create_model
|
508
|
+
continue
|
509
|
+
elif meta.key in pydantic_field_params:
|
510
|
+
# Pass through standard Pydantic field attributes
|
511
|
+
field_kwargs[meta.key] = meta.value
|
512
|
+
elif meta.key in {"nullable", "listable"}:
|
513
|
+
# These are FieldTemplate markers, don't pass to FieldInfo
|
514
|
+
pass
|
396
515
|
else:
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
elif meta.key in pydantic_field_params:
|
402
|
-
# Pass through standard Pydantic field attributes
|
403
|
-
field_kwargs[meta.key] = meta.value
|
404
|
-
elif meta.key in {"nullable", "listable"}:
|
405
|
-
# These are FieldTemplate markers, don't pass to FieldInfo
|
406
|
-
pass
|
407
|
-
else:
|
408
|
-
# Any other metadata goes in json_schema_extra
|
409
|
-
if "json_schema_extra" not in field_kwargs:
|
410
|
-
field_kwargs["json_schema_extra"] = {}
|
411
|
-
field_kwargs["json_schema_extra"][meta.key] = meta.value
|
516
|
+
# Any other metadata goes in json_schema_extra
|
517
|
+
if "json_schema_extra" not in field_kwargs:
|
518
|
+
field_kwargs["json_schema_extra"] = {}
|
519
|
+
field_kwargs["json_schema_extra"][meta.key] = meta.value
|
412
520
|
|
413
521
|
# Handle nullable case - ensure default is set if not already
|
414
522
|
if (
|
@@ -447,15 +555,21 @@ class FieldModel:
|
|
447
555
|
return _annotated_cache[cache_key]
|
448
556
|
|
449
557
|
# Handle nullable case - wrap in Optional-like union
|
450
|
-
actual_type =
|
451
|
-
|
558
|
+
actual_type = (
|
559
|
+
Any if self._is_sentinel(self.base_type) else self.base_type
|
560
|
+
)
|
561
|
+
current_metadata = (
|
562
|
+
() if self._is_sentinel(self.metadata) else self.metadata
|
563
|
+
)
|
564
|
+
|
565
|
+
if any(m.key == "nullable" and m.value for m in current_metadata):
|
452
566
|
# Use union syntax for nullable
|
453
567
|
actual_type = actual_type | None # type: ignore
|
454
568
|
|
455
|
-
if
|
569
|
+
if current_metadata:
|
456
570
|
# Python 3.10 doesn't support unpacking in Annotated, so we need to build it differently
|
457
571
|
# We'll use Annotated.__class_getitem__ to build the type dynamically
|
458
|
-
args = [actual_type] + list(
|
572
|
+
args = [actual_type] + list(current_metadata)
|
459
573
|
result = Annotated.__class_getitem__(tuple(args)) # type: ignore
|
460
574
|
else:
|
461
575
|
result = actual_type # type: ignore[misc]
|
@@ -482,9 +596,10 @@ class FieldModel:
|
|
482
596
|
Returns:
|
483
597
|
Metadata value if found, None otherwise
|
484
598
|
"""
|
485
|
-
|
486
|
-
|
487
|
-
|
599
|
+
if not self._is_sentinel(self.metadata):
|
600
|
+
for m in self.metadata:
|
601
|
+
if m.key == key:
|
602
|
+
return m.value
|
488
603
|
return None
|
489
604
|
|
490
605
|
def has_validator(self) -> bool:
|
@@ -493,6 +608,8 @@ class FieldModel:
|
|
493
608
|
Returns:
|
494
609
|
True if validator exists in metadata
|
495
610
|
"""
|
611
|
+
if self._is_sentinel(self.metadata):
|
612
|
+
return False
|
496
613
|
return any(m.key == "validator" for m in self.metadata)
|
497
614
|
|
498
615
|
def is_valid(self, value: Any) -> bool:
|
@@ -504,6 +621,8 @@ class FieldModel:
|
|
504
621
|
Returns:
|
505
622
|
True if all validators pass, False otherwise
|
506
623
|
"""
|
624
|
+
if self._is_sentinel(self.metadata):
|
625
|
+
return True
|
507
626
|
for m in self.metadata:
|
508
627
|
if m.key == "validator":
|
509
628
|
validator = m.value
|
@@ -525,41 +644,46 @@ class FieldModel:
|
|
525
644
|
if not self.has_validator():
|
526
645
|
return
|
527
646
|
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
647
|
+
if not self._is_sentinel(self.metadata):
|
648
|
+
for i, m in enumerate(self.metadata):
|
649
|
+
if m.key == "validator":
|
650
|
+
validator = m.value
|
651
|
+
# Try to call validator with correct signature
|
652
|
+
try:
|
653
|
+
# Try Pydantic-style validator (cls, value) - pass None for cls
|
654
|
+
result = validator(None, value)
|
655
|
+
# For Pydantic validators that return the value or raise exceptions,
|
656
|
+
# if we get here without exception, validation passed
|
657
|
+
except TypeError:
|
658
|
+
# Try simple validator that just takes value and returns boolean
|
659
|
+
result = validator(value)
|
660
|
+
# If validator returns False (simple boolean validator), raise error
|
661
|
+
if result is False:
|
662
|
+
validator_name = getattr(
|
663
|
+
validator, "__name__", f"validator_{i}"
|
664
|
+
)
|
665
|
+
raise ValidationError(
|
666
|
+
f"Validation failed for {validator_name}",
|
667
|
+
field_name=field_name,
|
668
|
+
value=value,
|
669
|
+
validator_name=validator_name,
|
670
|
+
)
|
671
|
+
except Exception:
|
672
|
+
# If validator raises any other exception, let it propagate
|
673
|
+
raise
|
554
674
|
|
555
675
|
@property
|
556
676
|
def is_nullable(self) -> bool:
|
557
677
|
"""Check if this field allows None values."""
|
678
|
+
if self._is_sentinel(self.metadata):
|
679
|
+
return False
|
558
680
|
return any(m.key == "nullable" and m.value for m in self.metadata)
|
559
681
|
|
560
682
|
@property
|
561
683
|
def is_listable(self) -> bool:
|
562
684
|
"""Check if this field is a list type."""
|
685
|
+
if self._is_sentinel(self.metadata):
|
686
|
+
return False
|
563
687
|
return any(m.key == "listable" and m.value for m in self.metadata)
|
564
688
|
|
565
689
|
@override
|
@@ -574,112 +698,94 @@ class FieldModel:
|
|
574
698
|
attrs.append("validated")
|
575
699
|
|
576
700
|
attr_str = f" [{', '.join(attrs)}]" if attrs else ""
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
"""Get field name from metadata for backward compatibility."""
|
584
|
-
return self.extract_metadata("name") or "field"
|
585
|
-
|
586
|
-
|
587
|
-
@property
|
588
|
-
def default(self) -> Any:
|
589
|
-
"""Get field default value from metadata for backward compatibility."""
|
590
|
-
return self.extract_metadata("default")
|
591
|
-
|
592
|
-
|
593
|
-
@property
|
594
|
-
def title(self) -> str | None:
|
595
|
-
"""Get field title from metadata for backward compatibility."""
|
596
|
-
return self.extract_metadata("title")
|
597
|
-
|
598
|
-
|
599
|
-
@property
|
600
|
-
def description(self) -> str | None:
|
601
|
-
"""Get field description from metadata for backward compatibility."""
|
602
|
-
return self.extract_metadata("description")
|
603
|
-
|
604
|
-
|
605
|
-
@property
|
606
|
-
def annotation(self) -> type[Any]:
|
607
|
-
"""Get field annotation (base_type) for backward compatibility."""
|
608
|
-
return self.base_type
|
701
|
+
base_type_name = (
|
702
|
+
"Any"
|
703
|
+
if self._is_sentinel(self.base_type)
|
704
|
+
else self.base_type.__name__
|
705
|
+
)
|
706
|
+
return f"FieldModel({base_type_name}{attr_str})"
|
609
707
|
|
708
|
+
@property
|
709
|
+
def field_validator(self) -> dict[str, Any] | None:
|
710
|
+
"""Create field validator configuration for backward compatibility.
|
610
711
|
|
611
|
-
|
612
|
-
|
613
|
-
|
712
|
+
Returns:
|
713
|
+
Dictionary mapping validator name to validator function if defined,
|
714
|
+
None otherwise.
|
715
|
+
"""
|
716
|
+
if not self.has_validator():
|
717
|
+
return None
|
614
718
|
|
615
|
-
|
719
|
+
# Extract validators and create field_validator config
|
720
|
+
from pydantic import field_validator
|
616
721
|
|
617
|
-
|
618
|
-
Configured Pydantic FieldInfo object.
|
619
|
-
"""
|
620
|
-
return self.create_field()
|
722
|
+
validators = {}
|
621
723
|
|
724
|
+
# Get field name from metadata or use default
|
725
|
+
field_name = self.extract_metadata("name") or "field"
|
622
726
|
|
623
|
-
|
624
|
-
|
625
|
-
|
727
|
+
if not self._is_sentinel(self.metadata):
|
728
|
+
for meta in self.metadata:
|
729
|
+
if meta.key == "validator":
|
730
|
+
validator_name = f"{field_name}_validator"
|
731
|
+
validators[validator_name] = field_validator(field_name)(
|
732
|
+
meta.value
|
733
|
+
)
|
626
734
|
|
627
|
-
|
628
|
-
Dictionary mapping validator name to validator function if defined,
|
629
|
-
None otherwise.
|
630
|
-
"""
|
631
|
-
if not self.has_validator():
|
632
|
-
return None
|
735
|
+
return validators if validators else None
|
633
736
|
|
634
|
-
|
635
|
-
|
737
|
+
@property
|
738
|
+
def annotation(self) -> type[Any]:
|
739
|
+
"""Get field annotation (base_type) for backward compatibility."""
|
740
|
+
return Any if self._is_sentinel(self.base_type) else self.base_type
|
636
741
|
|
637
|
-
|
742
|
+
def to_dict(self) -> dict[str, Any]:
|
743
|
+
"""Convert field model to dictionary for backward compatibility.
|
638
744
|
|
639
|
-
|
640
|
-
field_name = self.extract_metadata("name") or "field"
|
745
|
+
DEPRECATED: Use metadata_dict() instead.
|
641
746
|
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
meta.value
|
647
|
-
)
|
747
|
+
Returns:
|
748
|
+
Dictionary representation of field configuration.
|
749
|
+
"""
|
750
|
+
import warnings
|
648
751
|
|
649
|
-
|
752
|
+
warnings.warn(
|
753
|
+
"FieldModel.to_dict() is deprecated. Use metadata_dict() instead.",
|
754
|
+
DeprecationWarning,
|
755
|
+
stacklevel=2,
|
756
|
+
)
|
757
|
+
return self.metadata_dict(
|
758
|
+
exclude=[
|
759
|
+
"nullable",
|
760
|
+
"listable",
|
761
|
+
"validator",
|
762
|
+
"name",
|
763
|
+
"validator_kwargs",
|
764
|
+
"annotation",
|
765
|
+
]
|
766
|
+
)
|
650
767
|
|
768
|
+
def metadata_dict(
|
769
|
+
self, exclude: list[str] | None = None
|
770
|
+
) -> dict[str, Any]:
|
771
|
+
"""Convert all metadata to dictionary with optional exclusions.
|
651
772
|
|
652
|
-
|
653
|
-
|
773
|
+
Args:
|
774
|
+
exclude: List of metadata keys to exclude from the result
|
654
775
|
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
# Convert metadata to dictionary
|
661
|
-
for meta in self.metadata:
|
662
|
-
if meta.key not in (
|
663
|
-
"nullable",
|
664
|
-
"listable",
|
665
|
-
"validator",
|
666
|
-
"name",
|
667
|
-
"validator_kwargs",
|
668
|
-
"annotation",
|
669
|
-
):
|
670
|
-
result[meta.key] = meta.value
|
776
|
+
Returns:
|
777
|
+
Dictionary mapping metadata keys to their values
|
778
|
+
"""
|
779
|
+
result = {}
|
780
|
+
exclude_set = set(exclude or [])
|
671
781
|
|
672
|
-
|
782
|
+
# Convert metadata to dictionary
|
783
|
+
if not self._is_sentinel(self.metadata):
|
784
|
+
for meta in self.metadata:
|
785
|
+
if meta.key not in exclude_set:
|
786
|
+
result[meta.key] = meta.value
|
673
787
|
|
788
|
+
return result
|
674
789
|
|
675
|
-
# Monkey patch the methods onto FieldModel for backward compatibility
|
676
|
-
FieldModel.name = name
|
677
|
-
FieldModel.default = default
|
678
|
-
FieldModel.title = title
|
679
|
-
FieldModel.description = description
|
680
|
-
FieldModel.annotation = annotation
|
681
|
-
FieldModel.field_info = field_info
|
682
|
-
FieldModel.field_validator = field_validator
|
683
|
-
FieldModel.to_dict = to_dict
|
684
790
|
|
685
|
-
__all__ = ("FieldModel",
|
791
|
+
__all__ = ("FieldModel",)
|