python-flashapi 0.2.0__tar.gz → 0.3.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.3.0/.github/workflows/release-please.yml +48 -0
- python_flashapi-0.3.0/.release-please-manifest.json +3 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/CHANGELOG.md +17 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/PKG-INFO +8 -3
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/README.md +6 -1
- python_flashapi-0.3.0/docs/logo.svg +25 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/pyproject.toml +1 -1
- python_flashapi-0.3.0/release-please-config.json +10 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/adapters/django.py +37 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/adapters/fastapi.py +32 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/adapters/flask.py +34 -0
- python_flashapi-0.3.0/src/flashapi/features/health.py +69 -0
- python_flashapi-0.3.0/tests/test_health.py +42 -0
- python_flashapi-0.2.0/.github/workflows/publish.yml +0 -29
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/.github/workflows/ci.yml +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/.gitignore +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/LICENSE +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/NOTICE +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/docs/authentication.md +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/docs/custom-logic.md +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/docs/customization.md +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/docs/examples.md +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/docs/features.md +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/docs/framework-notes.md +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/docs/integration.md +0 -0
- /python_flashapi-0.2.0/docs/logo.svg → /python_flashapi-0.3.0/docs/logo1.svg +0 -0
- /python_flashapi-0.2.0/docs/logo1.svg → /python_flashapi-0.3.0/docs/logo2.svg +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/docs/relations.md +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/examples/fastapi_example.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/examples/flask_example.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/adapters/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/adapters/base.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/core/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/core/custom_routes.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/core/pluralize.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/core/relations.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/core/response.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/core/schema.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/core/visibility.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/django.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/docs/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/docs/openapi.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/fastapi.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/features/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/features/audit.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/features/auth.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/features/dashboard.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/features/export.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/features/filtering.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/features/pagination.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/features/rate_limit.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/features/search.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/features/sorting.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/features/webhooks.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/features/websocket.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/flask.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/inspectors/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/inspectors/base.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/inspectors/dataclass.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/inspectors/detect.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/inspectors/django.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/inspectors/pydantic.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/inspectors/sqlalchemy.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/py.typed +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/storage/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/storage/auto.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/storage/base.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/storage/orm.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/src/flashapi/storage/sqlalchemy.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_adapters/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_adapters/test_fastapi.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_adapters/test_flask.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_adapters/test_relations.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_adapters/test_soft_delete.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_core/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_core/test_pluralize.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_core/test_response.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_core/test_schema.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_core/test_visibility.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_features/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_features/test_audit.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_features/test_auth.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_features/test_dashboard.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_features/test_export.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_features/test_filtering.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_features/test_pagination.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_features/test_rate_limit.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_features/test_search.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_features/test_sorting.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_features/test_websocket.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_inspectors/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_inspectors/test_dataclass.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_inspectors/test_detect.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_inspectors/test_pydantic.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.0}/tests/test_storage/__init__.py +0 -0
- {python_flashapi-0.2.0 → python_flashapi-0.3.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 }}
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0](https://github.com/HackermanMe/flashapi/compare/v0.2.0...v0.3.0) (2026-08-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add production health check endpoints (/health and /ready) ([cc4e881](https://github.com/HackermanMe/flashapi/commit/cc4e881f09055fa990bb054298ebc8c1f55af7af))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* corriger les gradients et les couleurs dans les fichiers SVG du logo ([50aadaf](https://github.com/HackermanMe/flashapi/commit/50aadaff44919e5666621cc3f645dd34c1364a64))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
* add release-please version marker in README ([515aeb8](https://github.com/HackermanMe/flashapi/commit/515aeb83c2f021bebac25d9690c4726003769746))
|
|
19
|
+
|
|
3
20
|
## 0.1.0 (2026-07-09)
|
|
4
21
|
|
|
5
22
|
Initial release.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: python-flashapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -88,7 +88,12 @@ Part of the **FlashAPI Ecosystem** — ensuring SDK client compatibility across
|
|
|
88
88
|
## Installation
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
|
-
|
|
91
|
+
# x-release-please-start-version
|
|
92
|
+
pip install python-flashapi==0.2.0
|
|
93
|
+
# x-release-please-end
|
|
94
|
+
|
|
95
|
+
# With framework extras:
|
|
96
|
+
pip install python-flashapi[fastapi] # or [flask] or [django] or [all]
|
|
92
97
|
```
|
|
93
98
|
|
|
94
99
|
---
|
|
@@ -46,7 +46,12 @@ Part of the **FlashAPI Ecosystem** — ensuring SDK client compatibility across
|
|
|
46
46
|
## Installation
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
|
|
49
|
+
# x-release-please-start-version
|
|
50
|
+
pip install python-flashapi==0.2.0
|
|
51
|
+
# x-release-please-end
|
|
52
|
+
|
|
53
|
+
# With framework extras:
|
|
54
|
+
pip install python-flashapi[fastapi] # or [flask] or [django] or [all]
|
|
50
55
|
```
|
|
51
56
|
|
|
52
57
|
---
|
|
@@ -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>
|
|
@@ -12,6 +12,7 @@ from flashapi.core.schema import Model, ModelSchema
|
|
|
12
12
|
from flashapi.core.visibility import export_fields, filter_response, 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
|
|
|
@@ -113,6 +114,7 @@ def generate_urls(
|
|
|
113
114
|
))
|
|
114
115
|
|
|
115
116
|
urlpatterns.extend(_create_api_root_view(all_schemas, docs))
|
|
117
|
+
urlpatterns.extend(_create_health_views())
|
|
116
118
|
|
|
117
119
|
return urlpatterns
|
|
118
120
|
|
|
@@ -769,3 +771,38 @@ def _create_django_views(
|
|
|
769
771
|
patterns.append(path(f"{table}/<str:item_id>/history/", history_view, name=f"{table}_history"))
|
|
770
772
|
|
|
771
773
|
return patterns
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _create_health_views():
|
|
777
|
+
"""Create health check views for production monitoring."""
|
|
778
|
+
from django.http import JsonResponse
|
|
779
|
+
from django.urls import path
|
|
780
|
+
|
|
781
|
+
health_check = get_health_check()
|
|
782
|
+
|
|
783
|
+
def liveness_view(request):
|
|
784
|
+
"""Liveness probe — is the application running?"""
|
|
785
|
+
return JsonResponse(health_check.liveness())
|
|
786
|
+
|
|
787
|
+
def readiness_view(request):
|
|
788
|
+
"""Readiness probe — is the application ready to serve traffic?"""
|
|
789
|
+
data, status_code = health_check.readiness()
|
|
790
|
+
return JsonResponse(data, status=status_code)
|
|
791
|
+
|
|
792
|
+
# Register database check for Django ORM
|
|
793
|
+
def check_database():
|
|
794
|
+
try:
|
|
795
|
+
from django.db import connection
|
|
796
|
+
with connection.cursor() as cursor:
|
|
797
|
+
cursor.execute("SELECT 1")
|
|
798
|
+
return True
|
|
799
|
+
except Exception:
|
|
800
|
+
return False
|
|
801
|
+
|
|
802
|
+
health_check.register_check("database", check_database)
|
|
803
|
+
health_check.mark_ready()
|
|
804
|
+
|
|
805
|
+
return [
|
|
806
|
+
path("health/", liveness_view, name="health_liveness"),
|
|
807
|
+
path("ready/", readiness_view, name="health_readiness"),
|
|
808
|
+
]
|
|
@@ -14,6 +14,7 @@ from flashapi.core.visibility import export_fields, filter_response, writable_fi
|
|
|
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
|
|
|
@@ -13,6 +13,7 @@ from flashapi.core.schema import Model, ModelSchema
|
|
|
13
13
|
from flashapi.core.visibility import export_fields, filter_response, 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
|
|
|
@@ -726,3 +728,35 @@ def _create_flask_routes(
|
|
|
726
728
|
mimetype=CONTENT_TYPES[fmt],
|
|
727
729
|
headers={"Content-Disposition": f'attachment; filename="{_table}.{fmt}"'},
|
|
728
730
|
)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _add_health_routes(app, session_factory) -> None:
|
|
734
|
+
"""Add health check endpoints for production monitoring."""
|
|
735
|
+
from flask import jsonify
|
|
736
|
+
health_check = get_health_check()
|
|
737
|
+
|
|
738
|
+
@app.route("/health")
|
|
739
|
+
def liveness():
|
|
740
|
+
"""Liveness probe — is the application running?"""
|
|
741
|
+
return jsonify(health_check.liveness())
|
|
742
|
+
|
|
743
|
+
@app.route("/ready")
|
|
744
|
+
def readiness():
|
|
745
|
+
"""Readiness probe — is the application ready to serve traffic?"""
|
|
746
|
+
data, status_code = health_check.readiness()
|
|
747
|
+
return jsonify(data), status_code
|
|
748
|
+
|
|
749
|
+
# Register database check if using SQLAlchemy
|
|
750
|
+
if session_factory:
|
|
751
|
+
def check_database():
|
|
752
|
+
try:
|
|
753
|
+
session = session_factory()
|
|
754
|
+
session.execute("SELECT 1")
|
|
755
|
+
session.close()
|
|
756
|
+
return True
|
|
757
|
+
except Exception:
|
|
758
|
+
return False
|
|
759
|
+
health_check.register_check("database", check_database)
|
|
760
|
+
|
|
761
|
+
# Mark ready after all routes are registered
|
|
762
|
+
health_check.mark_ready()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Health check endpoints for production monitoring.
|
|
3
|
+
|
|
4
|
+
Provides /health (liveness) and /ready (readiness) endpoints for Kubernetes,
|
|
5
|
+
load balancers, and monitoring systems.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HealthCheck:
|
|
15
|
+
"""Health check manager with liveness and readiness probes."""
|
|
16
|
+
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self._start_time = time.time()
|
|
19
|
+
self._ready = False
|
|
20
|
+
self._checks: dict[str, callable] = {}
|
|
21
|
+
|
|
22
|
+
def mark_ready(self) -> None:
|
|
23
|
+
"""Mark the application as ready to receive traffic."""
|
|
24
|
+
self._ready = True
|
|
25
|
+
|
|
26
|
+
def register_check(self, name: str, check_fn: callable) -> None:
|
|
27
|
+
"""Register a custom readiness check (e.g., database connection)."""
|
|
28
|
+
self._checks[name] = check_fn
|
|
29
|
+
|
|
30
|
+
def liveness(self) -> dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Liveness probe — is the application running?
|
|
33
|
+
Returns 200 if alive, should return 5xx if deadlocked/crashed.
|
|
34
|
+
"""
|
|
35
|
+
uptime = int(time.time() - self._start_time)
|
|
36
|
+
return {"status": "ok", "uptime": uptime}
|
|
37
|
+
|
|
38
|
+
def readiness(self) -> tuple[dict[str, Any], int]:
|
|
39
|
+
"""
|
|
40
|
+
Readiness probe — is the application ready to serve traffic?
|
|
41
|
+
Returns 200 if ready, 503 if not yet ready or dependencies failing.
|
|
42
|
+
"""
|
|
43
|
+
if not self._ready:
|
|
44
|
+
return {"status": "not_ready", "reason": "Application still initializing"}, 503
|
|
45
|
+
|
|
46
|
+
failed_checks = []
|
|
47
|
+
for name, check_fn in self._checks.items():
|
|
48
|
+
try:
|
|
49
|
+
if not check_fn():
|
|
50
|
+
failed_checks.append(name)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
failed_checks.append(f"{name}: {e}")
|
|
53
|
+
|
|
54
|
+
if failed_checks:
|
|
55
|
+
return {
|
|
56
|
+
"status": "not_ready",
|
|
57
|
+
"failed_checks": failed_checks,
|
|
58
|
+
}, 503
|
|
59
|
+
|
|
60
|
+
uptime = int(time.time() - self._start_time)
|
|
61
|
+
return {"status": "ready", "uptime": uptime, "checks_passed": len(self._checks)}, 200
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_health = HealthCheck()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_health_check() -> HealthCheck:
|
|
68
|
+
"""Get the global health check instance."""
|
|
69
|
+
return _health
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Tests for health check endpoints."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from fastapi.testclient import TestClient
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from flashapi.adapters.fastapi import FlashAPI
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestItem(BaseModel):
|
|
11
|
+
name: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def app(tmp_path):
|
|
16
|
+
"""Create a test FastAPI app."""
|
|
17
|
+
db_path = str(tmp_path / "test.db")
|
|
18
|
+
flash = FlashAPI([TestItem], database=db_path)
|
|
19
|
+
return flash.app
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_liveness_probe(app):
|
|
23
|
+
"""Test GET /health returns liveness status."""
|
|
24
|
+
client = TestClient(app)
|
|
25
|
+
response = client.get("/health")
|
|
26
|
+
assert response.status_code == 200
|
|
27
|
+
data = response.json()
|
|
28
|
+
assert data["status"] == "ok"
|
|
29
|
+
assert "uptime" in data
|
|
30
|
+
assert isinstance(data["uptime"], int)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_readiness_probe(app):
|
|
34
|
+
"""Test GET /ready returns readiness status."""
|
|
35
|
+
client = TestClient(app)
|
|
36
|
+
response = client.get("/ready")
|
|
37
|
+
assert response.status_code == 200
|
|
38
|
+
data = response.json()
|
|
39
|
+
assert data["status"] == "ready"
|
|
40
|
+
assert "uptime" in data
|
|
41
|
+
assert "checks_passed" in data
|
|
42
|
+
assert data["checks_passed"] >= 0 # At least 0 checks (in-memory DB has no check)
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
name: Publish to PyPI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags: ['v*']
|
|
6
|
-
|
|
7
|
-
jobs:
|
|
8
|
-
publish:
|
|
9
|
-
runs-on: ubuntu-latest
|
|
10
|
-
|
|
11
|
-
steps:
|
|
12
|
-
- uses: actions/checkout@v4
|
|
13
|
-
|
|
14
|
-
- name: Set up Python
|
|
15
|
-
uses: actions/setup-python@v5
|
|
16
|
-
with:
|
|
17
|
-
python-version: '3.12'
|
|
18
|
-
|
|
19
|
-
- name: Install build tools
|
|
20
|
-
run: pip install build twine
|
|
21
|
-
|
|
22
|
-
- name: Build package
|
|
23
|
-
run: python -m build
|
|
24
|
-
|
|
25
|
-
- name: Publish to PyPI
|
|
26
|
-
run: twine upload dist/*
|
|
27
|
-
env:
|
|
28
|
-
TWINE_USERNAME: __token__
|
|
29
|
-
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|