lionagi 0.18.1__py3-none-any.whl → 0.18.2__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 +102 -59
 - lionagi/adapters/spec_adapters/__init__.py +9 -0
 - lionagi/adapters/spec_adapters/_protocol.py +236 -0
 - lionagi/adapters/spec_adapters/pydantic_field.py +158 -0
 - lionagi/ln/_async_call.py +2 -2
 - lionagi/ln/fuzzy/_fuzzy_match.py +2 -2
 - lionagi/ln/types/__init__.py +51 -0
 - lionagi/ln/types/_sentinel.py +154 -0
 - lionagi/ln/{types.py → types/base.py} +108 -168
 - lionagi/ln/types/operable.py +221 -0
 - lionagi/ln/types/spec.py +441 -0
 - lionagi/models/field_model.py +57 -3
 - lionagi/models/model_params.py +4 -3
 - lionagi/operations/operate/operate.py +116 -84
 - lionagi/operations/operate/operative.py +142 -305
 - lionagi/operations/operate/step.py +162 -181
 - lionagi/operations/types.py +6 -6
 - lionagi/protocols/messages/message.py +2 -2
 - lionagi/version.py +1 -1
 - {lionagi-0.18.1.dist-info → lionagi-0.18.2.dist-info}/METADATA +1 -1
 - {lionagi-0.18.1.dist-info → lionagi-0.18.2.dist-info}/RECORD +23 -16
 - {lionagi-0.18.1.dist-info → lionagi-0.18.2.dist-info}/WHEEL +0 -0
 - {lionagi-0.18.1.dist-info → lionagi-0.18.2.dist-info}/licenses/LICENSE +0 -0
 
| 
         @@ -0,0 +1,221 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            """Operable - Container for Spec collections with model generation.
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            This module provides the Operable class for managing collections of Spec objects
         
     | 
| 
      
 4 
     | 
    
         
            +
            and generating framework-specific models via adapters.
         
     | 
| 
      
 5 
     | 
    
         
            +
            """
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            from __future__ import annotations
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            from dataclasses import dataclass
         
     | 
| 
      
 10 
     | 
    
         
            +
            from typing import TYPE_CHECKING, Literal
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            from ._sentinel import MaybeUnset, Unset
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            if TYPE_CHECKING:
         
     | 
| 
      
 15 
     | 
    
         
            +
                from .spec import Spec
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            __all__ = ("Operable",)
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            @dataclass(frozen=True, slots=True, init=False)
         
     | 
| 
      
 21 
     | 
    
         
            +
            class Operable:
         
     | 
| 
      
 22 
     | 
    
         
            +
                """Collection of Spec objects with model generation capabilities.
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                Operable manages an ordered collection of Spec objects and provides
         
     | 
| 
      
 25 
     | 
    
         
            +
                methods to generate framework-specific models via adapters.
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                Attributes:
         
     | 
| 
      
 28 
     | 
    
         
            +
                    __op_fields__: Ordered tuple of Spec objects
         
     | 
| 
      
 29 
     | 
    
         
            +
                    name: Optional name for this operable
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                Example:
         
     | 
| 
      
 32 
     | 
    
         
            +
                    >>> from lionagi.ln.types import Spec, Operable
         
     | 
| 
      
 33 
     | 
    
         
            +
                    >>> specs = (
         
     | 
| 
      
 34 
     | 
    
         
            +
                    ...     Spec(str, name="username"),
         
     | 
| 
      
 35 
     | 
    
         
            +
                    ...     Spec(int, name="age"),
         
     | 
| 
      
 36 
     | 
    
         
            +
                    ... )
         
     | 
| 
      
 37 
     | 
    
         
            +
                    >>> operable = Operable(specs, name="User")
         
     | 
| 
      
 38 
     | 
    
         
            +
                    >>> UserModel = operable.create_model(adapter="pydantic")
         
     | 
| 
      
 39 
     | 
    
         
            +
                """
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                __op_fields__: tuple[Spec, ...]
         
     | 
| 
      
 42 
     | 
    
         
            +
                name: str | None
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                def __init__(
         
     | 
| 
      
 45 
     | 
    
         
            +
                    self,
         
     | 
| 
      
 46 
     | 
    
         
            +
                    specs: tuple[Spec, ...] | list[Spec] = (),
         
     | 
| 
      
 47 
     | 
    
         
            +
                    *,
         
     | 
| 
      
 48 
     | 
    
         
            +
                    name: str | None = None,
         
     | 
| 
      
 49 
     | 
    
         
            +
                ):
         
     | 
| 
      
 50 
     | 
    
         
            +
                    """Initialize Operable with specs.
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 53 
     | 
    
         
            +
                        specs: Tuple or list of Spec objects
         
     | 
| 
      
 54 
     | 
    
         
            +
                        name: Optional name for this operable
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                    Raises:
         
     | 
| 
      
 57 
     | 
    
         
            +
                        TypeError: If specs contains non-Spec objects
         
     | 
| 
      
 58 
     | 
    
         
            +
                        ValueError: If specs contains duplicate field names
         
     | 
| 
      
 59 
     | 
    
         
            +
                    """
         
     | 
| 
      
 60 
     | 
    
         
            +
                    # Import here to avoid circular import
         
     | 
| 
      
 61 
     | 
    
         
            +
                    from .spec import Spec
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                    # Convert to tuple if list
         
     | 
| 
      
 64 
     | 
    
         
            +
                    if isinstance(specs, list):
         
     | 
| 
      
 65 
     | 
    
         
            +
                        specs = tuple(specs)
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                    # Validate all items are Spec objects
         
     | 
| 
      
 68 
     | 
    
         
            +
                    for i, item in enumerate(specs):
         
     | 
| 
      
 69 
     | 
    
         
            +
                        if not isinstance(item, Spec):
         
     | 
| 
      
 70 
     | 
    
         
            +
                            raise TypeError(
         
     | 
| 
      
 71 
     | 
    
         
            +
                                f"All specs must be Spec objects, got {type(item).__name__} "
         
     | 
| 
      
 72 
     | 
    
         
            +
                                f"at index {i}"
         
     | 
| 
      
 73 
     | 
    
         
            +
                            )
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                    # Check for duplicate names
         
     | 
| 
      
 76 
     | 
    
         
            +
                    names = [s.name for s in specs if s.name is not None]
         
     | 
| 
      
 77 
     | 
    
         
            +
                    if len(names) != len(set(names)):
         
     | 
| 
      
 78 
     | 
    
         
            +
                        from collections import Counter
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                        duplicates = [
         
     | 
| 
      
 81 
     | 
    
         
            +
                            name for name, count in Counter(names).items() if count > 1
         
     | 
| 
      
 82 
     | 
    
         
            +
                        ]
         
     | 
