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.
Files changed (41) hide show
  1. fastschema/__init__.py +19 -0
  2. fastschema/core/__init__.py +13 -0
  3. fastschema/core/field_config.py +49 -0
  4. fastschema/core/metaclass.py +96 -0
  5. fastschema/core/model_factory.py +189 -0
  6. fastschema/core/route_base.py +70 -0
  7. fastschema/core/route_decorator.py +70 -0
  8. fastschema/core/route_field.py +135 -0
  9. fastschema/core/self_derived.py +27 -0
  10. formaxapi/__init__.py +19 -0
  11. formaxapi/core/__init__.py +13 -0
  12. formaxapi/core/field_config.py +49 -0
  13. formaxapi/core/metaclass.py +96 -0
  14. formaxapi/core/model_factory.py +189 -0
  15. formaxapi/core/route_base.py +70 -0
  16. formaxapi/core/route_decorator.py +70 -0
  17. formaxapi/core/route_field.py +135 -0
  18. formaxapi/core/self_derived.py +27 -0
  19. formaxapi-0.1.8.dist-info/METADATA +880 -0
  20. formaxapi-0.1.8.dist-info/RECORD +41 -0
  21. formaxapi-0.1.8.dist-info/WHEEL +5 -0
  22. formaxapi-0.1.8.dist-info/licenses/LICENSE +201 -0
  23. formaxapi-0.1.8.dist-info/top_level.txt +1 -0
  24. modelrouter/__init__.py +19 -0
  25. modelrouter/core/__init__.py +13 -0
  26. modelrouter/core/field_config.py +49 -0
  27. modelrouter/core/metaclass.py +96 -0
  28. modelrouter/core/model_factory.py +189 -0
  29. modelrouter/core/route_base.py +70 -0
  30. modelrouter/core/route_decorator.py +70 -0
  31. modelrouter/core/route_field.py +135 -0
  32. modelrouter/core/self_derived.py +27 -0
  33. routex/__init__.py +19 -0
  34. routex/core/__init__.py +13 -0
  35. routex/core/field_config.py +49 -0
  36. routex/core/metaclass.py +96 -0
  37. routex/core/model_factory.py +189 -0
  38. routex/core/route_base.py +70 -0
  39. routex/core/route_decorator.py +70 -0
  40. routex/core/route_field.py +135 -0
  41. 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})"