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