Skip to content

feat(cli): optional kerykeion[cli] β€” the whole library from the terminal (6.0.0a85) - #249

Open
g-battaglia wants to merge 11 commits into
alpha/v6from
feat/cli
Open

feat(cli): optional kerykeion[cli] β€” the whole library from the terminal (6.0.0a85)#249
g-battaglia wants to merge 11 commits into
alpha/v6from
feat/cli

Conversation

@g-battaglia

Copy link
Copy Markdown
Owner

What

Adds an optional command-line interface, kerykeion[cli], that exposes the whole library from the terminal β€” every chart type, analytical technique, sky event, factory and time series β€” without writing any Python.

No calculation changed, and no public name moved. kerykeion.__all__ is exactly what a84 exported, every Pydantic schema is byte-identical, and import kerykeion is unchanged. The CLI is an extra: pip install kerykeion without it behaves exactly as before.

Release: 6.0.0a85.

What you can do from the terminal

pip3 install "kerykeion[cli]"
  • Charts: natal, synastry, transit, composite, return, progression, now
  • Techniques: technique (profections, firdaria, zodiacal releasing, receptions, horary, midpoints, directions, acg, stars, relocate)
  • Sky events: sky (eclipses, lunations, ingresses, stations, mundane, phenomena, nodes, sun-times, hours, voc)
  • Time series: ephemeris, transits (with a pre-flight sampling guard β†’ exit 8)
  • Subject profiles: subject save|show|list|path|verify (perms 0600 β€” birth data is PII), reused via -s <name>
  • call: a guarded dispatcher reaching any public Factory.method in kerykeion.__all__
kerykeion subject save ada --name "Ada Lovelace" --date 1815-12-10 --time 18:00 \
      --lat 51.5074 --lng -0.1278 --tz Europe/London --offline
kerykeion natal -s ada                        # TTY  β†’ ASCII report
kerykeion natal -s ada | jq -r .sun.sign      # pipe β†’ JSON, no ANSI
kerykeion natal -s ada -f svg -o /tmp/ada.svg
kerykeion call DominantsFactory.from_subject -s ada
kerykeion call os.system --param cmd=ls       # refused β€” os βˆ‰ __all__

Design invariants

  • import kerykeion stays free of typer. The console script is static metadata, so the command is installed even without the extra: it then prints an install hint and exits 3 β€” never a traceback. Enforced in cli/__init__.py (lazy import), kerykeion/__init__.py (untouched β€” never imports kerykeion.cli), the walk_packages exclusion in test_public_api_surface.py, and a dedicated subprocess test.
  • Output discipline. Payloads go to stdout via sys.stdout/Path.write_text, never rich.Console.print. Format is chosen for you: text on a TTY, JSON in a pipe (-f text|json|xml|svg forces). Warnings always go to stderr, even under -f json, so a piped payload stays clean.
  • Clean errors, never a traceback. Every error is one line on stderr with a classified exit: 0 ok Β· 1 unexpected Β· 2 usage Β· 3 extra-missing Β· 4 invalid-input Β· 5 KerykeionException Β· 6 ephemeris Β· 7 network Β· 8 sampling-limit Β· 9 warnings-as-errors Β· 130 interrupted.
  • Security is an allowlist. call resolves only names in kerykeion.__all__, split on a single ., no private members, no activated descriptors. kerykeion call os.system is refused by an explicit test.
  • Backend-agnostic. The CLI uses the same in-process backend as the library (libephemeris default, Swiss Ephemeris optional). Verified on both: test_cli.py is green under KERYKEION_BACKEND=libephemeris and =swisseph.

Tests

tests/core/test_cli.py (25 cases, cli marker) asserts identity, not snapshots: a command's payload == json.loads(model.model_dump_json()) built from the same inputs. Identity is immune to ephemeris drift β€” if the Sun moves, both sides move together. There is deliberately no regenerate:cli task. The suite avoids the all_points token (silently skipped on swisseph by conftest.py). Lives under tests/core/ so poe check runs it; mostly in-process via typer.testing.CliRunner, subprocess only where CliRunner can't prove the point (entry point, python -m kerykeion, real TTY).

Dependencies & licensing

