from __future__ import annotations from dataclasses import dataclass, asdict from datetime import date, datetime, timedelta import math from pathlib import Path import pytz from skyfield.api import load from skyfield import almanac TZ = pytz.timezone("Europe/Paris") SIGN_NAMES = [ "Bélier", "Taureau", "Gémeaux", "Cancer", "Lion", "Vierge", "Balance", "Scorpion", "Sagittaire", "Capricorne", "Verseau", "Poissons", ] SIGN_TO_TYPE = { "Taureau": "Racine", "Vierge": "Racine", "Capricorne": "Racine", "Cancer": "Feuille", "Scorpion": "Feuille", "Poissons": "Feuille", "Gémeaux": "Fleur", "Balance": "Fleur", "Verseau": "Fleur", "Bélier": "Fruit", "Lion": "Fruit", "Sagittaire": "Fruit", } @dataclass class DayInfo: date: str phase: str illumination: float croissante_decroissante: str montante_descendante: str signe: str type_jour: str perigee: bool apogee: bool noeud_lunaire: bool def zodiac_sign_from_lon(lon_deg: float) -> str: return SIGN_NAMES[int(lon_deg // 30) % 12] def _local_noon(d: date) -> datetime: return TZ.localize(datetime(d.year, d.month, d.day, 12, 0, 0)) def _compute_perigee_apogee_days( ts, earth, moon, start: date, end: date ) -> tuple[set[date], set[date]]: sample_start = datetime.combine(start - timedelta(days=1), datetime.min.time()) sample_end = datetime.combine( end + timedelta(days=1), datetime.max.time().replace(microsecond=0) ) samples = [] current = TZ.localize(sample_start) end_local = TZ.localize(sample_end) step = timedelta(hours=1) while current <= end_local: t = ts.utc(current.astimezone(pytz.utc)) dist_km = earth.at(t).observe(moon).distance().km samples.append((current.date(), dist_km)) current += step perigee_days, apogee_days = set(), set() for i in range(1, len(samples) - 1): day, dist = samples[i] if not (start <= day <= end): continue prev_dist = samples[i - 1][1] next_dist = samples[i + 1][1] if dist < prev_dist and dist < next_dist: perigee_days.add(day) if dist > prev_dist and dist > next_dist: apogee_days.add(day) return perigee_days, apogee_days def build_calendar(start: date, end: date) -> list[DayInfo]: if end < start: raise ValueError(f"Invalid date range: {start} > {end}") ts = load.timescale() eph = load("de421.bsp") earth, moon, sun = eph["earth"], eph["moon"], eph["sun"] t0 = ts.utc(start.year, start.month, start.day) t1 = ts.utc(end.year, end.month, end.day + 1) f_phase = almanac.moon_phases(eph) phase_times, phase_events = almanac.find_discrete(t0, t1, f_phase) phase_by_day = {} for t, ev in zip(phase_times, phase_events): local_day = ( t.utc_datetime().replace(tzinfo=pytz.utc).astimezone(TZ).date() ) phase_by_day[local_day] = [ "Nouvelle Lune", "Premier Quartier", "Pleine Lune", "Dernier Quartier" ][int(ev)] f_nodes = almanac.moon_nodes(eph) node_times, _ = almanac.find_discrete(t0, t1, f_nodes) node_days = set() for t in node_times: local_day = ( t.utc_datetime().replace(tzinfo=pytz.utc).astimezone(TZ).date() ) node_days.add(local_day) perigee_days, apogee_days = _compute_perigee_apogee_days( ts, earth, moon, start, end ) result = [] d = start while d <= end: local_noon = _local_noon(d) t = ts.utc(local_noon.astimezone(pytz.utc)) e = earth.at(t) v_sun = e.observe(sun).apparent() v_moon = e.observe(moon).apparent() sep = v_sun.separation_from(v_moon).radians illum = (1 - math.cos(sep)) / 2 d2 = d + timedelta(days=1) local_noon2 = _local_noon(d2) t2 = ts.utc(local_noon2.astimezone(pytz.utc)) e2 = earth.at(t2) v_sun2 = e2.observe(sun).apparent() v_moon2 = e2.observe(moon).apparent() sep2 = v_sun2.separation_from(v_moon2).radians illum2 = (1 - math.cos(sep2)) / 2 croissante = "Croissante" if illum2 >= illum else "Décroissante" dec = v_moon.radec()[1].degrees dec2 = v_moon2.radec()[1].degrees montante = "Montante" if dec2 >= dec else "Descendante" lat, lon, dist = v_moon.ecliptic_latlon() signe = zodiac_sign_from_lon(lon.degrees % 360.0) type_jour = SIGN_TO_TYPE[signe] result.append( DayInfo( date=d.isoformat(), phase=phase_by_day.get(d, ""), illumination=round(illum * 100, 2), croissante_decroissante=croissante, montante_descendante=montante, signe=signe, type_jour=type_jour, perigee=(d in perigee_days), apogee=(d in apogee_days), noeud_lunaire=(d in node_days), ) ) d += timedelta(days=1) return result