Source code for stellium.core.protocols

"""
Protocol definitions for Stellium components.

Protocols define INTERFACES - what methods a component must implement.
They don't provide implementation - that's in the engine classes.

Think of these as contracts: "If you want to be an EphemerisEngine,
you must implement these methods with these signatures."
"""

from typing import TYPE_CHECKING, Any, Protocol

from stellium.core.models import (
    Aspect,
    CalculatedChart,
    CelestialPosition,
    ChartDateTime,
    ChartLocation,
    HouseCusps,
)

if TYPE_CHECKING:
    from stellium.core.config import CalculationConfig
    from stellium.visualization.builder import ChartDrawBuilder


# Type alias for any chart-like object
ChartType = "CalculatedChart | Comparison | MultiChart"


[docs] class EphemerisEngine(Protocol): """ Protocol for planetary position calculation engines. Different implementations might use: - Swiss Ephemeris - JPL Ephemeris - Custom calculation algorithms - Mock data for testing """
[docs] def calculate_positions( self, datetime: ChartDateTime, location: ChartLocation, objects: list[str] | None = None, config: "CalculationConfig | None" = None, ) -> list[CelestialPosition]: """ Calculate positions for celestial objects. Args: datetime: When to calculate positions location: Where to calculate from (for topocentric) objects: Which objects to calculate (None = all standard objects) config: Optional calculation configuration (zodiac type, etc.) Returns: List of CelestialPosition objects """ ...
[docs] class HouseSystemEngine(Protocol): """ Protocol for house system calculation engines. Different implementations for different house systems: - Whole Sign - Placidus - Koch - Equal House - etc """ @property def system_name(self) -> str: """Name of this house system (e.g. Placidus)""" ...
[docs] def calculate_house_data( self, datetime: ChartDateTime, location: ChartLocation, ) -> tuple[HouseCusps, list[CelestialPosition]]: """ Calculate house cusps for this system. Args: datetime: Chart datetime location: Chart location Returns: Tuple containing: 1. HouseCusps object with 12 cusp positions (For this specific system) 2. A List of CelestialPosition objects for the primary angles (ASC, MC, DSC, IC, Vertex) """ ...
[docs] def assign_houses( self, positions: list[CelestialPosition], cusps: HouseCusps ) -> dict[str, int]: """ Assign house numbers to celestial positions. Args: positions: Celestial objects to assign houses cusps: House cusps to use for assignment Returns: A dictionary of {object_name: house_number} """ ...
[docs] class OrbEngine(Protocol): """ Protocol for orb calculation. Encapsulates logic for determining orb allowance, which can be simple (by aspect) or complex (by planet, by planet pair, by day/night, etc.). """
[docs] def get_orb_allowance( self, obj1: CelestialPosition, obj2: CelestialPosition, aspect_name: str ) -> float: """ Get the allowed orb for a specific aspect between two objects. Args: obj1: The first celestial object obj2: The second celestial object aspect_name: The name of the aspect (e.g. Square) Returns: The maximum allowed orb in degrees """ ...
[docs] class CrossChartAspectEngine(Protocol): """ Protocol for calculating aspects between two charts. This is separate from AspectEngine to allow different orb configurations and aspect sets for cross-chart work. Use cases: - Synastry: Person A's planets aspecting Person B's planets - Transits: Current sky aspecting natal chart - Progressions: Progressed chart aspecting natal chart """
[docs] def calculate_cross_aspects( self, chart1_positions: list[CelestialPosition], chart2_positions: list[CelestialPosition], orb_engine: OrbEngine, ) -> list[Aspect]: """ Calculate aspects between two sets of positions. Only calculates aspects where one object is from chart1 and the other is from chart2. Internal aspects within each chart are not calculated. Args: chart1_positions: Positions from first chart (e.g., natal/inner) chart2_positions: Positions from second chart (e.g., transit/outer) orb_engine: The OrbEngine that will provide orb allowances Returns: List of Aspect objects representing cross-chart aspects """ ...
[docs] class AspectEngine(Protocol): """ Protocol for aspect calculation engines. Different implementations might use: - Traditional aspects (Ptolemaic) - Modern aspects (including minor aspects) - Harmonic aspects - Vedic aspects (completely different system) """
[docs] def calculate_aspects( self, positions: list[CelestialPosition], orb_engine: OrbEngine, ) -> list[Aspect]: """ Calculate aspects between celestial objects. Args: positions: Objects to find aspects between orb_engine: Optional custom orb settings Returns: List of Aspect objects """ ...
[docs] class DignityCalculator(Protocol): """ Protocol for dignity/debility calculation. Different implementations: - Traditional essential dignities - Modern rulerships - Vedic dignity system """
[docs] def calculate_dignities(self, position: CelestialPosition) -> dict[str, Any]: """ Calculate dignities for a celestial position. Args: position: Position to calculate dignities for Returns: Dictionary with dignity information """ ...
[docs] class ChartComponent(Protocol): """ Base protocol for chart calculation components. Components can be: - Arabic part calculators - Midpoint finders - Pattern detectors (grand trine, T-square, etc.) - Fixed star calculators - Harmonic charts """ metadata_name = "" @property def component_name(self) -> str: """Name of this component.""" ...
[docs] def calculate( self, datetime: ChartDateTime, location: ChartLocation, positions: list[CelestialPosition], house_systems_map: dict[str, HouseCusps], house_placements_map: dict[str, dict[str, int]], ) -> list[CelestialPosition]: """ Calculate additional chart objects. Args: datetime: Chart datetime location: Chart location positions: Already calculated positions house_systems_map: House cusps by system house_placements_map: House placements by system then planet Returns: List of additional CelestialPosition objects """ ...
[docs] class ChartLike(Protocol): """ Protocol for chart-like objects (single or multi-chart). This protocol defines the common interface that all chart types should support, enabling code to work with CalculatedChart, MultiChart, or Comparison objects interchangeably where appropriate. Note: The `chart` parameter in methods like `get_object()` defaults to 0, which works for single charts (ignored) and multi-charts (returns from first chart). """ @property def datetime(self) -> ChartDateTime: """The primary datetime of the chart (or first chart for multi-charts).""" ... @property def location(self) -> ChartLocation: """The primary location of the chart (or first chart for multi-charts).""" ... @property def metadata(self) -> dict[str, Any]: """Chart metadata dictionary.""" ...
[docs] def get_object(self, name: str, chart: int = 0) -> CelestialPosition | None: """ Get a celestial object by name. Args: name: Name of the object (e.g., "Sun", "Moon") chart: For multi-charts, which chart to query (0-indexed). Ignored for single charts. Returns: The CelestialPosition if found, None otherwise """ ...
[docs] def get_planets(self, chart: int = 0) -> list[CelestialPosition]: """ Get all planetary positions. Args: chart: For multi-charts, which chart to query (0-indexed). Ignored for single charts. Returns: List of planet CelestialPosition objects """ ...
[docs] def draw(self, filename: str = "chart.svg") -> "ChartDrawBuilder": """ Create a visualization builder for this chart. Args: filename: Default filename for saving Returns: ChartDrawBuilder configured for this chart """ ...
[docs] class ReportSection(Protocol): """ Protocol for report sections. Each section knows how to extract data from a chart (single or multi-chart) and format it into a standardized structure that renderers can consume. **Multi-Chart Support:** Sections may receive any of: - CalculatedChart: Single natal/event chart - Comparison: Two-chart comparison (deprecated, use MultiChart) - MultiChart: 2-4 charts for synastry, transits, progressions, etc. Implementations should use `stellium.core.chart_utils` helpers to handle different chart types consistently: - `get_all_charts(chart)` - Get list of all charts - `get_chart_labels(chart)` - Get labels for each chart - `chart_count(chart)` - Get number of charts Why a protocol? - Extensibility: Users can create custom sections - Type safety: MyPy/Pyright can verify implementations - No inheritance required: Keep components lightweight """ @property def section_name(self) -> str: """ Human-readable name for this section. Used as a header in rendered output. """ ...
[docs] def generate_data(self, chart: ChartType) -> dict[str, Any]: """ Extract and structure data from the chart. Returns a standardized dictionary format that renderers understand:: { "type": "table" | "text" | "key_value" | "side_by_side_tables" | "grouped_tables", "headers": [...], # For tables "rows": [...], # For tables "text": "...", # For text blocks "data": {...}, # For key-value pairs "tables": [...], # For side_by_side_tables or grouped_tables } Args: chart: The chart to extract data from (may be single or multi-chart) Returns: Structured data dictionary """ ...
[docs] class ReportRenderer(Protocol): """ Protocol for output renderers. Renderers take structured section data and format it for a specific output medium (terminal, plain text, HTML, etc.). Why separate renderers? - Same data, multiple output formats - Easy to add new formats without touching section code - Testable in isolation """
[docs] def render_section(self, section_name: str, section_data: dict[str, Any]) -> str: """ Render a single section. Args: section_name: Header for the section section_data: Structured data from section.generate_data() Returns: Formatted string for this section """ ...
[docs] def render_report(self, sections: list[tuple[str, dict[str, Any]]]) -> str: """ Render a complete report with multiple sections. Args: sections: List of (section_name, section_data) tuples Returns: Complete formatted report """ ...
[docs] class ChartAnalyzer(Protocol): """ Protocol for chart analysis components. Analyzers examine a calculated chart and return findings. """ @property def analyzer_name(self) -> str: """Name of this analyzer.""" ... @property def metadata_name(self) -> str: """Name that the metadata should be store under""" ...
[docs] def analyze(self, chart: CalculatedChart) -> list | dict: """ Analyze the chart. Args: chart: Chart to analyze Returns: Dict of findings (type depends on analyzer) """ ...