Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4a37aca52 | |||
| e39c31618d |
@@ -1,32 +0,0 @@
|
|||||||
name: build-swisseph
|
|
||||||
|
|
||||||
# Osobny pipeline dla silnika B (Swiss Ephemeris, AGPL) — celowo ODDZIELONY od
|
|
||||||
# głównego build.yaml (data/logic/presentation). Buduje się tylko, gdy zmienia się
|
|
||||||
# sam silnik, i nie miesza obrazu AGPL do pipeline'u permisywnego produktu.
|
|
||||||
#
|
|
||||||
# Obraz konsumuje profil deployu `astrololo-swisseph` w repo `deploy`.
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
paths:
|
|
||||||
- 'services/engine-swisseph/**'
|
|
||||||
- '.gitea/workflows/build-swisseph.yaml'
|
|
||||||
workflow_dispatch: {} # ręczne odpalenie (bootstrap pierwszego obrazu)
|
|
||||||
|
|
||||||
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 engine-swisseph (AGPL, izolowany)
|
|
||||||
run: |
|
|
||||||
TAG=${GITHUB_SHA::8}
|
|
||||||
IMG=gitea.czernobog.pl/gitea/astrololo-engine-swisseph
|
|
||||||
# Obraz kompiluje pyswisseph ze źródeł (brak wheeli dla cp312) — build jest
|
|
||||||
# zarazem realnym testem Dockerfile'a.
|
|
||||||
docker build -t $IMG:$TAG -t $IMG:latest ./services/engine-swisseph
|
|
||||||
docker push $IMG:$TAG
|
|
||||||
docker push $IMG:latest
|
|
||||||
echo "Zbudowano i wypchnięto: $IMG:$TAG (+ latest)"
|
|
||||||
@@ -51,37 +51,6 @@ jobs:
|
|||||||
EPHEMERIS_DIR: ${{ github.workspace }}/services/logic/.ephemeris
|
EPHEMERIS_DIR: ${{ github.workspace }}/services/logic/.ephemeris
|
||||||
run: pytest tests -q -rs
|
run: pytest tests -q -rs
|
||||||
|
|
||||||
swisseph-image:
|
|
||||||
name: Build obrazu silnika B (swisseph)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
# Obraz kompiluje pyswisseph ze źródeł (brak wheeli dla cp312), więc ten
|
|
||||||
# build jest realnym testem Dockerfile'a — nie tylko pobraniem paczek.
|
|
||||||
- name: docker build
|
|
||||||
run: docker build -t astrololo/engine-swisseph:ci services/engine-swisseph
|
|
||||||
|
|
||||||
- name: Smoke test (health + pozycje)
|
|
||||||
run: |
|
|
||||||
docker run -d --name swe -p 8003:8003 astrololo/engine-swisseph:ci
|
|
||||||
for i in $(seq 1 30); do
|
|
||||||
curl -fsS http://localhost:8003/health >/dev/null 2>&1 && break
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
curl -fsS http://localhost:8003/health
|
|
||||||
echo
|
|
||||||
# Horoskop referencyjny (30.04.1984) — ten sam, na którym opieramy testy
|
|
||||||
# silnika własnego; sprawdzamy, że silnik B faktycznie liczy.
|
|
||||||
curl -fsS -X POST http://localhost:8003/positions \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{"when_utc":"1984-04-30T09:20:00Z","lat":50.0647,"lon":19.9450}'
|
|
||||||
echo
|
|
||||||
|
|
||||||
- name: Logi kontenera (gdy coś padło)
|
|
||||||
if: failure()
|
|
||||||
run: docker logs swe || true
|
|
||||||
|
|
||||||
compile-all:
|
compile-all:
|
||||||
name: Kontrola składni wszystkich warstw
|
name: Kontrola składni wszystkich warstw
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -1,34 +1,10 @@
|
|||||||
# Build wieloetapowy — bo `pyswisseph` to rozszerzenie C bez gotowych wheeli.
|
|
||||||
#
|
|
||||||
# Na PyPI (2.10.3.2) wheels kończą się na cp311 i obejmują wyłącznie i686/x86_64.
|
|
||||||
# Dla Pythona 3.12 oraz dla arm64 pip ZAWSZE kompiluje ze źródeł, a `-slim` nie ma
|
|
||||||
# kompilatora — dlatego jednoetapowy build tu padał. Kompilujemy w etapie builder,
|
|
||||||
# a do obrazu finalnego wchodzi już tylko gotowy wheel (bez toolchaina).
|
|
||||||
|
|
||||||
FROM python:3.12-slim AS builder
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends build-essential \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
|
|
||||||
|
|
||||||
|
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /wheels /wheels
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt \
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
&& rm -rf /wheels
|
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Sanity check na etapie budowania: brak działającego swissepha ma wywalić build,
|
|
||||||
# a nie dopiero pierwszy request.
|
|
||||||
RUN python -c "import swisseph as swe; swe.set_ephe_path(None); print('swisseph OK', swe.version)"
|
|
||||||
|
|
||||||
EXPOSE 8003
|
EXPOSE 8003
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8003"]
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8003"]
|
||||||
|
|||||||
@@ -15,23 +15,9 @@ permisywnej reszty systemu. **Nie wchodzi do dystrybucji zamkniętego produktu.*
|
|||||||
|
|
||||||
Tryb Moshiera (`FLG_MOSEPH`) — bez plików efemeryd, zero konfiguracji.
|
Tryb Moshiera (`FLG_MOSEPH`) — bez plików efemeryd, zero konfiguracji.
|
||||||
|
|
||||||
## Build obrazu
|
|
||||||
```bash
|
|
||||||
docker compose --profile comparison build engine-swisseph
|
|
||||||
```
|
|
||||||
Dockerfile jest **wieloetapowy** i to nie jest ozdobnik: `pyswisseph` to rozszerzenie
|
|
||||||
C, a na PyPI (2.10.3.2) gotowe wheels kończą się na **cp311** i obejmują wyłącznie
|
|
||||||
**i686/x86_64**. Dla Pythona 3.12 oraz dla arm64 pip musi kompilować ze źródeł, więc
|
|
||||||
sam `python:3.12-slim` (bez kompilatora) build wywracał. Kompilacja idzie w etapie
|
|
||||||
`builder` (`build-essential`), a do obrazu finalnego trafia już tylko gotowy wheel —
|
|
||||||
runtime zostaje czysty i mały. Pierwszy build trwa ~1–2 min, kolejne idą z cache warstw.
|
|
||||||
|
|
||||||
Build kończy się sanity-checkiem (`import swisseph`), żeby niedziałający silnik
|
|
||||||
wykrzaczył build, a nie dopiero pierwszy request.
|
|
||||||
|
|
||||||
## Uruchomienie (tylko profil porównawczy / dev / CI)
|
## Uruchomienie (tylko profil porównawczy / dev / CI)
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt # wymaga kompilatora C (patrz wyżej)
|
pip install -r requirements.txt
|
||||||
uvicorn app.main:app --port 8003
|
uvicorn app.main:app --port 8003
|
||||||
```
|
```
|
||||||
Następnie w warstwie logicznej ustaw `ENGINE_SWISSEPH_URL=http://localhost:8003`,
|
Następnie w warstwie logicznej ustaw `ENGINE_SWISSEPH_URL=http://localhost:8003`,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ i nie w bazie.
|
|||||||
|
|
||||||
## API
|
## API
|
||||||
- `POST /api/query` → `QueryRequest` → `QueryResponse`
|
- `POST /api/query` → `QueryRequest` → `QueryResponse`
|
||||||
- `POST /chart/positions` → `{when_utc, lat, lon, house_system?}` → pełny horoskop: pozycje (LOG-01) + osie i domy (LOG-05) + aspekty główne z applying/separating (LOG-06) + opcjonalnie stacje planet (`stations:true`, LOG-03). Zwraca też sektę i 7 Lots hermetycznych z domami (LOG-08). Obiekty: 10 planet + mean NN/SN/Lilith (LOG-02). `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) + aspekty główne z applying/separating (LOG-06) + opcjonalnie stacje planet (`stations:true`, LOG-03). Obiekty: 10 planet + mean NN/SN/Lilith (LOG-02). `house_system`: `whole_sign` (dom.) / `equal` / `porphyry`.
|
||||||
- `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)
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ ABBREVIATIONS: dict[str, str] = {
|
|||||||
"NN": "North Node", "SN": "South Node", "Lilith": "Lilith", "Chiron": "Chiron",
|
"NN": "North Node", "SN": "South Node", "Lilith": "Lilith", "Chiron": "Chiron",
|
||||||
# osie
|
# osie
|
||||||
"Asc": "Ascendant", "Dsc": "Descendant", "MC": "Midheaven", "IC": "Imum Coeli",
|
"Asc": "Ascendant", "Dsc": "Descendant", "MC": "Midheaven", "IC": "Imum Coeli",
|
||||||
# Lots (punkty arabskie)
|
|
||||||
"PF": "Part of Fortune", "Fortune": "Part of Fortune", "Spirit": "Lot of Spirit",
|
|
||||||
# aspekty
|
# aspekty
|
||||||
"conj": "conjunction", "sex": "sextile", "sq": "square", "tri": "trine",
|
"conj": "conjunction", "sex": "sextile", "sq": "square", "tri": "trine",
|
||||||
"opp": "opposition", "semisex": "semisextile", "semisq": "semisquare",
|
"opp": "opposition", "semisex": "semisextile", "semisq": "semisquare",
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ def _fmt(name: str, lon: float) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str = H.WHOLE_SIGN,
|
def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str = H.WHOLE_SIGN) -> dict:
|
||||||
lots_method: str = "degree") -> dict:
|
|
||||||
from app.engine.aspects import find_aspects
|
from app.engine.aspects import find_aspects
|
||||||
|
|
||||||
positions = engine.positions(moment)
|
positions = engine.positions(moment)
|
||||||
@@ -51,20 +50,4 @@ def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str
|
|||||||
]
|
]
|
||||||
for pdict, obj in zip(result["positions"], positions):
|
for pdict, obj in zip(result["positions"], positions):
|
||||||
pdict["house"] = H.assign_house(obj.longitude, cusp_list)
|
pdict["house"] = H.assign_house(obj.longitude, cusp_list)
|
||||||
|
|
||||||
# Lots (LOG-08) — wymagają Asc i sekty (dzień/noc)
|
|
||||||
from app.engine.firdaria import is_day_birth
|
|
||||||
from app.engine.lots import compute_lots
|
|
||||||
|
|
||||||
pts = {p.name: p.longitude for p in positions}
|
|
||||||
pts["Asc"] = asc
|
|
||||||
day = is_day_birth(pts["Sun"], asc, mc) if "Sun" in pts else True
|
|
||||||
result["sect"] = "day" if day else "night"
|
|
||||||
result["lots"] = [
|
|
||||||
{**lot,
|
|
||||||
"sign": SIGNS[sign_index(lot["longitude"])],
|
|
||||||
"in_sign": in_sign(lot["longitude"]),
|
|
||||||
"house": H.assign_house(lot["longitude"], cusp_list)}
|
|
||||||
for lot in compute_lots(pts, day, lots_method)
|
|
||||||
]
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
"""Lots / punkty arabskie (LOG-08) — 7 Lots hermetycznych.
|
|
||||||
|
|
||||||
Formuła: Lot = C + A − B (od punktu C odmierzamy odległość między A i B).
|
|
||||||
Większość Lots **odwraca się w horoskopach nocnych** (zamiana A↔B) — np.
|
|
||||||
Fortuna: dzień Asc + Mo − Su, noc Asc + Su − Mo.
|
|
||||||
|
|
||||||
Dwa warianty liczenia (notes3):
|
|
||||||
- `degree` (domyślny) — dokładny stopień,
|
|
||||||
- `sign` — liczone całymi znakami (Lot wypada na 0° wyliczonego znaku).
|
|
||||||
|
|
||||||
Kolejność ma znaczenie: Fortuna i Duch liczone są pierwsze, bo pozostałe Lots
|
|
||||||
odwołują się do nich.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from app.engine.formats import norm360, sign_index
|
|
||||||
|
|
||||||
# (nazwa, C, A, B, odwracalny w nocy)
|
|
||||||
LOT_DEFS: list[tuple[str, str, str, str, bool]] = [
|
|
||||||
("Fortune", "Asc", "Moon", "Sun", True),
|
|
||||||
("Spirit", "Asc", "Sun", "Moon", True),
|
|
||||||
("Eros", "Asc", "Venus", "Spirit", True),
|
|
||||||
("Necessity", "Asc", "Fortune", "Mercury", True),
|
|
||||||
("Courage", "Asc", "Fortune", "Mars", True),
|
|
||||||
("Victory", "Asc", "Jupiter", "Spirit", True),
|
|
||||||
("Nemesis", "Asc", "Fortune", "Saturn", True),
|
|
||||||
]
|
|
||||||
|
|
||||||
METHODS = ("degree", "sign")
|
|
||||||
|
|
||||||
|
|
||||||
def compute_lots(
|
|
||||||
points: dict[str, float], is_day: bool, method: str = "degree"
|
|
||||||
) -> list[dict]:
|
|
||||||
"""points: nazwa → długość natalna (wymagane Asc + planety formuł).
|
|
||||||
|
|
||||||
Zwraca listę {name, longitude, formula} w kolejności definicji.
|
|
||||||
"""
|
|
||||||
if method not in METHODS:
|
|
||||||
raise ValueError(f"nieznana metoda liczenia Lots: {method}")
|
|
||||||
|
|
||||||
vals = dict(points)
|
|
||||||
out: list[dict] = []
|
|
||||||
for name, c, a, b, reversible in LOT_DEFS:
|
|
||||||
first, second = (a, b) if (is_day or not reversible) else (b, a)
|
|
||||||
if any(k not in vals for k in (c, first, second)):
|
|
||||||
continue # brak składnika — pomijamy
|
|
||||||
if method == "sign":
|
|
||||||
idx = (sign_index(vals[c]) + sign_index(vals[first])
|
|
||||||
- sign_index(vals[second])) % 12
|
|
||||||
lon = idx * 30.0
|
|
||||||
else:
|
|
||||||
lon = norm360(vals[c] + vals[first] - vals[second])
|
|
||||||
vals[name] = lon # dostępny dla kolejnych Lots
|
|
||||||
out.append({
|
|
||||||
"name": name,
|
|
||||||
"longitude": lon,
|
|
||||||
"formula": f"{c} + {first} − {second}",
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
@@ -22,8 +22,6 @@ PLANET_ABBR = {
|
|||||||
"Jupiter": "Ju", "Saturn": "Sa", "Uranus": "Ur", "Neptune": "Ne", "Pluto": "Pl",
|
"Jupiter": "Ju", "Saturn": "Sa", "Uranus": "Ur", "Neptune": "Ne", "Pluto": "Pl",
|
||||||
# punkty wirtualne — tokeny wg SIGNIFICATORS KEY ([NN, [SN, [Lilith)
|
# punkty wirtualne — tokeny wg SIGNIFICATORS KEY ([NN, [SN, [Lilith)
|
||||||
"North Node": "NN", "South Node": "SN", "Lilith": "Lilith",
|
"North Node": "NN", "South Node": "SN", "Lilith": "Lilith",
|
||||||
# Lots — w bazie Fortuna występuje jako [PF (Part of Fortune)
|
|
||||||
"Fortune": "PF", "Spirit": "Spirit",
|
|
||||||
}
|
}
|
||||||
SIGN_TO_ABBR = dict(zip(SIGNS, SIGN_ABBR))
|
SIGN_TO_ABBR = dict(zip(SIGNS, SIGN_ABBR))
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
"""Lots / punkty arabskie (LOG-08).
|
|
||||||
|
|
||||||
Wyrocznie z notes3 (astro-seek, horoskop referencyjny 30.04.1984, urodzenie DZIENNE):
|
|
||||||
- Fortuna wprost w tabeli obiektów: Cancer 12°35'24" = 102.5900°
|
|
||||||
- Ducha (Spirit) potwierdza jego antyscja: Taurus 28°14' -> Spirit = 180 − 58.2333
|
|
||||||
"""
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from app.engine.formats import sign_index
|
|
||||||
from app.engine.lots import LOT_DEFS, compute_lots
|
|
||||||
|
|
||||||
# natalne długości horoskopu referencyjnego
|
|
||||||
NATAL = {"Asc": 112.1786, "Sun": 40.1392, "Moon": 30.5514, "Mercury": 27.3839,
|
|
||||||
"Venus": 27.6769, "Mars": 234.5411, "Jupiter": 282.9617, "Saturn": 223.3108}
|
|
||||||
|
|
||||||
FORTUNE_REF = 102.5900 # Cancer 12°35'24"
|
|
||||||
SPIRIT_REF = 180.0 - 58.2333 # z antyscji Taurus 28°14'
|
|
||||||
|
|
||||||
|
|
||||||
def _by_name(lots):
|
|
||||||
return {lot["name"]: lot["longitude"] for lot in lots}
|
|
||||||
|
|
||||||
|
|
||||||
def _arcmin(a, b):
|
|
||||||
return abs(((a - b + 180.0) % 360.0) - 180.0) * 60.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_fortune_matches_astroseek():
|
|
||||||
lots = _by_name(compute_lots(NATAL, is_day=True))
|
|
||||||
assert _arcmin(lots["Fortune"], FORTUNE_REF) < 2.0
|
|
||||||
assert sign_index(lots["Fortune"]) == 3 # Cancer
|
|
||||||
|
|
||||||
|
|
||||||
def test_spirit_matches_antiscia_reference():
|
|
||||||
lots = _by_name(compute_lots(NATAL, is_day=True))
|
|
||||||
assert _arcmin(lots["Spirit"], SPIRIT_REF) < 2.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_fortune_and_spirit_reverse_at_night():
|
|
||||||
day = _by_name(compute_lots(NATAL, is_day=True))
|
|
||||||
night = _by_name(compute_lots(NATAL, is_day=False))
|
|
||||||
# nocą formuła się odwraca: Fortuna nocna = Duch dzienny (i odwrotnie)
|
|
||||||
assert _arcmin(night["Fortune"], day["Spirit"]) < 0.01
|
|
||||||
assert _arcmin(night["Spirit"], day["Fortune"]) < 0.01
|
|
||||||
|
|
||||||
|
|
||||||
def test_all_seven_hermetic_lots_present():
|
|
||||||
lots = compute_lots(NATAL, is_day=True)
|
|
||||||
assert [lot["name"] for lot in lots] == [d[0] for d in LOT_DEFS]
|
|
||||||
assert len(lots) == 7
|
|
||||||
|
|
||||||
|
|
||||||
def test_derived_lots_use_fortune_and_spirit():
|
|
||||||
lots = _by_name(compute_lots(NATAL, is_day=True))
|
|
||||||
# Necessity = Asc + Fortune − Mercury
|
|
||||||
expected = (NATAL["Asc"] + lots["Fortune"] - NATAL["Mercury"]) % 360.0
|
|
||||||
assert _arcmin(lots["Necessity"], expected) < 0.01
|
|
||||||
# Eros = Asc + Venus − Spirit
|
|
||||||
expected_eros = (NATAL["Asc"] + NATAL["Venus"] - lots["Spirit"]) % 360.0
|
|
||||||
assert _arcmin(lots["Eros"], expected_eros) < 0.01
|
|
||||||
|
|
||||||
|
|
||||||
def test_by_sign_method_lands_on_sign_start():
|
|
||||||
lots = compute_lots(NATAL, is_day=True, method="sign")
|
|
||||||
for lot in lots:
|
|
||||||
assert lot["longitude"] % 30.0 == 0.0 # 0° wyliczonego znaku
|
|
||||||
|
|
||||||
|
|
||||||
def test_unknown_method_rejected():
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
compute_lots(NATAL, is_day=True, method="bzdura")
|
|
||||||
@@ -90,18 +90,6 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% if result.lots %}
|
|
||||||
<div class="meta">Lots hermetyczne ({{ result.lots | length }}) · sekta: <strong>{{ result.sect }}</strong></div>
|
|
||||||
<table class="angles">
|
|
||||||
<thead><tr><th>Lot</th><th>Znak</th><th>W znaku</th><th>Dom</th><th>Formuła</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
{% for l in result.lots %}
|
|
||||||
<tr><td>{{ l.name }}</td><td>{{ l.sign }}</td><td class="mono">{{ l.in_sign }}</td><td>{{ l.house }}</td><td class="muted small">{{ l.formula }}</td></tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% set with_stations = result.positions | selectattr('stations', 'defined') | list %}
|
{% set with_stations = result.positions | selectattr('stations', 'defined') | list %}
|
||||||
{% if with_stations %}
|
{% if with_stations %}
|
||||||
<div class="meta">Stacje planet (poprzednia / następna; <span class="badge">blisko</span> = mniej niż 7 dni)</div>
|
<div class="meta">Stacje planet (poprzednia / następna; <span class="badge">blisko</span> = mniej niż 7 dni)</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user