python-flashapi 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- python_flashapi-0.4.0/.release-please-manifest.json +3 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/CHANGELOG.md +16 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/PKG-INFO +15 -4
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/README.md +14 -3
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/custom-logic.md +41 -2
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/pyproject.toml +1 -1
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/release-please-config.json +1 -1
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/adapters/django.py +35 -1
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/adapters/fastapi.py +15 -1
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/adapters/flask.py +15 -1
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/core/custom_routes.py +24 -2
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/core/schema.py +4 -1
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/core/visibility.py +36 -0
- python_flashapi-0.4.0/src/flashapi/features/cache.py +229 -0
- python_flashapi-0.4.0/src/flashapi/features/idempotency.py +375 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/storage/orm.py +3 -0
- python_flashapi-0.4.0/tests/test_features/test_field_selection.py +127 -0
- python_flashapi-0.3.0/.release-please-manifest.json +0 -3
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/.github/workflows/ci.yml +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/.github/workflows/release-please.yml +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/.gitignore +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/LICENSE +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/NOTICE +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/authentication.md +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/customization.md +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/examples.md +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/features.md +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/framework-notes.md +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/integration.md +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/logo.svg +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/logo1.svg +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/logo2.svg +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/relations.md +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/examples/fastapi_example.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/examples/flask_example.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/__init__.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/adapters/__init__.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/adapters/base.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/core/__init__.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/core/pluralize.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/core/relations.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/core/response.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/django.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/docs/__init__.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/docs/openapi.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/fastapi.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/__init__.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/audit.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/auth.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/dashboard.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/export.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/filtering.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/health.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/pagination.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/rate_limit.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/search.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/sorting.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/webhooks.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/websocket.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/flask.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/__init__.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/base.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/dataclass.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/detect.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/django.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/pydantic.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/sqlalchemy.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/py.typed +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/storage/__init__.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/storage/auto.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/storage/base.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/storage/sqlalchemy.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/__init__.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_adapters/__init__.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_adapters/test_fastapi.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_adapters/test_flask.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_adapters/test_relations.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_adapters/test_soft_delete.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_core/__init__.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_core/test_pluralize.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_core/test_response.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_core/test_schema.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_core/test_visibility.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/__init__.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_audit.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_auth.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_dashboard.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_export.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_filtering.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_pagination.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_rate_limit.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_search.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_sorting.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_websocket.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_health.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_inspectors/__init__.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_inspectors/test_dataclass.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_inspectors/test_detect.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_inspectors/test_pydantic.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_storage/__init__.py +0 -0
- {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_storage/test_auto.py +0 -0
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0](https://github.com/HackermanMe/flashapi/compare/v0.3.0...v0.4.0) (2026-09-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add field selection (?fields=id,name,price) ([4c07e45](https://github.com/HackermanMe/flashapi/commit/4c07e4536ab442f3a984ec8108154ef47f361098))
|
|
9
|
+
* add idempotency keys, cache layer, and currentUserField auto-injection ([141ae08](https://github.com/HackermanMe/flashapi/commit/141ae084afd4d605540a0cead5c4aac9cd6c41f6))
|
|
10
|
+
* add interactive CRUD dashboard with HTMX and real-time WebSocket ([1b1b4a5](https://github.com/HackermanMe/flashapi/commit/1b1b4a5c9924db5858119a69fb9c2585c687fe6d))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **django:** convert URL path params to OpenAPI format in discover_django_views ([78e5675](https://github.com/HackermanMe/flashapi/commit/78e5675b8fafe0a87a7711f30f9b3db26bfd9dd6))
|
|
16
|
+
* simplify nested if statement for ruff linter ([73fb95b](https://github.com/HackermanMe/flashapi/commit/73fb95bdd7282141081a85d0f44d6d8c4478967e))
|
|
17
|
+
* sync release-please manifest and README version to 0.3.0 ([1538199](https://github.com/HackermanMe/flashapi/commit/1538199c6f9edc3fb4be9a17d9b69c67838a90bb))
|
|
18
|
+
|
|
3
19
|
## [0.3.0](https://github.com/HackermanMe/flashapi/compare/v0.2.0...v0.3.0) (2026-08-08)
|
|
4
20
|
|
|
5
21
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-flashapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Define your models. FlashAPI does the rest.
|
|
5
5
|
Project-URL: Homepage, https://github.com/HackermanMe/flashapi
|
|
6
6
|
Project-URL: Documentation, https://github.com/HackermanMe/flashapi#readme
|
|
@@ -64,10 +64,12 @@ Description-Content-Type: text/markdown
|
|
|
64
64
|
|
|
65
65
|
---
|
|
66
66
|
|
|
67
|
-
FlashAPI generates a full REST API with CRUD, pagination, filtering, sorting, full-text search, relations, soft delete, bulk operations, export, audit trail, webhooks, rate limiting, and a live dashboard — from your existing models, in one line.
|
|
67
|
+
FlashAPI generates a full REST API with CRUD, pagination, filtering, sorting, full-text search, relations, soft delete, bulk operations, export, audit trail, webhooks, rate limiting, **idempotency keys**, **cache layer**, **currentUser auto-injection**, and a live dashboard — from your existing models, in one line.
|
|
68
68
|
|
|
69
69
|
Part of the **FlashAPI Ecosystem** — ensuring SDK client compatibility across all backends (Python, Java Spring, Node.js).
|
|
70
70
|
|
|
71
|
+
**New in 0.4.0:** Idempotency keys (prevent double-click/retry duplicates), cache layer with graceful fallback, currentUserField auto-injection, and audit enabled by default.
|
|
72
|
+
|
|
71
73
|
---
|
|
72
74
|
|
|
73
75
|
## Documentation
|
|
@@ -89,7 +91,7 @@ Part of the **FlashAPI Ecosystem** — ensuring SDK client compatibility across
|
|
|
89
91
|
|
|
90
92
|
```bash
|
|
91
93
|
# x-release-please-start-version
|
|
92
|
-
pip install python-flashapi==0.
|
|
94
|
+
pip install python-flashapi==0.3.0
|
|
93
95
|
# x-release-please-end
|
|
94
96
|
|
|
95
97
|
# With framework extras:
|
|
@@ -231,8 +233,9 @@ FlashAPI(
|
|
|
231
233
|
Model(Config, readonly=True, access="admin"), # GET only, admin
|
|
232
234
|
Model(Log, only=["list"]), # List only
|
|
233
235
|
Model(Animal, plural="animaux"), # Custom plural
|
|
234
|
-
Model(Invoice, soft_delete=True, audit=True), # Opt-in features
|
|
236
|
+
Model(Invoice, soft_delete=True, audit=True), # Opt-in features (audit=True by default)
|
|
235
237
|
Model(Eleve, access="staff", scope="tenant", tenant_field="ecole_id"),
|
|
238
|
+
Model(Post, current_user_field="author"), # Auto-inject authenticated user on create
|
|
236
239
|
],
|
|
237
240
|
base_path="/api", # Configurable prefix (default: /api)
|
|
238
241
|
auth_backend=MyAuth(), # Your AuthBackend implementation
|
|
@@ -240,6 +243,14 @@ FlashAPI(
|
|
|
240
243
|
rate_limit=100, # 100 requests per window
|
|
241
244
|
rate_window=60, # 60 seconds window
|
|
242
245
|
)
|
|
246
|
+
|
|
247
|
+
# Enable idempotency (prevents duplicate operations from double-click/retry)
|
|
248
|
+
# Client sends: Idempotency-Key: <uuid> header
|
|
249
|
+
# Same key + same request → returns stored response (no duplicate created)
|
|
250
|
+
|
|
251
|
+
# Enable cache layer (graceful fallback if Redis down)
|
|
252
|
+
from flashapi.features.cache import CacheLayer, RedisCache, register_cache
|
|
253
|
+
register_cache(CacheLayer(RedisCache(host='localhost')))
|
|
243
254
|
```
|
|
244
255
|
|
|
245
256
|
See [Customization docs](docs/customization.md) for all options.
|
|
@@ -22,10 +22,12 @@
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
FlashAPI generates a full REST API with CRUD, pagination, filtering, sorting, full-text search, relations, soft delete, bulk operations, export, audit trail, webhooks, rate limiting, and a live dashboard — from your existing models, in one line.
|
|
25
|
+
FlashAPI generates a full REST API with CRUD, pagination, filtering, sorting, full-text search, relations, soft delete, bulk operations, export, audit trail, webhooks, rate limiting, **idempotency keys**, **cache layer**, **currentUser auto-injection**, and a live dashboard — from your existing models, in one line.
|
|
26
26
|
|
|
27
27
|
Part of the **FlashAPI Ecosystem** — ensuring SDK client compatibility across all backends (Python, Java Spring, Node.js).
|
|
28
28
|
|
|
29
|
+
**New in 0.4.0:** Idempotency keys (prevent double-click/retry duplicates), cache layer with graceful fallback, currentUserField auto-injection, and audit enabled by default.
|
|
30
|
+
|
|
29
31
|
---
|
|
30
32
|
|
|
31
33
|
## Documentation
|
|
@@ -47,7 +49,7 @@ Part of the **FlashAPI Ecosystem** — ensuring SDK client compatibility across
|
|
|
47
49
|
|
|
48
50
|
```bash
|
|
49
51
|
# x-release-please-start-version
|
|
50
|
-
pip install python-flashapi==0.
|
|
52
|
+
pip install python-flashapi==0.3.0
|
|
51
53
|
# x-release-please-end
|
|
52
54
|
|
|
53
55
|
# With framework extras:
|
|
@@ -189,8 +191,9 @@ FlashAPI(
|
|
|
189
191
|
Model(Config, readonly=True, access="admin"), # GET only, admin
|
|
190
192
|
Model(Log, only=["list"]), # List only
|
|
191
193
|
Model(Animal, plural="animaux"), # Custom plural
|
|
192
|
-
Model(Invoice, soft_delete=True, audit=True), # Opt-in features
|
|
194
|
+
Model(Invoice, soft_delete=True, audit=True), # Opt-in features (audit=True by default)
|
|
193
195
|
Model(Eleve, access="staff", scope="tenant", tenant_field="ecole_id"),
|
|
196
|
+
Model(Post, current_user_field="author"), # Auto-inject authenticated user on create
|
|
194
197
|
],
|
|
195
198
|
base_path="/api", # Configurable prefix (default: /api)
|
|
196
199
|
auth_backend=MyAuth(), # Your AuthBackend implementation
|
|
@@ -198,6 +201,14 @@ FlashAPI(
|
|
|
198
201
|
rate_limit=100, # 100 requests per window
|
|
199
202
|
rate_window=60, # 60 seconds window
|
|
200
203
|
)
|
|
204
|
+
|
|
205
|
+
# Enable idempotency (prevents duplicate operations from double-click/retry)
|
|
206
|
+
# Client sends: Idempotency-Key: <uuid> header
|
|
207
|
+
# Same key + same request → returns stored response (no duplicate created)
|
|
208
|
+
|
|
209
|
+
# Enable cache layer (graceful fallback if Redis down)
|
|
210
|
+
from flashapi.features.cache import CacheLayer, RedisCache, register_cache
|
|
211
|
+
register_cache(CacheLayer(RedisCache(host='localhost')))
|
|
201
212
|
```
|
|
202
213
|
|
|
203
214
|
See [Customization docs](docs/customization.md) for all options.
|
|
@@ -66,6 +66,7 @@ Just add `@api_doc()` on your views. FlashAPI auto-discovers them:
|
|
|
66
66
|
# views.py
|
|
67
67
|
from django.http import JsonResponse
|
|
68
68
|
from django.views.decorators.csrf import csrf_exempt
|
|
69
|
+
from django.views.decorators.http import require_POST
|
|
69
70
|
from flashapi import api_doc
|
|
70
71
|
import json
|
|
71
72
|
|
|
@@ -73,6 +74,7 @@ import json
|
|
|
73
74
|
body={"client_id": "int", "table_id": "int", "plats": "array"},
|
|
74
75
|
body_required=["client_id", "table_id", "plats"])
|
|
75
76
|
@csrf_exempt
|
|
77
|
+
@require_POST
|
|
76
78
|
def checkout(request):
|
|
77
79
|
body = json.loads(request.body)
|
|
78
80
|
# your business logic...
|
|
@@ -106,12 +108,49 @@ custom_views = [
|
|
|
106
108
|
urlpatterns = [
|
|
107
109
|
path("api/", include(
|
|
108
110
|
generate_urls(models=[Plat, Commande, Client], extra_views=custom_views)
|
|
109
|
-
+ custom_views
|
|
110
111
|
)),
|
|
112
|
+
# Register custom views for actual routing (extra_views only adds them to Swagger)
|
|
113
|
+
path("api/checkout/", views.checkout),
|
|
114
|
+
path("api/stats/chiffre-affaires/", views.chiffre_affaires),
|
|
115
|
+
path("api/reports/bestsellers/", views.bestsellers),
|
|
116
|
+
]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
That's it. `@api_doc` on the view + pass `extra_views` to `generate_urls()` for Swagger documentation, and register the same paths in `urlpatterns` for actual routing.
|
|
120
|
+
|
|
121
|
+
#### Path parameters
|
|
122
|
+
|
|
123
|
+
Django URL parameters (`<int:id>`, `<str:slug>`, etc.) are automatically converted to OpenAPI path parameters in Swagger:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
# views.py
|
|
127
|
+
@api_doc(
|
|
128
|
+
tag="Ingestion",
|
|
129
|
+
summary="Ingest a book from its uploaded PDF",
|
|
130
|
+
methods=["post"],
|
|
131
|
+
params={"book_id": "int"},
|
|
132
|
+
body={"force": "bool"},
|
|
133
|
+
body_required=[],
|
|
134
|
+
response={"status": "string", "total_sentences": "int"},
|
|
135
|
+
)
|
|
136
|
+
@csrf_exempt
|
|
137
|
+
@require_POST
|
|
138
|
+
def ingest_view(request, book_id):
|
|
139
|
+
# book_id comes from the URL path
|
|
140
|
+
...
|
|
141
|
+
|
|
142
|
+
# urls.py
|
|
143
|
+
extra_views = [
|
|
144
|
+
path("books/<int:book_id>/ingest/", views.ingest_view),
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
urlpatterns = [
|
|
148
|
+
path("api/", include(generate_urls(models=[Book], extra_views=extra_views))),
|
|
149
|
+
path("api/books/<int:book_id>/ingest/", views.ingest_view),
|
|
111
150
|
]
|
|
112
151
|
```
|
|
113
152
|
|
|
114
|
-
|
|
153
|
+
In Swagger, this renders as `/books/{book_id}/ingest/` with `book_id` as a required integer path parameter. The `params` dict in `@api_doc` controls the type shown in Swagger (`"int"` → integer, `"string"` → string, etc.).
|
|
115
154
|
|
|
116
155
|
### Custom routes (Flask)
|
|
117
156
|
|
|
@@ -9,7 +9,7 @@ from flashapi.core.custom_routes import (
|
|
|
9
9
|
)
|
|
10
10
|
from flashapi.core.response import create_error_response, create_item_response, create_list_response
|
|
11
11
|
from flashapi.core.schema import Model, ModelSchema
|
|
12
|
-
from flashapi.core.visibility import export_fields, filter_response, writable_fields
|
|
12
|
+
from flashapi.core.visibility import export_fields, filter_response, select_fields, writable_fields
|
|
13
13
|
from flashapi.docs.openapi import generate_openapi_schema, get_swagger_html
|
|
14
14
|
from flashapi.features import apply_filters, apply_search, apply_sorting, paginate
|
|
15
15
|
from flashapi.features.health import get_health_check
|
|
@@ -78,6 +78,7 @@ def generate_urls(
|
|
|
78
78
|
schema.scope = wrapper.scope
|
|
79
79
|
schema.tenant_field = wrapper.tenant_field
|
|
80
80
|
schema.owner_field = wrapper.owner_field
|
|
81
|
+
schema.current_user_field = wrapper.current_user_field
|
|
81
82
|
|
|
82
83
|
from flashapi.core.schema import validate_soft_delete
|
|
83
84
|
validate_soft_delete(wrapper.model_class, wrapper.soft_delete)
|
|
@@ -266,6 +267,7 @@ def _create_django_views(
|
|
|
266
267
|
model_scope = schema.scope
|
|
267
268
|
model_tenant_field = schema.tenant_field
|
|
268
269
|
model_owner_field = schema.owner_field
|
|
270
|
+
model_current_user_field = schema.current_user_field
|
|
269
271
|
patterns = []
|
|
270
272
|
|
|
271
273
|
def _check_auth(request, operation):
|
|
@@ -335,6 +337,7 @@ def _create_django_views(
|
|
|
335
337
|
sort = params.get("sort")
|
|
336
338
|
search = params.get("search")
|
|
337
339
|
deleted_param = params.get("deleted", "false").lower() == "true"
|
|
340
|
+
fields_param = params.get("fields") # Field selection: ?fields=id,name,price
|
|
338
341
|
|
|
339
342
|
only_deleted = deleted_param and supports_soft_delete
|
|
340
343
|
items = storage.list_all(_table, only_deleted=only_deleted)
|
|
@@ -350,6 +353,12 @@ def _create_django_views(
|
|
|
350
353
|
items = apply_sorting(items, sort, _fields)
|
|
351
354
|
page_items, total = paginate(items, page, size)
|
|
352
355
|
page_items = [filter_response(item, _schema) for item in page_items]
|
|
356
|
+
|
|
357
|
+
# Apply field selection if ?fields parameter provided
|
|
358
|
+
if fields_param:
|
|
359
|
+
field_list = [f.strip() for f in fields_param.split(",")]
|
|
360
|
+
page_items = [select_fields(item, field_list, _schema) for item in page_items]
|
|
361
|
+
|
|
353
362
|
if metrics:
|
|
354
363
|
metrics.record("READ", entity_name)
|
|
355
364
|
return JsonResponse(
|
|
@@ -371,6 +380,24 @@ def _create_django_views(
|
|
|
371
380
|
if scope_filter:
|
|
372
381
|
data.update(scope_filter)
|
|
373
382
|
|
|
383
|
+
# Auto-inject current user if current_user_field specified
|
|
384
|
+
if model_current_user_field and user and auth_backend:
|
|
385
|
+
user_id = auth_backend.get_owner_id(user)
|
|
386
|
+
if user_id is not None:
|
|
387
|
+
# For ForeignKey fields, Django expects field_id
|
|
388
|
+
# For non-FK fields, use field directly
|
|
389
|
+
# Try FK syntax first (most common case)
|
|
390
|
+
try:
|
|
391
|
+
storage._model._meta.get_field(model_current_user_field)
|
|
392
|
+
field = storage._model._meta.get_field(model_current_user_field)
|
|
393
|
+
if field.many_to_one: # ForeignKey
|
|
394
|
+
data[f"{model_current_user_field}_id"] = user_id
|
|
395
|
+
else:
|
|
396
|
+
data[model_current_user_field] = user_id
|
|
397
|
+
except Exception:
|
|
398
|
+
# Fallback: assume FK
|
|
399
|
+
data[f"{model_current_user_field}_id"] = user_id
|
|
400
|
+
|
|
374
401
|
item = storage.create(_table, data)
|
|
375
402
|
if metrics:
|
|
376
403
|
metrics.record("CREATE", entity_name, str(item.get("id", "")))
|
|
@@ -661,6 +688,13 @@ def _create_django_views(
|
|
|
661
688
|
return JsonResponse(create_error_response("Not found", 404), status=404)
|
|
662
689
|
|
|
663
690
|
item = filter_response(item, _schema)
|
|
691
|
+
|
|
692
|
+
# Apply field selection if ?fields parameter provided
|
|
693
|
+
fields_param = request.GET.get("fields")
|
|
694
|
+
if fields_param:
|
|
695
|
+
field_list = [f.strip() for f in fields_param.split(",")]
|
|
696
|
+
item = select_fields(item, field_list, _schema)
|
|
697
|
+
|
|
664
698
|
return JsonResponse(create_item_response(item, formatter))
|
|
665
699
|
|
|
666
700
|
if request.method == "PUT" and "update" in _schema.permissions:
|
|
@@ -10,7 +10,7 @@ from pydantic import BaseModel, create_model
|
|
|
10
10
|
from flashapi.core.relations import find_expandable_fields, resolve_relations
|
|
11
11
|
from flashapi.core.response import create_error_response, create_item_response, create_list_response
|
|
12
12
|
from flashapi.core.schema import FieldType, Model, ModelSchema
|
|
13
|
-
from flashapi.core.visibility import export_fields, filter_response, writable_fields
|
|
13
|
+
from flashapi.core.visibility import export_fields, filter_response, select_fields, writable_fields
|
|
14
14
|
from flashapi.features import apply_filters, apply_search, apply_sorting, paginate
|
|
15
15
|
from flashapi.features.dashboard import DASHBOARD_HTML, MetricsCollector
|
|
16
16
|
from flashapi.features.export import CONTENT_TYPES, EXPORTERS
|
|
@@ -450,6 +450,13 @@ class FlashAPI:
|
|
|
450
450
|
|
|
451
451
|
metrics.record("READ", tag)
|
|
452
452
|
page_items = [filter_response(item, model_schema) for item in page_items]
|
|
453
|
+
|
|
454
|
+
# Apply field selection if ?fields parameter provided
|
|
455
|
+
fields_param = request.query_params.get("fields")
|
|
456
|
+
if fields_param:
|
|
457
|
+
field_list = [f.strip() for f in fields_param.split(",")]
|
|
458
|
+
page_items = [select_fields(item, field_list, model_schema) for item in page_items]
|
|
459
|
+
|
|
453
460
|
return create_list_response(page_items, total, page, size, formatter)
|
|
454
461
|
|
|
455
462
|
def _add_read_route(self, table, formatter, storage, tag, expandable, model_schema, lookup_field="id") -> None:
|
|
@@ -481,6 +488,13 @@ class FlashAPI:
|
|
|
481
488
|
item = self._expand_items([item], expand, expandable)[0]
|
|
482
489
|
|
|
483
490
|
item = filter_response(item, model_schema)
|
|
491
|
+
|
|
492
|
+
# Apply field selection if ?fields parameter provided
|
|
493
|
+
fields_param = request.query_params.get("fields")
|
|
494
|
+
if fields_param:
|
|
495
|
+
field_list = [f.strip() for f in fields_param.split(",")]
|
|
496
|
+
item = select_fields(item, field_list, model_schema)
|
|
497
|
+
|
|
484
498
|
return create_item_response(item, formatter)
|
|
485
499
|
|
|
486
500
|
def _add_history_route(self, table, entity_name, lookup_field="id", model_schema=None) -> None:
|
|
@@ -10,7 +10,7 @@ from flashapi.core.custom_routes import (
|
|
|
10
10
|
from flashapi.core.relations import find_expandable_fields, resolve_relations
|
|
11
11
|
from flashapi.core.response import create_error_response, create_item_response, create_list_response
|
|
12
12
|
from flashapi.core.schema import Model, ModelSchema
|
|
13
|
-
from flashapi.core.visibility import export_fields, filter_response, writable_fields
|
|
13
|
+
from flashapi.core.visibility import export_fields, filter_response, select_fields, writable_fields
|
|
14
14
|
from flashapi.docs.openapi import generate_openapi_schema, get_swagger_html
|
|
15
15
|
from flashapi.features import apply_filters, apply_search, apply_sorting, paginate
|
|
16
16
|
from flashapi.features.health import get_health_check
|
|
@@ -441,6 +441,13 @@ def _create_flask_routes(
|
|
|
441
441
|
if _metrics:
|
|
442
442
|
_metrics.record("READ", entity_name)
|
|
443
443
|
page_items = [filter_response(item, _schema) for item in page_items]
|
|
444
|
+
|
|
445
|
+
# Apply field selection if ?fields parameter provided
|
|
446
|
+
fields_param = request.args.get("fields")
|
|
447
|
+
if fields_param:
|
|
448
|
+
field_list = [f.strip() for f in fields_param.split(",")]
|
|
449
|
+
page_items = [select_fields(item, field_list, _schema) for item in page_items]
|
|
450
|
+
|
|
444
451
|
return jsonify(create_list_response(page_items, total, page, size, formatter))
|
|
445
452
|
|
|
446
453
|
if "read" in schema.permissions:
|
|
@@ -463,6 +470,13 @@ def _create_flask_routes(
|
|
|
463
470
|
item = _expand_items([item], expand, _exp, storage)[0]
|
|
464
471
|
|
|
465
472
|
item = filter_response(item, _schema)
|
|
473
|
+
|
|
474
|
+
# Apply field selection if ?fields parameter provided
|
|
475
|
+
fields_param = request.args.get("fields")
|
|
476
|
+
if fields_param:
|
|
477
|
+
field_list = [f.strip() for f in fields_param.split(",")]
|
|
478
|
+
item = select_fields(item, field_list, _schema)
|
|
479
|
+
|
|
466
480
|
return jsonify(create_item_response(item, formatter))
|
|
467
481
|
|
|
468
482
|
if "read" in schema.permissions and entity_audit:
|
|
@@ -121,6 +121,8 @@ def discover_django_views(url_patterns, trailing_slash: bool = True) -> dict[str
|
|
|
121
121
|
"""Scan Django URL patterns for @api_doc-decorated views and build OpenAPI paths."""
|
|
122
122
|
paths: dict[str, dict] = {}
|
|
123
123
|
|
|
124
|
+
import re
|
|
125
|
+
|
|
124
126
|
for pattern in url_patterns:
|
|
125
127
|
callback = getattr(pattern, "callback", None)
|
|
126
128
|
if callback is None:
|
|
@@ -130,8 +132,9 @@ def discover_django_views(url_patterns, trailing_slash: bool = True) -> dict[str
|
|
|
130
132
|
if doc is None:
|
|
131
133
|
continue
|
|
132
134
|
|
|
133
|
-
# Build path from Django pattern
|
|
135
|
+
# Build path from Django pattern, converting <type:name> to {name}
|
|
134
136
|
path_str = "/" + str(pattern.pattern)
|
|
137
|
+
path_str = re.sub(r"<(?:\w+:)?(\w+)>", r"{\1}", path_str)
|
|
135
138
|
if trailing_slash and not path_str.endswith("/"):
|
|
136
139
|
path_str += "/"
|
|
137
140
|
|
|
@@ -143,8 +146,27 @@ def discover_django_views(url_patterns, trailing_slash: bool = True) -> dict[str
|
|
|
143
146
|
if path_str not in paths:
|
|
144
147
|
paths[path_str] = {}
|
|
145
148
|
|
|
149
|
+
# Extract path parameters from URL pattern
|
|
150
|
+
path_params = re.findall(r"\{(\w+)\}", path_str)
|
|
151
|
+
|
|
146
152
|
for method in methods:
|
|
147
|
-
|
|
153
|
+
operation = _build_openapi_operation(doc, method)
|
|
154
|
+
if path_params:
|
|
155
|
+
if "parameters" not in operation:
|
|
156
|
+
operation["parameters"] = []
|
|
157
|
+
# Remove any query params that are actually path params
|
|
158
|
+
operation["parameters"] = [
|
|
159
|
+
p for p in operation["parameters"] if p["name"] not in path_params
|
|
160
|
+
]
|
|
161
|
+
for pname in path_params:
|
|
162
|
+
ptype = doc.get("params", {}).get(pname, "string") if doc.get("params") else "string"
|
|
163
|
+
operation["parameters"].append({
|
|
164
|
+
"name": pname,
|
|
165
|
+
"in": "path",
|
|
166
|
+
"required": True,
|
|
167
|
+
"schema": TYPE_MAP.get(ptype, {"type": "string"}),
|
|
168
|
+
})
|
|
169
|
+
paths[path_str][method] = operation
|
|
148
170
|
|
|
149
171
|
return paths
|
|
150
172
|
|
|
@@ -59,6 +59,7 @@ class ModelSchema:
|
|
|
59
59
|
scope: str | None = None # "tenant", "owner", or "both"
|
|
60
60
|
tenant_field: str | None = None
|
|
61
61
|
owner_field: str | None = None
|
|
62
|
+
current_user_field: str | None = None # Auto-inject authenticated user on CREATE
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
ALL_OPERATIONS = ["list", "read", "create", "update", "delete"]
|
|
@@ -112,12 +113,13 @@ class Model:
|
|
|
112
113
|
only: list[str] | None = None,
|
|
113
114
|
plural: str | None = None,
|
|
114
115
|
soft_delete: bool = False,
|
|
115
|
-
audit: bool =
|
|
116
|
+
audit: bool = True, # Enabled by default for full traceability
|
|
116
117
|
lookup_field: str = "id",
|
|
117
118
|
access: str | dict | bool | None = None,
|
|
118
119
|
scope: str | None = None,
|
|
119
120
|
tenant_field: str | None = None,
|
|
120
121
|
owner_field: str | None = None,
|
|
122
|
+
current_user_field: str | None = None,
|
|
121
123
|
) -> None:
|
|
122
124
|
self.model_class = model_class
|
|
123
125
|
self.plural = plural
|
|
@@ -128,6 +130,7 @@ class Model:
|
|
|
128
130
|
self.scope = scope
|
|
129
131
|
self.tenant_field = tenant_field
|
|
130
132
|
self.owner_field = owner_field
|
|
133
|
+
self.current_user_field = current_user_field
|
|
131
134
|
self.permissions = self._resolve_permissions(readonly, exclude, only)
|
|
132
135
|
|
|
133
136
|
def _resolve_permissions(
|
|
@@ -44,3 +44,39 @@ def filter_input(data: dict, schema: ModelSchema) -> dict:
|
|
|
44
44
|
"""Remove readonly/hidden fields from input dict."""
|
|
45
45
|
allowed = writable_fields(schema)
|
|
46
46
|
return {k: v for k, v in data.items() if k in allowed}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def select_fields(data: dict, fields: list[str] | None, schema: ModelSchema) -> dict:
|
|
50
|
+
"""
|
|
51
|
+
Select only requested fields from response data.
|
|
52
|
+
|
|
53
|
+
Field selection (?fields=id,name,price) allows clients to request only specific fields,
|
|
54
|
+
reducing payload size and improving performance.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
data: Response dict to filter
|
|
58
|
+
fields: List of field names to include (None = return all visible fields)
|
|
59
|
+
schema: Model schema for validation
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Dict containing only requested fields that exist and are visible (id always included)
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
>>> select_fields({"id": 1, "name": "Laptop", "price": 999, "stock": 10}, ["name"], schema)
|
|
66
|
+
{"id": 1, "name": "Laptop"} # id always included
|
|
67
|
+
"""
|
|
68
|
+
if not fields:
|
|
69
|
+
return data
|
|
70
|
+
|
|
71
|
+
# Get visible fields from schema
|
|
72
|
+
visible = response_fields(schema)
|
|
73
|
+
|
|
74
|
+
# Filter: requested fields + always include id
|
|
75
|
+
result = {}
|
|
76
|
+
for k, v in data.items():
|
|
77
|
+
if k == "id":
|
|
78
|
+
result[k] = v # Always include id
|
|
79
|
+
elif k in fields and k in visible:
|
|
80
|
+
result[k] = v
|
|
81
|
+
|
|
82
|
+
return result
|