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()
name: str
year: int
month: int
day: int
hour: int
minute: int
city: str
nation: str
lng: Union[int, float]
lat: Union[int, float]
tz_str: str
geonames_username: str
online: bool
zodiac_type: Literal['Tropic', 'Sidereal']
sidereal_mode: Literal['FAGAN_BRADLEY', 'LAHIRI', 'DELUCE', 'RAMAN', 'USHASHASHI', 'KRISHNAMURTI', 'DJWHAL_KHUL', 'YUKTESHWAR', 'JN_BHASIN', 'BABYL_KUGLER1', 'BABYL_KUGLER2', 'BABYL_KUGLER3', 'BABYL_HUBER', 'BABYL_ETPSC', 'ALDEBARAN_15TAU', 'HIPPARCHOS', 'SASSANIAN', 'J2000', 'J1900', 'B1950']
houses_system_identifier: Literal['A', 'B', 'C', 'D', 'F', 'H', 'I', 'i', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y']
houses_system_name: str
perspective_type: Literal['Apparent Geocentric', 'Heliocentric', 'Topocentric', 'True Geocentric']
city_data: dict[str, str]
julian_day: Union[int, float]
json_dir: pathlib.Path
iso_formatted_local_datetime: str
iso_formatted_utc_datetime: str
planets_degrees_ut: list[float]
houses_degree_ut: list[float]
disable_chiron
def get(self, item, default=None):
323    def get(self, item, default=None):
324        return getattr(self, item, default)
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.

643    def model(self) -> AstrologicalSubjectModel:
644        """
645        Creates a Pydantic model of the Kerykeion object.
646        """
647
648        return AstrologicalSubjectModel(**self.__dict__)

Creates a Pydantic model of the Kerykeion object.