kerykeion.astrological_subject

This is part of Kerykeion (C) 2025 Giacomo Battaglia

  1# -*- coding: utf-8 -*-
  2"""
  3    This is part of Kerykeion (C) 2025 Giacomo Battaglia
  4"""
  5
  6import pytz
  7import swisseph as swe
  8import logging
  9import warnings
 10
 11import math
 12from datetime import datetime
 13
 14from functools import cached_property
 15from kerykeion.fetch_geonames import FetchGeonames
 16from kerykeion.kr_types import (
 17    KerykeionException,
 18    ZodiacType,
 19    AstrologicalSubjectModel,
 20    LunarPhaseModel,
 21    KerykeionPointModel,
 22    PointType,
 23    SiderealMode,
 24    HousesSystemIdentifier,
 25    PerspectiveType,
 26    Planet,
 27    Houses,
 28    AxialCusps,
 29)
 30from kerykeion.utilities import (
 31    get_number_from_name,
 32    get_kerykeion_point_from_degree,
 33    get_planet_house,
 34    check_and_adjust_polar_latitude,
 35    calculate_moon_phase
 36)
 37from pathlib import Path
 38from typing import Union, get_args
 39
 40DEFAULT_GEONAMES_USERNAME = "century.boy"
 41DEFAULT_SIDEREAL_MODE: SiderealMode = "FAGAN_BRADLEY"
 42DEFAULT_HOUSES_SYSTEM_IDENTIFIER: HousesSystemIdentifier = "P"
 43DEFAULT_ZODIAC_TYPE: ZodiacType = "Tropic"
 44DEFAULT_PERSPECTIVE_TYPE: PerspectiveType = "Apparent Geocentric"
 45DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS = 30
 46GEONAMES_DEFAULT_USERNAME_WARNING = (
 47    "\n********\n"
 48    "NO GEONAMES USERNAME SET!\n"
 49    "Using the default geonames username is not recommended, please set a custom one!\n"
 50    "You can get one for free here:\n"
 51    "https://www.geonames.org/login\n"
 52    "Keep in mind that the default username is limited to 2000 requests per hour and is shared with everyone else using this library.\n"
 53    "********"
 54)
 55
 56NOW = datetime.now()
 57
 58
 59class AstrologicalSubject:
 60    """
 61    Calculates all the astrological information, the coordinates,
 62    it's utc and julian day and returns an object with all that data.
 63
 64    Args:
 65    - name (str, optional): The name of the subject. Defaults to "Now".
 66    - year (int, optional): The year of birth. Defaults to the current year.
 67    - month (int, optional): The month of birth. Defaults to the current month.
 68    - day (int, optional): The day of birth. Defaults to the current day.
 69    - hour (int, optional): The hour of birth. Defaults to the current hour.
 70    - minute (int, optional): Defaults to the current minute.
 71    - city (str, optional): City or location of birth. Defaults to "London", which is GMT time.
 72        The city argument is used to get the coordinates and timezone from geonames just in case
 73        you don't insert them manually (see _get_tz).
 74        If you insert the coordinates and timezone manually, the city argument is not used for calculations
 75        but it's still used as a value for the city attribute.
 76    - nat (str, optional): _ Defaults to "".
 77    - lng (Union[int, float], optional): Longitude of the birth location. Defaults to 0 (Greenwich, London).
 78    - lat (Union[int, float], optional): Latitude of the birth location. Defaults to 51.5074 (Greenwich, London).
 79    - tz_str (Union[str, bool], optional): Timezone of the birth location. Defaults to "GMT".
 80    - geonames_username (str, optional): The username for the geonames API. Note: Change this to your own username to avoid rate limits!
 81        You can get one for free here: https://www.geonames.org/login
 82    - online (bool, optional): Sets if you want to use the online mode, which fetches the timezone and coordinates from geonames.
 83        If you already have the coordinates and timezone, set this to False. Defaults to True.
 84    - disable_chiron: Deprecated, use disable_chiron_and_lilith instead.
 85    - sidereal_mode (SiderealMode, optional): Also known as Ayanamsa.
 86        The mode to use for the sidereal zodiac, according to the Swiss Ephemeris.
 87        Defaults to "FAGAN_BRADLEY".
 88        Available modes are visible in the SiderealMode Literal.
 89    - houses_system_identifier (HousesSystemIdentifier, optional): The system to use for the calculation of the houses.
 90        Defaults to "P" (Placidus).
 91        Available systems are visible in the HousesSystemIdentifier Literal.
 92    - perspective_type (PerspectiveType, optional): The perspective to use for the calculation of the chart.
 93        Defaults to "Apparent Geocentric".
 94        Available perspectives are visible in the PerspectiveType Literal.
 95    - cache_expire_after_days (int, optional): The number of days after which the geonames cache will expire. Defaults to 30.
 96    - is_dst (Union[None, bool], optional): Specify if the time is in DST. Defaults to None.
 97        By default (None), the library will try to guess if the time is in DST or not and raise an AmbiguousTimeError
 98        if it can't guess. If you know the time is in DST, set this to True, if you know it's not, set it to False.
 99    - disable_chiron_and_lilith (bool, optional): boolean representing if Chiron and Lilith should be disabled. Default is False.
100        Chiron calculation can create some issues with the Swiss Ephemeris when the date is too far in the past.
101    """
102
103    # Defined by the user
104    name: str
105    year: int
106    month: int
107    day: int
108    hour: int
109    minute: int
110    city: str
111    nation: str
112    lng: Union[int, float]
113    lat: Union[int, float]
114    tz_str: str
115    geonames_username: str
116    online: bool
117    zodiac_type: ZodiacType
118    sidereal_mode: Union[SiderealMode, None]
119    houses_system_identifier: HousesSystemIdentifier
120    houses_system_name: str
121    perspective_type: PerspectiveType
122    is_dst: Union[None, bool]
123
124    # Generated internally
125    city_data: dict[str, str]
126    julian_day: Union[int, float]
127    json_dir: Path
128    iso_formatted_local_datetime: str
129    iso_formatted_utc_datetime: str
130
131    # Planets
132    sun: KerykeionPointModel
133    moon: KerykeionPointModel
134    mercury: KerykeionPointModel
135    venus: KerykeionPointModel
136    mars: KerykeionPointModel
137    jupiter: KerykeionPointModel
138    saturn: KerykeionPointModel
139    uranus: KerykeionPointModel
140    neptune: KerykeionPointModel
141    pluto: KerykeionPointModel
142    true_node: KerykeionPointModel
143    mean_node: KerykeionPointModel
144    chiron: Union[KerykeionPointModel, None]
145    mean_lilith: Union[KerykeionPointModel, None]
146    true_south_node: KerykeionPointModel
147    mean_south_node: KerykeionPointModel
148
149    # Axes
150    asc: KerykeionPointModel
151    dsc: KerykeionPointModel
152    mc: KerykeionPointModel
153    ic: KerykeionPointModel
154
155    # Houses
156    first_house: KerykeionPointModel
157    second_house: KerykeionPointModel
158    third_house: KerykeionPointModel
159    fourth_house: KerykeionPointModel
160    fifth_house: KerykeionPointModel
161    sixth_house: KerykeionPointModel
162    seventh_house: KerykeionPointModel
163    eighth_house: KerykeionPointModel
164    ninth_house: KerykeionPointModel
165    tenth_house: KerykeionPointModel
166    eleventh_house: KerykeionPointModel
167    twelfth_house: KerykeionPointModel
168
169    # Lists
170    _houses_list: list[KerykeionPointModel]
171    _houses_degree_ut: list[float]
172    planets_names_list: list[Planet]
173    houses_names_list: list[Houses]
174    axial_cusps_names_list: list[AxialCusps]
175
176    # Enable or disable features
177    disable_chiron: Union[None, bool]
178    disable_chiron_and_lilith: bool
179
180    lunar_phase: LunarPhaseModel
181
182    def __init__(
183        self,
184        name="Now",
185        year: int = NOW.year,
186        month: int = NOW.month,
187        day: int = NOW.day,
188        hour: int = NOW.hour,
189        minute: int = NOW.minute,
190        city: Union[str, None] = None,
191        nation: Union[str, None] = None,
192        lng: Union[int, float, None] = None,
193        lat: Union[int, float, None] = None,
194        tz_str: Union[str, None] = None,
195        geonames_username: Union[str, None] = None,
196        zodiac_type: Union[ZodiacType, None] = DEFAULT_ZODIAC_TYPE,
197        online: bool = True,
198        disable_chiron: Union[None, bool] = None, # Deprecated
199        sidereal_mode: Union[SiderealMode, None] = None,
200        houses_system_identifier: Union[HousesSystemIdentifier, None] = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
201        perspective_type: Union[PerspectiveType, None] = DEFAULT_PERSPECTIVE_TYPE,
202        cache_expire_after_days: Union[int, None] = DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS,
203        is_dst: Union[None, bool] = None,
204        disable_chiron_and_lilith: bool = False
205    ) -> None:
206        logging.debug("Starting Kerykeion")
207
208        # Deprecation warnings --->
209        if disable_chiron is not None:
210            warnings.warn(
211                "The 'disable_chiron' argument is deprecated and will be removed in a future version. "
212                "Please use 'disable_chiron' instead.",
213                DeprecationWarning
214            )
215
216            if disable_chiron_and_lilith:
217                raise ValueError("Cannot specify both 'disable_chiron' and 'disable_chiron_and_lilith'. Use 'disable_chiron_and_lilith' only.")
218
219            self.disable_chiron_and_lilith = disable_chiron
220        # <--- Deprecation warnings
221
222        self.name = name
223        self.year = year
224        self.month = month
225        self.day = day
226        self.hour = hour
227        self.minute = minute
228        self.online = online
229        self.json_dir = Path.home()
230        self.disable_chiron = disable_chiron
231        self.sidereal_mode = sidereal_mode
232        self.cache_expire_after_days = cache_expire_after_days
233        self.is_dst = is_dst
234        self.disable_chiron_and_lilith = disable_chiron_and_lilith
235
236        #---------------#
237        # General setup #
238        #---------------#
239
240        # Geonames username
241        if geonames_username is None and online and (not lat or not lng or not tz_str):
242            logging.warning(GEONAMES_DEFAULT_USERNAME_WARNING)
243            self.geonames_username = DEFAULT_GEONAMES_USERNAME
244        else:
245            self.geonames_username = geonames_username # type: ignore
246
247        # City
248        if not city:
249            self.city = "London"
250            logging.info("No city specified, using London as default")
251        else:
252            self.city = city
253
254        # Nation
255        if not nation:
256            self.nation = "GB"
257            logging.info("No nation specified, using GB as default")
258        else:
259            self.nation = nation
260
261        # Latitude
262        if not lat and not self.online:
263            self.lat = 51.5074
264            logging.info("No latitude specified, using London as default")
265        else:
266            self.lat = lat # type: ignore
267
268        # Longitude
269        if not lng and not self.online:
270            self.lng = 0
271            logging.info("No longitude specified, using London as default")
272        else:
273            self.lng = lng # type: ignore
274
275        # Timezone
276        if (not self.online) and (not tz_str):
277            raise KerykeionException("You need to set the coordinates and timezone if you want to use the offline mode!")
278        else:
279            self.tz_str = tz_str # type: ignore
280
281        # Zodiac type
282        if not zodiac_type:
283            self.zodiac_type = DEFAULT_ZODIAC_TYPE
284        else:
285            self.zodiac_type = zodiac_type
286
287        # Perspective type
288        if not perspective_type:
289            self.perspective_type = DEFAULT_PERSPECTIVE_TYPE
290        else:
291            self.perspective_type = perspective_type
292
293        # Houses system identifier
294        if not houses_system_identifier:
295            self.houses_system_identifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER
296        else:
297            self.houses_system_identifier = houses_system_identifier
298
299        # Cache expire after days
300        if not cache_expire_after_days:
301            self.cache_expire_after_days = DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS
302        else:
303            self.cache_expire_after_days = cache_expire_after_days
304
305        #-----------------------#
306        # Swiss Ephemeris setup #
307        #-----------------------#
308
309        # We set the swisseph path to the current directory
310        swe.set_ephe_path(str(Path(__file__).parent.absolute() / "sweph"))
311
312        # Flags for the Swiss Ephemeris
313        self._iflag = swe.FLG_SWIEPH + swe.FLG_SPEED
314
315        # Chart Perspective check and setup --->
316        if self.perspective_type not in get_args(PerspectiveType):
317            raise KerykeionException(f"\n* ERROR: '{self.perspective_type}' is NOT a valid chart perspective! Available perspectives are: *" + "\n" + str(get_args(PerspectiveType)))
318
319        if self.perspective_type == "True Geocentric":
320            self._iflag += swe.FLG_TRUEPOS
321        elif self.perspective_type == "Heliocentric":
322            self._iflag += swe.FLG_HELCTR
323        elif self.perspective_type == "Topocentric":
324            self._iflag += swe.FLG_TOPOCTR
325            # geopos_is_set, for topocentric
326            if (self.online) and (not self.tz_str) and (not self.lat) and (not self.lng):
327                self._fetch_and_set_tz_and_coordinates_from_geonames()
328            swe.set_topo(self.lng, self.lat, 0)
329        # <--- Chart Perspective check and setup
330
331        # House System check and setup --->
332        if self.houses_system_identifier not in get_args(HousesSystemIdentifier):
333            raise KerykeionException(f"\n* ERROR: '{self.houses_system_identifier}' is NOT a valid house system! Available systems are: *" + "\n" + str(get_args(HousesSystemIdentifier)))
334
335        self.houses_system_name = swe.house_name(self.houses_system_identifier.encode('ascii'))
336        # <--- House System check and setup
337
338        # Zodiac Type and Sidereal mode checks and setup --->
339        if zodiac_type and not zodiac_type in get_args(ZodiacType):
340            raise KerykeionException(f"\n* ERROR: '{zodiac_type}' is NOT a valid zodiac type! Available types are: *" + "\n" + str(get_args(ZodiacType)))
341
342        if self.sidereal_mode and self.zodiac_type == "Tropic":
343            raise KerykeionException("You can't set a sidereal mode with a Tropic zodiac type!")
344
345        if self.zodiac_type == "Sidereal" and not self.sidereal_mode:
346            self.sidereal_mode = DEFAULT_SIDEREAL_MODE
347            logging.info("No sidereal mode set, using default FAGAN_BRADLEY")
348
349        if self.zodiac_type == "Sidereal":
350            # Check if the sidereal mode is valid
351
352            if not self.sidereal_mode or not self.sidereal_mode in get_args(SiderealMode):
353                raise KerykeionException(f"\n* ERROR: '{self.sidereal_mode}' is NOT a valid sidereal mode! Available modes are: *" + "\n" + str(get_args(SiderealMode)))
354
355            self._iflag += swe.FLG_SIDEREAL
356            mode = "SIDM_" + self.sidereal_mode
357            swe.set_sid_mode(getattr(swe, mode))
358            logging.debug(f"Using sidereal mode: {mode}")
359        # <--- Zodiac Type and Sidereal mode checks and setup
360
361        #------------------------#
362        # Start the calculations #
363        #------------------------#
364
365        # UTC, julian day and local time setup --->
366        if (self.online) and (not self.tz_str) and (not self.lat) and (not self.lng):
367            self._fetch_and_set_tz_and_coordinates_from_geonames()
368
369        self.lat = check_and_adjust_polar_latitude(self.lat)
370
371        # Local time to UTC
372        local_time = pytz.timezone(self.tz_str)
373        naive_datetime = datetime(self.year, self.month, self.day, self.hour, self.minute, 0)
374
375        try:
376            local_datetime = local_time.localize(naive_datetime, is_dst=self.is_dst)
377        except pytz.exceptions.AmbiguousTimeError:
378            raise KerykeionException("Ambiguous time! Please specify if the time is in DST or not with the is_dst argument.")
379
380        utc_object = local_datetime.astimezone(pytz.utc)
381        self.iso_formatted_utc_datetime = utc_object.isoformat()
382
383        # ISO formatted local datetime
384        self.iso_formatted_local_datetime = local_datetime.isoformat()
385
386        # Julian day calculation
387        utc_float_hour_with_minutes = utc_object.hour + (utc_object.minute / 60)
388        self.julian_day = float(swe.julday(utc_object.year, utc_object.month, utc_object.day, utc_float_hour_with_minutes))
389        # <--- UTC, julian day and local time setup
390
391        # Planets and Houses setup
392        self._initialize_houses()
393        self._initialize_planets()
394
395        # Lunar Phase
396        self.lunar_phase = calculate_moon_phase(
397            self.moon.abs_pos,
398            self.sun.abs_pos
399        )
400
401        # Deprecated properties
402        self.utc_time
403        self.local_time
404
405    def __str__(self) -> str:
406        return f"Astrological data for: {self.name}, {self.iso_formatted_utc_datetime} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}"
407
408    def __repr__(self) -> str:
409        return f"Astrological data for: {self.name}, {self.iso_formatted_utc_datetime} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}"
410
411    def __getitem__(self, item):
412        return getattr(self, item)
413
414    def get(self, item, default=None):
415        return getattr(self, item, default)
416
417    def _fetch_and_set_tz_and_coordinates_from_geonames(self) -> None:
418        """Gets the nearest time zone for the calculation"""
419        logging.info("Fetching timezone/coordinates from geonames")
420
421        geonames = FetchGeonames(
422            self.city,
423            self.nation,
424            username=self.geonames_username,
425            cache_expire_after_days=self.cache_expire_after_days
426        )
427        self.city_data: dict[str, str] = geonames.get_serialized_data()
428
429        if (
430            not "countryCode" in self.city_data
431            or not "timezonestr" in self.city_data
432            or not "lat" in self.city_data
433            or not "lng" in self.city_data
434        ):
435            raise KerykeionException("No data found for this city, try again! Maybe check your connection?")
436
437        self.nation = self.city_data["countryCode"]
438        self.lng = float(self.city_data["lng"])
439        self.lat = float(self.city_data["lat"])
440        self.tz_str = self.city_data["timezonestr"]
441
442    def _initialize_houses(self) -> None:
443        """
444        Calculate positions and store them in dictionaries
445
446        https://www.astro.com/faq/fq_fh_owhouse_e.htm
447        https://github.com/jwmatthys/pd-swisseph/blob/master/swehouse.c#L685
448        hsys = letter code for house system;
449            A  equal
450            E  equal
451            B  Alcabitius
452            C  Campanus
453            D  equal (MC)
454            F  Carter "Poli-Equatorial"
455            G  36 Gauquelin sectors
456            H  horizon / azimut
457            I  Sunshine solution Treindl
458            i  Sunshine solution Makransky
459            K  Koch
460            L  Pullen SD "sinusoidal delta", ex Neo-Porphyry
461            M  Morinus
462            N  equal/1=Aries
463            O  Porphyry
464            P  Placidus
465            Q  Pullen SR "sinusoidal ratio"
466            R  Regiomontanus
467            S  Sripati
468            T  Polich/Page ("topocentric")
469            U  Krusinski-Pisa-Goelzer
470            V  equal Vehlow
471            W  equal, whole sign
472            X  axial rotation system/ Meridian houses
473            Y  APC houses
474        """
475
476        _ascmc = (-1.0, -1.0)
477
478        if self.zodiac_type == "Sidereal":
479            cusps, ascmc = swe.houses_ex(
480                tjdut=self.julian_day,
481                lat=self.lat, lon=self.lng,
482                hsys=str.encode(self.houses_system_identifier),
483                flags=swe.FLG_SIDEREAL
484            )
485            self._houses_degree_ut = cusps
486            _ascmc = ascmc
487
488        elif self.zodiac_type == "Tropic":
489            cusps, ascmc = swe.houses(
490                tjdut=self.julian_day, lat=self.lat,
491                lon=self.lng,
492                hsys=str.encode(self.houses_system_identifier)
493            )
494            self._houses_degree_ut = cusps
495            _ascmc = ascmc
496
497        else:
498            raise KerykeionException("Not a valid zodiac type: " + self.zodiac_type)
499
500        point_type: PointType = "House"
501
502        # stores the house in singular dictionaries.
503        self.first_house = get_kerykeion_point_from_degree(self._houses_degree_ut[0], "First_House", point_type=point_type)
504        self.second_house = get_kerykeion_point_from_degree(self._houses_degree_ut[1], "Second_House", point_type=point_type)
505        self.third_house = get_kerykeion_point_from_degree(self._houses_degree_ut[2], "Third_House", point_type=point_type)
506        self.fourth_house = get_kerykeion_point_from_degree(self._houses_degree_ut[3], "Fourth_House", point_type=point_type)
507        self.fifth_house = get_kerykeion_point_from_degree(self._houses_degree_ut[4], "Fifth_House", point_type=point_type)
508        self.sixth_house = get_kerykeion_point_from_degree(self._houses_degree_ut[5], "Sixth_House", point_type=point_type)
509        self.seventh_house = get_kerykeion_point_from_degree(self._houses_degree_ut[6], "Seventh_House", point_type=point_type)
510        self.eighth_house = get_kerykeion_point_from_degree(self._houses_degree_ut[7], "Eighth_House", point_type=point_type)
511        self.ninth_house = get_kerykeion_point_from_degree(self._houses_degree_ut[8], "Ninth_House", point_type=point_type)
512        self.tenth_house = get_kerykeion_point_from_degree(self._houses_degree_ut[9], "Tenth_House", point_type=point_type)
513        self.eleventh_house = get_kerykeion_point_from_degree(self._houses_degree_ut[10], "Eleventh_House", point_type=point_type)
514        self.twelfth_house = get_kerykeion_point_from_degree(self._houses_degree_ut[11], "Twelfth_House", point_type=point_type)
515
516        self.houses_names_list = list(get_args(Houses))
517
518        # Deprecated
519        self._houses_list = [
520            self.first_house,
521            self.second_house,
522            self.third_house,
523            self.fourth_house,
524            self.fifth_house,
525            self.sixth_house,
526            self.seventh_house,
527            self.eighth_house,
528            self.ninth_house,
529            self.tenth_house,
530            self.eleventh_house,
531            self.twelfth_house,
532        ]
533
534        # AxialCusps
535        point_type: PointType = "AxialCusps"
536
537        # Calculate ascendant and medium coeli
538        self.ascendant = get_kerykeion_point_from_degree(_ascmc[0], "Ascendant", point_type=point_type)
539        self.medium_coeli = get_kerykeion_point_from_degree(_ascmc[1], "Medium_Coeli", point_type=point_type)
540        # For descendant and imum coeli there exist no Swiss Ephemeris library calculation function,
541        # but they are simply opposite the the ascendant and medium coeli
542        dsc_deg = math.fmod(_ascmc[0] + 180, 360)
543        ic_deg = math.fmod(_ascmc[1] + 180, 360)
544        self.descendant = get_kerykeion_point_from_degree(dsc_deg, "Descendant", point_type=point_type)
545        self.imum_coeli = get_kerykeion_point_from_degree(ic_deg, "Imum_Coeli", point_type=point_type)
546
547    def _initialize_planets(self) -> None:
548        """Defines body positon in signs and information and
549        stores them in dictionaries"""
550
551        point_type: PointType = "Planet"
552
553        sun_deg = swe.calc(self.julian_day, 0, self._iflag)[0][0]
554        moon_deg = swe.calc(self.julian_day, 1, self._iflag)[0][0]
555        mercury_deg = swe.calc(self.julian_day, 2, self._iflag)[0][0]
556        venus_deg = swe.calc(self.julian_day, 3, self._iflag)[0][0]
557        mars_deg = swe.calc(self.julian_day, 4, self._iflag)[0][0]
558        jupiter_deg = swe.calc(self.julian_day, 5, self._iflag)[0][0]
559        saturn_deg = swe.calc(self.julian_day, 6, self._iflag)[0][0]
560        uranus_deg = swe.calc(self.julian_day, 7, self._iflag)[0][0]
561        neptune_deg = swe.calc(self.julian_day, 8, self._iflag)[0][0]
562        pluto_deg = swe.calc(self.julian_day, 9, self._iflag)[0][0]
563        mean_node_deg = swe.calc(self.julian_day, 10, self._iflag)[0][0]
564        true_node_deg = swe.calc(self.julian_day, 11, self._iflag)[0][0]
565        # For south nodes there exist no Swiss Ephemeris library calculation function,
566        # but they are simply opposite the north node.
567        mean_south_node_deg = math.fmod(mean_node_deg + 180, 360)
568        true_south_node_deg = math.fmod(true_node_deg + 180, 360)
569
570        # AC/DC axis and MC/IC axis were already calculated previously...
571
572        self.sun = get_kerykeion_point_from_degree(sun_deg, "Sun", point_type=point_type)
573        self.moon = get_kerykeion_point_from_degree(moon_deg, "Moon", point_type=point_type)
574        self.mercury = get_kerykeion_point_from_degree(mercury_deg, "Mercury", point_type=point_type)
575        self.venus = get_kerykeion_point_from_degree(venus_deg, "Venus", point_type=point_type)
576        self.mars = get_kerykeion_point_from_degree(mars_deg, "Mars", point_type=point_type)
577        self.jupiter = get_kerykeion_point_from_degree(jupiter_deg, "Jupiter", point_type=point_type)
578        self.saturn = get_kerykeion_point_from_degree(saturn_deg, "Saturn", point_type=point_type)
579        self.uranus = get_kerykeion_point_from_degree(uranus_deg, "Uranus", point_type=point_type)
580        self.neptune = get_kerykeion_point_from_degree(neptune_deg, "Neptune", point_type=point_type)
581        self.pluto = get_kerykeion_point_from_degree(pluto_deg, "Pluto", point_type=point_type)
582        self.mean_node = get_kerykeion_point_from_degree(mean_node_deg, "Mean_Node", point_type=point_type)
583        self.true_node = get_kerykeion_point_from_degree(true_node_deg, "True_Node", point_type=point_type)
584        self.mean_south_node = get_kerykeion_point_from_degree(mean_south_node_deg, "Mean_South_Node", point_type=point_type)
585        self.true_south_node = get_kerykeion_point_from_degree(true_south_node_deg, "True_South_Node", point_type=point_type)
586
587        # Note that in whole-sign house systems ac/dc or mc/ic axes may not align with house cusps.
588        # Therefore, for the axes we need to calculate house positions explicitly too.
589        self.ascendant.house = get_planet_house(self.ascendant.abs_pos, self._houses_degree_ut)
590        self.descendant.house = get_planet_house(self.descendant.abs_pos, self._houses_degree_ut)
591        self.medium_coeli.house = get_planet_house(self.medium_coeli.abs_pos, self._houses_degree_ut)
592        self.imum_coeli.house = get_planet_house(self.imum_coeli.abs_pos, self._houses_degree_ut)
593
594        self.sun.house = get_planet_house(sun_deg, self._houses_degree_ut)
595        self.moon.house = get_planet_house(moon_deg, self._houses_degree_ut)
596        self.mercury.house = get_planet_house(mercury_deg, self._houses_degree_ut)
597        self.venus.house = get_planet_house(venus_deg, self._houses_degree_ut)
598        self.mars.house = get_planet_house(mars_deg, self._houses_degree_ut)
599        self.jupiter.house = get_planet_house(jupiter_deg, self._houses_degree_ut)
600        self.saturn.house = get_planet_house(saturn_deg, self._houses_degree_ut)
601        self.uranus.house = get_planet_house(uranus_deg, self._houses_degree_ut)
602        self.neptune.house = get_planet_house(neptune_deg, self._houses_degree_ut)
603        self.pluto.house = get_planet_house(pluto_deg, self._houses_degree_ut)
604        self.mean_node.house = get_planet_house(mean_node_deg, self._houses_degree_ut)
605        self.true_node.house = get_planet_house(true_node_deg, self._houses_degree_ut)
606        self.mean_south_node.house = get_planet_house(mean_south_node_deg, self._houses_degree_ut)
607        self.true_south_node.house = get_planet_house(true_south_node_deg, self._houses_degree_ut)
608
609
610        # Deprecated
611        planets_list = [
612            self.sun,
613            self.moon,
614            self.mercury,
615            self.venus,
616            self.mars,
617            self.jupiter,
618            self.saturn,
619            self.uranus,
620            self.neptune,
621            self.pluto,
622            self.mean_node,
623            self.true_node,
624            self.mean_south_node,
625            self.true_south_node,
626        ]
627
628        if not self.disable_chiron_and_lilith:
629            chiron_deg = swe.calc(self.julian_day, 15, self._iflag)[0][0]
630            mean_lilith_deg = swe.calc(self.julian_day, 12, self._iflag)[0][0]
631
632            self.chiron = get_kerykeion_point_from_degree(chiron_deg, "Chiron", point_type=point_type)
633            self.mean_lilith = get_kerykeion_point_from_degree(mean_lilith_deg, "Mean_Lilith", point_type=point_type)
634
635            self.chiron.house = get_planet_house(chiron_deg, self._houses_degree_ut)
636            self.mean_lilith.house = get_planet_house(mean_lilith_deg, self._houses_degree_ut)
637
638            # Deprecated
639            planets_list.append(self.chiron)
640            planets_list.append(self.mean_lilith)
641
642        else:
643            self.chiron = None
644            self.mean_lilith = None
645
646        # FIXME: Update after removing planets_list
647        self.planets_names_list = [planet["name"] for planet in planets_list]
648        self.axial_cusps_names_list = [
649            axis["name"] for axis in [self.ascendant, self.descendant, self.medium_coeli, self.imum_coeli]
650        ]
651
652        # Check in retrograde or not:
653        for planet in planets_list:
654            planet_number = get_number_from_name(planet["name"])
655
656            # Swiss ephemeris library does not offer calculation of direction of south nodes.
657            # But south nodes have same direction as north nodes. We can use those to calculate direction.
658            if planet_number == 1000:   # Number of Mean South Node
659                planet_number = 10      # Number of Mean North Node
660            elif planet_number == 1100: # Number of True South Node
661                planet_number = 11      # Number of True North Node
662
663
664            if swe.calc(self.julian_day, planet_number, self._iflag)[0][3] < 0:
665                planet["retrograde"] = True
666            else:
667                planet["retrograde"] = False
668
669        # AC/DC and MC/IC axes are never retrograde. For consistency, set them to be not retrograde.
670        self.ascendant.retrograde = False
671        self.descendant.retrograde = False
672        self.medium_coeli.retrograde = False
673        self.imum_coeli.retrograde = False
674
675
676    def json(self, dump=False, destination_folder: Union[str, None] = None, indent: Union[int, None] = None) -> str:
677        """
678        Dumps the Kerykeion object to a json string foramt,
679        if dump=True also dumps to file located in destination
680        or the home folder.
681        """
682
683        KrData = AstrologicalSubjectModel(**self.__dict__)
684        json_string = KrData.model_dump_json(exclude_none=True, indent=indent)
685
686        if dump:
687            if destination_folder:
688                destination_path = Path(destination_folder)
689                json_path = destination_path / f"{self.name}_kerykeion.json"
690
691            else:
692                json_path = self.json_dir / f"{self.name}_kerykeion.json"
693
694            with open(json_path, "w", encoding="utf-8") as file:
695                file.write(json_string)
696                logging.info(f"JSON file dumped in {json_path}.")
697
698        return json_string
699
700    def model(self) -> AstrologicalSubjectModel:
701        """
702        Creates a Pydantic model of the Kerykeion object.
703        """
704
705        return AstrologicalSubjectModel(**self.__dict__)
706
707    @cached_property
708    def utc_time(self) -> float:
709        """
710        Deprecated property, use iso_formatted_utc_datetime instead, will be removed in the future.
711        Returns the UTC time as a float.
712        """
713        dt = datetime.fromisoformat(self.iso_formatted_utc_datetime)
714
715        # Extract the hours, minutes, and seconds
716        hours = dt.hour
717        minutes = dt.minute
718        seconds = dt.second + dt.microsecond / 1_000_000
719
720        # Convert time to float hours
721        float_time = hours + minutes / 60 + seconds / 3600
722
723        return float_time
724
725    @cached_property
726    def local_time(self) -> float:
727        """
728        Deprecated property, use iso_formatted_local_datetime instead, will be removed in the future.
729        Returns the local time as a float.
730        """
731        dt = datetime.fromisoformat(self.iso_formatted_local_datetime)
732
733        # Extract the hours, minutes, and seconds
734        hours = dt.hour
735        minutes = dt.minute
736        seconds = dt.second + dt.microsecond / 1_000_000
737
738        # Convert time to float hours
739        float_time = hours + minutes / 60 + seconds / 3600
740
741        return float_time
742
743
744    @staticmethod
745    def get_from_iso_utc_time(
746        name: str,
747        iso_utc_time: str,
748        city: str = "Greenwich",
749        nation: str = "GB",
750        tz_str: str = "Etc/GMT",
751        online: bool = False,
752        lng: Union[int, float] = 0,
753        lat: Union[int, float] = 51.5074,
754        geonames_username: str = DEFAULT_GEONAMES_USERNAME,
755        zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE,
756        disable_chiron_and_lilith: bool = False,
757        sidereal_mode: Union[SiderealMode, None] = None,
758        houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
759        perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE
760
761    ) -> "AstrologicalSubject":
762        """
763        Creates an AstrologicalSubject object from an iso formatted UTC time.
764        This method is offline by default, set online=True to fetch the timezone and coordinates from geonames.
765
766        Args:
767        - name (str): The name of the subject.
768        - iso_utc_time (str): The iso formatted UTC time.
769        - city (str, optional): City or location of birth. Defaults to "Greenwich".
770        - nation (str, optional): Nation of birth. Defaults to "GB".
771        - tz_str (str, optional): Timezone of the birth location. Defaults to "Etc/GMT".
772        - online (bool, optional): Sets if you want to use the online mode, which fetches the timezone and coordinates from geonames.
773            If you already have the coordinates and timezone, set this to False. Defaults to False.
774        - lng (Union[int, float], optional): Longitude of the birth location. Defaults to 0 (Greenwich, London).
775        - lat (Union[int, float], optional): Latitude of the birth location. Defaults to 51.5074 (Greenwich, London).
776        - geonames_username (str, optional): The username for the geonames API. Note: Change this to your own username to avoid rate limits!
777            You can get one for free here: https://www.geonames.org/login
778        - zodiac_type (ZodiacType, optional): The zodiac type to use. Defaults to "Tropic".
779        - disable_chiron_and_lilith: boolean representing if Chiron and Lilith should be disabled. Default is False.
780            Chiron calculation can create some issues with the Swiss Ephemeris when the date is too far in the past.
781        - sidereal_mode (SiderealMode, optional): Also known as Ayanamsa.
782            The mode to use for the sidereal zodiac, according to the Swiss Ephemeris.
783            Defaults to None.
784            Available modes are visible in the SiderealMode Literal.
785        - houses_system_identifier (HousesSystemIdentifier, optional): The system to use for the calculation of the houses.
786            Defaults to "P" (Placidus).
787            Available systems are visible in the HousesSystemIdentifier Literal.
788        - perspective_type (PerspectiveType, optional): The perspective to use for the calculation of the chart.
789            Defaults to "Apparent Geocentric".
790
791        Returns:
792        - AstrologicalSubject: The AstrologicalSubject object.
793        """
794        dt = datetime.fromisoformat(iso_utc_time)
795
796        if online == True:
797            if geonames_username == DEFAULT_GEONAMES_USERNAME:
798                logging.warning(GEONAMES_DEFAULT_USERNAME_WARNING)
799
800            geonames = FetchGeonames(
801                city,
802                nation,
803                username=geonames_username,
804            )
805
806            city_data: dict[str, str] = geonames.get_serialized_data()
807            lng = float(city_data["lng"])
808            lat = float(city_data["lat"])
809
810        subject = AstrologicalSubject(
811            name=name,
812            year=dt.year,
813            month=dt.month,
814            day=dt.day,
815            hour=dt.hour,
816            minute=dt.minute,
817            city=city,
818            nation=city,
819            lng=lng,
820            lat=lat,
821            tz_str=tz_str,
822            online=False,
823            geonames_username=geonames_username,
824            zodiac_type=zodiac_type,
825            sidereal_mode=sidereal_mode,
826            houses_system_identifier=houses_system_identifier,
827            perspective_type=perspective_type,
828            disable_chiron_and_lilith=disable_chiron_and_lilith
829        )
830
831        return subject
832
833if __name__ == "__main__":
834    import json
835    from kerykeion.utilities import setup_logging
836
837    setup_logging(level="debug")
838
839    # With Chiron enabled
840    johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", zodiac_type=None)
841    print(json.loads(johnny.json(dump=True)))
DEFAULT_GEONAMES_USERNAME = 'century.boy'
DEFAULT_SIDEREAL_MODE: Literal['FAGAN_BRADLEY', 'LAHIRI', 'DELUCE', 'RAMAN', 'USHASHASHI', 'KRISHNAMURTI', 'DJWHAL_KHUL', 'YUKTESHWAR', 'JN_BHASIN', 'BABYL_KUGLER1', 'BABYL_KUGLER2', 'BABYL_KUGLER3', 'BABYL_HUBER', 'BABYL_ETPSC', 'ALDEBARAN_15TAU', 'HIPPARCHOS', 'SASSANIAN', 'J2000', 'J1900', 'B1950'] = 'FAGAN_BRADLEY'
DEFAULT_HOUSES_SYSTEM_IDENTIFIER: Literal['A', 'B', 'C', 'D', 'F', 'H', 'I', 'i', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y'] = 'P'
DEFAULT_ZODIAC_TYPE: Literal['Tropic', 'Sidereal'] = 'Tropic'
DEFAULT_PERSPECTIVE_TYPE: Literal['Apparent Geocentric', 'Heliocentric', 'Topocentric', 'True Geocentric'] = 'Apparent Geocentric'
DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS = 30
GEONAMES_DEFAULT_USERNAME_WARNING = '\n********\nNO GEONAMES USERNAME SET!\nUsing the default geonames username is not recommended, please set a custom one!\nYou can get one for free here:\nhttps://www.geonames.org/login\nKeep in mind that the default username is limited to 2000 requests per hour and is shared with everyone else using this library.\n********'
NOW = datetime.datetime(2025, 3, 29, 17, 13, 43, 666540)
class AstrologicalSubject:
 60class AstrologicalSubject:
 61    """
 62    Calculates all the astrological information, the coordinates,
 63    it's utc and julian day and returns an object with all that data.
 64
 65    Args:
 66    - name (str, optional): The name of the subject. Defaults to "Now".
 67    - year (int, optional): The year of birth. Defaults to the current year.
 68    - month (int, optional): The month of birth. Defaults to the current month.
 69    - day (int, optional): The day of birth. Defaults to the current day.
 70    - hour (int, optional): The hour of birth. Defaults to the current hour.
 71    - minute (int, optional): Defaults to the current minute.
 72    - city (str, optional): City or location of birth. Defaults to "London", which is GMT time.
 73        The city argument is used to get the coordinates and timezone from geonames just in case
 74        you don't insert them manually (see _get_tz).
 75        If you insert the coordinates and timezone manually, the city argument is not used for calculations
 76        but it's still used as a value for the city attribute.
 77    - nat (str, optional): _ Defaults to "".
 78    - lng (Union[int, float], optional): Longitude of the birth location. Defaults to 0 (Greenwich, London).
 79    - lat (Union[int, float], optional): Latitude of the birth location. Defaults to 51.5074 (Greenwich, London).
 80    - tz_str (Union[str, bool], optional): Timezone of the birth location. Defaults to "GMT".
 81    - geonames_username (str, optional): The username for the geonames API. Note: Change this to your own username to avoid rate limits!
 82        You can get one for free here: https://www.geonames.org/login
 83    - online (bool, optional): Sets if you want to use the online mode, which fetches the timezone and coordinates from geonames.
 84        If you already have the coordinates and timezone, set this to False. Defaults to True.
 85    - disable_chiron: Deprecated, use disable_chiron_and_lilith instead.
 86    - sidereal_mode (SiderealMode, optional): Also known as Ayanamsa.
 87        The mode to use for the sidereal zodiac, according to the Swiss Ephemeris.
 88        Defaults to "FAGAN_BRADLEY".
 89        Available modes are visible in the SiderealMode Literal.
 90    - houses_system_identifier (HousesSystemIdentifier, optional): The system to use for the calculation of the houses.
 91        Defaults to "P" (Placidus).
 92        Available systems are visible in the HousesSystemIdentifier Literal.
 93    - perspective_type (PerspectiveType, optional): The perspective to use for the calculation of the chart.
 94        Defaults to "Apparent Geocentric".
 95        Available perspectives are visible in the PerspectiveType Literal.
 96    - cache_expire_after_days (int, optional): The number of days after which the geonames cache will expire. Defaults to 30.
 97    - is_dst (Union[None, bool], optional): Specify if the time is in DST. Defaults to None.
 98        By default (None), the library will try to guess if the time is in DST or not and raise an AmbiguousTimeError
 99        if it can't guess. If you know the time is in DST, set this to True, if you know it's not, set it to False.
100    - disable_chiron_and_lilith (bool, optional): boolean representing if Chiron and Lilith should be disabled. Default is False.
101        Chiron calculation can create some issues with the Swiss Ephemeris when the date is too far in the past.
102    """
103
104    # Defined by the user
105    name: str
106    year: int
107    month: int
108    day: int
109    hour: int
110    minute: int
111    city: str
112    nation: str
113    lng: Union[int, float]
114    lat: Union[int, float]
115    tz_str: str
116    geonames_username: str
117    online: bool
118    zodiac_type: ZodiacType
119    sidereal_mode: Union[SiderealMode, None]
120    houses_system_identifier: HousesSystemIdentifier
121    houses_system_name: str
122    perspective_type: PerspectiveType
123    is_dst: Union[None, bool]
124
125    # Generated internally
126    city_data: dict[str, str]
127    julian_day: Union[int, float]
128    json_dir: Path
129    iso_formatted_local_datetime: str
130    iso_formatted_utc_datetime: str
131
132    # Planets
133    sun: KerykeionPointModel
134    moon: KerykeionPointModel
135    mercury: KerykeionPointModel
136    venus: KerykeionPointModel
137    mars: KerykeionPointModel
138    jupiter: KerykeionPointModel
139    saturn: KerykeionPointModel
140    uranus: KerykeionPointModel
141    neptune: KerykeionPointModel
142    pluto: KerykeionPointModel
143    true_node: KerykeionPointModel
144    mean_node: KerykeionPointModel
145    chiron: Union[KerykeionPointModel, None]
146    mean_lilith: Union[KerykeionPointModel, None]
147    true_south_node: KerykeionPointModel
148    mean_south_node: KerykeionPointModel
149
150    # Axes
151    asc: KerykeionPointModel
152    dsc: KerykeionPointModel
153    mc: KerykeionPointModel
154    ic: KerykeionPointModel
155
156    # Houses
157    first_house: KerykeionPointModel
158    second_house: KerykeionPointModel
159    third_house: KerykeionPointModel
160    fourth_house: KerykeionPointModel
161    fifth_house: KerykeionPointModel
162    sixth_house: KerykeionPointModel
163    seventh_house: KerykeionPointModel
164    eighth_house: KerykeionPointModel
165    ninth_house: KerykeionPointModel
166    tenth_house: KerykeionPointModel
167    eleventh_house: KerykeionPointModel
168    twelfth_house: KerykeionPointModel
169
170    # Lists
171    _houses_list: list[KerykeionPointModel]
172    _houses_degree_ut: list[float]
173    planets_names_list: list[Planet]
174    houses_names_list: list[Houses]
175    axial_cusps_names_list: list[AxialCusps]
176
177    # Enable or disable features
178    disable_chiron: Union[None, bool]
179    disable_chiron_and_lilith: bool
180
181    lunar_phase: LunarPhaseModel
182
183    def __init__(
184        self,
185        name="Now",
186        year: int = NOW.year,
187        month: int = NOW.month,
188        day: int = NOW.day,
189        hour: int = NOW.hour,
190        minute: int = NOW.minute,
191        city: Union[str, None] = None,
192        nation: Union[str, None] = None,
193        lng: Union[int, float, None] = None,
194        lat: Union[int, float, None] = None,
195        tz_str: Union[str, None] = None,
196        geonames_username: Union[str, None] = None,
197        zodiac_type: Union[ZodiacType, None] = DEFAULT_ZODIAC_TYPE,
198        online: bool = True,
199        disable_chiron: Union[None, bool] = None, # Deprecated
200        sidereal_mode: Union[SiderealMode, None] = None,
201        houses_system_identifier: Union[HousesSystemIdentifier, None] = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
202        perspective_type: Union[PerspectiveType, None] = DEFAULT_PERSPECTIVE_TYPE,
203        cache_expire_after_days: Union[int, None] = DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS,
204        is_dst: Union[None, bool] = None,
205        disable_chiron_and_lilith: bool = False
206    ) -> None:
207        logging.debug("Starting Kerykeion")
208
209        # Deprecation warnings --->
210        if disable_chiron is not None:
211            warnings.warn(
212                "The 'disable_chiron' argument is deprecated and will be removed in a future version. "
213                "Please use 'disable_chiron' instead.",
214                DeprecationWarning
215            )
216
217            if disable_chiron_and_lilith:
218                raise ValueError("Cannot specify both 'disable_chiron' and 'disable_chiron_and_lilith'. Use 'disable_chiron_and_lilith' only.")
219
220            self.disable_chiron_and_lilith = disable_chiron
221        # <--- Deprecation warnings
222
223        self.name = name
224        self.year = year
225        self.month = month
226        self.day = day
227        self.hour = hour
228        self.minute = minute
229        self.online = online
230        self.json_dir = Path.home()
231        self.disable_chiron = disable_chiron
232        self.sidereal_mode = sidereal_mode
233        self.cache_expire_after_days = cache_expire_after_days
234        self.is_dst = is_dst
235        self.disable_chiron_and_lilith = disable_chiron_and_lilith
236
237        #---------------#
238        # General setup #
239        #---------------#
240
241        # Geonames username
242        if geonames_username is None and online and (not lat or not lng or not tz_str):
243            logging.warning(GEONAMES_DEFAULT_USERNAME_WARNING)
244            self.geonames_username = DEFAULT_GEONAMES_USERNAME
245        else:
246            self.geonames_username = geonames_username # type: ignore
247
248        # City
249        if not city:
250            self.city = "London"
251            logging.info("No city specified, using London as default")
252        else:
253            self.city = city
254
255        # Nation
256        if not nation:
257            self.nation = "GB"
258            logging.info("No nation specified, using GB as default")
259        else:
260            self.nation = nation
261
262        # Latitude
263        if not lat and not self.online:
264            self.lat = 51.5074
265            logging.info("No latitude specified, using London as default")
266        else:
267            self.lat = lat # type: ignore
268
269        # Longitude
270        if not lng and not self.online:
271            self.lng = 0
272            logging.info("No longitude specified, using London as default")
273        else:
274            self.lng = lng # type: ignore
275
276        # Timezone
277        if (not self.online) and (not tz_str):
278            raise KerykeionException("You need to set the coordinates and timezone if you want to use the offline mode!")
279        else:
280            self.tz_str = tz_str # type: ignore
281
282        # Zodiac type
283        if not zodiac_type:
284            self.zodiac_type = DEFAULT_ZODIAC_TYPE
285        else:
286            self.zodiac_type = zodiac_type
287
288        # Perspective type
289        if not perspective_type:
290            self.perspective_type = DEFAULT_PERSPECTIVE_TYPE
291        else:
292            self.perspective_type = perspective_type
293
294        # Houses system identifier
295        if not houses_system_identifier:
296            self.houses_system_identifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER
297        else:
298            self.houses_system_identifier = houses_system_identifier
299
300        # Cache expire after days
301        if not cache_expire_after_days:
302            self.cache_expire_after_days = DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS
303        else:
304            self.cache_expire_after_days = cache_expire_after_days
305
306        #-----------------------#
307        # Swiss Ephemeris setup #
308        #-----------------------#
309
310        # We set the swisseph path to the current directory
311        swe.set_ephe_path(str(Path(__file__).parent.absolute() / "sweph"))
312
313        # Flags for the Swiss Ephemeris
314        self._iflag = swe.FLG_SWIEPH + swe.FLG_SPEED
315
316        # Chart Perspective check and setup --->
317        if self.perspective_type not in get_args(PerspectiveType):
318            raise KerykeionException(f"\n* ERROR: '{self.perspective_type}' is NOT a valid chart perspective! Available perspectives are: *" + "\n" + str(get_args(PerspectiveType)))
319
320        if self.perspective_type == "True Geocentric":
321            self._iflag += swe.FLG_TRUEPOS
322        elif self.perspective_type == "Heliocentric":
323            self._iflag += swe.FLG_HELCTR
324        elif self.perspective_type == "Topocentric":
325            self._iflag += swe.FLG_TOPOCTR
326            # geopos_is_set, for topocentric
327            if (self.online) and (not self.tz_str) and (not self.lat) and (not self.lng):
328                self._fetch_and_set_tz_and_coordinates_from_geonames()
329            swe.set_topo(self.lng, self.lat, 0)
330        # <--- Chart Perspective check and setup
331
332        # House System check and setup --->
333        if self.houses_system_identifier not in get_args(HousesSystemIdentifier):
334            raise KerykeionException(f"\n* ERROR: '{self.houses_system_identifier}' is NOT a valid house system! Available systems are: *" + "\n" + str(get_args(HousesSystemIdentifier)))
335
336        self.houses_system_name = swe.house_name(self.houses_system_identifier.encode('ascii'))
337        # <--- House System check and setup
338
339        # Zodiac Type and Sidereal mode checks and setup --->
340        if zodiac_type and not zodiac_type in get_args(ZodiacType):
341            raise KerykeionException(f"\n* ERROR: '{zodiac_type}' is NOT a valid zodiac type! Available types are: *" + "\n" + str(get_args(ZodiacType)))
342
343        if self.sidereal_mode and self.zodiac_type == "Tropic":
344            raise KerykeionException("You can't set a sidereal mode with a Tropic zodiac type!")
345
346        if self.zodiac_type == "Sidereal" and not self.sidereal_mode:
347            self.sidereal_mode = DEFAULT_SIDEREAL_MODE
348            logging.info("No sidereal mode set, using default FAGAN_BRADLEY")
349
350        if self.zodiac_type == "Sidereal":
351            # Check if the sidereal mode is valid
352
353            if not self.sidereal_mode or not self.sidereal_mode in get_args(SiderealMode):
354                raise KerykeionException(f"\n* ERROR: '{self.sidereal_mode}' is NOT a valid sidereal mode! Available modes are: *" + "\n" + str(get_args(SiderealMode)))
355
356            self._iflag += swe.FLG_SIDEREAL
357            mode = "SIDM_" + self.sidereal_mode
358            swe.set_sid_mode(getattr(swe, mode))
359            logging.debug(f"Using sidereal mode: {mode}")
360        # <--- Zodiac Type and Sidereal mode checks and setup
361
362        #------------------------#
363        # Start the calculations #
364        #------------------------#
365
366        # UTC, julian day and local time setup --->
367        if (self.online) and (not self.tz_str) and (not self.lat) and (not self.lng):
368            self._fetch_and_set_tz_and_coordinates_from_geonames()
369
370        self.lat = check_and_adjust_polar_latitude(self.lat)
371
372        # Local time to UTC
373        local_time = pytz.timezone(self.tz_str)
374        naive_datetime = datetime(self.year, self.month, self.day, self.hour, self.minute, 0)
375
376        try:
377            local_datetime = local_time.localize(naive_datetime, is_dst=self.is_dst)
378        except pytz.exceptions.AmbiguousTimeError:
379            raise KerykeionException("Ambiguous time! Please specify if the time is in DST or not with the is_dst argument.")
380
381        utc_object = local_datetime.astimezone(pytz.utc)
382        self.iso_formatted_utc_datetime = utc_object.isoformat()
383
384        # ISO formatted local datetime
385        self.iso_formatted_local_datetime = local_datetime.isoformat()
386
387        # Julian day calculation
388        utc_float_hour_with_minutes = utc_object.hour + (utc_object.minute / 60)
389        self.julian_day = float(swe.julday(utc_object.year, utc_object.month, utc_object.day, utc_float_hour_with_minutes))
390        # <--- UTC, julian day and local time setup
391
392        # Planets and Houses setup
393        self._initialize_houses()
394        self._initialize_planets()
395
396        # Lunar Phase
397        self.lunar_phase = calculate_moon_phase(
398            self.moon.abs_pos,
399            self.sun.abs_pos
400        )
401
402        # Deprecated properties
403        self.utc_time
404        self.local_time
405
406    def __str__(self) -> str:
407        return f"Astrological data for: {self.name}, {self.iso_formatted_utc_datetime} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}"
408
409    def __repr__(self) -> str:
410        return f"Astrological data for: {self.name}, {self.iso_formatted_utc_datetime} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}"
411
412    def __getitem__(self, item):
413        return getattr(self, item)
414
415    def get(self, item, default=None):
416        return getattr(self, item, default)
417
418    def _fetch_and_set_tz_and_coordinates_from_geonames(self) -> None:
419        """Gets the nearest time zone for the calculation"""
420        logging.info("Fetching timezone/coordinates from geonames")
421
422        geonames = FetchGeonames(
423            self.city,
424            self.nation,
425            username=self.geonames_username,
426            cache_expire_after_days=self.cache_expire_after_days
427        )
428        self.city_data: dict[str, str] = geonames.get_serialized_data()
429
430        if (
431            not "countryCode" in self.city_data
432            or not "timezonestr" in self.city_data
433            or not "lat" in self.city_data
434            or not "lng" in self.city_data
435        ):
436            raise KerykeionException("No data found for this city, try again! Maybe check your connection?")
437
438        self.nation = self.city_data["countryCode"]
439        self.lng = float(self.city_data["lng"])
440        self.lat = float(self.city_data["lat"])
441        self.tz_str = self.city_data["timezonestr"]
442
443    def _initialize_houses(self) -> None:
444        """
445        Calculate positions and store them in dictionaries
446
447        https://www.astro.com/faq/fq_fh_owhouse_e.htm
448        https://github.com/jwmatthys/pd-swisseph/blob/master/swehouse.c#L685
449        hsys = letter code for house system;
450            A  equal
451            E  equal
452            B  Alcabitius
453            C  Campanus
454            D  equal (MC)
455            F  Carter "Poli-Equatorial"
456            G  36 Gauquelin sectors
457            H  horizon / azimut
458            I  Sunshine solution Treindl
459            i  Sunshine solution Makransky
460            K  Koch
461            L  Pullen SD "sinusoidal delta", ex Neo-Porphyry
462            M  Morinus
463            N  equal/1=Aries
464            O  Porphyry
465            P  Placidus
466            Q  Pullen SR "sinusoidal ratio"
467            R  Regiomontanus
468            S  Sripati
469            T  Polich/Page ("topocentric")
470            U  Krusinski-Pisa-Goelzer
471            V  equal Vehlow
472            W  equal, whole sign
473            X  axial rotation system/ Meridian houses
474            Y  APC houses
475        """
476
477        _ascmc = (-1.0, -1.0)
478
479        if self.zodiac_type == "Sidereal":
480            cusps, ascmc = swe.houses_ex(
481                tjdut=self.julian_day,
482                lat=self.lat, lon=self.lng,
483                hsys=str.encode(self.houses_system_identifier),
484                flags=swe.FLG_SIDEREAL
485            )
486            self._houses_degree_ut = cusps
487            _ascmc = ascmc
488
489        elif self.zodiac_type == "Tropic":
490            cusps, ascmc = swe.houses(
491                tjdut=self.julian_day, lat=self.lat,
492                lon=self.lng,
493                hsys=str.encode(self.houses_system_identifier)
494            )
495            self._houses_degree_ut = cusps
496            _ascmc = ascmc
497
498        else:
499            raise KerykeionException("Not a valid zodiac type: " + self.zodiac_type)
500
501        point_type: PointType = "House"
502
503        # stores the house in singular dictionaries.
504        self.first_house = get_kerykeion_point_from_degree(self._houses_degree_ut[0], "First_House", point_type=point_type)
505        self.second_house = get_kerykeion_point_from_degree(self._houses_degree_ut[1], "Second_House", point_type=point_type)
506        self.third_house = get_kerykeion_point_from_degree(self._houses_degree_ut[2], "Third_House", point_type=point_type)
507        self.fourth_house = get_kerykeion_point_from_degree(self._houses_degree_ut[3], "Fourth_House", point_type=point_type)
508        self.fifth_house = get_kerykeion_point_from_degree(self._houses_degree_ut[4], "Fifth_House", point_type=point_type)
509        self.sixth_house = get_kerykeion_point_from_degree(self._houses_degree_ut[5], "Sixth_House", point_type=point_type)
510        self.seventh_house = get_kerykeion_point_from_degree(self._houses_degree_ut[6], "Seventh_House", point_type=point_type)
511        self.eighth_house = get_kerykeion_point_from_degree(self._houses_degree_ut[7], "Eighth_House", point_type=point_type)
512        self.ninth_house = get_kerykeion_point_from_degree(self._houses_degree_ut[8], "Ninth_House", point_type=point_type)
513        self.tenth_house = get_kerykeion_point_from_degree(self._houses_degree_ut[9], "Tenth_House", point_type=point_type)
514        self.eleventh_house = get_kerykeion_point_from_degree(self._houses_degree_ut[10], "Eleventh_House", point_type=point_type)
515        self.twelfth_house = get_kerykeion_point_from_degree(self._houses_degree_ut[11], "Twelfth_House", point_type=point_type)
516
517        self.houses_names_list = list(get_args(Houses))
518
519        # Deprecated
520        self._houses_list = [
521            self.first_house,
522            self.second_house,
523            self.third_house,
524            self.fourth_house,
525            self.fifth_house,
526            self.sixth_house,
527            self.seventh_house,
528            self.eighth_house,
529            self.ninth_house,
530            self.tenth_house,
531            self.eleventh_house,
532            self.twelfth_house,
533        ]
534
535        # AxialCusps
536        point_type: PointType = "AxialCusps"
537
538        # Calculate ascendant and medium coeli
539        self.ascendant = get_kerykeion_point_from_degree(_ascmc[0], "Ascendant", point_type=point_type)
540        self.medium_coeli = get_kerykeion_point_from_degree(_ascmc[1], "Medium_Coeli", point_type=point_type)
541        # For descendant and imum coeli there exist no Swiss Ephemeris library calculation function,
542        # but they are simply opposite the the ascendant and medium coeli
543        dsc_deg = math.fmod(_ascmc[0] + 180, 360)
544        ic_deg = math.fmod(_ascmc[1] + 180, 360)
545        self.descendant = get_kerykeion_point_from_degree(dsc_deg, "Descendant", point_type=point_type)
546        self.imum_coeli = get_kerykeion_point_from_degree(ic_deg, "Imum_Coeli", point_type=point_type)
547
548    def _initialize_planets(self) -> None:
549        """Defines body positon in signs and information and
550        stores them in dictionaries"""
551
552        point_type: PointType = "Planet"
553
554        sun_deg = swe.calc(self.julian_day, 0, self._iflag)[0][0]
555        moon_deg = swe.calc(self.julian_day, 1, self._iflag)[0][0]
556        mercury_deg = swe.calc(self.julian_day, 2, self._iflag)[0][0]
557        venus_deg = swe.calc(self.julian_day, 3, self._iflag)[0][0]
558        mars_deg = swe.calc(self.julian_day, 4, self._iflag)[0][0]
559        jupiter_deg = swe.calc(self.julian_day, 5, self._iflag)[0][0]
560        saturn_deg = swe.calc(self.julian_day, 6, self._iflag)[0][0]
561        uranus_deg = swe.calc(self.julian_day, 7, self._iflag)[0][0]
562        neptune_deg = swe.calc(self.julian_day, 8, self._iflag)[0][0]
563        pluto_deg = swe.calc(self.julian_day, 9, self._iflag)[0][0]
564        mean_node_deg = swe.calc(self.julian_day, 10, self._iflag)[0][0]
565        true_node_deg = swe.calc(self.julian_day, 11, self._iflag)[0][0]
566        # For south nodes there exist no Swiss Ephemeris library calculation function,
567        # but they are simply opposite the north node.
568        mean_south_node_deg = math.fmod(mean_node_deg + 180, 360)
569        true_south_node_deg = math.fmod(true_node_deg + 180, 360)
570
571        # AC/DC axis and MC/IC axis were already calculated previously...
572
573        self.sun = get_kerykeion_point_from_degree(sun_deg, "Sun", point_type=point_type)
574        self.moon = get_kerykeion_point_from_degree(moon_deg, "Moon", point_type=point_type)
575        self.mercury = get_kerykeion_point_from_degree(mercury_deg, "Mercury", point_type=point_type)
576        self.venus = get_kerykeion_point_from_degree(venus_deg, "Venus", point_type=point_type)
577        self.mars = get_kerykeion_point_from_degree(mars_deg, "Mars", point_type=point_type)
578        self.jupiter = get_kerykeion_point_from_degree(jupiter_deg, "Jupiter", point_type=point_type)
579        self.saturn = get_kerykeion_point_from_degree(saturn_deg, "Saturn", point_type=point_type)
580        self.uranus = get_kerykeion_point_from_degree(uranus_deg, "Uranus", point_type=point_type)
581        self.neptune = get_kerykeion_point_from_degree(neptune_deg, "Neptune", point_type=point_type)
582        self.pluto = get_kerykeion_point_from_degree(pluto_deg, "Pluto", point_type=point_type)
583        self.mean_node = get_kerykeion_point_from_degree(mean_node_deg, "Mean_Node", point_type=point_type)
584        self.true_node = get_kerykeion_point_from_degree(true_node_deg, "True_Node", point_type=point_type)
585        self.mean_south_node = get_kerykeion_point_from_degree(mean_south_node_deg, "Mean_South_Node", point_type=point_type)
586        self.true_south_node = get_kerykeion_point_from_degree(true_south_node_deg, "True_South_Node", point_type=point_type)
587
588        # Note that in whole-sign house systems ac/dc or mc/ic axes may not align with house cusps.
589        # Therefore, for the axes we need to calculate house positions explicitly too.
590        self.ascendant.house = get_planet_house(self.ascendant.abs_pos, self._houses_degree_ut)
591        self.descendant.house = get_planet_house(self.descendant.abs_pos, self._houses_degree_ut)
592        self.medium_coeli.house = get_planet_house(self.medium_coeli.abs_pos, self._houses_degree_ut)
593        self.imum_coeli.house = get_planet_house(self.imum_coeli.abs_pos, self._houses_degree_ut)
594
595        self.sun.house = get_planet_house(sun_deg, self._houses_degree_ut)
596        self.moon.house = get_planet_house(moon_deg, self._houses_degree_ut)
597        self.mercury.house = get_planet_house(mercury_deg, self._houses_degree_ut)
598        self.venus.house = get_planet_house(venus_deg, self._houses_degree_ut)
599        self.mars.house = get_planet_house(mars_deg, self._houses_degree_ut)
600        self.jupiter.house = get_planet_house(jupiter_deg, self._houses_degree_ut)
601        self.saturn.house = get_planet_house(saturn_deg, self._houses_degree_ut)
602        self.uranus.house = get_planet_house(uranus_deg, self._houses_degree_ut)
603        self.neptune.house = get_planet_house(neptune_deg, self._houses_degree_ut)
604        self.pluto.house = get_planet_house(pluto_deg, self._houses_degree_ut)
605        self.mean_node.house = get_planet_house(mean_node_deg, self._houses_degree_ut)
606        self.true_node.house = get_planet_house(true_node_deg, self._houses_degree_ut)
607        self.mean_south_node.house = get_planet_house(mean_south_node_deg, self._houses_degree_ut)
608        self.true_south_node.house = get_planet_house(true_south_node_deg, self._houses_degree_ut)
609
610
611        # Deprecated
612        planets_list = [
613            self.sun,
614            self.moon,
615            self.mercury,
616            self.venus,
617            self.mars,
618            self.jupiter,
619            self.saturn,
620            self.uranus,
621            self.neptune,
622            self.pluto,
623            self.mean_node,
624            self.true_node,
625            self.mean_south_node,
626            self.true_south_node,
627        ]
628
629        if not self.disable_chiron_and_lilith:
630            chiron_deg = swe.calc(self.julian_day, 15, self._iflag)[0][0]
631            mean_lilith_deg = swe.calc(self.julian_day, 12, self._iflag)[0][0]
632
633            self.chiron = get_kerykeion_point_from_degree(chiron_deg, "Chiron", point_type=point_type)
634            self.mean_lilith = get_kerykeion_point_from_degree(mean_lilith_deg, "Mean_Lilith", point_type=point_type)
635
636            self.chiron.house = get_planet_house(chiron_deg, self._houses_degree_ut)
637            self.mean_lilith.house = get_planet_house(mean_lilith_deg, self._houses_degree_ut)
638
639            # Deprecated
640            planets_list.append(self.chiron)
641            planets_list.append(self.mean_lilith)
642
643        else:
644            self.chiron = None
645            self.mean_lilith = None
646
647        # FIXME: Update after removing planets_list
648        self.planets_names_list = [planet["name"] for planet in planets_list]
649        self.axial_cusps_names_list = [
650            axis["name"] for axis in [self.ascendant, self.descendant, self.medium_coeli, self.imum_coeli]
651        ]
652
653        # Check in retrograde or not:
654        for planet in planets_list:
655            planet_number = get_number_from_name(planet["name"])
656
657            # Swiss ephemeris library does not offer calculation of direction of south nodes.
658            # But south nodes have same direction as north nodes. We can use those to calculate direction.
659            if planet_number == 1000:   # Number of Mean South Node
660                planet_number = 10      # Number of Mean North Node
661            elif planet_number == 1100: # Number of True South Node
662                planet_number = 11      # Number of True North Node
663
664
665            if swe.calc(self.julian_day, planet_number, self._iflag)[0][3] < 0:
666                planet["retrograde"] = True
667            else:
668                planet["retrograde"] = False
669
670        # AC/DC and MC/IC axes are never retrograde. For consistency, set them to be not retrograde.
671        self.ascendant.retrograde = False
672        self.descendant.retrograde = False
673        self.medium_coeli.retrograde = False
674        self.imum_coeli.retrograde = False
675
676
677    def json(self, dump=False, destination_folder: Union[str, None] = None, indent: Union[int, None] = None) -> str:
678        """
679        Dumps the Kerykeion object to a json string foramt,
680        if dump=True also dumps to file located in destination
681        or the home folder.
682        """
683
684        KrData = AstrologicalSubjectModel(**self.__dict__)
685        json_string = KrData.model_dump_json(exclude_none=True, indent=indent)
686
687        if dump:
688            if destination_folder:
689                destination_path = Path(destination_folder)
690                json_path = destination_path / f"{self.name}_kerykeion.json"
691
692            else:
693                json_path = self.json_dir / f"{self.name}_kerykeion.json"
694
695            with open(json_path, "w", encoding="utf-8") as file:
696                file.write(json_string)
697                logging.info(f"JSON file dumped in {json_path}.")
698
699        return json_string
700
701    def model(self) -> AstrologicalSubjectModel:
702        """
703        Creates a Pydantic model of the Kerykeion object.
704        """
705
706        return AstrologicalSubjectModel(**self.__dict__)
707
708    @cached_property
709    def utc_time(self) -> float:
710        """
711        Deprecated property, use iso_formatted_utc_datetime instead, will be removed in the future.
712        Returns the UTC time as a float.
713        """
714        dt = datetime.fromisoformat(self.iso_formatted_utc_datetime)
715
716        # Extract the hours, minutes, and seconds
717        hours = dt.hour
718        minutes = dt.minute
719        seconds = dt.second + dt.microsecond / 1_000_000
720
721        # Convert time to float hours
722        float_time = hours + minutes / 60 + seconds / 3600
723
724        return float_time
725
726    @cached_property
727    def local_time(self) -> float:
728        """
729        Deprecated property, use iso_formatted_local_datetime instead, will be removed in the future.
730        Returns the local time as a float.
731        """
732        dt = datetime.fromisoformat(self.iso_formatted_local_datetime)
733
734        # Extract the hours, minutes, and seconds
735        hours = dt.hour
736        minutes = dt.minute
737        seconds = dt.second + dt.microsecond / 1_000_000
738
739        # Convert time to float hours
740        float_time = hours + minutes / 60 + seconds / 3600
741
742        return float_time
743
744
745    @staticmethod
746    def get_from_iso_utc_time(
747        name: str,
748        iso_utc_time: str,
749        city: str = "Greenwich",
750        nation: str = "GB",
751        tz_str: str = "Etc/GMT",
752        online: bool = False,
753        lng: Union[int, float] = 0,
754        lat: Union[int, float] = 51.5074,
755        geonames_username: str = DEFAULT_GEONAMES_USERNAME,
756        zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE,
757        disable_chiron_and_lilith: bool = False,
758        sidereal_mode: Union[SiderealMode, None] = None,
759        houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
760        perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE
761
762    ) -> "AstrologicalSubject":
763        """
764        Creates an AstrologicalSubject object from an iso formatted UTC time.
765        This method is offline by default, set online=True to fetch the timezone and coordinates from geonames.
766
767        Args:
768        - name (str): The name of the subject.
769        - iso_utc_time (str): The iso formatted UTC time.
770        - city (str, optional): City or location of birth. Defaults to "Greenwich".
771        - nation (str, optional): Nation of birth. Defaults to "GB".
772        - tz_str (str, optional): Timezone of the birth location. Defaults to "Etc/GMT".
773        - online (bool, optional): Sets if you want to use the online mode, which fetches the timezone and coordinates from geonames.
774            If you already have the coordinates and timezone, set this to False. Defaults to False.
775        - lng (Union[int, float], optional): Longitude of the birth location. Defaults to 0 (Greenwich, London).
776        - lat (Union[int, float], optional): Latitude of the birth location. Defaults to 51.5074 (Greenwich, London).
777        - geonames_username (str, optional): The username for the geonames API. Note: Change this to your own username to avoid rate limits!
778            You can get one for free here: https://www.geonames.org/login
779        - zodiac_type (ZodiacType, optional): The zodiac type to use. Defaults to "Tropic".
780        - disable_chiron_and_lilith: boolean representing if Chiron and Lilith should be disabled. Default is False.
781            Chiron calculation can create some issues with the Swiss Ephemeris when the date is too far in the past.
782        - sidereal_mode (SiderealMode, optional): Also known as Ayanamsa.
783            The mode to use for the sidereal zodiac, according to the Swiss Ephemeris.
784            Defaults to None.
785            Available modes are visible in the SiderealMode Literal.
786        - houses_system_identifier (HousesSystemIdentifier, optional): The system to use for the calculation of the houses.
787            Defaults to "P" (Placidus).
788            Available systems are visible in the HousesSystemIdentifier Literal.
789        - perspective_type (PerspectiveType, optional): The perspective to use for the calculation of the chart.
790            Defaults to "Apparent Geocentric".
791
792        Returns:
793        - AstrologicalSubject: The AstrologicalSubject object.
794        """
795        dt = datetime.fromisoformat(iso_utc_time)
796
797        if online == True:
798            if geonames_username == DEFAULT_GEONAMES_USERNAME:
799                logging.warning(GEONAMES_DEFAULT_USERNAME_WARNING)
800
801            geonames = FetchGeonames(
802                city,
803                nation,
804                username=geonames_username,
805            )
806
807            city_data: dict[str, str] = geonames.get_serialized_data()
808            lng = float(city_data["lng"])
809            lat = float(city_data["lat"])
810
811        subject = AstrologicalSubject(
812            name=name,
813            year=dt.year,
814            month=dt.month,
815            day=dt.day,
816            hour=dt.hour,
817            minute=dt.minute,
818            city=city,
819            nation=city,
820            lng=lng,
821            lat=lat,
822            tz_str=tz_str,
823            online=False,
824            geonames_username=geonames_username,
825            zodiac_type=zodiac_type,
826            sidereal_mode=sidereal_mode,
827            houses_system_identifier=houses_system_identifier,
828            perspective_type=perspective_type,
829            disable_chiron_and_lilith=disable_chiron_and_lilith
830        )
831
832        return subject

Calculates all the astrological information, the coordinates, it's utc and julian day and returns an object with all that data.

Args:

  • name (str, optional): The name of the subject. Defaults to "Now".
  • year (int, optional): The year of birth. Defaults to the current year.
  • month (int, optional): The month of birth. Defaults to the current month.
  • day (int, optional): The day of birth. Defaults to the current day.
  • hour (int, optional): The hour of birth. Defaults to the current hour.
  • minute (int, optional): Defaults to the current minute.
  • city (str, optional): City or location of birth. Defaults to "London", which is GMT time. The city argument is used to get the coordinates and timezone from geonames just in case you don't insert them manually (see _get_tz). If you insert the coordinates and timezone manually, the city argument is not used for calculations but it's still used as a value for the city attribute.
  • nat (str, optional): _ Defaults to "".
  • lng (Union[int, float], optional): Longitude of the birth location. Defaults to 0 (Greenwich, London).
  • lat (Union[int, float], optional): Latitude of the birth location. Defaults to 51.5074 (Greenwich, London).
  • tz_str (Union[str, bool], optional): Timezone of the birth location. Defaults to "GMT".
  • geonames_username (str, optional): The username for the geonames API. Note: Change this to your own username to avoid rate limits! You can get one for free here: https://www.geonames.org/login
  • online (bool, optional): Sets if you want to use the online mode, which fetches the timezone and coordinates from geonames. If you already have the coordinates and timezone, set this to False. Defaults to True.
  • disable_chiron: Deprecated, use disable_chiron_and_lilith instead.
  • sidereal_mode (SiderealMode, optional): Also known as Ayanamsa. The mode to use for the sidereal zodiac, according to the Swiss Ephemeris. Defaults to "FAGAN_BRADLEY". Available modes are visible in the SiderealMode Literal.
  • houses_system_identifier (HousesSystemIdentifier, optional): The system to use for the calculation of the houses. Defaults to "P" (Placidus). Available systems are visible in the HousesSystemIdentifier Literal.
  • perspective_type (PerspectiveType, optional): The perspective to use for the calculation of the chart. Defaults to "Apparent Geocentric". Available perspectives are visible in the PerspectiveType Literal.
  • cache_expire_after_days (int, optional): The number of days after which the geonames cache will expire. Defaults to 30.
  • is_dst (Union[None, bool], optional): Specify if the time is in DST. Defaults to None. By default (None), the library will try to guess if the time is in DST or not and raise an AmbiguousTimeError if it can't guess. If you know the time is in DST, set this to True, if you know it's not, set it to False.
  • disable_chiron_and_lilith (bool, optional): boolean representing if Chiron and Lilith should be disabled. Default is False. Chiron calculation can create some issues with the Swiss Ephemeris when the date is too far in the past.
AstrologicalSubject( name='Now', year: int = 2025, month: int = 3, day: int = 29, hour: int = 17, minute: int = 13, city: Optional[str] = None, nation: Optional[str] = None, lng: Union[int, float, NoneType] = None, lat: Union[int, float, NoneType] = None, tz_str: Optional[str] = None, geonames_username: Optional[str] = None, zodiac_type: Optional[Literal['Tropic', 'Sidereal']] = 'Tropic', online: bool = True, disable_chiron: Optional[bool] = None, sidereal_mode: Optional[Literal['FAGAN_BRADLEY', 'LAHIRI', 'DELUCE', 'RAMAN', 'USHASHASHI', 'KRISHNAMURTI', 'DJWHAL_KHUL', 'YUKTESHWAR', 'JN_BHASIN', 'BABYL_KUGLER1', 'BABYL_KUGLER2', 'BABYL_KUGLER3', 'BABYL_HUBER', 'BABYL_ETPSC', 'ALDEBARAN_15TAU', 'HIPPARCHOS', 'SASSANIAN', 'J2000', 'J1900', 'B1950']] = None, houses_system_identifier: Optional[Literal['A', 'B', 'C', 'D', 'F', 'H', 'I', 'i', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y']] = 'P', perspective_type: Optional[Literal['Apparent Geocentric', 'Heliocentric', 'Topocentric', 'True Geocentric']] = 'Apparent Geocentric', cache_expire_after_days: Optional[int] = 30, is_dst: Optional[bool] = None, disable_chiron_and_lilith: bool = False)
183    def __init__(
184        self,
185        name="Now",
186        year: int = NOW.year,
187        month: int = NOW.month,
188        day: int = NOW.day,
189        hour: int = NOW.hour,
190        minute: int = NOW.minute,
191        city: Union[str, None] = None,
192        nation: Union[str, None] = None,
193        lng: Union[int, float, None] = None,
194        lat: Union[int, float, None] = None,
195        tz_str: Union[str, None] = None,
196        geonames_username: Union[str, None] = None,
197        zodiac_type: Union[ZodiacType, None] = DEFAULT_ZODIAC_TYPE,
198        online: bool = True,
199        disable_chiron: Union[None, bool] = None, # Deprecated
200        sidereal_mode: Union[SiderealMode, None] = None,
201        houses_system_identifier: Union[HousesSystemIdentifier, None] = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
202        perspective_type: Union[PerspectiveType, None] = DEFAULT_PERSPECTIVE_TYPE,
203        cache_expire_after_days: Union[int, None] = DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS,
204        is_dst: Union[None, bool] = None,
205        disable_chiron_and_lilith: bool = False
206    ) -> None:
207        logging.debug("Starting Kerykeion")
208
209        # Deprecation warnings --->
210        if disable_chiron is not None:
211            warnings.warn(
212                "The 'disable_chiron' argument is deprecated and will be removed in a future version. "
213                "Please use 'disable_chiron' instead.",
214                DeprecationWarning
215            )
216
217            if disable_chiron_and_lilith:
218                raise ValueError("Cannot specify both 'disable_chiron' and 'disable_chiron_and_lilith'. Use 'disable_chiron_and_lilith' only.")
219
220            self.disable_chiron_and_lilith = disable_chiron
221        # <--- Deprecation warnings
222
223        self.name = name
224        self.year = year
225        self.month = month
226        self.day = day
227        self.hour = hour
228        self.minute = minute
229        self.online = online
230        self.json_dir = Path.home()
231        self.disable_chiron = disable_chiron
232        self.sidereal_mode = sidereal_mode
233        self.cache_expire_after_days = cache_expire_after_days
234        self.is_dst = is_dst
235        self.disable_chiron_and_lilith = disable_chiron_and_lilith
236
237        #---------------#
238        # General setup #
239        #---------------#
240
241        # Geonames username
242        if geonames_username is None and online and (not lat or not lng or not tz_str):
243            logging.warning(GEONAMES_DEFAULT_USERNAME_WARNING)
244            self.geonames_username = DEFAULT_GEONAMES_USERNAME
245        else:
246            self.geonames_username = geonames_username # type: ignore
247
248        # City
249        if not city:
250            self.city = "London"
251            logging.info("No city specified, using London as default")
252        else:
253            self.city = city
254
255        # Nation
256        if not nation:
257            self.nation = "GB"
258            logging.info("No nation specified, using GB as default")
259        else:
260            self.nation = nation
261
262        # Latitude
263        if not lat and not self.online:
264            self.lat = 51.5074
265            logging.info("No latitude specified, using London as default")
266        else:
267            self.lat = lat # type: ignore
268
269        # Longitude
270        if not lng and not self.online:
271            self.lng = 0
272            logging.info("No longitude specified, using London as default")
273        else:
274            self.lng = lng # type: ignore
275
276        # Timezone
277        if (not self.online) and (not tz_str):
278            raise KerykeionException("You need to set the coordinates and timezone if you want to use the offline mode!")
279        else:
280            self.tz_str = tz_str # type: ignore
281
282        # Zodiac type
283        if not zodiac_type:
284            self.zodiac_type = DEFAULT_ZODIAC_TYPE
285        else:
286            self.zodiac_type = zodiac_type
287
288        # Perspective type
289        if not perspective_type:
290            self.perspective_type = DEFAULT_PERSPECTIVE_TYPE
291        else:
292            self.perspective_type = perspective_type
293
294        # Houses system identifier
295        if not houses_system_identifier:
296            self.houses_system_identifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER
297        else:
298            self.houses_system_identifier = houses_system_identifier
299
300        # Cache expire after days
301        if not cache_expire_after_days:
302            self.cache_expire_after_days = DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS
303        else:
304            self.cache_expire_after_days = cache_expire_after_days
305
306        #-----------------------#
307        # Swiss Ephemeris setup #
308        #-----------------------#
309
310        # We set the swisseph path to the current directory
311        swe.set_ephe_path(str(Path(__file__).parent.absolute() / "sweph"))
312
313        # Flags for the Swiss Ephemeris
314        self._iflag = swe.FLG_SWIEPH + swe.FLG_SPEED
315
316        # Chart Perspective check and setup --->
317        if self.perspective_type not in get_args(PerspectiveType):
318            raise KerykeionException(f"\n* ERROR: '{self.perspective_type}' is NOT a valid chart perspective! Available perspectives are: *" + "\n" + str(get_args(PerspectiveType)))
319
320        if self.perspective_type == "True Geocentric":
321            self._iflag += swe.FLG_TRUEPOS
322        elif self.perspective_type == "Heliocentric":
323            self._iflag += swe.FLG_HELCTR
324        elif self.perspective_type == "Topocentric":
325            self._iflag += swe.FLG_TOPOCTR
326            # geopos_is_set, for topocentric
327            if (self.online) and (not self.tz_str) and (not self.lat) and (not self.lng):
328                self._fetch_and_set_tz_and_coordinates_from_geonames()
329            swe.set_topo(self.lng, self.lat, 0)
330        # <--- Chart Perspective check and setup
331
332        # House System check and setup --->
333        if self.houses_system_identifier not in get_args(HousesSystemIdentifier):
334            raise KerykeionException(f"\n* ERROR: '{self.houses_system_identifier}' is NOT a valid house system! Available systems are: *" + "\n" + str(get_args(HousesSystemIdentifier)))
335
336        self.houses_system_name = swe.house_name(self.houses_system_identifier.encode('ascii'))
337        # <--- House System check and setup
338
339        # Zodiac Type and Sidereal mode checks and setup --->
340        if zodiac_type and not zodiac_type in get_args(ZodiacType):
341            raise KerykeionException(f"\n* ERROR: '{zodiac_type}' is NOT a valid zodiac type! Available types are: *" + "\n" + str(get_args(ZodiacType)))
342
343        if self.sidereal_mode and self.zodiac_type == "Tropic":
344            raise KerykeionException("You can't set a sidereal mode with a Tropic zodiac type!")
345
346        if self.zodiac_type == "Sidereal" and not self.sidereal_mode:
347            self.sidereal_mode = DEFAULT_SIDEREAL_MODE
348            logging.info("No sidereal mode set, using default FAGAN_BRADLEY")
349
350        if self.zodiac_type == "Sidereal":
351            # Check if the sidereal mode is valid
352
353            if not self.sidereal_mode or not self.sidereal_mode in get_args(SiderealMode):
354                raise KerykeionException(f"\n* ERROR: '{self.sidereal_mode}' is NOT a valid sidereal mode! Available modes are: *" + "\n" + str(get_args(SiderealMode)))
355
356            self._iflag += swe.FLG_SIDEREAL
357            mode = "SIDM_" + self.sidereal_mode
358            swe.set_sid_mode(getattr(swe, mode))
359            logging.debug(f"Using sidereal mode: {mode}")
360        # <--- Zodiac Type and Sidereal mode checks and setup
361
362        #------------------------#
363        # Start the calculations #
364        #------------------------#
365
366        # UTC, julian day and local time setup --->
367        if (self.online) and (not self.tz_str) and (not self.lat) and (not self.lng):
368            self._fetch_and_set_tz_and_coordinates_from_geonames()
369
370        self.lat = check_and_adjust_polar_latitude(self.lat)
371
372        # Local time to UTC
373        local_time = pytz.timezone(self.tz_str)
374        naive_datetime = datetime(self.year, self.month, self.day, self.hour, self.minute, 0)
375
376        try:
377            local_datetime = local_time.localize(naive_datetime, is_dst=self.is_dst)
378        except pytz.exceptions.AmbiguousTimeError:
379            raise KerykeionException("Ambiguous time! Please specify if the time is in DST or not with the is_dst argument.")
380
381        utc_object = local_datetime.astimezone(pytz.utc)
382        self.iso_formatted_utc_datetime = utc_object.isoformat()
383
384        # ISO formatted local datetime
385        self.iso_formatted_local_datetime = local_datetime.isoformat()
386
387        # Julian day calculation
388        utc_float_hour_with_minutes = utc_object.hour + (utc_object.minute / 60)
389        self.julian_day = float(swe.julday(utc_object.year, utc_object.month, utc_object.day, utc_float_hour_with_minutes))
390        # <--- UTC, julian day and local time setup
391
392        # Planets and Houses setup
393        self._initialize_houses()
394        self._initialize_planets()
395
396        # Lunar Phase
397        self.lunar_phase = calculate_moon_phase(
398            self.moon.abs_pos,
399            self.sun.abs_pos
400        )
401
402        # Deprecated properties
403        self.utc_time
404        self.local_time
name: str
year: int
month: int
day: int
hour: int
minute: int
city: str
nation: str
lng: Union[int, float]
lat: Union[int, float]
tz_str: str
geonames_username: str
online: bool
zodiac_type: Literal['Tropic', 'Sidereal']
sidereal_mode: Optional[Literal['FAGAN_BRADLEY', 'LAHIRI', 'DELUCE', 'RAMAN', 'USHASHASHI', 'KRISHNAMURTI', 'DJWHAL_KHUL', 'YUKTESHWAR', 'JN_BHASIN', 'BABYL_KUGLER1', 'BABYL_KUGLER2', 'BABYL_KUGLER3', 'BABYL_HUBER', 'BABYL_ETPSC', 'ALDEBARAN_15TAU', 'HIPPARCHOS', 'SASSANIAN', 'J2000', 'J1900', 'B1950']]
houses_system_identifier: Literal['A', 'B', 'C', 'D', 'F', 'H', 'I', 'i', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y']
houses_system_name: str
perspective_type: Literal['Apparent Geocentric', 'Heliocentric', 'Topocentric', 'True Geocentric']
is_dst: Optional[bool]
city_data: dict[str, str]
julian_day: Union[int, float]
json_dir: pathlib.Path
iso_formatted_local_datetime: str
iso_formatted_utc_datetime: str
planets_names_list: list[typing.Literal['Sun', 'Moon', 'Mercury', 'Venus', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', 'Pluto', 'Mean_Node', 'True_Node', 'Mean_South_Node', 'True_South_Node', 'Chiron', 'Mean_Lilith']]
houses_names_list: list[typing.Literal['First_House', 'Second_House', 'Third_House', 'Fourth_House', 'Fifth_House', 'Sixth_House', 'Seventh_House', 'Eighth_House', 'Ninth_House', 'Tenth_House', 'Eleventh_House', 'Twelfth_House']]
axial_cusps_names_list: list[typing.Literal['Ascendant', 'Medium_Coeli', 'Descendant', 'Imum_Coeli']]
disable_chiron: Optional[bool]
disable_chiron_and_lilith: bool
cache_expire_after_days
def get(self, item, default=None):
415    def get(self, item, default=None):
416        return getattr(self, item, default)
def json( self, dump=False, destination_folder: Optional[str] = None, indent: Optional[int] = None) -> str:
677    def json(self, dump=False, destination_folder: Union[str, None] = None, indent: Union[int, None] = None) -> str:
678        """
679        Dumps the Kerykeion object to a json string foramt,
680        if dump=True also dumps to file located in destination
681        or the home folder.
682        """
683
684        KrData = AstrologicalSubjectModel(**self.__dict__)
685        json_string = KrData.model_dump_json(exclude_none=True, indent=indent)
686
687        if dump:
688            if destination_folder:
689                destination_path = Path(destination_folder)
690                json_path = destination_path / f"{self.name}_kerykeion.json"
691
692            else:
693                json_path = self.json_dir / f"{self.name}_kerykeion.json"
694
695            with open(json_path, "w", encoding="utf-8") as file:
696                file.write(json_string)
697                logging.info(f"JSON file dumped in {json_path}.")
698
699        return json_string

Dumps the Kerykeion object to a json string foramt, if dump=True also dumps to file located in destination or the home folder.

701    def model(self) -> AstrologicalSubjectModel:
702        """
703        Creates a Pydantic model of the Kerykeion object.
704        """
705
706        return AstrologicalSubjectModel(**self.__dict__)

Creates a Pydantic model of the Kerykeion object.

utc_time: float
708    @cached_property
709    def utc_time(self) -> float:
710        """
711        Deprecated property, use iso_formatted_utc_datetime instead, will be removed in the future.
712        Returns the UTC time as a float.
713        """
714        dt = datetime.fromisoformat(self.iso_formatted_utc_datetime)
715
716        # Extract the hours, minutes, and seconds
717        hours = dt.hour
718        minutes = dt.minute
719        seconds = dt.second + dt.microsecond / 1_000_000
720
721        # Convert time to float hours
722        float_time = hours + minutes / 60 + seconds / 3600
723
724        return float_time

Deprecated property, use iso_formatted_utc_datetime instead, will be removed in the future. Returns the UTC time as a float.

local_time: float
726    @cached_property
727    def local_time(self) -> float:
728        """
729        Deprecated property, use iso_formatted_local_datetime instead, will be removed in the future.
730        Returns the local time as a float.
731        """
732        dt = datetime.fromisoformat(self.iso_formatted_local_datetime)
733
734        # Extract the hours, minutes, and seconds
735        hours = dt.hour
736        minutes = dt.minute
737        seconds = dt.second + dt.microsecond / 1_000_000
738
739        # Convert time to float hours
740        float_time = hours + minutes / 60 + seconds / 3600
741
742        return float_time

Deprecated property, use iso_formatted_local_datetime instead, will be removed in the future. Returns the local time as a float.

@staticmethod
def get_from_iso_utc_time( name: str, iso_utc_time: str, city: str = 'Greenwich', nation: str = 'GB', tz_str: str = 'Etc/GMT', online: bool = False, lng: Union[int, float] = 0, lat: Union[int, float] = 51.5074, geonames_username: str = 'century.boy', zodiac_type: Literal['Tropic', 'Sidereal'] = 'Tropic', disable_chiron_and_lilith: bool = False, sidereal_mode: Optional[Literal['FAGAN_BRADLEY', 'LAHIRI', 'DELUCE', 'RAMAN', 'USHASHASHI', 'KRISHNAMURTI', 'DJWHAL_KHUL', 'YUKTESHWAR', 'JN_BHASIN', 'BABYL_KUGLER1', 'BABYL_KUGLER2', 'BABYL_KUGLER3', 'BABYL_HUBER', 'BABYL_ETPSC', 'ALDEBARAN_15TAU', 'HIPPARCHOS', 'SASSANIAN', 'J2000', 'J1900', 'B1950']] = None, houses_system_identifier: Literal['A', 'B', 'C', 'D', 'F', 'H', 'I', 'i', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y'] = 'P', perspective_type: Literal['Apparent Geocentric', 'Heliocentric', 'Topocentric', 'True Geocentric'] = 'Apparent Geocentric') -> AstrologicalSubject:
745    @staticmethod
746    def get_from_iso_utc_time(
747        name: str,
748        iso_utc_time: str,
749        city: str = "Greenwich",
750        nation: str = "GB",
751        tz_str: str = "Etc/GMT",
752        online: bool = False,
753        lng: Union[int, float] = 0,
754        lat: Union[int, float] = 51.5074,
755        geonames_username: str = DEFAULT_GEONAMES_USERNAME,
756        zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE,
757        disable_chiron_and_lilith: bool = False,
758        sidereal_mode: Union[SiderealMode, None] = None,
759        houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
760        perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE
761
762    ) -> "AstrologicalSubject":
763        """
764        Creates an AstrologicalSubject object from an iso formatted UTC time.
765        This method is offline by default, set online=True to fetch the timezone and coordinates from geonames.
766
767        Args:
768        - name (str): The name of the subject.
769        - iso_utc_time (str): The iso formatted UTC time.
770        - city (str, optional): City or location of birth. Defaults to "Greenwich".
771        - nation (str, optional): Nation of birth. Defaults to "GB".
772        - tz_str (str, optional): Timezone of the birth location. Defaults to "Etc/GMT".
773        - online (bool, optional): Sets if you want to use the online mode, which fetches the timezone and coordinates from geonames.
774            If you already have the coordinates and timezone, set this to False. Defaults to False.
775        - lng (Union[int, float], optional): Longitude of the birth location. Defaults to 0 (Greenwich, London).
776        - lat (Union[int, float], optional): Latitude of the birth location. Defaults to 51.5074 (Greenwich, London).
777        - geonames_username (str, optional): The username for the geonames API. Note: Change this to your own username to avoid rate limits!
778            You can get one for free here: https://www.geonames.org/login
779        - zodiac_type (ZodiacType, optional): The zodiac type to use. Defaults to "Tropic".
780        - disable_chiron_and_lilith: boolean representing if Chiron and Lilith should be disabled. Default is False.
781            Chiron calculation can create some issues with the Swiss Ephemeris when the date is too far in the past.
782        - sidereal_mode (SiderealMode, optional): Also known as Ayanamsa.
783            The mode to use for the sidereal zodiac, according to the Swiss Ephemeris.
784            Defaults to None.
785            Available modes are visible in the SiderealMode Literal.
786        - houses_system_identifier (HousesSystemIdentifier, optional): The system to use for the calculation of the houses.
787            Defaults to "P" (Placidus).
788            Available systems are visible in the HousesSystemIdentifier Literal.
789        - perspective_type (PerspectiveType, optional): The perspective to use for the calculation of the chart.
790            Defaults to "Apparent Geocentric".
791
792        Returns:
793        - AstrologicalSubject: The AstrologicalSubject object.
794        """
795        dt = datetime.fromisoformat(iso_utc_time)
796
797        if online == True:
798            if geonames_username == DEFAULT_GEONAMES_USERNAME:
799                logging.warning(GEONAMES_DEFAULT_USERNAME_WARNING)
800
801            geonames = FetchGeonames(
802                city,
803                nation,
804                username=geonames_username,
805            )
806
807            city_data: dict[str, str] = geonames.get_serialized_data()
808            lng = float(city_data["lng"])
809            lat = float(city_data["lat"])
810
811        subject = AstrologicalSubject(
812            name=name,
813            year=dt.year,
814            month=dt.month,
815            day=dt.day,
816            hour=dt.hour,
817            minute=dt.minute,
818            city=city,
819            nation=city,
820            lng=lng,
821            lat=lat,
822            tz_str=tz_str,
823            online=False,
824            geonames_username=geonames_username,
825            zodiac_type=zodiac_type,
826            sidereal_mode=sidereal_mode,
827            houses_system_identifier=houses_system_identifier,
828            perspective_type=perspective_type,
829            disable_chiron_and_lilith=disable_chiron_and_lilith
830        )
831
832        return subject

Creates an AstrologicalSubject object from an iso formatted UTC time. This method is offline by default, set online=True to fetch the timezone and coordinates from geonames.

Args:

  • name (str): The name of the subject.
  • iso_utc_time (str): The iso formatted UTC time.
  • city (str, optional): City or location of birth. Defaults to "Greenwich".
  • nation (str, optional): Nation of birth. Defaults to "GB".
  • tz_str (str, optional): Timezone of the birth location. Defaults to "Etc/GMT".
  • online (bool, optional): Sets if you want to use the online mode, which fetches the timezone and coordinates from geonames. If you already have the coordinates and timezone, set this to False. Defaults to False.
  • lng (Union[int, float], optional): Longitude of the birth location. Defaults to 0 (Greenwich, London).
  • lat (Union[int, float], optional): Latitude of the birth location. Defaults to 51.5074 (Greenwich, London).
  • geonames_username (str, optional): The username for the geonames API. Note: Change this to your own username to avoid rate limits! You can get one for free here: https://www.geonames.org/login
  • zodiac_type (ZodiacType, optional): The zodiac type to use. Defaults to "Tropic".
  • disable_chiron_and_lilith: boolean representing if Chiron and Lilith should be disabled. Default is False. Chiron calculation can create some issues with the Swiss Ephemeris when the date is too far in the past.
  • sidereal_mode (SiderealMode, optional): Also known as Ayanamsa. The mode to use for the sidereal zodiac, according to the Swiss Ephemeris. Defaults to None. Available modes are visible in the SiderealMode Literal.
  • houses_system_identifier (HousesSystemIdentifier, optional): The system to use for the calculation of the houses. Defaults to "P" (Placidus). Available systems are visible in the HousesSystemIdentifier Literal.
  • perspective_type (PerspectiveType, optional): The perspective to use for the calculation of the chart. Defaults to "Apparent Geocentric".

Returns:

  • AstrologicalSubject: The AstrologicalSubject object.