fastapi-ht 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ from fastapi_health_check.checks import AppAliveCheck, FunctionHealthCheck, HealthCheck, health_check
2
+ from fastapi_health_check.integration.fastapi import install_health_check
3
+ from fastapi_health_check.models import HealthCheckResult, HealthReport
4
+ from fastapi_health_check.registry import HealthRegistry
5
+ from fastapi_health_check.ui import render_health_report_page
6
+
7
+ __all__ = [
8
+ "AppAliveCheck",
9
+ "FunctionHealthCheck",
10
+ "HealthCheck",
11
+ "HealthCheckResult",
12
+ "HealthRegistry",
13
+ "HealthReport",
14
+ "health_check",
15
+ "install_health_check",
16
+ "render_health_report_page",
17
+ ]
@@ -0,0 +1 @@
1
+ """Static assets for the health check UI."""
@@ -0,0 +1,178 @@
1
+ :root {
2
+ color-scheme: light;
3
+ --bg: #f4f8fb;
4
+ --panel: #ffffff;
5
+ --panel-border: #d7e4e9;
6
+ --text: #1d2733;
7
+ --muted: #5b6975;
8
+ --ok: #009688;
9
+ --ok-soft: #d8f2ee;
10
+ --fail: #e15656;
11
+ --fail-soft: #ffe2e2;
12
+ --accent: #0b7fab;
13
+ --accent-soft: rgba(11, 127, 171, 0.14);
14
+ --shadow: 0 22px 50px rgba(15, 23, 42, 0.08);
15
+ }
16
+
17
+ * {
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ body {
22
+ margin: 0;
23
+ min-height: 100vh;
24
+ font-family: "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
25
+ color: var(--text);
26
+ background:
27
+ radial-gradient(circle at top left, var(--accent-soft), transparent 30%),
28
+ linear-gradient(180deg, #ffffff 0%, var(--bg) 100%);
29
+ }
30
+
31
+ .page {
32
+ width: min(960px, calc(100% - 32px));
33
+ margin: 0 auto;
34
+ padding: 56px 0 72px;
35
+ }
36
+
37
+ .hero {
38
+ display: grid;
39
+ gap: 18px;
40
+ margin-bottom: 28px;
41
+ }
42
+
43
+ .eyebrow {
44
+ margin: 0;
45
+ color: var(--accent);
46
+ font-size: 0.9rem;
47
+ font-weight: 700;
48
+ letter-spacing: 0.08em;
49
+ text-transform: uppercase;
50
+ }
51
+
52
+ h1 {
53
+ margin: 0;
54
+ font-size: clamp(2rem, 4vw, 3.35rem);
55
+ line-height: 1.02;
56
+ }
57
+
58
+ .subtitle {
59
+ margin: 0;
60
+ max-width: 42rem;
61
+ color: var(--muted);
62
+ font-size: 1rem;
63
+ line-height: 1.6;
64
+ }
65
+
66
+ .summary {
67
+ display: grid;
68
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
69
+ gap: 16px;
70
+ margin-bottom: 24px;
71
+ }
72
+
73
+ .summary-card,
74
+ .check-card {
75
+ background: var(--panel);
76
+ border: 1px solid var(--panel-border);
77
+ border-radius: 20px;
78
+ box-shadow: var(--shadow);
79
+ }
80
+
81
+ .summary-card {
82
+ padding: 22px;
83
+ }
84
+
85
+ .summary-label {
86
+ margin: 0 0 10px;
87
+ color: var(--muted);
88
+ font-size: 0.9rem;
89
+ text-transform: uppercase;
90
+ letter-spacing: 0.06em;
91
+ }
92
+
93
+ .summary-value {
94
+ margin: 0;
95
+ font-size: 1.8rem;
96
+ font-weight: 700;
97
+ }
98
+
99
+ .badge {
100
+ display: inline-flex;
101
+ align-items: center;
102
+ gap: 8px;
103
+ width: fit-content;
104
+ padding: 8px 12px;
105
+ border-radius: 999px;
106
+ font-weight: 700;
107
+ }
108
+
109
+ .badge.ok {
110
+ color: var(--ok);
111
+ background: var(--ok-soft);
112
+ }
113
+
114
+ .badge.fail {
115
+ color: var(--fail);
116
+ background: var(--fail-soft);
117
+ }
118
+
119
+ .checks {
120
+ display: grid;
121
+ gap: 16px;
122
+ }
123
+
124
+ .check-card {
125
+ padding: 20px 22px;
126
+ }
127
+
128
+ .check-header {
129
+ display: flex;
130
+ flex-wrap: wrap;
131
+ justify-content: space-between;
132
+ gap: 12px;
133
+ margin-bottom: 12px;
134
+ }
135
+
136
+ .check-name {
137
+ margin: 0;
138
+ font-size: 1.1rem;
139
+ }
140
+
141
+ .check-meta {
142
+ display: flex;
143
+ flex-wrap: wrap;
144
+ gap: 10px;
145
+ align-items: center;
146
+ }
147
+
148
+ .duration {
149
+ color: var(--muted);
150
+ font-variant-numeric: tabular-nums;
151
+ }
152
+
153
+ .message {
154
+ margin: 0;
155
+ color: var(--muted);
156
+ line-height: 1.6;
157
+ }
158
+
159
+ .empty {
160
+ padding: 28px;
161
+ text-align: center;
162
+ color: var(--muted);
163
+ background: rgba(255, 255, 255, 0.72);
164
+ border: 1px dashed var(--panel-border);
165
+ border-radius: 20px;
166
+ }
167
+
168
+ @media (max-width: 640px) {
169
+ .page {
170
+ width: min(100% - 24px, 960px);
171
+ padding-top: 32px;
172
+ }
173
+
174
+ .summary-card,
175
+ .check-card {
176
+ border-radius: 16px;
177
+ }
178
+ }
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>{{ title }}</title>
7
+ <style>
8
+ {{ styles }}
9
+ </style>
10
+ </head>
11
+ <body>
12
+ <main class="page">
13
+ <section class="hero">
14
+ <p class="eyebrow">FastAPI Health Check</p>
15
+ <h1>{{ title }}</h1>
16
+ <p class="subtitle">A compact operational view of backend dependencies, designed for quick status inspection.</p>
17
+ </section>
18
+ <section class="summary">
19
+ <article class="summary-card">
20
+ <p class="summary-label">Overall status</p>
21
+ <div class="badge {{ summary_class }}">{{ summary_label }}</div>
22
+ </article>
23
+ <article class="summary-card">
24
+ <p class="summary-label">Checks</p>
25
+ <p class="summary-value">{{ checks_count }}</p>
26
+ </article>
27
+ </section>
28
+ <section class="checks">
29
+ {{ checks_markup }}
30
+ </section>
31
+ </main>
32
+ </body>
33
+ </html>
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from inspect import isawaitable
5
+ from abc import ABC, abstractmethod
6
+ from time import perf_counter
7
+
8
+ from fastapi_health_check.models import HealthCheckResult
9
+
10
+ CheckHandler = Callable[[], str | None | Awaitable[str | None]]
11
+
12
+
13
+ class HealthCheck(ABC):
14
+ default_name = ""
15
+
16
+ def __init__(self, name: str | None = None) -> None:
17
+ resolved_name = name or self.default_name or self.__class__.__name__.removesuffix("Check").lower()
18
+ if not resolved_name:
19
+ msg = "health checks must define a name"
20
+ raise ValueError(msg)
21
+
22
+ self.name = resolved_name
23
+
24
+ async def run(self) -> HealthCheckResult:
25
+ started_at = perf_counter()
26
+
27
+ try:
28
+ message = await self.check()
29
+ except Exception as exc:
30
+ return HealthCheckResult(
31
+ name=self.name,
32
+ status="fail",
33
+ message=str(exc),
34
+ duration_ms=round((perf_counter() - started_at) * 1000, 3),
35
+ )
36
+
37
+ return HealthCheckResult(
38
+ name=self.name,
39
+ status="ok",
40
+ message=message,
41
+ duration_ms=round((perf_counter() - started_at) * 1000, 3),
42
+ )
43
+
44
+ @abstractmethod
45
+ async def check(self) -> str | None:
46
+ """Execute the health check and return an optional success message."""
47
+
48
+
49
+ class AppAliveCheck(HealthCheck):
50
+ default_name = "app_alive"
51
+
52
+ async def check(self) -> str | None:
53
+ return None
54
+
55
+
56
+ class FunctionHealthCheck(HealthCheck):
57
+ def __init__(self, name: str, handler: CheckHandler) -> None:
58
+ super().__init__(name=name)
59
+ self._handler = handler
60
+
61
+ async def check(self) -> str | None:
62
+ result = self._handler()
63
+ if isawaitable(result):
64
+ return await result
65
+
66
+ return result
67
+
68
+
69
+ def health_check(name: str, handler: CheckHandler) -> FunctionHealthCheck:
70
+ return FunctionHealthCheck(name=name, handler=handler)
@@ -0,0 +1,3 @@
1
+ from fastapi_health_check.integration.fastapi import install_health_check
2
+
3
+ __all__ = ["install_health_check"]
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import FastAPI, Request
4
+ from fastapi.responses import HTMLResponse, JSONResponse, Response
5
+
6
+ from fastapi_health_check.registry import HealthRegistry
7
+ from fastapi_health_check.ui import render_health_report_page
8
+
9
+
10
+ def install_health_check(
11
+ app: FastAPI,
12
+ registry: HealthRegistry,
13
+ *,
14
+ path: str = "/ht",
15
+ ui_title: str = "System Health",
16
+ include_in_schema: bool = False,
17
+ ) -> None:
18
+ async def health_endpoint(request: Request) -> Response:
19
+ report = await registry.run_checks()
20
+ status_code = 200 if report.is_healthy else 503
21
+ accept = request.headers.get("accept", "")
22
+ prefers_json = "application/json" in accept and "text/html" not in accept
23
+
24
+ if prefers_json:
25
+ return JSONResponse(status_code=status_code, content=report.model_dump(mode="json"))
26
+
27
+ content = render_health_report_page(report, title=ui_title)
28
+ return HTMLResponse(status_code=status_code, content=content)
29
+
30
+ app.add_api_route(
31
+ path,
32
+ health_endpoint,
33
+ methods=["GET"],
34
+ include_in_schema=include_in_schema,
35
+ name="health_check",
36
+ )
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+ HealthStatus = Literal["ok", "fail"]
8
+
9
+
10
+ class HealthCheckResult(BaseModel):
11
+ model_config = ConfigDict(frozen=True)
12
+
13
+ name: str = Field(min_length=1)
14
+ status: HealthStatus
15
+ message: str | None = None
16
+ duration_ms: float = Field(ge=0)
17
+
18
+
19
+ class HealthReport(BaseModel):
20
+ model_config = ConfigDict(frozen=True)
21
+
22
+ status: HealthStatus
23
+ checks: list[HealthCheckResult]
24
+
25
+ @classmethod
26
+ def from_checks(cls, checks: list[HealthCheckResult]) -> "HealthReport":
27
+ status: HealthStatus = "fail" if any(check.status == "fail" for check in checks) else "ok"
28
+ return cls(status=status, checks=checks)
29
+
30
+ @property
31
+ def is_healthy(self) -> bool:
32
+ return self.status == "ok"
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ from fastapi_health_check.checks import HealthCheck
6
+ from fastapi_health_check.models import HealthReport
7
+
8
+
9
+ class HealthRegistry:
10
+ def __init__(self, checks: Iterable[HealthCheck] | None = None) -> None:
11
+ self._checks: list[HealthCheck] = []
12
+
13
+ if checks is not None:
14
+ for check in checks:
15
+ self.register(check)
16
+
17
+ @property
18
+ def checks(self) -> tuple[HealthCheck, ...]:
19
+ return tuple(self._checks)
20
+
21
+ def register(self, check: HealthCheck) -> HealthCheck:
22
+ if any(existing.name == check.name for existing in self._checks):
23
+ msg = f"health check with name '{check.name}' is already registered"
24
+ raise ValueError(msg)
25
+
26
+ self._checks.append(check)
27
+ return check
28
+
29
+ async def run_checks(self) -> HealthReport:
30
+ results = [await check.run() for check in self._checks]
31
+ return HealthReport.from_checks(results)
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from html import escape
4
+ from importlib.resources import files
5
+
6
+ from fastapi_health_check.models import HealthCheckResult, HealthReport
7
+
8
+ _ASSETS = files("fastapi_health_check.assets")
9
+ _HTML_TEMPLATE = (_ASSETS / "health-report.html").read_text(encoding="utf-8")
10
+ _CSS_STYLES = (_ASSETS / "health-report.css").read_text(encoding="utf-8")
11
+
12
+
13
+ def render_health_report_page(report: HealthReport, *, title: str = "FastAPI Health Check") -> str:
14
+ summary_label = "Healthy" if report.is_healthy else "Issues detected"
15
+ summary_class = "ok" if report.is_healthy else "fail"
16
+ checks_markup = "\n".join(_render_check_card(check) for check in report.checks) or _render_empty_state()
17
+
18
+ replacements = {
19
+ "{{ title }}": escape(title),
20
+ "{{ styles }}": _CSS_STYLES,
21
+ "{{ summary_class }}": summary_class,
22
+ "{{ summary_label }}": escape(summary_label),
23
+ "{{ checks_count }}": str(len(report.checks)),
24
+ "{{ checks_markup }}": checks_markup,
25
+ }
26
+
27
+ html = _HTML_TEMPLATE
28
+ for marker, value in replacements.items():
29
+ html = html.replace(marker, value)
30
+
31
+ return html
32
+
33
+
34
+ def _render_check_card(check: HealthCheckResult) -> str:
35
+ message = escape(check.message) if check.message else "No additional details."
36
+ return f""" <article class="check-card">
37
+ <div class="check-header">
38
+ <h2 class="check-name">{escape(check.name)}</h2>
39
+ <div class="check-meta">
40
+ <div class="badge {check.status}">{escape(check.status.upper())}</div>
41
+ <span class="duration">{check.duration_ms:.3f} ms</span>
42
+ </div>
43
+ </div>
44
+ <p class="message">{message}</p>
45
+ </article>"""
46
+
47
+
48
+ def _render_empty_state() -> str:
49
+ return ' <div class="empty">No health checks are currently registered.</div>'
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-ht
3
+ Version: 0.1.0
4
+ Summary: Health check utilities for FastAPI applications.
5
+ Project-URL: Homepage, https://github.com/PinnLabs/fastapi-health-check
6
+ Project-URL: Repository, https://github.com/PinnLabs/fastapi-health-check
7
+ Project-URL: Issues, https://github.com/PinnLabs/fastapi-health-check/issues
8
+ Requires-Python: >=3.12
9
+ Requires-Dist: fastapi>=0.135.3
10
+ Requires-Dist: pydantic>=2.11.7
11
+ Description-Content-Type: text/markdown
12
+
13
+ <p align="center">
14
+ <img src="https://raw.githubusercontent.com/PinnLabs/fastapi-health-check/main/public/logo.png" alt="fastapi-health-check logo" width="520" style="display: block; margin:
15
+ 0 auto;" />
16
+ </p>
17
+
18
+ # fastapi-health-check
19
+
20
+ FastAPI health checks with a small public API, a visual status page, and JSON responses from the same endpoint.
21
+
22
+ ### Example interface
23
+
24
+ ![fastapi-health-check example interface](https://raw.githubusercontent.com/PinnLabs/fastapi-health-check/main/public/example_use.png)
25
+
26
+ ## What the library provides
27
+
28
+ - A base contract for advanced checks
29
+ - A lightweight registry for collecting and running checks
30
+ - A single `/ht` endpoint with HTML by default
31
+ - JSON responses when the client sends `Accept: application/json`
32
+ - A simple way to monitor any custom area of your system
33
+
34
+ ## Important note
35
+
36
+ The library does not ship with a database check by default.
37
+
38
+ The only built-in check today is `AppAliveCheck`, which reports that the application is up. Database, Redis, queues, external APIs, or any other monitored area are meant to be registered by the user.
39
+
40
+ ## Quick start
41
+
42
+ ```python
43
+ from fastapi import FastAPI
44
+
45
+ from fastapi_health_check import AppAliveCheck, HealthRegistry, health_check, install_health_check
46
+
47
+
48
+ app = FastAPI()
49
+ registry = HealthRegistry(
50
+ [
51
+ AppAliveCheck(),
52
+ health_check("database", lambda: "connection ok"),
53
+ health_check("redis", lambda: "cache reachable"),
54
+ ]
55
+ )
56
+
57
+ install_health_check(app, registry)
58
+ ```
59
+
60
+ This exposes `GET /ht`.
61
+
62
+ - In a browser, the route renders an HTML health page
63
+ - For automated integrations, the same route returns JSON when the client sends `Accept: application/json`
64
+
65
+ ## Monitoring custom areas
66
+
67
+ If you want to monitor anything beyond the built-in app liveness check, the easiest option is the `health_check()` factory.
68
+
69
+ You can use it for:
70
+
71
+ - databases
72
+ - Redis or cache layers
73
+ - background queues
74
+ - external APIs
75
+ - storage services
76
+ - internal domain-specific dependencies
77
+
78
+ ### Synchronous checks
79
+
80
+ ```python
81
+ from fastapi_health_check import health_check
82
+
83
+ database_check = health_check("database", lambda: "connection ok")
84
+ redis_check = health_check("redis", lambda: "cache reachable")
85
+ ```
86
+
87
+ ### Asynchronous checks
88
+
89
+ ```python
90
+ from fastapi_health_check import health_check
91
+
92
+
93
+ async def payments_api_check() -> str | None:
94
+ return "payments API available"
95
+
96
+
97
+ payments_check = health_check("payments_api", payments_api_check)
98
+ ```
99
+
100
+ ### Class-based checks for advanced cases
101
+
102
+ ```python
103
+ from fastapi_health_check import HealthCheck
104
+
105
+
106
+ class QueueCheck(HealthCheck):
107
+ default_name = "queue"
108
+
109
+ async def check(self) -> str | None:
110
+ return "queue connected"
111
+ ```
112
+
113
+ Use class-based checks when you want:
114
+
115
+ - dependency injection through `__init__`
116
+ - reusable state
117
+ - more structured custom behavior
118
+
119
+ ## Local manual testing
120
+
121
+ The repository includes a local example application at `src/examples/basic_app.py`.
122
+
123
+ Run it with:
124
+
125
+ ```bash
126
+ uv run uvicorn src.examples.basic_app:app --reload
127
+ ```
128
+
129
+ Then open:
130
+
131
+ - `http://127.0.0.1:8000/ht` for the HTML page
132
+ - `curl -H "Accept: application/json" http://127.0.0.1:8000/ht` for JSON
@@ -0,0 +1,13 @@
1
+ fastapi_health_check/__init__.py,sha256=yI61LWfPHqWWFfdwstj9WqlLJSINvcNGm2CVRcDeIx0,597
2
+ fastapi_health_check/checks.py,sha256=S8Wjs8KFBJHgZJ3mmIXZtBNYOz7HyzTQ-djlMSDQl1g,2010
3
+ fastapi_health_check/models.py,sha256=Klv2gFP0wEzgiP9Ahcw-nt8txTmBnT8d2vHbf63Z-7Y,828
4
+ fastapi_health_check/registry.py,sha256=eCFw-rn75RmHiBLxf3yI14uShTYgFtZxMGMGtF_Yn40,994
5
+ fastapi_health_check/ui.py,sha256=MimjOnjxJx5-CAdxPFknxc6VFIFskxeKucil7JYbEGk,1863
6
+ fastapi_health_check/assets/__init__.py,sha256=jVgFKZWCvqBvs82qYebKU1Ju1MGHJtA6n5bFynTMxqU,45
7
+ fastapi_health_check/assets/health-report.css,sha256=I_l6w0jc4Hbt1_GDhjg9BBgC1BeM2NBF6KBNVepESvM,2815
8
+ fastapi_health_check/assets/health-report.html,sha256=Yn6vDaAimwiSN7ZEvSFoyTiuTruoyUPglSe_9lzHm08,1003
9
+ fastapi_health_check/integration/__init__.py,sha256=ybFTWgVTbcaFxgSJZ6r9aCZeG_4eKl16s5WtBGwuykY,110
10
+ fastapi_health_check/integration/fastapi.py,sha256=Nb3yCf9ZrwH_0M-mu1AbHGwGtOsxVlOQbQpdjaO9q5I,1176
11
+ fastapi_ht-0.1.0.dist-info/METADATA,sha256=RiDhlM8I964qsCKEkAB7jq3G2sHPfZSTCzpH5yuHCTE,3590
12
+ fastapi_ht-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
13
+ fastapi_ht-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any