diff --git a/Makefile b/Makefile index cea3734..00231e9 100644 --- a/Makefile +++ b/Makefile @@ -1,31 +1,56 @@ -.PHONY: help up down sample reindex migrate sql dev-data dev-logic dev-presentation +.PHONY: help install up down sample reindex migrate sql test clean-cache \ + dev-data dev-logic dev-presentation + +# Autodetekcja narzędzia compose: nowe "docker compose" albo stare "docker-compose". +COMPOSE := $(shell if docker compose version >/dev/null 2>&1; then echo "docker compose"; \ + elif command -v docker-compose >/dev/null 2>&1; then echo "docker-compose"; fi) + +# Python aktywnego środowiska (venv). Nadpisz: make install PY=python3.12 +PY ?= python help: - @echo "up - uruchom wszystkie 3 warstwy (docker compose)" - @echo "down - zatrzymaj" - @echo "sample - wygeneruj przykładowe pliki .xlsx" - @echo "reindex - zbuduj cache + indeks warstwy bazodanowej" - @echo "migrate - ETL: Excel -> SQL" - @echo "sql - uruchom z warstwą SQL (DATA_PROVIDER=sql)" - @echo "dev-* - uruchom pojedynczą warstwę lokalnie (bez dockera)" + @echo "install - zainstaluj zależności WSZYSTKICH warstw do aktywnego venv" + @echo "sample - wygeneruj przykładowe pliki .xlsx" + @echo "dev-data - uruchom warstwę danych lokalnie (:8002)" + @echo "dev-logic - uruchom warstwę logiczną lokalnie (:8001)" + @echo "dev-presentation - uruchom warstwę prezentacji lokalnie (:8000)" + @echo "test - testy warstwy logicznej (silnik efemeryd)" + @echo "clean-cache - wyczyść regenerowalny cache warstwy danych" + @echo "reindex - zbuduj cache + indeks warstwy bazodanowej" + @echo "migrate - ETL: Excel -> SQL" + @echo "up / down - uruchom / zatrzymaj cały stos (Docker; wykryto: $(COMPOSE))" + @echo "sql - Docker z warstwą SQL (DATA_PROVIDER=sql)" + @echo "" + @echo "Szybki start lokalny (bez Dockera), w 3 terminalach:" + @echo " make install" + @echo " make sample # opcjonalnie: dane przykładowe" + @echo " make dev-data # terminal 1" + @echo " make dev-logic # terminal 2" + @echo " make dev-presentation # terminal 3 -> http://localhost:8000" -up: - docker compose up --build - -down: - docker compose down +# --- instalacja zależności do aktywnego środowiska --- +install: + $(PY) -m pip install \ + -r services/data/requirements.txt \ + -r services/logic/requirements.txt \ + -r services/presentation/requirements.txt + @echo "OK. Zależności warstw zainstalowane. (Silnik AGPL 'engine-swisseph' instaluje się osobno.)" sample: - cd services/data && python scripts/make_sample_data.py + cd services/data && $(PY) scripts/make_sample_data.py reindex: - cd services/data && python -m app.ingest.build_index + cd services/data && $(PY) -m app.ingest.build_index migrate: - cd services/data && python -m app.ingest.to_sql + cd services/data && $(PY) -m app.ingest.to_sql -sql: - DATA_PROVIDER=sql docker compose up --build +test: + cd services/logic && PYTHONPATH=. $(PY) -m pytest tests -q + +clean-cache: + rm -rf services/data/.cache/* services/logic/.ephemeris/* 2>/dev/null || true + @echo "Cache wyczyszczony." # --- lokalny dev (3 osobne terminale) --- dev-data: @@ -36,3 +61,20 @@ dev-logic: dev-presentation: cd services/presentation && LOGIC_URL=http://localhost:8001 uvicorn app.main:app --reload --port 8000 + +# --- Docker (opcjonalnie) --- +up: + @if [ -z "$(COMPOSE)" ]; then \ + echo "Nie znaleziono Docker Compose (ani 'docker compose', ani 'docker-compose')."; \ + echo "Użyj trybu lokalnego: make install, potem dev-data / dev-logic / dev-presentation."; \ + exit 1; \ + fi + $(COMPOSE) up --build + +down: + @if [ -z "$(COMPOSE)" ]; then echo "Brak Docker Compose."; exit 1; fi + $(COMPOSE) down + +sql: + @if [ -z "$(COMPOSE)" ]; then echo "Brak Docker Compose."; exit 1; fi + DATA_PROVIDER=sql $(COMPOSE) up --build diff --git a/README.md b/README.md index 64cc77d..7b6969e 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,20 @@ make up # zbuduj i uruchom 3 warstwy # otwórz http://localhost:8000 ``` -## Szybki start (lokalnie, 3 terminale) +## Szybki start (lokalnie, bez Dockera — zalecane) +```bash +python -m venv .env && source .env/bin/activate # venv (jednorazowo) +make install # zależności WSZYSTKICH warstw +make sample # opcjonalnie: dane przykładowe +``` +Potem w 3 osobnych terminalach (w każdym `source .env/bin/activate`): ```bash -cd services/data && pip install -r requirements.txt && python scripts/make_sample_data.py make dev-data # terminal 1 -> :8002 make dev-logic # terminal 2 -> :8001 -make dev-presentation # terminal 3 -> :8000 +make dev-presentation # terminal 3 -> :8000 -> http://localhost:8000 ``` +> `make install` instaluje zależności wszystkich trzech warstw do aktywnego venv. +> Testy silnika: `make test`. Wyczyszczenie cache: `make clean-cache`. ## Modułowość — dowód - Wymień prezentację (np. na SPA/React) → reszta bez zmian, kontrakt `/api/query` stały. diff --git a/services/data/app/cache/frame_cache.py b/services/data/app/cache/frame_cache.py index c110c14..b699151 100644 --- a/services/data/app/cache/frame_cache.py +++ b/services/data/app/cache/frame_cache.py @@ -31,5 +31,10 @@ class FrameCache: return None def put(self, fp: str, sheet: str, frame: pd.DataFrame) -> None: - # astype(str) na kolumnach object zapewnia stabilny zapis Parquet - frame.to_parquet(self._path(fp, sheet), index=False) + # Kolumny z heterogenicznych plików Excela bywają mieszane (int + str + + # NaN w jednej kolumnie) — pyarrow tego nie zapisze. Warstwa danych i tak + # operuje na tekście (wyszukiwanie po .astype(str)), więc zapisujemy ramkę + # jako string (brak wartości -> pusty tekst). Gwarantuje to stabilny zapis + # Parquet niezależnie od zawartości pliku źródłowego. + safe = frame.astype("string").fillna("") + safe.to_parquet(self._path(fp, sheet), index=False) diff --git a/services/data/app/providers/excel_provider.py b/services/data/app/providers/excel_provider.py index a136ea2..d564010 100644 --- a/services/data/app/providers/excel_provider.py +++ b/services/data/app/providers/excel_provider.py @@ -82,7 +82,10 @@ class ExcelDataProvider(DataProvider): def warmup(self) -> None: for path in self._excel_files(): - self._ensure_indexed(path) + try: + self._ensure_indexed(path) + except Exception as e: # jeden uszkodzony plik nie może zablokować startu + print(f"[data] pominięto plik przy indeksowaniu: {path} — {e}") def _excel_files(self) -> list[str]: base = Path(self.s.excel_dir)