mirror of
https://github.com/migatu/astrololo.git
synced 2026-07-14 13:34:38 +00:00
Wyszukiwarka: wynik obliczeń szukany w bazie interpretacji
Pierwsza wersja mostu horoskop -> sygnifikatory -> baza (zalążek LOG-16/18/19).
- logic/significators.py: z pozycji generuje tokeny w składni bazy (planeta
[Su, znak [Tau...), pyta warstwę danych o rekordy z tokenem planety i zawęża
do tych, które wspominają też jej znak ("planeta w swoim znaku"); odsiewa szum.
- logic /chart/report: nowy endpoint (pozycje -> raport dopasowań z interpretacjami).
- logic DataClient.search: parametr fields (lżejszy payload).
- data: naprawa str.contains regex=True -> regex=False (sygnifikatory zawierają
[ + itd., metaznaki regex); podniesiony górny limit zapytania (le=50000).
- prezentacja: strona /interpret (formularz -> wyszukane interpretacje per obiekt)
+ nawigacja.
Zweryfikowano end-to-end na realnym pliku (Encyclopaedia of Medical Astrology,
53969 wierszy): dla horoskopu 30.04.1984 znaleziono m.in. Sun w Taurus 46,
Mars w Scorpio 57, Saturn w Scorpio 61 dopasowań; przykłady: "[Su in [Tau" ->
"the bump of amativeness prominent". 15 testów przechodzi.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ i nie w bazie.
|
||||
## API
|
||||
- `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/compare` → jak wyżej → raport różnic dwóch silników (LOG-26; wymaga silnika B)
|
||||
- `GET /health` (sprawdza też warstwę bazodanową)
|
||||
|
||||
|
||||
@@ -16,9 +16,16 @@ class DataClient:
|
||||
def __init__(self, base_url: str | None = None) -> None:
|
||||
self.base_url = (base_url or settings.data_url).rstrip("/")
|
||||
|
||||
def search(self, key: str, value: str, exact: bool, limit: int) -> dict[str, Any]:
|
||||
payload = {"key": key, "value": value, "exact": exact, "limit": limit}
|
||||
with httpx.Client(timeout=settings.http_timeout) as client:
|
||||
def search(
|
||||
self,
|
||||
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.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
@@ -77,6 +77,30 @@ def chart_compare(req: PositionsRequest) -> dict:
|
||||
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")
|
||||
def health() -> dict:
|
||||
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
|
||||
Reference in New Issue
Block a user