kerykeion 4.0.6__py3-none-any.whl → 4.12.3__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.

@@ -1,14 +1,14 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
- This is part of Kerykeion (C) 2023 Giacomo Battaglia
3
+ This is part of Kerykeion (C) 2024 Giacomo Battaglia
4
4
  """
5
5
 
6
- import math
7
6
  import pytz
8
7
  import swisseph as swe
8
+ import logging
9
9
 
10
10
  from datetime import datetime
11
- from logging import getLogger, basicConfig
11
+ from functools import cached_property
12
12
  from kerykeion.fetch_geonames import FetchGeonames
13
13
  from kerykeion.kr_types import (
14
14
  KerykeionException,
@@ -16,18 +16,39 @@ from kerykeion.kr_types import (
16
16
  AstrologicalSubjectModel,
17
17
  LunarPhaseModel,
18
18
  KerykeionPointModel,
19
+ PointType,
20
+ SiderealMode,
21
+ HousesSystemIdentifier,
22
+ PerspectiveType
23
+ )
24
+ from kerykeion.utilities import (
25
+ get_number_from_name,
26
+ calculate_position,
27
+ get_planet_house,
28
+ get_moon_emoji_from_phase_int,
29
+ get_moon_phase_name_from_phase_int,
30
+ check_and_adjust_polar_latitude
19
31
  )
20
- from kerykeion.utilities import get_number_from_name, calculate_position
21
32
  from pathlib import Path
22
- from typing import Union, Literal
23
-
24
-
25
- logger = getLogger(__name__)
26
- basicConfig(
27
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
28
- level="INFO"
33
+ from typing import Union, get_args
34
+
35
+ DEFAULT_GEONAMES_USERNAME = "century.boy"
36
+ DEFAULT_SIDEREAL_MODE = "FAGAN_BRADLEY"
37
+ DEFAULT_HOUSES_SYSTEM_IDENTIFIER = "P"
38
+ DEFAULT_ZODIAC_TYPE = "Tropic"
39
+ DEFAULT_PERSPECTIVE_TYPE = "Apparent Geocentric"
40
+ GEONAMES_DEFAULT_USERNAME_WARNING = (
41
+ "\n********\n"
42
+ "NO GEONAMES USERNAME SET!\n"
43
+ "Using the default geonames username is not recommended, please set a custom one!\n"
44
+ "You can get one for free here:\n"
45
+ "https://www.geonames.org/login\n"
46
+ "Keep in mind that the default username is limited to 2000 requests per hour and is shared with everyone else using this library.\n"
47
+ "********"
29
48
  )
30
49
 
50
+ NOW = datetime.now()
51
+
31
52
 
32
53
  class AstrologicalSubject:
33
54
  """
@@ -35,25 +56,37 @@ class AstrologicalSubject:
35
56
  it's utc and julian day and returns an object with all that data.
36
57
 
37
58
  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.
59
+ - name (str, optional): The name of the subject. Defaults to "Now".
60
+ - year (int, optional): The year of birth. Defaults to the current year.
61
+ - month (int, optional): The month of birth. Defaults to the current month.
62
+ - day (int, optional): The day of birth. Defaults to the current day.
63
+ - hour (int, optional): The hour of birth. Defaults to the current hour.
64
+ - minute (int, optional): Defaults to the current minute.
44
65
  - city (str, optional): City or location of birth. Defaults to "London", which is GMT time.
45
66
  The city argument is used to get the coordinates and timezone from geonames just in case
46
67
  you don't insert them manually (see _get_tz).
47
68
  If you insert the coordinates and timezone manually, the city argument is not used for calculations
48
69
  but it's still used as a value for the city attribute.
49
70
  - 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