| 
      
 83 
     | 
    
         
            +
                        raise ValueError(
         
     | 
| 
      
 84 
     | 
    
         
            +
                            f"Duplicate field names found: {duplicates}. "
         
     | 
| 
      
 85 
     | 
    
         
            +
                            "Each spec must have a unique name."
         
     | 
| 
      
 86 
     | 
    
         
            +
                        )
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                    object.__setattr__(self, "__op_fields__", specs)
         
     | 
| 
      
 89 
     | 
    
         
            +
                    object.__setattr__(self, "name", name)
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                def allowed(self) -> set[str]:
         
     | 
| 
      
 92 
     | 
    
         
            +
                    """Get set of allowed field names.
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 95 
     | 
    
         
            +
                        Set of field names from specs
         
     | 
| 
      
 96 
     | 
    
         
            +
                    """
         
     | 
| 
      
 97 
     | 
    
         
            +
                    return {i.name for i in self.__op_fields__}
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                def check_allowed(self, *args, as_boolean: bool = False):
         
     | 
| 
      
 100 
     | 
    
         
            +
                    """Check if field names are allowed.
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 103 
     | 
    
         
            +
                        *args: Field names to check
         
     | 
| 
      
 104 
     | 
    
         
            +
                        as_boolean: If True, return bool instead of raising
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 107 
     | 
    
         
            +
                        True if all allowed, False if as_boolean=True and not all allowed
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
                    Raises:
         
     | 
| 
      
 110 
     | 
    
         
            +
                        ValueError: If as_boolean=False and not all allowed
         
     | 
| 
      
 111 
     | 
    
         
            +
                    """
         
     | 
| 
      
 112 
     | 
    
         
            +
                    if not set(args).issubset(self.allowed()):
         
     | 
| 
      
 113 
     | 
    
         
            +
                        if as_boolean:
         
     | 
| 
      
 114 
     | 
    
         
            +
                            return False
         
     | 
| 
      
 115 
     | 
    
         
            +
                        raise ValueError(
         
     | 
| 
      
 116 
     | 
    
         
            +
                            "Some specified fields are not allowed: "
         
     | 
| 
      
 117 
     | 
    
         
            +
                            f"{set(args).difference(self.allowed())}"
         
     | 
| 
      
 118 
     | 
    
         
            +
                        )
         
     | 
| 
      
 119 
     | 
    
         
            +
                    return True
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
                def get(self, key: str, /, default=Unset) -> MaybeUnset[Spec]:
         
     | 
| 
      
 122 
     | 
    
         
            +
                    """Get Spec by field name.
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 125 
     | 
    
         
            +
                        key: Field name
         
     | 
| 
      
 126 
     | 
    
         
            +
                        default: Default value if not found
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 129 
     | 
    
         
            +
                        Spec object or default
         
     | 
| 
      
 130 
     | 
    
         
            +
                    """
         
     | 
| 
      
 131 
     | 
    
         
            +
                    if not self.check_allowed(key, as_boolean=True):
         
     | 
| 
      
 132 
     | 
    
         
            +
                        return default
         
     | 
| 
      
 133 
     | 
    
         
            +
                    for i in self.__op_fields__:
         
     | 
| 
      
 134 
     | 
    
         
            +
                        if i.name == key:
         
     | 
| 
      
 135 
     | 
    
         
            +
                            return i
         
     | 
| 
      
 136 
     | 
    
         
            +
                    return default
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
                def get_specs(
         
     | 
| 
      
 139 
     | 
    
         
            +
                    self,
         
     | 
| 
      
 140 
     | 
    
         
            +
                    *,
         
     | 
| 
      
 141 
     | 
    
         
            +
                    include: set[str] | None = None,
         
     | 
| 
      
 142 
     | 
    
         
            +
                    exclude: set[str] | None = None,
         
     | 
| 
      
 143 
     | 
    
         
            +
                ) -> tuple[Spec, ...]:
         
     | 
| 
      
 144 
     | 
    
         
            +
                    """Get filtered tuple of Specs.
         
     | 
| 
      
 145 
     | 
    
         
            +
             
     | 
| 
      
 146 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 147 
     | 
    
         
            +
                        include: Only include these field names
         
     | 
| 
      
 148 
     | 
    
         
            +
                        exclude: Exclude these field names
         
     | 
| 
      
 149 
     | 
    
         
            +
             
     | 
| 
      
 150 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 151 
     | 
    
         
            +
                        Filtered tuple of Specs
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
                    Raises:
         
     | 
| 
      
 154 
     | 
    
         
            +
                        ValueError: If both include and exclude specified, or if invalid names
         
     | 
| 
      
 155 
     | 
    
         
            +
                    """
         
     | 
| 
      
 156 
     | 
    
         
            +
                    if include is not None and exclude is not None:
         
     | 
| 
      
 157 
     | 
    
         
            +
                        raise ValueError("Cannot specify both include and exclude")
         
     | 
| 
      
 158 
     | 
    
         
            +
             
     | 
| 
      
 159 
     | 
    
         
            +
                    if include:
         
     | 
| 
      
 160 
     | 
    
         
            +
                        if self.check_allowed(*include, as_boolean=True) is False:
         
     | 
| 
      
 161 
     | 
    
         
            +
                            raise ValueError(
         
     | 
| 
      
 162 
     | 
    
         
            +
                                "Some specified fields are not allowed: "
         
     | 
| 
      
 163 
     | 
    
         
            +
                                f"{set(include).difference(self.allowed())}"
         
     | 
| 
      
 164 
     | 
    
         
            +
                            )
         
     | 
| 
      
 165 
     | 
    
         
            +
                        return tuple(
         
     | 
| 
      
 166 
     | 
    
         
            +
                            self.get(i) for i in include if self.get(i) is not Unset
         
     | 
| 
      
 167 
     | 
    
         
            +
                        )
         
     | 
| 
      
 168 
     | 
    
         
            +
             
     | 
| 
      
 169 
     | 
    
         
            +
                    if exclude:
         
     | 
| 
      
 170 
     | 
    
         
            +
                        _discards = {
         
     | 
| 
      
 171 
     | 
    
         
            +
                            self.get(i) for i in exclude if self.get(i) is not Unset
         
     | 
| 
      
 172 
     | 
    
         
            +
                        }
         
     | 
| 
      
 173 
     | 
    
         
            +
                        return tuple(s for s in self.__op_fields__ if s not in _discards)
         
     | 
| 
      
 174 
     | 
    
         
            +
             
     | 
| 
      
 175 
     | 
    
         
            +
                    return self.__op_fields__
         
     | 
| 
      
 176 
     | 
    
         
            +
             
     | 
