Files
astrololo/services/data/app/models.py
T
gitea 2d7775f9b3
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m17s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Testy astroklienta (wersja demo) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m21s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m25s
Testy / Testy astroklienta (wersja demo) (pull_request) Successful in 9m26s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 5s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 4s
feat(astroklient): pule plików per konto i izolacja od produkcji (PRE-29)
Demo ma być rozdawane szeroko i różnym osobom, więc pierwsza wersja — jedno konto
na produkcyjnej warstwie danych — nie nadawała się do użycia: każdy dostawałby
dostęp do oryginalnych baz, a wgrania jednego klienta widzieliby wszyscy.

IZOLACJA OD PRODUKCJI. Warstwa danych i logiczna demo są osobne (manifesty w repo
deploy). Osobna musi być TEŻ LOGICZNA, bo zna ona jeden adres warstwy danych —
demo korzystające z produkcyjnej logiki i tak trafiłoby na produkcyjne bazy.

PULE PER KONTO w warstwie danych. Zapytanie i lista plików niosą nazwę puli;
puste = cały udział, czyli produkcja działa dokładnie jak dotąd i o pulach nic
nie wie. Nazwa puli przechodzi przez sito dopuszczające wyłącznie znaki bezpieczne
w nazwie katalogu — „../..” albo ukośnik wyprowadziłyby zapytanie wprost do cudzych
baz, więc sito ZAMIENIA podejrzane znaki zamiast ufać, że nikt ich nie poda.

PULA MUSI BYĆ W KLUCZU CACHE ZAPYTAŃ. Bez tego wynik policzony dla jednego konta
trafiłby z cache do drugiego — cicha wymiana treści baz między klientami,
niewidoczna w logach i nie do wykrycia z zewnątrz. Osobny test tego pilnuje.

PULA WYNIKA Z LOGINU, nigdy z żądania. Klient warstwy logicznej jest budowany
per żądanie i związany z pulą zalogowanej osoby; gdyby nazwa przychodziła
z formularza, wystarczyłoby podstawić cudzy login. Test wysyła `tenant`, `user`
i `login` w polach formularza i sprawdza, że nie mają na nią wpływu.

Pulę wstrzykujemy w INSTANCJĘ klienta, nie w sygnatury metod. Argumentem trzeba
by ją przeprowadzić przez protokół DataSource i build_report — kod, który o kontach
nie ma prawa nic wiedzieć — a każde nowe wywołanie byłoby okazją, żeby o nią
zapomnieć i sięgnąć nie tam.

Konta demo to lista `login:sekret` (DEMO_USERS), bo jedno wspólne konto oznaczałoby
wspólną pulę. Format i skrypt haseł te same, co w głównej aplikacji.

Pula klienta to JEDEN KATALOG, więc przejście na pełną wersję nie oznacza utraty
wgrań — procedurę importu opisuje runbook w repo deploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 19:00:39 +02:00

50 lines
2.0 KiB
Python

"""Kontrakt danych warstwy bazodanowej.
Te modele są JEDYNYM publicznym interfejsem tej warstwy. Warstwa logiczna zna
wyłącznie te kształty (poprzez HTTP/JSON) — nie wie nic o Excelu, cache ani SQL.
Dzięki temu można podmienić implementację (Excel -> SQL) bez zmiany pozostałych
warstw.
"""
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, Field
class SearchQuery(BaseModel):
"""Znormalizowane zapytanie wyszukiwania przychodzące z warstwy logicznej."""
key: str = Field(..., description="Pole/kolumna kanoniczna, po której szukamy, np. 'name'.")
value: str = Field(..., description="Szukana wartość.")
exact: bool = Field(False, description="Dopasowanie dokładne vs. zawieranie (contains).")
# Górny limit celowo niski: to zapytanie oddaje SUROWE wiersze baz, więc wysoki
# pułap zamienia je w narzędzie do masowego pobrania (LOG-32). 5000 = tyle, ile
# realnie potrzebuje build_report na jeden obiekt.
limit: int = Field(50, ge=1, le=5000)
fields: list[str] | None = Field(
None, description="Lista pól kanonicznych do zwrócenia; None = wszystkie."
)
# PULA KONTA (PRE-29). Puste = cały udział, czyli zachowanie produkcji, która
# o pulach nic nie wie. Ustawione = wyszukiwanie widzi WYŁĄCZNIE podkatalog
# tego konta. Używa tego wersja demo, gdzie każde konto ma własny, niewidoczny
# dla pozostałych zbiór baz.
tenant: str = Field("", description="Pula plików konta; puste = cały udział.")
class SearchResult(BaseModel):
"""Wynik wyszukiwania zwracany w górę do warstwy logicznej."""
rows: list[dict[str, Any]]
total: int
elapsed_ms: float
cache: str = Field("miss", description="hit/miss/partial — skąd pochodzą dane.")
provider: str = Field(..., description="Nazwa aktywnej implementacji, np. 'excel' lub 'sql'.")
class HealthInfo(BaseModel):
status: str = "ok"
provider: str
indexed_files: int = 0
details: dict[str, Any] = Field(default_factory=dict)