formaxapi 0.1.8__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 +19 -0
- fastschema/core/__init__.py +13 -0
- fastschema/core/field_config.py +49 -0
- fastschema/core/metaclass.py +96 -0
- fastschema/core/model_factory.py +189 -0
- fastschema/core/route_base.py +70 -0
- fastschema/core/route_decorator.py +70 -0
- fastschema/core/route_field.py +135 -0
- fastschema/core/self_derived.py +27 -0
- formaxapi/__init__.py +19 -0
- formaxapi/core/__init__.py +13 -0
- formaxapi/core/field_config.py +49 -0
- formaxapi/core/metaclass.py +96 -0
- formaxapi/core/model_factory.py +189 -0
- formaxapi/core/route_base.py +70 -0
- formaxapi/core/route_decorator.py +70 -0
- formaxapi/core/route_field.py +135 -0
- formaxapi/core/self_derived.py +27 -0
- formaxapi-0.1.8.dist-info/METADATA +880 -0
- formaxapi-0.1.8.dist-info/RECORD +41 -0
- formaxapi-0.1.8.dist-info/WHEEL +5 -0
- formaxapi-0.1.8.dist-info/licenses/LICENSE +201 -0
- formaxapi-0.1.8.dist-info/top_level.txt +1 -0
- modelrouter/__init__.py +19 -0
- modelrouter/core/__init__.py +13 -0
- modelrouter/core/field_config.py +49 -0
- modelrouter/core/metaclass.py +96 -0
- modelrouter/core/model_factory.py +189 -0
- modelrouter/core/route_base.py +70 -0
- modelrouter/core/route_decorator.py +70 -0
- modelrouter/core/route_field.py +135 -0
- modelrouter/core/self_derived.py +27 -0
- routex/__init__.py +19 -0
- routex/core/__init__.py +13 -0
- routex/core/field_config.py +49 -0
- routex/core/metaclass.py +96 -0
- routex/core/model_factory.py +189 -0
- routex/core/route_base.py +70 -0
- routex/core/route_decorator.py +70 -0
- routex/core/route_field.py +135 -0
- routex/core/self_derived.py +27 -0
|
@@ -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})"
|