mirror of
https://github.com/migatu/astrololo.git
synced 2026-07-16 22:52:11 +00:00
Szkielet aplikacji trójwarstwowej (prezentacja / logika / dane)
Trzy niezależne usługi FastAPI komunikujące się przez HTTP/JSON, każda zna tylko adres warstwy bezpośrednio pod nią: - presentation (:8000) — strona WWW + formularz - logic (:8001) — reguły biznesowe, pośrednik - data (:8002) — wyszukiwanie danych za interfejsem DataProvider Warstwa danych: czytanie setek plików .xlsx z wykrywaniem nagłówka i mapowaniem układu kolumn na schemat kanoniczny, z 4-poziomowym cache (schemat L1, Parquet L2, wyniki zapytań L3, odwrócony indeks L4) i unieważnianiem po odcisku pliku. Gotowa ścieżka migracji do SQL (ingest/to_sql.py + SqlDataProvider, przełączane przez DATA_PROVIDER). Zawiera docker-compose, Makefile, generator danych przykładowych. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
"""Mapowanie układu kolumn na schemat kanoniczny.
|
||||
|
||||
Setki plików mogą mieć te same dane pod różnymi nagłówkami i w różnej kolejności
|
||||
kolumn ("Imię", "Name", "NAZWA" -> kanoniczne 'name'). Ta warstwa tłumaczy
|
||||
faktyczny układ kolumn pliku na wspólny słownik pól, dzięki czemu reszta systemu
|
||||
(i przyszła baza SQL) operuje na jednej, stabilnej nazwie pola.
|
||||
|
||||
Aliasowanie jest świadomie wydzielone i konfigurowalne — to jedyne miejsce do
|
||||
edycji, gdy pojawi się nowy wariant nagłówka.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
# kanoniczne_pole -> zbiór aliasów (po normalizacji)
|
||||
CANONICAL_ALIASES: dict[str, set[str]] = {
|
||||
"id": {"id", "identyfikator", "nr", "no", "number"},
|
||||
"name": {"name", "imie", "nazwa", "nazwisko", "title", "tytul"},
|
||||
"symbol": {"symbol", "znak", "sign", "glyph"},
|
||||
"date": {"date", "data", "datetime", "timestamp"},
|
||||
"value": {"value", "wartosc", "val", "amount", "kwota"},
|
||||
"category": {"category", "kategoria", "type", "typ", "group", "grupa"},
|
||||
}
|
||||
|
||||
|
||||
def _normalize(col: str) -> str:
|
||||
s = str(col).strip().lower()
|
||||
s = re.sub(r"[ąàá]", "a", s)
|
||||
s = s.replace("ł", "l").replace("ż", "z").replace("ź", "z").replace("ć", "c")
|
||||
s = s.replace("ę", "e").replace("ó", "o").replace("ś", "s").replace("ń", "n")
|
||||
s = re.sub(r"[^a-z0-9]+", "", s)
|
||||
return s
|
||||
|
||||
|
||||
def build_column_mapping(header_cells: list[str]) -> dict[str, str]:
|
||||
"""Zwraca mapę pole_kanoniczne -> faktyczna_nazwa_kolumny dla danego pliku.
|
||||
|
||||
Kolumny nierozpoznane są zachowywane pod swoją (znormalizowaną) nazwą, więc
|
||||
nic nie ginie — po prostu nie mają aliasu kanonicznego.
|
||||
"""
|
||||
reverse: dict[str, str] = {}
|
||||
for canonical, aliases in CANONICAL_ALIASES.items():
|
||||
for alias in aliases:
|
||||
reverse[alias] = canonical
|
||||
|
||||
mapping: dict[str, str] = {}
|
||||
for actual in header_cells:
|
||||
norm = _normalize(actual)
|
||||
canonical = reverse.get(norm, norm or "col")
|
||||
# pierwsze trafienie wygrywa (stabilność przy duplikatach)
|
||||
mapping.setdefault(canonical, actual)
|
||||
return mapping
|
||||
Reference in New Issue
Block a user