kerykeion 4.8.1__py3-none-any.whl → 4.14.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kerykeion might be problematic. Click here for more details.

@@ -6,8 +6,10 @@
6
6
  import pytz
7
7
  import swisseph as swe
8
8
  import logging
9
+ import warnings
9
10
 
10
11
  from datetime import datetime
12
+ from functools import cached_property
11
13
  from kerykeion.fetch_geonames import FetchGeonames
12
14
  from kerykeion.kr_types import (
13
15
  KerykeionException,
@@ -15,6 +17,10 @@ from kerykeion.kr_types import (
15
17
  AstrologicalSubjectModel,
16
18
  LunarPhaseModel,
17
19
  KerykeionPointModel,
20
+ PointType,
21
+ SiderealMode,
22
+ HousesSystemIdentifier,
23
+ PerspectiveType
18
24
  )
19
25
  from kerykeion.utilities import (
20
26
  get_number_from_name,
@@ -22,11 +28,27 @@ from kerykeion.utilities import (
22
28
  get_planet_house,
23
29
  get_moon_emoji_from_phase_int,
24
30
  get_moon_phase_name_from_phase_int,
31
+ check_and_adjust_polar_latitude
25
32
  )
26
33
  from pathlib import Path
27
- from typing import Union, Literal
34
+ from typing import Union, get_args
28
35
 
29
36
  DEFAULT_GEONAMES_USERNAME = "century.boy"
37
+ DEFAULT_SIDEREAL_MODE = "FAGAN_BRADLEY"
38
+ DEFAULT_HOUSES_SYSTEM_IDENTIFIER = "P"
39
+ DEFAULT_ZODIAC_TYPE = "Tropic"
40
+ DEFAULT_PERSPECTIVE_TYPE = "Apparent Geocentric"
41
+ GEONAMES_DEFAULT_USERNAME_WARNING = (
42
+ "\n********\n"
43
+ "NO GEONAMES USERNAME SET!\n"
44
+ "Using the default geonames username is not recommended, please set a custom one!\n"
45
+ "You can get one for free here:\n"
46
+ "https://www.geonames.org/login\n"
47
+ "Keep in mind that the default username is limited to 2000 requests per hour and is shared with everyone else using this library.\n"
48
+ "********"
49
+ )
50
+
51
+ NOW = datetime.now()
30
52
 
31
53
 
32
54
  class AstrologicalSubject:
@@ -35,55 +57,70 @@ class AstrologicalSubject:
35
57
  it's utc and julian day and returns an object with all that data.
36
58
 
37
59
  Args:
38
- - name (str, optional): _ Defaults to "Now".
39
- - year (int, optional): _ Defaults to now.year.
40
- - month (int, optional): _ Defaults to now.month.
41
- - day (int, optional): _ Defaults to now.day.
42
- - hour (int, optional): _ Defaults to now.hour.
43
- - minute (int, optional): _ Defaults to now.minute.
60
+ - name (str, optional): The name of the subject. Defaults to "Now".
61
+ - year (int, optional): The year of birth. Defaults to the current year.
62
+ - month (int, optional): The month of birth. Defaults to the current month.
63
+ - day (int, optional): The day of birth. Defaults to the current day.
64
+ - hour (int, optional): The hour of birth. Defaults to the current hour.
65
+ - minute (int, optional): Defaults to the current minute.
44
66
  - city (str, optional): City or location of birth. Defaults to "London", which is GMT time.
45
67
  The city argument is used to get the coordinates and timezone from geonames just in case
46
68
  you don't insert them manually (see _get_tz).
47
69
  If you insert the coordinates and timezone manually, the city argument is not used for calculations
48
70
  but it's still used as a value for the city attribute.
49
71
  - nat (str, optional): _ Defaults to "".
50
- - lng (Union[int, float], optional): _ Defaults to False.
51
- - lat (Union[int, float], optional): _ Defaults to False.
52
- - tz_str (Union[str, bool], optional): _ Defaults to False.
53
- - geonames_username (str, optional): _ Defaults to 'century.boy'.
54
- - online (bool, optional): Sets if you want to use the online mode (using
55
- geonames) or not. Defaults to True.
56
- - utc_datetime (datetime, optional): An alternative way of constructing the object,
57
- if you know the UTC datetime but do not have easy access to e.g. timezone identifier
58
- _ Defaults to None.
59
- - disable_chiron (bool, optional): Disables the calculation of Chiron. Defaults to False.
72
+ - lng (Union[int, float], optional): Longitude of the birth location. Defaults to 0 (Greenwich, London).
73
+ - lat (Union[int, float], optional): Latitude of the birth location. Defaults to 51.5074 (Greenwich, London).
74
+ - tz_str (Union[str, bool], optional): Timezone of the birth location. Defaults to "GMT".
75
+ - geonames_username (str, optional): The username for the geonames API. Note: Change this to your own username to avoid rate limits!
76
+ You can get one for free here: https://www.geonames.org/login
77
+ - online (bool, optional): Sets if you want to use the online mode, which fetches the timezone and coordinates from geonames.
78
+ If you already have the coordinates and timezone, set this to False. Defaults to True.
79
+ - disable_chiron: Deprecated, use disable_chiron_and_lilith instead.
80
+ - sidereal_mode (SiderealMode, optional): Also known as Ayanamsa.
81
+ The mode to use for the sidereal zodiac, according to the Swiss Ephemeris.
82
+ Defaults to "FAGAN_BRADLEY".
83
+ Available modes are visible in the SiderealMode Literal.
84
+ - houses_system_identifier (HousesSystemIdentifier, optional): The system to use for the calculation of the houses.
85
+ Defaults to "P" (Placidus).
86
+ Available systems are visible in the HousesSystemIdentifier Literal.
87
+ - perspective_type (PerspectiveType, optional): The perspective to use for the calculation of the chart.
88
+ Defaults to "Apparent Geocentric".
89
+ Available perspectives are visible in the PerspectiveType Literal.
90
+ - is_dst (Union[None, bool], optional): Specify if the time is in DST. Defaults to None.
91
+ By default (None), the library will try to guess if the time is in DST or not and raise an AmbiguousTimeError
92
+ if it can't guess. If you know the time is in DST, set this to True, if you know it's not, set it to False.
93
+ - disable_chiron_and_lilith (bool, optional): boolean representing if Chiron and Lilith should be disabled. Default is False.
60
94
  Chiron calculation can create some issues with the Swiss Ephemeris when the date is too far in the past.
61
95
  """
62
96
 
63
97
  # Defined by the user
64
98
  name: str
65
- utc_datetime: Union[datetime, None]
66
99
  year: int
67
100
  month: int
68
101
  day: int
69
102
  hour: int
70
103
  minute: int
71
- city: str
72
- nation: str
73
- lng: Union[int, float]
74
- lat: Union[int, float]
75
- tz_str: str
76
- geonames_username: str
104
+ city: Union[str, None]
105
+ nation: Union[str, None]
106
+ lng: Union[int, float, None]
107
+ lat: Union[int, float, None]
108
+ tz_str: Union[str, None]
109
+ geonames_username: Union[str, None]
77
110
  online: bool
78
111
  zodiac_type: ZodiacType
112
+ sidereal_mode: Union[SiderealMode, None]
113
+ houses_system_identifier: HousesSystemIdentifier
114
+ houses_system_name: str
115
+ perspective_type: PerspectiveType
116
+ is_dst: Union[None, bool]
79
117
 
80
118
  # Generated internally
81
119
  city_data: dict[str, str]
82
120
  julian_day: Union[int, float]
83
- utc_time: float
84
- local_time: float
85
- utc: datetime
86
121
  json_dir: Path
122
+ iso_formatted_local_datetime: str
123
+ iso_formatted_utc_datetime: str
87
124
 
88
125
  # Planets
89
126
  sun: KerykeionPointModel
@@ -99,6 +136,7 @@ class AstrologicalSubject:
99
136
  true_node: KerykeionPointModel
100
137
  mean_node: KerykeionPointModel
101
138
  chiron: Union[KerykeionPointModel, None]
139
+ mean_lilit: Union[KerykeionPointModel, None]
102
140
 
103
141
  # Houses
104
142
  first_house: KerykeionPointModel
@@ -120,35 +158,48 @@ class AstrologicalSubject:
120
158
  planets_degrees_ut: list[float]
121
159
  houses_degree_ut: list[float]
122
160
 
123
- now = datetime.now()
161
+ # Enable or disable features
162
+ disable_chiron: bool # Deprecated
163
+ disable_chiron_and_lilith: bool
124
164
 
125
165
  def __init__(
126
166
  self,
127
167
  name="Now",
128
- year: int = now.year,
129
- month: int = now.month,
130
- day: int = now.day,
131
- hour: int = now.hour,
132
- minute: int = now.minute,
168
+ year: int = NOW.year,
169
+ month: int = NOW.month,
170
+ day: int = NOW.day,
171
+ hour: int = NOW.hour,
172
+ minute: int = NOW.minute,
133
173
  city: Union[str, None] = None,
134
174
  nation: Union[str, None] = None,
135
175
  lng: Union[int, float, None] = None,
136
176
  lat: Union[int, float, None] = None,
137
177
  tz_str: Union[str, None] = None,
138
178
  geonames_username: Union[str, None] = None,
139
- zodiac_type: ZodiacType = "Tropic",
179
+ zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE,
140
180
  online: bool = True,
141
- utc_datetime: Union[datetime, None] = None,
142
- disable_chiron: bool = False
181
+ disable_chiron: Union[None, bool] = None,
182
+ sidereal_mode: Union[SiderealMode, None] = None,
183
+ houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
184
+ perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE,
185
+ is_dst: Union[None, bool] = None,
186
+ disable_chiron_and_lilith: bool = False
143
187
  ) -> None:
144
188
  logging.debug("Starting Kerykeion")
145
189
 
146
- # We set the swisseph path to the current directory
147
- swe.set_ephe_path(
148
- str(
149
- Path(__file__).parent.absolute() / "sweph"
190
+ # Deprecation warnings --->
191
+ if disable_chiron is not None:
192
+ warnings.warn(
193
+ "The 'disable_chiron' argument is deprecated and will be removed in a future version. "
194
+ "Please use 'disable_chiron' instead.",
195
+ DeprecationWarning
150
196
  )
151
- )
197
+
198
+ if disable_chiron_and_lilith:
199
+ raise ValueError("Cannot specify both 'disable_chiron' and 'disable_chiron_and_lilith'. Use 'disable_chiron_and_lilith' only.")
200
+
201
+ self.disable_chiron_and_lilith = disable_chiron
202
+ # <--- Deprecation warnings
152
203
 
153
204
  self.name = name
154
205
  self.year = year
@@ -165,27 +216,20 @@ class AstrologicalSubject:
165
216
  self.online = online
166
217
  self.json_dir = Path.home()
167
218
  self.geonames_username = geonames_username
168
- self.utc_datetime = utc_datetime
169
219
  self.disable_chiron = disable_chiron
220
+ self.sidereal_mode = sidereal_mode
221
+ self.houses_system_identifier = houses_system_identifier
222
+ self.perspective_type = perspective_type
223
+ self.is_dst = is_dst
224
+ self.disable_chiron_and_lilith = disable_chiron_and_lilith
225
+
226
+ #---------------#
227
+ # General setup #
228
+ #---------------#
170
229
 
171
230
  # This message is set to encourage the user to set a custom geonames username
172
231
  if geonames_username is None and online:
173
- logging.warning(
174
- "\n"
175
- "********" +
176
- "\n" +
177
- "NO GEONAMES USERNAME SET!" +
178
- "\n" +
179
- "Using the default geonames username is not recommended, please set a custom one!" +
180
- "\n" +
181
- "You can get one for free here:" +
182
- "\n" +
183
- "https://www.geonames.org/login" +
184
- "\n" +
185
- "Keep in mind that the default username is limited to 2000 requests per hour and is shared with everyone else using this library." +
186
- "\n" +
187
- "********"
188
- )
232
+ logging.warning(GEONAMES_DEFAULT_USERNAME_WARNING)
189
233
 
190
234
  self.geonames_username = DEFAULT_GEONAMES_USERNAME
191
235
 
@@ -197,36 +241,117 @@ class AstrologicalSubject:
197
241
  self.nation = "GB"
198
242
  logging.info("No nation specified, using GB as default")
199
243
 
200
- if not self.lat:
244
+ if not self.lat and not self.online:
201
245
  self.lat = 51.5074
202
246
  logging.info("No latitude specified, using London as default")
203
247
 
204
- if not self.lng:
248
+ if not self.lng and not self.online:
205
249
  self.lng = 0
206
250
  logging.info("No longitude specified, using London as default")
207
251
 
208
252
  if (not self.online) and (not tz_str):
209
- raise KerykeionException(
210
- "You need to set the coordinates and timezone if you want to use the offline mode!"
211
- )
253
+ raise KerykeionException("You need to set the coordinates and timezone if you want to use the offline mode!")
254
+
255
+ #-----------------------#
256
+ # Swiss Ephemeris setup #
257
+ #-----------------------#
258
+
259
+ # We set the swisseph path to the current directory
260
+ swe.set_ephe_path(str(Path(__file__).parent.absolute() / "sweph"))
212
261
 
213
- self._check_if_poles()
262
+ # Flags for the Swiss Ephemeris
263
+ self._iflag = swe.FLG_SWIEPH + swe.FLG_SPEED
264
+
265
+ # Chart Perspective check and setup --->
266
+ if self.perspective_type not in get_args(PerspectiveType):
267
+ raise KerykeionException(f"\n* ERROR: '{self.perspective_type}' is NOT a valid chart perspective! Available perspectives are: *" + "\n" + str(get_args(PerspectiveType)))
268
+
269
+ if self.perspective_type == "True Geocentric":
270
+ self._iflag += swe.FLG_TRUEPOS
271
+ elif self.perspective_type == "Heliocentric":
272
+ self._iflag += swe.FLG_HELCTR
273
+ elif self.perspective_type == "Topocentric":
274
+ self._iflag += swe.FLG_TOPOCTR
275
+ # geopos_is_set, for topocentric
276
+ if (self.online) and (not self.tz_str) and (not self.lat) and (not self.lng):
277
+ self._fetch_and_set_tz_and_coordinates_from_geonames()
278
+ swe.set_topo(self.lng, self.lat, 0)
279
+ # <--- Chart Perspective check and setup
280
+
281
+ # House System check and setup --->
282
+ if self.houses_system_identifier not in get_args(HousesSystemIdentifier):
283
+ raise KerykeionException(f"\n* ERROR: '{self.houses_system_identifier}' is NOT a valid house system! Available systems are: *" + "\n" + str(get_args(HousesSystemIdentifier)))
284
+
285
+ self.houses_system_name = swe.house_name(self.houses_system_identifier.encode('ascii'))
286
+ # <--- House System check and setup
287
+
288
+ # Zodiac Type and Sidereal mode checks and setup --->
289
+ if zodiac_type and not zodiac_type in get_args(ZodiacType):
290
+ raise KerykeionException(f"\n* ERROR: '{zodiac_type}' is NOT a valid zodiac type! Available types are: *" + "\n" + str(get_args(ZodiacType)))
291
+
292
+ if self.sidereal_mode and self.zodiac_type == "Tropic":
293
+ raise KerykeionException("You can't set a sidereal mode with a Tropic zodiac type!")
294
+
295
+ if self.zodiac_type == "Sidereal" and not self.sidereal_mode:
296
+ self.sidereal_mode = DEFAULT_SIDEREAL_MODE
297
+ logging.info("No sidereal mode set, using default FAGAN_BRADLEY")
298
+
299
+ if self.zodiac_type == "Sidereal":
300
+ # Check if the sidereal mode is valid
301
+ if not self.sidereal_mode in get_args(SiderealMode):
302
+ raise KerykeionException(f"\n* ERROR: '{self.sidereal_mode}' is NOT a valid sidereal mode! Available modes are: *" + "\n" + str(get_args(SiderealMode)))
303
+
304
+ self._iflag += swe.FLG_SIDEREAL
305
+ mode = "SIDM_" + self.sidereal_mode
306
+ swe.set_sid_mode(getattr(swe, mode))
307
+ logging.debug(f"Using sidereal mode: {mode}")
308
+ # <--- Zodiac Type and Sidereal mode checks and setup
309
+
310
+ #------------------------#
311
+ # Start the calculations #
312
+ #------------------------#
313
+
314
+ # UTC, julian day and local time setup --->
315
+ if (self.online) and (not self.tz_str) and (not self.lat) and (not self.lng):
316
+ self._fetch_and_set_tz_and_coordinates_from_geonames()
317
+
318
+ self.lat = check_and_adjust_polar_latitude(self.lat)
319
+
320
+ # Local time to UTC
321
+ local_time = pytz.timezone(self.tz_str)
322
+ naive_datetime = datetime(self.year, self.month, self.day, self.hour, self.minute, 0)
323
+
324
+ try:
325
+ local_datetime = local_time.localize(naive_datetime, is_dst=self.is_dst)
326
+ except pytz.exceptions.AmbiguousTimeError:
327
+ raise KerykeionException("Ambiguous time! Please specify if the time is in DST or not with the is_dst argument.")
328
+
329
+ utc_object = local_datetime.astimezone(pytz.utc)
330
+ self.iso_formatted_utc_datetime = utc_object.isoformat()
331
+
332
+ # ISO formatted local datetime
333
+ self.iso_formatted_local_datetime = local_datetime.isoformat()
334
+
335
+ # Julian day calculation
336
+ utc_float_hour_with_minutes = utc_object.hour + (utc_object.minute / 60)
337
+ self.julian_day = float(swe.julday(utc_object.year, utc_object.month, utc_object.day, utc_float_hour_with_minutes))
338
+ # <--- UTC, julian day and local time setup
214
339
 
215
- # Initialize everything
216
- self._get_utc()
217
- self._get_jd()
218
340
  self._planets_degrees_lister()
219
341
  self._planets()
220
342
  self._houses()
221
-
222
343
  self._planets_in_houses()
223
344
  self._lunar_phase_calc()
224
345
 
346
+ # Deprecated properties
347
+ self.utc_time
348
+ self.local_time
349
+
225
350
  def __str__(self) -> str:
226
- return f"Astrological data for: {self.name}, {self.utc} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}"
351
+ return f"Astrological data for: {self.name}, {self.iso_formatted_utc_datetime} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}"
227
352
 
228
353
  def __repr__(self) -> str:
229
- return f"Astrological data for: {self.name}, {self.utc} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}"
354
+ return f"Astrological data for: {self.name}, {self.iso_formatted_utc_datetime} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}"
230
355
 
231
356
  def __getitem__(self, item):
232
357
  return getattr(self, item)
@@ -234,7 +359,7 @@ class AstrologicalSubject:
234
359
  def get(self, item, default=None):
235
360
  return getattr(self, item, default)
236
361
 
237
- def _fetch_tz_from_geonames(self) -> None:
362
+ def _fetch_and_set_tz_and_coordinates_from_geonames(self) -> None:
238
363
  """Gets the nearest time zone for the calculation"""
239
364
  logging.info("Fetching timezone/coordinates from geonames")
240
365
 
@@ -258,33 +383,6 @@ class AstrologicalSubject:
258
383
  self.lat = float(self.city_data["lat"])
259
384
  self.tz_str = self.city_data["timezonestr"]
260
385
 
261
- self._check_if_poles()
262
-
263
- def _get_utc(self) -> None:
264
- """Converts local time to utc time."""
265
-
266
- # If the coordinates are not set, get them from geonames.
267
- if (self.online) and (not self.tz_str):
268
- self._fetch_tz_from_geonames()
269
-
270
- # If UTC datetime is provided, then use it directly
271
- if (self.utc_datetime):
272
- self.utc = self.utc_datetime
273
- return
274
-
275
- local_time = pytz.timezone(self.tz_str)
276
-
277
- naive_datetime = datetime(self.year, self.month, self.day, self.hour, self.minute, 0)
278
-
279
- local_datetime = local_time.localize(naive_datetime, is_dst=None)
280
- self.utc = local_datetime.astimezone(pytz.utc)
281
-
282
- def _get_jd(self) -> None:
283
- """Calculates julian day from the utc time."""
284
- self.utc_time = self.utc.hour + self.utc.minute / 60
285
- self.local_time = self.hour + self.minute / 60
286
- self.julian_day = float(swe.julday(self.utc.year, self.utc.month, self.utc.day, self.utc_time))
287
-
288
386
  def _houses(self) -> None:
289
387
  """
290
388
  Calculate positions and store them in dictionaries
@@ -321,18 +419,21 @@ class AstrologicalSubject:
321
419
 
322
420
  if self.zodiac_type == "Sidereal":
323
421
  self.houses_degree_ut = swe.houses_ex(
324
- tjdut=self.julian_day, lat=self.lat, lon=self.lng, hsys=str.encode('P'), flags=swe.FLG_SIDEREAL
422
+ tjdut=self.julian_day,
423
+ lat=self.lat, lon=self.lng,
424
+ hsys=str.encode(self.houses_system_identifier),
425
+ flags=swe.FLG_SIDEREAL
325
426
  )[0]
427
+
326
428
  elif self.zodiac_type == "Tropic":
327
429
  self.houses_degree_ut = swe.houses(
328
- tjdut=self.julian_day, lat=self.lat, lon=self.lng, hsys=str.encode('P')
430
+ tjdut=self.julian_day, lat=self.lat,
431
+ lon=self.lng,
432
+ hsys=str.encode(self.houses_system_identifier)
329
433
  )[0]
330
- else:
331
- raise KerykeionException("Zodiac type not recognized! Please use 'Tropic' or 'Sidereal'")
332
434
 
333
- point_type: Literal["Planet", "House"] = "House"
334
- # creates the list of the house in 360°
335
- self.houses_degree_ut = swe.houses(self.julian_day, self.lat, self.lng)[0]
435
+ point_type: PointType = "House"
436
+
336
437
  # stores the house in singular dictionaries.
337
438
  self.first_house = calculate_position(self.houses_degree_ut[0], "First_House", point_type=point_type)
338
439
  self.second_house = calculate_position(self.houses_degree_ut[1], "Second_House", point_type=point_type)
@@ -364,12 +465,6 @@ class AstrologicalSubject:
364
465
 
365
466
  def _planets_degrees_lister(self):
366
467
  """Sidereal or tropic mode."""
367
- self._iflag = swe.FLG_SWIEPH + swe.FLG_SPEED
368
-
369
- if self.zodiac_type == "Sidereal":
370
- self._iflag += swe.FLG_SIDEREAL
371
- mode = "SIDM_FAGAN_BRADLEY"
372
- swe.set_sid_mode(getattr(swe, mode))
373
468
 
374
469
  # Calculates the position of the planets and stores it in a list.
375
470
  sun_deg = swe.calc(self.julian_day, 0, self._iflag)[0][0]
@@ -384,11 +479,6 @@ class AstrologicalSubject:
384
479
  pluto_deg = swe.calc(self.julian_day, 9, self._iflag)[0][0]
385
480
  mean_node_deg = swe.calc(self.julian_day, 10, self._iflag)[0][0]
386
481
  true_node_deg = swe.calc(self.julian_day, 11, self._iflag)[0][0]
387
-
388
- if not self.disable_chiron:
389
- chiron_deg = swe.calc(self.julian_day, 15, self._iflag)[0][0]
390
- else:
391
- chiron_deg = 0
392
482
 
393
483
  self.planets_degrees_ut = [
394
484
  sun_deg,
@@ -405,14 +495,23 @@ class AstrologicalSubject:
405
495
  true_node_deg,
406
496
  ]
407
497
 
408
- if not self.disable_chiron:
498
+ if not self.disable_chiron_and_lilith:
499
+ chiron_deg = swe.calc(self.julian_day, 15, self._iflag)[0][0]
409
500
  self.planets_degrees_ut.append(chiron_deg)
410
501
 
502
+ mean_lilith_deg = swe.calc(self.julian_day, 12, self._iflag)[0][0]
503
+ self.planets_degrees_ut.append(mean_lilith_deg)
504
+
505
+ else:
506
+ self.chiron = None
507
+ self.mean_lilith = None
508
+
509
+
411
510
  def _planets(self) -> None:
412
511
  """Defines body positon in signs and information and
413
512
  stores them in dictionaries"""
414
513
 
415
- point_type: Literal["Planet", "House"] = "Planet"
514
+ point_type: PointType = "Planet"
416
515
  # stores the planets in singular dictionaries.
417
516
  self.sun = calculate_position(self.planets_degrees_ut[0], "Sun", point_type=point_type)
418
517
  self.moon = calculate_position(self.planets_degrees_ut[1], "Moon", point_type=point_type)
@@ -427,10 +526,14 @@ class AstrologicalSubject:
427
526
  self.mean_node = calculate_position(self.planets_degrees_ut[10], "Mean_Node", point_type=point_type)
428
527
  self.true_node = calculate_position(self.planets_degrees_ut[11], "True_Node", point_type=point_type)
429
528
 
430
- if not self.disable_chiron:
529
+ if not self.disable_chiron_and_lilith:
431
530
  self.chiron = calculate_position(self.planets_degrees_ut[12], "Chiron", point_type=point_type)
531
+ self.mean_lilith = calculate_position(self.planets_degrees_ut[13], "Mean_Lilith", point_type=point_type)
532
+
432
533
  else:
433
534
  self.chiron = None
535
+ self.mean_lilith = None
536
+
434
537
 
435
538
  def _planets_in_houses(self) -> None:
436
539
  """Calculates the house of the planet and updates
@@ -449,11 +552,6 @@ class AstrologicalSubject:
449
552
  self.mean_node.house = get_planet_house(self.planets_degrees_ut[10], self.houses_degree_ut)
450
553
  self.true_node.house = get_planet_house(self.planets_degrees_ut[11], self.houses_degree_ut)
451
554
 
452
- if not self.disable_chiron:
453
- self.chiron.house = get_planet_house(self.planets_degrees_ut[12], self.houses_degree_ut)
454
- else:
455
- self.chiron = None
456
-
457
555
  self.planets_list = [
458
556
  self.sun,
459
557
  self.moon,
@@ -469,18 +567,26 @@ class AstrologicalSubject:
469
567
  self.true_node,
470
568
  ]
471
569
 
472
- if not self.disable_chiron:
570
+ if not self.disable_chiron_and_lilith:
571
+ self.chiron.house = get_planet_house(self.planets_degrees_ut[12], self.houses_degree_ut)
473
572
  self.planets_list.append(self.chiron)
474
573
 
574
+ self.mean_lilith.house = get_planet_house(self.planets_degrees_ut[13], self.houses_degree_ut)
575
+ self.planets_list.append(self.mean_lilith)
576
+
577
+ else:
578
+ self.chiron = None
579
+ self.mean_lilith = None
580
+
475
581
  # Check in retrograde or not:
476
582
  planets_ret = []
477
- for p in self.planets_list:
478
- planet_number = get_number_from_name(p["name"])
583
+ for planet in self.planets_list:
584
+ planet_number = get_number_from_name(planet["name"])
479
585
  if swe.calc(self.julian_day, planet_number, self._iflag)[0][3] < 0:
480
- p["retrograde"] = True
586
+ planet["retrograde"] = True
481
587
  else:
482
- p["retrograde"] = False
483
- planets_ret.append(p)
588
+ planet["retrograde"] = False
589
+ planets_ret.append(planet)
484
590
 
485
591
  def _lunar_phase_calc(self) -> None:
486
592
  """Function to calculate the lunar phase"""
@@ -555,20 +661,7 @@ class AstrologicalSubject:
555
661
 
556
662
  self.lunar_phase = LunarPhaseModel(**lunar_phase_dictionary)
557
663
 
558
- def _check_if_poles(self):
559
- """
560
- Utility function to check if the location is in the polar circle.
561
- If it is, it sets the latitude to 66 or -66 degrees.
562
- """
563
- if self.lat > 66.0:
564
- self.lat = 66.0
565
- logging.info("Polar circle override for houses, using 66 degrees")
566
-
567
- elif self.lat < -66.0:
568
- self.lat = -66.0
569
- logging.info("Polar circle override for houses, using -66 degrees")
570
-
571
- def json(self, dump=False, destination_folder: Union[str, None] = None) -> str:
664
+ def json(self, dump=False, destination_folder: Union[str, None] = None, indent: Union[int, None] = None) -> str:
572
665
  """
573
666
  Dumps the Kerykeion object to a json string foramt,
574
667
  if dump=True also dumps to file located in destination
@@ -576,8 +669,7 @@ class AstrologicalSubject:
576
669
  """
577
670
 
578
671
  KrData = AstrologicalSubjectModel(**self.__dict__)
579
- json_string = KrData.model_dump_json(exclude_none=True)
580
- print(json_string)
672
+ json_string = KrData.model_dump_json(exclude_none=True, indent=indent)
581
673
 
582
674
  if dump:
583
675
  if destination_folder:
@@ -600,6 +692,131 @@ class AstrologicalSubject:
600
692
 
601
693
  return AstrologicalSubjectModel(**self.__dict__)
602
694
 
695
+ @cached_property
696
+ def utc_time(self) -> float:
697
+ """
698
+ Deprecated property, use iso_formatted_utc_datetime instead, will be removed in the future.
699
+ Returns the UTC time as a float.
700
+ """
701
+ dt = datetime.fromisoformat(self.iso_formatted_utc_datetime)
702
+
703
+ # Extract the hours, minutes, and seconds
704
+ hours = dt.hour
705
+ minutes = dt.minute
706
+ seconds = dt.second + dt.microsecond / 1_000_000
707
+
708
+ # Convert time to float hours
709
+ float_time = hours + minutes / 60 + seconds / 3600
710
+
711
+ return float_time
712
+
713
+ @cached_property
714
+ def local_time(self) -> float:
715
+ """
716
+ Deprecated property, use iso_formatted_local_datetime instead, will be removed in the future.
717
+ Returns the local time as a float.
718
+ """
719
+ dt = datetime.fromisoformat(self.iso_formatted_local_datetime)
720
+
721
+ # Extract the hours, minutes, and seconds
722
+ hours = dt.hour
723
+ minutes = dt.minute
724
+ seconds = dt.second + dt.microsecond / 1_000_000
725
+
726
+ # Convert time to float hours
727
+ float_time = hours + minutes / 60 + seconds / 3600
728
+
729
+ return float_time
730
+
731
+
732
+ @staticmethod
733
+ def get_from_iso_utc_time(
734
+ name: str,
735
+ iso_utc_time: str,
736
+ city: str = "Greenwich",
737
+ nation: str = "GB",
738
+ tz_str: str = "Etc/GMT",
739
+ online: bool = False,
740
+ lng: Union[int, float] = 0,
741
+ lat: Union[int, float] = 51.5074,
742
+ geonames_username: str = DEFAULT_GEONAMES_USERNAME,
743
+ zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE,
744
+ disable_chiron_and_lilith: bool = False,
745
+ sidereal_mode: Union[SiderealMode, None] = None,
746
+ houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
747
+ perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE
748
+
749
+ ) -> "AstrologicalSubject":
750
+ """
751
+ Creates an AstrologicalSubject object from an iso formatted UTC time.
752
+ This method is offline by default, set online=True to fetch the timezone and coordinates from geonames.
753
+
754
+ Args:
755
+ - name (str): The name of the subject.
756
+ - iso_utc_time (str): The iso formatted UTC time.
757
+ - city (str, optional): City or location of birth. Defaults to "Greenwich".
758
+ - nation (str, optional): Nation of birth. Defaults to "GB".
759
+ - tz_str (str, optional): Timezone of the birth location. Defaults to "Etc/GMT".
760
+ - online (bool, optional): Sets if you want to use the online mode, which fetches the timezone and coordinates from geonames.
761
+ If you already have the coordinates and timezone, set this to False. Defaults to False.
762
+ - lng (Union[int, float], optional): Longitude of the birth location. Defaults to 0 (Greenwich, London).
763
+ - lat (Union[int, float], optional): Latitude of the birth location. Defaults to 51.5074 (Greenwich, London).
764
+ - geonames_username (str, optional): The username for the geonames API. Note: Change this to your own username to avoid rate limits!
765
+ You can get one for free here: https://www.geonames.org/login
766
+ - zodiac_type (ZodiacType, optional): The zodiac type to use. Defaults to "Tropic".
767
+ - disable_chiron_and_lilith: boolean representing if Chiron and Lilith should be disabled. Default is False.
768
+ Chiron calculation can create some issues with the Swiss Ephemeris when the date is too far in the past.
769
+ - sidereal_mode (SiderealMode, optional): Also known as Ayanamsa.
770
+ The mode to use for the sidereal zodiac, according to the Swiss Ephemeris.
771
+ Defaults to None.
772
+ Available modes are visible in the SiderealMode Literal.
773
+ - houses_system_identifier (HousesSystemIdentifier, optional): The system to use for the calculation of the houses.
774
+ Defaults to "P" (Placidus).
775
+ Available systems are visible in the HousesSystemIdentifier Literal.
776
+ - perspective_type (PerspectiveType, optional): The perspective to use for the calculation of the chart.
777
+ Defaults to "Apparent Geocentric".
778
+
779
+ Returns:
780
+ - AstrologicalSubject: The AstrologicalSubject object.
781
+ """
782
+ dt = datetime.fromisoformat(iso_utc_time)
783
+
784
+ if online == True:
785
+ if geonames_username == DEFAULT_GEONAMES_USERNAME:
786
+ logging.warning(GEONAMES_DEFAULT_USERNAME_WARNING)
787
+
788
+ geonames = FetchGeonames(
789
+ city,
790
+ nation,
791
+ username=geonames_username,
792
+ )
793
+
794
+ city_data: dict[str, str] = geonames.get_serialized_data()
795
+ lng = float(city_data["lng"])
796
+ lat = float(city_data["lat"])
797
+
798
+ subject = AstrologicalSubject(
799
+ name=name,
800
+ year=dt.year,
801
+ month=dt.month,
802
+ day=dt.day,
803
+ hour=dt.hour,
804
+ minute=dt.minute,
805
+ city=city,
806
+ nation=city,
807
+ lng=lng,
808
+ lat=lat,
809
+ tz_str=tz_str,
810
+ online=False,
811
+ geonames_username=geonames_username,
812
+ zodiac_type=zodiac_type,
813
+ sidereal_mode=sidereal_mode,
814
+ houses_system_identifier=houses_system_identifier,
815
+ perspective_type=perspective_type,
816
+ disable_chiron_and_lilith=disable_chiron_and_lilith
817
+ )
818
+
819
+ return subject
603
820
 
604
821
  if __name__ == "__main__":
605
822
  import json
@@ -620,3 +837,26 @@ if __name__ == "__main__":
620
837
 
621
838
  print('\n')
622
839
  print(johnny.chiron)
840
+
841
+ # With Sidereal Zodiac
842
+ johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", zodiac_type="Sidereal", sidereal_mode="LAHIRI")
843
+ print(johnny.json(dump=True, indent=2))
844
+
845
+ # With Morinus Houses
846
+ johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", houses_system_identifier="M")
847
+ print(johnny.json(dump=True, indent=2))
848
+
849
+ # With True Geocentric Perspective
850
+ johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", perspective_type="True Geocentric")
851
+ print(johnny.json(dump=True, indent=2))
852
+
853
+ # With Heliocentric Perspective
854
+ johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", perspective_type="Heliocentric")
855
+ print(johnny.json(dump=True, indent=2))
856
+
857
+ # With Topocentric Perspective
858
+ johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", perspective_type="Topocentric")
859
+
860
+ # Test Mean Lilith
861
+ johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", disable_chiron_and_lilith=True)
862
+ print(johnny.mean_lilith)