kerykeion.astrological_subject
This is part of Kerykeion (C) 2024 Giacomo Battaglia
1# -*- coding: utf-8 -*- 2""" 3 This is part of Kerykeion (C) 2024 Giacomo Battaglia 4""" 5 6import pytz 7import swisseph as swe 8import logging 9 10from datetime import datetime 11from kerykeion.fetch_geonames import FetchGeonames 12from kerykeion.kr_types import ( 13 KerykeionException, 14 ZodiacType, 15 AstrologicalSubjectModel, 16 LunarPhaseModel, 17 KerykeionPointModel, 18 PointType, 19 SiderealMode, 20 HousesSystemIdentifier, 21 PerspectiveType 22) 23from kerykeion.utilities import ( 24 get_number_from_name, 25 calculate_position, 26 get_planet_house, 27 get_moon_emoji_from_phase_int, 28 get_moon_phase_name_from_phase_int, 29 check_and_adjust_polar_latitude 30) 31from pathlib import Path 32from typing import Union, get_args 33 34DEFAULT_GEONAMES_USERNAME = "century.boy" 35DEFAULT_SIDEREAL_MODE = "FAGAN_BRADLEY" 36DEFAULT_HOUSES_SYSTEM = "P" 37PERSPECTIVE_TYPE = "Apparent Geocentric" 38NOW = datetime.now() 39 40 41class AstrologicalSubject: 42 """ 43 Calculates all the astrological information, the coordinates, 44 it's utc and julian day and returns an object with all that data. 45 46 Args: 47 - name (str, optional): The name of the subject. Defaults to "Now". 48 - year (int, optional): The year of birth. Defaults to the current year. 49 - month (int, optional): The month of birth. Defaults to the current month. 50 - day (int, optional): The day of birth. Defaults to the current day. 51 - hour (int, optional): The hour of birth. Defaults to the current hour. 52 - minute (int, optional): Defaults to the current minute. 53 - city (str, optional): City or location of birth. Defaults to "London", which is GMT time. 54 The city argument is used to get the coordinates and timezone from geonames just in case 55 you don't insert them manually (see _get_tz). 56 If you insert the coordinates and timezone manually, the city argument is not used for calculations 57 but it's still used as a value for the city attribute. 58 - nat (str, optional): _ Defaults to "". 59 - lng (Union[int, float], optional): Longitude of the birth location. Defaults to 0 (Greenwich, London). 60 - lat (Union[int, float], optional): Latitude of the birth location. Defaults to 51.5074 (Greenwich, London). 61 - tz_str (Union[str, bool], optional): Timezone of the birth location. Defaults to "GMT". 62 - geonames_username (str, optional): The username for the geonames API. Note: Change this to your own username to avoid rate limits! 63 You can get one for free here: https://www.geonames.org/login 64 - online (bool, optional): Sets if you want to use the online mode, which fetches the timezone and coordinates from geonames. 65 If you already have the coordinates and timezone, set this to False. Defaults to True. 66 - disable_chiron (bool, optional): Disables the calculation of Chiron. Defaults to False. 67 Chiron calculation can create some issues with the Swiss Ephemeris when the date is too far in the past. 68 - sidereal_mode (SiderealMode, optional): Also known as Ayanamsa. 69 The mode to use for the sidereal zodiac, according to the Swiss Ephemeris. 70 Defaults to "FAGAN_BRADLEY". 71 Available modes are visible in the SiderealMode Literal. 72 - houses_system_identifier (HousesSystemIdentifier, optional): The system to use for the calculation of the houses. 73 Defaults to "P" (Placidus). 74 Available systems are visible in the HousesSystemIdentifier Literal. 75 - perspective_type (PerspectiveType, optional): The perspective to use for the calculation of the chart. 76 Defaults to "Apparent Geocentric". 77 Available perspectives are visible in the PerspectiveType Literal. 78 """ 79 80 # Defined by the user 81 name: str 82 year: int 83 month: int 84 day: int 85 hour: int 86 minute: int 87 city: str 88 nation: str 89 lng: Union[int, float] 90 lat: Union[int, float] 91 tz_str: str 92 geonames_username: str 93 online: bool 94 zodiac_type: ZodiacType 95 sidereal_mode: SiderealMode 96 houses_system_identifier: HousesSystemIdentifier 97 houses_system_name: str 98 perspective_type: PerspectiveType 99 100 # Generated internally 101 city_data: dict[str, str] 102 julian_day: Union[int, float] 103 json_dir: Path 104 iso_formatted_local_datetime: str 105 iso_formatted_utc_datetime: str 106 107 # Planets 108 sun: KerykeionPointModel 109 moon: KerykeionPointModel 110 mercury: KerykeionPointModel 111 venus: KerykeionPointModel 112 mars: KerykeionPointModel 113 jupiter: KerykeionPointModel 114 saturn: KerykeionPointModel 115 uranus: KerykeionPointModel 116 neptune: KerykeionPointModel 117 pluto: KerykeionPointModel 118 true_node: KerykeionPointModel 119 mean_node: KerykeionPointModel 120 chiron: Union[KerykeionPointModel, None] 121 122 # Houses 123 first_house: KerykeionPointModel 124 second_house: KerykeionPointModel 125 third_house: KerykeionPointModel 126 fourth_house: KerykeionPointModel 127 fifth_house: KerykeionPointModel 128 sixth_house: KerykeionPointModel 129 seventh_house: KerykeionPointModel 130 eighth_house: KerykeionPointModel 131 ninth_house: KerykeionPointModel 132 tenth_house: KerykeionPointModel 133 eleventh_house: KerykeionPointModel 134 twelfth_house: KerykeionPointModel 135 136 # Lists 137 houses_list: list[KerykeionPointModel] 138 planets_list: list[KerykeionPointModel] 139 planets_degrees_ut: list[float] 140 houses_degree_ut: list[float] 141 142 def __init__( 143 self, 144 name="Now", 145 year: int = NOW.year, 146 month: int = NOW.month, 147 day: int = NOW.day, 148 hour: int = NOW.hour, 149 minute: int = NOW.minute, 150 city: Union[str, None] = None, 151 nation: Union[str, None] = None, 152 lng: Union[int, float, None] = None, 153 lat: Union[int, float, None] = None, 154 tz_str: Union[str, None] = None, 155 geonames_username: Union[str, None] = None, 156 zodiac_type: ZodiacType = "Tropic", 157 online: bool = True, 158 disable_chiron: bool = False, 159 sidereal_mode: Union[SiderealMode, None] = None, 160 houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM, 161 perspective_type: PerspectiveType = PERSPECTIVE_TYPE 162 ) -> None: 163 logging.debug("Starting Kerykeion") 164 165 self.name = name 166 self.year = year 167 self.month = month 168 self.day = day 169 self.hour = hour 170 self.minute = minute 171 self.city = city 172 self.nation = nation 173 self.lng = lng 174 self.lat = lat 175 self.tz_str = tz_str 176 self.zodiac_type = zodiac_type 177 self.online = online 178 self.json_dir = Path.home() 179 self.geonames_username = geonames_username 180 self.disable_chiron = disable_chiron 181 self.sidereal_mode = sidereal_mode 182 self.houses_system_identifier = houses_system_identifier 183 self.perspective_type = perspective_type 184 185 #---------------# 186 # General setup # 187 #---------------# 188 189 # This message is set to encourage the user to set a custom geonames username 190 if geonames_username is None and online: 191 logging.warning( 192 "\n" 193 "********" + 194 "\n" + 195 "NO GEONAMES USERNAME SET!" + 196 "\n" + 197 "Using the default geonames username is not recommended, please set a custom one!" + 198 "\n" + 199 "You can get one for free here:" + 200 "\n" + 201 "https://www.geonames.org/login" + 202 "\n" + 203 "Keep in mind that the default username is limited to 2000 requests per hour and is shared with everyone else using this library." + 204 "\n" + 205 "********" 206 ) 207 208 self.geonames_username = DEFAULT_GEONAMES_USERNAME 209 210 if not self.city: 211 self.city = "London" 212 logging.info("No city specified, using London as default") 213 214 if not self.nation: 215 self.nation = "GB" 216 logging.info("No nation specified, using GB as default") 217 218 if not self.lat: 219 self.lat = 51.5074 220 logging.info("No latitude specified, using London as default") 221 222 if not self.lng: 223 self.lng = 0 224 logging.info("No longitude specified, using London as default") 225 226 if (not self.online) and (not tz_str): 227 raise KerykeionException("You need to set the coordinates and timezone if you want to use the offline mode!") 228 229 #-----------------------# 230 # Swiss Ephemeris setup # 231 #-----------------------# 232 233 # We set the swisseph path to the current directory 234 swe.set_ephe_path(str(Path(__file__).parent.absolute() / "sweph")) 235 236 # Flags for the Swiss Ephemeris 237 self._iflag = swe.FLG_SWIEPH + swe.FLG_SPEED 238 239 # Chart Perspective check and setup ---> 240 if self.perspective_type not in get_args(PerspectiveType): 241 raise KerykeionException(f"\n* ERROR: '{self.perspective_type}' is NOT a valid chart perspective! Available perspectives are: *" + "\n" + str(get_args(PerspectiveType))) 242 243 if self.perspective_type == "True Geocentric": 244 self._iflag += swe.FLG_TRUEPOS 245 elif self.perspective_type == "Heliocentric": 246 self._iflag += swe.FLG_HELCTR 247 elif self.perspective_type == "Topocentric": 248 self._iflag += swe.FLG_TOPOCTR 249 # geopos_is_set, for topocentric 250 swe.set_topo(self.lng, self.lat, 0) 251 # <--- Chart Perspective check and setup 252 253 # House System check and setup ---> 254 if self.houses_system_identifier not in get_args(HousesSystemIdentifier): 255 raise KerykeionException(f"\n* ERROR: '{self.houses_system_identifier}' is NOT a valid house system! Available systems are: *" + "\n" + str(get_args(HousesSystemIdentifier))) 256 257 self.houses_system_name = swe.house_name(self.houses_system_identifier.encode('ascii')) 258 # <--- House System check and setup 259 260 # Zodiac Type and Sidereal mode checks and setup ---> 261 if zodiac_type and not zodiac_type in get_args(ZodiacType): 262 raise KerykeionException(f"\n* ERROR: '{zodiac_type}' is NOT a valid zodiac type! Available types are: *" + "\n" + str(get_args(ZodiacType))) 263 264 if self.sidereal_mode and self.zodiac_type == "Tropic": 265 raise KerykeionException("You can't set a sidereal mode with a Tropic zodiac type!") 266 267 if self.zodiac_type == "Sidereal" and not self.sidereal_mode: 268 self.sidereal_mode = DEFAULT_SIDEREAL_MODE 269 logging.info("No sidereal mode set, using default FAGAN_BRADLEY") 270 271 if self.zodiac_type == "Sidereal": 272 # Check if the sidereal mode is valid 273 if not self.sidereal_mode in get_args(SiderealMode): 274 raise KerykeionException(f"\n* ERROR: '{self.sidereal_mode}' is NOT a valid sidereal mode! Available modes are: *" + "\n" + str(get_args(SiderealMode))) 275 276 self._iflag += swe.FLG_SIDEREAL 277 mode = "SIDM_" + self.sidereal_mode 278 swe.set_sid_mode(getattr(swe, mode)) 279 logging.debug(f"Using sidereal mode: {mode}") 280 # <--- Zodiac Type and Sidereal mode checks and setup 281 282 #------------------------# 283 # Start the calculations # 284 #------------------------# 285 286 check_and_adjust_polar_latitude(self.lat, self.lng) 287 288 # UTC, julian day and local time setup ---> 289 if (self.online) and (not self.tz_str): 290 self._fetch_tz_from_geonames() 291 292 # Local time to UTC 293 local_time = pytz.timezone(self.tz_str) 294 naive_datetime = datetime(self.year, self.month, self.day, self.hour, self.minute, 0) 295 local_datetime = local_time.localize(naive_datetime, is_dst=None) 296 utc_object = local_datetime.astimezone(pytz.utc) 297 self.iso_formatted_utc_datetime = utc_object.isoformat() 298 299 # ISO formatted local datetime 300 self.iso_formatted_local_datetime = local_datetime.isoformat() 301 302 # Julian day calculation 303 utc_float_hour_with_minutes = utc_object.hour + (utc_object.minute / 60) 304 self.julian_day = float(swe.julday(utc_object.year, utc_object.month, utc_object.day, utc_float_hour_with_minutes)) 305 # <--- UTC, julian day and local time setup 306 307 self._planets_degrees_lister() 308 self._planets() 309 self._houses() 310 self._planets_in_houses() 311 self._lunar_phase_calc() 312 313 def __str__(self) -> str: 314 return f"Astrological data for: {self.name}, {self.iso_formatted_utc_datetime} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}" 315 316 def __repr__(self) -> str: 317 return f"Astrological data for: {self.name}, {self.iso_formatted_utc_datetime} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}" 318 319 def __getitem__(self, item): 320 return getattr(self, item) 321 322 def get(self, item, default=None): 323 return getattr(self, item, default) 324 325 def _fetch_tz_from_geonames(self) -> None: 326 """Gets the nearest time zone for the calculation""" 327 logging.info("Fetching timezone/coordinates from geonames") 328 329 geonames = FetchGeonames( 330 self.city, 331 self.nation, 332 username=self.geonames_username, 333 ) 334 self.city_data: dict[str, str] = geonames.get_serialized_data() 335 336 if ( 337 not "countryCode" in self.city_data 338 or not "timezonestr" in self.city_data 339 or not "lat" in self.city_data 340 or not "lng" in self.city_data 341 ): 342 raise KerykeionException("No data found for this city, try again! Maybe check your connection?") 343 344 self.nation = self.city_data["countryCode"] 345 self.lng = float(self.city_data["lng"]) 346 self.lat = float(self.city_data["lat"]) 347 self.tz_str = self.city_data["timezonestr"] 348 349 check_and_adjust_polar_latitude(self.lat, self.lng) 350 351 def _houses(self) -> None: 352 """ 353 Calculate positions and store them in dictionaries 354 355 https://www.astro.com/faq/fq_fh_owhouse_e.htm 356 https://github.com/jwmatthys/pd-swisseph/blob/master/swehouse.c#L685 357 hsys = letter code for house system; 358 A equal 359 E equal 360 B Alcabitius 361 C Campanus 362 D equal (MC) 363 F Carter "Poli-Equatorial" 364 G 36 Gauquelin sectors 365 H horizon / azimut 366 I Sunshine solution Treindl 367 i Sunshine solution Makransky 368 K Koch 369 L Pullen SD "sinusoidal delta", ex Neo-Porphyry 370 M Morinus 371 N equal/1=Aries 372 O Porphyry 373 P Placidus 374 Q Pullen SR "sinusoidal ratio" 375 R Regiomontanus 376 S Sripati 377 T Polich/Page ("topocentric") 378 U Krusinski-Pisa-Goelzer 379 V equal Vehlow 380 W equal, whole sign 381 X axial rotation system/ Meridian houses 382 Y APC houses 383 """ 384 385 if self.zodiac_type == "Sidereal": 386 self.houses_degree_ut = swe.houses_ex( 387 tjdut=self.julian_day, 388 lat=self.lat, lon=self.lng, 389 hsys=str.encode(self.houses_system_identifier), 390 flags=swe.FLG_SIDEREAL 391 )[0] 392 393 elif self.zodiac_type == "Tropic": 394 self.houses_degree_ut = swe.houses( 395 tjdut=self.julian_day, lat=self.lat, 396 lon=self.lng, 397 hsys=str.encode(self.houses_system_identifier) 398 )[0] 399 400 point_type: PointType = "House" 401 402 # stores the house in singular dictionaries. 403 self.first_house = calculate_position(self.houses_degree_ut[0], "First_House", point_type=point_type) 404 self.second_house = calculate_position(self.houses_degree_ut[1], "Second_House", point_type=point_type) 405 self.third_house = calculate_position(self.houses_degree_ut[2], "Third_House", point_type=point_type) 406 self.fourth_house = calculate_position(self.houses_degree_ut[3], "Fourth_House", point_type=point_type) 407 self.fifth_house = calculate_position(self.houses_degree_ut[4], "Fifth_House", point_type=point_type) 408 self.sixth_house = calculate_position(self.houses_degree_ut[5], "Sixth_House", point_type=point_type) 409 self.seventh_house = calculate_position(self.houses_degree_ut[6], "Seventh_House", point_type=point_type) 410 self.eighth_house = calculate_position(self.houses_degree_ut[7], "Eighth_House", point_type=point_type) 411 self.ninth_house = calculate_position(self.houses_degree_ut[8], "Ninth_House", point_type=point_type) 412 self.tenth_house = calculate_position(self.houses_degree_ut[9], "Tenth_House", point_type=point_type) 413 self.eleventh_house = calculate_position(self.houses_degree_ut[10], "Eleventh_House", point_type=point_type) 414 self.twelfth_house = calculate_position(self.houses_degree_ut[11], "Twelfth_House", point_type=point_type) 415 416 self.houses_list = [ 417 self.first_house, 418 self.second_house, 419 self.third_house, 420 self.fourth_house, 421 self.fifth_house, 422 self.sixth_house, 423 self.seventh_house, 424 self.eighth_house, 425 self.ninth_house, 426 self.tenth_house, 427 self.eleventh_house, 428 self.twelfth_house, 429 ] 430 431 def _planets_degrees_lister(self): 432 """Sidereal or tropic mode.""" 433 434 # Calculates the position of the planets and stores it in a list. 435 sun_deg = swe.calc(self.julian_day, 0, self._iflag)[0][0] 436 moon_deg = swe.calc(self.julian_day, 1, self._iflag)[0][0] 437 mercury_deg = swe.calc(self.julian_day, 2, self._iflag)[0][0] 438 venus_deg = swe.calc(self.julian_day, 3, self._iflag)[0][0] 439 mars_deg = swe.calc(self.julian_day, 4, self._iflag)[0][0] 440 jupiter_deg = swe.calc(self.julian_day, 5, self._iflag)[0][0] 441 saturn_deg = swe.calc(self.julian_day, 6, self._iflag)[0][0] 442 uranus_deg = swe.calc(self.julian_day, 7, self._iflag)[0][0] 443 neptune_deg = swe.calc(self.julian_day, 8, self._iflag)[0][0] 444 pluto_deg = swe.calc(self.julian_day, 9, self._iflag)[0][0] 445 mean_node_deg = swe.calc(self.julian_day, 10, self._iflag)[0][0] 446 true_node_deg = swe.calc(self.julian_day, 11, self._iflag)[0][0] 447 448 if not self.disable_chiron: 449 chiron_deg = swe.calc(self.julian_day, 15, self._iflag)[0][0] 450 else: 451 chiron_deg = 0 452 453 self.planets_degrees_ut = [ 454 sun_deg, 455 moon_deg, 456 mercury_deg, 457 venus_deg, 458 mars_deg, 459 jupiter_deg, 460 saturn_deg, 461 uranus_deg, 462 neptune_deg, 463 pluto_deg, 464 mean_node_deg, 465 true_node_deg, 466 ] 467 468 if not self.disable_chiron: 469 self.planets_degrees_ut.append(chiron_deg) 470 471 def _planets(self) -> None: 472 """Defines body positon in signs and information and 473 stores them in dictionaries""" 474 475 point_type: PointType = "Planet" 476 # stores the planets in singular dictionaries. 477 self.sun = calculate_position(self.planets_degrees_ut[0], "Sun", point_type=point_type) 478 self.moon = calculate_position(self.planets_degrees_ut[1], "Moon", point_type=point_type) 479 self.mercury = calculate_position(self.planets_degrees_ut[2], "Mercury", point_type=point_type) 480 self.venus = calculate_position(self.planets_degrees_ut[3], "Venus", point_type=point_type) 481 self.mars = calculate_position(self.planets_degrees_ut[4], "Mars", point_type=point_type) 482 self.jupiter = calculate_position(self.planets_degrees_ut[5], "Jupiter", point_type=point_type) 483 self.saturn = calculate_position(self.planets_degrees_ut[6], "Saturn", point_type=point_type) 484 self.uranus = calculate_position(self.planets_degrees_ut[7], "Uranus", point_type=point_type) 485 self.neptune = calculate_position(self.planets_degrees_ut[8], "Neptune", point_type=point_type) 486 self.pluto = calculate_position(self.planets_degrees_ut[9], "Pluto", point_type=point_type) 487 self.mean_node = calculate_position(self.planets_degrees_ut[10], "Mean_Node", point_type=point_type) 488 self.true_node = calculate_position(self.planets_degrees_ut[11], "True_Node", point_type=point_type) 489 490 if not self.disable_chiron: 491 self.chiron = calculate_position(self.planets_degrees_ut[12], "Chiron", point_type=point_type) 492 else: 493 self.chiron = None 494 495 def _planets_in_houses(self) -> None: 496 """Calculates the house of the planet and updates 497 the planets dictionary.""" 498 499 self.sun.house = get_planet_house(self.planets_degrees_ut[0], self.houses_degree_ut) 500 self.moon.house = get_planet_house(self.planets_degrees_ut[1], self.houses_degree_ut) 501 self.mercury.house = get_planet_house(self.planets_degrees_ut[2], self.houses_degree_ut) 502 self.venus.house = get_planet_house(self.planets_degrees_ut[3], self.houses_degree_ut) 503 self.mars.house = get_planet_house(self.planets_degrees_ut[4], self.houses_degree_ut) 504 self.jupiter.house = get_planet_house(self.planets_degrees_ut[5], self.houses_degree_ut) 505 self.saturn.house = get_planet_house(self.planets_degrees_ut[6], self.houses_degree_ut) 506 self.uranus.house = get_planet_house(self.planets_degrees_ut[7], self.houses_degree_ut) 507 self.neptune.house = get_planet_house(self.planets_degrees_ut[8], self.houses_degree_ut) 508 self.pluto.house = get_planet_house(self.planets_degrees_ut[9], self.houses_degree_ut) 509 self.mean_node.house = get_planet_house(self.planets_degrees_ut[10], self.houses_degree_ut) 510 self.true_node.house = get_planet_house(self.planets_degrees_ut[11], self.houses_degree_ut) 511 512 if not self.disable_chiron: 513 self.chiron.house = get_planet_house(self.planets_degrees_ut[12], self.houses_degree_ut) 514 else: 515 self.chiron = None 516 517 self.planets_list = [ 518 self.sun, 519 self.moon, 520 self.mercury, 521 self.venus, 522 self.mars, 523 self.jupiter, 524 self.saturn, 525 self.uranus, 526 self.neptune, 527 self.pluto, 528 self.mean_node, 529 self.true_node, 530 ] 531 532 if not self.disable_chiron: 533 self.planets_list.append(self.chiron) 534 535 # Check in retrograde or not: 536 planets_ret = [] 537 for planet in self.planets_list: 538 planet_number = get_number_from_name(planet["name"]) 539 if swe.calc(self.julian_day, planet_number, self._iflag)[0][3] < 0: 540 planet["retrograde"] = True 541 else: 542 planet["retrograde"] = False 543 planets_ret.append(planet) 544 545 def _lunar_phase_calc(self) -> None: 546 """Function to calculate the lunar phase""" 547 548 # If ther's an error: 549 moon_phase, sun_phase = None, None 550 551 # anti-clockwise degrees between sun and moon 552 moon, sun = self.planets_degrees_ut[1], self.planets_degrees_ut[0] 553 degrees_between = moon - sun 554 555 if degrees_between < 0: 556 degrees_between += 360.0 557 558 step = 360.0 / 28.0 559 560 for x in range(28): 561 low = x * step 562 high = (x + 1) * step 563 564 if degrees_between >= low and degrees_between < high: 565 moon_phase = x + 1 566 567 sunstep = [ 568 0, 569 30, 570 40, 571 50, 572 60, 573 70, 574 80, 575 90, 576 120, 577 130, 578 140, 579 150, 580 160, 581 170, 582 180, 583 210, 584 220, 585 230, 586 240, 587 250, 588 260, 589 270, 590 300, 591 310, 592 320, 593 330, 594 340, 595 350, 596 ] 597 598 for x in range(len(sunstep)): 599 low = sunstep[x] 600 601 if x == 27: 602 high = 360 603 else: 604 high = sunstep[x + 1] 605 if degrees_between >= low and degrees_between < high: 606 sun_phase = x + 1 607 608 lunar_phase_dictionary = { 609 "degrees_between_s_m": degrees_between, 610 "moon_phase": moon_phase, 611 "sun_phase": sun_phase, 612 "moon_emoji": get_moon_emoji_from_phase_int(moon_phase), 613 "moon_phase_name": get_moon_phase_name_from_phase_int(moon_phase) 614 } 615 616 self.lunar_phase = LunarPhaseModel(**lunar_phase_dictionary) 617 618 def json(self, dump=False, destination_folder: Union[str, None] = None, indent: Union[int, None] = None) -> str: 619 """ 620 Dumps the Kerykeion object to a json string foramt, 621 if dump=True also dumps to file located in destination 622 or the home folder. 623 """ 624 625 KrData = AstrologicalSubjectModel(**self.__dict__) 626 json_string = KrData.model_dump_json(exclude_none=True, indent=indent) 627 628 if dump: 629 if destination_folder: 630 destination_path = Path(destination_folder) 631 json_path = destination_path / f"{self.name}_kerykeion.json" 632 633 else: 634 json_path = self.json_dir / f"{self.name}_kerykeion.json" 635 636 with open(json_path, "w", encoding="utf-8") as file: 637 file.write(json_string) 638 logging.info(f"JSON file dumped in {json_path}.") 639 640 return json_string 641 642 def model(self) -> AstrologicalSubjectModel: 643 """ 644 Creates a Pydantic model of the Kerykeion object. 645 """ 646 647 return AstrologicalSubjectModel(**self.__dict__) 648 649 650if __name__ == "__main__": 651 import json 652 from kerykeion.utilities import setup_logging 653 654 setup_logging(level="debug") 655 656 # With Chiron enabled 657 johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US") 658 print(json.loads(johnny.json(dump=True))) 659 660 print('\n') 661 print(johnny.chiron) 662 663 # With Chiron disabled 664 johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", disable_chiron=True) 665 print(json.loads(johnny.json(dump=True))) 666 667 print('\n') 668 print(johnny.chiron) 669 670 # With Sidereal Zodiac 671 johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", zodiac_type="Sidereal", sidereal_mode="LAHIRI") 672 print(johnny.json(dump=True, indent=2)) 673 674 # With Morinus Houses 675 johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", houses_system_identifier="M") 676 print(johnny.json(dump=True, indent=2)) 677 678 # With True Geocentric Perspective 679 johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", perspective_type="True Geocentric") 680 print(johnny.json(dump=True, indent=2)) 681 682 # With Heliocentric Perspective 683 johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", perspective_type="Heliocentric") 684 print(johnny.json(dump=True, indent=2)) 685 686 # With Topocentric Perspective 687 johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", perspective_type="Topocentric") 688 print(johnny.json(dump=True, indent=2))
DEFAULT_GEONAMES_USERNAME =
'century.boy'
DEFAULT_SIDEREAL_MODE =
'FAGAN_BRADLEY'
DEFAULT_HOUSES_SYSTEM =
'P'
PERSPECTIVE_TYPE =
'Apparent Geocentric'
NOW =
datetime.datetime(2024, 8, 23, 16, 53, 11, 460991)
class
AstrologicalSubject:
42class AstrologicalSubject: 43 """ 44 Calculates all the astrological information, the coordinates, 45 it's utc and julian day and returns an object with all that data. 46 47 Args: 48 - name (str, optional): The name of the subject. Defaults to "Now". 49 - year (int, optional): The year of birth. Defaults to the current year. 50 - month (int, optional): The month of birth. Defaults to the current month. 51 - day (int, optional): The day of birth. Defaults to the current day. 52 - hour (int, optional): The hour of birth. Defaults to the current hour. 53 - minute (int, optional): Defaults to the current minute. 54 - city (str, optional): City or location of birth. Defaults to "London", which is GMT time. 55 The city argument is used to get the coordinates and timezone from geonames just in case 56 you don't insert them manually (see _get_tz). 57 If you insert the coordinates and timezone manually, the city argument is not used for calculations 58 but it's still used as a value for the city attribute. 59 - nat (str, optional): _ Defaults to "". 60 - lng (Union[int, float], optional): Longitude of the birth location. Defaults to 0 (Greenwich, London). 61 - lat (Union[int, float], optional): Latitude of the birth location. Defaults to 51.5074 (Greenwich, London). 62 - tz_str (Union[str, bool], optional): Timezone of the birth location. Defaults to "GMT". 63 - geonames_username (str, optional): The username for the geonames API. Note: Change this to your own username to avoid rate limits! 64 You can get one for free here: https://www.geonames.org/login 65 - online (bool, optional): Sets if you want to use the online mode, which fetches the timezone and coordinates from geonames. 66 If you already have the coordinates and timezone, set this to False. Defaults to True. 67 - disable_chiron (bool, optional): Disables the calculation of Chiron. Defaults to False. 68 Chiron calculation can create some issues with the Swiss Ephemeris when the date is too far in the past. 69 - sidereal_mode (SiderealMode, optional): Also known as Ayanamsa. 70 The mode to use for the sidereal zodiac, according to the Swiss Ephemeris. 71 Defaults to "FAGAN_BRADLEY". 72 Available modes are visible in the SiderealMode Literal. 73 - houses_system_identifier (HousesSystemIdentifier, optional): The system to use for the calculation of the houses. 74 Defaults to "P" (Placidus). 75 Available systems are visible in the HousesSystemIdentifier Literal. 76 - perspective_type (PerspectiveType, optional): The perspective to use for the calculation of the chart. 77 Defaults to "Apparent Geocentric". 78 Available perspectives are visible in the PerspectiveType Literal. 79 """ 80 81 # Defined by the user 82 name: str 83 year: int 84 month: int 85 day: int 86 hour: int 87 minute: int 88 city: str 89 nation: str 90 lng: Union[int, float] 91 lat: Union[int, float] 92 tz_str: str 93 geonames_username: str 94 online: bool 95 zodiac_type: ZodiacType 96 sidereal_mode: SiderealMode 97 houses_system_identifier: HousesSystemIdentifier 98 houses_system_name: str 99 perspective_type: PerspectiveType 100 101 # Generated internally 102 city_data: dict[str, str] 103 julian_day: Union[int, float] 104 json_dir: Path 105 iso_formatted_local_datetime: str 106 iso_formatted_utc_datetime: str 107 108 # Planets 109 sun: KerykeionPointModel 110 moon: KerykeionPointModel 111 mercury: KerykeionPointModel 112 venus: KerykeionPointModel 113 mars: KerykeionPointModel 114 jupiter: KerykeionPointModel 115 saturn: KerykeionPointModel 116 uranus: KerykeionPointModel 117 neptune: KerykeionPointModel 118 pluto: KerykeionPointModel 119 true_node: KerykeionPointModel 120 mean_node: KerykeionPointModel 121 chiron: Union[KerykeionPointModel, None] 122 123 # Houses 124 first_house: KerykeionPointModel 125 second_house: KerykeionPointModel 126 third_house: KerykeionPointModel 127 fourth_house: KerykeionPointModel 128 fifth_house: KerykeionPointModel 129 sixth_house: KerykeionPointModel 130 seventh_house: KerykeionPointModel 131 eighth_house: KerykeionPointModel 132 ninth_house: KerykeionPointModel 133 tenth_house: KerykeionPointModel 134 eleventh_house: KerykeionPointModel 135 twelfth_house: KerykeionPointModel 136 137 # Lists 138 houses_list: list[KerykeionPointModel] 139 planets_list: list[KerykeionPointModel] 140 planets_degrees_ut: list[float] 141 houses_degree_ut: list[float] 142 143 def __init__( 144 self, 145 name="Now", 146 year: int = NOW.year, 147 month: int = NOW.month, 148 day: int = NOW.day, 149 hour: int = NOW.hour, 150 minute: int = NOW.minute, 151 city: Union[str, None] = None, 152 nation: Union[str, None] = None, 153 lng: Union[int, float, None] = None, 154 lat: Union[int, float, None] = None, 155 tz_str: Union[str, None] = None, 156 geonames_username: Union[str, None] = None, 157 zodiac_type: ZodiacType = "Tropic", 158 online: bool = True, 159 disable_chiron: bool = False, 160 sidereal_mode: Union[SiderealMode, None] = None, 161 houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM, 162 perspective_type: PerspectiveType = PERSPECTIVE_TYPE 163 ) -> None: 164 logging.debug("Starting Kerykeion") 165 166 self.name = name 167 self.year = year 168 self.month = month 169 self.day = day 170 self.hour = hour 171 self.minute = minute 172 self.city = city 173 self.nation = nation 174 self.lng = lng 175 self.lat = lat 176 self.tz_str = tz_str 177 self.zodiac_type = zodiac_type 178 self.online = online 179 self.json_dir = Path.home() 180 self.geonames_username = geonames_username 181 self.disable_chiron = disable_chiron 182 self.sidereal_mode = sidereal_mode 183 self.houses_system_identifier = houses_system_identifier 184 self.perspective_type = perspective_type 185 186 #---------------# 187 # General setup # 188 #---------------# 189 190 # This message is set to encourage the user to set a custom geonames username 191 if geonames_username is None and online: 192 logging.warning( 193 "\n" 194 "********" + 195 "\n" + 196 "NO GEONAMES USERNAME SET!" + 197 "\n" + 198 "Using the default geonames username is not recommended, please set a custom one!" + 199 "\n" + 200 "You can get one for free here:" + 201 "\n" + 202 "https://www.geonames.org/login" + 203 "\n" + 204 "Keep in mind that the default username is limited to 2000 requests per hour and is shared with everyone else using this library." + 205 "\n" + 206 "********" 207 ) 208 209 self.geonames_username = DEFAULT_GEONAMES_USERNAME 210 211 if not self.city: 212 self.city = "London" 213 logging.info("No city specified, using London as default") 214 215 if not self.nation: 216 self.nation = "GB" 217 logging.info("No nation specified, using GB as default") 218 219 if not self.lat: 220 self.lat = 51.5074 221 logging.info("No latitude specified, using London as default") 222 223 if not self.lng: 224 self.lng = 0 225 logging.info("No longitude specified, using London as default") 226 227 if (not self.online) and (not tz_str): 228 raise KerykeionException("You need to set the coordinates and timezone if you want to use the offline mode!") 229 230 #-----------------------# 231 # Swiss Ephemeris setup # 232 #-----------------------# 233 234 # We set the swisseph path to the current directory 235 swe.set_ephe_path(str(Path(__file__).parent.absolute() / "sweph")) 236 237 # Flags for the Swiss Ephemeris 238 self._iflag = swe.FLG_SWIEPH + swe.FLG_SPEED 239 240 # Chart Perspective check and setup ---> 241 if self.perspective_type not in get_args(PerspectiveType): 242 raise KerykeionException(f"\n* ERROR: '{self.perspective_type}' is NOT a valid chart perspective! Available perspectives are: *" + "\n" + str(get_args(PerspectiveType))) 243 244 if self.perspective_type == "True Geocentric": 245 self._iflag += swe.FLG_TRUEPOS 246 elif self.perspective_type == "Heliocentric": 247 self._iflag += swe.FLG_HELCTR 248 elif self.perspective_type == "Topocentric": 249 self._iflag += swe.FLG_TOPOCTR 250 # geopos_is_set, for topocentric 251 swe.set_topo(self.lng, self.lat, 0) 252 # <--- Chart Perspective check and setup 253 254 # House System check and setup ---> 255 if self.houses_system_identifier not in get_args(HousesSystemIdentifier): 256 raise KerykeionException(f"\n* ERROR: '{self.houses_system_identifier}' is NOT a valid house system! Available systems are: *" + "\n" + str(get_args(HousesSystemIdentifier))) 257 258 self.houses_system_name = swe.house_name(self.houses_system_identifier.encode('ascii')) 259 # <--- House System check and setup 260 261 # Zodiac Type and Sidereal mode checks and setup ---> 262 if zodiac_type and not zodiac_type in get_args(ZodiacType): 263 raise KerykeionException(f"\n* ERROR: '{zodiac_type}' is NOT a valid zodiac type! Available types are: *" + "\n" + str(get_args(ZodiacType))) 264 265 if self.sidereal_mode and self.zodiac_type == "Tropic": 266 raise KerykeionException("You can't set a sidereal mode with a Tropic zodiac type!") 267 268 if self.zodiac_type == "Sidereal" and not self.sidereal_mode: 269 self.sidereal_mode = DEFAULT_SIDEREAL_MODE 270 logging.info("No sidereal mode set, using default FAGAN_BRADLEY") 271 272 if self.zodiac_type == "Sidereal": 273 # Check if the sidereal mode is valid 274 if not self.sidereal_mode in get_args(SiderealMode): 275 raise KerykeionException(f"\n* ERROR: '{self.sidereal_mode}' is NOT a valid sidereal mode! Available modes are: *" + "\n" + str(get_args(SiderealMode))) 276 277 self._iflag += swe.FLG_SIDEREAL 278 mode = "SIDM_" + self.sidereal_mode 279 swe.set_sid_mode(getattr(swe, mode)) 280 logging.debug(f"Using sidereal mode: {mode}") 281 # <--- Zodiac Type and Sidereal mode checks and setup 282 283 #------------------------# 284 # Start the calculations # 285 #------------------------# 286 287 check_and_adjust_polar_latitude(self.lat, self.lng) 288 289 # UTC, julian day and local time setup ---> 290 if (self.online) and (not self.tz_str): 291 self._fetch_tz_from_geonames() 292 293 # Local time to UTC 294 local_time = pytz.timezone(self.tz_str) 295 naive_datetime = datetime(self.year, self.month, self.day, self.hour, self.minute, 0) 296 local_datetime = local_time.localize(naive_datetime, is_dst=None) 297 utc_object = local_datetime.astimezone(pytz.utc) 298 self.iso_formatted_utc_datetime = utc_object.isoformat() 299 300 # ISO formatted local datetime 301 self.iso_formatted_local_datetime = local_datetime.isoformat() 302 303 # Julian day calculation 304 utc_float_hour_with_minutes = utc_object.hour + (utc_object.minute / 60) 305 self.julian_day = float(swe.julday(utc_object.year, utc_object.month, utc_object.day, utc_float_hour_with_minutes)) 306 # <--- UTC, julian day and local time setup 307 308 self._planets_degrees_lister() 309 self._planets() 310 self._houses() 311 self._planets_in_houses() 312 self._lunar_phase_calc() 313 314 def __str__(self) -> str: 315 return f"Astrological data for: {self.name}, {self.iso_formatted_utc_datetime} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}" 316 317 def __repr__(self) -> str: 318 return f"Astrological data for: {self.name}, {self.iso_formatted_utc_datetime} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}" 319 320 def __getitem__(self, item): 321 return getattr(self, item) 322 323 def get(self, item, default=None): 324 return getattr(self, item, default) 325 326 def _fetch_tz_from_geonames(self) -> None: 327 """Gets the nearest time zone for the calculation""" 328 logging.info("Fetching timezone/coordinates from geonames") 329 330 geonames = FetchGeonames( 331 self.city, 332 self.nation, 333 username=self.geonames_username, 334 ) 335 self.city_data: dict[str, str] = geonames.get_serialized_data() 336 337 if ( 338 not "countryCode" in self.city_data 339 or not "timezonestr" in self.city_data 340 or not "lat" in self.city_data 341 or not "lng" in self.city_data 342 ): 343 raise KerykeionException("No data found for this city, try again! Maybe check your connection?") 344 345 self.nation = self.city_data["countryCode"] 346 self.lng = float(self.city_data["lng"]) 347 self.lat = float(self.city_data["lat"]) 348 self.tz_str = self.city_data["timezonestr"] 349 350 check_and_adjust_polar_latitude(self.lat, self.lng) 351 352 def _houses(self) -> None: 353 """ 354 Calculate positions and store them in dictionaries 355 356 https://www.astro.com/faq/fq_fh_owhouse_e.htm 357 https://github.com/jwmatthys/pd-swisseph/blob/master/swehouse.c#L685 358 hsys = letter code for house system; 359 A equal 360 E equal 361 B Alcabitius 362 C Campanus 363 D equal (MC) 364 F Carter "Poli-Equatorial" 365 G 36 Gauquelin sectors 366 H horizon / azimut 367 I Sunshine solution Treindl 368 i Sunshine solution Makransky 369 K Koch 370 L Pullen SD "sinusoidal delta", ex Neo-Porphyry 371 M Morinus 372 N equal/1=Aries 373 O Porphyry 374 P Placidus 375 Q Pullen SR "sinusoidal ratio" 376 R Regiomontanus 377 S Sripati 378 T Polich/Page ("topocentric") 379 U Krusinski-Pisa-Goelzer 380 V equal Vehlow 381 W equal, whole sign 382 X axial rotation system/ Meridian houses 383 Y APC houses 384 """ 385 386 if self.zodiac_type == "Sidereal": 387 self.houses_degree_ut = swe.houses_ex( 388 tjdut=self.julian_day, 389 lat=self.lat, lon=self.lng, 390 hsys=str.encode(self.houses_system_identifier), 391 flags=swe.FLG_SIDEREAL 392 )[0] 393 394 elif self.zodiac_type == "Tropic": 395 self.houses_degree_ut = swe.houses( 396 tjdut=self.julian_day, lat=self.lat, 397 lon=self.lng, 398 hsys=str.encode(self.houses_system_identifier) 399 )[0] 400 401 point_type: PointType = "House" 402 403 # stores the house in singular dictionaries. 404 self.first_house = calculate_position(self.houses_degree_ut[0], "First_House", point_type=point_type) 405 self.second_house = calculate_position(self.houses_degree_ut[1], "Second_House", point_type=point_type) 406 self.third_house = calculate_position(self.houses_degree_ut[2], "Third_House", point_type=point_type) 407 self.fourth_house = calculate_position(self.houses_degree_ut[3], "Fourth_House", point_type=point_type) 408 self.fifth_house = calculate_position(self.houses_degree_ut[4], "Fifth_House", point_type=point_type) 409 self.sixth_house = calculate_position(self.houses_degree_ut[5], "Sixth_House", point_type=point_type) 410 self.seventh_house = calculate_position(self.houses_degree_ut[6], "Seventh_House", point_type=point_type) 411 self.eighth_house = calculate_position(self.houses_degree_ut[7], "Eighth_House", point_type=point_type) 412 self.ninth_house = calculate_position(self.houses_degree_ut[8], "Ninth_House", point_type=point_type) 413 self.tenth_house = calculate_position(self.houses_degree_ut[9], "Tenth_House", point_type=point_type) 414 self.eleventh_house = calculate_position(self.houses_degree_ut[10], "Eleventh_House", point_type=point_type) 415 self.twelfth_house = calculate_position(self.houses_degree_ut[11], "Twelfth_House", point_type=point_type) 416 417 self.houses_list = [ 418 self.first_house, 419 self.second_house, 420 self.third_house, 421 self.fourth_house, 422 self.fifth_house, 423 self.sixth_house, 424 self.seventh_house, 425 self.eighth_house, 426 self.ninth_house, 427 self.tenth_house, 428 self.eleventh_house, 429 self.twelfth_house, 430 ] 431 432 def _planets_degrees_lister(self): 433 """Sidereal or tropic mode.""" 434 435 # Calculates the position of the planets and stores it in a list. 436 sun_deg = swe.calc(self.julian_day, 0, self._iflag)[0][0] 437 moon_deg = swe.calc(self.julian_day, 1, self._iflag)[0][0] 438 mercury_deg = swe.calc(self.julian_day, 2, self._iflag)[0][0] 439 venus_deg = swe.calc(self.julian_day, 3, self._iflag)[0][0] 440 mars_deg = swe.calc(self.julian_day, 4, self._iflag)[0][0] 441 jupiter_deg = swe.calc(self.julian_day, 5, self._iflag)[0][0] 442 saturn_deg = swe.calc(self.julian_day, 6, self._iflag)[0][0] 443 uranus_deg = swe.calc(self.julian_day, 7, self._iflag)[0][0] 444 neptune_deg = swe.calc(self.julian_day, 8, self._iflag)[0][0] 445 pluto_deg = swe.calc(self.julian_day, 9, self._iflag)[0][0] 446 mean_node_deg = swe.calc(self.julian_day, 10, self._iflag)[0][0] 447 true_node_deg = swe.calc(self.julian_day, 11, self._iflag)[0][0] 448 449 if not self.disable_chiron: 450 chiron_deg = swe.calc(self.julian_day, 15, self._iflag)[0][0] 451 else: 452 chiron_deg = 0 453 454 self.planets_degrees_ut = [ 455 sun_deg, 456 moon_deg, 457 mercury_deg, 458 venus_deg, 459 mars_deg, 460 jupiter_deg, 461 saturn_deg, 462 uranus_deg, 463 neptune_deg, 464 pluto_deg, 465 mean_node_deg, 466 true_node_deg, 467 ] 468 469 if not self.disable_chiron: 470 self.planets_degrees_ut.append(chiron_deg) 471 472 def _planets(self) -> None: 473 """Defines body positon in signs and information and 474 stores them in dictionaries""" 475 476 point_type: PointType = "Planet" 477 # stores the planets in singular dictionaries. 478 self.sun = calculate_position(self.planets_degrees_ut[0], "Sun", point_type=point_type) 479 self.moon = calculate_position(self.planets_degrees_ut[1], "Moon", point_type=point_type) 480 self.mercury = calculate_position(self.planets_degrees_ut[2], "Mercury", point_type=point_type) 481 self.venus = calculate_position(self.planets_degrees_ut[3], "Venus", point_type=point_type) 482 self.mars = calculate_position(self.planets_degrees_ut[4], "Mars", point_type=point_type) 483 self.jupiter = calculate_position(self.planets_degrees_ut[5], "Jupiter", point_type=point_type) 484 self.saturn = calculate_position(self.planets_degrees_ut[6], "Saturn", point_type=point_type) 485 self.uranus = calculate_position(self.planets_degrees_ut[7], "Uranus", point_type=point_type) 486 self.neptune = calculate_position(self.planets_degrees_ut[8], "Neptune", point_type=point_type) 487 self.pluto = calculate_position(self.planets_degrees_ut[9], "Pluto", point_type=point_type) 488 self.mean_node = calculate_position(self.planets_degrees_ut[10], "Mean_Node", point_type=point_type) 489 self.true_node = calculate_position(self.planets_degrees_ut[11], "True_Node", point_type=point_type) 490 491 if not self.disable_chiron: 492 self.chiron = calculate_position(self.planets_degrees_ut[12], "Chiron", point_type=point_type) 493 else: 494 self.chiron = None 495 496 def _planets_in_houses(self) -> None: 497 """Calculates the house of the planet and updates 498 the planets dictionary.""" 499 500 self.sun.house = get_planet_house(self.planets_degrees_ut[0], self.houses_degree_ut) 501 self.moon.house = get_planet_house(self.planets_degrees_ut[1], self.houses_degree_ut) 502 self.mercury.house = get_planet_house(self.planets_degrees_ut[2], self.houses_degree_ut) 503 self.venus.house = get_planet_house(self.planets_degrees_ut[3], self.houses_degree_ut) 504 self.mars.house = get_planet_house(self.planets_degrees_ut[4], self.houses_degree_ut) 505 self.jupiter.house = get_planet_house(self.planets_degrees_ut[5], self.houses_degree_ut) 506 self.saturn.house = get_planet_house(self.planets_degrees_ut[6], self.houses_degree_ut) 507 self.uranus.house = get_planet_house(self.planets_degrees_ut[7], self.houses_degree_ut) 508 self.neptune.house = get_planet_house(self.planets_degrees_ut[8], self.houses_degree_ut) 509 self.pluto.house = get_planet_house(self.planets_degrees_ut[9], self.houses_degree_ut) 510 self.mean_node.house = get_planet_house(self.planets_degrees_ut[10], self.houses_degree_ut) 511 self.true_node.house = get_planet_house(self.planets_degrees_ut[11], self.houses_degree_ut) 512 513 if not self.disable_chiron: 514 self.chiron.house = get_planet_house(self.planets_degrees_ut[12], self.houses_degree_ut) 515 else: 516 self.chiron = None 517 518 self.planets_list = [ 519 self.sun, 520 self.moon, 521 self.mercury, 522 self.venus, 523 self.mars, 524 self.jupiter, 525 self.saturn, 526 self.uranus, 527 self.neptune, 528 self.pluto, 529 self.mean_node, 530 self.true_node, 531 ] 532 533 if not self.disable_chiron: 534 self.planets_list.append(self.chiron) 535 536 # Check in retrograde or not: 537 planets_ret = [] 538 for planet in self.planets_list: 539 planet_number = get_number_from_name(planet["name"]) 540 if swe.calc(self.julian_day, planet_number, self._iflag)[0][3] < 0: 541 planet["retrograde"] = True 542 else: 543 planet["retrograde"] = False 544 planets_ret.append(planet) 545 546 def _lunar_phase_calc(self) -> None: 547 """Function to calculate the lunar phase""" 548 549 # If ther's an error: 550 moon_phase, sun_phase = None, None 551 552 # anti-clockwise degrees between sun and moon 553 moon, sun = self.planets_degrees_ut[1], self.planets_degrees_ut[0] 554 degrees_between = moon - sun 555 556 if degrees_between < 0: 557 degrees_between += 360.0 558 559 step = 360.0 / 28.0 560 561 for x in range(28): 562 low = x * step 563 high = (x + 1) * step 564 565 if degrees_between >= low and degrees_between < high: 566 moon_phase = x + 1 567 568 sunstep = [ 569 0, 570 30, 571 40, 572 50, 573 60, 574 70, 575 80, 576 90, 577 120, 578 130, 579 140, 580 150, 581 160, 582 170, 583 180, 584 210, 585 220, 586 230, 587 240, 588 250, 589 260, 590 270, 591 300, 592 310, 593 320, 594 330, 595 340, 596 350, 597 ] 598 599 for x in range(len(sunstep)): 600 low = sunstep[x] 601 602 if x == 27: 603 high = 360 604 else: 605 high = sunstep[x + 1] 606 if degrees_between >= low and degrees_between < high: 607 sun_phase = x + 1 608 609 lunar_phase_dictionary = { 610 "degrees_between_s_m": degrees_between, 611 "moon_phase": moon_phase, 612 "sun_phase": sun_phase, 613 "moon_emoji": get_moon_emoji_from_phase_int(moon_phase), 614 "moon_phase_name": get_moon_phase_name_from_phase_int(moon_phase) 615 } 616 617 self.lunar_phase = LunarPhaseModel(**lunar_phase_dictionary) 618 619 def json(self, dump=False, destination_folder: Union[str, None] = None, indent: Union[int, None] = None) -> str: 620 """ 621 Dumps the Kerykeion object to a json string foramt, 622 if dump=True also dumps to file located in destination 623 or the home folder. 624 """ 625 626 KrData = AstrologicalSubjectModel(**self.__dict__) 627 json_string = KrData.model_dump_json(exclude_none=True, indent=indent) 628 629 if dump: 630 if destination_folder: 631 destination_path = Path(destination_folder) 632 json_path = destination_path / f"{self.name}_kerykeion.json" 633 634 else: 635 json_path = self.json_dir / f"{self.name}_kerykeion.json" 636 637 with open(json_path, "w", encoding="utf-8") as file: 638 file.write(json_string) 639 logging.info(f"JSON file dumped in {json_path}.") 640 641 return json_string 642 643 def model(self) -> AstrologicalSubjectModel: 644 """ 645 Creates a Pydantic model of the Kerykeion object. 646 """ 647 648 return AstrologicalSubjectModel(**self.__dict__)
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 (bool, optional): Disables the calculation of Chiron. Defaults to 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 "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.
AstrologicalSubject( name='Now', year: int = 2024, month: int = 8, day: int = 23, hour: int = 16, minute: int = 53, 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: Literal['Tropic', 'Sidereal'] = 'Tropic', online: bool = True, disable_chiron: 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')
143 def __init__( 144 self, 145 name="Now", 146 year: int = NOW.year, 147 month: int = NOW.month, 148 day: int = NOW.day, 149 hour: int = NOW.hour, 150 minute: int = NOW.minute, 151 city: Union[str, None] = None, 152 nation: Union[str, None] = None, 153 lng: Union[int, float, None] = None, 154 lat: Union[int, float, None] = None, 155 tz_str: Union[str, None] = None, 156 geonames_username: Union[str, None] = None, 157 zodiac_type: ZodiacType = "Tropic", 158 online: bool = True, 159 disable_chiron: bool = False, 160 sidereal_mode: Union[SiderealMode, None] = None, 161 houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM, 162 perspective_type: PerspectiveType = PERSPECTIVE_TYPE 163 ) -> None: 164 logging.debug("Starting Kerykeion") 165 166 self.name = name 167 self.year = year 168 self.month = month 169 self.day = day 170 self.hour = hour 171 self.minute = minute 172 self.city = city 173 self.nation = nation 174 self.lng = lng 175 self.lat = lat 176 self.tz_str = tz_str 177 self.zodiac_type = zodiac_type 178 self.online = online 179 self.json_dir = Path.home() 180 self.geonames_username = geonames_username 181 self.disable_chiron = disable_chiron 182 self.sidereal_mode = sidereal_mode 183 self.houses_system_identifier = houses_system_identifier 184 self.perspective_type = perspective_type 185 186 #---------------# 187 # General setup # 188 #---------------# 189 190 # This message is set to encourage the user to set a custom geonames username 191 if geonames_username is None and online: 192 logging.warning( 193 "\n" 194 "********" + 195 "\n" + 196 "NO GEONAMES USERNAME SET!" + 197 "\n" + 198 "Using the default geonames username is not recommended, please set a custom one!" + 199 "\n" + 200 "You can get one for free here:" + 201 "\n" + 202 "https://www.geonames.org/login" + 203 "\n" + 204 "Keep in mind that the default username is limited to 2000 requests per hour and is shared with everyone else using this library." + 205 "\n" + 206 "********" 207 ) 208 209 self.geonames_username = DEFAULT_GEONAMES_USERNAME 210 211 if not self.city: 212 self.city = "London" 213 logging.info("No city specified, using London as default") 214 215 if not self.nation: 216 self.nation = "GB" 217 logging.info("No nation specified, using GB as default") 218 219 if not self.lat: 220 self.lat = 51.5074 221 logging.info("No latitude specified, using London as default") 222 223 if not self.lng: 224 self.lng = 0 225 logging.info("No longitude specified, using London as default") 226 227 if (not self.online) and (not tz_str): 228 raise KerykeionException("You need to set the coordinates and timezone if you want to use the offline mode!") 229 230 #-----------------------# 231 # Swiss Ephemeris setup # 232 #-----------------------# 233 234 # We set the swisseph path to the current directory 235 swe.set_ephe_path(str(Path(__file__).parent.absolute() / "sweph")) 236 237 # Flags for the Swiss Ephemeris 238 self._iflag = swe.FLG_SWIEPH + swe.FLG_SPEED 239 240 # Chart Perspective check and setup ---> 241 if self.perspective_type not in get_args(PerspectiveType): 242 raise KerykeionException(f"\n* ERROR: '{self.perspective_type}' is NOT a valid chart perspective! Available perspectives are: *" + "\n" + str(get_args(PerspectiveType))) 243 244 if self.perspective_type == "True Geocentric": 245 self._iflag += swe.FLG_TRUEPOS 246 elif self.perspective_type == "Heliocentric": 247 self._iflag += swe.FLG_HELCTR 248 elif self.perspective_type == "Topocentric": 249 self._iflag += swe.FLG_TOPOCTR 250 # geopos_is_set, for topocentric 251 swe.set_topo(self.lng, self.lat, 0) 252 # <--- Chart Perspective check and setup 253 254 # House System check and setup ---> 255 if self.houses_system_identifier not in get_args(HousesSystemIdentifier): 256 raise KerykeionException(f"\n* ERROR: '{self.houses_system_identifier}' is NOT a valid house system! Available systems are: *" + "\n" + str(get_args(HousesSystemIdentifier))) 257 258 self.houses_system_name = swe.house_name(self.houses_system_identifier.encode('ascii')) 259 # <--- House System check and setup 260 261 # Zodiac Type and Sidereal mode checks and setup ---> 262 if zodiac_type and not zodiac_type in get_args(ZodiacType): 263 raise KerykeionException(f"\n* ERROR: '{zodiac_type}' is NOT a valid zodiac type! Available types are: *" + "\n" + str(get_args(ZodiacType))) 264 265 if self.sidereal_mode and self.zodiac_type == "Tropic": 266 raise KerykeionException("You can't set a sidereal mode with a Tropic zodiac type!") 267 268 if self.zodiac_type == "Sidereal" and not self.sidereal_mode: 269 self.sidereal_mode = DEFAULT_SIDEREAL_MODE 270 logging.info("No sidereal mode set, using default FAGAN_BRADLEY") 271 272 if self.zodiac_type == "Sidereal": 273 # Check if the sidereal mode is valid 274 if not self.sidereal_mode in get_args(SiderealMode): 275 raise KerykeionException(f"\n* ERROR: '{self.sidereal_mode}' is NOT a valid sidereal mode! Available modes are: *" + "\n" + str(get_args(SiderealMode))) 276 277 self._iflag += swe.FLG_SIDEREAL 278 mode = "SIDM_" + self.sidereal_mode 279 swe.set_sid_mode(getattr(swe, mode)) 280 logging.debug(f"Using sidereal mode: {mode}") 281 # <--- Zodiac Type and Sidereal mode checks and setup 282 283 #------------------------# 284 # Start the calculations # 285 #------------------------# 286 287 check_and_adjust_polar_latitude(self.lat, self.lng) 288 289 # UTC, julian day and local time setup ---> 290 if (self.online) and (not self.tz_str): 291 self._fetch_tz_from_geonames() 292 293 # Local time to UTC 294 local_time = pytz.timezone(self.tz_str) 295 naive_datetime = datetime(self.year, self.month, self.day, self.hour, self.minute, 0) 296 local_datetime = local_time.localize(naive_datetime, is_dst=None) 297 utc_object = local_datetime.astimezone(pytz.utc) 298 self.iso_formatted_utc_datetime = utc_object.isoformat() 299 300 # ISO formatted local datetime 301 self.iso_formatted_local_datetime = local_datetime.isoformat() 302 303 # Julian day calculation 304 utc_float_hour_with_minutes = utc_object.hour + (utc_object.minute / 60) 305 self.julian_day = float(swe.julday(utc_object.year, utc_object.month, utc_object.day, utc_float_hour_with_minutes)) 306 # <--- UTC, julian day and local time setup 307 308 self._planets_degrees_lister() 309 self._planets() 310 self._houses() 311 self._planets_in_houses() 312 self._lunar_phase_calc()
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']
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']
chiron: Optional[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
houses_list: list[kerykeion.kr_types.kr_models.KerykeionPointModel]
planets_list: list[kerykeion.kr_types.kr_models.KerykeionPointModel]
def
json( self, dump=False, destination_folder: Optional[str] = None, indent: Optional[int] = None) -> str:
619 def json(self, dump=False, destination_folder: Union[str, None] = None, indent: Union[int, None] = None) -> str: 620 """ 621 Dumps the Kerykeion object to a json string foramt, 622 if dump=True also dumps to file located in destination 623 or the home folder. 624 """ 625 626 KrData = AstrologicalSubjectModel(**self.__dict__) 627 json_string = KrData.model_dump_json(exclude_none=True, indent=indent) 628 629 if dump: 630 if destination_folder: 631 destination_path = Path(destination_folder) 632 json_path = destination_path / f"{self.name}_kerykeion.json" 633 634 else: 635 json_path = self.json_dir / f"{self.name}_kerykeion.json" 636 637 with open(json_path, "w", encoding="utf-8") as file: 638 file.write(json_string) 639 logging.info(f"JSON file dumped in {json_path}.") 640 641 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.