services: add clan consent/visibility and oneok adapter stack
This commit is contained in:
13
services/oneok-docs-adapter/Dockerfile
Normal file
13
services/oneok-docs-adapter/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app.py .
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
EXPOSE 8090
|
||||
|
||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8090"]
|
||||
102
services/oneok-docs-adapter/app.py
Normal file
102
services/oneok-docs-adapter/app.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import base64
|
||||
import html
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import httpx
|
||||
from fastapi import Depends, FastAPI, Header, HTTPException
|
||||
|
||||
GOTENBERG_URL = os.getenv("ONEOK_GOTENBERG_URL", "http://oneok-gotenberg:3000").rstrip("/")
|
||||
API_KEY = os.getenv("ONEOK_ADAPTER_API_KEY", "").strip()
|
||||
|
||||
app = FastAPI(title="1OK Docs Adapter", version="1.0.0")
|
||||
|
||||
|
||||
def _auth(authorization: Optional[str] = Header(default=None)) -> None:
|
||||
if not API_KEY:
|
||||
return
|
||||
expected = f"Bearer {API_KEY}"
|
||||
if authorization != expected:
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
|
||||
def _render_html(title: str, data: Dict[str, Any]) -> str:
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
||||
rows = []
|
||||
for k, v in data.items():
|
||||
rows.append(
|
||||
f"<tr><td style='padding:6px;border:1px solid #ddd;vertical-align:top'><b>{html.escape(str(k))}</b></td>"
|
||||
f"<td style='padding:6px;border:1px solid #ddd'>{html.escape(str(v))}</td></tr>"
|
||||
)
|
||||
table = "\n".join(rows) if rows else "<tr><td style='padding:6px;border:1px solid #ddd'>No data</td></tr>"
|
||||
return f"""<!doctype html>
|
||||
<html lang="uk">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; color: #111; margin: 24px; }}
|
||||
h1 {{ margin: 0 0 8px; font-size: 24px; }}
|
||||
.meta {{ color: #666; margin: 0 0 18px; font-size: 12px; }}
|
||||
table {{ border-collapse: collapse; width: 100%; }}
|
||||
td {{ font-size: 13px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{html.escape(title)}</h1>
|
||||
<div class="meta">Generated: {html.escape(ts)}</div>
|
||||
<table>{table}</table>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
async def _render_pdf(file_name: str, html_text: str) -> Dict[str, Any]:
|
||||
endpoints = [
|
||||
f"{GOTENBERG_URL}/forms/chromium/convert/html",
|
||||
f"{GOTENBERG_URL}/forms/libreoffice/convert",
|
||||
]
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
files = {file_name: (file_name, html_text.encode("utf-8"), "text/html")}
|
||||
last_error = None
|
||||
pdf = b""
|
||||
for url in endpoints:
|
||||
resp = await client.post(url, files=files)
|
||||
if resp.status_code < 400:
|
||||
pdf = resp.content
|
||||
break
|
||||
last_error = f"{url} -> {resp.status_code} {resp.text[:200]}"
|
||||
if not pdf:
|
||||
raise HTTPException(status_code=502, detail=f"Gotenberg error: {last_error}")
|
||||
return {
|
||||
"file_name": file_name.replace(".html", ".pdf"),
|
||||
"mime": "application/pdf",
|
||||
"size_bytes": len(pdf),
|
||||
"pdf_base64": base64.b64encode(pdf).decode("utf-8"),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> Dict[str, Any]:
|
||||
return {"status": "ok", "service": "oneok-docs-adapter"}
|
||||
|
||||
|
||||
@app.post("/docs/render_quote_pdf")
|
||||
async def render_quote_pdf(body: Dict[str, Any], _: None = Depends(_auth)) -> Dict[str, Any]:
|
||||
quote_id = body.get("quote_id")
|
||||
payload = body.get("quote_payload") or {}
|
||||
if not quote_id and not payload:
|
||||
raise HTTPException(status_code=400, detail="quote_id or quote_payload required")
|
||||
if quote_id and "quote_id" not in payload:
|
||||
payload = dict(payload)
|
||||
payload["quote_id"] = quote_id
|
||||
html_doc = _render_html("Комерційна пропозиція (1OK)", payload)
|
||||
return await _render_pdf("quote.html", html_doc)
|
||||
|
||||
|
||||
@app.post("/docs/render_invoice_pdf")
|
||||
async def render_invoice_pdf(body: Dict[str, Any], _: None = Depends(_auth)) -> Dict[str, Any]:
|
||||
payload = body.get("invoice_payload")
|
||||
if not isinstance(payload, dict):
|
||||
raise HTTPException(status_code=400, detail="invoice_payload required")
|
||||
html_doc = _render_html("Рахунок (1OK)", payload)
|
||||
return await _render_pdf("invoice.html", html_doc)
|
||||
3
services/oneok-docs-adapter/requirements.txt
Normal file
3
services/oneok-docs-adapter/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
fastapi==0.115.6
|
||||
uvicorn[standard]==0.32.1
|
||||
httpx==0.28.1
|
||||
Reference in New Issue
Block a user