| 
      
 177 
     | 
    
         
            +
                def create_model(
         
     | 
| 
      
 178 
     | 
    
         
            +
                    self,
         
     | 
| 
      
 179 
     | 
    
         
            +
                    adapter: Literal["pydantic"] = "pydantic",
         
     | 
| 
      
 180 
     | 
    
         
            +
                    model_name: str | None = None,
         
     | 
| 
      
 181 
     | 
    
         
            +
                    include: set[str] | None = None,
         
     | 
| 
      
 182 
     | 
    
         
            +
                    exclude: set[str] | None = None,
         
     | 
| 
      
 183 
     | 
    
         
            +
                    **kw,
         
     | 
| 
      
 184 
     | 
    
         
            +
                ):
         
     | 
| 
      
 185 
     | 
    
         
            +
                    """Create framework-specific model from specs.
         
     | 
| 
      
 186 
     | 
    
         
            +
             
     | 
| 
      
 187 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 188 
     | 
    
         
            +
                        adapter: Adapter type (currently only "pydantic")
         
     | 
| 
      
 189 
     | 
    
         
            +
                        model_name: Name for generated model
         
     | 
| 
      
 190 
     | 
    
         
            +
                        include: Only include these fields
         
     | 
| 
      
 191 
     | 
    
         
            +
                        exclude: Exclude these fields
         
     | 
| 
      
 192 
     | 
    
         
            +
                        **kw: Additional adapter-specific kwargs
         
     | 
| 
      
 193 
     | 
    
         
            +
             
     | 
| 
      
 194 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 195 
     | 
    
         
            +
                        Generated model class
         
     | 
| 
      
 196 
     | 
    
         
            +
             
     | 
| 
      
 197 
     | 
    
         
            +
                    Raises:
         
     | 
| 
      
 198 
     | 
    
         
            +
                        ImportError: If adapter not installed
         
     | 
| 
      
 199 
     | 
    
         
            +
                        ValueError: If adapter not supported
         
     | 
| 
      
 200 
     | 
    
         
            +
                    """
         
     | 
| 
      
 201 
     | 
    
         
            +
                    match adapter:
         
     | 
| 
      
 202 
     | 
    
         
            +
                        case "pydantic":
         
     | 
| 
      
 203 
     | 
    
         
            +
                            try:
         
     | 
| 
      
 204 
     | 
    
         
            +
                                from lionagi.adapters.spec_adapters import (
         
     | 
| 
      
 205 
     | 
    
         
            +
                                    PydanticSpecAdapter,
         
     | 
| 
      
 206 
     | 
    
         
            +
                                )
         
     | 
| 
      
 207 
     | 
    
         
            +
                            except ImportError as e:
         
     | 
| 
      
 208 
     | 
    
         
            +
                                raise ImportError(
         
     | 
| 
      
 209 
     | 
    
         
            +
                                    "PydanticSpecAdapter requires Pydantic. "
         
     | 
| 
      
 210 
     | 
    
         
            +
                                    "Install with: pip install pydantic"
         
     | 
| 
      
 211 
     | 
    
         
            +
                                ) from e
         
     | 
| 
      
 212 
     | 
    
         
            +
             
     | 
| 
      
 213 
     | 
    
         
            +
                            kws = {
         
     | 
| 
      
 214 
     | 
    
         
            +
                                "model_name": model_name or self.name or "DynamicModel",
         
     | 
| 
      
 215 
     | 
    
         
            +
                                "include": include,
         
     | 
| 
      
 216 
     | 
    
         
            +
                                "exclude": exclude,
         
     | 
| 
      
 217 
     | 
    
         
            +
                                **kw,
         
     | 
| 
      
 218 
     | 
    
         
            +
                            }
         
     | 
| 
      
 219 
     | 
    
         
            +
                            return PydanticSpecAdapter.create_model(self, **kws)
         
     | 
| 
      
 220 
     | 
    
         
            +
                        case _:
         
     | 
| 
      
 221 
     | 
    
         
            +
                            raise ValueError(f"Unsupported adapter: {adapter}")
         
     | 
    
        lionagi/ln/types/spec.py
    ADDED
    
    | 
         @@ -0,0 +1,441 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            """Spec - Universal type specification for framework-agnostic field definitions.
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            This module provides the Spec class for defining field specifications that can be
         
     | 
| 
      
 4 
     | 
    
         
            +
            adapted to any framework (Pydantic, attrs, dataclasses, etc.) via adapters.
         
     | 
| 
      
 5 
     | 
    
         
            +
            """
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            from __future__ import annotations
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            import contextlib
         
     | 
| 
      
 10 
     | 
    
         
            +
            import inspect
         
     | 
| 
      
 11 
     | 
    
         
            +
            import os
         
     | 
| 
      
 12 
     | 
    
         
            +
            import threading
         
     | 
| 
      
 13 
     | 
    
         
            +
            from dataclasses import dataclass
         
     | 
| 
      
 14 
     | 
    
         
            +
            from enum import Enum
         
     | 
| 
      
 15 
     | 
    
         
            +
            from typing import Any, Callable
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            from typing_extensions import Annotated, OrderedDict
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
            from lionagi.ln.concurrency.utils import is_coro_func
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
            from ._sentinel import MaybeUndefined, Undefined, is_sentinel, not_sentinel
         
     | 
| 
      
 22 
     | 
    
         
            +
            from .base import Meta
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
            # Global cache for annotated types with bounded size
         
     | 
| 
      
 25 
     | 
    
         
            +
            _MAX_CACHE_SIZE = int(os.environ.get("LIONAGI_FIELD_CACHE_SIZE", "10000"))
         
     | 
| 
      
 26 
     | 
    
         
            +
            _annotated_cache: OrderedDict[tuple[type, tuple[Meta, ...]], type] = (
         
     | 
| 
      
 27 
     | 
    
         
            +
                OrderedDict()
         
     | 
| 
      
 28 
     | 
    
         
            +
            )
         
     | 
| 
      
 29 
     | 
    
         
            +
            _cache_lock = threading.RLock()  # Thread-safe access to cache
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
            __all__ = ("Spec", "CommonMeta")
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
            class CommonMeta(Enum):
         
     | 
| 
      
 36 
     | 
    
         
            +
                """Common metadata keys used across field specifications."""
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                NAME = "name"
         
     | 
| 
      
 39 
     | 
    
         
            +
                NULLABLE = "nullable"
         
     | 
| 
      
 40 
     | 
    
         
            +
                LISTABLE = "listable"
         
     | 
| 
      
 41 
     | 
    
         
            +
                VALIDATOR = "validator"
         
     | 
| 
      
 42 
     | 
    
         
            +
                DEFAULT = "default"
         
     | 
| 
      
 43 
     | 
    
         
            +
                DEFAULT_FACTORY = "default_factory"
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                @classmethod
         
     | 
| 
      
 46 
     | 
    
         
            +
                def allowed(cls) -> set[str]:
         
     | 
