mirror of
https://github.com/migatu/astrololo.git
synced 2026-07-14 13:34:38 +00:00
Merge pull request #9 from migatu/feat/search-integration
Wyszukiwarka: wynik obliczen szukany w bazie interpretacji
This commit is contained in:
@@ -18,7 +18,7 @@ class SearchQuery(BaseModel):
|
|||||||
key: str = Field(..., description="Pole/kolumna kanoniczna, po której szukamy, np. 'name'.")
|
key: str = Field(..., description="Pole/kolumna kanoniczna, po której szukamy, np. 'name'.")
|
||||||
value: str = Field(..., description="Szukana wartość.")
|
value: str = Field(..., description="Szukana wartość.")
|
||||||
exact: bool = Field(False, description="Dopasowanie dokładne vs. zawieranie (contains).")
|
exact: bool = Field(False, description="Dopasowanie dokładne vs. zawieranie (contains).")
|
||||||
limit: int = Field(50, ge=1, le=1000)
|
limit: int = Field(50, ge=1, le=50000)
|
||||||
fields: list[str] | None = Field(
|
fields: list[str] | None = Field(
|
||||||
None, description="Lista pól kanonicznych do zwrócenia; None = wszystkie."
|
None, description="Lista pól kanonicznych do zwrócenia; None = wszystkie."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -115,7 +115,9 @@ class ExcelDataProvider(DataProvider):
|
|||||||
if query.exact:
|
if query.exact:
|
||||||
mask = col.str.lower() == query.value.lower()
|
mask = col.str.lower() == query.value.lower()
|
||||||
else:
|
else:
|
||||||
mask = col.str.lower().str.contains(query.value.lower(), na=False)
|
# regex=False: wartości sygnifikatorów zawierają znaki [ + itd.,
|
||||||
|
# które są metaznakami regex — szukamy dosłownie.
|
||||||
|
mask = col.str.lower().str.contains(query.value.lower(), na=False, regex=False)
|
||||||
matched = frame[mask]
|
matched = frame[mask]
|
||||||
if query.fields:
|
if query.fields:
|
||||||
keep = [c for c in query.fields if c in matched.columns]
|
keep = [c for c in query.fields if c in matched.columns]
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ i nie w bazie.
|
|||||||
|
|
||||||
## API
|
## API
|
||||||
- `POST /api/query` → `QueryRequest` → `QueryResponse`
|
- `POST /api/query` → `QueryRequest` → `QueryResponse`
|
||||||
|
- `POST /chart/positions` → `{when_utc, lat, lon, objects?}` → pozycje obiektów (LOG-01)
|
||||||
|
- `POST /chart/report` → `{when_utc, lat, lon, limit?}` → wynik obliczeń wyszukany w bazie: z pozycji generuje sygnifikatory (planeta w znaku) i zwraca pasujące interpretacje z warstwy danych (zalążek LOG-16/18/19)
|
||||||
- `POST /chart/positions` → `{when_utc, lat, lon, house_system?}` → pełny horoskop: pozycje (LOG-01) + osie i domy (LOG-05). `house_system`: `whole_sign` (dom.) / `equal` / `porphyry`.
|
- `POST /chart/positions` → `{when_utc, lat, lon, house_system?}` → pełny horoskop: pozycje (LOG-01) + osie i domy (LOG-05). `house_system`: `whole_sign` (dom.) / `equal` / `porphyry`.
|
||||||
- `POST /chart/compare` → jak wyżej → raport różnic dwóch silników (LOG-26; wymaga silnika B)
|
- `POST /chart/compare` → jak wyżej → raport różnic dwóch silników (LOG-26; wymaga silnika B)
|
||||||
- `GET /health` (sprawdza też warstwę bazodanową)
|
- `GET /health` (sprawdza też warstwę bazodanową)
|
||||||
|
|||||||
@@ -16,9 +16,16 @@ class DataClient:
|
|||||||
def __init__(self, base_url: str | None = None) -> None:
|
def __init__(self, base_url: str | None = None) -> None:
|
||||||
self.base_url = (base_url or settings.data_url).rstrip("/")
|
self.base_url = (base_url or settings.data_url).rstrip("/")
|
||||||
|
|
||||||
def search(self, key: str, value: str, exact: bool, limit: int) -> dict[str, Any]:
|
def search(
|
||||||
payload = {"key": key, "value": value, "exact": exact, "limit": limit}
|
self,
|
||||||
with httpx.Client(timeout=settings.http_timeout) as client:
|
key: str,
|
||||||
|
value: str,
|
||||||
|
exact: bool,
|
||||||
|
limit: int,
|
||||||
|
fields: list[str] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
payload = {"key": key, "value": value, "exact": exact, "limit": limit, "fields": fields}
|
||||||
|
with httpx.Client(timeout=max(settings.http_timeout, 30.0)) as client:
|
||||||
r = client.post(f"{self.base_url}/search", json=payload)
|
r = client.post(f"{self.base_url}/search", json=payload)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|||||||
@@ -78,6 +78,30 @@ def chart_compare(req: PositionsRequest) -> dict:
|
|||||||
return report.summary()
|
return report.summary()
|
||||||
|
|
||||||
|
|
||||||
|
class ReportRequest(BaseModel):
|
||||||
|
when_utc: datetime
|
||||||
|
lat: float = 0.0
|
||||||
|
lon: float = 0.0
|
||||||
|
limit: int = 5000
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/chart/report")
|
||||||
|
def chart_report(req: ReportRequest) -> dict:
|
||||||
|
"""Wynik obliczeń szukany w bazie: z pozycji generuje sygnifikatory i pyta
|
||||||
|
warstwę danych o pasujące interpretacje (pierwsza wersja przepływu)."""
|
||||||
|
from app.engine.models import ChartMoment
|
||||||
|
from app.significators import build_report
|
||||||
|
|
||||||
|
engine = get_engine()
|
||||||
|
moment = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon)
|
||||||
|
positions = engine.positions(moment)
|
||||||
|
try:
|
||||||
|
report = build_report(positions, DataClient(), per_object_limit=req.limit)
|
||||||
|
except httpx.HTTPError as e:
|
||||||
|
return {"engine": engine.name, "objects": [], "data_error": f"Warstwa danych niedostępna: {e}"}
|
||||||
|
return {"engine": engine.name, **report}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health() -> dict:
|
def health() -> dict:
|
||||||
info = {"status": "ok", "layer": "logic"}
|
info = {"status": "ok", "layer": "logic"}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"""Most: policzony horoskop → tokeny sygnifikatorów → wyszukiwanie w bazie.
|
||||||
|
|
||||||
|
Pierwsza wersja (LOG-16/18/19 w zalążku): z pozycji obiektów generujemy tokeny w
|
||||||
|
składni bazy (np. Słońce w Byku → planeta `[Su`, znak `[Tau`), pytamy warstwę
|
||||||
|
danych o rekordy zawierające token planety, a następnie zawężamy do tych, które
|
||||||
|
wspominają też jej znak („planeta w swoim znaku"). To realizuje przepływ „wynik
|
||||||
|
obliczeń szukany w pliku".
|
||||||
|
|
||||||
|
Format skrótów odczytany z realnej bazy (Encyclopaedia of Medical Astrology):
|
||||||
|
planety `[Su`,`[Mo`,…; znaki `[Ari`,`[Tau`,…; np. `[Su affl. in [Vir`.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Protocol
|
||||||
|
|
||||||
|
from app.engine.formats import SIGN_ABBR, sign_index
|
||||||
|
|
||||||
|
PLANET_ABBR = {
|
||||||
|
"Sun": "Su", "Moon": "Mo", "Mercury": "Me", "Venus": "Ve", "Mars": "Ma",
|
||||||
|
"Jupiter": "Ju", "Saturn": "Sa", "Uranus": "Ur", "Neptune": "Ne", "Pluto": "Pl",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DataSource(Protocol):
|
||||||
|
def search(
|
||||||
|
self, key: str, value: str, exact: bool, limit: int, fields: list[str] | None = None
|
||||||
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
|
|
||||||
|
def _effect(row: dict) -> str:
|
||||||
|
for col in ("actioneffect", "topicresult", "bodypart"):
|
||||||
|
v = row.get(col)
|
||||||
|
if v and str(v).strip().lower() not in ("", "nan"):
|
||||||
|
return str(v).strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _is_noise(sig: str, effect: str) -> bool:
|
||||||
|
s = sig.strip().lower()
|
||||||
|
return (
|
||||||
|
not effect
|
||||||
|
or s.startswith("significator")
|
||||||
|
or "header" in s
|
||||||
|
or s in ("x", "x?", "nan")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_report(positions, data: DataSource, per_object_limit: int = 60) -> dict:
|
||||||
|
"""Dla każdego obiektu: wyszukaj sygnifikatory planety i zawęź do jej znaku."""
|
||||||
|
items: list[dict] = []
|
||||||
|
provider = None
|
||||||
|
for p in positions:
|
||||||
|
if p.name not in PLANET_ABBR:
|
||||||
|
continue
|
||||||
|
planet_tok = "[" + PLANET_ABBR[p.name]
|
||||||
|
sign_tok = "[" + SIGN_ABBR[sign_index(p.longitude)]
|
||||||
|
|
||||||
|
raw = data.search(
|
||||||
|
key="significator",
|
||||||
|
value=planet_tok,
|
||||||
|
exact=False,
|
||||||
|
limit=per_object_limit,
|
||||||
|
fields=["significator", "actioneffect", "topicresult", "bodypart"],
|
||||||
|
)
|
||||||
|
provider = raw.get("provider", provider)
|
||||||
|
rows = raw.get("rows", [])
|
||||||
|
|
||||||
|
samples: list[dict] = []
|
||||||
|
for r in rows:
|
||||||
|
sig = str(r.get("significator") or "")
|
||||||
|
if sign_tok.lower() not in sig.lower():
|
||||||
|
continue
|
||||||
|
eff = _effect(r)
|
||||||
|
if _is_noise(sig, eff):
|
||||||
|
continue
|
||||||
|
samples.append({"significator": sig.strip(), "effect": eff})
|
||||||
|
|
||||||
|
items.append({
|
||||||
|
"object": p.name,
|
||||||
|
"sign": p.sign,
|
||||||
|
"direction": p.direction,
|
||||||
|
"planet_token": planet_tok,
|
||||||
|
"sign_token": sign_tok,
|
||||||
|
"planet_total": raw.get("total", 0),
|
||||||
|
"in_sign_count": len(samples),
|
||||||
|
"samples": samples[:5],
|
||||||
|
})
|
||||||
|
return {"provider": provider, "objects": items}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""Testy mostu obliczenia → sygnifikatory → wyszukiwanie (bez efemeryd/HTTP)."""
|
||||||
|
from app.engine.models import DEFAULT_OBJECTS, ObjectPosition
|
||||||
|
from app.significators import PLANET_ABBR, build_report
|
||||||
|
|
||||||
|
|
||||||
|
class FakeData:
|
||||||
|
def __init__(self, rows_by_value: dict) -> None:
|
||||||
|
self.rows_by_value = rows_by_value
|
||||||
|
|
||||||
|
def search(self, key, value, exact, limit, fields=None) -> dict:
|
||||||
|
rows = self.rows_by_value.get(value, [])
|
||||||
|
return {"provider": "fake", "total": len(rows), "rows": rows}
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_report_filters_to_sign_and_drops_noise():
|
||||||
|
positions = [ObjectPosition("Sun", 40.0, 0.0, 0.95, False)] # Taurus 10° -> [Su + [Tau
|
||||||
|
data = FakeData({"[Su": [
|
||||||
|
{"significator": "[Su in [Tau", "actioneffect": "efekt A"}, # trafienie
|
||||||
|
{"significator": "[Su in [Vir", "actioneffect": "inny znak"}, # zły znak
|
||||||
|
{"significator": "[Su in [Tau", "actioneffect": "nan"}, # szum (pusty efekt)
|
||||||
|
{"significator": "SIGNIFICATOR nagłówek", "actioneffect": "x"}, # szum (nagłówek)
|
||||||
|
]})
|
||||||
|
item = build_report(positions, data, per_object_limit=100)["objects"][0]
|
||||||
|
assert item["object"] == "Sun" and item["sign"] == "Taurus"
|
||||||
|
assert item["planet_token"] == "[Su" and item["sign_token"] == "[Tau"
|
||||||
|
assert item["planet_total"] == 4
|
||||||
|
assert item["in_sign_count"] == 1
|
||||||
|
assert item["samples"][0]["effect"] == "efekt A"
|
||||||
|
|
||||||
|
|
||||||
|
def test_effect_falls_back_to_topic_or_bodypart():
|
||||||
|
positions = [ObjectPosition("Mars", 220.0, 0.0, -0.2, True)] # Scorpio -> [Ma + [Sco
|
||||||
|
data = FakeData({"[Ma": [
|
||||||
|
{"significator": "[Ma in [Sco", "actioneffect": "", "topicresult": "temat X"},
|
||||||
|
{"significator": "[Ma in [Sco", "bodypart": "część ciała"},
|
||||||
|
]})
|
||||||
|
samples = build_report(positions, data)["objects"][0]["samples"]
|
||||||
|
assert samples[0]["effect"] == "temat X"
|
||||||
|
assert samples[1]["effect"] == "część ciała"
|
||||||
|
|
||||||
|
|
||||||
|
def test_planet_abbr_covers_all_default_objects():
|
||||||
|
for o in DEFAULT_OBJECTS:
|
||||||
|
assert o in PLANET_ABBR
|
||||||
@@ -43,3 +43,11 @@ class LogicClient:
|
|||||||
r = client.post(f"{self.base_url}/chart/positions", json=payload)
|
r = client.post(f"{self.base_url}/chart/positions", json=payload)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
def report(self, when_utc_iso: str, lat: float, lon: float, limit: int = 5000) -> dict[str, Any]:
|
||||||
|
"""Sygnifikatory z obliczeń szukane w bazie — woła logic /chart/report."""
|
||||||
|
payload = {"when_utc": when_utc_iso, "lat": lat, "lon": lon, "limit": limit}
|
||||||
|
with httpx.Client(timeout=max(settings.http_timeout, 30.0)) as client:
|
||||||
|
r = client.post(f"{self.base_url}/chart/report", json=payload)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|||||||
@@ -100,6 +100,34 @@ def significators_search(
|
|||||||
return templates.TemplateResponse(request, "significators.html", ctx)
|
return templates.TemplateResponse(request, "significators.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- Interpretacje (wynik obliczeń szukany w bazie) ----------------
|
||||||
|
@app.get("/interpret", response_class=HTMLResponse)
|
||||||
|
def interpret_form(request: Request):
|
||||||
|
return templates.TemplateResponse(request, "interpret.html", {"result": None, "form": {}})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/interpret", response_class=HTMLResponse)
|
||||||
|
def interpret_run(
|
||||||
|
request: Request,
|
||||||
|
date: str = Form(...),
|
||||||
|
time: str = Form(...),
|
||||||
|
tz_offset: float = Form(0.0),
|
||||||
|
lat: float = Form(0.0),
|
||||||
|
lon: float = Form(0.0),
|
||||||
|
):
|
||||||
|
form = {"date": date, "time": time, "tz_offset": tz_offset, "lat": lat, "lon": lon}
|
||||||
|
ctx: dict = {"form": form, "result": None, "error": None, "moment": None}
|
||||||
|
try:
|
||||||
|
iso_utc, label = _build_utc(date, time, tz_offset)
|
||||||
|
ctx["moment"] = label
|
||||||
|
ctx["result"] = logic.report(when_utc_iso=iso_utc, lat=lat, lon=lon)
|
||||||
|
except httpx.HTTPError as e:
|
||||||
|
ctx["error"] = _logic_error(e)
|
||||||
|
except ValueError as e:
|
||||||
|
ctx["error"] = f"Niepoprawne dane wejściowe: {e}"
|
||||||
|
return templates.TemplateResponse(request, "interpret.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health() -> dict:
|
def health() -> dict:
|
||||||
return {"status": "ok", "layer": "presentation"}
|
return {"status": "ok", "layer": "presentation"}
|
||||||
|
|||||||
@@ -50,3 +50,12 @@ table { width: 100%; border-collapse: collapse; margin-top: .5rem; background: v
|
|||||||
th, td { text-align: left; padding: .6rem .8rem; border-bottom: 1px solid var(--line); }
|
th, td { text-align: left; padding: .6rem .8rem; border-bottom: 1px solid var(--line); }
|
||||||
th { color: var(--accent); font-size: .8rem; text-transform: uppercase; letter-spacing: .5px; }
|
th { color: var(--accent); font-size: .8rem; text-transform: uppercase; letter-spacing: .5px; }
|
||||||
tr:last-child td { border-bottom: none; }
|
tr:last-child td { border-bottom: none; }
|
||||||
|
|
||||||
|
.muted { color: var(--muted); }
|
||||||
|
.small { font-size: .85rem; }
|
||||||
|
.nowrap { white-space: nowrap; }
|
||||||
|
.sig-item { margin-top: 1.25rem; }
|
||||||
|
.sig-head { padding: .5rem 0; }
|
||||||
|
.samples { margin-top: .3rem; }
|
||||||
|
.samples td { font-size: .92rem; vertical-align: top; }
|
||||||
|
.samples td.nowrap { color: var(--accent); padding-right: 1rem; }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<h1>astrololo</h1>
|
<h1>astrololo</h1>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/" class="{% block nav_chart %}{% endblock %}">Horoskop</a>
|
<a href="/" class="{% block nav_chart %}{% endblock %}">Horoskop</a>
|
||||||
|
<a href="/interpret" class="{% block nav_interp %}{% endblock %}">Interpretacje</a>
|
||||||
<a href="/significators" class="{% block nav_sig %}{% endblock %}">Sygnifikatory</a>
|
<a href="/significators" class="{% block nav_sig %}{% endblock %}">Sygnifikatory</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Interpretacje{% endblock %}
|
||||||
|
{% block nav_interp %}active{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p class="sub">Program policzy horoskop i wyszuka w bazie interpretacje pasujące do obliczeń (pierwsza wersja: planeta w swoim znaku).</p>
|
||||||
|
|
||||||
|
<form method="post" action="/interpret">
|
||||||
|
<div class="grid">
|
||||||
|
<label>Data
|
||||||
|
<input type="date" name="date" value="{{ form.date or '' }}" required>
|
||||||
|
</label>
|
||||||
|
<label>Godzina (lokalna)
|
||||||
|
<input type="time" name="time" value="{{ form.time or '' }}" required>
|
||||||
|
</label>
|
||||||
|
<label>Strefa (offset h)
|
||||||
|
<input type="number" name="tz_offset" step="0.25" value="{{ form.tz_offset if form.tz_offset is not none else 0 }}">
|
||||||
|
</label>
|
||||||
|
<label>Szerokość (lat)
|
||||||
|
<input type="number" name="lat" step="0.0001" value="{{ form.lat if form.lat is not none else 0 }}">
|
||||||
|
</label>
|
||||||
|
<label>Długość (lon)
|
||||||
|
<input type="number" name="lon" step="0.0001" value="{{ form.lon if form.lon is not none else 0 }}">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" id="nowBtn" class="ghost">Tu i teraz</button>
|
||||||
|
<button type="submit">Szukaj interpretacji</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if error %}<div class="error">{{ error }}</div>{% endif %}
|
||||||
|
|
||||||
|
{% if result %}
|
||||||
|
{% if result.data_error %}
|
||||||
|
<div class="error">{{ result.data_error }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="meta">
|
||||||
|
Silnik: <strong>{{ result.engine }}</strong>
|
||||||
|
{% if result.provider %}· baza: {{ result.provider }}{% endif %}
|
||||||
|
{% if moment %}· moment: {{ moment }}{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for o in result.objects %}
|
||||||
|
<div class="sig-item">
|
||||||
|
<div class="sig-head">
|
||||||
|
<strong>{{ o.object }}</strong> w <strong>{{ o.sign }}</strong>
|
||||||
|
<span class="{{ 'retro' if o.direction == 'Rx' else '' }}">{{ o.direction }}</span>
|
||||||
|
<span class="muted">— szukano {{ o.planet_token }} + {{ o.sign_token }};
|
||||||
|
w znaku: <strong>{{ o.in_sign_count }}</strong> (planeta ogółem: {{ o.planet_total }})</span>
|
||||||
|
</div>
|
||||||
|
{% if o.samples %}
|
||||||
|
<table class="samples">
|
||||||
|
<tbody>
|
||||||
|
{% for s in o.samples %}
|
||||||
|
<tr><td class="mono nowrap">{{ s.significator }}</td><td>{{ s.effect }}</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="muted small">brak dopasowań „w znaku" w bazie</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('nowBtn').addEventListener('click', function () {
|
||||||
|
const d = new Date();
|
||||||
|
const pad = n => String(n).padStart(2, '0');
|
||||||
|
document.querySelector('input[name=date]').value =
|
||||||
|
d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
|
||||||
|
document.querySelector('input[name=time]').value = pad(d.getHours()) + ':' + pad(d.getMinutes());
|
||||||
|
document.querySelector('input[name=tz_offset]').value = (-d.getTimezoneOffset() / 60);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user