- - logger (Union[Logger, None], optional): _ Defaults to None.
54
- - geonames_username (str, optional): _ Defaults to 'century.boy'.
55
- - online (bool, optional): Sets if you want to use the online mode (using
56
- geonames) or not. Defaults to True.
71
+ - lng (Union[int, float], optional): Longitude of the birth location. Defaults to 0 (Greenwich, London).
72
+ - lat (Union[int, float], optional): Latitude of the birth location. Defaults to 51.5074 (Greenwich, London).
73
+ - tz_str (Union[str, bool], optional): Timezone of the birth location. Defaults to "GMT".
74
+ - geonames_username (str, optional): The username for the geonames API. Note: Change this to your own username to avoid rate limits!
75
+ You can get one for free here: https://www.geonames.org/login
76
+ - online (bool, optional): Sets if you want to use the online mode, which fetches the timezone and coordinates from geonames.
77
+ If you already have the coordinates and timezone, set this to False. Defaults to True.
78
+ - disable_chiron (bool, optional): Disables the calculation of Chiron. Defaults to False.
79
+ Chiron calculation can create some issues with the Swiss Ephemeris when the date is too far in the past.
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.
57
90
  """
58
91
 
59
92
  # Defined by the user
@@ -71,14 +104,17 @@ class AstrologicalSubject:
71
104
  geonames_username: str
72
105
  online: bool
73
106
  zodiac_type: ZodiacType
107
+ sidereal_mode: SiderealMode
108
+ houses_system_identifier: HousesSystemIdentifier
109
+ houses_system_name: str
110
+ perspective_type: PerspectiveType
74
111
 
75
112
  # Generated internally
76
113
  city_data: dict[str, str]
77
114
  julian_day: Union[int, float]
78
- utc_time: float
79
- local_time: float
80
- utc: datetime
81
115
  json_dir: Path
116
+ iso_formatted_local_datetime: str
117
+ iso_formatted_utc_datetime: str
82
118
 
83
119
  # Planets
84
120
  sun: KerykeionPointModel
@@ -93,7 +129,7 @@ class AstrologicalSubject:
93
129
  pluto: KerykeionPointModel
94
130
  true_node: KerykeionPointModel
95
131
  mean_node: KerykeionPointModel
96
- chiron: KerykeionPointModel
132
+ chiron: Union[KerykeionPointModel, None]
97
133
 
98
134
  # Houses
99
135
  first_house: KerykeionPointModel
@@ -110,40 +146,33 @@ class AstrologicalSubject:
110
146
  twelfth_house: KerykeionPointModel
111
147
 
112
148
  # Lists
113
- houses: list[KerykeionPointModel]
114
- planets: list[KerykeionPointModel]
149
+ houses_list: list[KerykeionPointModel]
150
+ planets_list: list[KerykeionPointModel]
115
151
  planets_degrees_ut: list[float]
116
152
  houses_degree_ut: list[float]
117
153
 
118
- now = datetime.now()
119
-
120
-
121
154
  def __init__(
122
155
  self,
123
156
  name="Now",
124
- year: int = now.year,
125
- month: int = now.month,
126
- day: int = now.day,
127
- hour: int = now.hour,
128
- minute: int = now.minute,
129
- city: str = "",
130
- nation: str = "",
131
- lng: Union[int, float] = 0,
132
- lat: Union[int, float] = 0,
133
- tz_str: str = "",
134
- geonames_username: str = "century.boy",
135
- zodiac_type: ZodiacType = "Tropic",
157
+ year: int = NOW.year,
158
+ month: int = NOW.month,
159
+ day: int = NOW.day,
160
+ hour: int = NOW.hour,
161
+ minute: int = NOW.minute,
162
+ city: Union[str, None] = None,
163
+ nation: Union[str, None] = None,
164
+ lng: Union[int, float, None] = None,
165
+ lat: Union[int, float, None] = None,
166
+ tz_str: Union[str, None] = None,
167
+ geonames_username: Union[str, None] = None,
168
+ zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE,
136
169
  online: bool = True,
170
+ disable_chiron: bool = False,
171
+ sidereal_mode: Union[SiderealMode, None] = None,
172
+ houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
173
+ perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE
137
174
  ) -> None:
138
- logger.debug("Starting Kerykeion")
139
-
140
- # We set the swisseph path to the current directory
141
- swe.set_ephe_path(
142
- str(
143
- Path(__file__).parent.absolute() / "sweph"
144
- )
145
- )
146
-
175
+ logging.debug("Starting Kerykeion")
147
176
 
148
177
  self.name = name
149
178
  self.year = year
@@ -156,48 +185,154 @@ class AstrologicalSubject:
156
185
  self.lng = lng
157
186
  self.lat = lat
158
187
  self.tz_str = tz_str
159
- self._geonames_username = geonames_username
160
188
  self.zodiac_type = zodiac_type
161
189
  self.online = online
162
190
  self.json_dir = Path.home()
191
+ self.geonames_username = geonames_username
192
+ self.disable_chiron = disable_chiron
193
+ self.sidereal_mode = sidereal_mode
194
+ self.houses_system_identifier = houses_system_identifier
195
+ self.perspective_type = perspective_type
196
+
197
+ #---------------#
198
+ # General setup #
199
+ #---------------#
200
+
201
+ # This message is set to encourage the user to set a custom geonames username
202
+ if geonames_username is None and online:
203
+ logging.warning(
204
+
205
+ )
206
+
207
+ self.geonames_username = DEFAULT_GEONAMES_USERNAME
163
208
 
164
209
  if not self.city:
165
210
  self.city = "London"
166
- logger.warning("No city specified, using London as default")
211
+ logging.info("No city specified, using London as default")
167
212
 
168
213
  if not self.nation:
169
214
  self.nation = "GB"
170
- logger.warning("No nation specified, using GB as default")
215
+ logging.info("No nation specified, using GB as default")
171
216
 
172
- if (not self.online) and (not lng or not lat or not tz_str):
173
- raise KerykeionException(
174
- "You need to set the coordinates and timezone if you want to use the offline mode!"
175
- )
217
+ if not self.lat:
218
+ self.lat = 51.5074
219
+ logging.info("No latitude specified, using London as default")
220
+
221
+ if not self.lng:
222
+ self.lng = 0
223
+ logging.info("No longitude specified, using London as default")
224
+
225
+ if (not self.online) and (not tz_str):
226
+ raise KerykeionException("You need to set the coordinates and timezone if you want to use the offline mode!")
227
+
228
+ #-----------------------#
229
+ # Swiss Ephemeris setup #
230
+ #-----------------------#
231
+
232
+ # We set the swisseph path to the current directory
233
+ swe.set_ephe_path(str(Path(__file__).parent.absolute() / "sweph"))
234
+
235
+ # Flags for the Swiss Ephemeris
236
+ self._iflag = swe.FLG_SWIEPH + swe.FLG_SPEED
237
+
238
+ # Chart Perspective check and setup --->
239
+ if self.perspective_type not in get_args(PerspectiveType):
240
+ raise KerykeionException(f"\n* ERROR: '{self.perspective_type}' is NOT a valid chart perspective! Available perspectives are: *" + "\n" + str(get_args(PerspectiveType)))
241
+
242
+ if self.perspective_type == "True Geocentric":
243
+ self._iflag += swe.FLG_TRUEPOS
244
+ elif self.perspective_type == "Heliocentric":
245
+ self._iflag += swe.FLG_HELCTR
246
+ elif self.perspective_type == "Topocentric":
247
+ self._iflag += swe.FLG_TOPOCTR
248
+ # geopos_is_set, for topocentric
249
+ swe.set_topo(self.lng, self.lat, 0)
250
+ # <--- Chart Perspective check and setup
251
+
252
+ # House System check and setup --->
253
+ if self.houses_system_identifier not in get_args(HousesSystemIdentifier):
254
+ raise KerykeionException(f"\n* ERROR: '{self.houses_system_identifier}' is NOT a valid house system! Available systems are: *" + "\n" + str(get_args(HousesSystemIdentifier)))
255
+
256
+ self.houses_system_name = swe.house_name(self.houses_system_identifier.encode('ascii'))
257
+ # <--- House System check and setup
258
+
259
+ # Zodiac Type and Sidereal mode checks and setup --->
260
+ if zodiac_type and not zodiac_type in get_args(ZodiacType):
261
+ raise KerykeionException(f"\n* ERROR: '{zodiac_type}' is NOT a valid zodiac type! Available types are: *" + "\n" + str(get_args(ZodiacType)))
262
+
263
+ if self.sidereal_mode and self.zodiac_type == "Tropic":
264
+ raise KerykeionException("You can't set a sidereal mode with a Tropic zodiac type!")
265
+
266
+ if self.zodiac_type == "Sidereal" and not self.sidereal_mode:
267
+ self.sidereal_mode = DEFAULT_SIDEREAL_MODE
268
+ logging.info("No sidereal mode set, using default FAGAN_BRADLEY")
269
+
270
+ if self.zodiac_type == "Sidereal":
271
+ # Check if the sidereal mode is valid
272
+ if not self.sidereal_mode in get_args(SiderealMode):
273
+ raise KerykeionException(f"\n* ERROR: '{self.sidereal_mode}' is NOT a valid sidereal mode! Available modes are: *" + "\n" + str(get_args(SiderealMode)))
274
+
275
+ self._iflag += swe.FLG_SIDEREAL
276
+ mode = "SIDM_" + self.sidereal_mode
277
+ swe.set_sid_mode(getattr(swe, mode))
278
+ logging.debug(f"Using sidereal mode: {mode}")
279
+ # <--- Zodiac Type and Sidereal mode checks and setup
280
+
281
+ #------------------------#
282
+ # Start the calculations #
283
+ #------------------------#
284
+
285
+ check_and_adjust_polar_latitude(self.lat, self.lng)
286
+
287
+ # UTC, julian day and local time setup --->
288
+ if (self.online) and (not self.tz_str):
289
+ self._fetch_tz_from_geonames()
290
+
291
+ # Local time to UTC
292
+ local_time = pytz.timezone(self.tz_str)
293
+ naive_datetime = datetime(self.year, self.month, self.day, self.hour, self.minute, 0)
294
+ local_datetime = local_time.localize(naive_datetime, is_dst=None)
295
+ utc_object = local_datetime.astimezone(pytz.utc)
296
+ self.iso_formatted_utc_datetime = utc_object.isoformat()
297
+
298
+ # ISO formatted local datetime
299
+ self.iso_formatted_local_datetime = local_datetime.isoformat()
300
+
301
+ # Julian day calculation
302
+ utc_float_hour_with_minutes = utc_object.hour + (utc_object.minute / 60)
303
+ self.julian_day = float(swe.julday(utc_object.year, utc_object.month, utc_object.day, utc_float_hour_with_minutes))
304
+ # <--- UTC, julian day and local time setup
176
305
 
177
- # Initialize everything
178
- self._get_utc()
179
- self._get_jd()
180
306
  self._planets_degrees_lister()
181
307
  self._planets()
182
308
  self._houses()
183
-
184
309
  self._planets_in_houses()
185
310
  self._lunar_phase_calc()
186
311
 
312
+ # Deprecated properties
313
+ self.utc_time
314
+ self.local_time
315
+
187
316
  def __str__(self) -> str:
188
- return f"Astrological data for: {self.name}, {self.utc} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}"
317
+ return f"Astrological data for: {self.name}, {self.iso_formatted_utc_datetime} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}"
189
318
 
190
319
  def __repr__(self) -> str:
191
- return f"Astrological data for: {self.name}, {self.utc} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}"
320
+ return f"Astrological data for: {self.name}, {self.iso_formatted_utc_datetime} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}"
321
+
322
+ def __getitem__(self, item):
323
+ return getattr(self, item)
324
+
325
+ def get(self, item, default=None):
326
+ return getattr(self, item, default)
192
327
 
193
328
  def _fetch_tz_from_geonames(self) -> None:
194
329
  """Gets the nearest time zone for the calculation"""
195
- logger.debug("Conneting to Geonames...")
330
+ logging.info("Fetching timezone/coordinates from geonames")
196
331
 
197
332
  geonames = FetchGeonames(
198
333
  self.city,
199
334
  self.nation,
200
- username=self._geonames_username,
335
+ username=self.geonames_username,
201
336
  )
202
337
  self.city_data: dict[str, str] = geonames.get_serialized_data()
203
338
 
@@ -214,39 +349,59 @@ class AstrologicalSubject:
214
349
  self.lat = float(self.city_data["lat"])
215
350
  self.tz_str = self.city_data["timezonestr"]
216
351
 
217
- if self.lat > 66.0:
218
- self.lat = 66.0
219
- logger.info("Polar circle override for houses, using 66 degrees")
220
-
221
- elif self.lat < -66.0:
222
- self.lat = -66.0
223
- logger.info("Polar circle override for houses, using -66 degrees")
352
+ check_and_adjust_polar_latitude(self.lat, self.lng)
224
353
 
225
- def _get_utc(self) -> None:
226
- """Converts local time to utc time."""
227
-
228
- # If the coordinates are not set, get them from geonames.
229
- if (self.online) and (not self.tz_str or not self.lng or not self.lat):
230
- self._fetch_tz_from_geonames()
231
-
232
- local_time = pytz.timezone(self.tz_str)
233
-
234
- naive_datetime = datetime(self.year, self.month, self.day, self.hour, self.minute, 0)
235
-
236
- local_datetime = local_time.localize(naive_datetime, is_dst=None)
237
- self.utc = local_datetime.astimezone(pytz.utc)
354
+ def _houses(self) -> None:
355
+ """
356
+ Calculate positions and store them in dictionaries
357
+
358
+ https://www.astro.com/faq/fq_fh_owhouse_e.htm
359
+ https://github.com/jwmatthys/pd-swisseph/blob/master/swehouse.c#L685
360
+ hsys = letter code for house system;
361
+ A equal
362
+ E equal
363
+ B Alcabitius
364
+ C Campanus
365
+ D equal (MC)
366
+ F Carter "Poli-Equatorial"
367
+ G 36 Gauquelin sectors
368
+ H horizon / azimut
369
+ I Sunshine solution Treindl
370
+ i Sunshine solution Makransky
371
+ K Koch
372
+ L Pullen SD "sinusoidal delta", ex Neo-Porphyry
373
+ M Morinus
374
+ N equal/1=Aries
375
+ O Porphyry
376
+ P Placidus
377
+ Q Pullen SR "sinusoidal ratio"
378
+ R Regiomontanus
379
+ S Sripati
380
+ T Polich/Page ("topocentric")
381
+ U Krusinski-Pisa-Goelzer
382
+ V equal Vehlow
383
+ W equal, whole sign
384
+ X axial rotation system/ Meridian houses
385
+ Y APC houses
386
+ """
238
387
 
239
- def _get_jd(self) -> None:
240
- """Calculates julian day from the utc time."""
241
- self.utc_time = self.utc.hour + self.utc.minute / 60
242
- self.local_time = self.hour + self.minute / 60
243
- self.julian_day = float(swe.julday(self.utc.year, self.utc.month, self.utc.day, self.utc_time))
388
+ if self.zodiac_type == "Sidereal":
389
+ self.houses_degree_ut = swe.houses_ex(
390
+ tjdut=self.julian_day,
391
+ lat=self.lat, lon=self.lng,
392
+ hsys=str.encode(self.houses_system_identifier),
393
+ flags=swe.FLG_SIDEREAL
394
+ )[0]
395
+
396
+ elif self.zodiac_type == "Tropic":
397
+ self.houses_degree_ut = swe.houses(
398
+ tjdut=self.julian_day, lat=self.lat,
399
+ lon=self.lng,
400
+ hsys=str.encode(self.houses_system_identifier)
401
+ )[0]
402
+
403
+ point_type: PointType = "House"
244
404
 
245
- def _houses(self) -> None:
246
- """Calculate positions and store them in dictionaries"""
247
- point_type: Literal["Planet", "House"] = "House"
248
- # creates the list of the house in 360°
249
- self.houses_degree_ut = swe.houses(self.julian_day, self.lat, self.lng)[0]
250
405
  # stores the house in singular dictionaries.
251
406
  self.first_house = calculate_position(self.houses_degree_ut[0], "First_House", point_type=point_type)
252
407
  self.second_house = calculate_position(self.houses_degree_ut[1], "Second_House", point_type=point_type)
@@ -278,12 +433,6 @@ class AstrologicalSubject:
278
433
 
279
434
  def _planets_degrees_lister(self):
280
435
  """Sidereal or tropic mode."""
281
- self._iflag = swe.FLG_SWIEPH + swe.FLG_SPEED
282
-
283
- if self.zodiac_type == "Sidereal":
284
- self._iflag += swe.FLG_SIDEREAL
285
- mode = "SIDM_FAGAN_BRADLEY"
286
- swe.set_sid_mode(getattr(swe, mode))
287
436
 
288
437
  # Calculates the position of the planets and stores it in a list.
289
438
  sun_deg = swe.calc(self.julian_day, 0, self._iflag)[0][0]
@@ -298,7 +447,11 @@ class AstrologicalSubject:
298
447
  pluto_deg = swe.calc(self.julian_day, 9, self._iflag)[0][0]
299
448
  mean_node_deg = swe.calc(self.julian_day, 10, self._iflag)[0][0]
300
449
  true_node_deg = swe.calc(self.julian_day, 11, self._iflag)[0][0]
301
- chiron_deg = swe.calc(self.julian_day, 15, self._iflag)[0][0]
450
+
451
+ if not self.disable_chiron:
452
+ chiron_deg = swe.calc(self.julian_day, 15, self._iflag)[0][0]
453
+ else:
454
+ chiron_deg = 0
302
455
 
303
456
  self.planets_degrees_ut = [
304
457
  sun_deg,
@@ -313,14 +466,16 @@ class AstrologicalSubject:
313
466
  pluto_deg,
314
467
  mean_node_deg,
315
468
  true_node_deg,
316
- chiron_deg,
317
469
  ]
470
+
471
+ if not self.disable_chiron:
472
+ self.planets_degrees_ut.append(chiron_deg)
318
473
 
319
474
  def _planets(self) -> None:
320
475
  """Defines body positon in signs and information and
321
476
  stores them in dictionaries"""
322
477
 
323
- point_type: Literal["Planet", "House"] = "Planet"
478
+ point_type: PointType = "Planet"
324
479
  # stores the planets in singular dictionaries.
325
480
  self.sun = calculate_position(self.planets_degrees_ut[0], "Sun", point_type=point_type)
326
481
  self.moon = calculate_position(self.planets_degrees_ut[1], "Moon", point_type=point_type)
@@ -334,68 +489,33 @@ class AstrologicalSubject:
334
489
  self.pluto = calculate_position(self.planets_degrees_ut[9], "Pluto", point_type=point_type)
335
490
  self.mean_node = calculate_position(self.planets_degrees_ut[10], "Mean_Node", point_type=point_type)
336
491
  self.true_node = calculate_position(self.planets_degrees_ut[11], "True_Node", point_type=point_type)
337
- self.chiron = calculate_position(self.planets_degrees_ut[12], "Chiron", point_type=point_type)
492
+
493
+ if not self.disable_chiron:
494
+ self.chiron = calculate_position(self.planets_degrees_ut[12], "Chiron", point_type=point_type)
495
+ else:
496
+ self.chiron = None
338
497
 
339
498
  def _planets_in_houses(self) -> None:
340
499
  """Calculates the house of the planet and updates
341
500
  the planets dictionary."""
342
501
 
343
- def for_every_planet(planet, planet_deg):
344
- """Function to do the calculation.
345
- Args: planet dictionary, planet degree"""
346
-
347
- def point_between(p1, p2, p3):
348
- """Finds if a point is between two other in a circle
349
- args: first point, second point, point in the middle"""
350
- p1_p2 = math.fmod(p2 - p1 + 360, 360)
351
- p1_p3 = math.fmod(p3 - p1 + 360, 360)
352
- if (p1_p2 <= 180) != (p1_p3 > p1_p2):
353
- return True
354
- else:
355
- return False
356
-
357
- if point_between(self.houses_degree_ut[0], self.houses_degree_ut[1], planet_deg) == True:
358
- planet["house"] = "First_House"
359
- elif point_between(self.houses_degree_ut[1], self.houses_degree_ut[2], planet_deg) == True:
360
- planet["house"] = "Second_House"
361
- elif point_between(self.houses_degree_ut[2], self.houses_degree_ut[3], planet_deg) == True:
362
- planet["house"] = "Third_House"
363
- elif point_between(self.houses_degree_ut[3], self.houses_degree_ut[4], planet_deg) == True:
364
- planet["house"] = "Fourth_House"
365
- elif point_between(self.houses_degree_ut[4], self.houses_degree_ut[5], planet_deg) == True:
366
- planet["house"] = "Fifth_House"
367
- elif point_between(self.houses_degree_ut[5], self.houses_degree_ut[6], planet_deg) == True:
368
- planet["house"] = "Sixth_House"
369
- elif point_between(self.houses_degree_ut[6], self.houses_degree_ut[7], planet_deg) == True:
370
- planet["house"] = "Seventh_House"
371
- elif point_between(self.houses_degree_ut[7], self.houses_degree_ut[8], planet_deg) == True:
372
- planet["house"] = "Eighth_House"
373
- elif point_between(self.houses_degree_ut[8], self.houses_degree_ut[9], planet_deg) == True:
374
- planet["house"] = "Ninth_House"
375
- elif point_between(self.houses_degree_ut[9], self.houses_degree_ut[10], planet_deg) == True:
376
- planet["house"] = "Tenth_House"
377
- elif point_between(self.houses_degree_ut[10], self.houses_degree_ut[11], planet_deg) == True:
378
- planet["house"] = "Eleventh_House"
379
- elif point_between(self.houses_degree_ut[11], self.houses_degree_ut[0], planet_deg) == True:
380
- planet["house"] = "Twelfth_House"
381
- else:
382
- planet["house"] = "error!"
383
-
384
- return planet
385
-
386
- self.sun = for_every_planet(self.sun, self.planets_degrees_ut[0])
387
- self.moon = for_every_planet(self.moon, self.planets_degrees_ut[1])
388
- self.mercury = for_every_planet(self.mercury, self.planets_degrees_ut[2])
389
- self.venus = for_every_planet(self.venus, self.planets_degrees_ut[3])
390
- self.mars = for_every_planet(self.mars, self.planets_degrees_ut[4])
391
- self.jupiter = for_every_planet(self.jupiter, self.planets_degrees_ut[5])
392
- self.saturn = for_every_planet(self.saturn, self.planets_degrees_ut[6])
393
- self.uranus = for_every_planet(self.uranus, self.planets_degrees_ut[7])
394
- self.neptune = for_every_planet(self.neptune, self.planets_degrees_ut[8])
395
- self.pluto = for_every_planet(self.pluto, self.planets_degrees_ut[9])
396
- self.mean_node = for_every_planet(self.mean_node, self.planets_degrees_ut[10])
397
- self.true_node = for_every_planet(self.true_node, self.planets_degrees_ut[11])
398
- self.chiron = for_every_planet(self.chiron, self.planets_degrees_ut[12])
502
+ self.sun.house = get_planet_house(self.planets_degrees_ut[0], self.houses_degree_ut)
503
+ self.moon.house = get_planet_house(self.planets_degrees_ut[1], self.houses_degree_ut)
504
+ self.mercury.house = get_planet_house(self.planets_degrees_ut[2], self.houses_degree_ut)
505
+ self.venus.house = get_planet_house(self.planets_degrees_ut[3], self.houses_degree_ut)
506
+ self.mars.house = get_planet_house(self.planets_degrees_ut[4], self.houses_degree_ut)
507
+ self.jupiter.house = get_planet_house(self.planets_degrees_ut[5], self.houses_degree_ut)
508
+ self.saturn.house = get_planet_house(self.planets_degrees_ut[6], self.houses_degree_ut)
509
+ self.uranus.house = get_planet_house(self.planets_degrees_ut[7], self.houses_degree_ut)
510
+ self.neptune.house = get_planet_house(self.planets_degrees_ut[8], self.houses_degree_ut)
511
+ self.pluto.house = get_planet_house(self.planets_degrees_ut[9], self.houses_degree_ut)
512
+ self.mean_node.house = get_planet_house(self.planets_degrees_ut[10], self.houses_degree_ut)
513
+ self.true_node.house = get_planet_house(self.planets_degrees_ut[11], self.houses_degree_ut)
514
+
515
+ if not self.disable_chiron:
516
+ self.chiron.house = get_planet_house(self.planets_degrees_ut[12], self.houses_degree_ut)
517
+ else:
518
+ self.chiron = None
399
519
 
400
520
  self.planets_list = [
401
521
  self.sun,
@@ -410,18 +530,20 @@ class AstrologicalSubject:
410
530
  self.pluto,
411
531
  self.mean_node,
412
532
  self.true_node,
413
- self.chiron
414
533
  ]
534
+
535
+ if not self.disable_chiron:
536
+ self.planets_list.append(self.chiron)
415
537
 
416
538
  # Check in retrograde or not:
417
539
  planets_ret = []
418
- for p in self.planets_list:
419
- planet_number = get_number_from_name(p["name"])
540
+ for planet in self.planets_list:
541
+ planet_number = get_number_from_name(planet["name"])
420
542
  if swe.calc(self.julian_day, planet_number, self._iflag)[0][3] < 0:
421
- p["retrograde"] = True
543
+ planet["retrograde"] = True
422
544
  else:
423
- p["retrograde"] = False
424
- planets_ret.append(p)
545
+ planet["retrograde"] = False
546
+ planets_ret.append(planet)
425
547
 
426
548
  def _lunar_phase_calc(self) -> None:
427
549
  """Function to calculate the lunar phase"""
@@ -486,38 +608,17 @@ class AstrologicalSubject:
486
608
  if degrees_between >= low and degrees_between < high:
487
609
  sun_phase = x + 1
488
610
 
489
- def moon_emoji(phase):
490
- if phase == 1:
491
- result = "🌑"
492
- elif phase < 7:
493
- result = "🌒"
494
- elif 7 <= phase <= 9:
495
- result = "🌓"
496
- elif phase < 14:
497
- result = "🌔"
498
- elif phase == 14:
499
- result = "🌕"
500
- elif phase < 20:
501
- result = "🌖"
502
- elif 20 <= phase <= 22:
503
- result = "🌗"
504
- elif phase <= 28:
505
- result = "🌘"
506
- else:
507
- result = phase
508
-
509
- return result
510
-
511
611
  lunar_phase_dictionary = {
512
612
  "degrees_between_s_m": degrees_between,
513
613
  "moon_phase": moon_phase,
514
614
  "sun_phase": sun_phase,
515
- "moon_emoji": moon_emoji(moon_phase),
615
+ "moon_emoji": get_moon_emoji_from_phase_int(moon_phase),
616
+ "moon_phase_name": get_moon_phase_name_from_phase_int(moon_phase)
516
617
  }
517
618
 
518
619
  self.lunar_phase = LunarPhaseModel(**lunar_phase_dictionary)
519
620
 
520
- def json(self, dump=False, destination_folder: Union[str, None] = None) -> str:
621
+ def json(self, dump=False, destination_folder: Union[str, None] = None, indent: Union[int, None] = None) -> str:
521
622
  """
