Spaces:
Sleeping
Sleeping
Download pdf_export.py from rairo/chilltrack-api: direct link, hf CLI and curl.
- Browser
- Download file 5.12 kB
-
https://huggingface.co/spaces/rairo/chilltrack-api/resolve/main/pdf_export.py
- Command line
-
hf download hf://spaces/rairo/chilltrack-api/pdf_export.py
-
curl -L -o pdf_export.py https://huggingface.co/spaces/rairo/chilltrack-api/resolve/main/pdf_export.py
5.12 kB
| """ | |
| ChillTrack API — room-temperature log PDF export. | |
| Recreates the paper form: one row per unit, one column per daily slot. | |
| Built entirely in memory with an io.BytesIO() buffer (see sqx-api's | |
| cohort_reports.build_pdf pattern) — nothing is ever written to disk. | |
| """ | |
| import io | |
| from reportlab.lib import colors | |
| from reportlab.lib.pagesizes import A4, landscape | |
| from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet | |
| from reportlab.lib.units import mm | |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle | |
| from slots import SLOT_TIMES | |
| INK = colors.HexColor("#172033") | |
| MUTED = colors.HexColor("#64748B") | |
| BRAND = colors.HexColor("#1D4ED8") | |
| OUT_OF_TOLERANCE_BG = colors.HexColor("#FDE2E1") | |
| OUT_OF_TOLERANCE_TEXT = colors.HexColor("#B42318") | |
| HEADER_BG = colors.HexColor("#1A1A2E") | |
| def _styles(): | |
| base = getSampleStyleSheet() | |
| return { | |
| "title": ParagraphStyle("title", parent=base["Title"], fontName="Helvetica-Bold", | |
| fontSize=16, leading=19, textColor=INK), | |
| "subtitle": ParagraphStyle("subtitle", parent=base["Normal"], fontSize=9, | |
| leading=12, textColor=MUTED), | |
| "small": ParagraphStyle("small", parent=base["Normal"], fontSize=7.5, | |
| leading=10, textColor=MUTED), | |
| } | |
| def build_room_temperature_pdf(units: list, readings: list, shift_date: str, roster: dict | None = None) -> io.BytesIO: | |
| """Builds the room-temperature log PDF for one shift_date. | |
| units: list of ctk_units dicts (each needs id/name/setpoint_c/tolerance_c). | |
| readings: list of ctk_readings dicts for this shift_date (any units/slots). | |
| roster: optional {slot_label_or_range: [names]} for the footer signature block. | |
| Returns a BytesIO positioned at 0, ready for send_file(...). | |
| """ | |
| buffer = io.BytesIO() | |
| styles = _styles() | |
| doc = SimpleDocTemplate( | |
| buffer, | |
| pagesize=landscape(A4), | |
| leftMargin=10 * mm, | |
| rightMargin=10 * mm, | |
| topMargin=10 * mm, | |
| bottomMargin=10 * mm, | |
| title=f"Room Temperature Log — {shift_date}", | |
| ) | |
| # Index readings by (unit_id, slot_index) for O(1) cell lookup. | |
| by_cell = {} | |
| for r in readings: | |
| key = (r.get("unit_id"), r.get("slot_index")) | |
| by_cell[key] = r | |
| story = [ | |
| Paragraph("COLCOM — ROOM TEMPERATURE LOG", styles["title"]), | |
| Paragraph(f"Shift date: {shift_date} (08:00 to 08:00 next day)", styles["subtitle"]), | |
| Spacer(1, 3 * mm), | |
| ] | |
| header_row = ["Unit (setpoint)"] + SLOT_TIMES | |
| table_data = [header_row] | |
| cell_styles = [] # list of TableStyle commands for out-of-tolerance cells | |
| for row_idx, unit in enumerate(units, start=1): | |
| unit_id = unit.get("id") or unit.get("unit_id") | |
| label = f"{unit.get('name', '')} ({unit.get('setpoint_c')}°C ±{unit.get('tolerance_c')})" | |
| row = [label] | |
| for slot_index in range(len(SLOT_TIMES)): | |
| reading = by_cell.get((unit_id, slot_index)) | |
| if reading is None: | |
| row.append("") | |
| else: | |
| value = reading.get("value_c") | |
| row.append(f"{value}" if value is not None else "") | |
| if reading.get("within_tolerance") is False: | |
| cell_styles.append(("BACKGROUND", (slot_index + 1, row_idx), (slot_index + 1, row_idx), OUT_OF_TOLERANCE_BG)) | |
| cell_styles.append(("TEXTCOLOR", (slot_index + 1, row_idx), (slot_index + 1, row_idx), OUT_OF_TOLERANCE_TEXT)) | |
| cell_styles.append(("FONTNAME", (slot_index + 1, row_idx), (slot_index + 1, row_idx), "Helvetica-Bold")) | |
| table_data.append(row) | |
| col_widths = [55 * mm] + [18 * mm] * len(SLOT_TIMES) | |
| table = Table(table_data, colWidths=col_widths, repeatRows=1, hAlign="LEFT") | |
| base_style = [ | |
| ("FONTNAME", (0, 0), (-1, -1), "Helvetica"), | |
| ("FONTSIZE", (0, 0), (-1, -1), 7), | |
| ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), | |
| ("ALIGN", (1, 0), (-1, -1), "CENTER"), | |
| ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#CBD5E1")), | |
| ("BACKGROUND", (0, 0), (-1, 0), HEADER_BG), | |
| ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), | |
| ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), | |
| ("FONTSIZE", (0, 0), (-1, 0), 7.5), | |
| ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F8FAFC")]), | |
| ("LEFTPADDING", (0, 0), (-1, -1), 3), | |
| ("RIGHTPADDING", (0, 0), (-1, -1), 3), | |
| ("TOPPADDING", (0, 0), (-1, -1), 3), | |
| ("BOTTOMPADDING", (0, 0), (-1, -1), 3), | |
| ] + cell_styles | |
| table.setStyle(TableStyle(base_style)) | |
| story.append(table) | |
| if roster: | |
| story.append(Spacer(1, 5 * mm)) | |
| story.append(Paragraph("Shift roster", styles["subtitle"])) | |
| for label, names in roster.items(): | |
| names_str = ", ".join(names) if isinstance(names, list) else str(names) | |
| story.append(Paragraph(f"{label}: {names_str}", styles["small"])) | |
| doc.build(story) | |
| buffer.seek(0) | |
| return buffer | |