| 
      
 47 
     | 
    
         
            +
                    """Return all allowed common metadata keys."""
         
     | 
| 
      
 48 
     | 
    
         
            +
                    return {i.value for i in cls}
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                @classmethod
         
     | 
| 
      
 51 
     | 
    
         
            +
                def _validate_common_metas(cls, **kw):
         
     | 
| 
      
 52 
     | 
    
         
            +
                    """Validate common metadata constraints."""
         
     | 
| 
      
 53 
     | 
    
         
            +
                    if kw.get("default") and kw.get("default_factory"):
         
     | 
| 
      
 54 
     | 
    
         
            +
                        raise ValueError(
         
     | 
| 
      
 55 
     | 
    
         
            +
                            "Cannot provide both 'default' and 'default_factory'"
         
     | 
| 
      
 56 
     | 
    
         
            +
                        )
         
     | 
| 
      
 57 
     | 
    
         
            +
                    if _df := kw.get("default_factory"):
         
     | 
| 
      
 58 
     | 
    
         
            +
                        if not callable(_df):
         
     | 
| 
      
 59 
     | 
    
         
            +
                            raise ValueError("'default_factory' must be callable")
         
     | 
| 
      
 60 
     | 
    
         
            +
                    if _val := kw.get("validator"):
         
     | 
| 
      
 61 
     | 
    
         
            +
                        _val = [_val] if not isinstance(_val, list) else _val
         
     | 
| 
      
 62 
     | 
    
         
            +
                        if not all(callable(v) for v in _val):
         
     | 
| 
      
 63 
     | 
    
         
            +
                            raise ValueError(
         
     | 
| 
      
 64 
     | 
    
         
            +
                                "Validators must be a list of functions or a function"
         
     | 
| 
      
 65 
     | 
    
         
            +
                            )
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                @classmethod
         
     | 
| 
      
 68 
     | 
    
         
            +
                def prepare(
         
     | 
| 
      
 69 
     | 
    
         
            +
                    cls, *args: Meta, metadata: tuple[Meta, ...] = None, **kw: Any
         
     | 
| 
      
 70 
     | 
    
         
            +
                ) -> tuple[Meta, ...]:
         
     | 
| 
      
 71 
     | 
    
         
            +
                    """Prepare metadata tuple from various inputs, checking for duplicates.
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 74 
     | 
    
         
            +
                        *args: Individual Meta objects
         
     | 
| 
      
 75 
     | 
    
         
            +
                        metadata: Existing metadata tuple
         
     | 
| 
      
 76 
     | 
    
         
            +
                        **kw: Keyword arguments to convert to Meta objects
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 79 
     | 
    
         
            +
                        Tuple of Meta objects
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                    Raises:
         
     | 
| 
      
 82 
     | 
    
         
            +
                        ValueError: If duplicate keys are found
         
     | 
| 
      
 83 
     | 
    
         
            +
                    """
         
     | 
| 
      
 84 
     | 
    
         
            +
                    # Lazy import to avoid circular dependency
         
     | 
| 
      
 85 
     | 
    
         
            +
                    from .._to_list import to_list
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
                    seen_keys = set()
         
     | 
| 
      
 88 
     | 
    
         
            +
                    metas = []
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                    # Process existing metadata
         
     | 
| 
      
 91 
     | 
    
         
            +
                    if metadata:
         
     | 
| 
      
 92 
     | 
    
         
            +
                        for meta in metadata:
         
     | 
| 
      
 93 
     | 
    
         
            +
                            if meta.key in seen_keys:
         
     | 
| 
      
 94 
     | 
    
         
            +
                                raise ValueError(f"Duplicate metadata key: {meta.key}")
         
     | 
| 
      
 95 
     | 
    
         
            +
                            seen_keys.add(meta.key)
         
     | 
| 
      
 96 
     | 
    
         
            +
                            metas.append(meta)
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                    # Process args
         
     | 
| 
      
 99 
     | 
    
         
            +
                    if args:
         
     | 
| 
      
 100 
     | 
    
         
            +
                        _args = to_list(
         
     | 
| 
      
 101 
     | 
    
         
            +
                            args, flatten=True, flatten_tuple_set=True, dropna=True
         
     | 
| 
      
 102 
     | 
    
         
            +
                        )
         
     | 
| 
      
 103 
     | 
    
         
            +
                        for meta in _args:
         
     | 
| 
      
 104 
     | 
    
         
            +
                            if meta.key in seen_keys:
         
     | 
| 
      
 105 
     | 
    
         
            +
                                raise ValueError(f"Duplicate metadata key: {meta.key}")
         
     | 
| 
      
 106 
     | 
    
         
            +
                            seen_keys.add(meta.key)
         
     | 
| 
      
 107 
     | 
    
         
            +
                            metas.append(meta)
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
                    # Process kwargs
         
     | 
| 
      
 110 
     | 
    
         
            +
                    for k, v in kw.items():
         
     | 
| 
      
 111 
     | 
    
         
            +
                        if k in seen_keys:
         
     | 
| 
      
 112 
     | 
    
         
            +
                            raise ValueError(f"Duplicate metadata key: {k}")
         
     | 
| 
      
 113 
     | 
    
         
            +
                        seen_keys.add(k)
         
     | 
| 
      
 114 
     | 
    
         
            +
                        metas.append(Meta(k, v))
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                    # Validate common metadata constraints
         
     | 
| 
      
 117 
     | 
    
         
            +
                    meta_dict = {m.key: m.value for m in metas}
         
     | 
| 
      
 118 
     | 
    
         
            +
                    cls._validate_common_metas(**meta_dict)
         
     | 
| 
      
 119 
     | 
    
         
            +
             
     | 
| 
      
 120 
     | 
    
         
            +
                    return tuple(metas)
         
     | 
| 
      
 121 
     | 
    
         
            +
             
     | 
| 
      
 122 
     | 
    
         
            +
             
     | 
| 
      
 123 
     | 
    
         
            +
            @dataclass(frozen=True, slots=True, init=False)
         
     | 
| 
      
 124 
     | 
    
         
            +
            class Spec:
         
     | 
