austial-terminus 0.1.5__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.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .env
5
+ *.db
6
+ .pytest_cache/
7
+ .coverage
8
+ htmlcov/
9
+ dist/
10
+ build/
11
+ *.egg-info/
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: austial-terminus
3
+ Version: 0.1.5
4
+ Summary: Health-check aggregator and indicators (memory, database) -- mirrors @nestjs/terminus (no main-monorepo equivalent).
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: austial-common
7
+ Requires-Dist: austial-orm
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "austial-terminus"
3
+ dynamic = ["version"]
4
+ description = "Health-check aggregator and indicators (memory, database) -- mirrors @nestjs/terminus (no main-monorepo equivalent)."
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "austial-common",
8
+ "austial-orm",
9
+ ]
10
+
11
+ [build-system]
12
+ requires = ["hatchling", "hatch-vcs"]
13
+ build-backend = "hatchling.build"
14
+
15
+ [tool.hatch.build.targets.wheel]
16
+ packages = ["src/austial"]
17
+
18
+ [tool.uv.sources]
19
+ austial-common = { workspace = true }
20
+ austial-orm = { workspace = true }
21
+
22
+ [tool.hatch.version]
23
+ source = "vcs"
24
+
25
+ [tool.hatch.version.raw-options]
26
+ root = "../.."
@@ -0,0 +1,14 @@
1
+ from austial.terminus.database_health_indicator import DatabaseHealthIndicator
2
+ from austial.terminus.health_check_service import HealthCheckService, HealthIndicatorFunction
3
+ from austial.terminus.health_indicator import HealthCheckError, HealthIndicator, HealthIndicatorResult
4
+ from austial.terminus.memory_health_indicator import MemoryHealthIndicator
5
+
6
+ __all__ = [
7
+ "DatabaseHealthIndicator",
8
+ "HealthCheckService",
9
+ "HealthIndicatorFunction",
10
+ "HealthCheckError",
11
+ "HealthIndicator",
12
+ "HealthIndicatorResult",
13
+ "MemoryHealthIndicator",
14
+ ]
@@ -0,0 +1,26 @@
1
+ """``DatabaseHealthIndicator`` -- mirrors ``@nestjs/terminus``'s ``TypeOrmHealthIndicator``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from sqlalchemy import text
8
+
9
+ from austial.common.decorators.injectable import Inject, Injectable
10
+ from austial.orm.database_module import DATABASE_SESSION_FACTORY
11
+ from austial.terminus.health_indicator import HealthCheckError, HealthIndicator, HealthIndicatorResult
12
+
13
+
14
+ @Injectable()
15
+ class DatabaseHealthIndicator(HealthIndicator):
16
+ def __init__(self, session_factory: Any = Inject(DATABASE_SESSION_FACTORY)):
17
+ self._session_factory = session_factory
18
+
19
+ async def ping_check(self, key: str) -> HealthIndicatorResult:
20
+ try:
21
+ async with self._session_factory() as session:
22
+ await session.execute(text("SELECT 1"))
23
+ return self.get_status(key, True)
24
+ except Exception as exc: # noqa: BLE001
25
+ result = self.get_status(key, False, {"message": str(exc)})
26
+ raise HealthCheckError("Database health check failed", result) from exc
@@ -0,0 +1,42 @@
1
+ """``HealthCheckService`` -- mirrors ``@nestjs/terminus``'s ``HealthCheckService``.
2
+
3
+ Usage, identical in shape to real Nest/Terminus code::
4
+
5
+ result = await self.health_check_service.check([
6
+ lambda: self.memory.check_heap("memory_heap", 300 * 1024 * 1024),
7
+ lambda: self.database.ping_check("database"),
8
+ ])
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Awaitable, Callable
14
+
15
+ from austial.common.decorators.injectable import Injectable
16
+ from austial.terminus.health_indicator import HealthCheckError, HealthIndicatorResult
17
+
18
+ HealthIndicatorFunction = Callable[[], Awaitable[HealthIndicatorResult]]
19
+
20
+
21
+ @Injectable()
22
+ class HealthCheckService:
23
+ async def check(self, indicators: list[HealthIndicatorFunction]) -> dict:
24
+ info: HealthIndicatorResult = {}
25
+ error: HealthIndicatorResult = {}
26
+ details: HealthIndicatorResult = {}
27
+
28
+ for indicator in indicators:
29
+ try:
30
+ result = await indicator()
31
+ info.update(result)
32
+ details.update(result)
33
+ except HealthCheckError as exc:
34
+ error.update(exc.causes)
35
+ details.update(exc.causes)
36
+ except Exception as exc: # noqa: BLE001 - an indicator blew up unexpectedly
37
+ failure = {"unknown": {"status": "down", "message": str(exc)}}
38
+ error.update(failure)
39
+ details.update(failure)
40
+
41
+ status = "error" if error else "ok"
42
+ return {"status": status, "info": info, "error": error, "details": details}
@@ -0,0 +1,28 @@
1
+ """Base types for health indicators -- mirrors ``@nestjs/terminus``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ HealthIndicatorResult = dict[str, dict[str, Any]]
8
+
9
+
10
+ class HealthCheckError(Exception):
11
+ """Raised by an indicator to signal it's unhealthy; carries the same
12
+ ``HealthIndicatorResult`` shape that a healthy check would have returned."""
13
+
14
+ def __init__(self, message: str, causes: HealthIndicatorResult):
15
+ super().__init__(message)
16
+ self.causes = causes
17
+
18
+
19
+ class HealthIndicator:
20
+ """Base class for health indicators. Subclasses expose one or more
21
+ ``async def xxx_check(self, key: str, ...) -> HealthIndicatorResult``
22
+ methods built on top of :meth:`get_status`."""
23
+
24
+ def get_status(self, key: str, is_healthy: bool, data: dict | None = None) -> HealthIndicatorResult:
25
+ payload: dict[str, Any] = {"status": "up" if is_healthy else "down"}
26
+ if data:
27
+ payload.update(data)
28
+ return {key: payload}
@@ -0,0 +1,20 @@
1
+ """``MemoryHealthIndicator`` -- mirrors ``@nestjs/terminus``'s ``MemoryHealthIndicator``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import resource
6
+
7
+ from austial.common.decorators.injectable import Injectable
8
+ from austial.terminus.health_indicator import HealthCheckError, HealthIndicator, HealthIndicatorResult
9
+
10
+
11
+ @Injectable()
12
+ class MemoryHealthIndicator(HealthIndicator):
13
+ async def check_heap(self, key: str, threshold_bytes: int) -> HealthIndicatorResult:
14
+ # ru_maxrss is KB on Linux, bytes on macOS -- close enough for a demo indicator.
15
+ used_bytes = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss * 1024
16
+ is_healthy = used_bytes < threshold_bytes
17
+ result = self.get_status(key, is_healthy, {"used_bytes": used_bytes, "threshold_bytes": threshold_bytes})
18
+ if not is_healthy:
19
+ raise HealthCheckError("Memory usage exceeded threshold", result)
20
+ return result
File without changes
@@ -0,0 +1,48 @@
1
+ """Unit test for ``HealthCheckService`` -- the framework's own Terminus-style
2
+ health-check aggregator, exercised directly (no consumer app required): this
3
+ is a library test, so it builds indicators inline instead of importing a
4
+ sample app's ``HealthService``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import pytest
10
+
11
+ from austial.terminus import (
12
+ HealthCheckError,
13
+ HealthCheckService,
14
+ HealthIndicator,
15
+ HealthIndicatorResult,
16
+ MemoryHealthIndicator,
17
+ )
18
+
19
+
20
+ @pytest.mark.asyncio
21
+ async def test_reports_ok_status_when_all_indicators_pass():
22
+ service = HealthCheckService()
23
+ memory = MemoryHealthIndicator()
24
+
25
+ result = await service.check([lambda: memory.check_heap("memory_heap", 1024 * 1024 * 1024)])
26
+
27
+ assert result["status"] == "ok"
28
+ assert result["error"] == {}
29
+ assert result["info"]["memory_heap"]["status"] == "up"
30
+ assert result["details"] == result["info"]
31
+
32
+
33
+ @pytest.mark.asyncio
34
+ async def test_reports_error_status_when_an_indicator_fails():
35
+ service = HealthCheckService()
36
+
37
+ class _AlwaysDownIndicator(HealthIndicator):
38
+ async def ping(self, key: str) -> HealthIndicatorResult:
39
+ raise HealthCheckError("simulated failure", self.get_status(key, False, {"reason": "simulated"}))
40
+
41
+ indicator = _AlwaysDownIndicator()
42
+
43
+ result = await service.check([lambda: indicator.ping("dependency")])
44
+
45
+ assert result["status"] == "error"
46
+ assert result["info"] == {}
47
+ assert result["error"]["dependency"]["status"] == "down"
48
+ assert result["details"] == result["error"]