Skip to content

Commit 856635a

Browse files
Fix formatting in requirements
1 parent 1cd51b3 commit 856635a

3 files changed

Lines changed: 181 additions & 0 deletions

File tree

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
beautifulsoup4
22
fake_useragent
33
imageio
4+
jinja2
45
keras
56
lxml
67
matplotlib
78
numpy
89
opencv-python
910
pandas
11+
pdfkit
1012
pillow
1113
requests
1214
rich

sustainability/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Sustainability compliance tools for CTRL Environmental."""
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
"""Generate a sustainability compliance dashboard for CTRL Environmental.
2+
3+
The script reads inspection data from CSV or Excel files, categorizes findings
4+
into environmental topics, flags non-compliances with traffic light colours,
5+
and exports the results to a styled HTML report. Optional PDF export is
6+
attempted if a suitable backend is available. The layout follows a black,
7+
red, and white palette reminiscent of Berlin poster aesthetics and includes
8+
placeholders for the CTRL logo and report metadata.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import argparse
14+
from dataclasses import dataclass
15+
from datetime import UTC, datetime
16+
from pathlib import Path
17+
18+
import pandas as pd
19+
from jinja2 import Environment
20+
21+
try:
22+
import pdfkit # type: ignore[import-not-found]
23+
except ImportError:
24+
pdfkit = None # type: ignore[assignment]
25+
26+
try:
27+
from weasyprint import HTML # type: ignore[import-not-found]
28+
except ImportError:
29+
HTML = None # type: ignore[assignment]
30+
31+
CATEGORY_KEYWORDS = {
32+
"Waste": ["waste", "trash", "garbage", "recycle"],
33+
"Water": ["water", "effluent", "sewage", "storm"],
34+
"Air": ["air", "emission", "dust", "smoke"],
35+
"Chemicals": ["chemical", "hazard", "solvent", "acid", "alkali"],
36+
"ESG": ["esg", "governance", "social", "sustainability", "diversity"],
37+
}
38+
39+
40+
@dataclass
41+
class Metadata:
42+
"""Metadata describing the inspection report."""
43+
44+
site: str
45+
client: str
46+
inspector: str
47+
date: str
48+
49+
50+
def read_inspection_data(path: str) -> pd.DataFrame:
51+
"""Read inspection data from a CSV or Excel file."""
52+
53+
ext = Path(path).suffix.lower()
54+
if ext in {".xls", ".xlsx"}:
55+
return pd.read_excel(path)
56+
return pd.read_csv(path)
57+
58+
59+
def categorize_finding(text: str) -> str:
60+
"""Return the category that best matches *text*."""
61+
62+
lowered = text.lower()
63+
for category, keywords in CATEGORY_KEYWORDS.items():
64+
if any(word in lowered for word in keywords):
65+
return category
66+
return "Other"
67+
68+
69+
def traffic_light(status: str) -> str:
70+
"""Return a CSS colour for a traffic light style status."""
71+
72+
lowered = status.lower()
73+
if any(word in lowered for word in ["non", "major", "fail", "nc"]):
74+
return "#d50000" # red
75+
if any(word in lowered for word in ["minor", "obs", "warning"]):
76+
return "#ffab00" # amber
77+
return "#00c853" # green
78+
79+
80+
def build_table(df: pd.DataFrame) -> str:
81+
"""Return an HTML table with traffic light styling."""
82+
83+
styled = (
84+
df.style.applymap(
85+
lambda v: f"background-color:{traffic_light(v)}", subset=["Status"]
86+
)
87+
.set_table_styles(
88+
[
89+
{
90+
"selector": "th, td",
91+
"props": [("border", "1px solid black"), ("padding", "4px")],
92+
}
93+
]
94+
)
95+
.hide_index()
96+
)
97+
return styled.to_html()
98+
99+
100+
TEMPLATE = """
101+
<!DOCTYPE html>
102+
<html>
103+
<head>
104+
<meta charset="utf-8" />
105+
<title>CTRL Environmental Compliance Report</title>
106+
<style>
107+
body { background: #fff; color: #000; font-family: Arial, sans-serif; }
108+
header { background: #000; color: #fff; padding: 20px; text-align: center; }
109+
h1 { color: #e10600; margin: 0; text-transform: uppercase; }
110+
.meta { margin-top: 10px; }
111+
</style>
112+
</head>
113+
<body>
114+
<header>
115+
<img src="{{ logo }}" alt="CTRL Logo" style="max-height:80px;" />
116+
<h1>Compliance Dashboard</h1>
117+
<div class="meta">
118+
<strong>Site:</strong> {{ meta.site }} |
119+
<strong>Client:</strong> {{ meta.client }} |
120+
<strong>Inspector:</strong> {{ meta.inspector }} |
121+
<strong>Date:</strong> {{ meta.date }}
122+
</div>
123+
</header>
124+
<div class="table-container">
125+
{{ table | safe }}
126+
</div>
127+
</body>
128+
</html>
129+
"""
130+
131+
132+
def render_report(
133+
df: pd.DataFrame, meta: Metadata, logo: str, html_path: str, pdf_path: str | None
134+
) -> None:
135+
"""Render *df* to HTML and optionally PDF."""
136+
137+
table_html = build_table(df)
138+
env = Environment(autoescape=True)
139+
template = env.from_string(TEMPLATE)
140+
html_content = template.render(table=table_html, meta=meta, logo=logo)
141+
Path(html_path).write_text(html_content, encoding="utf-8")
142+
143+
if pdf_path:
144+
if pdfkit is not None:
145+
pdfkit.from_string(html_content, pdf_path) # type: ignore[no-untyped-call]
146+
elif HTML is not None:
147+
HTML(string=html_content).write_pdf(pdf_path) # type: ignore[no-untyped-call]
148+
else:
149+
print("PDF output requested but no PDF backend is installed.")
150+
151+
152+
def main() -> None:
153+
"""Command line interface for the compliance dashboard."""
154+
155+
parser = argparse.ArgumentParser(description=__doc__)
156+
parser.add_argument("input", help="CSV or Excel file containing inspection data")
157+
parser.add_argument("--html", default="report.html", help="Output HTML report path")
158+
parser.add_argument("--pdf", help="Optional output PDF path")
159+
parser.add_argument(
160+
"--logo", default="logo_placeholder.png", help="Path to CTRL logo image"
161+
)
162+
parser.add_argument("--site", default="Unknown Site")
163+
parser.add_argument("--client", default="Unknown Client")
164+
parser.add_argument("--inspector", default="Unknown Inspector")
165+
parser.add_argument("--date", default=datetime.now(tz=UTC).date().isoformat())
166+
args = parser.parse_args()
167+
168+
data = read_inspection_data(args.input)
169+
if "Category" not in data.columns and "Finding" in data.columns:
170+
data["Category"] = data["Finding"].map(categorize_finding)
171+
172+
meta = Metadata(args.site, args.client, args.inspector, args.date)
173+
render_report(data, meta, args.logo, args.html, args.pdf)
174+
print(f"Report written to {args.html}")
175+
176+
177+
if __name__ == "__main__":
178+
main()

0 commit comments

Comments
 (0)