| 
      
 125 
     | 
    
         
            +
                """Framework-agnostic field specification.
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
                A Spec defines the type and metadata for a field without coupling to any
         
     | 
| 
      
 128 
     | 
    
         
            +
                specific framework. Use adapters to convert Spec to framework-specific
         
     | 
| 
      
 129 
     | 
    
         
            +
                field definitions (e.g., Pydantic Field, attrs attribute).
         
     | 
| 
      
 130 
     | 
    
         
            +
             
     | 
| 
      
 131 
     | 
    
         
            +
                Attributes:
         
     | 
| 
      
 132 
     | 
    
         
            +
                    base_type: The base Python type for this field
         
     | 
| 
      
 133 
     | 
    
         
            +
                    metadata: Tuple of metadata objects attached to this spec
         
     | 
| 
      
 134 
     | 
    
         
            +
             
     | 
| 
      
 135 
     | 
    
         
            +
                Example:
         
     | 
| 
      
 136 
     | 
    
         
            +
                    >>> spec = Spec(str, name="username", nullable=False)
         
     | 
| 
      
 137 
     | 
    
         
            +
                    >>> spec.name
         
     | 
| 
      
 138 
     | 
    
         
            +
                    'username'
         
     | 
| 
      
 139 
     | 
    
         
            +
                    >>> spec.annotation
         
     | 
| 
      
 140 
     | 
    
         
            +
                    str
         
     | 
| 
      
 141 
     | 
    
         
            +
                """
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
                base_type: type
         
     | 
| 
      
 144 
     | 
    
         
            +
                metadata: tuple[Meta, ...]
         
     | 
| 
      
 145 
     | 
    
         
            +
             
     | 
| 
      
 146 
     | 
    
         
            +
                def __init__(
         
     | 
| 
      
 147 
     | 
    
         
            +
                    self,
         
     | 
| 
      
 148 
     | 
    
         
            +
                    base_type: type = None,
         
     | 
| 
      
 149 
     | 
    
         
            +
                    *args,
         
     | 
| 
      
 150 
     | 
    
         
            +
                    metadata: tuple[Meta, ...] = None,
         
     | 
| 
      
 151 
     | 
    
         
            +
                    **kw,
         
     | 
| 
      
 152 
     | 
    
         
            +
                ) -> None:
         
     | 
| 
      
 153 
     | 
    
         
            +
                    """Initialize Spec with type and metadata.
         
     | 
| 
      
 154 
     | 
    
         
            +
             
     | 
| 
      
 155 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 156 
     | 
    
         
            +
                        base_type: Base Python type
         
     | 
| 
      
 157 
     | 
    
         
            +
                        *args: Additional Meta objects
         
     | 
| 
      
 158 
     | 
    
         
            +
                        metadata: Existing metadata tuple
         
     | 
| 
      
 159 
     | 
    
         
            +
                        **kw: Keyword arguments converted to Meta objects
         
     | 
| 
      
 160 
     | 
    
         
            +
                    """
         
     | 
| 
      
 161 
     | 
    
         
            +
                    metas = CommonMeta.prepare(*args, metadata=metadata, **kw)
         
     | 
| 
      
 162 
     | 
    
         
            +
             
     | 
| 
      
 163 
     | 
    
         
            +
                    if not_sentinel(base_type, True):
         
     | 
| 
      
 164 
     | 
    
         
            +
                        import types
         
     | 
| 
      
 165 
     | 
    
         
            +
             
     | 
| 
      
 166 
     | 
    
         
            +
                        is_valid_type = (
         
     | 
| 
      
 167 
     | 
    
         
            +
                            isinstance(base_type, type)
         
     | 
| 
      
 168 
     | 
    
         
            +
                            or hasattr(base_type, "__origin__")
         
     | 
| 
      
 169 
     | 
    
         
            +
                            or isinstance(base_type, types.UnionType)
         
     | 
| 
      
 170 
     | 
    
         
            +
                            or str(type(base_type)) == "<class 'types.UnionType'>"
         
     | 
| 
      
 171 
     | 
    
         
            +
                        )
         
     | 
| 
      
 172 
     | 
    
         
            +
                        if not is_valid_type:
         
     | 
| 
      
 173 
     | 
    
         
            +
                            raise ValueError(
         
     | 
| 
      
 174 
     | 
    
         
            +
                                f"base_type must be a type or type annotation, got {base_type}"
         
     | 
| 
      
 175 
     | 
    
         
            +
                            )
         
     | 
| 
      
 176 
     | 
    
         
            +
             
     | 
| 
      
 177 
     | 
    
         
            +
                    # Check for async default factory and warn
         
     | 
| 
      
 178 
     | 
    
         
            +
                    if kw.get("default_factory") and is_coro_func(kw["default_factory"]):
         
     | 
| 
      
 179 
     | 
    
         
            +
                        import warnings
         
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
      
 181 
     | 
    
         
            +
                        warnings.warn(
         
     | 
| 
      
 182 
     | 
    
         
            +
                            "Async default factories are not yet fully supported by all adapters. "
         
     | 
| 
      
 183 
     | 
    
         
            +
                            "Consider using sync factories for compatibility.",
         
     | 
| 
      
 184 
     | 
    
         
            +
                            UserWarning,
         
     | 
| 
      
 185 
     | 
    
         
            +
                            stacklevel=2,
         
     | 
| 
      
 186 
     | 
    
         
            +
                        )
         
     | 
| 
      
 187 
     | 
    
         
            +
             
     | 
| 
      
 188 
     | 
    
         
            +
                    object.__setattr__(self, "base_type", base_type)
         
     | 
| 
      
 189 
     | 
    
         
            +
                    object.__setattr__(self, "metadata", metas)
         
     | 
| 
      
 190 
     | 
    
         
            +
             
     | 
| 
      
 191 
     | 
    
         
            +
                def __getitem__(self, key: str) -> Any:
         
     | 
| 
      
 192 
     | 
    
         
            +
                    """Get metadata value by key.
         
     | 
| 
      
 193 
     | 
    
         
            +
             
     | 
| 
      
 194 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 195 
     | 
    
         
            +
                        key: Metadata key
         
     | 
| 
      
 196 
     | 
    
         
            +
             
     | 
| 
      
 197 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 198 
     | 
    
         
            +
                        Metadata value
         
     | 
| 
      
 199 
     | 
    
         
            +
             
     | 
| 
      
 200 
     | 
    
         
            +
                    Raises:
         
     | 
| 
      
 201 
     | 
    
         
            +
                        KeyError: If key not found
         
     | 
| 
      
 202 
     | 
    
         
            +
                    """
         
     | 
| 
      
 203 
     | 
    
         
            +
                    for meta in self.metadata:
         
     | 
| 
      
 204 
     | 
    
         
            +
                        if meta.key == key:
         
     | 
| 
      
 205 
     | 
    
         
            +
                            return meta.value
         
     | 
| 
      
 206 
     | 
    
         
            +
                    raise KeyError(f"Metadata key '{key}' undefined in Spec.")
         
     | 
| 
      
 207 
     | 
    
         
            +
             
     | 
| 
      
 208 
     | 
    
         
            +
                def get(self, key: str, default: Any = Undefined) -> Any:
         
     | 
