chap-checker 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. chap_checker-0.1.0/PKG-INFO +139 -0
  2. chap_checker-0.1.0/README.md +111 -0
  3. chap_checker-0.1.0/pyproject.toml +124 -0
  4. chap_checker-0.1.0/src/chap_checker/__init__.py +10 -0
  5. chap_checker-0.1.0/src/chap_checker/__main__.py +6 -0
  6. chap_checker-0.1.0/src/chap_checker/alerts/__init__.py +6 -0
  7. chap_checker-0.1.0/src/chap_checker/alerts/base.py +99 -0
  8. chap_checker-0.1.0/src/chap_checker/alerts/slack.py +125 -0
  9. chap_checker-0.1.0/src/chap_checker/checks/__init__.py +37 -0
  10. chap_checker-0.1.0/src/chap_checker/checks/base.py +127 -0
  11. chap_checker-0.1.0/src/chap_checker/checks/dhis2_chap_modeling_app.py +118 -0
  12. chap_checker-0.1.0/src/chap_checker/checks/dhis2_chap_ping.py +56 -0
  13. chap_checker-0.1.0/src/chap_checker/checks/dhis2_chap_route.py +121 -0
  14. chap_checker-0.1.0/src/chap_checker/checks/dhis2_chap_system_info.py +103 -0
  15. chap_checker-0.1.0/src/chap_checker/checks/dhis2_ping.py +95 -0
  16. chap_checker-0.1.0/src/chap_checker/checks/dhis2_system_info.py +89 -0
  17. chap_checker-0.1.0/src/chap_checker/cli.py +951 -0
  18. chap_checker-0.1.0/src/chap_checker/client.py +112 -0
  19. chap_checker-0.1.0/src/chap_checker/config.py +255 -0
  20. chap_checker-0.1.0/src/chap_checker/dashboard.py +702 -0
  21. chap_checker-0.1.0/src/chap_checker/logging.py +32 -0
  22. chap_checker-0.1.0/src/chap_checker/output.py +69 -0
  23. chap_checker-0.1.0/src/chap_checker/runner.py +151 -0
  24. chap_checker-0.1.0/src/chap_checker/state.py +13 -0
  25. chap_checker-0.1.0/src/chap_checker/state_store.py +184 -0
  26. chap_checker-0.1.0/src/chap_checker/web.py +337 -0
  27. chap_checker-0.1.0/src/chap_checker/web_ui/_state.js +151 -0
  28. chap_checker-0.1.0/src/chap_checker/web_ui/index.html +102 -0
  29. chap_checker-0.1.0/src/chap_checker/web_ui/src/app.jsx +448 -0
  30. chap_checker-0.1.0/src/chap_checker/web_ui/src/card.jsx +537 -0
  31. chap_checker-0.1.0/src/chap_checker/web_ui/src/palette.jsx +105 -0
  32. chap_checker-0.1.0/src/chap_checker/web_ui/src/tweaks-panel.jsx +568 -0
  33. chap_checker-0.1.0/src/chap_checker/web_ui/vendor/babel.min.js +4 -0
  34. chap_checker-0.1.0/src/chap_checker/web_ui/vendor/react-dom.production.min.js +267 -0
  35. chap_checker-0.1.0/src/chap_checker/web_ui/vendor/react.production.min.js +31 -0
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.3
2
+ Name: chap-checker
3
+ Version: 0.1.0
4
+ Summary: Health-check CLI for DHIS2 instances integrated with chap-core; cron-friendly with Slack alerts on status transitions.
5
+ Keywords: dhis2,chap,chap-core,healthcheck,monitoring,slack,cli
6
+ Author: Morten Hansen
7
+ Author-email: Morten Hansen <morten@dhis2.org>
8
+ License: AGPL-3.0-or-later
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: System :: Monitoring
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Dist: typer>=0.24.2
17
+ Requires-Dist: httpx>=0.28.1
18
+ Requires-Dist: pydantic>=2.9.0
19
+ Requires-Dist: rich>=13.9.0
20
+ Requires-Dist: textual>=8.0.0
21
+ Requires-Dist: fastapi>=0.115.0
22
+ Requires-Dist: uvicorn[standard]>=0.30.0
23
+ Requires-Python: >=3.13
24
+ Project-URL: Homepage, https://github.com/dhis2-chap/chap-checker
25
+ Project-URL: Repository, https://github.com/dhis2-chap/chap-checker
26
+ Project-URL: Issues, https://github.com/dhis2-chap/chap-checker/issues
27
+ Description-Content-Type: text/markdown
28
+
29
+ # chap-checker
30
+
31
+ [![CI](https://github.com/dhis2-chap/chap-checker/actions/workflows/ci.yml/badge.svg)](https://github.com/dhis2-chap/chap-checker/actions/workflows/ci.yml)
32
+ [![PyPI version](https://img.shields.io/pypi/v/chap-checker)](https://pypi.org/project/chap-checker/)
33
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
34
+ [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
35
+ [![Documentation](https://img.shields.io/badge/docs-mkdocs-blue.svg)](https://dhis2-chap.github.io/chap-checker/)
36
+
37
+ A small command-line health-check and alerting tool for DHIS2 instances that
38
+ integrate with `chap-core` via a DHIS2 route. Cron-friendly, with optional
39
+ Slack alerts on status transitions and a Textual TUI dashboard for the
40
+ at-a-glance "leave it on a TV" view.
41
+
42
+ **Documentation:** <https://dhis2-chap.github.io/chap-checker>
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ # One-shot run without installing (no PATH pollution):
48
+ uvx chap-checker --version
49
+ uvx chap-checker verify --url https://dhis2.example.com --token-env DHIS2_TOKEN
50
+
51
+ # Persistent install into uv's isolated tool environment:
52
+ uv tool install chap-checker
53
+ chap-checker --version
54
+
55
+ # Upgrade to the latest release later:
56
+ uv tool upgrade chap-checker
57
+
58
+ # Or, if you're embedding into another uv project:
59
+ uv add chap-checker
60
+ ```
61
+
62
+ ## Quick start
63
+
64
+ Ad-hoc against a single instance (credentials resolved safely):
65
+
66
+ ```bash
67
+ # With a DHIS2 Personal Access Token (recommended on modern servers):
68
+ export PROD_TOKEN=...
69
+ chap-checker verify \
70
+ --url https://dhis2.example.com \
71
+ --token-env PROD_TOKEN
72
+
73
+ # With a password (Basic auth) read from a named env var:
74
+ export PROD_PASSWORD=...
75
+ chap-checker verify \
76
+ --url https://dhis2.example.com \
77
+ --username admin \
78
+ --password-env PROD_PASSWORD
79
+
80
+ # Omit --password / --token entirely and you'll be prompted on the
81
+ # terminal (hidden input). DHIS2_TOKEN / DHIS2_PASSWORD env vars
82
+ # work as defaults too. Passing --password / --token inline still
83
+ # works but is discouraged - the value lands in shell history and
84
+ # `ps` output. Token and password flags are mutually exclusive.
85
+ ```
86
+
87
+ Multiple instances in `./chap-checker.toml`:
88
+
89
+ ```toml
90
+ [instances.prod]
91
+ url = "https://dhis2.example.com"
92
+ username = "ops"
93
+ password_env = "PROD_PASS"
94
+ alerts = ["slack"]
95
+
96
+ [alerts.slack]
97
+ webhook_url_env = "SLACK_WEBHOOK_URL"
98
+ ```
99
+
100
+ Then `chap-checker verify` runs every configured instance and pages Slack on
101
+ status transitions. See [chap-checker.toml.example](./chap-checker.toml.example)
102
+ for the full template.
103
+
104
+ The TUI:
105
+
106
+ ```bash
107
+ chap-checker dashboard
108
+ ```
109
+
110
+ ## Built-in checks
111
+
112
+ Two namespaces — `dhis2_*` probes DHIS2 itself, `dhis2_chap_*` probes
113
+ chap-core through the DHIS2 route. Each tile in the dashboard, each row in
114
+ `chap-checker checks list`, each entry in the JSON output:
115
+
116
+ - `dhis2_ping` — `/api/me`
117
+ - `dhis2_system_info` — `/api/system/info`
118
+ - `dhis2_chap_route` — `/api/routes?filter=code:eq:chap`
119
+ - `dhis2_chap_ping` — `/api/routes/chap/run/health`
120
+ - `dhis2_chap_system_info` — `/api/routes/chap/run/system/info`
121
+ - `dhis2_chap_modeling_app` — `/api/apps` (matched by `app_hub_id`)
122
+
123
+ Full reference + endpoint details: [Built-in checks](https://dhis2-chap.github.io/chap-checker/guides/checks/).
124
+
125
+ ## Development
126
+
127
+ ```bash
128
+ make install
129
+ make lint # ruff + mypy + pyright
130
+ make test
131
+ make docs # serve docs locally
132
+ ```
133
+
134
+ See [Development](https://dhis2-chap.github.io/chap-checker/guides/development/)
135
+ for repo layout and house rules.
136
+
137
+ ## License
138
+
139
+ AGPL-3.0-or-later
@@ -0,0 +1,111 @@
1
+ # chap-checker
2
+
3
+ [![CI](https://github.com/dhis2-chap/chap-checker/actions/workflows/ci.yml/badge.svg)](https://github.com/dhis2-chap/chap-checker/actions/workflows/ci.yml)
4
+ [![PyPI version](https://img.shields.io/pypi/v/chap-checker)](https://pypi.org/project/chap-checker/)
5
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
6
+ [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
7
+ [![Documentation](https://img.shields.io/badge/docs-mkdocs-blue.svg)](https://dhis2-chap.github.io/chap-checker/)
8
+
9
+ A small command-line health-check and alerting tool for DHIS2 instances that
10
+ integrate with `chap-core` via a DHIS2 route. Cron-friendly, with optional
11
+ Slack alerts on status transitions and a Textual TUI dashboard for the
12
+ at-a-glance "leave it on a TV" view.
13
+
14
+ **Documentation:** <https://dhis2-chap.github.io/chap-checker>
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ # One-shot run without installing (no PATH pollution):
20
+ uvx chap-checker --version
21
+ uvx chap-checker verify --url https://dhis2.example.com --token-env DHIS2_TOKEN
22
+
23
+ # Persistent install into uv's isolated tool environment:
24
+ uv tool install chap-checker
25
+ chap-checker --version
26
+
27
+ # Upgrade to the latest release later:
28
+ uv tool upgrade chap-checker
29
+
30
+ # Or, if you're embedding into another uv project:
31
+ uv add chap-checker
32
+ ```
33
+
34
+ ## Quick start
35
+
36
+ Ad-hoc against a single instance (credentials resolved safely):
37
+
38
+ ```bash
39
+ # With a DHIS2 Personal Access Token (recommended on modern servers):
40
+ export PROD_TOKEN=...
41
+ chap-checker verify \
42
+ --url https://dhis2.example.com \
43
+ --token-env PROD_TOKEN
44
+
45
+ # With a password (Basic auth) read from a named env var:
46
+ export PROD_PASSWORD=...
47
+ chap-checker verify \
48
+ --url https://dhis2.example.com \
49
+ --username admin \
50
+ --password-env PROD_PASSWORD
51
+
52
+ # Omit --password / --token entirely and you'll be prompted on the
53
+ # terminal (hidden input). DHIS2_TOKEN / DHIS2_PASSWORD env vars
54
+ # work as defaults too. Passing --password / --token inline still
55
+ # works but is discouraged - the value lands in shell history and
56
+ # `ps` output. Token and password flags are mutually exclusive.
57
+ ```
58
+
59
+ Multiple instances in `./chap-checker.toml`:
60
+
61
+ ```toml
62
+ [instances.prod]
63
+ url = "https://dhis2.example.com"
64
+ username = "ops"
65
+ password_env = "PROD_PASS"
66
+ alerts = ["slack"]
67
+
68
+ [alerts.slack]
69
+ webhook_url_env = "SLACK_WEBHOOK_URL"
70
+ ```
71
+
72
+ Then `chap-checker verify` runs every configured instance and pages Slack on
73
+ status transitions. See [chap-checker.toml.example](./chap-checker.toml.example)
74
+ for the full template.
75
+
76
+ The TUI:
77
+
78
+ ```bash
79
+ chap-checker dashboard
80
+ ```
81
+
82
+ ## Built-in checks
83
+
84
+ Two namespaces — `dhis2_*` probes DHIS2 itself, `dhis2_chap_*` probes
85
+ chap-core through the DHIS2 route. Each tile in the dashboard, each row in
86
+ `chap-checker checks list`, each entry in the JSON output:
87
+
88
+ - `dhis2_ping` — `/api/me`
89
+ - `dhis2_system_info` — `/api/system/info`
90
+ - `dhis2_chap_route` — `/api/routes?filter=code:eq:chap`
91
+ - `dhis2_chap_ping` — `/api/routes/chap/run/health`
92
+ - `dhis2_chap_system_info` — `/api/routes/chap/run/system/info`
93
+ - `dhis2_chap_modeling_app` — `/api/apps` (matched by `app_hub_id`)
94
+
95
+ Full reference + endpoint details: [Built-in checks](https://dhis2-chap.github.io/chap-checker/guides/checks/).
96
+
97
+ ## Development
98
+
99
+ ```bash
100
+ make install
101
+ make lint # ruff + mypy + pyright
102
+ make test
103
+ make docs # serve docs locally
104
+ ```
105
+
106
+ See [Development](https://dhis2-chap.github.io/chap-checker/guides/development/)
107
+ for repo layout and house rules.
108
+
109
+ ## License
110
+
111
+ AGPL-3.0-or-later
@@ -0,0 +1,124 @@
1
+ [project]
2
+ name = "chap-checker"
3
+ version = "0.1.0"
4
+ description = "Health-check CLI for DHIS2 instances integrated with chap-core; cron-friendly with Slack alerts on status transitions."
5
+ readme = "README.md"
6
+ authors = [{ name = "Morten Hansen", email = "morten@dhis2.org" }]
7
+ license = { text = "AGPL-3.0-or-later" }
8
+ requires-python = ">=3.13"
9
+ keywords = ["dhis2", "chap", "chap-core", "healthcheck", "monitoring", "slack", "cli"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Developers",
13
+ "Intended Audience :: System Administrators",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.13",
16
+ "Topic :: System :: Monitoring",
17
+ "Topic :: Software Development :: Libraries :: Python Modules",
18
+ ]
19
+ dependencies = [
20
+ "typer>=0.24.2",
21
+ "httpx>=0.28.1",
22
+ "pydantic>=2.9.0",
23
+ "rich>=13.9.0",
24
+ "textual>=8.0.0",
25
+ "fastapi>=0.115.0",
26
+ "uvicorn[standard]>=0.30.0",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/dhis2-chap/chap-checker"
31
+ Repository = "https://github.com/dhis2-chap/chap-checker"
32
+ Issues = "https://github.com/dhis2-chap/chap-checker/issues"
33
+
34
+ [project.scripts]
35
+ chap-checker = "chap_checker.cli:main"
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "coverage[toml]>=7.6.0",
40
+ "mkdocs>=1.6.0",
41
+ "mkdocs-material>=9.5.0",
42
+ "mypy>=1.13.0",
43
+ "pyright>=1.1.400",
44
+ "pytest>=8.3.0",
45
+ "pytest-asyncio>=0.24.0",
46
+ "ruff>=0.7.0",
47
+ ]
48
+
49
+ [build-system]
50
+ requires = ["uv_build>=0.9.0,<0.12.0"]
51
+ build-backend = "uv_build"
52
+
53
+ [tool.ruff]
54
+ target-version = "py313"
55
+ line-length = 120
56
+
57
+ [tool.ruff.lint]
58
+ fixable = ["ALL"]
59
+ select = ["E", "W", "F", "I", "D"]
60
+ ignore = ["D203", "D213"]
61
+
62
+ [tool.ruff.lint.pydocstyle]
63
+ convention = "google"
64
+
65
+ [tool.ruff.lint.per-file-ignores]
66
+ "tests/**/*.py" = ["D"]
67
+ "**/__init__.py" = ["D104"]
68
+ "src/**/*.py" = ["D102", "D105", "D107"]
69
+
70
+ [tool.ruff.format]
71
+ quote-style = "double"
72
+ indent-style = "space"
73
+ skip-magic-trailing-comma = false
74
+ docstring-code-format = true
75
+ docstring-code-line-length = "dynamic"
76
+
77
+ [tool.pytest.ini_options]
78
+ asyncio_mode = "auto"
79
+ testpaths = ["tests"]
80
+ norecursedirs = [".git", ".venv", "__pycache__"]
81
+
82
+ [tool.coverage.run]
83
+ branch = true
84
+ relative_files = true
85
+ source = ["chap_checker"]
86
+
87
+ [tool.coverage.report]
88
+ exclude_also = ["if TYPE_CHECKING:"]
89
+ precision = 2
90
+ show_missing = true
91
+ skip_covered = true
92
+
93
+ [tool.mypy]
94
+ python_version = "3.13"
95
+ warn_return_any = true
96
+ warn_unused_configs = true
97
+ disallow_untyped_defs = true
98
+ check_untyped_defs = true
99
+ no_implicit_optional = true
100
+ warn_unused_ignores = true
101
+ strict_equality = true
102
+ mypy_path = ["src"]
103
+
104
+ [[tool.mypy.overrides]]
105
+ module = "tests.*"
106
+ disallow_untyped_defs = false
107
+
108
+ [tool.pyright]
109
+ include = ["src", "tests"]
110
+ exclude = ["**/.venv"]
111
+ pythonVersion = "3.13"
112
+ typeCheckingMode = "strict"
113
+ useLibraryCodeForTypes = true
114
+ reportPrivateUsage = false
115
+ reportUnusedFunction = false
116
+ reportUnknownMemberType = false
117
+ reportUnknownArgumentType = false
118
+ reportUnknownParameterType = false
119
+ reportUnknownVariableType = false
120
+ reportUnknownLambdaType = false
121
+ reportMissingTypeArgument = false
122
+ reportMissingTypeStubs = false
123
+ reportMissingImports = "warning"
124
+ reportMissingModuleSource = false
@@ -0,0 +1,10 @@
1
+ """chap-checker - run a suite of checks against a DHIS2 server."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("chap-checker")
7
+ except PackageNotFoundError: # pragma: no cover
8
+ __version__ = "0.0.0"
9
+
10
+ __all__ = ["__version__"]
@@ -0,0 +1,6 @@
1
+ """Allow ``python -m chap_checker`` to invoke the CLI."""
2
+
3
+ from chap_checker.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,6 @@
1
+ """Alerter implementations (currently: Slack Incoming Webhook)."""
2
+
3
+ from chap_checker.alerts.base import Alerter, AlerterBinding, Transition, TransitionKind
4
+ from chap_checker.alerts.slack import SlackAlerter
5
+
6
+ __all__ = ["Alerter", "AlerterBinding", "SlackAlerter", "Transition", "TransitionKind"]
@@ -0,0 +1,99 @@
1
+ """Alerter protocol and the Transition pydantic model that drives it."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from datetime import datetime
7
+ from typing import ClassVar, Literal, Protocol, TypeVar, runtime_checkable
8
+
9
+ from pydantic import BaseModel, ConfigDict
10
+
11
+ from chap_checker.checks.base import Status
12
+
13
+ TransitionKind = Literal["failure", "recovery"]
14
+
15
+
16
+ class Transition(BaseModel):
17
+ """A single status change worth alerting on.
18
+
19
+ Emitted by :func:`chap_checker.state_store.compute_transitions` when a
20
+ check's status flips between OK and a non-OK family member.
21
+ """
22
+
23
+ kind: TransitionKind
24
+ target_name: str
25
+ target_url: str
26
+ check_name: str
27
+ previous_status: Status
28
+ current_status: Status
29
+ message: str
30
+ duration_ms: float
31
+ occurred_at: datetime
32
+
33
+
34
+ @runtime_checkable
35
+ class Alerter(Protocol):
36
+ """Protocol all alerters implement.
37
+
38
+ Implementations *may* raise on delivery failure - the dispatcher
39
+ (:func:`chap_checker.cli._dispatch_alerts`) catches the exception, logs
40
+ it, and skips the state-file save so the transition is retried on the
41
+ next run. Alert delivery failures never change the run's exit code
42
+ regardless of whether the alerter raises or swallows.
43
+ """
44
+
45
+ name: ClassVar[str]
46
+
47
+ async def notify(self, transitions: list[Transition]) -> None:
48
+ """Send the given transitions out-of-band.
49
+
50
+ May raise on transport / non-2xx responses; the dispatcher decides
51
+ whether to surface the failure.
52
+ """
53
+ ...
54
+
55
+
56
+ class AlerterBinding(BaseModel):
57
+ """Pairs a runtime :class:`Alerter` instance with the statuses it cares about."""
58
+
59
+ model_config = ConfigDict(arbitrary_types_allowed=True)
60
+
61
+ alerter: Alerter
62
+ notify_on: set[Status]
63
+
64
+
65
+ _ALERTER_CLASSES: dict[str, type[Alerter]] = {}
66
+
67
+ _TAlerter = TypeVar("_TAlerter", bound=type[Alerter])
68
+
69
+
70
+ def register_alerter(name: str) -> Callable[[_TAlerter], _TAlerter]:
71
+ """Class decorator: register an alerter class under ``name``.
72
+
73
+ This is currently a discovery aid - the CLI's per-alerter config wiring
74
+ (:func:`chap_checker.cli._build_alerters`) still instantiates known
75
+ alerters by hand, but the registry makes new alerters introspectable
76
+ and is the seam for a future plugin-style config layer.
77
+
78
+ Example:
79
+ @register_alerter("slack")
80
+ class SlackAlerter:
81
+ name = "slack"
82
+ ...
83
+ """
84
+
85
+ def deco(cls: _TAlerter) -> _TAlerter:
86
+ _ALERTER_CLASSES[name] = cls
87
+ return cls
88
+
89
+ return deco
90
+
91
+
92
+ def alerter_class(name: str) -> type[Alerter] | None:
93
+ """Return the registered alerter class for ``name``, or ``None``."""
94
+ return _ALERTER_CLASSES.get(name)
95
+
96
+
97
+ def all_alerter_classes() -> dict[str, type[Alerter]]:
98
+ """Return a copy of the alerter-class registry keyed by name."""
99
+ return dict(_ALERTER_CLASSES)
@@ -0,0 +1,125 @@
1
+ """Slack Incoming Webhook alerter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import ClassVar, Literal
6
+
7
+ import httpx
8
+ from pydantic import BaseModel
9
+
10
+ from chap_checker.alerts.base import Transition, register_alerter
11
+ from chap_checker.checks.base import Status
12
+ from chap_checker.logging import get_logger
13
+
14
+ _log = get_logger("alerts.slack")
15
+
16
+ # Slack's status colors (from their brand guide; render well on dark and light).
17
+ _COLOR_BY_STATUS: dict[Status, str] = {
18
+ Status.OK: "#2EB67D", # green
19
+ Status.WARN: "#ECB22E", # yellow
20
+ Status.FAIL: "#E01E5A", # red
21
+ Status.ERROR: "#E01E5A", # red (same family as FAIL)
22
+ }
23
+
24
+
25
+ class SlackBlockText(BaseModel):
26
+ """Slack Block Kit text element (``plain_text`` or ``mrkdwn``)."""
27
+
28
+ type: Literal["plain_text", "mrkdwn"]
29
+ text: str
30
+
31
+
32
+ class SlackBlock(BaseModel):
33
+ """Single Slack Block Kit block (header or section)."""
34
+
35
+ type: Literal["header", "section"]
36
+ text: SlackBlockText
37
+
38
+
39
+ class SlackAttachment(BaseModel):
40
+ """Legacy Slack attachment that gives Block Kit a colored left border.
41
+
42
+ Block Kit blocks on their own can't render the vertical color stripe;
43
+ wrapping the same blocks in an attachment with ``color`` gets it back.
44
+ """
45
+
46
+ color: str
47
+ blocks: list[SlackBlock]
48
+
49
+
50
+ class SlackPayload(BaseModel):
51
+ """Body posted to a Slack Incoming Webhook."""
52
+
53
+ text: str
54
+ blocks: list[SlackBlock]
55
+ attachments: list[SlackAttachment]
56
+
57
+
58
+ @register_alerter("slack")
59
+ class SlackAlerter:
60
+ """POST a Block Kit message to a Slack Incoming Webhook URL.
61
+
62
+ Always raises on transport / 5xx so the cron-side dispatcher can decide
63
+ whether to commit state (it doesn't) and whether to surface the failure
64
+ in exit code (it doesn't - just logs).
65
+ """
66
+
67
+ name: ClassVar[str] = "slack"
68
+
69
+ def __init__(
70
+ self,
71
+ webhook_url: str,
72
+ timeout_s: float = 10.0,
73
+ transport: httpx.AsyncBaseTransport | None = None,
74
+ ) -> None:
75
+ self._webhook_url = webhook_url
76
+ self._timeout_s = timeout_s
77
+ self._transport = transport
78
+
79
+ async def notify(self, transitions: list[Transition]) -> None:
80
+ """POST the transitions to Slack.
81
+
82
+ Raises on transport errors or non-2xx responses. The caller is
83
+ responsible for deciding what to do with the failure (the cron-side
84
+ dispatcher swallows it but skips the state save so the transition
85
+ retries next run).
86
+ """
87
+ if not transitions:
88
+ return
89
+ payload = _build_payload(transitions)
90
+ async with httpx.AsyncClient(timeout=self._timeout_s, transport=self._transport) as client:
91
+ response = await client.post(self._webhook_url, json=payload.model_dump(mode="json"))
92
+ if response.status_code >= 400:
93
+ raise RuntimeError(f"slack webhook returned {response.status_code}: {response.text[:200]}")
94
+
95
+
96
+ def _color_for(transition: Transition) -> str:
97
+ if transition.kind == "recovery":
98
+ return _COLOR_BY_STATUS[Status.OK]
99
+ return _COLOR_BY_STATUS.get(transition.current_status, _COLOR_BY_STATUS[Status.FAIL])
100
+
101
+
102
+ def _build_payload(transitions: list[Transition]) -> SlackPayload:
103
+ failures = [t for t in transitions if t.kind == "failure"]
104
+ recoveries = [t for t in transitions if t.kind == "recovery"]
105
+ failure_word = "failure" if len(failures) == 1 else "failures"
106
+ recovery_word = "recovery" if len(recoveries) == 1 else "recoveries"
107
+ summary = f"chap-checker: {len(failures)} new {failure_word}, {len(recoveries)} {recovery_word}"
108
+
109
+ header_blocks: list[SlackBlock] = [
110
+ SlackBlock(type="header", text=SlackBlockText(type="plain_text", text=summary)),
111
+ ]
112
+
113
+ attachments: list[SlackAttachment] = []
114
+ for t in transitions:
115
+ label = "FAILURE" if t.kind == "failure" else "RECOVERY"
116
+ status_label = t.current_status.value.upper()
117
+ body = f"*{label} — `{t.target_name}`*\n{t.target_url}\n`{t.check_name}` *{status_label}* {t.message}"
118
+ attachments.append(
119
+ SlackAttachment(
120
+ color=_color_for(t),
121
+ blocks=[SlackBlock(type="section", text=SlackBlockText(type="mrkdwn", text=body))],
122
+ )
123
+ )
124
+
125
+ return SlackPayload(text=summary, blocks=header_blocks, attachments=attachments)
@@ -0,0 +1,37 @@
1
+ """Check implementations.
2
+
3
+ Importing this package triggers each check module, which registers itself
4
+ via :func:`chap_checker.checks.base.register_check`.
5
+ """
6
+
7
+ from chap_checker.checks import (
8
+ dhis2_chap_modeling_app,
9
+ dhis2_chap_ping,
10
+ dhis2_chap_route,
11
+ dhis2_chap_system_info,
12
+ dhis2_ping,
13
+ dhis2_system_info,
14
+ )
15
+ from chap_checker.checks.base import (
16
+ Check,
17
+ CheckResult,
18
+ Status,
19
+ all_checks,
20
+ register_check,
21
+ resolve_checks,
22
+ )
23
+
24
+ __all__ = [
25
+ "Check",
26
+ "CheckResult",
27
+ "Status",
28
+ "all_checks",
29
+ "dhis2_chap_modeling_app",
30
+ "dhis2_chap_ping",
31
+ "dhis2_chap_route",
32
+ "dhis2_chap_system_info",
33
+ "dhis2_ping",
34
+ "dhis2_system_info",
35
+ "register_check",
36
+ "resolve_checks",
37
+ ]