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.
- diagnostics_framework-0.1.0/LICENSE +21 -0
- diagnostics_framework-0.1.0/MANIFEST.in +3 -0
- diagnostics_framework-0.1.0/PKG-INFO +162 -0
- diagnostics_framework-0.1.0/README.md +132 -0
- diagnostics_framework-0.1.0/diagnostics_framework/__init__.py +8 -0
- diagnostics_framework-0.1.0/diagnostics_framework/app.py +185 -0
- diagnostics_framework-0.1.0/diagnostics_framework/models.py +71 -0
- diagnostics_framework-0.1.0/diagnostics_framework/registry.py +79 -0
- diagnostics_framework-0.1.0/diagnostics_framework/runner.py +57 -0
- diagnostics_framework-0.1.0/diagnostics_framework/systems/__init__.py +9 -0
- diagnostics_framework-0.1.0/diagnostics_framework/systems/generic_example.py +201 -0
- diagnostics_framework-0.1.0/diagnostics_framework/systems/sensor_monitoring.py +321 -0
- diagnostics_framework-0.1.0/diagnostics_framework.egg-info/PKG-INFO +162 -0
- diagnostics_framework-0.1.0/diagnostics_framework.egg-info/SOURCES.txt +19 -0
- diagnostics_framework-0.1.0/diagnostics_framework.egg-info/dependency_links.txt +1 -0
- diagnostics_framework-0.1.0/diagnostics_framework.egg-info/entry_points.txt +2 -0
- diagnostics_framework-0.1.0/diagnostics_framework.egg-info/requires.txt +4 -0
- diagnostics_framework-0.1.0/diagnostics_framework.egg-info/top_level.txt +1 -0
- diagnostics_framework-0.1.0/pyproject.toml +45 -0
- diagnostics_framework-0.1.0/sample_data.csv +73 -0
- diagnostics_framework-0.1.0/setup.cfg +4 -0
|
@@ -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,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
|