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.
Files changed (101) hide show
  1. python_flashapi-0.4.0/.release-please-manifest.json +3 -0
  2. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/CHANGELOG.md +16 -0
  3. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/PKG-INFO +15 -4
  4. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/README.md +14 -3
  5. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/custom-logic.md +41 -2
  6. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/pyproject.toml +1 -1
  7. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/release-please-config.json +1 -1
  8. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/adapters/django.py +35 -1
  9. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/adapters/fastapi.py +15 -1
  10. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/adapters/flask.py +15 -1
  11. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/core/custom_routes.py +24 -2
  12. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/core/schema.py +4 -1
  13. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/core/visibility.py +36 -0
  14. python_flashapi-0.4.0/src/flashapi/features/cache.py +229 -0
  15. python_flashapi-0.4.0/src/flashapi/features/idempotency.py +375 -0
  16. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/storage/orm.py +3 -0
  17. python_flashapi-0.4.0/tests/test_features/test_field_selection.py +127 -0
  18. python_flashapi-0.3.0/.release-please-manifest.json +0 -3
  19. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/.github/workflows/ci.yml +0 -0
  20. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/.github/workflows/release-please.yml +0 -0
  21. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/.gitignore +0 -0
  22. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/LICENSE +0 -0
  23. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/NOTICE +0 -0
  24. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/authentication.md +0 -0
  25. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/customization.md +0 -0
  26. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/examples.md +0 -0
  27. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/features.md +0 -0
  28. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/framework-notes.md +0 -0
  29. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/integration.md +0 -0
  30. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/logo.svg +0 -0
  31. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/logo1.svg +0 -0
  32. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/logo2.svg +0 -0
  33. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/docs/relations.md +0 -0
  34. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/examples/fastapi_example.py +0 -0
  35. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/examples/flask_example.py +0 -0
  36. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/__init__.py +0 -0
  37. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/adapters/__init__.py +0 -0
  38. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/adapters/base.py +0 -0
  39. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/core/__init__.py +0 -0
  40. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/core/pluralize.py +0 -0
  41. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/core/relations.py +0 -0
  42. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/core/response.py +0 -0
  43. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/django.py +0 -0
  44. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/docs/__init__.py +0 -0
  45. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/docs/openapi.py +0 -0
  46. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/fastapi.py +0 -0
  47. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/__init__.py +0 -0
  48. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/audit.py +0 -0
  49. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/auth.py +0 -0
  50. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/dashboard.py +0 -0
  51. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/export.py +0 -0
  52. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/filtering.py +0 -0
  53. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/health.py +0 -0
  54. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/pagination.py +0 -0
  55. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/rate_limit.py +0 -0
  56. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/search.py +0 -0
  57. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/sorting.py +0 -0
  58. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/webhooks.py +0 -0
  59. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/features/websocket.py +0 -0
  60. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/flask.py +0 -0
  61. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/__init__.py +0 -0
  62. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/base.py +0 -0
  63. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/dataclass.py +0 -0
  64. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/detect.py +0 -0
  65. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/django.py +0 -0
  66. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/pydantic.py +0 -0
  67. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/sqlalchemy.py +0 -0
  68. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/py.typed +0 -0
  69. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/storage/__init__.py +0 -0
  70. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/storage/auto.py +0 -0
  71. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/storage/base.py +0 -0
  72. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/src/flashapi/storage/sqlalchemy.py +0 -0
  73. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/__init__.py +0 -0
  74. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_adapters/__init__.py +0 -0
  75. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_adapters/test_fastapi.py +0 -0
  76. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_adapters/test_flask.py +0 -0
  77. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_adapters/test_relations.py +0 -0
  78. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_adapters/test_soft_delete.py +0 -0
  79. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_core/__init__.py +0 -0
  80. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_core/test_pluralize.py +0 -0
  81. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_core/test_response.py +0 -0
  82. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_core/test_schema.py +0 -0
  83. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_core/test_visibility.py +0 -0
  84. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/__init__.py +0 -0
  85. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_audit.py +0 -0
  86. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_auth.py +0 -0
  87. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_dashboard.py +0 -0
  88. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_export.py +0 -0
  89. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_filtering.py +0 -0
  90. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_pagination.py +0 -0
  91. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_rate_limit.py +0 -0
  92. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_search.py +0 -0
  93. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_sorting.py +0 -0
  94. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_features/test_websocket.py +0 -0
  95. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_health.py +0 -0
  96. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_inspectors/__init__.py +0 -0
  97. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_inspectors/test_dataclass.py +0 -0
  98. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_inspectors/test_detect.py +0 -0
  99. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_inspectors/test_pydantic.py +0 -0
  100. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_storage/__init__.py +0 -0
  101. {python_flashapi-0.3.0 → python_flashapi-0.4.0}/tests/test_storage/test_auto.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.4.0"
3
+ }
@@ -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.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.2.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.2.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
- That's it. `@api_doc` on the view + pass `extra_views` to `generate_urls()`. FlashAPI reads the decorator metadata and adds your endpoints to Swagger no duplicate declarations.
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "python-flashapi"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Define your models. FlashAPI does the rest."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -4,7 +4,7 @@
4
4
  "release-type": "python",
5
5
  "package-name": "python-flashapi",
6
6
  "bump-minor-pre-major": true,
7
- "extra-files": ["pyproject.toml"]
7
+ "extra-files": ["pyproject.toml", "README.md"]
8
8
  }
9
9
  }
10
10
  }
@@ -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
- paths[path_str][method] = _build_openapi_operation(doc, method)
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 = False,
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