# kerykeion.astrological_subject

This is part of Kerykeion (C) 2025 Giacomo Battaglia

View Source

      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, 4, 20, 0, 23, 22, 259004)
class AstrologicalSubject: View Source

     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 = 4, day: int = 20, hour: int = 0, minute: int = 23, 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) View Source

    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._local.Path
iso_formatted_local_datetime: str
iso_formatted_utc_datetime: str
sun: kerykeion.kr_types.kr_models.KerykeionPointModel
moon: kerykeion.kr_types.kr_models.KerykeionPointModel
mercury: kerykeion.kr_types.kr_models.KerykeionPointModel
venus: kerykeion.kr_types.kr_models.KerykeionPointModel
mars: kerykeion.kr_types.kr_models.KerykeionPointModel
jupiter: kerykeion.kr_types.kr_models.KerykeionPointModel
saturn: kerykeion.kr_types.kr_models.KerykeionPointModel
uranus: kerykeion.kr_types.kr_models.KerykeionPointModel
neptune: kerykeion.kr_types.kr_models.KerykeionPointModel
pluto: kerykeion.kr_types.kr_models.KerykeionPointModel
true_node: kerykeion.kr_types.kr_models.KerykeionPointModel
mean_node: kerykeion.kr_types.kr_models.KerykeionPointModel
chiron: Optional[kerykeion.kr_types.kr_models.KerykeionPointModel]
mean_lilith: Optional[kerykeion.kr_types.kr_models.KerykeionPointModel]
true_south_node: kerykeion.kr_types.kr_models.KerykeionPointModel
mean_south_node: kerykeion.kr_types.kr_models.KerykeionPointModel
asc: kerykeion.kr_types.kr_models.KerykeionPointModel
dsc: kerykeion.kr_types.kr_models.KerykeionPointModel
mc: kerykeion.kr_types.kr_models.KerykeionPointModel
ic: kerykeion.kr_types.kr_models.KerykeionPointModel
first_house: kerykeion.kr_types.kr_models.KerykeionPointModel
second_house: kerykeion.kr_types.kr_models.KerykeionPointModel
third_house: kerykeion.kr_types.kr_models.KerykeionPointModel
fourth_house: kerykeion.kr_types.kr_models.KerykeionPointModel
fifth_house: kerykeion.kr_types.kr_models.KerykeionPointModel
sixth_house: kerykeion.kr_types.kr_models.KerykeionPointModel
seventh_house: kerykeion.kr_types.kr_models.KerykeionPointModel
eighth_house: kerykeion.kr_types.kr_models.KerykeionPointModel
ninth_house: kerykeion.kr_types.kr_models.KerykeionPointModel
tenth_house: kerykeion.kr_types.kr_models.KerykeionPointModel
eleventh_house: kerykeion.kr_types.kr_models.KerykeionPointModel
twelfth_house: kerykeion.kr_types.kr_models.KerykeionPointModel
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
lunar_phase: kerykeion.kr_types.kr_models.LunarPhaseModel
cache_expire_after_days
def get(self, item, default=None): View Source

    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: View Source

    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.

def model(self) -> kerykeion.kr_types.kr_models.AstrologicalSubjectModel: View Source

    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 View Source

    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 View Source

    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: View Source

    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.