services: add clan consent/visibility and oneok adapter stack

This commit is contained in:
Apple
2026-02-19 00:14:12 -08:00
parent dfc0ef1ceb
commit c201d105f6
20 changed files with 1510 additions and 0 deletions

View 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"]

View 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)

View File

@@ -0,0 +1,3 @@
fastapi==0.115.6
uvicorn[standard]==0.32.1
httpx==0.28.1