Skip to content

Commit 478ef32

Browse files
committed
Merge branch 'review' into alpha/v6
2 parents 81cef51 + aaa9b79 commit 478ef32

128 files changed

Lines changed: 7997 additions & 6741 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,11 @@ sync-docs.sh
8585

8686
# Build-time font cache for scripts/build_chart_glyphs.py (not redistributed)
8787
scripts/.glyph-cache/
88+
89+
# Artifacts written to the repo root by doc-snippet harness runs
90+
/report.txt
91+
/alice_report.txt
92+
/chart_data.json
93+
/planetary_positions.csv
94+
/transit_charts/
95+
/*.svg

DEVELOPMENT.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Kerykeion uses [poethepoet](https://github.com/nat-n/poethepoet) as a task runne
4545
Tests are organized in 4 tiers (each tier includes the previous):
4646

4747
```bash
48-
# Core tests (fastest, ~1750+ tests, excludes heavy parametrized suites)
48+
# Core tests (fastest, ~4,500 tests, excludes heavy parametrized suites)
4949
uv run poe test:core
5050

5151
# Base tier (DE440s range: 1849-2150)
@@ -57,7 +57,7 @@ uv run poe test:medium
5757
# Extended tier (DE441 full range)
5858
uv run poe test:extended
5959

60-
# All tests including online (GeoNames API)
60+
# All offline tests (alias for test:extended; online GeoNames tests stay excluded)
6161
uv run poe test:all
6262

6363
# Run with coverage
@@ -87,7 +87,7 @@ uv run poe typecheck
8787
# Type checking with MyPy
8888
uv run poe analyze
8989

90-
# Run all quality checks (lint + typecheck)
90+
# Run the full quality gate (ruff lint + mypy + pyright + full pytest suite)
9191
uv run poe quality
9292
```
9393

@@ -136,7 +136,7 @@ kerykeion/
136136
│ ├── secondary_progressions/ # Progressions & solar arc
137137
│ ├── settings/ # Configuration & constants
138138
│ └── vedic/ # Nakshatra support
139-
├── tests/core/ # Test suite (59+ files)
139+
├── tests/core/ # Test suite (72 files)
140140
├── examples/ # Usage examples
141141
├── site/docs/ # Documentation source (markdown)
142142
├── release_notes/ # Per-version release notes
@@ -282,7 +282,7 @@ uv sync --upgrade
282282
uv add "requests>=2.32.0" --upgrade
283283

284284
# Check for outdated dependencies
285-
uv tree
285+
uv tree --outdated
286286
```
287287

288288
## 🏗️ Building the Package

MANDATORY_EVOLUTIONS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ HIGH was exactly this: heliacal treating every backend error as "no event").
8282
(libephemeris runs `heliacal_ut` through Skyfield, which raises its own
8383
`ValueError` subclass — round 23 discovered this).
8484
- Migrate every ad-hoc `getattr(ephe, "Error", …)` handler and the remaining
85-
dead `except RuntimeError` site (`sun_times/utils.py:346`, left untouched in
85+
dead `except RuntimeError` site (`sun_times/utils.py:368`, left untouched in
8686
round 23 because no harm was reproduced) to the canonical types.
8787
- **Runtime validation of open `str` name parameters** at public entries whose
8888
type is only a `Literal` at static-check time (no runtime enforcement for
@@ -120,7 +120,7 @@ different chart with only scattered log lines to explain it:
120120
needs a missing planetary kernel (notably the **Sun**, which needs `sepl_18.se1`)
121121
falls back to its **geocentric** position — returned under the planetocentric
122122
label, ~62° off, with only a `logging.warning`. The fallback is deliberate and
123-
logged (see the comment at `astrological_subject_factory.py` ~2353), but the
123+
logged (see the comment at `astrological_subject_factory.py` ~2386), but the
124124
point is not flagged as degraded on the returned model.
125125
- **Chiron / asteroids / TNOs** need `seas_18.se1`; **fixed stars** need
126126
`sefstars.txt`; **barycentric** perspectives need `sepl_*.se1`. These are

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ It is [open source](https://github.com/g-battaglia/Astrologer-API) and directly
8282
- [Fixed Stars](#fixed-stars)
8383
- [JSON Support](#json-support)
8484
- [Moon Phase Details](#moon-phase-details)
85+
- [Timing Factories](#timing-factories)
86+
- [Sun Times](#sun-times)
87+
- [Planetary Hours](#planetary-hours)
88+
- [Void of Course Moon](#void-of-course-moon)
89+
- [Lunation Finder](#lunation-finder)
90+
- [Retrograde Stations](#retrograde-stations)
91+
- [Sign Ingresses](#sign-ingresses)
8592
- [V6 Advanced Features](#v6-advanced-features)
8693
- [Uranian / Hamburg School Planets](#uranian--hamburg-school-planets)
8794
- [Essential Dignities](#essential-dignities)
@@ -111,6 +118,7 @@ It is [open source](https://github.com/g-battaglia/Astrologer-API) and directly
111118
- [Documentation](#documentation)
112119
- [Projects built with Kerykeion](#projects-built-with-kerykeion)
113120
- [Development](#development)
121+
- [Using the Swiss Ephemeris Backend (Optional)](#using-the-swiss-ephemeris-backend-optional)
114122
- [Integrating Kerykeion into Your Project](#integrating-kerykeion-into-your-project)
115123
- [License](#license)
116124
- [Contributing](#contributing)

TEST.md

Lines changed: 106 additions & 90 deletions
Large diffs are not rendered by default.

examples/current_time_report.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@
44
from kerykeion import ChartDataFactory
55
from kerykeion import AstrologicalSubjectFactory
66

7-
now = AstrologicalSubjectFactory.from_current_time(geonames_username="century.boy")
7+
# Offline location (Rome): no GeoNames account needed. For online lookup,
8+
# pass city/nation plus your own geonames_username instead.
9+
now = AstrologicalSubjectFactory.from_current_time(
10+
city="Rome",
11+
nation="IT",
12+
lng=12.4964,
13+
lat=41.8933,
14+
tz_str="Europe/Rome",
15+
online=False,
16+
)
817

918
# Create chart data - this calculates elements, qualities, and aspects
1019
chart = ChartDataFactory.create_chart_data(

examples/modern_chart_john_lennon.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from pathlib import Path
55
from os import makedirs as mkdirs
66

7-
OUTPUT = str(Path.home() / "kerykeion_charts_output")
7+
OUTPUT = str(Path(__file__).parent / "output")
88
THEMES = ["classic", "black-and-white", "dark", "dark-high-contrast", "light", "strawberry"]
99

1010
# ── Subject 1: John Lennon ──────────────────────────────────────────────────

kerykeion/astrological_subject_factory.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@
146146
}
147147
)
148148

149+
# Every name accepted in ``active_points``. Unknown names (typos) must raise
150+
# instead of silently vanishing from the calculation loop.
151+
_VALID_ACTIVE_POINT_NAMES = frozenset(get_args(AstrologicalPoint))
152+
149153
# Declarative mapping of geometrically opposite point pairs.
150154
# Each derived point is computed as primary.abs_pos + 180 (mod 360).
151155
# negate_speed/negate_dec/negate_lat control whether speed, declination and
@@ -191,10 +195,23 @@
191195
bid for bid in (
192196
getattr(ephe, "MEAN_NODE", None), getattr(ephe, "TRUE_NODE", None),
193197
getattr(ephe, "MEAN_APOG", None), getattr(ephe, "OSCU_APOG", None),
194-
getattr(ephe, "INTP_APOG", None),
198+
getattr(ephe, "INTP_APOG", None), getattr(ephe, "INTP_PERG", None),
195199
) if bid is not None
196200
)
197201

202+
# Point names dropped by the exclusion above (plus their derived opposites,
203+
# which cannot exist without their primaries). Consumers that diagnose
204+
# missing points (e.g. the transit factory's misconfiguration warning) use
205+
# this to recognize by-design absences in non-geocentric frames.
206+
_GEOCENTRIC_ONLY_POINT_NAMES = frozenset(
207+
{
208+
"Mean_North_Lunar_Node", "True_North_Lunar_Node",
209+
"Mean_South_Lunar_Node", "True_South_Lunar_Node",
210+
"Mean_Lilith", "True_Lilith", "Interpolated_Lilith",
211+
"Mean_Priapus", "True_Priapus", "Interpolated_Perigee",
212+
}
213+
)
214+
198215

199216
def _degenerate_center_body_id(perspective_type: Optional[str]) -> Optional[int]:
200217
"""Body id that IS the origin of ``perspective_type``.
@@ -824,6 +841,9 @@ def from_birth_data(
824841
- If invalid zodiac/sidereal mode combinations are specified
825842
- If GeoNames data is missing or invalid
826843
- If timezone localization fails (ambiguous DST times)
844+
- If active_points contains unknown point names, or is an
845+
empty list (pass None to use the defaults). Fixed star
846+
names are redirected to active_fixed_stars with a warning.
827847
828848
Examples:
829849
>>> # Basic natal chart with online location lookup
@@ -885,6 +905,16 @@ def from_birth_data(
885905
active_points_list: List[AstrologicalPoint] = list(DEFAULT_ACTIVE_POINTS)
886906
else:
887907
active_points_list = list(active_points)
908+
if not active_points_list:
909+
# An empty list means 'no filter' downstream (_should_calculate
910+
# treats it as falsy), which would silently invert the caller's
911+
# explicit "nothing" into a FULL chart — the exact inversion the
912+
# emptied-list branches below fail loudly for. Reject it up
913+
# front; None is the documented way to request the defaults.
914+
raise KerykeionException(
915+
"active_points is an empty list. Pass None (or omit it) to use "
916+
"DEFAULT_ACTIVE_POINTS, or list at least one point to calculate."
917+
)
888918
# v6: ``active_points`` is no longer a channel for fixed stars.
889919
# Star names that v5 accepted here (e.g. "Regulus", "Spica") are
890920
# redirected to the ``active_fixed_stars`` channel — with a
@@ -928,6 +958,19 @@ def from_birth_data(
928958
_merged_stars.append(_star)
929959
active_fixed_stars = _merged_stars
930960

961+
# Anything left must be a real AstrologicalPoint: an unknown name
962+
# (e.g. a typo like "Sunn") would otherwise never be iterated by
963+
# the calculation loop and simply vanish from the chart — a silent
964+
# wrong result rather than an error.
965+
_unknown_points = [p for p in active_points_list if p not in _VALID_ACTIVE_POINT_NAMES]
966+
if _unknown_points:
967+
raise KerykeionException(
968+
f"Unknown active_points {_unknown_points}: not valid astrological "
969+
"points (and not fixed star names, which are redirected to "
970+
"active_fixed_stars). Check the AstrologicalPoint literal in "
971+
"kerykeion.schemas.kr_literals for valid names."
972+
)
973+
931974
# The center body of the perspective has no position as seen from itself
932975
# (Earth in geocentric/topocentric, Sun in heliocentric, the center
933976
# planet in a planetocentric chart), so drop it from the active points
@@ -957,6 +1000,30 @@ def from_birth_data(
9571000
"one other point or omit active_points."
9581001
)
9591002

1003+
# Geocentric-only points (lunar nodes, Lilith/apogee variants) have no
1004+
# meaning in non-geocentric frames; the calculation loop drops them
1005+
# (see _GEOCENTRIC_ONLY_BODY_IDS). Mirror the center-body pass: give
1006+
# the user-facing warning here instead of a silent disappearance, and
1007+
# reject a list that would empty out (the 'no filter' inversion).
1008+
if perspective_type not in _GEO_TOPO_PERSPECTIVES:
1009+
_geo_only_dropped = [p for p in active_points_list if p in _GEOCENTRIC_ONLY_POINT_NAMES]
1010+
if _geo_only_dropped:
1011+
logging.warning(
1012+
"Excluding %s from active_points: geocentric-only points "
1013+
"(lunar nodes, Lilith/apogee variants) have no meaning in "
1014+
"the %r perspective.",
1015+
_geo_only_dropped,
1016+
perspective_type,
1017+
)
1018+
active_points_list = [p for p in active_points_list if p not in _GEOCENTRIC_ONLY_POINT_NAMES]
1019+
if not active_points_list:
1020+
raise KerykeionException(
1021+
f"active_points contained only {_geo_only_dropped}, which are "
1022+
f"geocentric-only points with no meaning in the "
1023+
f"{perspective_type!r} perspective. Include at least one "
1024+
"other point or omit active_points."
1025+
)
1026+
9601027
calc_data["active_points"] = active_points_list
9611028

9621029
# Initialize configuration

kerykeion/chart_data_factory.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"""
3131

3232
import logging
33-
from typing import Mapping, Union, Optional, Literal, cast
33+
from typing import Mapping, Union, Optional, Literal, cast, get_args
3434

3535
from kerykeion.aspects import AspectsFactory
3636
from kerykeion.house_comparison.house_comparison_factory import HouseComparisonFactory
@@ -153,8 +153,21 @@ def create_chart_data(
153153
ChartDataModel: Comprehensive chart data model
154154
155155
Raises:
156-
KerykeionException: If chart type requirements are not met
156+
KerykeionException: If ``chart_type`` is not a valid ``ChartType``,
157+
or if chart type requirements are not met (e.g. a missing
158+
second subject for dual charts)
157159
"""
160+
# An unknown chart_type must fail here with the valid options; letting
161+
# it fall through produces misleading errors ("Second subject is
162+
# required for NatalFoo charts") or a late pydantic ValidationError
163+
# after the full dual pipeline has already run.
164+
_valid_chart_types = get_args(ChartType)
165+
if chart_type not in _valid_chart_types:
166+
raise KerykeionException(
167+
f"Unknown chart_type {chart_type!r}. Valid chart types: "
168+
f"{', '.join(_valid_chart_types)}."
169+
)
170+
158171
# Resolve per-chart-type defaults when the caller did not specify them.
159172
is_natal_family = chart_type in ChartDataFactory._NATAL_FAMILY_CHART_TYPES
160173
if active_aspects is None:

kerykeion/charts/chart_drawer.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1762,9 +1762,11 @@ class ChartDrawer: # type: ignore[no-redef]
17621762
chart_language (KerykeionChartLanguage, optional):
17631763
Language code for chart labels. Defaults to 'EN'.
17641764
language_pack (dict | None, optional):
1765-
Additional translations merged over the bundled defaults for the
1766-
selected language. Useful to introduce new languages or override
1767-
existing labels.
1765+
Additional translations. For one of the bundled languages the pack
1766+
is merged over that language's defaults (partial packs override
1767+
individual labels). For a NEW language code the pack itself is the
1768+
language: it must be complete (clone the EN block and edit), or
1769+
model validation fails listing the missing fields.
17681770
external_view (bool, optional):
17691771
For Natal charts only: place planets outside the zodiac ring.
17701772
Defaults to False.
@@ -2018,9 +2020,12 @@ def __init__(
20182020
chart_language (KerykeionChartLanguage, optional):
20192021
Language code for chart labels (e.g., 'EN', 'IT'). Defaults to 'EN'.
20202022
language_pack (dict | None, optional):
2021-
Additional translations merged over the bundled defaults for the
2022-
selected language. Useful to introduce new languages or override
2023-
existing labels.
2023+
Additional translations. For one of the bundled languages the
2024+
pack is merged over that language's defaults (partial packs
2025+
override individual labels). For a NEW language code the pack
2026+
itself is the language: it must be complete (clone the EN
2027+
block and edit), or model validation fails listing the
2028+
missing fields.
20242029
external_view (bool, optional):
20252030
Whether to use external visualization (planets on outer ring) for
20262031
single-subject charts. Only applies to Natal charts. Defaults to False.
@@ -2061,10 +2066,34 @@ def __init__(
20612066
show_zodiac_background_ring (bool, optional):
20622067
Default for whether to draw colored zodiac wedges (modern style only).
20632068
Can be overridden at render time. Defaults to True.
2069+
2070+
Raises:
2071+
KerykeionException: If ``theme`` is not a valid KerykeionChartTheme
2072+
(and not None), if ``chart_language`` is not a valid
2073+
KerykeionChartLanguage (unless a ``language_pack`` supplies the
2074+
custom language), or if ``double_chart_aspect_grid_type`` is
2075+
not 'list' or 'table'.
20642076
"""
20652077
# =====================================================================
20662078
# STEP 1: Store basic configuration parameters
20672079
# =====================================================================
2080+
# Validate the open string parameters up front, mirroring the theme
2081+
# contract below: an unknown language would otherwise silently fall
2082+
# back to EN and an unknown grid type would silently render as
2083+
# "table" — plausible-looking output hiding the caller's mistake.
2084+
# A language_pack legitimizes ANY code: it is the documented way to
2085+
# introduce new languages (e.g. chart_language="JP" + a JP pack).
2086+
if chart_language not in get_args(KerykeionChartLanguage) and language_pack is None:
2087+
raise KerykeionException(
2088+
f"chart_language {chart_language!r} is not available. "
2089+
f"Valid languages: {', '.join(get_args(KerykeionChartLanguage))} — "
2090+
"or supply a language_pack to introduce a custom language."
2091+
)
2092+
if double_chart_aspect_grid_type not in ("list", "table"):
2093+
raise KerykeionException(
2094+
f"double_chart_aspect_grid_type {double_chart_aspect_grid_type!r} "
2095+
"is not valid. Use 'list' or 'table'."
2096+
)
20682097
# These are direct assignments of constructor parameters to instance
20692098
# attributes. They form the foundation for all subsequent setup.
20702099
self._store_basic_configuration(

0 commit comments

Comments
 (0)