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,880 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: formaxapi
|
|
3
|
+
Version: 0.1.8
|
|
4
|
+
Summary: Class-based routing with dynamic Pydantic model generation for FastAPI
|
|
5
|
+
Author: EXO
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/siavashnouri/fastschema
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: fastapi>=0.100.0
|
|
20
|
+
Requires-Dist: pydantic>=2.0.0
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: classifier
|
|
23
|
+
Dynamic: description
|
|
24
|
+
Dynamic: description-content-type
|
|
25
|
+
Dynamic: license
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: project-url
|
|
28
|
+
Dynamic: requires-dist
|
|
29
|
+
Dynamic: requires-python
|
|
30
|
+
Dynamic: summary
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<img src="fastschema.png" alt="fastschema" width="400">
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
# fastschema
|
|
37
|
+
|
|
38
|
+
Class-based routing with dynamic Pydantic model generation for FastAPI.
|
|
39
|
+
|
|
40
|
+
[](https://www.python.org/downloads/)
|
|
41
|
+
[](https://fastapi.tiangolo.com/)
|
|
42
|
+
[](https://www.apache.org/licenses/LICENSE-2.0)
|
|
43
|
+
|
|
44
|
+
## Why fastschema?
|
|
45
|
+
|
|
46
|
+
Traditional FastAPI development scatters code across Pydantic models, route handlers, and router configuration. **fastschema** consolidates everything into a single class:
|
|
47
|
+
|
|
48
|
+
- **Routes inside the class** — defined with `@route` decorator
|
|
49
|
+
- **Typed input** — `UserRoute.schema("add")` auto-validates request body
|
|
50
|
+
- **Auto-discovered schemas** — no manual registration
|
|
51
|
+
- **SelfDerivedModel** — derive schemas from own fields for bulk operations
|
|
52
|
+
- **One-liner router** — `route_factory()` returns a FastAPI `APIRouter`
|
|
53
|
+
- **Works with any ORM** — Pydantic, Beanie, SQLAlchemy/SQLModel
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install fastschema
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 30-Second Demo
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from __future__ import annotations
|
|
65
|
+
from fastapi import FastAPI, Request
|
|
66
|
+
from fastschema import FieldConfig, RouteField, RouteBase, route, route_factory
|
|
67
|
+
|
|
68
|
+
app = FastAPI()
|
|
69
|
+
|
|
70
|
+
class Add(FieldConfig):
|
|
71
|
+
required = True
|
|
72
|
+
|
|
73
|
+
class Edit(FieldConfig):
|
|
74
|
+
default = None
|
|
75
|
+
|
|
76
|
+
class UserRoute(RouteBase):
|
|
77
|
+
name: str = RouteField(add=Add(), edit=Edit(), min_length=1, max_length=100)
|
|
78
|
+
email: str = RouteField(add=Add(), edit=Edit())
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
@route(path="/users", method="POST", status_code=201)
|
|
82
|
+
async def create_user(cls, request: Request, data: UserRoute.schema("add")):
|
|
83
|
+
return {"id": "1", "name": data.name}
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
@route(path="/users", method="GET")
|
|
87
|
+
async def get_users(cls, request: Request):
|
|
88
|
+
return {"users": []}
|
|
89
|
+
|
|
90
|
+
app.include_router(route_factory(UserRoute))
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Note:** Always use `from __future__ import annotations` at the top of your file.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Table of Contents
|
|
98
|
+
|
|
99
|
+
- [Installation](#installation)
|
|
100
|
+
- [Quick Start](#quick-start)
|
|
101
|
+
- [Core Concepts](#core-concepts)
|
|
102
|
+
- [FieldConfig](#1-fieldconfig---schema-type-configurations)
|
|
103
|
+
- [RouteField](#2-routefield---declarative-fields)
|
|
104
|
+
- [RouteBase](#3-routebase---base-class)
|
|
105
|
+
- [@route Decorator](#4-route-decorator)
|
|
106
|
+
- [route_factory](#5-route_factory)
|
|
107
|
+
- [Examples](#examples)
|
|
108
|
+
- [Raw Pydantic](#raw-pydantic)
|
|
109
|
+
- [Beanie (MongoDB)](#beanie-mongodb)
|
|
110
|
+
- [SQLModel (SQLAlchemy)](#sqlmodel-sqlalchemy)
|
|
111
|
+
- [Advanced Features](#advanced-features)
|
|
112
|
+
- [Chain](#chain---compose-functions)
|
|
113
|
+
- [SelfDerivedModel](#selfderivedmodel---bulk-operations)
|
|
114
|
+
- [ClassVar Fields](#classvar-fields)
|
|
115
|
+
- [from_schema()](#from_schema)
|
|
116
|
+
- [Validators](#validators)
|
|
117
|
+
- [Constraint Params](#constraint-params)
|
|
118
|
+
- [API Reference](#api-reference)
|
|
119
|
+
- [License](#license)
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Installation
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pip install fastschema
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Optional dependencies:**
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# For Beanie (MongoDB)
|
|
133
|
+
pip install beanie motor
|
|
134
|
+
|
|
135
|
+
# For SQLModel (SQLAlchemy)
|
|
136
|
+
pip install sqlmodel aiosqlite
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Quick Start
|
|
142
|
+
|
|
143
|
+
### 1. Define Schema Configs
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from fastschema import FieldConfig
|
|
147
|
+
|
|
148
|
+
class Add(FieldConfig):
|
|
149
|
+
required = True
|
|
150
|
+
|
|
151
|
+
class Edit(FieldConfig):
|
|
152
|
+
default = None
|
|
153
|
+
|
|
154
|
+
class Output(FieldConfig):
|
|
155
|
+
pass
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 2. Define Route Class
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from fastschema import RouteField, RouteBase, route
|
|
162
|
+
|
|
163
|
+
class UserRoute(RouteBase):
|
|
164
|
+
name: str = RouteField(add=Add(), edit=Edit(), output=Output(), min_length=1)
|
|
165
|
+
email: str = RouteField(add=Add(), edit=Edit(), output=Output())
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
@route(path="/users", method="POST", status_code=201)
|
|
169
|
+
async def create_user(cls, request: Request, data: UserRoute.schema("add")):
|
|
170
|
+
# data.name: str (required, min_length=1)
|
|
171
|
+
# data.email: str (required)
|
|
172
|
+
return {"id": "1", "name": data.name}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 3. Register Routes
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from fastapi import FastAPI
|
|
179
|
+
from fastschema import route_factory
|
|
180
|
+
|
|
181
|
+
app = FastAPI()
|
|
182
|
+
app.include_router(route_factory(UserRoute))
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Core Concepts
|
|
188
|
+
|
|
189
|
+
### 1. FieldConfig - Schema Type Configurations
|
|
190
|
+
|
|
191
|
+
`FieldConfig` defines how a field behaves in different schema contexts:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from fastschema import FieldConfig
|
|
195
|
+
|
|
196
|
+
class Add(FieldConfig):
|
|
197
|
+
required = True
|
|
198
|
+
|
|
199
|
+
class Edit(FieldConfig):
|
|
200
|
+
default = None
|
|
201
|
+
|
|
202
|
+
class Filter(FieldConfig):
|
|
203
|
+
default = None
|
|
204
|
+
|
|
205
|
+
class Output(FieldConfig):
|
|
206
|
+
pass
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
| Attribute | Type | Default | Description |
|
|
210
|
+
|-----------|------|---------|-------------|
|
|
211
|
+
| `required` | `bool` | `False` | No default — must be provided |
|
|
212
|
+
| `default` | `Any` | `_UNSET` | Default value |
|
|
213
|
+
| `default_factory` | `Callable` | `None` | Factory for mutable defaults |
|
|
214
|
+
| `alias` | `str` | `None` | Field alias |
|
|
215
|
+
| `description` | `str` | `None` | Field description |
|
|
216
|
+
| `type_override` | `type` | `None` | Override field type |
|
|
217
|
+
| `exclude` | `bool` | `False` | Exclude from this schema |
|
|
218
|
+
| `frozen` | `bool` | `False` | Immutable field |
|
|
219
|
+
| `apply_func` | `Callable` | `None` | Transform function |
|
|
220
|
+
| `before` | `bool` | `True` | Run apply_func before/after validators |
|
|
221
|
+
| `metadata` | `dict` | `None` | Extra kwargs for `pydantic.Field` |
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### 2. RouteField - Declarative Fields
|
|
226
|
+
|
|
227
|
+
`RouteField` inherits from Pydantic's `FieldInfo`, so it works everywhere:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from fastschema import RouteField, FieldConfig
|
|
231
|
+
|
|
232
|
+
class Add(FieldConfig):
|
|
233
|
+
required = True
|
|
234
|
+
|
|
235
|
+
# In RouteBase — schema configs work
|
|
236
|
+
class UserRoute(RouteBase):
|
|
237
|
+
title: str = RouteField(
|
|
238
|
+
add=Add(),
|
|
239
|
+
edit=Edit(),
|
|
240
|
+
alias="product_title",
|
|
241
|
+
description="The product title",
|
|
242
|
+
min_length=1,
|
|
243
|
+
max_length=200,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# In BaseModel — Pydantic Field params work
|
|
247
|
+
from pydantic import BaseModel
|
|
248
|
+
|
|
249
|
+
class MyModel(BaseModel):
|
|
250
|
+
title: str = RouteField(alias="x", description="test", min_length=1)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**All Pydantic Field params are supported:**
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
RouteField(
|
|
257
|
+
# Pydantic params
|
|
258
|
+
alias="x",
|
|
259
|
+
validation_alias="title",
|
|
260
|
+
description="Field description",
|
|
261
|
+
exclude=False,
|
|
262
|
+
frozen=False,
|
|
263
|
+
min_length=1,
|
|
264
|
+
max_length=100,
|
|
265
|
+
gt=0,
|
|
266
|
+
ge=0,
|
|
267
|
+
lt=100,
|
|
268
|
+
le=100,
|
|
269
|
+
pattern=r'^[A-Z]+$',
|
|
270
|
+
multiple_of=5,
|
|
271
|
+
json_schema_extra={"example": "hello"},
|
|
272
|
+
# Schema configs
|
|
273
|
+
add=Add(),
|
|
274
|
+
edit=Edit(),
|
|
275
|
+
output=Output(),
|
|
276
|
+
)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
### 3. RouteBase - Base Class
|
|
282
|
+
|
|
283
|
+
`RouteBase` provides schema generation and introspection:
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
class UserRoute(RouteBase):
|
|
287
|
+
name: str = RouteField(add=Add(), edit=Edit())
|
|
288
|
+
|
|
289
|
+
# Generate schemas
|
|
290
|
+
UserRoute.schema("add") # => name: str (required)
|
|
291
|
+
UserRoute.schema("edit") # => name: str | None (optional)
|
|
292
|
+
|
|
293
|
+
# Introspection
|
|
294
|
+
UserRoute.schema_types() # => ["add", "edit"]
|
|
295
|
+
UserRoute.schema_fields("add") # => ["name"]
|
|
296
|
+
UserRoute.all_fields() # => {"name": FieldInfo(...)}
|
|
297
|
+
UserRoute.field_names() # => ["name"]
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
### 4. @route Decorator
|
|
303
|
+
|
|
304
|
+
Define endpoints inside the class:
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
from fastschema import route
|
|
308
|
+
|
|
309
|
+
class UserRoute(RouteBase):
|
|
310
|
+
@classmethod
|
|
311
|
+
@route(
|
|
312
|
+
path="/users",
|
|
313
|
+
method="POST",
|
|
314
|
+
name="create_user",
|
|
315
|
+
description="Create a new user",
|
|
316
|
+
status_code=201,
|
|
317
|
+
tags=["users"],
|
|
318
|
+
)
|
|
319
|
+
async def create_user(cls, request: Request, data: UserRoute.schema("add")):
|
|
320
|
+
return {"id": "1"}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
| Parameter | Type | Default | Description |
|
|
324
|
+
|-----------|------|---------|-------------|
|
|
325
|
+
| `path` | `str` | *required* | URL path |
|
|
326
|
+
| `method` | `str` | `"GET"` | HTTP method |
|
|
327
|
+
| `name` | `str` | `None` | Route name |
|
|
328
|
+
| `description` | `str` | `None` | Route description |
|
|
329
|
+
| `status_code` | `int` | `200` | Response status code |
|
|
330
|
+
| `tags` | `list[str]` | `None` | OpenAPI tags |
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
### 5. route_factory
|
|
335
|
+
|
|
336
|
+
Collect all routes into a FastAPI router:
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
from fastschema import route_factory
|
|
340
|
+
|
|
341
|
+
router = route_factory(UserRoute, ProductRoute)
|
|
342
|
+
app.include_router(router)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Examples
|
|
348
|
+
|
|
349
|
+
### Raw Pydantic
|
|
350
|
+
|
|
351
|
+
Full example without any ORM:
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
from __future__ import annotations
|
|
355
|
+
from fastapi import FastAPI, Request
|
|
356
|
+
from fastschema import FieldConfig, RouteField, RouteBase, route, route_factory
|
|
357
|
+
from pydantic import BaseModel, field_validator
|
|
358
|
+
|
|
359
|
+
app = FastAPI()
|
|
360
|
+
|
|
361
|
+
# --- Configs ---
|
|
362
|
+
|
|
363
|
+
class Add(FieldConfig):
|
|
364
|
+
required = True
|
|
365
|
+
|
|
366
|
+
class Edit(FieldConfig):
|
|
367
|
+
default = None
|
|
368
|
+
|
|
369
|
+
class Output(FieldConfig):
|
|
370
|
+
pass
|
|
371
|
+
|
|
372
|
+
# --- Route Class ---
|
|
373
|
+
|
|
374
|
+
class UserRoute(RouteBase):
|
|
375
|
+
name: str = RouteField(
|
|
376
|
+
add=Add(),
|
|
377
|
+
edit=Edit(),
|
|
378
|
+
output=Output(),
|
|
379
|
+
min_length=1,
|
|
380
|
+
max_length=100,
|
|
381
|
+
)
|
|
382
|
+
email: str = RouteField(add=Add(), edit=Edit(), output=Output())
|
|
383
|
+
age: int = RouteField(add=Add(default=0), edit=Edit(), output=Output(), ge=0)
|
|
384
|
+
|
|
385
|
+
@classmethod
|
|
386
|
+
@route(path="/users", method="GET")
|
|
387
|
+
async def get_users(cls, request: Request):
|
|
388
|
+
return {"users": []}
|
|
389
|
+
|
|
390
|
+
@classmethod
|
|
391
|
+
@route(path="/users", method="POST", status_code=201)
|
|
392
|
+
async def create_user(cls, request: Request, data: UserRoute.schema("add")):
|
|
393
|
+
return {"id": "1", "name": data.name}
|
|
394
|
+
|
|
395
|
+
@classmethod
|
|
396
|
+
@route(path="/users/{user_id}", method="PUT")
|
|
397
|
+
async def update_user(cls, request: Request, user_id: str, data: UserRoute.schema("edit")):
|
|
398
|
+
return {"id": user_id}
|
|
399
|
+
|
|
400
|
+
@classmethod
|
|
401
|
+
@route(path="/users/{user_id}", method="DELETE")
|
|
402
|
+
async def delete_user(cls, request: Request, user_id: str):
|
|
403
|
+
return {"deleted": True}
|
|
404
|
+
|
|
405
|
+
app.include_router(route_factory(UserRoute))
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
### Beanie (MongoDB)
|
|
411
|
+
|
|
412
|
+
Full example with Beanie ODM:
|
|
413
|
+
|
|
414
|
+
```python
|
|
415
|
+
from __future__ import annotations
|
|
416
|
+
from fastapi import FastAPI, Request
|
|
417
|
+
from fastschema import FieldConfig, RouteField, RouteBase, route, route_factory
|
|
418
|
+
from beanie import Document, PydanticObjectId
|
|
419
|
+
from contextlib import asynccontextmanager
|
|
420
|
+
from pymongo import AsyncMongoClient
|
|
421
|
+
from beanie import init_beanie
|
|
422
|
+
from typing import ClassVar
|
|
423
|
+
|
|
424
|
+
app = FastAPI()
|
|
425
|
+
|
|
426
|
+
# --- Configs ---
|
|
427
|
+
|
|
428
|
+
class Add(FieldConfig):
|
|
429
|
+
required = True
|
|
430
|
+
|
|
431
|
+
class Edit(FieldConfig):
|
|
432
|
+
default = None
|
|
433
|
+
|
|
434
|
+
class Output(FieldConfig):
|
|
435
|
+
pass
|
|
436
|
+
|
|
437
|
+
class Get(FieldConfig):
|
|
438
|
+
default = None
|
|
439
|
+
|
|
440
|
+
# --- Document + Route Class ---
|
|
441
|
+
|
|
442
|
+
class UserRoute(RouteBase, Document):
|
|
443
|
+
name: str = RouteField(add=Add(), edit=Edit(), output=Output(), min_length=1)
|
|
444
|
+
email: str = RouteField(add=Add(), edit=Edit(), output=Output())
|
|
445
|
+
|
|
446
|
+
# ClassVar — not in DB, available for schema generation
|
|
447
|
+
token: ClassVar[str | None] = RouteField(get=Get(), add=Add(exclude=True))
|
|
448
|
+
|
|
449
|
+
class Settings:
|
|
450
|
+
name = "users"
|
|
451
|
+
|
|
452
|
+
@classmethod
|
|
453
|
+
@route(path="/users", method="GET")
|
|
454
|
+
async def get_users(cls, request: Request):
|
|
455
|
+
users = await cls.find_all().to_list()
|
|
456
|
+
return [{"id": str(u.id), "name": u.name} for u in users]
|
|
457
|
+
|
|
458
|
+
@classmethod
|
|
459
|
+
@route(path="/users", method="POST", status_code=201)
|
|
460
|
+
async def create_user(cls, request: Request, data: UserRoute.schema("add")):
|
|
461
|
+
user = cls.from_schema(data)
|
|
462
|
+
await user.insert()
|
|
463
|
+
return {"id": str(user.id), "name": user.name}
|
|
464
|
+
|
|
465
|
+
@classmethod
|
|
466
|
+
@route(path="/users/{user_id}", method="GET")
|
|
467
|
+
async def get_user(cls, request: Request, user_id: str):
|
|
468
|
+
user = await cls.get(PydanticObjectId(user_id))
|
|
469
|
+
if not user:
|
|
470
|
+
return {"error": "not found"}
|
|
471
|
+
return {"id": str(user.id), "name": user.name}
|
|
472
|
+
|
|
473
|
+
@classmethod
|
|
474
|
+
@route(path="/users/{user_id}", method="DELETE")
|
|
475
|
+
async def delete_user(cls, request: Request, user_id: str):
|
|
476
|
+
user = await cls.get(PydanticObjectId(user_id))
|
|
477
|
+
if user:
|
|
478
|
+
await user.delete()
|
|
479
|
+
return {"deleted": True}
|
|
480
|
+
|
|
481
|
+
# --- Lifespan ---
|
|
482
|
+
|
|
483
|
+
@asynccontextmanager
|
|
484
|
+
async def lifespan(app: FastAPI):
|
|
485
|
+
client = AsyncMongoClient("mongodb://localhost:27017")
|
|
486
|
+
await init_beanie(database=client["mydb"], document_models=[UserRoute])
|
|
487
|
+
yield
|
|
488
|
+
client.close()
|
|
489
|
+
|
|
490
|
+
app = FastAPI(lifespan=lifespan)
|
|
491
|
+
app.include_router(route_factory(UserRoute))
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
### SQLModel (SQLAlchemy)
|
|
497
|
+
|
|
498
|
+
Full example with SQLModel:
|
|
499
|
+
|
|
500
|
+
```python
|
|
501
|
+
from __future__ import annotations
|
|
502
|
+
from fastapi import FastAPI, Request
|
|
503
|
+
from fastschema import FieldConfig, RouteField, RouteBase, route, route_factory
|
|
504
|
+
from sqlmodel import SQLModel, Field, Session, create_engine
|
|
505
|
+
from typing import ClassVar
|
|
506
|
+
|
|
507
|
+
# --- Database ---
|
|
508
|
+
|
|
509
|
+
engine = create_engine("sqlite:///database.db")
|
|
510
|
+
|
|
511
|
+
# --- Configs ---
|
|
512
|
+
|
|
513
|
+
class Add(FieldConfig):
|
|
514
|
+
required = True
|
|
515
|
+
|
|
516
|
+
class Edit(FieldConfig):
|
|
517
|
+
default = None
|
|
518
|
+
|
|
519
|
+
class Output(FieldConfig):
|
|
520
|
+
pass
|
|
521
|
+
|
|
522
|
+
# --- Model + Route Class ---
|
|
523
|
+
|
|
524
|
+
class UserRoute(RouteBase, SQLModel, table=True):
|
|
525
|
+
__tablename__ = "users"
|
|
526
|
+
|
|
527
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
528
|
+
name: str = RouteField(
|
|
529
|
+
add=Add(),
|
|
530
|
+
edit=Edit(),
|
|
531
|
+
output=Output(),
|
|
532
|
+
min_length=1,
|
|
533
|
+
max_length=100,
|
|
534
|
+
)
|
|
535
|
+
email: str = RouteField(add=Add(), edit=Edit(), output=Output())
|
|
536
|
+
|
|
537
|
+
# ClassVar — not in DB, available for schema generation
|
|
538
|
+
token: ClassVar[str | None] = RouteField(
|
|
539
|
+
get=Get(),
|
|
540
|
+
add=Add(exclude=True),
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
@classmethod
|
|
544
|
+
@route(path="/users", method="GET")
|
|
545
|
+
async def get_users(cls, request: Request):
|
|
546
|
+
with Session(engine) as session:
|
|
547
|
+
users = session.exec(select(cls)).all()
|
|
548
|
+
return [{"id": u.id, "name": u.name} for u in users]
|
|
549
|
+
|
|
550
|
+
@classmethod
|
|
551
|
+
@route(path="/users", method="POST", status_code=201)
|
|
552
|
+
async def create_user(cls, request: Request, data: UserRoute.schema("add")):
|
|
553
|
+
user = cls.from_schema(data)
|
|
554
|
+
with Session(engine) as session:
|
|
555
|
+
session.add(user)
|
|
556
|
+
session.commit()
|
|
557
|
+
session.refresh(user)
|
|
558
|
+
return {"id": user.id, "name": user.name}
|
|
559
|
+
|
|
560
|
+
@classmethod
|
|
561
|
+
@route(path="/users/{user_id}", method="GET")
|
|
562
|
+
async def get_user(cls, request: Request, user_id: int):
|
|
563
|
+
with Session(engine) as session:
|
|
564
|
+
user = session.get(cls, user_id)
|
|
565
|
+
if not user:
|
|
566
|
+
return {"error": "not found"}
|
|
567
|
+
return {"id": user.id, "name": user.name}
|
|
568
|
+
|
|
569
|
+
@classmethod
|
|
570
|
+
@route(path="/users/{user_id}", method="DELETE")
|
|
571
|
+
async def delete_user(cls, request: Request, user_id: int):
|
|
572
|
+
with Session(engine) as session:
|
|
573
|
+
user = session.get(cls, user_id)
|
|
574
|
+
if user:
|
|
575
|
+
session.delete(user)
|
|
576
|
+
session.commit()
|
|
577
|
+
return {"deleted": True}
|
|
578
|
+
|
|
579
|
+
# --- Create tables and app ---
|
|
580
|
+
|
|
581
|
+
SQLModel.metadata.create_all(engine)
|
|
582
|
+
|
|
583
|
+
app = FastAPI()
|
|
584
|
+
app.include_router(route_factory(UserRoute))
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## Advanced Features
|
|
590
|
+
|
|
591
|
+
### Chain - Compose Functions
|
|
592
|
+
|
|
593
|
+
Chain multiple functions for `apply_func`:
|
|
594
|
+
|
|
595
|
+
```python
|
|
596
|
+
from fastschema import FieldConfig, RouteField, Chain
|
|
597
|
+
|
|
598
|
+
def strip(v):
|
|
599
|
+
return v.strip() if isinstance(v, str) else v
|
|
600
|
+
|
|
601
|
+
def upper(v):
|
|
602
|
+
return v.upper() if isinstance(v, str) else v
|
|
603
|
+
|
|
604
|
+
def remove_spaces(v):
|
|
605
|
+
return v.replace(" ", "_") if isinstance(v, str) else v
|
|
606
|
+
|
|
607
|
+
class Add(FieldConfig):
|
|
608
|
+
required = True
|
|
609
|
+
|
|
610
|
+
class ProductRoute(RouteBase):
|
|
611
|
+
title: str = RouteField(
|
|
612
|
+
add=Add(apply_func=Chain(strip, upper, remove_spaces)),
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# " hello world " → "hello_world"
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
### SelfDerivedModel - Bulk Operations
|
|
621
|
+
|
|
622
|
+
Derive a field's schema from the route's own fields:
|
|
623
|
+
|
|
624
|
+
```python
|
|
625
|
+
from fastschema import FieldConfig, RouteField, RouteBase, SelfDerivedModel
|
|
626
|
+
|
|
627
|
+
class Add(FieldConfig):
|
|
628
|
+
required = True
|
|
629
|
+
|
|
630
|
+
class Output(FieldConfig):
|
|
631
|
+
pass
|
|
632
|
+
|
|
633
|
+
class BulkAdd(FieldConfig):
|
|
634
|
+
required = True
|
|
635
|
+
|
|
636
|
+
class BulkEdit(FieldConfig):
|
|
637
|
+
default = None
|
|
638
|
+
|
|
639
|
+
class UserRoute(RouteBase):
|
|
640
|
+
name: str = RouteField(add=Add(), output=Output())
|
|
641
|
+
email: str = RouteField(add=Add(), output=Output())
|
|
642
|
+
|
|
643
|
+
items: list = RouteField(
|
|
644
|
+
bulk_add=BulkAdd(
|
|
645
|
+
default=SelfDerivedModel(schema="add", exclude_fields=["email"])
|
|
646
|
+
),
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
# UserRoute.schema("bulk_add") => items: list[name: str] (email excluded)
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
### ClassVar Fields
|
|
655
|
+
|
|
656
|
+
Use `ClassVar` for fields not in the database but available for schema generation:
|
|
657
|
+
|
|
658
|
+
```python
|
|
659
|
+
from typing import ClassVar
|
|
660
|
+
|
|
661
|
+
class UserRoute(RouteBase, Document):
|
|
662
|
+
name: str = RouteField(add=Add(), edit=Edit())
|
|
663
|
+
|
|
664
|
+
# ClassVar — not in DB, available for schema generation
|
|
665
|
+
token: ClassVar[str | None] = RouteField(get=Get(), add=Add(exclude=True))
|
|
666
|
+
|
|
667
|
+
# token is NOT in DB columns
|
|
668
|
+
# token IS in _fields for schema generation
|
|
669
|
+
# token appears in "get" schema, excluded from "add" schema
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
### from_schema()
|
|
675
|
+
|
|
676
|
+
Create Document instances without re-validating nested models:
|
|
677
|
+
|
|
678
|
+
```python
|
|
679
|
+
class UserRoute(RouteBase, Document):
|
|
680
|
+
name: str = RouteField(add=Add())
|
|
681
|
+
|
|
682
|
+
@classmethod
|
|
683
|
+
@route(path="/users", method="POST", status_code=201)
|
|
684
|
+
async def create_user(cls, request: Request, data: UserRoute.schema("add")):
|
|
685
|
+
# WRONG — re-validates nested models
|
|
686
|
+
# user = cls(**data.dict())
|
|
687
|
+
|
|
688
|
+
# CORRECT — preserves already-validated instances
|
|
689
|
+
user = cls.from_schema(data)
|
|
690
|
+
await user.insert()
|
|
691
|
+
return {"id": str(user.id)}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
### Validators
|
|
697
|
+
|
|
698
|
+
Validators defined on the route class are propagated to generated models:
|
|
699
|
+
|
|
700
|
+
```python
|
|
701
|
+
from pydantic import BaseModel, field_validator
|
|
702
|
+
|
|
703
|
+
class Data(BaseModel):
|
|
704
|
+
username: str
|
|
705
|
+
password: str
|
|
706
|
+
|
|
707
|
+
@field_validator("username", mode="before")
|
|
708
|
+
@classmethod
|
|
709
|
+
def validate_username(cls, v):
|
|
710
|
+
return v.strip()
|
|
711
|
+
|
|
712
|
+
class UserRoute(RouteBase, Document):
|
|
713
|
+
data: Data = RouteField(add=Add())
|
|
714
|
+
|
|
715
|
+
@field_validator("name", mode="before")
|
|
716
|
+
@classmethod
|
|
717
|
+
def validate_name(cls, v):
|
|
718
|
+
return v.strip()
|
|
719
|
+
|
|
720
|
+
# Both validators run when UserRoute.schema("add") is used
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
### Constraint Params
|
|
726
|
+
|
|
727
|
+
All Pydantic constraint params work in `RouteField`:
|
|
728
|
+
|
|
729
|
+
```python
|
|
730
|
+
class ProductRoute(RouteBase):
|
|
731
|
+
title: str = RouteField(add=Add(), min_length=1, max_length=200)
|
|
732
|
+
price: float = RouteField(add=Add(), gt=0, le=9999.99)
|
|
733
|
+
quantity: int = RouteField(add=Add(), ge=0, multiple_of=1)
|
|
734
|
+
sku: str = RouteField(add=Add(), pattern=r"^[A-Z]{3}-\d{4}$")
|
|
735
|
+
description: str = RouteField(add=Add(), max_length=5000)
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
---
|
|
739
|
+
|
|
740
|
+
## API Reference
|
|
741
|
+
|
|
742
|
+
### FieldConfig
|
|
743
|
+
|
|
744
|
+
```python
|
|
745
|
+
class FieldConfig:
|
|
746
|
+
required: bool = False
|
|
747
|
+
default: Any = _UNSET
|
|
748
|
+
default_factory: Callable | None = None
|
|
749
|
+
alias: str | None = None
|
|
750
|
+
description: str | None = None
|
|
751
|
+
type_override: type | None = None
|
|
752
|
+
exclude: bool = False
|
|
753
|
+
frozen: bool = False
|
|
754
|
+
apply_func: Callable | None = None
|
|
755
|
+
before: bool = True
|
|
756
|
+
metadata: dict | None = None
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### RouteField
|
|
760
|
+
|
|
761
|
+
```python
|
|
762
|
+
class RouteField(PydanticFieldInfo):
|
|
763
|
+
def __init__(
|
|
764
|
+
self,
|
|
765
|
+
default: Any = _UNSET,
|
|
766
|
+
*,
|
|
767
|
+
default_factory: Callable | None = None,
|
|
768
|
+
alias: str | None = None,
|
|
769
|
+
validation_alias: str | None = None,
|
|
770
|
+
serialization_alias: str | None = None,
|
|
771
|
+
description: str | None = None,
|
|
772
|
+
title: str | None = None,
|
|
773
|
+
exclude: bool = False,
|
|
774
|
+
frozen: bool = False,
|
|
775
|
+
deprecated: str | None = None,
|
|
776
|
+
json_schema_extra: dict | None = None,
|
|
777
|
+
validate_default: bool = False,
|
|
778
|
+
repr: bool = True,
|
|
779
|
+
**kwargs: Any, # Pydantic constraint params + schema configs
|
|
780
|
+
): ...
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
### RouteBase
|
|
784
|
+
|
|
785
|
+
```python
|
|
786
|
+
class RouteBase(BaseModel, metaclass=RouteMetaclass):
|
|
787
|
+
@classmethod
|
|
788
|
+
def schema(
|
|
789
|
+
cls,
|
|
790
|
+
schema_type: str,
|
|
791
|
+
*,
|
|
792
|
+
name: str | None = None,
|
|
793
|
+
include_fields: list[str] | None = None,
|
|
794
|
+
exclude_fields: list[str] | None = None,
|
|
795
|
+
forbid_extra: bool = True,
|
|
796
|
+
as_literal: bool = False,
|
|
797
|
+
) -> type[BaseModel]: ...
|
|
798
|
+
|
|
799
|
+
@classmethod
|
|
800
|
+
def Schema(cls, schema_type: str) -> type[BaseModel]: ...
|
|
801
|
+
|
|
802
|
+
@classmethod
|
|
803
|
+
def schema_fields(cls, schema_type: str) -> list[str]: ...
|
|
804
|
+
|
|
805
|
+
@classmethod
|
|
806
|
+
def all_fields(cls) -> dict[str, FieldInfo]: ...
|
|
807
|
+
|
|
808
|
+
@classmethod
|
|
809
|
+
def field_names(cls) -> list[str]: ...
|
|
810
|
+
|
|
811
|
+
@classmethod
|
|
812
|
+
def schema_types(cls) -> list[str]: ...
|
|
813
|
+
|
|
814
|
+
@classmethod
|
|
815
|
+
def from_schema(cls, data) -> Self: ...
|
|
816
|
+
|
|
817
|
+
@classmethod
|
|
818
|
+
def field_config_for(cls, field_name: str, schema_type: str) -> FieldConfig | None: ...
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
### SelfDerivedModel
|
|
822
|
+
|
|
823
|
+
```python
|
|
824
|
+
class SelfDerivedModel:
|
|
825
|
+
def __init__(
|
|
826
|
+
self,
|
|
827
|
+
schema: str,
|
|
828
|
+
is_optional: bool = True,
|
|
829
|
+
format: str = "model",
|
|
830
|
+
include_fields: list[str] | None = None,
|
|
831
|
+
exclude_fields: list[str] | None = None,
|
|
832
|
+
): ...
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
### route
|
|
836
|
+
|
|
837
|
+
```python
|
|
838
|
+
def route(
|
|
839
|
+
path: str,
|
|
840
|
+
method: Literal["GET", "POST", "PUT", "DELETE", "PATCH"] = "GET",
|
|
841
|
+
name: str | None = None,
|
|
842
|
+
description: str | None = None,
|
|
843
|
+
status_code: int = 200,
|
|
844
|
+
tags: list[str] | None = None,
|
|
845
|
+
) -> Callable: ...
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
### route_factory
|
|
849
|
+
|
|
850
|
+
```python
|
|
851
|
+
def route_factory(*route_classes: type) -> APIRouter: ...
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
### Chain
|
|
855
|
+
|
|
856
|
+
```python
|
|
857
|
+
class Chain:
|
|
858
|
+
def __init__(self, *funcs: Callable): ...
|
|
859
|
+
def __call__(self, v: Any) -> Any: ...
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
---
|
|
863
|
+
|
|
864
|
+
## Skills
|
|
865
|
+
|
|
866
|
+
fastschema includes a `SKILL.md` file for AI code agents (Codex, MiMo, etc.). This file provides agents with specialized knowledge to work effectively with the fastschema package.
|
|
867
|
+
|
|
868
|
+
**What it covers:**
|
|
869
|
+
- Core concepts (FieldConfig, RouteField, RouteBase, @route, route_factory)
|
|
870
|
+
- ORM integration patterns (SQLModel, Beanie)
|
|
871
|
+
- Advanced features (SelfDerivedModel, Chain, ClassVar, validators)
|
|
872
|
+
- Common CRUD and bulk operation patterns
|
|
873
|
+
|
|
874
|
+
**Usage:** Agents automatically discover and load the skill when working with fastschema-related tasks. The skill file is located at the project root alongside `README.md`.
|
|
875
|
+
|
|
876
|
+
---
|
|
877
|
+
|
|
878
|
+
## License
|
|
879
|
+
|
|
880
|
+
Apache License 2.0 — see [LICENSE](LICENSE) for details.
|