fastschema 0.1.3__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.
fastschema/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """
2
+ framework - Class-based routing with dynamic Pydantic model generation for FastAPI.
3
+
4
+ Routes defined inside the class with @route decorator.
5
+ Typed input via UserRoute.schema() — request body auto-validated.
6
+ SelfDerivedModel for bulk operations — derive schemas from own fields.
7
+ """
8
+
9
+ from .core import (
10
+ FieldConfig, Chain, RouteField, FieldInfo, SelfDerivedModel,
11
+ RouteBase, ModelFactory, RouteMetaclass,
12
+ route, route_factory,
13
+ )
14
+
15
+ __all__ = [
16
+ 'FieldConfig', 'Chain', 'RouteField', 'FieldInfo', 'SelfDerivedModel',
17
+ 'RouteBase', 'ModelFactory', 'RouteMetaclass',
18
+ 'route', 'route_factory',
19
+ ]
@@ -0,0 +1,13 @@
1
+ from .field_config import FieldConfig, Chain
2
+ from .route_field import RouteField, FieldInfo
3
+ from .self_derived import SelfDerivedModel
4
+ from .route_base import RouteBase
5
+ from .model_factory import ModelFactory
6
+ from .metaclass import RouteMetaclass
7
+ from .route_decorator import route, route_factory
8
+
9
+ __all__ = [
10
+ 'FieldConfig', 'Chain', 'RouteField', 'FieldInfo', 'SelfDerivedModel',
11
+ 'RouteBase', 'ModelFactory', 'RouteMetaclass',
12
+ 'route', 'route_factory',
13
+ ]
@@ -0,0 +1,49 @@
1
+ """FieldConfig - Base class for schema type configurations."""
2
+
3
+ from __future__ import annotations
4
+ from typing import Any, Callable
5
+
6
+
7
+ _UNSET = object()
8
+
9
+
10
+ class Chain:
11
+ """Compose multiple functions to run sequentially on a field value."""
12
+
13
+ def __init__(self, *funcs: Callable):
14
+ self.funcs = funcs
15
+
16
+ def __call__(self, v: Any) -> Any:
17
+ result = v
18
+ for func in self.funcs:
19
+ result = func(result)
20
+ return result
21
+
22
+
23
+ class FieldConfig:
24
+ """Extend to define schema types."""
25
+
26
+ required: bool = False
27
+ default: Any = _UNSET
28
+ default_factory: Callable | None = None
29
+ alias: str | None = None
30
+ description: str | None = None
31
+ type_override: type | None = None
32
+ exclude: bool = False
33
+ frozen: bool = False
34
+ apply_func: Callable | None = None
35
+ before: bool = True # True = before validators, False = after validators
36
+ metadata: dict | None = None
37
+
38
+ def __init__(self, **overrides):
39
+ for key, value in overrides.items():
40
+ if hasattr(self, key) or key in self.__class__.__dict__:
41
+ setattr(self, key, value)
42
+
43
+ def __repr__(self):
44
+ attrs = {k: v for k, v in self.__dict__.items() if v is not _UNSET and v is not None and v is not False}
45
+ return f"{self.__name__}({attrs})"
46
+
47
+ @property
48
+ def __name__(self):
49
+ return self.__class__.__name__
@@ -0,0 +1,96 @@
1
+ """RouteMetaclass - Collects fields and discovers schema types."""
2
+
3
+ from __future__ import annotations
4
+ import sys
5
+ import typing
6
+ from typing import get_type_hints, ClassVar, get_origin, get_args
7
+ from .route_field import RouteField, FieldInfo
8
+
9
+ try:
10
+ from sqlmodel.main import SQLModelMetaclass as _BaseMetaclass
11
+ except ImportError:
12
+ from pydantic._internal._model_construction import ModelMetaclass as _BaseMetaclass
13
+
14
+
15
+ def _resolve_annotations(cls: type) -> dict[str, type]:
16
+ try:
17
+ module = sys.modules.get(cls.__module__)
18
+ if not module:
19
+ return {}
20
+ return get_type_hints(cls, localns=vars(module))
21
+ except Exception:
22
+ return getattr(cls, '__annotations__', {})
23
+
24
+
25
+ def _has_pydantic_base(bases: tuple) -> bool:
26
+ for base in bases:
27
+ if getattr(base, '__pydantic_complete__', False):
28
+ return True
29
+ if getattr(base, '__pydantic_validator__', None) is not None:
30
+ return True
31
+ return False
32
+
33
+
34
+ def _is_classvar(annotation) -> bool:
35
+ origin = get_origin(annotation)
36
+ if origin is ClassVar:
37
+ return True
38
+ if isinstance(annotation, str):
39
+ return 'ClassVar' in annotation
40
+ return False
41
+
42
+
43
+ def _unwrap_classvar(annotation):
44
+ if _is_classvar(annotation):
45
+ args = get_args(annotation)
46
+ if args:
47
+ return args[0]
48
+ return str
49
+ return annotation
50
+
51
+
52
+ class RouteMetaclass(_BaseMetaclass):
53
+ def __new__(mcs, cls_name, bases, namespace, **kwargs):
54
+ if _has_pydantic_base(bases):
55
+ cls = super().__new__(mcs, cls_name, bases, namespace, **kwargs)
56
+ else:
57
+ cls = type.__new__(mcs, cls_name, bases, namespace)
58
+
59
+ fields = {}
60
+ for base in bases:
61
+ bf = getattr(base, '_fields', None)
62
+ if bf:
63
+ fields.update(bf)
64
+
65
+ resolved = _resolve_annotations(cls)
66
+
67
+ # Collect RouteField entries
68
+ for attr_name, annotation in resolved.items():
69
+ attr_value = namespace.get(attr_name)
70
+ if isinstance(attr_value, RouteField):
71
+ # Unwrap ClassVar[str] -> str for the schema
72
+ field_type = _unwrap_classvar(annotation) if _is_classvar(annotation) else annotation
73
+ fields[attr_name] = attr_value.to_field_info(attr_name, field_type)
74
+
75
+ # Collect ClassVar fields (not RouteField, but user wants them in schemas)
76
+ for attr_name, annotation in resolved.items():
77
+ if attr_name in fields:
78
+ continue
79
+ if _is_classvar(annotation) and attr_name in namespace:
80
+ inner_type = _unwrap_classvar(annotation)
81
+ field_info = FieldInfo(
82
+ name=attr_name,
83
+ annotation=inner_type,
84
+ default=namespace.get(attr_name, None),
85
+ configs={},
86
+ )
87
+ fields[attr_name] = field_info
88
+
89
+ cls._fields = fields
90
+
91
+ schema_types = set()
92
+ for info in fields.values():
93
+ schema_types.update(info.configs.keys())
94
+ cls._schema_types = schema_types
95
+
96
+ return cls
@@ -0,0 +1,189 @@
1
+ """ModelFactory - Generates Pydantic models from FieldInfo + schema type."""
2
+
3
+ from __future__ import annotations
4
+ from types import NoneType
5
+ from typing import Literal
6
+ from pydantic import BaseModel, Field, create_model, ConfigDict
7
+ from pydantic import field_validator as pv_field_validator
8
+ from pydantic import model_validator as pv_model_validator
9
+ from .route_field import FieldInfo
10
+ from .self_derived import SelfDerivedModel
11
+ from .field_config import _UNSET
12
+
13
+
14
+ class ModelFactory:
15
+
16
+ @staticmethod
17
+ def create(fields, schema_type, model_name="GeneratedModel",
18
+ forbid_extra=True, include_fields=None, exclude_fields=None,
19
+ as_literal=False, route_cls=None) -> type[BaseModel]:
20
+ skip = set(exclude_fields or [])
21
+ only = set(include_fields) if include_fields else None
22
+
23
+ if as_literal:
24
+ names = [n for n, info in fields.items()
25
+ if n not in skip and (only is None or n in only)
26
+ and info.has_config(schema_type)
27
+ and not info.get_config(schema_type).exclude]
28
+ return Literal[tuple(names)]
29
+
30
+ model_fields = {}
31
+ apply_funcs = {}
32
+ for name, info in fields.items():
33
+ if name in skip or (only is not None and name not in only):
34
+ continue
35
+ config = info.get_config(schema_type)
36
+ if config is None or config.exclude:
37
+ continue
38
+
39
+ ftype = config.type_override or info.annotation
40
+
41
+ if isinstance(ftype, str):
42
+ import typing
43
+ ftype = typing._eval_type(typing.ForwardRef(ftype), globals(), None)
44
+
45
+ fdefault = ModelFactory._resolve_default(config, info)
46
+
47
+ # Resolve SelfDerivedModel
48
+ if isinstance(fdefault, SelfDerivedModel) and route_cls:
49
+ ftype, fdefault = ModelFactory._resolve_self_derived(fdefault, route_cls)
50
+
51
+ if fdefault is not None and fdefault is not ...:
52
+ if NoneType not in (getattr(ftype, '__args__', ()) or ()):
53
+ ftype = ftype | None
54
+
55
+ # Start with RouteField's Pydantic Field kwargs
56
+ fk = dict(info.field_info_kwargs) if info.field_info_kwargs else {}
57
+
58
+ # Remove 'default' from kwargs — we pass it as positional arg
59
+ fk.pop('default', None)
60
+ fk.pop('default_factory', None)
61
+
62
+ # Config-level overrides take precedence
63
+ if info.alias and 'alias' not in fk:
64
+ fk['alias'] = info.alias
65
+ desc = config.description or info.description
66
+ if desc and 'description' not in fk:
67
+ fk['description'] = desc
68
+ if config.frozen and 'frozen' not in fk:
69
+ fk['frozen'] = True
70
+ if config.metadata:
71
+ fk.update(config.metadata)
72
+
73
+ # Pass constraint kwargs (max_length, min_length, gt, etc.) from RouteField
74
+ field_metadata = info.metadata if hasattr(info, 'metadata') and info.metadata else None
75
+ if field_metadata and isinstance(field_metadata, dict):
76
+ fk.update(field_metadata)
77
+
78
+ model_fields[name] = (ftype, Field(fdefault, **fk))
79
+
80
+ if config.apply_func is not None:
81
+ func = config.apply_func
82
+ if hasattr(func, '__func__'):
83
+ func = func.__func__
84
+ apply_funcs[name] = (func, 'before' if config.before else 'after')
85
+
86
+ validator_base = ModelFactory._create_validator_base(route_cls, fields, schema_type, apply_funcs)
87
+ model_module = route_cls.__module__ if route_cls else __name__
88
+
89
+ if validator_base:
90
+ model = create_model(
91
+ model_name,
92
+ __base__=validator_base,
93
+ __config__=ConfigDict(extra='forbid' if forbid_extra else 'ignore'),
94
+ __module__=model_module,
95
+ **model_fields,
96
+ )
97
+ else:
98
+ model = create_model(
99
+ model_name,
100
+ __config__=ConfigDict(extra='forbid' if forbid_extra else 'ignore'),
101
+ __module__=model_module,
102
+ **model_fields,
103
+ )
104
+
105
+ try:
106
+ model.model_rebuild(force=True)
107
+ except Exception:
108
+ pass
109
+
110
+ return model
111
+
112
+ @staticmethod
113
+ def _create_validator_base(route_cls, fields, schema_type, apply_funcs=None):
114
+ """Create a base class with validators from route_cls and apply_func."""
115
+ apply_funcs = apply_funcs or {}
116
+ namespace = {}
117
+
118
+ # Collect validators from route_cls
119
+ if route_cls and getattr(route_cls, '__pydantic_complete__', False):
120
+ decorators = getattr(route_cls, '__pydantic_decorators__', None)
121
+ if decorators:
122
+ for name, dec in decorators.field_validators.items():
123
+ func = getattr(route_cls, name, None)
124
+ if func is None:
125
+ continue
126
+ raw_func = getattr(func, '__func__', func)
127
+ mode = dec.info.mode or 'before'
128
+ for field_name in dec.info.fields:
129
+ validator_name = f'_validator_{field_name}_{mode}'
130
+ namespace[validator_name] = pv_field_validator(field_name, mode=mode, check_fields=False)(raw_func)
131
+
132
+ for name, dec in decorators.model_validators.items():
133
+ if name == 'fill_back_refs':
134
+ continue
135
+ func = getattr(route_cls, name, None)
136
+ if func is None:
137
+ continue
138
+ raw_func = getattr(func, '__func__', func)
139
+ namespace[f'_model_validator_{name}'] = pv_model_validator(mode=dec.info.mode)(raw_func)
140
+
141
+ # Create validators from apply_func
142
+ for field_name, (func, mode) in apply_funcs.items():
143
+ validator_name = f'_apply_func_{field_name}_{mode}'
144
+ def _make_validator(f):
145
+ def validator(cls, v):
146
+ return f(v)
147
+ return validator
148
+ v = _make_validator(func)
149
+ v.__qualname__ = validator_name
150
+ namespace[validator_name] = pv_field_validator(field_name, mode=mode, check_fields=False)(v)
151
+
152
+ if namespace:
153
+ base_name = f'{route_cls.__name__}Validators' if route_cls else 'Validators'
154
+ return type(base_name, (BaseModel,), namespace)
155
+ return None
156
+
157
+ @staticmethod
158
+ def _resolve_default(config, info):
159
+ if config.default is not _UNSET:
160
+ return config.default
161
+ if config.default_factory is not None:
162
+ return config.default_factory
163
+ if info.default is not _UNSET:
164
+ return info.default
165
+ if info.default_factory is not None:
166
+ return info.default_factory
167
+ if config.required:
168
+ return ...
169
+ return ...
170
+
171
+ @staticmethod
172
+ def _resolve_self_derived(sdm, route_cls):
173
+ derived = route_cls.schema(
174
+ sdm.schema,
175
+ include_fields=sdm.include_fields,
176
+ exclude_fields=sdm.exclude_fields,
177
+ )
178
+ field_type = list[derived]
179
+ default = [] if not sdm.is_optional else None
180
+ return field_type, default
181
+
182
+ @staticmethod
183
+ def field_names(fields, schema_type, include=None, exclude=None):
184
+ skip = set(exclude or [])
185
+ only = set(include) if include else None
186
+ return [n for n, info in fields.items()
187
+ if n not in skip and (only is None or n in only)
188
+ and info.has_config(schema_type)
189
+ and not info.get_config(schema_type).exclude]
@@ -0,0 +1,70 @@
1
+ """RouteBase - Base class with schema generation."""
2
+
3
+ from __future__ import annotations
4
+ from typing import ClassVar
5
+ from .metaclass import RouteMetaclass
6
+ from .route_field import FieldInfo, RouteField
7
+ from .field_config import FieldConfig
8
+ from .model_factory import ModelFactory
9
+ from .self_derived import SelfDerivedModel
10
+ from pydantic import BaseModel
11
+
12
+
13
+ class RouteBase(BaseModel,metaclass=RouteMetaclass):
14
+ """Base class. Routes defined inside with @route decorator.
15
+
16
+ class UserRoute(RouteBase):
17
+ name: str = RouteField(add=Add(), edit=Edit())
18
+
19
+ @route(path="/users", method="POST")
20
+ async def create_user(cls, request, data: UserRoute.schema("add")):
21
+ ...
22
+ """
23
+
24
+ _fields: ClassVar[dict[str, FieldInfo]] = {}
25
+ _schema_types: ClassVar[set[str]] = set()
26
+ _route_group: ClassVar[str] = ""
27
+
28
+ @classmethod
29
+ def schema(cls, schema_type: str, *, name: str | None = None,
30
+ include_fields: list[str] | None = None,
31
+ exclude_fields: list[str] | None = None,
32
+ forbid_extra: bool = True, as_literal: bool = False) -> type[BaseModel]:
33
+ """Generate a Pydantic model for the given schema type."""
34
+ if schema_type not in cls._schema_types:
35
+ raise ValueError(
36
+ f"Unknown schema type '{schema_type}'. Available: {sorted(cls._schema_types)}"
37
+ )
38
+ model_name = name or f"{cls.__name__}_{schema_type.title()}"
39
+ return ModelFactory.create(
40
+ fields=cls._fields, schema_type=schema_type,
41
+ model_name=model_name, forbid_extra=forbid_extra,
42
+ include_fields=include_fields, exclude_fields=exclude_fields,
43
+ as_literal=as_literal, route_cls=cls,
44
+ )
45
+
46
+ @classmethod
47
+ def schema_fields(cls, schema_type: str) -> list[str]:
48
+ return ModelFactory.field_names(cls._fields, schema_type)
49
+
50
+ @classmethod
51
+ def all_fields(cls) -> dict[str, FieldInfo]:
52
+ return dict(cls._fields)
53
+
54
+ @classmethod
55
+ def field_names(cls) -> list[str]:
56
+ return list(cls._fields.keys())
57
+
58
+ @classmethod
59
+ def schema_types(cls) -> list[str]:
60
+ return sorted(cls._schema_types)
61
+
62
+ @classmethod
63
+ def from_schema(cls, data):
64
+ """Create an instance from a schema model without re-validating nested models."""
65
+ return cls.model_validate(data, from_attributes=True)
66
+
67
+ @classmethod
68
+ def field_config_for(cls, field_name: str, schema_type: str) -> FieldConfig | None:
69
+ info = cls._fields.get(field_name)
70
+ return info.get_config(schema_type) if info else None
@@ -0,0 +1,70 @@
1
+ """route decorator and route_factory."""
2
+
3
+ from __future__ import annotations
4
+ from typing import Callable, Literal
5
+ from fastapi import APIRouter, status
6
+
7
+
8
+ class _RouteInfo:
9
+ __slots__ = ('path', 'method', 'name', 'description', 'status_code', 'tags')
10
+
11
+ def __init__(self, path, method, name, description, status_code, tags):
12
+ self.path = path
13
+ self.method = method
14
+ self.name = name
15
+ self.description = description
16
+ self.status_code = status_code
17
+ self.tags = tags
18
+
19
+
20
+ def route(
21
+ path: str,
22
+ method: Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] = 'GET',
23
+ name: str | None = None,
24
+ description: str | None = None,
25
+ status_code: int = status.HTTP_200_OK,
26
+ tags: list[str] | None = None,
27
+ ) -> Callable:
28
+ """Decorator to mark a method as a route endpoint."""
29
+ def decorator(func: Callable) -> Callable:
30
+ func._route_info = _RouteInfo(
31
+ path=path, method=method,
32
+ name=name or func.__name__,
33
+ description=description,
34
+ status_code=status_code,
35
+ tags=tags or [],
36
+ )
37
+ return func
38
+ return decorator
39
+
40
+
41
+ def route_factory(*route_classes: type) -> APIRouter:
42
+ """Collect all @route methods from Route classes into a FastAPI APIRouter."""
43
+ router = APIRouter()
44
+
45
+ for cls in route_classes:
46
+ default_tags = [getattr(cls, '_route_group', None) or cls.__name__]
47
+
48
+ for attr_name in dir(cls):
49
+ attr = getattr(cls, attr_name, None)
50
+ if attr is None:
51
+ continue
52
+
53
+ func = getattr(attr, '__func__', attr)
54
+ info = getattr(func, '_route_info', None)
55
+ if info is None:
56
+ continue
57
+
58
+ tags = info.tags if info.tags else default_tags
59
+
60
+ router.add_api_route(
61
+ path=info.path,
62
+ endpoint=attr,
63
+ methods=[info.method],
64
+ name=info.name,
65
+ description=info.description,
66
+ status_code=info.status_code,
67
+ tags=tags,
68
+ )
69
+
70
+ return router
@@ -0,0 +1,135 @@
1
+ """RouteField and FieldInfo."""
2
+
3
+ from __future__ import annotations
4
+ from typing import Any, Callable
5
+ from pydantic.fields import FieldInfo as PydanticFieldInfo
6
+ from .field_config import FieldConfig, _UNSET
7
+
8
+
9
+ # Pydantic FieldInfo attribute names (to separate from schema_configs)
10
+ _PYDANTIC_FIELD_ATTRS = frozenset(PydanticFieldInfo.__slots__)
11
+
12
+
13
+ class FieldInfo:
14
+ __slots__ = ('name', 'annotation', 'default', 'default_factory', 'configs',
15
+ 'db_field', 'alias', 'description', 'field_info_kwargs', 'metadata')
16
+
17
+ def __init__(self, name, annotation, default=_UNSET, default_factory=None,
18
+ configs=None, db_field=None, alias=None, description=None,
19
+ field_info_kwargs=None, metadata=None):
20
+ self.name = name
21
+ self.annotation = annotation
22
+ self.default = default
23
+ self.default_factory = default_factory
24
+ self.configs = configs or {}
25
+ self.db_field = db_field or alias or name
26
+ self.alias = alias
27
+ self.description = description
28
+ self.field_info_kwargs = field_info_kwargs or {}
29
+ self.metadata = metadata
30
+
31
+ def get_config(self, schema_type: str) -> FieldConfig | None:
32
+ return self.configs.get(schema_type)
33
+
34
+ def has_config(self, schema_type: str) -> bool:
35
+ return schema_type in self.configs
36
+
37
+
38
+ class RouteField(PydanticFieldInfo):
39
+ """Declarative field with per-schema configs.
40
+
41
+ Inherits from Pydantic's FieldInfo, so it works in both RouteBase and BaseModel:
42
+
43
+ # In RouteBase — schema configs work
44
+ class UserRoute(RouteBase):
45
+ title: str = RouteField(add=Add(), edit=Edit())
46
+
47
+ # In BaseModel — Pydantic Field params work
48
+ class MyModel(BaseModel):
49
+ title: str = RouteField(alias="x", description="test")
50
+ """
51
+
52
+ def __init__(self, default: Any = _UNSET, *, default_factory: Callable | None = None,
53
+ alias: str | None = None, validation_alias: str | None = None,
54
+ serialization_alias: str | None = None,
55
+ description: str | None = None, title: str | None = None,
56
+ exclude: bool = False, frozen: bool = False,
57
+ deprecated: str | None = None,
58
+ json_schema_extra: dict | None = None,
59
+ validate_default: bool = False,
60
+ repr: bool = True,
61
+ **kwargs: Any):
62
+ # Extract schema_configs (FieldConfig instances) from all kwargs
63
+ self.schema_configs = {k: v for k, v in kwargs.items() if isinstance(v, FieldConfig)}
64
+
65
+ # Build kwargs for Pydantic FieldInfo
66
+ # These are the params Pydantic FieldInfo stores as direct attributes
67
+ pydantic_kwargs = {}
68
+ if default is not _UNSET:
69
+ pydantic_kwargs['default'] = default
70
+ if default_factory is not None:
71
+ pydantic_kwargs['default_factory'] = default_factory
72
+ if alias is not None:
73
+ pydantic_kwargs['alias'] = alias
74
+ if validation_alias is not None:
75
+ pydantic_kwargs['validation_alias'] = validation_alias
76
+ if serialization_alias is not None:
77
+ pydantic_kwargs['serialization_alias'] = serialization_alias
78
+ if description is not None:
79
+ pydantic_kwargs['description'] = description
80
+ if title is not None:
81
+ pydantic_kwargs['title'] = title
82
+ if exclude:
83
+ pydantic_kwargs['exclude'] = exclude
84
+ if frozen:
85
+ pydantic_kwargs['frozen'] = frozen
86
+ if deprecated is not None:
87
+ pydantic_kwargs['deprecated'] = deprecated
88
+ if json_schema_extra is not None:
89
+ pydantic_kwargs['json_schema_extra'] = json_schema_extra
90
+ if validate_default:
91
+ pydantic_kwargs['validate_default'] = validate_default
92
+ if not repr:
93
+ pydantic_kwargs['repr'] = repr
94
+
95
+ # Pass constraint kwargs (min_length, max_length, gt, ge, lt, le, pattern, etc.)
96
+ # These are stored in metadata by Pydantic
97
+ for k, v in kwargs.items():
98
+ if k not in self.schema_configs and k not in pydantic_kwargs:
99
+ pydantic_kwargs[k] = v
100
+
101
+ super().__init__(**pydantic_kwargs)
102
+
103
+ def to_field_info(self, name: str, annotation: type) -> FieldInfo:
104
+ return FieldInfo(
105
+ name=name, annotation=annotation,
106
+ default=self.default, default_factory=self.default_factory,
107
+ configs=self.schema_configs,
108
+ alias=getattr(self, 'alias', None),
109
+ description=getattr(self, 'description', None),
110
+ field_info_kwargs=self._field_info_kwargs(),
111
+ metadata=self._constraint_kwargs(),
112
+ )
113
+
114
+ def _field_info_kwargs(self) -> dict:
115
+ kwargs = {}
116
+ # Only include public FieldInfo attributes, skip internal ones
117
+ _skip = {'_original_assignment', '_attributes_set', '_original_annotation',
118
+ '_qualifiers', '_complete', '_final', 'metadata'}
119
+ for attr in _PYDANTIC_FIELD_ATTRS:
120
+ if attr in _skip:
121
+ continue
122
+ val = getattr(self, attr, None)
123
+ if val is not None and val is not False and val != []:
124
+ kwargs[attr] = val
125
+ return kwargs
126
+
127
+ def _constraint_kwargs(self) -> dict:
128
+ """Return constraint kwargs (max_length, min_length, gt, ge, lt, le, etc.) for Field()."""
129
+ constraints = {}
130
+ for item in (self.metadata or []):
131
+ for attr in ('max_length', 'min_length', 'gt', 'ge', 'lt', 'le',
132
+ 'multiple_of', 'pattern'):
133
+ if hasattr(item, attr):
134
+ constraints[attr] = getattr(item, attr)
135
+ return constraints
@@ -0,0 +1,27 @@
1
+ """SelfDerivedModel - Derive a field's schema from the route's own fields."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class SelfDerivedModel:
7
+ """Metadata for a field whose schema is derived from the route's own fields.
8
+
9
+ class UserRoute(RouteBase):
10
+ name: str = RouteField(add=Add(), edit=Edit())
11
+ items: list = RouteField(
12
+ bulk_add=BulkAddConfig(
13
+ default=SelfDerivedModel(schema='add', exclude_fields=['email'])
14
+ )
15
+ )
16
+ """
17
+
18
+ def __init__(self, schema: str, is_optional: bool = True, format: str = 'model',
19
+ include_fields: list[str] | None = None, exclude_fields: list[str] | None = None):
20
+ self.schema = schema
21
+ self.is_optional = is_optional
22
+ self.format = format
23
+ self.include_fields = include_fields
24
+ self.exclude_fields = exclude_fields
25
+
26
+ def __repr__(self):
27
+ return f"SelfDerivedModel(schema={self.schema!r}, include={self.include_fields}, exclude={self.exclude_fields})"