python-flashapi 0.2.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/.github/workflows/release-please.yml +48 -0
- python_flashapi-0.4.0/.release-please-manifest.json +3 -0
- python_flashapi-0.4.0/CHANGELOG.md +57 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/PKG-INFO +21 -5
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/README.md +19 -3
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/docs/custom-logic.md +41 -2
- python_flashapi-0.4.0/docs/logo.svg +25 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/pyproject.toml +1 -1
- python_flashapi-0.4.0/release-please-config.json +10 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/adapters/django.py +72 -1
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/adapters/fastapi.py +47 -1
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/adapters/flask.py +49 -1
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/core/custom_routes.py +24 -2
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/core/schema.py +4 -1
- {python_flashapi-0.2.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/health.py +69 -0
- python_flashapi-0.4.0/src/flashapi/features/idempotency.py +375 -0
- {python_flashapi-0.2.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.4.0/tests/test_health.py +42 -0
- python_flashapi-0.2.0/.github/workflows/publish.yml +0 -29
- python_flashapi-0.2.0/CHANGELOG.md +0 -24
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/.github/workflows/ci.yml +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/.gitignore +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/LICENSE +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/NOTICE +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/docs/authentication.md +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/docs/customization.md +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/docs/examples.md +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/docs/features.md +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/docs/framework-notes.md +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/docs/integration.md +0 -0
- /python_flashapi-0.2.0/docs/logo.svg → /python_flashapi-0.4.0/docs/logo1.svg +0 -0
- /python_flashapi-0.2.0/docs/logo1.svg → /python_flashapi-0.4.0/docs/logo2.svg +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/docs/relations.md +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/examples/fastapi_example.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/examples/flask_example.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/adapters/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/adapters/base.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/core/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/core/pluralize.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/core/relations.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/core/response.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/django.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/docs/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/docs/openapi.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/fastapi.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/features/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/features/audit.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/features/auth.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/features/dashboard.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/features/export.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/features/filtering.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/features/pagination.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/features/rate_limit.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/features/search.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/features/sorting.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/features/webhooks.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/features/websocket.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/flask.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/base.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/dataclass.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/detect.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/django.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/pydantic.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/inspectors/sqlalchemy.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/py.typed +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/storage/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/storage/auto.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/storage/base.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/src/flashapi/storage/sqlalchemy.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_adapters/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_adapters/test_fastapi.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_adapters/test_flask.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_adapters/test_relations.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_adapters/test_soft_delete.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_core/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_core/test_pluralize.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_core/test_response.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_core/test_schema.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_core/test_visibility.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_features/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_features/test_audit.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_features/test_auth.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_features/test_dashboard.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_features/test_export.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_features/test_filtering.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_features/test_pagination.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_features/test_rate_limit.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_features/test_search.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_features/test_sorting.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_features/test_websocket.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_inspectors/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_inspectors/test_dataclass.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_inspectors/test_detect.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_inspectors/test_pydantic.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_storage/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.4.0}/tests/test_storage/test_auto.py +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Release Please
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
pull-requests: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
release-please:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
outputs:
|
|
15
|
+
release_created: ${{ steps.release.outputs.release_created }}
|
|
16
|
+
tag_name: ${{ steps.release.outputs.tag_name }}
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: googleapis/release-please-action@v4
|
|
20
|
+
id: release
|
|
21
|
+
with:
|
|
22
|
+
release-type: python
|
|
23
|
+
package-name: python-flashapi
|
|
24
|
+
|
|
25
|
+
publish:
|
|
26
|
+
needs: release-please
|
|
27
|
+
if: ${{ needs.release-please.outputs.release_created }}
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
|
|
33
|
+
- name: Set up Python
|
|
34
|
+
uses: actions/setup-python@v5
|
|
35
|
+
with:
|
|
36
|
+
python-version: '3.12'
|
|
37
|
+
|
|
38
|
+
- name: Install build tools
|
|
39
|
+
run: pip install build twine
|
|
40
|
+
|
|
41
|
+
- name: Build package
|
|
42
|
+
run: python -m build
|
|
43
|
+
|
|
44
|
+
- name: Publish to PyPI
|
|
45
|
+
run: twine upload dist/*
|
|
46
|
+
env:
|
|
47
|
+
TWINE_USERNAME: __token__
|
|
48
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Changelog
|
|
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
|
+
|
|
19
|
+
## [0.3.0](https://github.com/HackermanMe/flashapi/compare/v0.2.0...v0.3.0) (2026-08-08)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* add production health check endpoints (/health and /ready) ([cc4e881](https://github.com/HackermanMe/flashapi/commit/cc4e881f09055fa990bb054298ebc8c1f55af7af))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* corriger les gradients et les couleurs dans les fichiers SVG du logo ([50aadaf](https://github.com/HackermanMe/flashapi/commit/50aadaff44919e5666621cc3f645dd34c1364a64))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Documentation
|
|
33
|
+
|
|
34
|
+
* add release-please version marker in README ([515aeb8](https://github.com/HackermanMe/flashapi/commit/515aeb83c2f021bebac25d9690c4726003769746))
|
|
35
|
+
|
|
36
|
+
## 0.1.0 (2026-07-09)
|
|
37
|
+
|
|
38
|
+
Initial release.
|
|
39
|
+
|
|
40
|
+
### Features
|
|
41
|
+
- Auto-generate REST API + CRUD from model definitions
|
|
42
|
+
- Framework support: FastAPI, Django, Flask
|
|
43
|
+
- Model support: Django ORM, SQLAlchemy, Pydantic, dataclass
|
|
44
|
+
- Pagination, filtering, sorting, full-text search
|
|
45
|
+
- Relation detection: nested routes and `?expand=`
|
|
46
|
+
- OpenAPI 3.1.0 schema generation + Swagger UI
|
|
47
|
+
- French + English pluralization
|
|
48
|
+
- `Model()` wrapper: readonly, exclude, only, plural
|
|
49
|
+
- `@api_doc()` decorator for custom routes in Swagger
|
|
50
|
+
- `engine=` parameter for SQLAlchemy database reuse
|
|
51
|
+
- Custom response formatter support
|
|
52
|
+
|
|
53
|
+
### Security
|
|
54
|
+
- SQL identifier validation (prevents injection via table/column names)
|
|
55
|
+
- Session rollback on SQLAlchemy errors
|
|
56
|
+
- Input validation for pagination parameters
|
|
57
|
+
- CSRF exempt on Django API views (documented behavior)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
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
|
|
@@ -88,7 +90,12 @@ Part of the **FlashAPI Ecosystem** — ensuring SDK client compatibility across
|
|
|
88
90
|
## Installation
|
|
89
91
|
|
|
90
92
|
```bash
|
|
91
|
-
|
|
93
|
+
# x-release-please-start-version
|
|
94
|
+
pip install python-flashapi==0.3.0
|
|
95
|
+
# x-release-please-end
|
|
96
|
+
|
|
97
|
+
# With framework extras:
|
|
98
|
+
pip install python-flashapi[fastapi] # or [flask] or [django] or [all]
|
|
92
99
|
```
|
|
93
100
|
|
|
94
101
|
---
|
|
@@ -226,8 +233,9 @@ FlashAPI(
|
|
|
226
233
|
Model(Config, readonly=True, access="admin"), # GET only, admin
|
|
227
234
|
Model(Log, only=["list"]), # List only
|
|
228
235
|
Model(Animal, plural="animaux"), # Custom plural
|
|
229
|
-
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)
|
|
230
237
|
Model(Eleve, access="staff", scope="tenant", tenant_field="ecole_id"),
|
|
238
|
+
Model(Post, current_user_field="author"), # Auto-inject authenticated user on create
|
|
231
239
|
],
|
|
232
240
|
base_path="/api", # Configurable prefix (default: /api)
|
|
233
241
|
auth_backend=MyAuth(), # Your AuthBackend implementation
|
|
@@ -235,6 +243,14 @@ FlashAPI(
|
|
|
235
243
|
rate_limit=100, # 100 requests per window
|
|
236
244
|
rate_window=60, # 60 seconds window
|
|
237
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')))
|
|
238
254
|
```
|
|
239
255
|
|
|
240
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
|
|
@@ -46,7 +48,12 @@ Part of the **FlashAPI Ecosystem** — ensuring SDK client compatibility across
|
|
|
46
48
|
## Installation
|
|
47
49
|
|
|
48
50
|
```bash
|
|
49
|
-
|
|
51
|
+
# x-release-please-start-version
|
|
52
|
+
pip install python-flashapi==0.3.0
|
|
53
|
+
# x-release-please-end
|
|
54
|
+
|
|
55
|
+
# With framework extras:
|
|
56
|
+
pip install python-flashapi[fastapi] # or [flask] or [django] or [all]
|
|
50
57
|
```
|
|
51
58
|
|
|
52
59
|
---
|
|
@@ -184,8 +191,9 @@ FlashAPI(
|
|
|
184
191
|
Model(Config, readonly=True, access="admin"), # GET only, admin
|
|
185
192
|
Model(Log, only=["list"]), # List only
|
|
186
193
|
Model(Animal, plural="animaux"), # Custom plural
|
|
187
|
-
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)
|
|
188
195
|
Model(Eleve, access="staff", scope="tenant", tenant_field="ecole_id"),
|
|
196
|
+
Model(Post, current_user_field="author"), # Auto-inject authenticated user on create
|
|
189
197
|
],
|
|
190
198
|
base_path="/api", # Configurable prefix (default: /api)
|
|
191
199
|
auth_backend=MyAuth(), # Your AuthBackend implementation
|
|
@@ -193,6 +201,14 @@ FlashAPI(
|
|
|
193
201
|
rate_limit=100, # 100 requests per window
|
|
194
202
|
rate_window=60, # 60 seconds window
|
|
195
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')))
|
|
196
212
|
```
|
|
197
213
|
|
|
198
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
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<svg width="387" height="122" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bolt" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" stop-color="#FFD700"/>
|
|
5
|
+
<stop offset="100%" stop-color="#FF8C00"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<!-- Lightning bolt icon -->
|
|
9
|
+
<!-- FlashAPI text -->
|
|
10
|
+
<!-- Tagline -->
|
|
11
|
+
<g>
|
|
12
|
+
<title>Layer 1</title>
|
|
13
|
+
<text x="113" y="71" font-family="Inter, -apple-system, sans-serif" font-size="42" font-weight="bold" fill="#ff5000" id="svg_2" font-style="italic">Flash</text>
|
|
14
|
+
<text x="222" y="70" font-family="Inter, -apple-system, sans-serif" font-size="42" font-weight="bold" fill="url(#bolt)" id="svg_3" font-style="italic">API</text>
|
|
15
|
+
<text x="113" y="95" font-family="Inter, -apple-system, sans-serif" font-size="13" fill="#666" id="svg_4">Define your models. FlashAPI does the rest.</text>
|
|
16
|
+
<g id="svg_1">
|
|
17
|
+
<circle id="svg_5" cx="52" cy="63.5" r="48" opacity="0.35"/>
|
|
18
|
+
<circle id="svg_6" cx="52" cy="59.5" r="48" fill="#ffc400"/>
|
|
19
|
+
<circle id="svg_7" cx="52" cy="63.5" r="39" opacity="0.35"/>
|
|
20
|
+
<circle id="svg_8" cx="52" cy="59.5" r="39" fill="#ff1200"/>
|
|
21
|
+
<polygon id="svg_9" points="83,8.5 34,59.5 50.01200866699219,56.45100402832031 29.772994995117188,85.5 45,82.5 22,117.5 69,70.5 55,72.5 76,44.5 59,49.5 " opacity="0.35"/>
|
|
22
|
+
<polygon id="svg_10" fill="#ffc400" points="83,4.5 34,55.5 50.01200866699219,52.45100402832031 29.772994995117188,81.5 45,78.5 22,113.5 69,66.5 55,68.5 76,40.5 59,45.5 "/>
|
|
23
|
+
</g>
|
|
24
|
+
</g>
|
|
25
|
+
</svg>
|
|
@@ -9,9 +9,10 @@ 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
|
+
from flashapi.features.health import get_health_check
|
|
15
16
|
from flashapi.inspectors import inspect_model
|
|
16
17
|
from flashapi.storage.orm import DjangoORMStorage
|
|
17
18
|
|
|
@@ -77,6 +78,7 @@ def generate_urls(
|
|
|
77
78
|
schema.scope = wrapper.scope
|
|
78
79
|
schema.tenant_field = wrapper.tenant_field
|
|
79
80
|
schema.owner_field = wrapper.owner_field
|
|
81
|
+
schema.current_user_field = wrapper.current_user_field
|
|
80
82
|
|
|
81
83
|
from flashapi.core.schema import validate_soft_delete
|
|
82
84
|
validate_soft_delete(wrapper.model_class, wrapper.soft_delete)
|
|
@@ -113,6 +115,7 @@ def generate_urls(
|
|
|
113
115
|
))
|
|
114
116
|
|
|
115
117
|
urlpatterns.extend(_create_api_root_view(all_schemas, docs))
|
|
118
|
+
urlpatterns.extend(_create_health_views())
|
|
116
119
|
|
|
117
120
|
return urlpatterns
|
|
118
121
|
|
|
@@ -264,6 +267,7 @@ def _create_django_views(
|
|
|
264
267
|
model_scope = schema.scope
|
|
265
268
|
model_tenant_field = schema.tenant_field
|
|
266
269
|
model_owner_field = schema.owner_field
|
|
270
|
+
model_current_user_field = schema.current_user_field
|
|
267
271
|
patterns = []
|
|
268
272
|
|
|
269
273
|
def _check_auth(request, operation):
|
|
@@ -333,6 +337,7 @@ def _create_django_views(
|
|
|
333
337
|
sort = params.get("sort")
|
|
334
338
|
search = params.get("search")
|
|
335
339
|
deleted_param = params.get("deleted", "false").lower() == "true"
|
|
340
|
+
fields_param = params.get("fields") # Field selection: ?fields=id,name,price
|
|
336
341
|
|
|
337
342
|
only_deleted = deleted_param and supports_soft_delete
|
|
338
343
|
items = storage.list_all(_table, only_deleted=only_deleted)
|
|
@@ -348,6 +353,12 @@ def _create_django_views(
|
|
|
348
353
|
items = apply_sorting(items, sort, _fields)
|
|
349
354
|
page_items, total = paginate(items, page, size)
|
|
350
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
|
+
|
|
351
362
|
if metrics:
|
|
352
363
|
metrics.record("READ", entity_name)
|
|
353
364
|
return JsonResponse(
|
|
@@ -369,6 +380,24 @@ def _create_django_views(
|
|
|
369
380
|
if scope_filter:
|
|
370
381
|
data.update(scope_filter)
|
|
371
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
|
+
|
|
372
401
|
item = storage.create(_table, data)
|
|
373
402
|
if metrics:
|
|
374
403
|
metrics.record("CREATE", entity_name, str(item.get("id", "")))
|
|
@@ -659,6 +688,13 @@ def _create_django_views(
|
|
|
659
688
|
return JsonResponse(create_error_response("Not found", 404), status=404)
|
|
660
689
|
|
|
661
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
|
+
|
|
662
698
|
return JsonResponse(create_item_response(item, formatter))
|
|
663
699
|
|
|
664
700
|
if request.method == "PUT" and "update" in _schema.permissions:
|
|
@@ -769,3 +805,38 @@ def _create_django_views(
|
|
|
769
805
|
patterns.append(path(f"{table}/<str:item_id>/history/", history_view, name=f"{table}_history"))
|
|
770
806
|
|
|
771
807
|
return patterns
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def _create_health_views():
|
|
811
|
+
"""Create health check views for production monitoring."""
|
|
812
|
+
from django.http import JsonResponse
|
|
813
|
+
from django.urls import path
|
|
814
|
+
|
|
815
|
+
health_check = get_health_check()
|
|
816
|
+
|
|
817
|
+
def liveness_view(request):
|
|
818
|
+
"""Liveness probe — is the application running?"""
|
|
819
|
+
return JsonResponse(health_check.liveness())
|
|
820
|
+
|
|
821
|
+
def readiness_view(request):
|
|
822
|
+
"""Readiness probe — is the application ready to serve traffic?"""
|
|
823
|
+
data, status_code = health_check.readiness()
|
|
824
|
+
return JsonResponse(data, status=status_code)
|
|
825
|
+
|
|
826
|
+
# Register database check for Django ORM
|
|
827
|
+
def check_database():
|
|
828
|
+
try:
|
|
829
|
+
from django.db import connection
|
|
830
|
+
with connection.cursor() as cursor:
|
|
831
|
+
cursor.execute("SELECT 1")
|
|
832
|
+
return True
|
|
833
|
+
except Exception:
|
|
834
|
+
return False
|
|
835
|
+
|
|
836
|
+
health_check.register_check("database", check_database)
|
|
837
|
+
health_check.mark_ready()
|
|
838
|
+
|
|
839
|
+
return [
|
|
840
|
+
path("health/", liveness_view, name="health_liveness"),
|
|
841
|
+
path("ready/", readiness_view, name="health_readiness"),
|
|
842
|
+
]
|
|
@@ -10,10 +10,11 @@ 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
|
|
17
|
+
from flashapi.features.health import get_health_check
|
|
17
18
|
from flashapi.inspectors import inspect_model
|
|
18
19
|
from flashapi.storage.auto import AutoStorage
|
|
19
20
|
from flashapi.storage.sqlalchemy import SQLAlchemyStorage
|
|
@@ -143,6 +144,7 @@ class FlashAPI:
|
|
|
143
144
|
self._add_dashboard_routes()
|
|
144
145
|
self._add_websocket_route()
|
|
145
146
|
self._add_api_root()
|
|
147
|
+
self._add_health_routes()
|
|
146
148
|
|
|
147
149
|
def _prepare_model(self, model_entry) -> None:
|
|
148
150
|
wrapper = model_entry if isinstance(model_entry, Model) else Model(model_entry)
|
|
@@ -248,6 +250,36 @@ class FlashAPI:
|
|
|
248
250
|
}
|
|
249
251
|
return {"resources": resources, "links": links}
|
|
250
252
|
|
|
253
|
+
def _add_health_routes(self) -> None:
|
|
254
|
+
"""Add health check endpoints for production monitoring."""
|
|
255
|
+
health_check = get_health_check()
|
|
256
|
+
|
|
257
|
+
@self._app.get("/health", tags=["Health"], include_in_schema=False)
|
|
258
|
+
async def liveness():
|
|
259
|
+
"""Liveness probe — is the application running?"""
|
|
260
|
+
return health_check.liveness()
|
|
261
|
+
|
|
262
|
+
@self._app.get("/ready", tags=["Health"], include_in_schema=False)
|
|
263
|
+
async def readiness():
|
|
264
|
+
"""Readiness probe — is the application ready to serve traffic?"""
|
|
265
|
+
data, status_code = health_check.readiness()
|
|
266
|
+
return JSONResponse(content=data, status_code=status_code)
|
|
267
|
+
|
|
268
|
+
# Register database check if using SQLAlchemy
|
|
269
|
+
if self._session_factory:
|
|
270
|
+
def check_database():
|
|
271
|
+
try:
|
|
272
|
+
session = self._session_factory()
|
|
273
|
+
session.execute("SELECT 1")
|
|
274
|
+
session.close()
|
|
275
|
+
return True
|
|
276
|
+
except Exception:
|
|
277
|
+
return False
|
|
278
|
+
health_check.register_check("database", check_database)
|
|
279
|
+
|
|
280
|
+
# Mark ready after all routes are registered
|
|
281
|
+
health_check.mark_ready()
|
|
282
|
+
|
|
251
283
|
def _add_rate_limit_middleware(self) -> None:
|
|
252
284
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
253
285
|
|
|
@@ -418,6 +450,13 @@ class FlashAPI:
|
|
|
418
450
|
|
|
419
451
|
metrics.record("READ", tag)
|
|
420
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
|
+
|
|
421
460
|
return create_list_response(page_items, total, page, size, formatter)
|
|
422
461
|
|
|
423
462
|
def _add_read_route(self, table, formatter, storage, tag, expandable, model_schema, lookup_field="id") -> None:
|
|
@@ -449,6 +488,13 @@ class FlashAPI:
|
|
|
449
488
|
item = self._expand_items([item], expand, expandable)[0]
|
|
450
489
|
|
|
451
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
|
+
|
|
452
498
|
return create_item_response(item, formatter)
|
|
453
499
|
|
|
454
500
|
def _add_history_route(self, table, entity_name, lookup_field="id", model_schema=None) -> None:
|
|
@@ -10,9 +10,10 @@ 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
|
+
from flashapi.features.health import get_health_check
|
|
16
17
|
from flashapi.inspectors import inspect_model
|
|
17
18
|
from flashapi.storage.auto import AutoStorage
|
|
18
19
|
from flashapi.storage.sqlalchemy import SQLAlchemyStorage
|
|
@@ -138,6 +139,7 @@ def register_models(
|
|
|
138
139
|
_add_docs_routes(blueprint, all_schemas, custom_routes or [], flask_app=app)
|
|
139
140
|
|
|
140
141
|
_add_api_root_route(blueprint, all_schemas, base_path, docs)
|
|
142
|
+
_add_health_routes(app, session_factory)
|
|
141
143
|
|
|
142
144
|
app.register_blueprint(blueprint)
|
|
143
145
|
|
|
@@ -439,6 +441,13 @@ def _create_flask_routes(
|
|
|
439
441
|
if _metrics:
|
|
440
442
|
_metrics.record("READ", entity_name)
|
|
441
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
|
+
|
|
442
451
|
return jsonify(create_list_response(page_items, total, page, size, formatter))
|
|
443
452
|
|
|
444
453
|
if "read" in schema.permissions:
|
|
@@ -461,6 +470,13 @@ def _create_flask_routes(
|
|
|
461
470
|
item = _expand_items([item], expand, _exp, storage)[0]
|
|
462
471
|
|
|
463
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
|
+
|
|
464
480
|
return jsonify(create_item_response(item, formatter))
|
|
465
481
|
|
|
466
482
|
if "read" in schema.permissions and entity_audit:
|
|
@@ -726,3 +742,35 @@ def _create_flask_routes(
|
|
|
726
742
|
mimetype=CONTENT_TYPES[fmt],
|
|
727
743
|
headers={"Content-Disposition": f'attachment; filename="{_table}.{fmt}"'},
|
|
728
744
|
)
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def _add_health_routes(app, session_factory) -> None:
|
|
748
|
+
"""Add health check endpoints for production monitoring."""
|
|
749
|
+
from flask import jsonify
|
|
750
|
+
health_check = get_health_check()
|
|
751
|
+
|
|
752
|
+
@app.route("/health")
|
|
753
|
+
def liveness():
|
|
754
|
+
"""Liveness probe — is the application running?"""
|
|
755
|
+
return jsonify(health_check.liveness())
|
|
756
|
+
|
|
757
|
+
@app.route("/ready")
|
|
758
|
+
def readiness():
|
|
759
|
+
"""Readiness probe — is the application ready to serve traffic?"""
|
|
760
|
+
data, status_code = health_check.readiness()
|
|
761
|
+
return jsonify(data), status_code
|
|
762
|
+
|
|
763
|
+
# Register database check if using SQLAlchemy
|
|
764
|
+
if session_factory:
|
|
765
|
+
def check_database():
|
|
766
|
+
try:
|
|
767
|
+
session = session_factory()
|
|
768
|
+
session.execute("SELECT 1")
|
|
769
|
+
session.close()
|
|
770
|
+
return True
|
|
771
|
+
except Exception:
|
|
772
|
+
return False
|
|
773
|
+
health_check.register_check("database", check_database)
|
|
774
|
+
|
|
775
|
+
# Mark ready after all routes are registered
|
|
776
|
+
health_check.mark_ready()
|