Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d5b65ddbd | |||
| cff0ac194c | |||
| 73f39e7df8 | |||
| 96d983b26a | |||
| c4a181b810 | |||
| 97ad21d2e4 |
@@ -0,0 +1,19 @@
|
|||||||
|
name: build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Login
|
||||||
|
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login gitea.czernobog.pl -u gitea --password-stdin
|
||||||
|
- name: Build & push (data, logic, presentation)
|
||||||
|
run: |
|
||||||
|
TAG=${GITHUB_SHA::8}
|
||||||
|
for SVC in data logic presentation; do
|
||||||
|
docker build -t gitea.czernobog.pl/gitea/astrololo-$SVC:$TAG ./services/$SVC
|
||||||
|
docker push gitea.czernobog.pl/gitea/astrololo-$SVC:$TAG
|
||||||
|
done
|
||||||
|
echo "Tag: $TAG"
|
||||||
@@ -10,7 +10,8 @@ i nie w bazie.
|
|||||||
- `POST /chart/report` → `{when_utc, lat, lon, limit?}` → wynik obliczeń wyszukany w bazie: fasety sygnifikatorów **w znaku / w domu / w aspekcie**, z rozwinięciem skrótów, odsiewaniem duplikatów (ten sam sygnifikator i opis), rankingiem siły (LOG-21) oraz opcją group (grupowanie identycznych opisów)
|
- `POST /chart/report` → `{when_utc, lat, lon, limit?}` → wynik obliczeń wyszukany w bazie: fasety sygnifikatorów **w znaku / w domu / w aspekcie**, z rozwinięciem skrótów, odsiewaniem duplikatów (ten sam sygnifikator i opis), rankingiem siły (LOG-21) oraz opcją group (grupowanie identycznych opisów)
|
||||||
- `POST /chart/profections` → `{when_utc, lat, lon, start_age?, count?}` → profekcje roczne: wiek, profektowany Asc, Władca Roku (+MC/Su/Mo) (LOG-10)
|
- `POST /chart/profections` → `{when_utc, lat, lon, start_age?, count?}` → profekcje roczne: wiek, profektowany Asc, Władca Roku (+MC/Su/Mo) (LOG-10)
|
||||||
- `POST /chart/return` → `{when_utc, lat, lon, kind, around?}` → Solar/Lunar Return: moment powrotu + pełny horoskop na ten moment (LOG-12)
|
- `POST /chart/return` → `{when_utc, lat, lon, kind, around?}` → Solar/Lunar Return: moment powrotu + pełny horoskop na ten moment (LOG-12)
|
||||||
- `POST /chart/timeline` → `{when_utc, lat, lon, from_date, to_date, techniques?}` → zbiorcza oś czasu: profekcje + Solar Return + dyrekcje solar-arc, posortowane (technique | significator | start | exact | end) (LOG-14)
|
- `POST /chart/firdaria` → `{when_utc, lat, lon}` → Firdaria: sekta (dzień/noc), okresy główne i podokresy time-lordów (LOG-11)
|
||||||
|
- `POST /chart/timeline` → `{when_utc, lat, lon, from_date, to_date, techniques?}` → zbiorcza oś czasu: profekcje + Solar Return + dyrekcje solar-arc + Firdaria, posortowane (technique | significator | start | exact | end); z interpret=true dopina interpretacje z bazy do dat (LOG-14, 1B->2B)
|
||||||
- `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ą)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"""Firdaria (LOG-11) — perska technika time-lord.
|
||||||
|
|
||||||
|
Sekwencja okresów głównych zależy od sekty (dzień/noc). Sekta: urodzenie dzienne,
|
||||||
|
gdy Słońce jest nad horyzontem, czyli po tej samej stronie osi Asc–Dsc co MC.
|
||||||
|
|
||||||
|
Klasyczne długości okresów (lata): Su 10, Ve 8, Me 13, Mo 9, Sa 11, Ju 12, Ma 7
|
||||||
|
(razem 70) + Węzeł Północny 3 + Węzeł Południowy 2 = 75 lat. Każdy okres główny
|
||||||
|
planety dzieli się na 7 podokresów (sub-lord w tej samej kolejności, cyklicznie).
|
||||||
|
Węzły — bez podokresów (najczęstsza konwencja).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
DAY_ORDER = ["Sun", "Venus", "Mercury", "Moon", "Saturn", "Jupiter", "Mars"]
|
||||||
|
NIGHT_ORDER = ["Moon", "Saturn", "Jupiter", "Mars", "Sun", "Venus", "Mercury"]
|
||||||
|
YEARS = {"Sun": 10, "Venus": 8, "Mercury": 13, "Moon": 9,
|
||||||
|
"Saturn": 11, "Jupiter": 12, "Mars": 7}
|
||||||
|
NODES = [("North Node", 3), ("South Node", 2)]
|
||||||
|
DAYS_PER_YEAR = 365.2422
|
||||||
|
|
||||||
|
|
||||||
|
def is_day_birth(sun_lon: float, asc: float, mc: float) -> bool:
|
||||||
|
"""Słońce nad horyzontem = ta sama półkula osi Asc–Dsc co MC."""
|
||||||
|
return (((sun_lon - asc) % 360.0) < 180.0) == (((mc - asc) % 360.0) < 180.0)
|
||||||
|
|
||||||
|
|
||||||
|
def _date(birth: datetime, years: float) -> str:
|
||||||
|
return (birth + timedelta(days=years * DAYS_PER_YEAR)).date().isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def firdaria(birth: datetime, sun_lon: float, asc: float, mc: float) -> dict:
|
||||||
|
"""Pełny rozkład Firdarii: sekta, kolejność, okresy główne i podokresy."""
|
||||||
|
day = is_day_birth(sun_lon, asc, mc)
|
||||||
|
order = DAY_ORDER if day else NIGHT_ORDER
|
||||||
|
majors = [(lord, YEARS[lord]) for lord in order] + NODES
|
||||||
|
|
||||||
|
periods: list[dict] = []
|
||||||
|
age = 0.0
|
||||||
|
for lord, yrs in majors:
|
||||||
|
period = {"lord": lord, "years": yrs,
|
||||||
|
"start": _date(birth, age), "end": _date(birth, age + yrs)}
|
||||||
|
if lord in YEARS: # planeta -> 7 podokresów
|
||||||
|
sub_len = yrs / 7.0
|
||||||
|
i = order.index(lord)
|
||||||
|
sub_age = age
|
||||||
|
subs: list[dict] = []
|
||||||
|
for k in range(7):
|
||||||
|
sub_lord = order[(i + k) % 7]
|
||||||
|
subs.append({"lord": sub_lord,
|
||||||
|
"start": _date(birth, sub_age),
|
||||||
|
"end": _date(birth, sub_age + sub_len)})
|
||||||
|
sub_age += sub_len
|
||||||
|
period["sub"] = subs
|
||||||
|
periods.append(period)
|
||||||
|
age += yrs
|
||||||
|
return {"sect": "day" if day else "night", "order": order, "periods": periods}
|
||||||
@@ -58,13 +58,15 @@ def solar_arc_directions(
|
|||||||
if not (lo_age <= age <= hi_age) or (p == q and arc < 1e-6):
|
if not (lo_age <= age <= hi_age) or (p == q and arc < 1e-6):
|
||||||
continue
|
continue
|
||||||
exact = _add_years(birth, age)
|
exact = _add_years(birth, age)
|
||||||
out.append(_row(
|
row = _row(
|
||||||
"solar_arc",
|
"solar_arc",
|
||||||
f"dyr. {p} {PL_NAME[asp]} {q}",
|
f"dyr. {p} {PL_NAME[asp]} {q}",
|
||||||
_add_years(birth, age - orb_years),
|
_add_years(birth, age - orb_years),
|
||||||
exact,
|
exact,
|
||||||
_add_years(birth, age + orb_years),
|
_add_years(birth, age + orb_years),
|
||||||
))
|
)
|
||||||
|
row.update(directed=p, aspect=asp, target=q) # do budowy tokenów (1B->2B)
|
||||||
|
out.append(row)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -83,10 +85,13 @@ def profection_events(natal_asc: float, birth: datetime, lo: datetime, hi: datet
|
|||||||
if end < lo or start > hi:
|
if end < lo or start > hi:
|
||||||
continue
|
continue
|
||||||
sign = profected_sign(natal_asc, age)
|
sign = profected_sign(natal_asc, age)
|
||||||
out.append(_row(
|
lord = DOMICILE_RULERS[sign]
|
||||||
"profection", f"Władca Roku: {DOMICILE_RULERS[sign]} (Asc {sign}, wiek {age})",
|
row = _row(
|
||||||
|
"profection", f"Władca Roku: {lord} (Asc {sign}, wiek {age})",
|
||||||
start, start, end,
|
start, start, end,
|
||||||
))
|
)
|
||||||
|
row.update(lord=lord, sign=sign) # do budowy tokenów (1B->2B)
|
||||||
|
out.append(row)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -104,6 +109,28 @@ def solar_return_events(engine, natal_moment, birth: datetime, lo: datetime, hi:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def firdaria_events(natal_points: dict[str, float], birth: datetime, lo: datetime, hi: datetime) -> list[dict]:
|
||||||
|
"""Starty okresów/podokresów Firdarii (LOG-11) nachodzące na okno."""
|
||||||
|
from app.engine.firdaria import firdaria
|
||||||
|
|
||||||
|
fd = firdaria(birth, natal_points["Sun"], natal_points["Asc"], natal_points["MC"])
|
||||||
|
out: list[dict] = []
|
||||||
|
for period in fd["periods"]:
|
||||||
|
if "sub" in period:
|
||||||
|
for s in period["sub"]:
|
||||||
|
if lo <= _as_dt(s["start"]) <= hi:
|
||||||
|
row = _row("firdaria", f"Firdaria: {period['lord']} / {s['lord']}",
|
||||||
|
s["start"], s["start"], s["end"])
|
||||||
|
row.update(fd_major=period["lord"], fd_sub=s["lord"])
|
||||||
|
out.append(row)
|
||||||
|
elif lo <= _as_dt(period["start"]) <= hi: # węzeł — bez podokresów
|
||||||
|
row = _row("firdaria", f"Firdaria: {period['lord']}",
|
||||||
|
period["start"], period["start"], period["end"])
|
||||||
|
row.update(fd_major=period["lord"])
|
||||||
|
out.append(row)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _as_dt(d) -> datetime:
|
def _as_dt(d) -> datetime:
|
||||||
if isinstance(d, datetime):
|
if isinstance(d, datetime):
|
||||||
return d if d.tzinfo else d.replace(tzinfo=timezone.utc)
|
return d if d.tzinfo else d.replace(tzinfo=timezone.utc)
|
||||||
@@ -119,7 +146,7 @@ def build_timeline(
|
|||||||
"""Scala wybrane techniki w jedną oś czasu, posortowaną po dacie dokładnej."""
|
"""Scala wybrane techniki w jedną oś czasu, posortowaną po dacie dokładnej."""
|
||||||
lo, hi = _as_dt(from_d), _as_dt(to_d)
|
lo, hi = _as_dt(from_d), _as_dt(to_d)
|
||||||
birth = natal_moment.when_utc
|
birth = natal_moment.when_utc
|
||||||
want = set(techniques or ["profection", "solar_return", "solar_arc"])
|
want = set(techniques or ["profection", "solar_return", "solar_arc", "firdaria"])
|
||||||
events: list[dict] = []
|
events: list[dict] = []
|
||||||
if "profection" in want:
|
if "profection" in want:
|
||||||
events += profection_events(natal_points["Asc"], birth, lo, hi)
|
events += profection_events(natal_points["Asc"], birth, lo, hi)
|
||||||
@@ -127,5 +154,7 @@ def build_timeline(
|
|||||||
events += solar_return_events(engine, natal_moment, birth, lo, hi)
|
events += solar_return_events(engine, natal_moment, birth, lo, hi)
|
||||||
if "solar_arc" in want:
|
if "solar_arc" in want:
|
||||||
events += solar_arc_directions(natal_points, birth, lo, hi)
|
events += solar_arc_directions(natal_points, birth, lo, hi)
|
||||||
|
if "firdaria" in want:
|
||||||
|
events += firdaria_events(natal_points, birth, lo, hi)
|
||||||
events.sort(key=lambda e: e["exact"])
|
events.sort(key=lambda e: e["exact"])
|
||||||
return events
|
return events
|
||||||
|
|||||||
@@ -173,6 +173,27 @@ def chart_return(req: ReturnRequest) -> dict:
|
|||||||
"return_utc": hit.isoformat(), **chart}
|
"return_utc": hit.isoformat(), **chart}
|
||||||
|
|
||||||
|
|
||||||
|
class FirdariaRequest(BaseModel):
|
||||||
|
when_utc: datetime # moment urodzenia (UTC)
|
||||||
|
lat: float = 0.0
|
||||||
|
lon: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/chart/firdaria")
|
||||||
|
def chart_firdaria(req: FirdariaRequest) -> dict:
|
||||||
|
"""Firdaria (LOG-11): sekta + okresy główne i podokresy time-lordów."""
|
||||||
|
from app.engine import houses as H
|
||||||
|
from app.engine.firdaria import firdaria
|
||||||
|
from app.engine.models import ChartMoment
|
||||||
|
|
||||||
|
engine = get_engine()
|
||||||
|
natal = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon)
|
||||||
|
ramc, eps = engine.sidereal(natal)
|
||||||
|
asc, mc = H.compute_asc(ramc, eps, natal.lat), H.compute_mc(ramc, eps)
|
||||||
|
sun = engine.positions(natal, ["Sun"])[0].longitude
|
||||||
|
return {"engine": engine.name, **firdaria(req.when_utc, sun, asc, mc)}
|
||||||
|
|
||||||
|
|
||||||
class TimelineRequest(BaseModel):
|
class TimelineRequest(BaseModel):
|
||||||
when_utc: datetime # moment urodzenia (UTC)
|
when_utc: datetime # moment urodzenia (UTC)
|
||||||
lat: float = 0.0
|
lat: float = 0.0
|
||||||
@@ -180,11 +201,15 @@ class TimelineRequest(BaseModel):
|
|||||||
from_date: str # zakres: YYYY-MM-DD
|
from_date: str # zakres: YYYY-MM-DD
|
||||||
to_date: str
|
to_date: str
|
||||||
techniques: list[str] | None = None # profection | solar_return | solar_arc
|
techniques: list[str] | None = None # profection | solar_return | solar_arc
|
||||||
|
interpret: bool = False # dopnij interpretacje z bazy (1B->2B)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/chart/timeline")
|
@app.post("/chart/timeline")
|
||||||
def chart_timeline(req: TimelineRequest) -> dict:
|
def chart_timeline(req: TimelineRequest) -> dict:
|
||||||
"""Zbiorcza oś czasu z technik (LOG-14): technique | significator | start | exact | end."""
|
"""Zbiorcza oś czasu z technik (LOG-14): technique | significator | start | exact | end.
|
||||||
|
|
||||||
|
Z interpret=true dopina do zdarzeń interpretacje z warstwy danych (LOG-19, 1B->2B).
|
||||||
|
"""
|
||||||
from app.engine import houses as H
|
from app.engine import houses as H
|
||||||
from app.engine.models import ChartMoment
|
from app.engine.models import ChartMoment
|
||||||
from app.engine.timeline import build_timeline
|
from app.engine.timeline import build_timeline
|
||||||
@@ -196,8 +221,16 @@ def chart_timeline(req: TimelineRequest) -> dict:
|
|||||||
for p in engine.positions(natal):
|
for p in engine.positions(natal):
|
||||||
points[p.name] = p.longitude
|
points[p.name] = p.longitude
|
||||||
events = build_timeline(engine, natal, points, req.from_date, req.to_date, req.techniques)
|
events = build_timeline(engine, natal, points, req.from_date, req.to_date, req.techniques)
|
||||||
return {"engine": engine.name, "from": req.from_date, "to": req.to_date,
|
|
||||||
"count": len(events), "events": events}
|
out = {"engine": engine.name, "from": req.from_date, "to": req.to_date}
|
||||||
|
if req.interpret:
|
||||||
|
from app.significators import interpret_events
|
||||||
|
try:
|
||||||
|
interpret_events(events, DataClient())
|
||||||
|
except httpx.HTTPError as e:
|
||||||
|
out["data_error"] = f"Warstwa danych niedostępna: {e}"
|
||||||
|
out.update(count=len(events), events=events)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
|
|||||||
@@ -195,3 +195,61 @@ def build_report(
|
|||||||
"facets": facets,
|
"facets": facets,
|
||||||
})
|
})
|
||||||
return {"provider": provider, "objects": items}
|
return {"provider": provider, "objects": items}
|
||||||
|
|
||||||
|
|
||||||
|
def _event_tokens(event: dict) -> list[str]:
|
||||||
|
"""Tokeny bazy dla zdarzenia osi czasu (spięcie 1B→2B). Pierwszy = planeta."""
|
||||||
|
from app.engine.aspects import DB_TOKEN
|
||||||
|
|
||||||
|
if event.get("technique") == "solar_arc":
|
||||||
|
p = PLANET_ABBR.get(event.get("directed"))
|
||||||
|
a = DB_TOKEN.get(event.get("aspect"))
|
||||||
|
q = PLANET_ABBR.get(event.get("target")) # None dla Asc/MC (nie ma tokenu)
|
||||||
|
toks = []
|
||||||
|
if p:
|
||||||
|
toks.append("[" + p)
|
||||||
|
if a:
|
||||||
|
toks.append(a)
|
||||||
|
if q:
|
||||||
|
toks.append("[" + q)
|
||||||
|
return toks if p else []
|
||||||
|
if event.get("technique") == "profection":
|
||||||
|
lord = PLANET_ABBR.get(event.get("lord"))
|
||||||
|
sign = SIGN_TO_ABBR.get(event.get("sign"))
|
||||||
|
toks = []
|
||||||
|
if lord:
|
||||||
|
toks.append("[" + lord)
|
||||||
|
if sign:
|
||||||
|
toks.append("[" + sign)
|
||||||
|
return toks if lord else []
|
||||||
|
if event.get("technique") == "firdaria":
|
||||||
|
major = PLANET_ABBR.get(event.get("fd_major"))
|
||||||
|
sub = PLANET_ABBR.get(event.get("fd_sub"))
|
||||||
|
toks = []
|
||||||
|
if major:
|
||||||
|
toks.append("[" + major)
|
||||||
|
if sub:
|
||||||
|
toks.append("[" + sub)
|
||||||
|
return toks if major else []
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def interpret_events(events: list[dict], data: DataSource, limit: int = 4,
|
||||||
|
per_object_limit: int = 5000) -> list[dict]:
|
||||||
|
"""Dopina interpretacje z bazy do zdarzeń osi czasu (predykcyjne 1B → 2B).
|
||||||
|
|
||||||
|
Wyszukuje po tokenie planety zdarzenia i zawęża do wszystkich tokenów
|
||||||
|
(aspekt/druga planeta lub znak), z odsiewaniem szumu i duplikatów.
|
||||||
|
"""
|
||||||
|
for ev in events:
|
||||||
|
tokens = _event_tokens(ev)
|
||||||
|
if not tokens:
|
||||||
|
continue
|
||||||
|
raw = data.search(
|
||||||
|
key="significator", value=tokens[0], exact=False, limit=per_object_limit,
|
||||||
|
fields=["significator", "actioneffect", "topicresult", "bodypart"],
|
||||||
|
)
|
||||||
|
samples = _facet_samples(raw.get("rows", []), tokens)
|
||||||
|
ev["interpretations"] = samples[:limit]
|
||||||
|
ev["interpretations_count"] = len(samples)
|
||||||
|
return events
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""Firdaria (LOG-11) — sekta, kolejność, długości okresów, podokresy."""
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
from app.engine.firdaria import DAY_ORDER, NIGHT_ORDER, YEARS, firdaria, is_day_birth
|
||||||
|
from app.significators import _event_tokens
|
||||||
|
|
||||||
|
BIRTH = dt.datetime(1984, 4, 30, 7, 35, tzinfo=dt.timezone.utc)
|
||||||
|
# horoskop referencyjny: Asc 112.18, MC 352.59, Sun 40.14 -> Day birth (notes3)
|
||||||
|
ASC, MC, SUN = 112.18, 352.59, 40.14
|
||||||
|
|
||||||
|
|
||||||
|
def test_reference_is_day_birth():
|
||||||
|
assert is_day_birth(SUN, ASC, MC) is True # notes3: "Day birth (Diurnal)"
|
||||||
|
|
||||||
|
|
||||||
|
def test_night_birth_uses_night_order():
|
||||||
|
# Słońce po stronie IC (przeciwna półkula) -> noc
|
||||||
|
assert is_day_birth((SUN + 180) % 360, ASC, MC) is False
|
||||||
|
fd = firdaria(BIRTH, (SUN + 180) % 360, ASC, MC)
|
||||||
|
assert fd["sect"] == "night" and fd["order"] == NIGHT_ORDER
|
||||||
|
|
||||||
|
|
||||||
|
def test_diurnal_sequence_and_totals():
|
||||||
|
fd = firdaria(BIRTH, SUN, ASC, MC)
|
||||||
|
assert fd["sect"] == "day" and fd["order"] == DAY_ORDER
|
||||||
|
lords = [p["lord"] for p in fd["periods"]]
|
||||||
|
assert lords == DAY_ORDER + ["North Node", "South Node"]
|
||||||
|
assert sum(p["years"] for p in fd["periods"]) == 75 # 70 + 3 + 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_planet_majors_have_seven_subperiods_summing_to_period():
|
||||||
|
fd = firdaria(BIRTH, SUN, ASC, MC)
|
||||||
|
for p in fd["periods"]:
|
||||||
|
if p["lord"] in YEARS:
|
||||||
|
assert len(p["sub"]) == 7
|
||||||
|
assert p["sub"][0]["lord"] == p["lord"] # sub zaczyna się od władcy okresu
|
||||||
|
assert p["sub"][0]["start"] == p["start"]
|
||||||
|
assert p["sub"][-1]["end"] == p["end"]
|
||||||
|
else:
|
||||||
|
assert "sub" not in p # węzły bez podokresów
|
||||||
|
|
||||||
|
|
||||||
|
def test_periods_are_contiguous():
|
||||||
|
fd = firdaria(BIRTH, SUN, ASC, MC)
|
||||||
|
for a, b in zip(fd["periods"], fd["periods"][1:]):
|
||||||
|
assert a["end"] == b["start"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_age_42_is_saturn_major():
|
||||||
|
# kumulatywnie: Su10 Ve8 Me13 Mo9 -> 40; Saturn 40-51 -> wiek 42 w Saturnie
|
||||||
|
fd = firdaria(BIRTH, SUN, ASC, MC)
|
||||||
|
saturn = next(p for p in fd["periods"] if p["lord"] == "Saturn")
|
||||||
|
assert saturn["start"].startswith("2024") and saturn["end"].startswith("2035")
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_tokens_firdaria():
|
||||||
|
ev = {"technique": "firdaria", "fd_major": "Saturn", "fd_sub": "Jupiter"}
|
||||||
|
assert _event_tokens(ev) == ["[Sa", "[Ju"]
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""Spięcie osi czasu z bazą interpretacji (LOG-14 → 1B→2B)."""
|
||||||
|
from app.significators import _event_tokens, interpret_events
|
||||||
|
|
||||||
|
|
||||||
|
class FakeData:
|
||||||
|
def __init__(self, rows_by_value):
|
||||||
|
self.rows_by_value = rows_by_value
|
||||||
|
|
||||||
|
def search(self, key, value, exact, limit, fields=None):
|
||||||
|
rows = self.rows_by_value.get(value, [])
|
||||||
|
return {"provider": "fake", "total": len(rows), "rows": rows}
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_tokens_solar_arc():
|
||||||
|
ev = {"technique": "solar_arc", "directed": "Venus", "aspect": "conjunction", "target": "North Node"}
|
||||||
|
assert _event_tokens(ev) == ["[Ve", "[conj", "[NN"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_tokens_solar_arc_to_angle_has_no_target_token():
|
||||||
|
ev = {"technique": "solar_arc", "directed": "Sun", "aspect": "square", "target": "MC"}
|
||||||
|
assert _event_tokens(ev) == ["[Su", "[sq"] # MC nie ma tokenu planety
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_tokens_profection():
|
||||||
|
ev = {"technique": "profection", "lord": "Saturn", "sign": "Capricorn"}
|
||||||
|
assert _event_tokens(ev) == ["[Sa", "[Cap"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_solar_return_has_no_tokens():
|
||||||
|
assert _event_tokens({"technique": "solar_return"}) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_interpret_attaches_matches_with_all_tokens():
|
||||||
|
events = [
|
||||||
|
{"technique": "solar_arc", "directed": "Venus", "aspect": "conjunction", "target": "North Node"},
|
||||||
|
{"technique": "solar_return"},
|
||||||
|
]
|
||||||
|
data = FakeData({"[Ve": [
|
||||||
|
{"significator": "[Ve [conj [NN", "actioneffect": "spotkanie losowe"}, # wszystkie tokeny
|
||||||
|
{"significator": "[Ve [conj [Mo", "actioneffect": "inny"}, # brak [NN
|
||||||
|
{"significator": "[Ve [conj [NN", "actioneffect": "spotkanie losowe"}, # duplikat
|
||||||
|
]})
|
||||||
|
interpret_events(events, data)
|
||||||
|
assert events[0]["interpretations_count"] == 1 # duplikat odsiany, tylko z [NN
|
||||||
|
assert events[0]["interpretations"][0]["expanded"] == "Venus conjunction North Node"
|
||||||
|
assert "interpretations" not in events[1] # solar_return pominięty
|
||||||
@@ -56,3 +56,17 @@ class LogicClient:
|
|||||||
r = client.post(f"{self.base_url}/chart/report", json=payload)
|
r = client.post(f"{self.base_url}/chart/report", json=payload)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
def timeline(
|
||||||
|
self, when_utc_iso: str, lat: float, lon: float,
|
||||||
|
from_date: str, to_date: str, interpret: bool = True,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Oś czasu z technik (+interpretacje z bazy) — woła logic /chart/timeline."""
|
||||||
|
payload = {
|
||||||
|
"when_utc": when_utc_iso, "lat": lat, "lon": lon,
|
||||||
|
"from_date": from_date, "to_date": to_date, "interpret": interpret,
|
||||||
|
}
|
||||||
|
with httpx.Client(timeout=max(settings.http_timeout, 60.0)) as client:
|
||||||
|
r = client.post(f"{self.base_url}/chart/timeline", json=payload)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|||||||
@@ -132,6 +132,40 @@ def interpret_run(
|
|||||||
return templates.TemplateResponse(request, "interpret.html", ctx)
|
return templates.TemplateResponse(request, "interpret.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- Kalendarz (oś czasu z technik + interpretacje) ----------------
|
||||||
|
@app.get("/timeline", response_class=HTMLResponse)
|
||||||
|
def timeline_form(request: Request):
|
||||||
|
return templates.TemplateResponse(request, "timeline.html", {"result": None, "form": {}})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/timeline", response_class=HTMLResponse)
|
||||||
|
def timeline_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),
|
||||||
|
from_date: str = Form(...),
|
||||||
|
to_date: str = Form(...),
|
||||||
|
):
|
||||||
|
form = {"date": date, "time": time, "tz_offset": tz_offset, "lat": lat, "lon": lon,
|
||||||
|
"from_date": from_date, "to_date": to_date}
|
||||||
|
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.timeline(
|
||||||
|
when_utc_iso=iso_utc, lat=lat, lon=lon,
|
||||||
|
from_date=from_date, to_date=to_date, interpret=True,
|
||||||
|
)
|
||||||
|
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, "timeline.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health() -> dict:
|
def health() -> dict:
|
||||||
return {"status": "ok", "layer": "presentation"}
|
return {"status": "ok", "layer": "presentation"}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<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="/interpret" class="{% block nav_interp %}{% endblock %}">Interpretacje</a>
|
||||||
|
<a href="/timeline" class="{% block nav_timeline %}{% endblock %}">Kalendarz</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 %}Kalendarz{% endblock %}
|
||||||
|
{% block nav_timeline %}active{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p class="sub">Zbiorcza oś czasu technik predykcyjnych (profekcje, solariusze, dyrekcje solar-arc) z interpretacjami z bazy dla dat.</p>
|
||||||
|
|
||||||
|
<form method="post" action="/timeline">
|
||||||
|
<div class="grid">
|
||||||
|
<label>Data urodzenia
|
||||||
|
<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="grid">
|
||||||
|
<label>Zakres od
|
||||||
|
<input type="date" name="from_date" value="{{ form.from_date or '' }}" required>
|
||||||
|
</label>
|
||||||
|
<label>Zakres do
|
||||||
|
<input type="date" name="to_date" value="{{ form.to_date or '' }}" required>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" id="nowBtn" class="ghost">Tu i teraz</button>
|
||||||
|
<button type="submit">Pokaż kalendarz</button>
|
||||||
|
<span id="geoNote" class="muted small"></span>
|
||||||
|
</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> ·
|
||||||
|
zdarzeń: {{ result.count }} · zakres {{ result.from }} → {{ result.to }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for e in result.events %}
|
||||||
|
<div class="sig-item">
|
||||||
|
<div class="sig-head">
|
||||||
|
<span class="badge">{{ e.exact }}</span>
|
||||||
|
<span class="badge">{{ e.technique }}</span>
|
||||||
|
<strong>{{ e.significator }}</strong>
|
||||||
|
<span class="muted small">(okno {{ e.start }} → {{ e.end }})</span>
|
||||||
|
</div>
|
||||||
|
{% if e.interpretations %}
|
||||||
|
<table class="samples">
|
||||||
|
<tbody>
|
||||||
|
{% for s in e.interpretations %}
|
||||||
|
<tr><td class="sig nowrap" title="{{ s.significator }}">{{ s.expanded }}</td><td>{{ s.effect }}</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% if e.interpretations_count > e.interpretations | length %}
|
||||||
|
<div class="muted small">…i {{ e.interpretations_count - (e.interpretations | length) }} więcej</div>
|
||||||
|
{% endif %}
|
||||||
|
{% elif e.technique != 'solar_return' %}
|
||||||
|
<div class="muted small">brak dopasowań w bazie</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script src="/static/now.js"></script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user