| 
      
 209 
     | 
    
         
            +
                    """Get metadata value by key with default.
         
     | 
| 
      
 210 
     | 
    
         
            +
             
     | 
| 
      
 211 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 212 
     | 
    
         
            +
                        key: Metadata key
         
     | 
| 
      
 213 
     | 
    
         
            +
                        default: Default value if key not found
         
     | 
| 
      
 214 
     | 
    
         
            +
             
     | 
| 
      
 215 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 216 
     | 
    
         
            +
                        Metadata value or default
         
     | 
| 
      
 217 
     | 
    
         
            +
                    """
         
     | 
| 
      
 218 
     | 
    
         
            +
                    with contextlib.suppress(KeyError):
         
     | 
| 
      
 219 
     | 
    
         
            +
                        return self[key]
         
     | 
| 
      
 220 
     | 
    
         
            +
                    return default
         
     | 
| 
      
 221 
     | 
    
         
            +
             
     | 
| 
      
 222 
     | 
    
         
            +
                @property
         
     | 
| 
      
 223 
     | 
    
         
            +
                def name(self) -> MaybeUndefined[str]:
         
     | 
| 
      
 224 
     | 
    
         
            +
                    """Get the field name from metadata."""
         
     | 
| 
      
 225 
     | 
    
         
            +
                    return self.get(CommonMeta.NAME.value)
         
     | 
| 
      
 226 
     | 
    
         
            +
             
     | 
| 
      
 227 
     | 
    
         
            +
                @property
         
     | 
| 
      
 228 
     | 
    
         
            +
                def is_nullable(self) -> bool:
         
     | 
| 
      
 229 
     | 
    
         
            +
                    """Check if field is nullable."""
         
     | 
| 
      
 230 
     | 
    
         
            +
                    return self.get(CommonMeta.NULLABLE.value) is True
         
     | 
| 
      
 231 
     | 
    
         
            +
             
     | 
| 
      
 232 
     | 
    
         
            +
                @property
         
     | 
| 
      
 233 
     | 
    
         
            +
                def is_listable(self) -> bool:
         
     | 
| 
      
 234 
     | 
    
         
            +
                    """Check if field is listable."""
         
     | 
| 
      
 235 
     | 
    
         
            +
                    return self.get(CommonMeta.LISTABLE.value) is True
         
     | 
| 
      
 236 
     | 
    
         
            +
             
     | 
| 
      
 237 
     | 
    
         
            +
                @property
         
     | 
| 
      
 238 
     | 
    
         
            +
                def default(self) -> MaybeUndefined[Any]:
         
     | 
| 
      
 239 
     | 
    
         
            +
                    """Get default value or factory."""
         
     | 
| 
      
 240 
     | 
    
         
            +
                    return self.get(
         
     | 
| 
      
 241 
     | 
    
         
            +
                        CommonMeta.DEFAULT.value,
         
     | 
| 
      
 242 
     | 
    
         
            +
                        self.get(CommonMeta.DEFAULT_FACTORY.value),
         
     | 
| 
      
 243 
     | 
    
         
            +
                    )
         
     | 
| 
      
 244 
     | 
    
         
            +
             
     | 
| 
      
 245 
     | 
    
         
            +
                @property
         
     | 
| 
      
 246 
     | 
    
         
            +
                def has_default_factory(self) -> bool:
         
     | 
| 
      
 247 
     | 
    
         
            +
                    """Check if this spec has a default factory."""
         
     | 
| 
      
 248 
     | 
    
         
            +
                    return _is_factory(self.get(CommonMeta.DEFAULT_FACTORY.value))[0]
         
     | 
| 
      
 249 
     | 
    
         
            +
             
     | 
| 
      
 250 
     | 
    
         
            +
                @property
         
     | 
| 
      
 251 
     | 
    
         
            +
                def has_async_default_factory(self) -> bool:
         
     | 
| 
      
 252 
     | 
    
         
            +
                    """Check if this spec has an async default factory."""
         
     | 
| 
      
 253 
     | 
    
         
            +
                    return _is_factory(self.get(CommonMeta.DEFAULT_FACTORY.value))[1]
         
     | 
| 
      
 254 
     | 
    
         
            +
             
     | 
| 
      
 255 
     | 
    
         
            +
                def create_default_value(self) -> Any:
         
     | 
| 
      
 256 
     | 
    
         
            +
                    """Create default value synchronously.
         
     | 
| 
      
 257 
     | 
    
         
            +
             
     | 
| 
      
 258 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 259 
     | 
    
         
            +
                        Default value
         
     | 
| 
      
 260 
     | 
    
         
            +
             
     | 
| 
      
 261 
     | 
    
         
            +
                    Raises:
         
     | 
| 
      
 262 
     | 
    
         
            +
                        ValueError: If no default or factory defined, or if factory is async
         
     | 
| 
      
 263 
     | 
    
         
            +
                    """
         
     | 
| 
      
 264 
     | 
    
         
            +
                    if self.default is Undefined:
         
     | 
| 
      
 265 
     | 
    
         
            +
                        raise ValueError("No default value or factory defined in Spec.")
         
     | 
| 
      
 266 
     | 
    
         
            +
                    if self.has_async_default_factory:
         
     | 
| 
      
 267 
     | 
    
         
            +
                        raise ValueError(
         
     | 
| 
      
 268 
     | 
    
         
            +
                            "Default factory is asynchronous; cannot create default synchronously. "
         
     | 
| 
      
 269 
     | 
    
         
            +
                            "Use 'await spec.acreate_default_value()' instead."
         
     | 
| 
      
 270 
     | 
    
         
            +
                        )
         
     | 
| 
      
 271 
     | 
    
         
            +
                    if self.has_default_factory:
         
     | 
| 
      
 272 
     | 
    
         
            +
                        return self.default()
         
     | 
| 
      
 273 
     | 
    
         
            +
                    return self.default
         
     | 
| 
      
 274 
     | 
    
         
            +
             
     | 
| 
      
 275 
     | 
    
         
            +
                async def acreate_default_value(self) -> Any:
         
     | 
| 
      
 276 
     | 
    
         
            +
                    """Create default value asynchronously.
         
     | 
| 
      
 277 
     | 
    
         
            +
             
     | 
| 
      
 278 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 279 
     | 
    
         
            +
                        Default value
         
     | 
| 
      
 280 
     | 
    
         
            +
                    """
         
     | 
| 
      
 281 
     | 
    
         
            +
                    if self.has_async_default_factory:
         
     | 
| 
      
 282 
     | 
    
         
            +
                        return await self.default()
         
     | 
| 
      
 283 
     | 
    
         
            +
                    return self.create_default_value()
         
     | 
| 
      
 284 
     | 
    
         
            +
             
     | 
| 
      
 285 
     | 
    
         
            +
                def with_updates(self, **kw):
         
     | 
