diagnostics-framework 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pam Painter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include LICENSE
2
+ include README.md
3
+ include sample_data.csv
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: diagnostics-framework
3
+ Version: 0.1.0
4
+ Summary: A pluggable diagnostics framework for running system-specific tests, plots, and reports.
5
+ Author: Pam Painter
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/pampainter/diagnostics-framework
8
+ Project-URL: Repository, https://github.com/pampainter/diagnostics-framework
9
+ Project-URL: Issues, https://github.com/pampainter/diagnostics-framework/issues
10
+ Keywords: diagnostics,testing,dashboard,streamlit,data-quality,monitoring
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Classifier: Topic :: Software Development :: Quality Assurance
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: streamlit>=1.30
26
+ Requires-Dist: pandas>=2.0
27
+ Requires-Dist: matplotlib>=3.7
28
+ Requires-Dist: seaborn>=0.13
29
+ Dynamic: license-file
30
+
31
+ # Diagnostics Framework
32
+
33
+ A pluggable Python framework for running diagnostic tests, generating plots, and viewing reports for any system — all from a Streamlit dashboard.
34
+
35
+ ## Overview
36
+
37
+ The framework uses a **plugin architecture**: each "system" is a self-contained module that registers its own diagnostic tests, plots, and reports using simple decorators. Adding a new system is as easy as dropping a new Python file into the `systems/` folder.
38
+
39
+ **Built-in example systems:**
40
+
41
+ | System | Description |
42
+ |--------|-------------|
43
+ | `generic_example` | Basic tabular data checks (nulls, ranges, emptiness) |
44
+ | `sensor_monitoring` | IoT sensor diagnostics with battery health, temperature validation, and status tracking |
45
+
46
+ ## Quick Start
47
+
48
+ ### Install
49
+
50
+ ```bash
51
+ # Create a virtual environment (Python 3.10+ required)
52
+ python -m venv .venv
53
+ source .venv/bin/activate
54
+
55
+ # Install in editable mode
56
+ pip install -e .
57
+ ```
58
+
59
+ ### Run the Dashboard
60
+
61
+ ```bash
62
+ streamlit run diagnostics_framework/app.py
63
+ ```
64
+
65
+ Open http://localhost:8501, select a system, upload a data file, and click **Run Diagnostics**.
66
+
67
+ A sample dataset is included at `sample_data.csv` for testing the `sensor_monitoring` system.
68
+
69
+ ## Project Structure
70
+
71
+ ```
72
+ diagnostics_framework/
73
+ ├── models.py # Dataclasses: DiagnosticResult, DiagnosticStatus, etc.
74
+ ├── registry.py # Singleton registry + decorator API
75
+ ├── runner.py # Test execution engine with error isolation
76
+ ├── app.py # Streamlit dashboard
77
+ └── systems/
78
+ ├── __init__.py # Auto-discovers all system modules
79
+ ├── generic_example.py # Example: generic tabular data checks
80
+ └── sensor_monitoring.py # Example: IoT sensor diagnostics
81
+ ```
82
+
83
+ ## Adding a New System
84
+
85
+ Create a new file in `diagnostics_framework/systems/` — it will be auto-discovered on import.
86
+
87
+ ```python
88
+ # diagnostics_framework/systems/my_system.py
89
+ from diagnostics_framework import (
90
+ register_system, register_test, register_plot, register_report,
91
+ DiagnosticResult, DiagnosticStatus,
92
+ )
93
+
94
+ SYSTEM_NAME = "my_system"
95
+
96
+ @register_system(SYSTEM_NAME, description="My custom system")
97
+ class MySystem:
98
+ pass
99
+
100
+ @register_test(SYSTEM_NAME, name="my_check", description="Validates something important")
101
+ def my_check(data):
102
+ # Your test logic here
103
+ return DiagnosticResult(
104
+ test_name="my_check",
105
+ status=DiagnosticStatus.PASS,
106
+ message="Everything looks good.",
107
+ )
108
+
109
+ @register_plot(SYSTEM_NAME, name="my_plot", description="Visualizes the data")
110
+ def my_plot(data):
111
+ import matplotlib.pyplot as plt
112
+ fig, ax = plt.subplots()
113
+ # Your plotting logic here
114
+ return fig
115
+
116
+ @register_report(SYSTEM_NAME, name="my_report", description="Summary of findings")
117
+ def my_report(data):
118
+ return "# My Report\n\nEverything is fine."
119
+ ```
120
+
121
+ That's it — the new system will appear in the dashboard dropdown automatically.
122
+
123
+ ## Decorator API
124
+
125
+ | Decorator | Purpose |
126
+ |-----------|---------|
127
+ | `@register_system(name, description, version)` | Register a new system |
128
+ | `@register_test(system, name, description)` | Add a diagnostic test |
129
+ | `@register_plot(system, name, description)` | Add a plot generator |
130
+ | `@register_report(system, name, description)` | Add a report generator |
131
+
132
+ ## Diagnostic Result Statuses
133
+
134
+ | Status | Meaning |
135
+ |--------|---------|
136
+ | `PASS` | Test passed successfully |
137
+ | `FAIL` | Test found a problem |
138
+ | `WARNING` | Test found something worth noting |
139
+ | `ERROR` | Test itself crashed (caught automatically by the runner) |
140
+
141
+ ## Programmatic Usage
142
+
143
+ You can also use the framework without the dashboard:
144
+
145
+ ```python
146
+ import pandas as pd
147
+ from diagnostics_framework import run_diagnostics
148
+
149
+ data = pd.read_csv("sample_data.csv")
150
+ summary = run_diagnostics("sensor_monitoring", data)
151
+
152
+ for result in summary.results:
153
+ print(f"[{result.status.value}] {result.test_name}: {result.message}")
154
+ ```
155
+
156
+ ## Dependencies
157
+
158
+ - Python >= 3.10
159
+ - streamlit
160
+ - pandas
161
+ - matplotlib
162
+ - seaborn
@@ -0,0 +1,132 @@
1
+ # Diagnostics Framework
2
+
3
+ A pluggable Python framework for running diagnostic tests, generating plots, and viewing reports for any system — all from a Streamlit dashboard.
4
+
5
+ ## Overview
6
+
7
+ The framework uses a **plugin architecture**: each "system" is a self-contained module that registers its own diagnostic tests, plots, and reports using simple decorators. Adding a new system is as easy as dropping a new Python file into the `systems/` folder.
8
+
9
+ **Built-in example systems:**
10
+
11
+ | System | Description |
12
+ |--------|-------------|
13
+ | `generic_example` | Basic tabular data checks (nulls, ranges, emptiness) |
14
+ | `sensor_monitoring` | IoT sensor diagnostics with battery health, temperature validation, and status tracking |
15
+
16
+ ## Quick Start
17
+
18
+ ### Install
19
+
20
+ ```bash
21
+ # Create a virtual environment (Python 3.10+ required)
22
+ python -m venv .venv
23
+ source .venv/bin/activate
24
+
25
+ # Install in editable mode
26
+ pip install -e .
27
+ ```
28
+
29
+ ### Run the Dashboard
30
+
31
+ ```bash
32
+ streamlit run diagnostics_framework/app.py
33
+ ```
34
+
35
+ Open http://localhost:8501, select a system, upload a data file, and click **Run Diagnostics**.
36
+
37
+ A sample dataset is included at `sample_data.csv` for testing the `sensor_monitoring` system.
38
+
39
+ ## Project Structure
40
+
41
+ ```
42
+ diagnostics_framework/
43
+ ├── models.py # Dataclasses: DiagnosticResult, DiagnosticStatus, etc.
44
+ ├── registry.py # Singleton registry + decorator API
45
+ ├── runner.py # Test execution engine with error isolation
46
+ ├── app.py # Streamlit dashboard
47
+ └── systems/
48
+ ├── __init__.py # Auto-discovers all system modules
49
+ ├── generic_example.py # Example: generic tabular data checks
50
+ └── sensor_monitoring.py # Example: IoT sensor diagnostics
51
+ ```
52
+
53
+ ## Adding a New System
54
+
55
+ Create a new file in `diagnostics_framework/systems/` — it will be auto-discovered on import.
56
+
57
+ ```python
58
+ # diagnostics_framework/systems/my_system.py
59
+ from diagnostics_framework import (
60
+ register_system, register_test, register_plot, register_report,
61
+ DiagnosticResult, DiagnosticStatus,
62
+ )
63
+
64
+ SYSTEM_NAME = "my_system"
65
+
66
+ @register_system(SYSTEM_NAME, description="My custom system")
67
+ class MySystem:
68
+ pass
69
+
70
+ @register_test(SYSTEM_NAME, name="my_check", description="Validates something important")
71
+ def my_check(data):
72
+ # Your test logic here
73
+ return DiagnosticResult(
74
+ test_name="my_check",
75
+ status=DiagnosticStatus.PASS,
76
+ message="Everything looks good.",
77
+ )
78
+
79
+ @register_plot(SYSTEM_NAME, name="my_plot", description="Visualizes the data")
80
+ def my_plot(data):
81
+ import matplotlib.pyplot as plt
82
+ fig, ax = plt.subplots()
83
+ # Your plotting logic here
84
+ return fig
85
+
86
+ @register_report(SYSTEM_NAME, name="my_report", description="Summary of findings")
87
+ def my_report(data):
88
+ return "# My Report\n\nEverything is fine."
89
+ ```
90
+
91
+ That's it — the new system will appear in the dashboard dropdown automatically.
92
+
93
+ ## Decorator API
94
+
95
+ | Decorator | Purpose |
96
+ |-----------|---------|
97
+ | `@register_system(name, description, version)` | Register a new system |
98
+ | `@register_test(system, name, description)` | Add a diagnostic test |
99
+ | `@register_plot(system, name, description)` | Add a plot generator |
100
+ | `@register_report(system, name, description)` | Add a report generator |
101
+
102
+ ## Diagnostic Result Statuses
103
+
104
+ | Status | Meaning |
105
+ |--------|---------|
106
+ | `PASS` | Test passed successfully |
107
+ | `FAIL` | Test found a problem |
108
+ | `WARNING` | Test found something worth noting |
109
+ | `ERROR` | Test itself crashed (caught automatically by the runner) |
110
+
111
+ ## Programmatic Usage
112
+
113
+ You can also use the framework without the dashboard:
114
+
115
+ ```python
116
+ import pandas as pd
117
+ from diagnostics_framework import run_diagnostics
118
+
119
+ data = pd.read_csv("sample_data.csv")
120
+ summary = run_diagnostics("sensor_monitoring", data)
121
+
122
+ for result in summary.results:
123
+ print(f"[{result.status.value}] {result.test_name}: {result.message}")
124
+ ```
125
+
126
+ ## Dependencies
127
+
128
+ - Python >= 3.10
129
+ - streamlit
130
+ - pandas
131
+ - matplotlib
132
+ - seaborn
@@ -0,0 +1,8 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from diagnostics_framework.registry import register_system, register_test, register_plot, register_report, registry
4
+ from diagnostics_framework.models import DiagnosticResult, DiagnosticStatus, DiagnosticSummary
5
+ from diagnostics_framework.runner import run_diagnostics, generate_plot, generate_report
6
+
7
+ # Auto-discover and register all system plugins
8
+ import diagnostics_framework.systems # noqa: F401
@@ -0,0 +1,185 @@
1
+ import io
2
+ import json
3
+
4
+ import pandas as pd
5
+ import streamlit as st
6
+
7
+ # Import systems to trigger decorator registration
8
+ import diagnostics_framework.systems # noqa: F401
9
+
10
+ from diagnostics_framework.models import DiagnosticStatus
11
+ from diagnostics_framework.registry import registry
12
+ from diagnostics_framework.runner import run_diagnostics, generate_plot, generate_report
13
+
14
+
15
+ STATUS_COLORS = {
16
+ DiagnosticStatus.PASS: "#28a745",
17
+ DiagnosticStatus.FAIL: "#dc3545",
18
+ DiagnosticStatus.WARNING: "#ffc107",
19
+ DiagnosticStatus.ERROR: "#6c757d",
20
+ }
21
+
22
+ STATUS_ICONS = {
23
+ DiagnosticStatus.PASS: "PASS",
24
+ DiagnosticStatus.FAIL: "FAIL",
25
+ DiagnosticStatus.WARNING: "WARN",
26
+ DiagnosticStatus.ERROR: "ERR",
27
+ }
28
+
29
+
30
+ def load_data(uploaded_file) -> pd.DataFrame | dict | None:
31
+ """Attempt to load an uploaded file as a DataFrame or dict."""
32
+ if uploaded_file is None:
33
+ return None
34
+
35
+ name = uploaded_file.name.lower()
36
+ try:
37
+ if name.endswith(".csv"):
38
+ return pd.read_csv(uploaded_file)
39
+ elif name.endswith(".json"):
40
+ content = json.load(uploaded_file)
41
+ if isinstance(content, list) and all(isinstance(r, dict) for r in content):
42
+ return pd.DataFrame(content)
43
+ return content
44
+ elif name.endswith((".xls", ".xlsx")):
45
+ return pd.read_excel(uploaded_file)
46
+ elif name.endswith(".parquet"):
47
+ return pd.read_parquet(io.BytesIO(uploaded_file.read()))
48
+ else:
49
+ return uploaded_file.read().decode("utf-8", errors="replace")
50
+ except Exception as e:
51
+ st.error(f"Failed to load file: {e}")
52
+ return None
53
+
54
+
55
+ def render_results(summary):
56
+ """Render diagnostic results as a styled table."""
57
+ st.subheader("Diagnostic Results")
58
+
59
+ cols = st.columns(4)
60
+ cols[0].metric("Total", len(summary.results))
61
+ cols[1].metric("Pass", summary.pass_count)
62
+ cols[2].metric("Fail", summary.fail_count)
63
+ cols[3].metric("Warn / Error", summary.warning_count + summary.error_count)
64
+
65
+ for result in summary.results:
66
+ color = STATUS_COLORS[result.status]
67
+ icon = STATUS_ICONS[result.status]
68
+ with st.container():
69
+ st.markdown(
70
+ f"<div style='border-left: 4px solid {color}; padding: 8px 12px; margin: 4px 0;'>"
71
+ f"<strong>[{icon}]</strong> <strong>{result.test_name}</strong><br/>"
72
+ f"{result.message}"
73
+ f"</div>",
74
+ unsafe_allow_html=True,
75
+ )
76
+ if result.details:
77
+ with st.expander("Details"):
78
+ st.json(result.details)
79
+
80
+
81
+ def render_plots(system_name, data):
82
+ """Render plot buttons and display generated plots."""
83
+ st.subheader("Plots")
84
+ plots = registry.get_plots(system_name)
85
+ if not plots:
86
+ st.info("No plots registered for this system.")
87
+ return
88
+
89
+ for plot_info in plots:
90
+ with st.expander(f"{plot_info.name} — {plot_info.description}"):
91
+ try:
92
+ fig = generate_plot(system_name, plot_info.name, data)
93
+ st.pyplot(fig)
94
+ except Exception as e:
95
+ st.error(f"Error generating plot: {e}")
96
+
97
+
98
+ def render_reports(system_name, data):
99
+ """Render report buttons and display generated reports."""
100
+ st.subheader("Reports")
101
+ reports = registry.get_reports(system_name)
102
+ if not reports:
103
+ st.info("No reports registered for this system.")
104
+ return
105
+
106
+ for report_info in reports:
107
+ with st.expander(f"{report_info.name} — {report_info.description}"):
108
+ try:
109
+ report_text = generate_report(system_name, report_info.name, data)
110
+ st.markdown(report_text)
111
+ except Exception as e:
112
+ st.error(f"Error generating report: {e}")
113
+
114
+
115
+ def main():
116
+ st.set_page_config(page_title="Diagnostics Dashboard", layout="wide")
117
+ st.title("Diagnostics Dashboard")
118
+
119
+ # --- Sidebar ---
120
+ with st.sidebar:
121
+ st.header("Configuration")
122
+
123
+ systems = registry.get_systems()
124
+ if not systems:
125
+ st.warning("No systems registered. Add a system module to diagnostics_framework/systems/.")
126
+ return
127
+
128
+ system_names = list(systems.keys())
129
+ selected = st.selectbox(
130
+ "Select System",
131
+ system_names,
132
+ format_func=lambda s: f"{s} — {systems[s].description}" if systems[s].description else s,
133
+ )
134
+
135
+ st.markdown("---")
136
+ st.subheader("Upload Data")
137
+ uploaded_file = st.file_uploader(
138
+ "Choose a file",
139
+ type=["csv", "json", "xlsx", "xls", "parquet", "txt"],
140
+ )
141
+
142
+ run_button = st.button("Run Diagnostics", type="primary", use_container_width=True)
143
+
144
+ # --- Main area ---
145
+ if uploaded_file is not None:
146
+ data = load_data(uploaded_file)
147
+ if data is None:
148
+ return
149
+
150
+ if isinstance(data, pd.DataFrame):
151
+ with st.expander("Preview uploaded data"):
152
+ st.dataframe(data.head(50))
153
+
154
+ if run_button:
155
+ with st.spinner("Running diagnostics..."):
156
+ summary = run_diagnostics(selected, data)
157
+ st.session_state["last_summary"] = summary
158
+ st.session_state["last_data"] = data
159
+ st.session_state["last_system"] = selected
160
+
161
+ if "last_summary" in st.session_state and st.session_state.get("last_system") == selected:
162
+ tab_results, tab_plots, tab_reports = st.tabs(["Results", "Plots", "Reports"])
163
+ with tab_results:
164
+ render_results(st.session_state["last_summary"])
165
+ with tab_plots:
166
+ render_plots(selected, st.session_state.get("last_data", data))
167
+ with tab_reports:
168
+ render_reports(selected, st.session_state.get("last_data", data))
169
+ else:
170
+ st.info("Upload a data file in the sidebar and click **Run Diagnostics** to begin.")
171
+
172
+ # Show registered info for the selected system
173
+ if "selected" not in dir():
174
+ return
175
+ st.markdown(f"### System: {selected}")
176
+ tests = registry.get_tests(selected)
177
+ plots = registry.get_plots(selected)
178
+ reports = registry.get_reports(selected)
179
+ st.markdown(f"- **{len(tests)}** diagnostic test(s) registered")
180
+ st.markdown(f"- **{len(plots)}** plot(s) registered")
181
+ st.markdown(f"- **{len(reports)}** report(s) registered")
182
+
183
+
184
+ if __name__ == "__main__":
185
+ main()
@@ -0,0 +1,71 @@
1
+ from dataclasses import dataclass, field
2
+ from datetime import datetime
3
+ from enum import Enum
4
+ from typing import Any, Callable
5
+
6
+
7
+ class DiagnosticStatus(Enum):
8
+ PASS = "pass"
9
+ FAIL = "fail"
10
+ WARNING = "warning"
11
+ ERROR = "error"
12
+
13
+
14
+ @dataclass
15
+ class DiagnosticResult:
16
+ test_name: str
17
+ status: DiagnosticStatus
18
+ message: str
19
+ details: dict[str, Any] = field(default_factory=dict)
20
+ timestamp: datetime = field(default_factory=datetime.now)
21
+
22
+
23
+ @dataclass
24
+ class SystemInfo:
25
+ name: str
26
+ description: str = ""
27
+ version: str = "0.1.0"
28
+
29
+
30
+ @dataclass
31
+ class TestInfo:
32
+ name: str
33
+ description: str
34
+ fn: Callable
35
+
36
+
37
+ @dataclass
38
+ class PlotInfo:
39
+ name: str
40
+ description: str
41
+ fn: Callable
42
+
43
+
44
+ @dataclass
45
+ class ReportInfo:
46
+ name: str
47
+ description: str
48
+ fn: Callable
49
+
50
+
51
+ @dataclass
52
+ class DiagnosticSummary:
53
+ system_name: str
54
+ results: list[DiagnosticResult]
55
+ timestamp: datetime = field(default_factory=datetime.now)
56
+
57
+ @property
58
+ def pass_count(self) -> int:
59
+ return sum(1 for r in self.results if r.status == DiagnosticStatus.PASS)
60
+
61
+ @property
62
+ def fail_count(self) -> int:
63
+ return sum(1 for r in self.results if r.status == DiagnosticStatus.FAIL)
64
+
65
+ @property
66
+ def warning_count(self) -> int:
67
+ return sum(1 for r in self.results if r.status == DiagnosticStatus.WARNING)
68
+
69
+ @property
70
+ def error_count(self) -> int:
71
+ return sum(1 for r in self.results if r.status == DiagnosticStatus.ERROR)
@@ -0,0 +1,79 @@
1
+ from diagnostics_framework.models import SystemInfo, TestInfo, PlotInfo, ReportInfo
2
+
3
+
4
+ class DiagnosticsRegistry:
5
+ """Singleton registry for systems, tests, plots, and reports."""
6
+
7
+ _instance = None
8
+
9
+ def __new__(cls):
10
+ if cls._instance is None:
11
+ cls._instance = super().__new__(cls)
12
+ cls._instance._systems = {}
13
+ cls._instance._tests = {}
14
+ cls._instance._plots = {}
15
+ cls._instance._reports = {}
16
+ return cls._instance
17
+
18
+ def add_system(self, name: str, description: str = "", version: str = "0.1.0"):
19
+ self._systems[name] = SystemInfo(name=name, description=description, version=version)
20
+ self._tests.setdefault(name, [])
21
+ self._plots.setdefault(name, [])
22
+ self._reports.setdefault(name, [])
23
+
24
+ def add_test(self, system: str, test_info: TestInfo):
25
+ self._tests.setdefault(system, []).append(test_info)
26
+
27
+ def add_plot(self, system: str, plot_info: PlotInfo):
28
+ self._plots.setdefault(system, []).append(plot_info)
29
+
30
+ def add_report(self, system: str, report_info: ReportInfo):
31
+ self._reports.setdefault(system, []).append(report_info)
32
+
33
+ def get_systems(self) -> dict[str, SystemInfo]:
34
+ return dict(self._systems)
35
+
36
+ def get_tests(self, system: str) -> list[TestInfo]:
37
+ return list(self._tests.get(system, []))
38
+
39
+ def get_plots(self, system: str) -> list[PlotInfo]:
40
+ return list(self._plots.get(system, []))
41
+
42
+ def get_reports(self, system: str) -> list[ReportInfo]:
43
+ return list(self._reports.get(system, []))
44
+
45
+
46
+ # Module-level singleton
47
+ registry = DiagnosticsRegistry()
48
+
49
+
50
+ def register_system(name: str, description: str = "", version: str = "0.1.0"):
51
+ """Decorator that registers a system. Apply to any function or class (it's returned unchanged)."""
52
+ def decorator(obj):
53
+ registry.add_system(name, description, version)
54
+ return obj
55
+ return decorator
56
+
57
+
58
+ def register_test(system: str, name: str, description: str = ""):
59
+ """Decorator that registers a diagnostic test function for a system."""
60
+ def decorator(fn):
61
+ registry.add_test(system, TestInfo(name=name, description=description, fn=fn))
62
+ return fn
63
+ return decorator
64
+
65
+
66
+ def register_plot(system: str, name: str, description: str = ""):
67
+ """Decorator that registers a plot function for a system."""
68
+ def decorator(fn):
69
+ registry.add_plot(system, PlotInfo(name=name, description=description, fn=fn))
70
+ return fn
71
+ return decorator
72
+
73
+
74
+ def register_report(system: str, name: str, description: str = ""):
75
+ """Decorator that registers a report function for a system."""
76
+ def decorator(fn):
77
+ registry.add_report(system, ReportInfo(name=name, description=description, fn=fn))
78
+ return fn
79
+ return decorator