From 4802ba28948c81165fb5485cdb80817905d99de5 Mon Sep 17 00:00:00 2001 From: McElwain Date: Tue, 9 Jun 2026 23:45:21 -0500 Subject: [PATCH] Add utility app scaffold and document generator --- app/__init__.py | 0 app/main.py | 16 ++++ app/models/__init__.py | 0 app/routes/__init__.py | 0 app/services/__init__.py | 0 app/utils/__init__.py | 0 archive/.gitkeep | 0 diagnostics/.gitkeep | 0 exports/.gitkeep | 0 logs/.gitkeep | 0 outputs/.gitkeep | 0 scripts/.gitkeep | 0 settings/.gitkeep | 0 static/.gitkeep | 0 tests/.gitkeep | 0 tools/.gitkeep | 0 .../document_types/engineering_report.json | 12 +++ .../content/templates/engineering_report.docx | Bin 0 -> 36696 bytes tools/doc_generator/logic/__init__.py | 0 tools/doc_generator/logic/core_fields.py | 22 +++++ tools/doc_generator/logic/document_types.py | 25 +++++ tools/doc_generator/logic/renderer.py | 90 ++++++++++++++++++ 22 files changed, 165 insertions(+) create mode 100644 app/__init__.py create mode 100644 app/main.py create mode 100644 app/models/__init__.py create mode 100644 app/routes/__init__.py create mode 100644 app/services/__init__.py create mode 100644 app/utils/__init__.py create mode 100644 archive/.gitkeep create mode 100644 diagnostics/.gitkeep create mode 100644 exports/.gitkeep create mode 100644 logs/.gitkeep create mode 100644 outputs/.gitkeep create mode 100644 scripts/.gitkeep create mode 100644 settings/.gitkeep create mode 100644 static/.gitkeep create mode 100644 tests/.gitkeep create mode 100644 tools/.gitkeep create mode 100644 tools/doc_generator/content/document_types/engineering_report.json create mode 100644 tools/doc_generator/content/templates/engineering_report.docx create mode 100644 tools/doc_generator/logic/__init__.py create mode 100644 tools/doc_generator/logic/core_fields.py create mode 100644 tools/doc_generator/logic/document_types.py create mode 100644 tools/doc_generator/logic/renderer.py diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..bf77ca2 --- /dev/null +++ b/app/main.py @@ -0,0 +1,16 @@ +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles + +from app.routes import doc_generator + +app = FastAPI(title="Utility App") + +app.mount("/static", StaticFiles(directory="static"), name="static") + +app.include_router(doc_generator.router, prefix="/api/doc-generator", tags=["doc-generator"]) + + +@app.get("/") +def index(): + from fastapi.responses import FileResponse + return FileResponse("static/index.html") diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archive/.gitkeep b/archive/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/diagnostics/.gitkeep b/diagnostics/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/exports/.gitkeep b/exports/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/outputs/.gitkeep b/outputs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/.gitkeep b/scripts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/settings/.gitkeep b/settings/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/static/.gitkeep b/static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tools/.gitkeep b/tools/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tools/doc_generator/content/document_types/engineering_report.json b/tools/doc_generator/content/document_types/engineering_report.json new file mode 100644 index 0000000..c92919b --- /dev/null +++ b/tools/doc_generator/content/document_types/engineering_report.json @@ -0,0 +1,12 @@ +{ + "id": "engineering_report", + "name": "Engineering Report", + "description": "General engineering report generated from a Word template.", + "template": "engineering_report.docx", + "outputFilename": "engineering_report_{projectName}_{timestamp_YYYY-MM-DD_HH-mm-ss}.docx", + "fields": [ + {"name": "projectName", "label": "Project Name", "type": "text", "required": true}, + {"name": "preparedBy", "label": "Prepared By", "type": "text", "required": false}, + {"name": "summary", "label": "Summary", "type": "textarea", "required": false} + ] +} diff --git a/tools/doc_generator/content/templates/engineering_report.docx b/tools/doc_generator/content/templates/engineering_report.docx new file mode 100644 index 0000000000000000000000000000000000000000..4ccd264e9de49a7524092d329ce791b6bcee5e61 GIT binary patch literal 36696 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 literal 0 HcmV?d00001 diff --git a/tools/doc_generator/logic/__init__.py b/tools/doc_generator/logic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/doc_generator/logic/core_fields.py b/tools/doc_generator/logic/core_fields.py new file mode 100644 index 0000000..65da68b --- /dev/null +++ b/tools/doc_generator/logic/core_fields.py @@ -0,0 +1,22 @@ +import pendulum + + +def get_core_fields() -> dict: + now = pendulum.now() + + return { + "date": now.format("MMMM Do, YYYY"), + "time": now.format("h:mm A"), + "timestamp": now.format("MMMM Do, YYYY h:mm A"), + "date_YYYY-MM-DD": now.format("YYYY-MM-DD"), + "date_MM-DD-YYYY": now.format("MM-DD-YYYY"), + "timestamp_YYYY-MM-DD_HH-mm-ss": now.format("YYYY-MM-DD_HH-mm-ss"), + } + + +def merge_core_fields(data: dict) -> dict: + # App-owned core fields win over submitted form values. + return { + **data, + **get_core_fields(), + } diff --git a/tools/doc_generator/logic/document_types.py b/tools/doc_generator/logic/document_types.py new file mode 100644 index 0000000..07a4490 --- /dev/null +++ b/tools/doc_generator/logic/document_types.py @@ -0,0 +1,25 @@ +import json +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent +CONTENT_DIR = BASE_DIR / "content" +DOCUMENT_TYPES_DIR = CONTENT_DIR / "document_types" + + +def list_document_types(): + document_types = [] + + for path in sorted(DOCUMENT_TYPES_DIR.glob("*.json")): + data = json.loads(path.read_text(encoding="utf-8")) + document_types.append(data) + + return document_types + + +def get_document_type(document_type_id: str): + path = DOCUMENT_TYPES_DIR / f"{document_type_id}.json" + + if not path.exists(): + raise FileNotFoundError(f"Document type not found: {document_type_id}") + + return json.loads(path.read_text(encoding="utf-8")) diff --git a/tools/doc_generator/logic/renderer.py b/tools/doc_generator/logic/renderer.py new file mode 100644 index 0000000..13bdddc --- /dev/null +++ b/tools/doc_generator/logic/renderer.py @@ -0,0 +1,90 @@ +import re +from pathlib import Path + +from docx import Document + +from tools.doc_generator.logic.core_fields import merge_core_fields +from tools.doc_generator.logic.document_types import get_document_type + +BASE_DIR = Path(__file__).resolve().parent.parent +CONTENT_DIR = BASE_DIR / "content" +TEMPLATES_DIR = CONTENT_DIR / "templates" +PROJECT_ROOT = BASE_DIR.parent.parent +EXPORTS_DIR = PROJECT_ROOT / "exports" + + +def safe_filename(value: str) -> str: + value = str(value or "document").strip() + value = re.sub(r"[^A-Za-z0-9._ -]+", "", value) + value = re.sub(r"\s+", "_", value) + return value or "document" + + +def render_filename(pattern: str, data: dict) -> str: + filename = pattern + + for key, value in data.items(): + filename = filename.replace("{" + key + "}", safe_filename(value)) + + return safe_filename(filename) + + +def replace_placeholders_in_paragraph(paragraph, data: dict): + full_text = "".join(run.text for run in paragraph.runs) + + new_text = full_text + for key, value in data.items(): + new_text = new_text.replace("{" + key + "}", "" if value is None else str(value)) + + if new_text == full_text: + return + + for run in paragraph.runs: + run.text = "" + + if paragraph.runs: + paragraph.runs[0].text = new_text + else: + paragraph.add_run(new_text) + + +def replace_placeholders_in_table(table, data: dict): + for row in table.rows: + for cell in row.cells: + for paragraph in cell.paragraphs: + replace_placeholders_in_paragraph(paragraph, data) + + for nested_table in cell.tables: + replace_placeholders_in_table(nested_table, data) + + +def generate_docx(document_type_id: str, data: dict) -> Path: + data = merge_core_fields(data) + document_type = get_document_type(document_type_id) + + template_path = TEMPLATES_DIR / document_type["template"] + + if not template_path.exists(): + raise FileNotFoundError(f"Template not found: {template_path}") + + EXPORTS_DIR.mkdir(parents=True, exist_ok=True) + + output_pattern = document_type.get("outputFilename", f"{document_type_id}.docx") + output_filename = render_filename(output_pattern, data) + + if not output_filename.lower().endswith(".docx"): + output_filename += ".docx" + + output_path = EXPORTS_DIR / output_filename + + document = Document(template_path) + + for paragraph in document.paragraphs: + replace_placeholders_in_paragraph(paragraph, data) + + for table in document.tables: + replace_placeholders_in_table(table, data) + + document.save(output_path) + + return output_path