The extra adds typer and rich β€” both MIT. No Apache-2.0 dependency was added (the repo's standing rule). NOTICE carries a new "Optional command-line interface" block (typer, rich, shellingham ISC, annotated-doc, markdown-it-py, mdurl, pygments BSD-2, colorama BSD-3; Click is vendored by Typer under its MIT).

typer>=0.26.4 vendors Click β€” important because click 8.3.1 is already in the graph as a transitive of libephemeris; vendoring eliminates a silent correctness dependence on whichever click another dependency resolves.

typer/rich are declared in both the [cli] extra and the [dev] group β€” intentional, not redundancy: pyrightconfig.json runs in basic mode where reportMissingImports is an error across the whole kerykeion/ tree, so without them in dev every contributor's poe typecheck fails on kerykeion/cli/. Documented in DEVELOPMENT.md so it doesn't get "cleaned up".

The local-gates-only policy is unchanged: no CI added, .github/workflows/ verified empty. New local tasks poe test:cli and poe cli:smoke; poe build:smoke grew a third isolated environment that verifies the no-extra degradation.

Gate results

  • poe quality β€” 5/5 (lint, analyze, typecheck, test, cli)
  • poe test:extended β€” 10929 passed, 1390 skipped
  • poe test:lib β€” 5665 passed (libephemeris backend)
  • tests/core/test_cli.py β€” 25 passed under both KERYKEION_BACKEND=libephemeris and =swisseph
  • poe build:smoke β€” 3 environments green; --version (6.0.0a85), wheel-only degrades to exit 3 with the hint, wheel+[cli] functional

Scope notes

  • Local, not a client of AstrologerAPI (per the plan): the CLI is a local engine with the backend already abstracted internally (engine=local), so a future --remote is an addition, not a refactor. requests is already a runtime dependency, so an HTTP client costs zero new deps. A future importable Python SDK for the API would be MIT and a separate package β€” out of scope here.
  • The 1.48s cost of import kerykeion (eager __init__.py) is not addressed here; making it lazy via PEP 562 touches every user of the library and is tracked as a separate issue.

Commits

10 phases, one commit each (plus the release): metadata β†’ lazy-import guard & entry points β†’ smoke gates β†’ subject & rendering β†’ error boundary & natal β†’ chart family & SVG β†’ techniques/sky/series β†’ call dispatcher β†’ tests & docs β†’ release 6.0.0a85.

πŸ€– Generated with Claude Code

g-battaglia and others added 11 commits August 12, 2026 02:20
``_resolve_model`` extracted the kind/chart_type tuple with ``next()`` over a
generator expression. Pyright (1.1.408, the declared floor) widens the unpacked
literal to ``str`` inside a genexpr, so the assignment to ``self._model_kind``
failed type-checking β€” a pre-existing regression the no-CI policy let slip.

A plain for-loop with unpacking keeps the ``LiteralReportKind`` narrowing.
Verified: ``0 errors, 0 warnings, 0 informations``.

Co-Authored-By: Claude <noreply@anthropic.com>
Phase 1 of the staged CLI build-out β€” metadata only, no command logic yet.

* pyproject: new ``[cli]`` extra (typer>=0.26.4, rich>=13.8.0); ``[all]`` now
  includes it (an "all" that omits an extra lies in its name); a
  ``[project.scripts] kerykeion`` entry point installed for every user, even a
  bare ``pip install kerykeion``; a ``cli`` pytest marker; typer+rich also in
  the dev group because pyright's reportMissingImports is an ERROR in basic
  mode and the include covers the whole ``kerykeion`` tree.
* kerykeion/cli/__init__.py: ``main()`` is the entry point. It imports the Typer
  app lazily and degrades to an install hint (exit 3) when [cli] is absent, so
  the bare command never crashes with a traceback. The module imports NO typer
  and NO kerykeion symbol at module level.
* kerykeion/cli/app.py: placeholder ``run()`` so the command is exercisable
  end-to-end before any command is wired.

typer 0.27.1 vendorizes click (it added no click to the graph β€” click 8.3 was
already present as a transitive of libephemeris), which is the floor's reason.

Verified: ``uv run kerykeion`` exits 0; ``import kerykeion`` and
``import kerykeion.cli`` keep typer out of sys.modules; pyright/ruff/mypy clean;
fresh-imports OK (132 checked); public-API surface walker green.

Co-Authored-By: Claude <noreply@anthropic.com>
Phase 2 of the CLI build-out.

* kerykeion/__main__.py and kerykeion/cli/__main__.py enable
  ``python -m kerykeion`` and ``python -m kerykeion.cli``. A plain
  ``import kerykeion`` still never runs them, so typer stays out of the
  library import path; the cold-import gate already skips every ``__main__``.
* tests/core/test_public_api_surface.py: the ``pkgutil.walk_packages`` walk now
  skips the whole ``kerykeion.cli`` subtree, whose leaves import the optional
  [cli] extra. Exclusion is exact / dot-prefixed-subtree / dot-suffixed-leaf,
  never a bare substring β€” ``"kerykeion.cli" in "kerykeion.client"`` is True,
  so a substring test would swallow a future client package too.

Verified: both -m forms exit 0; walker green (23 passed); the exclusion
predicate keeps ``kerykeion.client`` importable while skipping ``kerykeion.cli``
and its leaves.

Co-Authored-By: Claude <noreply@anthropic.com>
Phase 3 of the CLI build-out β€” the gate before any domain command exists.

* kerykeion/cli/app.py: a real (minimal) Typer app with --version/-V and --help.
  Imports typer at module level ON PURPOSE: that import is what lets
  kerykeion.cli.main detect a missing [cli] extra and print the install hint.
  No kerykeion symbol at module level (cold-import gate stays green). A plain
  ``kerykeion`` shows help and exits 0 (not click's exit 2): handled in the
  callback rather than via no_args_is_help.
* scripts/cli_smoke_check.py: from the dev checkout, proves ``import kerykeion``
  stays typer-free, the console_scripts entry point is wired, and --version /
  --help / bare all behave. Run by ``poe cli:smoke``.
* scripts/build_smoke_check_cli.py: against the BUILT WHEEL in a clean room,
  proves the two install paths β€” wheel alone degrades to exit 3 + install hint
  with no traceback; wheel + [cli] runs --version/--help. The only place the
  install-hint guard is exercised for real.
* pyproject: new ``test:cli`` and ``cli:smoke`` tasks; ``check`` now ends with
  cli:smoke; ``build:smoke`` runs both isolated environments (wheel-only, and
  wheel+[cli]) so the degration path ships verified.
* scripts/quality_check.py: adds the cli smoke step.

Verified: poe check green (5670 passed + cli:smoke); poe build:smoke green
across both isolated envs; mypy/pyright/ruff clean on cli/.

Co-Authored-By: Claude <noreply@anthropic.com>
…` command

Fase 4 del build-out kerykeion[cli]. Aggiunge il nucleo che ogni comando chart
riusera:

- subject_resolver: merge profilo + flag inline; parsing date/time via regex
  (non date.fromisoformat, che rifiuta anno < 1 e quindi le date BCE); rename
  degli alias (--houses placidus -> P, --points all -> costante, --fixed-stars
  royal -> lista); --with/--without -> calculate_*; --set key=value whitelisted
  contro la firma di from_birth_data (rifiuta le chiavi _private); default
  online inferito da lat+lng+tz; dispatch from_birth_data/from_iso_utc_time.
  Separato in merge_inputs (ricetta ProfileInput) + materialize (chiamata
  factory), cosi' save persiste la ricetta e natal/verify la materializzano.
- profiles: store JSON in $XDG_CONFIG_HOME/kerykeion/subjects/, file 0600
  (birth data = PII) via os.open+fdopen; ProfileInput extra=forbid;
  resolve_path (file path -> nome nello store -> difflib suggestions).
- rendering: render()/emit()/write_output() funnel unico (payload sempre su
  stdout via sys.stdout.write, mai rich.Console.print); dispatcher text/json/
  xml/svg con import lazy (svg_out e' stub); text via probe ReportGenerator +
  fallback generico (list[str] un-per-riga, list[BaseModel] a blocchi, JSON);
  formats.resolve (explicit -f -> suffisso -o -> $KERYKEION_CLI_FORMAT -> TTY).
- commands/subject: save/show/list/path/verify, registrato in app.py.
- profiles.make_meta: rimosso refuso `backend = None` inutilizzato (F841).
- rendering/text: consolidato il probe ReportGenerator in _try_report con un
  solo type: ignore[arg-type] documentato (mypy vede l'Union ristretta; il
  probe passa un BaseModel qualunque di proposito).

verify materializza via effemeridi: Ada Lovelace 1815-12-10 18:00 London ->
Sole in Sagittario, Luna Ariete, Asc Cancro.

Gate: lint/analyze/typecheck verdi; imports:fresh OK (147); test:core 5670
passed / 144 skipped; cli:smoke OK; check end-to-end save->show->list->path->
verify con permessi 0600 e JSON valido in pipe.

Co-Authored-By: Claude <noreply@anthropic.com>
Fase 5 del build-out kerykeion[cli].

- errors: ExitCode (0 ok, 1 unexpected, 3 extra mancante, 4 input, 5
  KerykeionException, 6 ephemeris, 7 network, 8 sampling, 9 warnings-as-errors,
  130 interrupted); classify() ordina i tipi backend PRIMA di ValueError
  (lo EphemerisRangeError di Skyfield e` ValueError subclass); _backend_error_types
  best-effort (cattura Skyfield; TODO BACKEND_ERROR_TYPES in MANDATORY_EVOLUTIONS
  Β§2 per i tipi nativi libephemeris/swisseph); error_boundary decorator +
  handle_uncaught (messaggio pulito di default, traceback con --traceback o per
  UNEXPECTED). FileNotFoundError -> exit 4 (copre ProfileNotFound).
- typer_app: KerykeionTyper(typer.Typer) il cui command() wrappa ogni comando
  (top-level e sottocomandi delle subapp) con error_boundary. Separato da app.py
  perche' app esegue _register_commands come side-effect modulo-livello: se un
  comando importasse KerykeionTyper da app, l'ordine di cold-import del gate
  import-graph genererebbe un circular import partially-initialized (subject_app
  letto prima della definizione).
- warnings: collector ricorsivo di ephemeris_warnings + polar_house_fallbacks
  che attraversa .subject / .first_subject / .second_subject (chart data model
  non li hanno diretti) e ricorre nei CompositeSubjectModel annidati, con cycle
  protection per id e dedup per contenuto. output_with_warnings emette il payload
  su stdout, i warning su stderr (sempre, anche con --format json, cosi' jq resta
  pulito) ed esce 9 con --warnings-as-errors (dopo aver emesso il payload).
- commands/charts: natal (resolve + output_with_warnings, 4 formati).
- app: opzioni globali --traceback / --warnings-as-errors sul root callback;
  registrazione di natal; run() avvolge app() con handle_uncaught come backstop.
- subject_resolver: build_flags() spostato qui (condiviso da subject save e
  natal); options: SubjectProfile (-s).
- svg_out: lo stub ora solleva KerykeionException (exit 5, clean) invece di
  NotImplementedError (exit 1 + traceback); sara` cablato con SVG reale in Fase 6.

Verifiche: natal text/json/xml exit 0 (XML via to_context funziona sul subject);
svg exit 5 clean; date malformate e profilo inesistente -> exit 4 clean senza
traceback; --traceback mostra lo stack. Ada Lovelace 1815-12-10 -> Sole Sag.

Gate: lint/analyze/typecheck verdi; imports:fresh OK (151); test:core 5670
passed / 144 skipped; cli:smoke OK.

Co-Authored-By: Claude <noreply@anthropic.com>
Adds the rest of the chart commands on top of `natal`:

- `now` β€” a subject for the current moment (from_current_time), with the
  same inline flag set as `natal`.
- `synastry` / `composite` β€” dual/midpoint charts of two stored profiles
  (`-s` + `-S`).
- `transit` β€” natal vs a transit moment (now, or `--to-date`/`--to-time`).
- `return` β€” Solar/Lunar return dual wheel; the return-chart location
  defaults to the natal birthplace (offline) and can be relocated with
  `--lat/--lng/--tz` or `--city --online`. PlanetaryReturnFactory does its
  own geocoding, unlike the other factories, so it needs the location
  passed explicitly.
- `progression` β€” secondary progression dual wheel for `--target-year`.

SVG is now real: `svg_out.render_svg` goes through
`ChartDrawer.generate_svg_string()` (never `save_svg()`, which writes into
`Path.home()` and prints to stdout). `natal` emits the subject for
text/json/xml and builds a natal chart-data wrapper only for the svg path.

subject_resolver gains a `mode_override` ("current") so `now` and transit
without `--to-date` dispatch to `from_current_time`.

Co-Authored-By: Claude <noreply@anthropic.com>
Adds the three remaining command groups on top of the chart family.

`technique <sub>` (subject-based, -s <profile>):
  profections, firdaria, zr (zodiacal releasing), receptions, horary,
  midpoints, directions (primary), acg (astro-cartography), stars
  (heliacal), nodes (planetary), relocate.

`sky <sub>` (date/location-based, inline --lat/--lng/--tz or -s):
  sun-times, hours (planetary), voc (instant or --to range), eclipses
  (located or global), lunations, ingresses, stations.

`ephemeris` / `transits` (top-level time series): ephemeris is a planet
  position series; transits runs it against a natal chart as per-sample
  aspects (--events collapses to discrete applying→exact→separating).

Sampling guard: EphemerisDataFactory materialises one subject per sample,
  so a long range at a fine step is an OOM risk. The library's own
  max_days/max_hours/max_minutes defaults (730/8760/525600) raise a bare
  ValueError AFTER starting and map to the wrong exit. We read the ceilings
  off the factory signature, count samples BEFORE constructing anything,
  and raise SamplingLimitError (exit 8) if too large. --no-limit skips the
  pre-check and passes max_*=None through.

errors.py gains SamplingLimitError (a ValueError subclass, so a missed
  classify branch still falls through sanely) mapped to exit 8.

Co-Authored-By: Claude <noreply@anthropic.com>
`kerykeion call Factory.method` reaches any factory method (or bare
function) in kerykeion.__all__ without writing Python:

    kerykeion call ProfectionsFactory.from_subject -s ada
    kerykeion call DominantsFactory.from_subject -s ada --strategy modern
    kerykeion call --list | --explain Factory.method
    kerykeion call PlanetaryReturnFactory.next_return_from_date -s ada \
        --param year=2000 --param return_type=Solar

Three modules:

* registry.py β€” the security layer. Only __all__ names, one split on '.',
  no private members, no models/exceptions/protocols, getattr_static so no
  descriptor fires. `call os.system` fails because 'os' is not in __all__ β€”
  that is the test this command exists to pass.
* introspect.py β€” type coercion (bool/int/float/str/Literal/list/tuple/
  datetime, multi-type Unions) and parameter classification (cli | subject
  | json-only | unsupported) for --explain. Annotations are resolved with
  get_type_hints so the factories' string forward-refs match.
* commands/call.py β€” --list/--explain and the invocation. Subject params
  bind from -s/-S; everything else from --param key=value. classmethod/
  static go through the owner so Python binds cls; instance factories
  (PlanetaryReturnFactory, HeliacalFactory, ...) split the namespace into
  __init__ vs method kwargs.

errors.py: TypeError now maps to exit 4 (a missing/wrong argument to a
  dispatched factory is bad input, not a crash) alongside ValueError.

Co-Authored-By: Claude <noreply@anthropic.com>
Phase 9 of the kerykeion[cli] feature.

tests/core/test_cli.py β€” 25 cases, marker cli, identity (not snapshot): the CLI payload is asserted to equal the library model built from the same inputs (json.loads(stdout) == json.loads(model.model_dump_json())), so the suite is immune to ephemeris drift and there is no regenerate:cli task. Covers version/help/exit codes, clean error boundaries (no traceback), stdout-vs-stderr discipline, ANSI-free pipes, the 4 formats, the sampling limit (exit 8) and its --no-limit bypass, the call dispatcher security guarantee (os.system refused), entry-point registration, import-kerykeion stays typer-free, and the TTY-detection seam. Runs fully offline on base-tier dates; avoids the all_points token (skipped on Swiss Ephemeris).

Documentation: README section + ToC entry, new site/docs/cli.md (order 64) linked from the index, DEVELOPMENT (cli/ in the tree, test:cli/cli:smoke tasks, why typer/rich live in both the extra and the dev group), TEST principle #7 (identity not snapshots), llms.txt CLI section, CHANGELOG [Unreleased], and a NOTICE block for the optional CLI dependencies.

docs:snippets 400/400, docs:check 100%, ruff clean, cli:smoke OK.

Co-Authored-By: Claude <noreply@anthropic.com>
Optional command-line interface (kerykeion[cli]): charts, techniques, sky
events, time series, subject profiles and a guarded call dispatcher over
__all__. Local engine, backend-agnostic; text on a TTY, JSON in a pipe;
classified exit codes 0-9/130; import kerykeion stays free of typer.

No calculation changed and no public name moved: __all__ is exactly a84,
every Pydantic schema byte-identical. Adds typer+rich (MIT); no Apache-2.0
dependency added (NOTICE carries the new CLI license block).

Co-Authored-By: Claude <noreply@anthropic.com>
@cla-bot cla-bot Bot added the cla-signed label Aug 12, 2026
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

βš™οΈ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ed521396-c56b-4a88-b418-d3c49a114970

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • πŸ” Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant