kerykeion 4.0.3__py3-none-any.whl → 4.8.1__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,13 @@
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
12
11
  from kerykeion.fetch_geonames import FetchGeonames
13
12
  from kerykeion.kr_types import (
14
13
  KerykeionException,
@@ -17,16 +16,17 @@ from kerykeion.kr_types import (
17
16
  LunarPhaseModel,
18
17
  KerykeionPointModel,
19
18
  )
20
- from kerykeion.utilities import get_number_from_name, calculate_position
19
+ from kerykeion.utilities import (
20
+ get_number_from_name,
21
+ calculate_position,
22
+ get_planet_house,
23
+ get_moon_emoji_from_phase_int,
24
+ get_moon_phase_name_from_phase_int,
25
+ )
21
26
  from pathlib import Path
22
27
  from typing import Union, Literal
23
28
 
24
-
25
- logger = getLogger(__name__)
26
- basicConfig(
27
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
28
- level="INFO"
29
- )
29
+ DEFAULT_GEONAMES_USERNAME = "century.boy"
30
30
 
31
31
 
32
32
  class AstrologicalSubject:
@@ -50,14 +50,19 @@ class AstrologicalSubject:
50
50
  - lng (Union[int, float], optional): _ Defaults to False.
51
51
  - lat (Union[int, float], optional): _ Defaults to False.
52
52
  - tz_str (Union[str, bool], optional): _ Defaults to False.
53
- - logger (Union[Logger, None], optional): _ Defaults to None.
54
53
  - geonames_username (str, optional): _ Defaults to 'century.boy'.
55
54
  - online (bool, optional): Sets if you want to use the online mode (using
56
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.
60
+ Chiron calculation can create some issues with the Swiss Ephemeris when the date is too far in the past.
57
61
  """
58
62
 
59
63
  # Defined by the user
60
64
  name: str
65
+ utc_datetime: Union[datetime, None]
61
66
  year: int
62
67
  month: int
63
68
  day: int
@@ -93,7 +98,7 @@ class AstrologicalSubject:
93
98
  pluto: KerykeionPointModel
94
99
  true_node: KerykeionPointModel
95
100
  mean_node: KerykeionPointModel
96
- chiron: KerykeionPointModel
101
+ chiron: Union[KerykeionPointModel, None]
97
102
 
98
103
  # Houses
99
104
  first_house: KerykeionPointModel
@@ -110,14 +115,13 @@ class AstrologicalSubject:
110
115
  twelfth_house: KerykeionPointModel
111
116
 
112
117
  # Lists
113
- houses: list[KerykeionPointModel]
114
- planets: list[KerykeionPointModel]
118
+ houses_list: list[KerykeionPointModel]
119
+ planets_list: list[KerykeionPointModel]
115
120
  planets_degrees_ut: list[float]
116
121
  houses_degree_ut: list[float]
117
122
 
118
123
  now = datetime.now()
119
124
 
120
-
121
125
  def __init__(
122
126
  self,
123
127
  name="Now",
@@ -126,16 +130,18 @@ class AstrologicalSubject:
126
130
  day: int = now.day,
127
131
  hour: int = now.hour,
128
132
  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",
133
+ city: Union[str, None] = None,
134
+ nation: Union[str, None] = None,
135
+ lng: Union[int, float, None] = None,
136
+ lat: Union[int, float, None] = None,
137
+ tz_str: Union[str, None] = None,
138
+ geonames_username: Union[str, None] = None,
135
139
  zodiac_type: ZodiacType = "Tropic",
136
140
  online: bool = True,
141
+ utc_datetime: Union[datetime, None] = None,
142
+ disable_chiron: bool = False
137
143
  ) -> None:
138
- logger.debug("Starting Kerykeion")
144
+ logging.debug("Starting Kerykeion")
139
145
 
140
146
  # We set the swisseph path to the current directory
141
147
  swe.set_ephe_path(
@@ -144,7 +150,6 @@ class AstrologicalSubject:
144
150
  )
145
151
  )
146
152
 
147
-
148
153
  self.name = name
149
154
  self.year = year
150
155
  self.month = month
@@ -156,24 +161,57 @@ class AstrologicalSubject:
156
161
  self.lng = lng
157
162
  self.lat = lat
158
163
  self.tz_str = tz_str
159
- self._geonames_username = geonames_username
160
164
  self.zodiac_type = zodiac_type
161
165
  self.online = online
162
166
  self.json_dir = Path.home()
167
+ self.geonames_username = geonames_username
168
+ self.utc_datetime = utc_datetime
169
+ self.disable_chiron = disable_chiron
170
+
171
+ # This message is set to encourage the user to set a custom geonames username
172
+ 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
+ )
189
+
190
+ self.geonames_username = DEFAULT_GEONAMES_USERNAME
163
191
 
164
192
  if not self.city:
165
193
  self.city = "London"
166
- logger.warning("No city specified, using London as default")
194
+ logging.info("No city specified, using London as default")
167
195
 
168
196
  if not self.nation:
169
197
  self.nation = "GB"
170
- logger.warning("No nation specified, using GB as default")
198
+ logging.info("No nation specified, using GB as default")
199
+
200
+ if not self.lat:
201
+ self.lat = 51.5074
202
+ logging.info("No latitude specified, using London as default")
171
203
 
172
- if (not self.online) and (not lng or not lat or not tz_str):
204
+ if not self.lng:
205
+ self.lng = 0
206
+ logging.info("No longitude specified, using London as default")
207
+
208
+ if (not self.online) and (not tz_str):
173
209
  raise KerykeionException(
174
210
  "You need to set the coordinates and timezone if you want to use the offline mode!"
175
211
  )
176
212
 
213
+ self._check_if_poles()
214
+
177
215
  # Initialize everything
178
216
  self._get_utc()
179
217
  self._get_jd()
@@ -190,14 +228,20 @@ class AstrologicalSubject:
190
228
  def __repr__(self) -> str:
191
229
  return f"Astrological data for: {self.name}, {self.utc} UTC\nBirth location: {self.city}, Lat {self.lat}, Lon {self.lng}"
192
230
 
231
+ def __getitem__(self, item):
232
+ return getattr(self, item)
233
+
234
+ def get(self, item, default=None):
235
+ return getattr(self, item, default)
236
+
193
237
  def _fetch_tz_from_geonames(self) -> None:
194
238
  """Gets the nearest time zone for the calculation"""
195
- logger.debug("Conneting to Geonames...")
239
+ logging.info("Fetching timezone/coordinates from geonames")
196
240
 
197
241
  geonames = FetchGeonames(
198
242
  self.city,
199
243
  self.nation,
200
- username=self._geonames_username,
244
+ username=self.geonames_username,
201
245
  )
202
246
  self.city_data: dict[str, str] = geonames.get_serialized_data()
203
247
 
@@ -214,21 +258,20 @@ class AstrologicalSubject:
214
258
  self.lat = float(self.city_data["lat"])
215
259
  self.tz_str = self.city_data["timezonestr"]
216
260
 
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")
261
+ self._check_if_poles()
224
262
 
225
263
  def _get_utc(self) -> None:
226
264
  """Converts local time to utc time."""
227
265
 
228
266
  # 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):
267
+ if (self.online) and (not self.tz_str):
230
268
  self._fetch_tz_from_geonames()
231
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
+
232
275
  local_time = pytz.timezone(self.tz_str)
233
276
 
234
277
  naive_datetime = datetime(self.year, self.month, self.day, self.hour, self.minute, 0)
@@ -243,7 +286,50 @@ class AstrologicalSubject:
243
286
  self.julian_day = float(swe.julday(self.utc.year, self.utc.month, self.utc.day, self.utc_time))
244
287
 
245
288
  def _houses(self) -> None:
246
- """Calculate positions and store them in dictionaries"""
289
+ """
290
+ Calculate positions and store them in dictionaries
291
+
292
+ https://www.astro.com/faq/fq_fh_owhouse_e.htm
293
+ https://github.com/jwmatthys/pd-swisseph/blob/master/swehouse.c#L685
294
+ hsys = letter code for house system;
295
+ A equal
296
+ E equal
297
+ B Alcabitius
298
+ C Campanus
299
+ D equal (MC)
300
+ F Carter "Poli-Equatorial"
301
+ G 36 Gauquelin sectors
302
+ H horizon / azimut
303
+ I Sunshine solution Treindl
304
+ i Sunshine solution Makransky
305
+ K Koch
306
+ L Pullen SD "sinusoidal delta", ex Neo-Porphyry
307
+ M Morinus
308
+ N equal/1=Aries
309
+ O Porphyry
310
+ P Placidus
311
+ Q Pullen SR "sinusoidal ratio"
312
+ R Regiomontanus
313
+ S Sripati
314
+ T Polich/Page ("topocentric")
315
+ U Krusinski-Pisa-Goelzer
316
+ V equal Vehlow
317
+ W equal, whole sign
318
+ X axial rotation system/ Meridian houses
319
+ Y APC houses
320
+ """
321
+
322
+ if self.zodiac_type == "Sidereal":
323
+ 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
325
+ )[0]
326
+ elif self.zodiac_type == "Tropic":
327
+ self.houses_degree_ut = swe.houses(
328
+ tjdut=self.julian_day, lat=self.lat, lon=self.lng, hsys=str.encode('P')
329
+ )[0]
330
+ else:
331
+ raise KerykeionException("Zodiac type not recognized! Please use 'Tropic' or 'Sidereal'")
332
+
247
333
  point_type: Literal["Planet", "House"] = "House"
248
334
  # creates the list of the house in 360°
249
335
  self.houses_degree_ut = swe.houses(self.julian_day, self.lat, self.lng)[0]
@@ -298,7 +384,11 @@ class AstrologicalSubject:
298
384
  pluto_deg = swe.calc(self.julian_day, 9, self._iflag)[0][0]
299
385
  mean_node_deg = swe.calc(self.julian_day, 10, self._iflag)[0][0]
300
386
  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]
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
302
392
 
303
393
  self.planets_degrees_ut = [
304
394
  sun_deg,
@@ -313,8 +403,10 @@ class AstrologicalSubject:
313
403
  pluto_deg,
314
404
  mean_node_deg,
315
405
  true_node_deg,
316
- chiron_deg,
317
406
  ]
407
+
408
+ if not self.disable_chiron:
409
+ self.planets_degrees_ut.append(chiron_deg)
318
410
 
319
411
  def _planets(self) -> None:
320
412
  """Defines body positon in signs and information and
@@ -334,68 +426,33 @@ class AstrologicalSubject:
334
426
  self.pluto = calculate_position(self.planets_degrees_ut[9], "Pluto", point_type=point_type)
335
427
  self.mean_node = calculate_position(self.planets_degrees_ut[10], "Mean_Node", point_type=point_type)
336
428
  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)
429
+
430
+ if not self.disable_chiron:
431
+ self.chiron = calculate_position(self.planets_degrees_ut[12], "Chiron", point_type=point_type)
432
+ else:
433
+ self.chiron = None
338
434
 
339
435
  def _planets_in_houses(self) -> None:
340
436
  """Calculates the house of the planet and updates
341
437
  the planets dictionary."""
342
438
 
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])
439
+ self.sun.house = get_planet_house(self.planets_degrees_ut[0], self.houses_degree_ut)
440
+ self.moon.house = get_planet_house(self.planets_degrees_ut[1], self.houses_degree_ut)
441
+ self.mercury.house = get_planet_house(self.planets_degrees_ut[2], self.houses_degree_ut)
442
+ self.venus.house = get_planet_house(self.planets_degrees_ut[3], self.houses_degree_ut)
443
+ self.mars.house = get_planet_house(self.planets_degrees_ut[4], self.houses_degree_ut)
444
+ self.jupiter.house = get_planet_house(self.planets_degrees_ut[5], self.houses_degree_ut)
445
+ self.saturn.house = get_planet_house(self.planets_degrees_ut[6], self.houses_degree_ut)
446
+ self.uranus.house = get_planet_house(self.planets_degrees_ut[7], self.houses_degree_ut)
447
+ self.neptune.house = get_planet_house(self.planets_degrees_ut[8], self.houses_degree_ut)
448
+ self.pluto.house = get_planet_house(self.planets_degrees_ut[9], self.houses_degree_ut)
449
+ self.mean_node.house = get_planet_house(self.planets_degrees_ut[10], self.houses_degree_ut)
450
+ self.true_node.house = get_planet_house(self.planets_degrees_ut[11], self.houses_degree_ut)
451
+
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
399
456
 
400
457
  self.planets_list = [
401
458
  self.sun,
@@ -410,8 +467,10 @@ class AstrologicalSubject:
410
467
  self.pluto,
411
468
  self.mean_node,
412
469
  self.true_node,
413
- self.chiron
414
470
  ]
471
+
472
+ if not self.disable_chiron:
473
+ self.planets_list.append(self.chiron)
415
474
 
416
475
  # Check in retrograde or not:
417
476
  planets_ret = []
@@ -486,37 +545,29 @@ class AstrologicalSubject:
486
545
  if degrees_between >= low and degrees_between < high:
487
546
  sun_phase = x + 1
488
547
 
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
548
  lunar_phase_dictionary = {
512
549
  "degrees_between_s_m": degrees_between,
513
550
  "moon_phase": moon_phase,
514
551
  "sun_phase": sun_phase,
515
- "moon_emoji": moon_emoji(moon_phase),
552
+ "moon_emoji": get_moon_emoji_from_phase_int(moon_phase),
553
+ "moon_phase_name": get_moon_phase_name_from_phase_int(moon_phase)
516
554
  }
517
555
 
518
556
  self.lunar_phase = LunarPhaseModel(**lunar_phase_dictionary)
519
557
 
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
+
520
571
  def json(self, dump=False, destination_folder: Union[str, None] = None) -> str:
521
572
  """
522
573
  Dumps the Kerykeion object to a json string foramt,
@@ -525,7 +576,7 @@ class AstrologicalSubject:
525
576
  """
526
577
 
527
578
  KrData = AstrologicalSubjectModel(**self.__dict__)
528
- json_string = KrData.json(exclude_none=True)
579
+ json_string = KrData.model_dump_json(exclude_none=True)
529
580
  print(json_string)
530
581
 
531
582
  if dump:
@@ -538,7 +589,7 @@ class AstrologicalSubject:
538
589
 
539
590
  with open(json_path, "w", encoding="utf-8") as file:
540
591
  file.write(json_string)
541
- logger.info(f"JSON file dumped in {json_path}.")
592
+ logging.info(f"JSON file dumped in {json_path}.")
542
593
 
543
594
  return json_string
544
595
 
@@ -552,10 +603,20 @@ class AstrologicalSubject:
552
603
 
553
604
  if __name__ == "__main__":
554
605
  import json
555
- basicConfig(level="DEBUG", force=True)
606
+ from kerykeion.utilities import setup_logging
556
607
 
608
+ setup_logging(level="debug")
609
+
610
+ # With Chiron enabled
557
611
  johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US")
558
612
  print(json.loads(johnny.json(dump=True)))
559
613
 
560
614
  print('\n')
561
615
  print(johnny.chiron)
616
+
617
+ # With Chiron disabled
618
+ johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US", disable_chiron=True)
619
+ print(json.loads(johnny.json(dump=True)))
620
+
621
+ print('\n')
622
+ print(johnny.chiron)
@@ -1,5 +1,5 @@
1
1
  """
2
- This is part of Kerykeion (C) 2023 Giacomo Battaglia
2
+ This is part of Kerykeion (C) 2024 Giacomo Battaglia
3
3
 
4
4
  This modules contains the charts logic for the Kerykeion project.
5
5
  """