| 
      
 286 
     | 
    
         
            +
                    """Create new Spec with updated metadata.
         
     | 
| 
      
 287 
     | 
    
         
            +
             
     | 
| 
      
 288 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 289 
     | 
    
         
            +
                        **kw: Metadata updates
         
     | 
| 
      
 290 
     | 
    
         
            +
             
     | 
| 
      
 291 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 292 
     | 
    
         
            +
                        New Spec instance with updates
         
     | 
| 
      
 293 
     | 
    
         
            +
                    """
         
     | 
| 
      
 294 
     | 
    
         
            +
                    _filtered = [meta for meta in self.metadata if meta.key not in kw]
         
     | 
| 
      
 295 
     | 
    
         
            +
                    for k, v in kw.items():
         
     | 
| 
      
 296 
     | 
    
         
            +
                        if not_sentinel(v):
         
     | 
| 
      
 297 
     | 
    
         
            +
                            _filtered.append(Meta(k, v))
         
     | 
| 
      
 298 
     | 
    
         
            +
                    _metas = tuple(_filtered)
         
     | 
| 
      
 299 
     | 
    
         
            +
                    return type(self)(self.base_type, metadata=_metas)
         
     | 
| 
      
 300 
     | 
    
         
            +
             
     | 
| 
      
 301 
     | 
    
         
            +
                def as_nullable(self) -> Spec:
         
     | 
| 
      
 302 
     | 
    
         
            +
                    """Create nullable version of this spec."""
         
     | 
| 
      
 303 
     | 
    
         
            +
                    return self.with_updates(nullable=True)
         
     | 
| 
      
 304 
     | 
    
         
            +
             
     | 
| 
      
 305 
     | 
    
         
            +
                def as_listable(self) -> Spec:
         
     | 
| 
      
 306 
     | 
    
         
            +
                    """Create listable version of this spec."""
         
     | 
| 
      
 307 
     | 
    
         
            +
                    return self.with_updates(listable=True)
         
     | 
| 
      
 308 
     | 
    
         
            +
             
     | 
| 
      
 309 
     | 
    
         
            +
                def with_default(self, default: Any) -> Spec:
         
     | 
| 
      
 310 
     | 
    
         
            +
                    """Create spec with default value or factory.
         
     | 
| 
      
 311 
     | 
    
         
            +
             
     | 
| 
      
 312 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 313 
     | 
    
         
            +
                        default: Default value or factory function
         
     | 
| 
      
 314 
     | 
    
         
            +
             
     | 
| 
      
 315 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 316 
     | 
    
         
            +
                        New Spec with default
         
     | 
| 
      
 317 
     | 
    
         
            +
                    """
         
     | 
| 
      
 318 
     | 
    
         
            +
                    if callable(default):
         
     | 
| 
      
 319 
     | 
    
         
            +
                        return self.with_updates(default_factory=default)
         
     | 
| 
      
 320 
     | 
    
         
            +
                    return self.with_updates(default=default)
         
     | 
| 
      
 321 
     | 
    
         
            +
             
     | 
| 
      
 322 
     | 
    
         
            +
                def with_validator(
         
     | 
| 
      
 323 
     | 
    
         
            +
                    self, validator: Callable[..., Any] | list[Callable[..., Any]]
         
     | 
| 
      
 324 
     | 
    
         
            +
                ) -> Spec:
         
     | 
| 
      
 325 
     | 
    
         
            +
                    """Create spec with validator(s).
         
     | 
| 
      
 326 
     | 
    
         
            +
             
     | 
| 
      
 327 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 328 
     | 
    
         
            +
                        validator: Single validator or list of validators
         
     | 
| 
      
 329 
     | 
    
         
            +
             
     | 
| 
      
 330 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 331 
     | 
    
         
            +
                        New Spec with validator(s)
         
     | 
| 
      
 332 
     | 
    
         
            +
                    """
         
     | 
| 
      
 333 
     | 
    
         
            +
                    return self.with_updates(validator=validator)
         
     | 
| 
      
 334 
     | 
    
         
            +
             
     | 
| 
      
 335 
     | 
    
         
            +
                @property
         
     | 
| 
      
 336 
     | 
    
         
            +
                def annotation(self) -> type[Any]:
         
     | 
| 
      
 337 
     | 
    
         
            +
                    """Plain type annotation representing base type, nullable, and listable.
         
     | 
| 
      
 338 
     | 
    
         
            +
             
     | 
| 
      
 339 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 340 
     | 
    
         
            +
                        Type annotation
         
     | 
| 
      
 341 
     | 
    
         
            +
                    """
         
     | 
| 
      
 342 
     | 
    
         
            +
                    if is_sentinel(self.base_type, none_as_sentinel=True):
         
     | 
| 
      
 343 
     | 
    
         
            +
                        return Any
         
     | 
| 
      
 344 
     | 
    
         
            +
                    t_ = self.base_type
         
     | 
| 
      
 345 
     | 
    
         
            +
                    if self.is_listable:
         
     | 
| 
      
 346 
     | 
    
         
            +
                        t_ = list[t_]
         
     | 
| 
      
 347 
     | 
    
         
            +
                    if self.is_nullable:
         
     | 
| 
      
 348 
     | 
    
         
            +
                        return t_ | None
         
     | 
| 
      
 349 
     | 
    
         
            +
                    return t_
         
     | 
| 
      
 350 
     | 
    
         
            +
             
     | 
| 
      
 351 
     | 
    
         
            +
                def annotated(self) -> type[Any]:
         
     | 
| 
      
 352 
     | 
    
         
            +
                    """Materialize this spec into an Annotated type.
         
     | 
| 
      
 353 
     | 
    
         
            +
             
     | 
| 
      
 354 
     | 
    
         
            +
                    This method is cached to ensure repeated calls return the same
         
     | 
| 
      
 355 
     | 
    
         
            +
                    type object for performance and identity checks. The cache is bounded
         
     | 
| 
      
 356 
     | 
    
         
            +
                    using LRU eviction to prevent unbounded memory growth.
         
     | 
| 
      
 357 
     | 
    
         
            +
             
     | 
| 
      
 358 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 359 
     | 
    
         
            +
                        Annotated type with all metadata attached
         
     | 
| 
      
 360 
     | 
    
         
            +
                    """
         
     | 
| 
      
 361 
     | 
    
         
            +
                    # Check cache first with thread safety
         
     | 
| 
      
 362 
     | 
    
         
            +
                    cache_key = (self.base_type, self.metadata)
         
     | 
| 
      
 363 
     | 
    
         
            +
             
     | 
| 
      
 364 
     | 
    
         
            +
                    with _cache_lock:
         
     | 
| 
      
 365 
     | 
    
         
            +
                        if cache_key in _annotated_cache:
         
     | 
| 
      
 366 
     | 
    
         
            +
                            # Move to end to mark as recently used
         
     | 
