feat(render): raport PDF przez LaTeX jako osobna usluga (PRE-24)
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m57s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 34s
Testy / Kontrola składni wszystkich warstw (push) Successful in 22s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m57s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 34s
Testy / Kontrola składni wszystkich warstw (push) Successful in 22s
Ostatnia z osmiu wskazowek partnerow. Nowa usluga services/render sklada raport:
generuje plik posredni .tex i kompiluje go XeLaTeX-em do PDF.
OSOBNY komponent, nie czesc prezentacji — ta sama zasada co przy izolacji
swissepha (LOG-27). TeX Live wazy setki megabajtow; w obrazie produktu
spowalnialby kazdy build, a tak aktualizuje sie niezaleznie i jego awaria nie
kladzie aplikacji, tylko przycisk „Pobierz PDF".
SZYFROWANIE, o ktore prosiles: lacze prezentacja↔render idzie tak samo jak
pozostale, ale z WLASNYM, TRZECIM kluczem (LINK_KEY_PRESENTATION_RENDER). Osobny,
bo tym laczem plynie CALY raport — dane urodzeniowe i opisy z baz — wiec przejecie
go nie moze otwierac lacza do logiki ani do danych. Fail-closed: bez klucza pod
nie wstaje. Test kopii link_crypto obejmuje teraz cztery uslugi.
Uklad PDF wg prosby: imie i nazwisko -> wprowadzone dane -> RYSUNEK kosmogramu
-> interpretacja natalna -> predykcje okresowe. Test pilnuje kolejnosci.
Dwie rzeczy, ktore wyszly dopiero przy skladaniu tego kawalka:
1. KOSMOGRAM NIE NADAWAL SIE DO PDF. Uzywa zmiennych CSS (var(--line)) i klasy
.glyph, ktorej font podaje styles.css — a samodzielny konwerter SVG→PDF nie zna
naszego arkusza. Wyszlyby czarne kreski BEZ SYMBOLI. Stad wariant „print":
konkretne kolory na bialym tle i font glifow wpisany wprost w rysunek. Przy
okazji zalatwia to etap 4 planu kola (wariant do druku).
2. UCIECZKA ZNAKOW LATEXA byla zepsuta — i zlapal to moj wlasny test. Zamiana
„\” na \textbackslash{} szla w tej samej petli co nawiasy, wiec kolejne
podmiany ucieklyby nawiasy dopiero co wstawione: wychodzilo
\textbackslash\{\}. Poprawka: ukosnik chowany pod znacznik zastepczy i
rozwijany na koncu. To nie kosmetyka — niezauwazony „%” komentuje RESZTE LINII,
wiec zdanie od modelu urywaloby sie w polowie, a PDF powstawalby normalnie,
tylko krotszy. Test na wrogim tekscie sprawdza tez, ze \end{document} ani
\input{} nie wyrwa sie z dokumentu.
Usluga nie zapisuje nic poza katalogiem tymczasowym, ktory sprzata po sobie;
w manifescie readOnlyRootFilesystem + emptyDir na /tmp. /health raportuje
obecnosc xelatex i rsvg-convert, zeby zepsuty obraz bylo widac od razu.
Zweryfikowane na zywo (TestClient uslugi render): zadanie bez szyfrowania
z POPRAWNYM tokenem -> 400; obcy klucz -> 400 i tajny opis nie wraca; wlasciwy
klucz -> zadanie dochodzi do aplikacji, tresci baz NIE MA na kablu, odpowiedz
zaszyfrowana. Manifesty przechodza kubectl apply --dry-run=server na zywym
klastrze.
CZEGO NIE SPRAWDZILEM: samej kompilacji PDF. W tym srodowisku nie ma ani TeX
Live, ani dzialajacego runtime'u kontenerow (docker CLI jest, daemon nie) —
probowalem zbudowac obraz i sie nie dalo. Sprawdzone jest wszystko dookola:
generowanie .tex, ucieczka znakow, szyfrowanie, kontrakt API, samowystarczalnosc
SVG. Pierwsze uruchomienie na klastrze trzeba obejrzec — dlatego PRE-24 zostaje
jako „W trakcie", nie „Zrobione".
Instrukcja wdrozenia: docs/wdrozenie-render-pdf.md (klucz, obraz, merge,
weryfikacja, znane ograniczenia).
Testy: 15 nowych (render) + 14 (lacze i wariant druku w prezentacji).
Calosc: prezentacja 131, logika 265/1 skip, render 15.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"""Łącze do usługi render i wariant kosmogramu do druku (PRE-24).
|
||||
|
||||
Przez to łącze leci CAŁY raport: dane urodzeniowe i opisy z baz. Musi być
|
||||
szyfrowane tak samo jak pozostałe, WŁASNYM kluczem — i musi obowiązywać ten sam
|
||||
niezmiennik co dla klienta logiki: żadnego wyjścia w dół bez tokenu i klucza.
|
||||
"""
|
||||
import pathlib
|
||||
|
||||
import pytest
|
||||
|
||||
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||
CLIENT = APP / "clients" / "render_client.py"
|
||||
SRC = CLIENT.read_text(encoding="utf-8")
|
||||
WHEEL = (APP / "chartwheel.py").read_text(encoding="utf-8")
|
||||
MAIN = (APP / "main.py").read_text(encoding="utf-8")
|
||||
COMPILE_JS = (APP / "static" / "compile.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
# ─────────────────────────── szyfrowanie łącza ───────────────────────────
|
||||
|
||||
def test_uses_its_own_third_key():
|
||||
"""Osobny klucz na parę rozmówców. Przejęcie klucza renderu nie może otwierać
|
||||
łącza do logiki ani do danych."""
|
||||
from app import link_crypto
|
||||
|
||||
assert link_crypto.ENV_PRESENTATION_RENDER == "LINK_KEY_PRESENTATION_RENDER"
|
||||
assert link_crypto.ENV_PRESENTATION_RENDER != link_crypto.ENV_PRESENTATION_LOGIC
|
||||
assert link_crypto.ENV_PRESENTATION_RENDER != link_crypto.ENV_LOGIC_DATA
|
||||
assert "ENV_PRESENTATION_RENDER" in SRC
|
||||
|
||||
|
||||
def test_report_goes_through_the_encrypted_link():
|
||||
assert "link_crypto.call(" in SRC
|
||||
assert "link=_link()" in SRC
|
||||
|
||||
|
||||
def test_every_outbound_call_carries_token_and_key():
|
||||
"""Ten sam niezmiennik co przy kliencie logiki — tam brak nagłówka wyszedł
|
||||
dopiero na produkcji (401)."""
|
||||
import ast
|
||||
|
||||
tree = ast.parse(SRC)
|
||||
calls = [n for n in ast.walk(tree)
|
||||
if isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute)
|
||||
and n.func.attr in ("call", "call_json")]
|
||||
assert calls, "nie znaleziono wywołania przez link_crypto"
|
||||
for c in calls:
|
||||
kwargs = {k.arg for k in c.keywords}
|
||||
assert "headers" in kwargs and "link" in kwargs
|
||||
|
||||
|
||||
def test_health_is_the_only_unencrypted_path():
|
||||
"""/health musi zostać otwarty dla sond k8s — ale nic poza nim."""
|
||||
from app import link_crypto
|
||||
|
||||
assert "/health" in link_crypto.PUBLIC_PATHS
|
||||
raw = [line for line in SRC.splitlines() if "client.get(" in line or "client.post(" in line]
|
||||
assert all("/health" in line for line in raw), f"surowe wywołania poza /health: {raw}"
|
||||
|
||||
|
||||
def test_key_is_read_lazily():
|
||||
"""Konfiguracja może się zmienić bez restartu procesu (podmiana sekretu)."""
|
||||
assert "def _link()" in SRC and "key_from_env" in SRC
|
||||
|
||||
|
||||
# ──────────────────────── kosmogram w wariancie do druku ─────────────────
|
||||
|
||||
def test_pdf_route_uses_print_theme():
|
||||
"""Samodzielny konwerter SVG→PDF nie zna naszego arkusza stylów."""
|
||||
assert 'chartwheel.render(chart, theme="print")' in MAIN
|
||||
|
||||
|
||||
def test_print_theme_has_no_css_variables():
|
||||
"""`var(--…)` w SVG poza stroną nie rozwiąże się — wyszłyby czarne albo
|
||||
żadne kreski."""
|
||||
from app import chartwheel
|
||||
|
||||
chart = _sample_chart()
|
||||
printed = chartwheel.render(chart, theme="print")
|
||||
assert printed and "var(--" not in printed
|
||||
|
||||
|
||||
def test_screen_theme_still_uses_css_variables():
|
||||
"""Na stronie koło ma się dostrajać do motywu aplikacji."""
|
||||
from app import chartwheel
|
||||
|
||||
assert "var(--" in chartwheel.render(_sample_chart(), theme="screen")
|
||||
|
||||
|
||||
def test_print_theme_embeds_the_glyph_font():
|
||||
"""Font glifów podaje CSS klasy `.glyph` — poza stroną trzeba go wpisać
|
||||
W SAM rysunek, inaczej kosmogram w PDF wyszedłby BEZ symboli."""
|
||||
from app import chartwheel
|
||||
|
||||
printed = chartwheel.render(_sample_chart(), theme="print")
|
||||
assert 'font-family=' in printed
|
||||
assert "DejaVu Sans" in printed # font obecny w obrazie usługi render
|
||||
|
||||
|
||||
def test_screen_theme_leaves_font_to_css():
|
||||
from app import chartwheel
|
||||
|
||||
assert "font-family=" not in chartwheel.render(_sample_chart(), theme="screen")
|
||||
|
||||
|
||||
def test_unknown_theme_falls_back_to_screen():
|
||||
from app import chartwheel
|
||||
|
||||
assert chartwheel.render(_sample_chart(), theme="cokolwiek") == \
|
||||
chartwheel.render(_sample_chart(), theme="screen")
|
||||
|
||||
|
||||
# ─────────────────────────── składanie po stronie klienta ────────────────
|
||||
|
||||
def test_browser_sends_all_three_parts():
|
||||
for field in ("person", "natal", "predictions"):
|
||||
assert field in COMPILE_JS, f"raport wysyłany bez części: {field}"
|
||||
|
||||
|
||||
def test_pdf_button_exists():
|
||||
tpl = (APP / "templates" / "compile.html").read_text(encoding="utf-8")
|
||||
assert 'id="pdfBtn"' in tpl
|
||||
|
||||
|
||||
def test_wheel_failure_does_not_block_the_report():
|
||||
"""Brak rysunku ma dać raport bez kosmogramu, a nie brak raportu — tekst
|
||||
kosztował wywołanie modelu, rysunek policzymy zawsze."""
|
||||
assert "wheel_svg = \"\"" in MAIN or 'wheel_svg, = ""' in MAIN
|
||||
assert "Brak rysunku nie może zablokować raportu" in MAIN
|
||||
|
||||
|
||||
# ─────────────────────────────────── pomocnicze ──────────────────────────
|
||||
|
||||
def _sample_chart():
|
||||
signs = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||||
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"]
|
||||
glyphs = ["♈", "♉", "♊", "♋", "♌", "♍", "♎", "♏", "♐", "♑", "♒", "♓"]
|
||||
asc = 128.9
|
||||
asc_sign = int(asc // 30)
|
||||
return {
|
||||
"angles": {"Asc": {"name": "Asc", "decimal": asc, "sign": signs[asc_sign]},
|
||||
"MC": {"name": "MC", "decimal": 5.0},
|
||||
"Dsc": {"name": "Dsc", "decimal": (asc + 180) % 360},
|
||||
"IC": {"name": "IC", "decimal": 185.0}},
|
||||
"cusps": [{"house": i + 1, "sign": signs[(asc_sign + i) % 12],
|
||||
"decimal": (asc_sign * 30 + i * 30) % 360} for i in range(12)],
|
||||
"sign_glyphs": [{"sign": s, "glyph": g} for s, g in zip(signs, glyphs)],
|
||||
"positions": [{"name": "Sun", "decimal": 40.2, "glyph": "☉", "direction": "D"}],
|
||||
}
|
||||
Reference in New Issue
Block a user