522
623
  Dumps the Kerykeion object to a json string foramt,
523
624
  if dump=True also dumps to file located in destination
@@ -525,8 +626,7 @@ class AstrologicalSubject:
525
626
  """
526
627
 
527
628
  KrData = AstrologicalSubjectModel(**self.__dict__)
528
- json_string = KrData.json(exclude_none=True)
529
- print(json_string)
629
+ json_string = KrData.model_dump_json(exclude_none=True, indent=indent)
530
630
 
531
631
  if dump:
532
632
  if destination_folder:
@@ -538,7 +638,7 @@ class AstrologicalSubject:
538
638
 
539
639
  with open(json_path, "w", encoding="utf-8") as file:
540
640
  file.write(json_string)
541
- logger.info(f"JSON file dumped in {json_path}.")
641
+ logging.info(f"JSON file dumped in {json_path}.")
542
642
 
543
643
  return json_string
544
644
 
@@ -549,13 +649,167 @@ class AstrologicalSubject:
549
649
 
550
650
  return AstrologicalSubjectModel(**self.__dict__)
551
651
 
652
+ @cached_property
653
+ def utc_time(self) -> float:
654
+ """
655
+ Deprecated property, use iso_formatted_utc_datetime instead, will be removed in the future.
656
+ Returns the UTC time as a float.
657
+ """
658
+ dt = datetime.fromisoformat(self.iso_formatted_utc_datetime)
659
+
660
+ # Extract the hours, minutes, and seconds
661
+ hours = dt.hour
662
+ minutes = dt.minute
663
+ seconds = dt.second + dt.microsecond / 1_000_000
664
+
665
+ # Convert time to float hours
666
+ float_time = hours + minutes / 60 + seconds / 3600
667
+
668
+ return float_time
669
+
670
+ @cached_property
671
+ def local_time(self) -> float:
672
+ """
673
+ Deprecated property, use iso_formatted_local_datetime instead, will be removed in the future.
674
+ Returns the local time as a float.
675
+ """
676
+ dt = datetime.fromisoformat(self.iso_formatted_local_datetime)
677
+
678
+ # Extract the hours, minutes, and seconds
679
+ hours = dt.hour
680
+ minutes = dt.minute
681
+ seconds = dt.second + dt.microsecond / 1_000_000
682
+
683
+ # Convert time to float hours
684
+ float_time = hours + minutes / 60 + seconds / 3600
685
+
686
+ return float_time
687
+
688
+
689
+ @staticmethod
690
+ def get_from_iso_utc_time(
691
+ name: str,
692
+ iso_utc_time: str,
693
+ city: str = "Greenwich",
694
+ nation: str = "GB",
695
+ tz_str: str = "Etc/GMT",
696
+ online: bool = False,
697
+ lng: Union[int, float] = 0,
698
+ lat: Union[int, float] = 51.5074,
699
+ geonames_username: str = DEFAULT_GEONAMES_USERNAME,
700
+ zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE,
701
+ disable_chiron: bool = False,
702
+ sidereal_mode: Union[SiderealMode, None] = None,
703
+ houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
704
+ perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE
705
+
706
+ ) -> "AstrologicalSubject":
707
+ """
708
+ Creates an AstrologicalSubject object from an iso formatted UTC time.
709
+ This method is offline by default, set online=True to fetch the timezone and coordinates from geonames.
710
+
711
+ Args:
712
+ - name (str): The name of the subject.
713
+ - iso_utc_time (str): The iso formatted UTC time.
714
+ - city (str, optional): City or location of birth. Defaults to "Greenwich".
715
+ - nation (str, optional): Nation of birth. Defaults to "GB".
716
+ - tz_str (str, optional): Timezone of the birth location. Defaults to "Etc/GMT".
717
+ - online (bool, optional): Sets if you want to use the online mode, which fetches the timezone and coordinates from geonames.
718
+ If you already have the coordinates and timezone, set this to False. Defaults to False.
719
+ - lng (Union[int, float], optional): Longitude of the birth location. Defaults to 0 (Greenwich, London).
720
+ - lat (Union[int, float], optional): Latitude of the birth location. Defaults to 51.5074 (Greenwich, London).
721
+ - geonames_username (str, optional): The username for the geonames API. Note: Change this to your own username to avoid rate limits!
722
+ You can get one for free here: https://www.geonames.org/login
723
+ - zodiac_type (ZodiacType, optional): The zodiac type to use. Defaults to "Tropic".
724
+ - disable_chiron (bool, optional): Disables the calculation of Chiron. Defaults to False.
725
+ Chiron calculation can create some issues with the Swiss Ephemeris when the date is too far in the past.
726
+ - sidereal_mode (SiderealMode, optional): Also known as Ayanamsa.
727
+ The mode to use for the sidereal zodiac, according to the Swiss Ephemeris.
728
+ Defaults to None.
729
+ Available modes are visible in the SiderealMode Literal.
730
+ - houses_system_identifier (HousesSystemIdentifier, optional): The system to use for the calculation of the houses.
731
+ Defaults to "P" (Placidus).
732
+ Available systems are visible in the HousesSystemIdentifier Literal.
733
+ - perspective_type (PerspectiveType, optional): The perspective to use for the calculation of the chart.
734
+ Defaults to "Apparent Geocentric".
735
+
736
+ Returns:
737
+ - AstrologicalSubject: The AstrologicalSubject object.
738
+ """
739
+ dt = datetime.fromisoformat(iso_utc_time)
740
+
741
+ if online == True:
742
+ if geonames_username == DEFAULT_GEONAMES_USERNAME:
743
+ logging.warning(GEONAMES_DEFAULT_USERNAME_WARNING)
744
+
745
+ geonames = FetchGeonames(
746
+ city,
747
+ nation,
748
+ username=geonames_username,
749
+ )
750
+
751
+ city_data: dict[str, str] = geonames.get_serialized_data()
752
+ lng = float(city_data["lng"])
753
+ lat = float(city_data["lat"])
754
+
755
+ subject = AstrologicalSubject(
756
+ name=name,
757
+ year=dt.year,
758
+ month=dt.month,
759
+ day=dt.day,
760
+ hour=dt.hour,
761
+ minute=dt.minute,
762
+ city=city,
763
+ nation=city,
764
+ lng=lng,
765
+ lat=lat,
766
+ tz_str=tz_str,
767
+ online=False,
768
+ geonames_username=geonames_username,
769
+ zodiac_type=zodiac_type,
770
+ disable_chiron=disable_chiron,
771
+ sidereal_mode=sidereal_mode,
772
+ houses_system_identifier=houses_system_identifier,
773
+ perspective_type=perspective_type
774
+ )
775
+
776
+ return subject
552
777
 
553
778
  if __name__ == "__main__":
554
779
  import json
555
- basicConfig(level="DEBUG", force=True)
780
+ from kerykeion.utilities import setup_logging
556
781
 
782
+ setup_logging(level="debug")
783
+
784
+ # With Chiron enabled
557
785
  johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US")
558
786
  print(json.loads(johnny.json(dump=True)))
559
787
 
560
788
  print('\n')
561
789
  print(johnny.chiron)
790
+
791
+ # With Chiron disabled
792
+ johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", disable_chiron=True)
793
+ print(json.loads(johnny.json(dump=True)))
794
+
795
+ print('\n')
796
+ print(johnny.chiron)
797
+
798
+ # With Sidereal Zodiac
799
+ johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", zodiac_type="Sidereal", sidereal_mode="LAHIRI")
800
+ print(johnny.json(dump=True, indent=2))
801
+
802
+ # With Morinus Houses
803
+ johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", houses_system_identifier="M")
804
+ print(johnny.json(dump=True, indent=2))
805
+
806
+ # With True Geocentric Perspective
807
+ johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", perspective_type="True Geocentric")
808
+ print(johnny.json(dump=True, indent=2))
809
+
810
+ # With Heliocentric Perspective
811
+ johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", perspective_type="Heliocentric")
812
+ print(johnny.json(dump=True, indent=2))
813
+
814
+ # With Topocentric Perspective
815
+ johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", perspective_type="Topocentric")