| 
      
 367 
     | 
    
         
            +
                            _annotated_cache.move_to_end(cache_key)
         
     | 
| 
      
 368 
     | 
    
         
            +
                            return _annotated_cache[cache_key]
         
     | 
| 
      
 369 
     | 
    
         
            +
             
     | 
| 
      
 370 
     | 
    
         
            +
                        # Handle nullable case - wrap in Optional-like union
         
     | 
| 
      
 371 
     | 
    
         
            +
                        actual_type = (
         
     | 
| 
      
 372 
     | 
    
         
            +
                            Any
         
     | 
| 
      
 373 
     | 
    
         
            +
                            if is_sentinel(self.base_type, none_as_sentinel=True)
         
     | 
| 
      
 374 
     | 
    
         
            +
                            else self.base_type
         
     | 
| 
      
 375 
     | 
    
         
            +
                        )
         
     | 
| 
      
 376 
     | 
    
         
            +
                        current_metadata = (
         
     | 
| 
      
 377 
     | 
    
         
            +
                            ()
         
     | 
| 
      
 378 
     | 
    
         
            +
                            if is_sentinel(self.metadata, none_as_sentinel=True)
         
     | 
| 
      
 379 
     | 
    
         
            +
                            else self.metadata
         
     | 
| 
      
 380 
     | 
    
         
            +
                        )
         
     | 
| 
      
 381 
     | 
    
         
            +
             
     | 
| 
      
 382 
     | 
    
         
            +
                        if any(m.key == "nullable" and m.value for m in current_metadata):
         
     | 
| 
      
 383 
     | 
    
         
            +
                            # Use union syntax for nullable
         
     | 
| 
      
 384 
     | 
    
         
            +
                            actual_type = actual_type | None  # type: ignore
         
     | 
| 
      
 385 
     | 
    
         
            +
             
     | 
| 
      
 386 
     | 
    
         
            +
                        if current_metadata:
         
     | 
| 
      
 387 
     | 
    
         
            +
                            args = [actual_type] + list(current_metadata)
         
     | 
| 
      
 388 
     | 
    
         
            +
                            result = Annotated.__class_getitem__(tuple(args))  # type: ignore
         
     | 
| 
      
 389 
     | 
    
         
            +
                        else:
         
     | 
| 
      
 390 
     | 
    
         
            +
                            result = actual_type  # type: ignore[misc]
         
     | 
| 
      
 391 
     | 
    
         
            +
             
     | 
| 
      
 392 
     | 
    
         
            +
                        # Cache the result with LRU eviction
         
     | 
| 
      
 393 
     | 
    
         
            +
                        _annotated_cache[cache_key] = result  # type: ignore[assignment]
         
     | 
| 
      
 394 
     | 
    
         
            +
             
     | 
| 
      
 395 
     | 
    
         
            +
                        # Evict oldest if cache is too large (guard against empty cache)
         
     | 
| 
      
 396 
     | 
    
         
            +
                        while len(_annotated_cache) > _MAX_CACHE_SIZE:
         
     | 
| 
      
 397 
     | 
    
         
            +
                            try:
         
     | 
| 
      
 398 
     | 
    
         
            +
                                _annotated_cache.popitem(last=False)  # Remove oldest
         
     | 
| 
      
 399 
     | 
    
         
            +
                            except KeyError:
         
     | 
| 
      
 400 
     | 
    
         
            +
                                # Cache became empty during race, safe to continue
         
     | 
| 
      
 401 
     | 
    
         
            +
                                break
         
     | 
| 
      
 402 
     | 
    
         
            +
             
     | 
| 
      
 403 
     | 
    
         
            +
                    return result  # type: ignore[return-value]
         
     | 
| 
      
 404 
     | 
    
         
            +
             
     | 
| 
      
 405 
     | 
    
         
            +
                def metadict(
         
     | 
| 
      
 406 
     | 
    
         
            +
                    self, exclude: set[str] | None = None, exclude_common: bool = False
         
     | 
| 
      
 407 
     | 
    
         
            +
                ) -> dict[str, Any]:
         
     | 
| 
      
 408 
     | 
    
         
            +
                    """Get metadata as dictionary.
         
     | 
| 
      
 409 
     | 
    
         
            +
             
     | 
| 
      
 410 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 411 
     | 
    
         
            +
                        exclude: Keys to exclude
         
     | 
| 
      
 412 
     | 
    
         
            +
                        exclude_common: Exclude all common metadata keys
         
     | 
| 
      
 413 
     | 
    
         
            +
             
     | 
| 
      
 414 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 415 
     | 
    
         
            +
                        Dictionary of metadata
         
     | 
| 
      
 416 
     | 
    
         
            +
                    """
         
     | 
| 
      
 417 
     | 
    
         
            +
                    if exclude is None:
         
     | 
| 
      
 418 
     | 
    
         
            +
                        exclude = set()
         
     | 
| 
      
 419 
     | 
    
         
            +
                    if exclude_common:
         
     | 
| 
      
 420 
     | 
    
         
            +
                        exclude = exclude | CommonMeta.allowed()
         
     | 
| 
      
 421 
     | 
    
         
            +
                    return {
         
     | 
| 
      
 422 
     | 
    
         
            +
                        meta.key: meta.value
         
     | 
| 
      
 423 
     | 
    
         
            +
                        for meta in self.metadata
         
     | 
| 
      
 424 
     | 
    
         
            +
                        if meta.key not in exclude
         
     | 
| 
      
 425 
     | 
    
         
            +
                    }
         
     | 
| 
      
 426 
     | 
    
         
            +
             
     | 
| 
      
 427 
     | 
    
         
            +
             
     | 
| 
      
 428 
     | 
    
         
            +
            def _is_factory(obj: Any) -> tuple[bool, bool]:
         
     | 
| 
      
 429 
     | 
    
         
            +
                """Check if object is a factory function.
         
     | 
| 
      
 430 
     | 
    
         
            +
             
     | 
| 
      
 431 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 432 
     | 
    
         
            +
                    obj: Object to check
         
     | 
| 
      
 433 
     | 
    
         
            +
             
     | 
| 
      
 434 
     | 
    
         
            +
                Returns:
         
     | 
| 
      
 435 
     | 
    
         
            +
                    Tuple of (is_factory, is_async)
         
     | 
| 
      
 436 
     | 
    
         
            +
                """
         
     | 
| 
      
 437 
     | 
    
         
            +
                if not callable(obj):
         
     | 
| 
      
 438 
     | 
    
         
            +
                    return (False, False)
         
     | 
| 
      
 439 
     | 
    
         
            +
                if is_coro_func(obj):
         
     | 
| 
      
 440 
     | 
    
         
            +
                    return (True, True)
         
     | 
| 
      
 441 
     | 
    
         
            +
                return (True, False)
         
     |