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

@@ -59,7 +59,8 @@ from kerykeion.utilities import (
59
59
  check_and_adjust_polar_latitude,
60
60
  calculate_moon_phase,
61
61
  datetime_to_julian,
62
- get_house_number
62
+ get_house_number,
63
+ normalize_zodiac_type,
63
64
  )
64
65
  from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS
65
66
 
@@ -67,7 +68,7 @@ from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS
67
68
  DEFAULT_GEONAMES_USERNAME = "century.boy"
68
69
  DEFAULT_SIDEREAL_MODE: SiderealMode = "FAGAN_BRADLEY"
69
70
  DEFAULT_HOUSES_SYSTEM_IDENTIFIER: HousesSystemIdentifier = "P"
70
- DEFAULT_ZODIAC_TYPE: ZodiacType = "Tropic"
71
+ DEFAULT_ZODIAC_TYPE: ZodiacType = "Tropical"
71
72
  DEFAULT_PERSPECTIVE_TYPE: PerspectiveType = "Apparent Geocentric"
72
73
  DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS = 30
73
74
 
@@ -148,8 +149,8 @@ class ChartConfiguration:
148
149
  combinations.
149
150
 
150
151
  Attributes:
151
- zodiac_type (ZodiacType): The zodiac system to use ('Tropic' or 'Sidereal').
152
- Defaults to 'Tropic'.
152
+ zodiac_type (ZodiacType): The zodiac system to use ('Tropical' or 'Sidereal').
153
+ Defaults to 'Tropical'.
153
154
  sidereal_mode (Optional[SiderealMode]): The sidereal calculation mode when using
154
155
  sidereal zodiac. Only required/used when zodiac_type is 'Sidereal'.
155
156
  Defaults to None (auto-set to FAGAN_BRADLEY for sidereal).
@@ -198,14 +199,17 @@ class ChartConfiguration:
198
199
  - Logs informational message when setting default sidereal mode
199
200
  """
200
201
  # Validate zodiac type
201
- if self.zodiac_type not in get_args(ZodiacType):
202
- raise KerykeionException(
203
- f"'{self.zodiac_type}' is not a valid zodiac type! Available types are: {get_args(ZodiacType)}"
204
- )
202
+ try:
203
+ normalized_zodiac_type = normalize_zodiac_type(self.zodiac_type)
204
+ except ValueError as exc:
205
+ raise KerykeionException(str(exc)) from exc
206
+ else:
207
+ if normalized_zodiac_type != self.zodiac_type:
208
+ self.zodiac_type = normalized_zodiac_type
205
209
 
206
210
  # Validate sidereal mode settings
207
- if self.sidereal_mode and self.zodiac_type == "Tropic":
208
- raise KerykeionException("You can't set a sidereal mode with a Tropic zodiac type!")
211
+ if self.sidereal_mode and self.zodiac_type == "Tropical":
212
+ raise KerykeionException("You can't set a sidereal mode with a Tropical zodiac type!")
209
213
 
210
214
  if self.zodiac_type == "Sidereal":
211
215
  if not self.sidereal_mode:
@@ -475,8 +479,8 @@ class AstrologicalSubjectFactory:
475
479
  online location lookup. Get one free at geonames.org.
476
480
  online (bool, optional): Whether to fetch location data online. If False,
477
481
  lng, lat, and tz_str must be provided. Defaults to True.
478
- zodiac_type (ZodiacType, optional): Zodiac system - 'Tropic' or 'Sidereal'.
479
- Defaults to 'Tropic'.
482
+ zodiac_type (ZodiacType, optional): Zodiac system - 'Tropical' or 'Sidereal'.
483
+ Defaults to 'Tropical'.
480
484
  sidereal_mode (SiderealMode, optional): Sidereal calculation mode (e.g.,
481
485
  'FAGAN_BRADLEY', 'LAHIRI'). Only used with zodiac_type='Sidereal'.
482
486
  houses_system_identifier (HousesSystemIdentifier, optional): House system
@@ -726,7 +730,7 @@ class AstrologicalSubjectFactory:
726
730
  or as fallback. Defaults to 51.5074 (Greenwich).
727
731
  geonames_username (str, optional): GeoNames API username. Required when
728
732
  online=True. Defaults to DEFAULT_GEONAMES_USERNAME.
729
- zodiac_type (ZodiacType, optional): Zodiac system. Defaults to 'Tropic'.
733
+ zodiac_type (ZodiacType, optional): Zodiac system. Defaults to 'Tropical'.
730
734
  sidereal_mode (SiderealMode, optional): Sidereal mode when zodiac_type
731
735
  is 'Sidereal'. Defaults to None.
732
736
  houses_system_identifier (HousesSystemIdentifier, optional): House system.
@@ -866,7 +870,7 @@ class AstrologicalSubjectFactory:
866
870
  online (bool, optional): Whether to fetch location data online.
867
871
  Defaults to True.
868
872
  zodiac_type (ZodiacType, optional): Zodiac system to use.
869
- Defaults to 'Tropic'.
873
+ Defaults to 'Tropical'.
870
874
  sidereal_mode (SiderealMode, optional): Sidereal calculation mode.
871
875
  Only used when zodiac_type is 'Sidereal'. Defaults to None.
872
876
  houses_system_identifier (HousesSystemIdentifier, optional): House
kerykeion/backword.py CHANGED
@@ -36,6 +36,7 @@ from .chart_data_factory import ChartDataFactory
36
36
  from .charts.chart_drawer import ChartDrawer
37
37
  from .aspects import AspectsFactory
38
38
  from .settings.config_constants import DEFAULT_ACTIVE_POINTS, DEFAULT_ACTIVE_ASPECTS
39
+ from .utilities import normalize_zodiac_type
39
40
  from .schemas.kr_models import (
40
41
  AstrologicalSubjectModel,
41
42
  CompositeSubjectModel,
@@ -74,6 +75,42 @@ LEGACY_NODE_NAMES_MAP = {
74
75
  }
75
76
 
76
77
 
78
+ def _normalize_zodiac_type_with_warning(zodiac_type: Optional[Union[str, ZodiacType]]) -> Optional[ZodiacType]:
79
+ """Normalize legacy zodiac type values with deprecation warning.
80
+
81
+ Wraps the utilities.normalize_zodiac_type function and adds a deprecation
82
+ warning for legacy formats like "tropic" or case-insensitive variants.
83
+
84
+ Args:
85
+ zodiac_type: Input zodiac type (may be legacy format)
86
+
87
+ Returns:
88
+ Normalized ZodiacType or None if input was None
89
+ """
90
+ if zodiac_type is None:
91
+ return None
92
+
93
+ zodiac_str = str(zodiac_type)
94
+
95
+ # Check if this is a legacy format (case-insensitive "tropic" or non-canonical case)
96
+ zodiac_lower = zodiac_str.lower()
97
+ if zodiac_lower in ("tropic", "tropical", "sidereal") and zodiac_str not in ("Tropical", "Sidereal"):
98
+ # Normalize using the utilities function
99
+ normalized = normalize_zodiac_type(zodiac_str)
100
+
101
+ # Emit deprecation warning for legacy usage
102
+ warnings.warn(
103
+ f"Zodiac type '{zodiac_str}' is deprecated in Kerykeion v5. "
104
+ f"Use '{normalized}' instead.",
105
+ DeprecationWarning,
106
+ stacklevel=4,
107
+ )
108
+ return normalized
109
+
110
+ # Already in correct format or will be normalized by utilities function
111
+ return cast(ZodiacType, normalize_zodiac_type(zodiac_str))
112
+
113
+
77
114
  def _normalize_active_points(points: Optional[Iterable[Union[str, AstrologicalPoint]]]) -> Optional[List[AstrologicalPoint]]:
78
115
  """Best-effort normalization of legacy string active points list.
79
116
 
@@ -159,7 +196,10 @@ class AstrologicalSubject:
159
196
  stacklevel=2,
160
197
  )
161
198
 
199
+ # Normalize legacy zodiac type values
200
+ zodiac_type = _normalize_zodiac_type_with_warning(zodiac_type)
162
201
  zodiac_type = DEFAULT_ZODIAC_TYPE if zodiac_type is None else zodiac_type
202
+
163
203
  houses_system_identifier = (
164
204
  DEFAULT_HOUSES_SYSTEM_IDENTIFIER if houses_system_identifier is None else houses_system_identifier
165
205
  )
@@ -352,6 +392,9 @@ class AstrologicalSubject:
352
392
  if online and resolved_geonames == DEFAULT_GEONAMES_USERNAME:
353
393
  warnings.warn(GEONAMES_DEFAULT_USERNAME_WARNING, UserWarning, stacklevel=2)
354
394
 
395
+ # Normalize legacy zodiac type values
396
+ normalized_zodiac_type = _normalize_zodiac_type_with_warning(zodiac_type)
397
+
355
398
  model = AstrologicalSubjectFactory.from_iso_utc_time(
356
399
  name=name,
357
400
  iso_utc_time=iso_utc_time,
@@ -362,7 +405,7 @@ class AstrologicalSubject:
362
405
  lng=float(lng),
363
406
  lat=float(lat),
364
407
  geonames_username=resolved_geonames,
365
- zodiac_type=(zodiac_type or DEFAULT_ZODIAC_TYPE), # type: ignore[arg-type]
408
+ zodiac_type=(normalized_zodiac_type or DEFAULT_ZODIAC_TYPE), # type: ignore[arg-type]
366
409
  sidereal_mode=sidereal_mode,
367
410
  houses_system_identifier=(houses_system_identifier or DEFAULT_HOUSES_SYSTEM_IDENTIFIER), # type: ignore[arg-type]
368
411
  perspective_type=(perspective_type or DEFAULT_PERSPECTIVE_TYPE), # type: ignore[arg-type]
@@ -78,6 +78,9 @@ from kerykeion.settings.chart_defaults import (
78
78
  from typing import List, Literal
79
79
 
80
80
 
81
+ logger = logging.getLogger(__name__)
82
+
83
+
81
84
  class ChartDrawer:
82
85
  """
83
86
  ChartDrawer generates astrological chart visualizations as SVG files from pre-computed chart data.
@@ -315,7 +318,7 @@ class ChartDrawer:
315
318
 
316
319
  active_points_count = len(self.available_planets_setting)
317
320
  if active_points_count > 24:
318
- logging.warning(
321
+ logger.warning(
319
322
  "ChartDrawer detected %s active celestial points; rendering may look crowded beyond 24.",
320
323
  active_points_count,
321
324
  )
@@ -856,7 +859,7 @@ class ChartDrawer:
856
859
  try:
857
860
  required_width = self._estimate_required_width_full()
858
861
  except Exception as e:
859
- logging.debug(f"Auto-size width calculation failed: {e}")
862
+ logger.debug("Auto-size width calculation failed: %s", e)
860
863
  return
861
864
 
862
865
  minimum_width = self._minimum_width_for_chart_type()
@@ -1271,7 +1274,7 @@ class ChartDrawer:
1271
1274
  template_dict["top_left_5"] = f"{self._translate('day_of_week', 'Day of Week')}: {localized_weekday}" # type: ignore
1272
1275
 
1273
1276
  # Bottom left section
1274
- if self.first_obj.zodiac_type == "Tropic":
1277
+ if self.first_obj.zodiac_type == "Tropical":
1275
1278
  zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
1276
1279
  else:
1277
1280
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
@@ -1433,7 +1436,7 @@ class ChartDrawer:
1433
1436
  template_dict["top_left_5"] = f"{latitude_string} / {longitude_string}"
1434
1437
 
1435
1438
  # Bottom left section
1436
- if self.first_obj.zodiac_type == "Tropic":
1439
+ if self.first_obj.zodiac_type == "Tropical":
1437
1440
  zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
1438
1441
  else:
1439
1442
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
@@ -1589,7 +1592,7 @@ class ChartDrawer:
1589
1592
  template_dict["top_left_5"] = ""#f"{self._translate('type', 'Type')}: {self._translate(self.chart_type, self.chart_type)}"
1590
1593
 
1591
1594
  # Bottom left section
1592
- if self.first_obj.zodiac_type == "Tropic":
1595
+ if self.first_obj.zodiac_type == "Tropical":
1593
1596
  zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
1594
1597
  else:
1595
1598
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
@@ -1609,9 +1612,9 @@ class ChartDrawer:
1609
1612
 
1610
1613
  template_dict["bottom_left_4"] = f'{self._translate("perspective_type", "Perspective")}: {self._translate(self.second_obj.perspective_type.lower().replace(" ", "_"), self.second_obj.perspective_type)}' # type: ignore
1611
1614
 
1612
- # Moon phase section calculations - use first_obj for visualization
1613
- if self.first_obj.lunar_phase is not None:
1614
- template_dict["makeLunarPhase"] = makeLunarPhase(self.first_obj.lunar_phase["degrees_between_s_m"], self.geolat)
1615
+ # Moon phase section calculations - use transit subject data only
1616
+ if self.second_obj is not None and getattr(self.second_obj, "lunar_phase", None):
1617
+ template_dict["makeLunarPhase"] = makeLunarPhase(self.second_obj.lunar_phase["degrees_between_s_m"], self.geolat)
1615
1618
  else:
1616
1619
  template_dict["makeLunarPhase"] = ""
1617
1620
 
@@ -1767,7 +1770,7 @@ class ChartDrawer:
1767
1770
  template_dict["top_left_5"] = format_datetime_with_timezone(self.second_obj.iso_formatted_local_datetime) # type: ignore
1768
1771
 
1769
1772
  # Bottom left section
1770
- if self.first_obj.zodiac_type == "Tropic":
1773
+ if self.first_obj.zodiac_type == "Tropical":
1771
1774
  zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
1772
1775
  else:
1773
1776
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
@@ -1967,7 +1970,7 @@ class ChartDrawer:
1967
1970
  template_dict["top_left_5"] = f"{latitude_string} / {longitude_string}"
1968
1971
 
1969
1972
  # Bottom left section
1970
- if self.first_obj.zodiac_type == "Tropic":
1973
+ if self.first_obj.zodiac_type == "Tropical":
1971
1974
  zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
1972
1975
  else:
1973
1976
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
@@ -2140,7 +2143,7 @@ class ChartDrawer:
2140
2143
  template_dict["top_left_5"] = f"{self._translate('type', 'Type')}: {self._translate('lunar_return', 'Lunar Return')}"
2141
2144
 
2142
2145
  # Bottom left section
2143
- if self.first_obj.zodiac_type == "Tropic":
2146
+ if self.first_obj.zodiac_type == "Tropical":
2144
2147
  zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
2145
2148
  else:
2146
2149
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
@@ -2237,7 +2240,7 @@ class ChartDrawer:
2237
2240
 
2238
2241
  # return filename
2239
2242
 
2240
- logging.debug(f"Template dictionary has {len(td.model_dump())} fields")
2243
+ logger.debug("Template dictionary includes %s fields", len(td.model_dump()))
2241
2244
 
2242
2245
  self._create_template_dictionary()
2243
2246
 
@@ -48,10 +48,19 @@ License: AGPL-3.0
48
48
 
49
49
  from kerykeion import AstrologicalSubjectFactory
50
50
  from kerykeion.schemas.kr_models import AstrologicalSubjectModel
51
- from kerykeion.utilities import get_houses_list, get_available_astrological_points_list
51
+ from kerykeion.utilities import (
52
+ get_houses_list,
53
+ get_available_astrological_points_list,
54
+ normalize_zodiac_type,
55
+ )
52
56
  from kerykeion.astrological_subject_factory import DEFAULT_HOUSES_SYSTEM_IDENTIFIER, DEFAULT_PERSPECTIVE_TYPE, DEFAULT_ZODIAC_TYPE
53
- from kerykeion.schemas import EphemerisDictModel
54
- from kerykeion.schemas import SiderealMode, HousesSystemIdentifier, PerspectiveType, ZodiacType
57
+ from kerykeion.schemas import (
58
+ EphemerisDictModel,
59
+ SiderealMode,
60
+ HousesSystemIdentifier,
61
+ PerspectiveType,
62
+ ZodiacType,
63
+ )
55
64
  from datetime import datetime, timedelta
56
65
  from typing import Literal, Union, List
57
66
  import logging
@@ -161,7 +170,7 @@ class EphemerisDataFactory:
161
170
  self.lng = lng
162
171
  self.tz_str = tz_str
163
172
  self.is_dst = is_dst
164
- self.zodiac_type = zodiac_type
173
+ self.zodiac_type = normalize_zodiac_type(zodiac_type)
165
174
  self.sidereal_mode = sidereal_mode
166
175
  self.houses_system_identifier = houses_system_identifier
167
176
  self.perspective_type = perspective_type
@@ -6,13 +6,16 @@ License: AGPL-3.0
6
6
  """
7
7
 
8
8
 
9
- import logging
9
+ from logging import getLogger
10
10
  from datetime import timedelta
11
11
  from requests import Request
12
12
  from requests_cache import CachedSession
13
13
  from typing import Union
14
14
 
15
15
 
16
+ logger = getLogger(__name__)
17
+
18
+
16
19
  class FetchGeonames:
17
20
  """
18
21
  Class to handle requests to the GeoNames API for location data and timezone information.
@@ -63,21 +66,21 @@ class FetchGeonames:
63
66
  params = {"lat": lat, "lng": lon, "username": self.username}
64
67
 
65
68
  prepared_request = Request("GET", self.timezone_url, params=params).prepare()
66
- logging.debug(f"Requesting data from GeoName timezones: {prepared_request.url}")
69
+ logger.debug("GeoNames timezone lookup url=%s", prepared_request.url)
67
70
 
68
71
  try:
69
72
  response = self.session.send(prepared_request)
70
73
  response_json = response.json()
71
74
 
72
75
  except Exception as e:
73
- logging.error(f"Error fetching {self.timezone_url}: {e}")
76
+ logger.error("GeoNames timezone request failed for %s: %s", self.timezone_url, e)
74
77
  return {}
75
78
 
76
79
  try:
77
80
  timezone_data["timezonestr"] = response_json["timezoneId"]
78
81
 
79
82
  except Exception as e:
80
- logging.error(f"Error serializing data maybe wrong username? Details: {e}")
83
+ logger.error("GeoNames timezone payload missing expected keys: %s", e)
81
84
  return {}
82
85
 
83
86
  if hasattr(response, "from_cache"):
@@ -109,15 +112,16 @@ class FetchGeonames:
109
112
  }
110
113
 
111
114
  prepared_request = Request("GET", self.base_url, params=params).prepare()
112
- logging.debug(f"Requesting data from geonames basic: {prepared_request.url}")
115
+ logger.debug("GeoNames search url=%s", prepared_request.url)
113
116
 
114
117
  try:
115
118
  response = self.session.send(prepared_request)
119
+ response.raise_for_status()
116
120
  response_json = response.json()
117
- logging.debug(f"Response from GeoNames: {response_json}")
121
+ logger.debug("GeoNames search response: %s", response_json)
118
122
 
119
123
  except Exception as e:
120
- logging.error(f"Error in fetching {self.base_url}: {e}")
124
+ logger.error("GeoNames search request failed for %s: %s", self.base_url, e)
121
125
  return {}
122
126
 
123
127
  try:
@@ -127,7 +131,7 @@ class FetchGeonames:
127
131
  city_data_whitout_tz["countryCode"] = response_json["geonames"][0]["countryCode"]
128
132
 
129
133
  except Exception as e:
130
- logging.error(f"Error serializing data maybe wrong username? Details: {e}")
134
+ logger.error("GeoNames search payload missing expected keys: %s", e)
131
135
  return {}
132
136
 
133
137
  if hasattr(response, "from_cache"):
@@ -147,15 +151,16 @@ class FetchGeonames:
147
151
  timezone_response = self.__get_timezone(city_data_response["lat"], city_data_response["lng"])
148
152
 
149
153
  except Exception as e:
150
- logging.error(f"Error in fetching timezone: {e}")
154
+ logger.error("Unable to fetch timezone details: %s", e)
151
155
  return {}
152
156
 
153
157
  return {**timezone_response, **city_data_response}
154
158
 
155
159
 
156
160
  if __name__ == "__main__":
157
- from kerykeion.utilities import setup_logging
158
- setup_logging(level="debug")
161
+ """Run a tiny demonstration when executing the module directly."""
162
+ from kerykeion.utilities import setup_logging as configure_logging
159
163
 
164
+ configure_logging("debug")
160
165
  geonames = FetchGeonames("Montichiari", "IT")
161
166
  print(geonames.get_serialized_data())
kerykeion/report.py CHANGED
@@ -727,8 +727,7 @@ if __name__ == "__main__":
727
727
  natal_subject.iso_formatted_local_datetime,
728
728
  "Solar",
729
729
  )
730
-
731
- # Composite chart subject
730
+ # Derive a composite subject representing the pair's midpoint configuration
732
731
  composite_subject = CompositeSubjectFactory(
733
732
  natal_subject,
734
733
  partner_subject,
@@ -5,7 +5,7 @@ This is part of Kerykeion (C) 2025 Giacomo Battaglia
5
5
  from typing import Literal
6
6
 
7
7
 
8
- ZodiacType = Literal["Tropic", "Sidereal"]
8
+ ZodiacType = Literal["Tropical", "Sidereal"]
9
9
  """Literal type for Zodiac Types"""
10
10
 
11
11
 
@@ -47,5 +47,5 @@ def _deep_merge(base: Mapping[str, Any], overrides: Mapping[str, Any]) -> dict[s
47
47
  merged[key] = deepcopy(value)
48
48
  return merged
49
49
 
50
-
50
+ # Keep the public surface area explicit for downstream imports.
51
51
  __all__ = ["SettingsSource", "load_settings_mapping", "LANGUAGE_SETTINGS"]
kerykeion/utilities.py CHANGED
@@ -12,15 +12,108 @@ from kerykeion.schemas import (
12
12
  LunarPhaseModel,
13
13
  CompositeSubjectModel,
14
14
  PlanetReturnModel,
15
+ ZodiacType,
15
16
  )
16
- from kerykeion.schemas.kr_literals import LunarPhaseEmoji, LunarPhaseName, PointType, AstrologicalPoint, Houses
17
- from typing import Union, Optional, get_args
18
- import logging
17
+ from kerykeion.schemas.kr_literals import (
18
+ LunarPhaseEmoji,
19
+ LunarPhaseName,
20
+ PointType,
21
+ AstrologicalPoint,
22
+ Houses,
23
+ )
24
+ from typing import Union, Optional, get_args, cast
25
+ from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL, basicConfig, getLogger
19
26
  import math
20
27
  import re
21
28
  from datetime import datetime
22
29
 
23
30
 
31
+ logger = getLogger(__name__)
32
+
33
+ def normalize_zodiac_type(value: str) -> ZodiacType:
34
+ """
35
+ Normalize a zodiac type string to its canonical representation.
36
+
37
+ Handles case-insensitive matching and legacy formats like "tropic" or "Tropic",
38
+ automatically converting them to the canonical forms "Tropical" or "Sidereal".
39
+
40
+ Args:
41
+ value: Input zodiac type string (case-insensitive).
42
+
43
+ Returns:
44
+ ZodiacType: Canonical zodiac type ("Tropical" or "Sidereal").
45
+
46
+ Raises:
47
+ ValueError: If `value` is not a recognized zodiac type.
48
+
49
+ Examples:
50
+ >>> normalize_zodiac_type("tropical")
51
+ 'Tropical'
52
+ >>> normalize_zodiac_type("Tropic")
53
+ 'Tropical'
54
+ >>> normalize_zodiac_type("SIDEREAL")
55
+ 'Sidereal'
56
+ """
57
+ # Normalize to lowercase for comparison
58
+ value_lower = value.lower()
59
+
60
+ # Map legacy and case-insensitive variants to canonical forms
61
+ if value_lower in ("tropical", "tropic"):
62
+ return cast(ZodiacType, "Tropical")
63
+ elif value_lower == "sidereal":
64
+ return cast(ZodiacType, "Sidereal")
65
+ else:
66
+ raise ValueError(
67
+ "'{value}' is not a valid zodiac type. Accepted values are: Tropical, Sidereal "
68
+ "(case-insensitive, 'tropic' also accepted as legacy).".format(value=value)
69
+ )
70
+
71
+ _POINT_NUMBER_MAP: dict[str, int] = {
72
+ "Sun": 0,
73
+ "Moon": 1,
74
+ "Mercury": 2,
75
+ "Venus": 3,
76
+ "Mars": 4,
77
+ "Jupiter": 5,
78
+ "Saturn": 6,
79
+ "Uranus": 7,
80
+ "Neptune": 8,
81
+ "Pluto": 9,
82
+ "Mean_North_Lunar_Node": 10,
83
+ "True_North_Lunar_Node": 11,
84
+ # Swiss Ephemeris has no dedicated IDs for the south nodes; we reserve high values.
85
+ "Mean_South_Lunar_Node": 1000,
86
+ "True_South_Lunar_Node": 1100,
87
+ "Chiron": 15,
88
+ "Mean_Lilith": 12,
89
+ "Ascendant": 9900,
90
+ "Descendant": 9901,
91
+ "Medium_Coeli": 9902,
92
+ "Imum_Coeli": 9903,
93
+ }
94
+
95
+ # Logging helpers
96
+ def setup_logging(level: str) -> None:
97
+ """Configure the root logger so demo scripts share the same formatting."""
98
+ normalized_level = (level or "").strip().lower()
99
+ level_map: dict[str, int] = {
100
+ "debug": DEBUG,
101
+ "info": INFO,
102
+ "warning": WARNING,
103
+ "error": ERROR,
104
+ "critical": CRITICAL,
105
+ }
106
+
107
+ selected_level = level_map.get(normalized_level, INFO)
108
+ basicConfig(
109
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
110
+ level=selected_level,
111
+ )
112
+ logger.setLevel(selected_level)
113
+
114
+
115
+
116
+
24
117
  def get_number_from_name(name: AstrologicalPoint) -> int:
25
118
  """
26
119
  Convert an astrological point name to its corresponding numerical identifier.
@@ -35,49 +128,10 @@ def get_number_from_name(name: AstrologicalPoint) -> int:
35
128
  KerykeionException: If the name is not recognized
36
129
  """
37
130
 
38
- if name == "Sun":
39
- return 0
40
- elif name == "Moon":
41
- return 1
42
- elif name == "Mercury":
43
- return 2
44
- elif name == "Venus":
45
- return 3
46
- elif name == "Mars":
47
- return 4
48
- elif name == "Jupiter":
49
- return 5
50
- elif name == "Saturn":
51
- return 6
52
- elif name == "Uranus":
53
- return 7
54
- elif name == "Neptune":
55
- return 8
56
- elif name == "Pluto":
57
- return 9
58
- elif name == "Mean_North_Lunar_Node":
59
- return 10
60
- elif name == "True_North_Lunar_Node":
61
- return 11
62
- # Note: Swiss ephemeris library has no constants for south nodes. We're using integers >= 1000 for them.
63
- elif name == "Mean_South_Lunar_Node":
64
- return 1000
65
- elif name == "True_South_Lunar_Node":
66
- return 1100
67
- elif name == "Chiron":
68
- return 15
69
- elif name == "Mean_Lilith":
70
- return 12
71
- elif name == "Ascendant": # TODO: Is this needed?
72
- return 9900
73
- elif name == "Descendant": # TODO: Is this needed?
74
- return 9901
75
- elif name == "Medium_Coeli": # TODO: Is this needed?
76
- return 9902
77
- elif name == "Imum_Coeli": # TODO: Is this needed?
78
- return 9903
79
- else:
80
- raise KerykeionException(f"Error in getting number from name! Name: {name}")
131
+ try:
132
+ return _POINT_NUMBER_MAP[str(name)]
133
+ except KeyError as exc: # pragma: no cover - defensive branch
134
+ raise KerykeionException(f"Error in getting number from name! Name: {name}") from exc
81
135
 
82
136
 
83
137
  def get_kerykeion_point_from_degree(
@@ -124,7 +178,6 @@ def get_kerykeion_point_from_degree(
124
178
  sign_index = int(degree // 30)
125
179
  sign_degree = degree % 30
126
180
  zodiac_sign = ZODIAC_SIGNS[sign_index]
127
-
128
181
  return KerykeionPointModel(
129
182
  name=name,
130
183
  quality=zodiac_sign.quality,
@@ -139,87 +192,34 @@ def get_kerykeion_point_from_degree(
139
192
  declination=declination,
140
193
  )
141
194
 
195
+ # Angular helpers
196
+ def is_point_between(start_angle: Union[int, float], end_angle: Union[int, float], candidate: Union[int, float]) -> bool:
197
+ """Return True when ``candidate`` lies on the clockwise arc from ``start_angle`` to ``end_angle``."""
142
198
 
143
- def setup_logging(level: str) -> None:
144
- """
145
- Configure logging for the application.
146
-
147
- Args:
148
- level: Log level as string (debug, info, warning, error, critical)
149
- """
150
- logging_options: dict[str, int] = {
151
- "debug": logging.DEBUG,
152
- "info": logging.INFO,
153
- "warning": logging.WARNING,
154
- "error": logging.ERROR,
155
- "critical": logging.CRITICAL,
156
- }
157
- format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
158
- loglevel: int = logging_options.get(level, logging.INFO)
159
- logging.basicConfig(format=format, level=loglevel)
160
-
199
+ normalize = lambda value: value % 360
161
200
 
162
- def is_point_between(
163
- start_point: Union[int, float], end_point: Union[int, float], evaluated_point: Union[int, float]
164
- ) -> bool:
165
- """
166
- Determine if a point lies between two other points on a circle.
167
-
168
- Special rules:
169
- - If evaluated_point equals start_point, returns True
170
- - If evaluated_point equals end_point, returns False
171
- - The arc between start_point and end_point must not exceed 180°
172
-
173
- Args:
174
- start_point: The starting point on the circle
175
- end_point: The ending point on the circle
176
- evaluated_point: The point to evaluate
177
-
178
- Returns:
179
- True if evaluated_point is between start_point and end_point, False otherwise
180
-
181
- Raises:
182
- KerykeionException: If the angular difference exceeds 180°
183
- """
184
-
185
- # Normalize angles to [0, 360)
186
- start_point = start_point % 360
187
- end_point = end_point % 360
188
- evaluated_point = evaluated_point % 360
189
-
190
- # Compute angular difference
191
- angular_difference = math.fmod(end_point - start_point + 360, 360)
192
-
193
- # Ensure the range is not greater than 180°. Otherwise, it is not truly defined what
194
- # being located in between two points on a circle actually means.
195
- if angular_difference > 180:
201
+ start = normalize(start_angle)
202
+ end = normalize(end_angle)
203
+ target = normalize(candidate)
204
+ span = (end - start) % 360
205
+ if span > 180:
196
206
  raise KerykeionException(
197
- f"The angle between start and end point is not allowed to exceed 180°, yet is: {angular_difference}"
207
+ f"The angle between start and end point is not allowed to exceed 180°, yet is: {span}"
198
208
  )
199
-
200
- # Handle explicitly when evaluated_point == start_point. Note: It may happen for mathematical
201
- # reasons that evaluated_point and start_point deviate very slightly from each other, but
202
- # should really be same value. This case is captured later below by the term 0 <= p1_p3.
203
- if evaluated_point == start_point:
209
+ if target == start:
204
210
  return True
205
-
206
- # Handle explicitly when evaluated_point == end_point
207
- if evaluated_point == end_point:
211
+ if target == end:
208
212
  return False
213
+ distance_from_start = (target - start) % 360
214
+ return distance_from_start < span
209
215
 
210
- # Compute angular differences for evaluation
211
- p1_p3 = math.fmod(evaluated_point - start_point + 360, 360)
212
-
213
- # Check if point lies in the interval
214
- return (0 <= p1_p3) and (p1_p3 < angular_difference)
215
-
216
-
217
- def get_planet_house(planet_position_degree: Union[int, float], houses_degree_ut_list: list) -> Houses:
216
+ # House helpers
217
+ def get_planet_house(planet_degree: Union[int, float], houses_degree_ut_list: list) -> Houses:
218
218
  """
219
219
  Determine which house contains a planet based on its degree position.
220
220
 
221
221
  Args:
222
- planet_position_degree: The planet's position in degrees (0-360)
222
+ planet_degree: The planet's position in degrees (0-360)
223
223
  houses_degree_ut_list: List of house cusp degrees
224
224
 
225
225
  Returns:
@@ -236,11 +236,11 @@ def get_planet_house(planet_position_degree: Union[int, float], houses_degree_ut
236
236
  start_degree = houses_degree_ut_list[i]
237
237
  end_degree = houses_degree_ut_list[(i + 1) % len(houses_degree_ut_list)]
238
238
 
239
- if is_point_between(start_degree, end_degree, planet_position_degree):
239
+ if is_point_between(start_degree, end_degree, planet_degree):
240
240
  return house_names[i]
241
241
 
242
242
  # If no house is found, raise an error
243
- raise ValueError(f"Error in house calculation, planet: {planet_position_degree}, houses: {houses_degree_ut_list}")
243
+ raise ValueError(f"Error in house calculation, planet: {planet_degree}, houses: {houses_degree_ut_list}")
244
244
 
245
245
 
246
246
  def get_moon_emoji_from_phase_int(phase: int) -> LunarPhaseEmoji:
@@ -334,11 +334,11 @@ def check_and_adjust_polar_latitude(latitude: float) -> float:
334
334
  """
335
335
  if latitude > 66.0:
336
336
  latitude = 66.0
337
- logging.info("Polar circle override for houses, using 66 degrees")
337
+ logger.info("Latitude capped at 66° to keep house calculations stable.")
338
338
 
339
339
  elif latitude < -66.0:
340
340
  latitude = -66.0
341
- logging.info("Polar circle override for houses, using -66 degrees")
341
+ logger.info("Latitude capped at -66° to keep house calculations stable.")
342
342
 
343
343
  return latitude
344
344
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kerykeion
3
- Version: 5.1.5
3
+ Version: 5.1.7
4
4
  Summary: A Python library for astrological calculations, including natal charts, houses, planetary aspects, and SVG chart generation.
5
5
  Project-URL: Homepage, https://www.kerykeion.net/
6
6
  Project-URL: Repository, https://github.com/g-battaglia/kerykeion
@@ -1781,6 +1781,8 @@ Since the AstrologerAPI is an external third-party service, using it does _not_
1781
1781
 
1782
1782
  Contributions are welcome! Feel free to submit pull requests or report issues.
1783
1783
 
1784
+ By submitting a contribution, you agree to assign the copyright of that contribution to the maintainer. The project stays openly available under the AGPL for everyone, while the re-licensing option helps sustain future development. Your authorship remains acknowledged in the commit history and release notes.
1785
+
1784
1786
  ## Citations
1785
1787
 
1786
1788
  If using Kerykeion in published or academic work, please cite as follows:
@@ -1,20 +1,20 @@
1
1
  kerykeion/__init__.py,sha256=7gI_kWXhsmEK4SvNV6CkRmI0yjA3r7EO8UfQc_44UZU,1963
2
- kerykeion/astrological_subject_factory.py,sha256=KaNuPSgVOgAva9z83fTuCpvIfEm3hsJro-z_mFPU1Q4,94604
3
- kerykeion/backword.py,sha256=zqsFWE0xI7vwXnFT2vngufFaJueI3qCIDCVgG-alzCM,32860
2
+ kerykeion/astrological_subject_factory.py,sha256=qR6dWULcF4J2ym2oTI4CQ-zfQfMgAFNyLwhGXhJkg-A,94740
3
+ kerykeion/backword.py,sha256=aZrC6gRhLuewv3PvpHWlPvqrDGxspCEN2mMAh8tLtfM,34500
4
4
  kerykeion/chart_data_factory.py,sha256=gtAlZs_IjPMUJUitLjkRFCESmAj-wEOlYdCPQ2b8XwA,25123
5
5
  kerykeion/composite_subject_factory.py,sha256=eUtjThDlYr6SQsEi02_CgO1kEMUMYgQGzR5rX91GEwY,17120
6
- kerykeion/ephemeris_data_factory.py,sha256=uwEhVqqkB56HbxKueMAvSAbEKkYTJ9GH3mVLDNxIQnc,20358
7
- kerykeion/fetch_geonames.py,sha256=SPI4fSvD59C-IVpaoeOHuD7_kjGbTLo2fypO2x57-p4,5432
6
+ kerykeion/ephemeris_data_factory.py,sha256=UKgoJQSqiyy4U0jNw23NI_BP0kbZblJU5mtBnNZtnb0,20417
7
+ kerykeion/fetch_geonames.py,sha256=VO-TK4m_xSE62yWE5cf6CE2OLVfkPLeTPqXw8igzDnU,5619
8
8
  kerykeion/planetary_return_factory.py,sha256=bIVPqa2gs7hYHi8aaMfVr4U99xWZk3LmYqZI9KG6ACo,37572
9
9
  kerykeion/relationship_score_factory.py,sha256=pVrsRlFs__bfVe8K3cOirsnRtdBOW2DbnKqPa7jCWF0,11396
10
- kerykeion/report.py,sha256=MK5_E_DkG09kEdFMueyQ45J_6JJg3QvkYSA0tywehx4,31414
10
+ kerykeion/report.py,sha256=fHGzPh9vhDIWzT_Gr4V5KrfLjiDWLlSlBgOk6-DeH80,31463
11
11
  kerykeion/transits_time_range_factory.py,sha256=YRSFVa2MIZFQMPbEL2njKd-P7p431zoAsINrddZz5Yw,13934
12
- kerykeion/utilities.py,sha256=LKljXx0HSjlT5pspvG4VIY1po_xG1dLGNNfEWg0G5Fs,24391
12
+ kerykeion/utilities.py,sha256=AqcJ9zmVEcwiurMfN63MDSeegJ1dJ4L1sQVJeoNIGOE,24016
13
13
  kerykeion/aspects/__init__.py,sha256=csJmxvLdBu-bHuW676f3dpY__Qyc6LwRyrpWVTh3n1A,287
14
14
  kerykeion/aspects/aspects_factory.py,sha256=XCuOOpo0ZW7sihYT2f50bLVpebEk19b9EtEjXonwte0,24156
15
15
  kerykeion/aspects/aspects_utils.py,sha256=00-MMLEGChpceab8sHKB1_qg6EG4ycTG2u2vYZcyLmQ,5784
16
16
  kerykeion/charts/__init__.py,sha256=i9NMZ7LdkllPlqQSi1or9gTobHbROGDKmJhBDO4R0mA,128
17
- kerykeion/charts/chart_drawer.py,sha256=VCk2s6kUT2AhNWhVdepuhp2_6qHk3Fiq0d4EMq0pvZQ,122240
17
+ kerykeion/charts/chart_drawer.py,sha256=Rhv2eVV2G_GBb6Khh4uor9sqSH6KJ5i6RA5ZFpVPzuI,122333
18
18
  kerykeion/charts/charts_utils.py,sha256=iwgDhVc_GwKuQBZ6JNmn6RWVILD5KIcjbrnDTR3wrAc,70911
19
19
  kerykeion/charts/draw_planets.py,sha256=tIj3FeLLomVSbCaZUxfw_qBEB3pxi4EIEhqaeTgTyTY,29061
20
20
  kerykeion/charts/templates/aspect_grid_only.xml,sha256=v3QtNMjk-kBdUTfB0r6thg--Ta_tNFdRQCzdk5PAycY,86429
@@ -38,13 +38,13 @@ kerykeion/kr_types/settings_models.py,sha256=xtpsrhjmhdowDSBeQ7TEMR53-uEEspCoXKC
38
38
  kerykeion/schemas/__init__.py,sha256=EcwbnoYPKLq3m7n5s2cEZ8UyWpxdmHXbvM23hLNBNBI,2376
39
39
  kerykeion/schemas/chart_template_model.py,sha256=fQ_EZ8ccOgNd4gXu5KilF1dUH9B2RVCDLHc09YkYLyY,8978
40
40
  kerykeion/schemas/kerykeion_exception.py,sha256=vTYdwj_mL-Q-MqHJvEzzBXxQ5YI2kAwUC6ImoWxMKXc,454
41
- kerykeion/schemas/kr_literals.py,sha256=p0lm598ut0GyeT-sRzWNcp5BFk9-FdYme4XjEUFJ3cA,5656
41
+ kerykeion/schemas/kr_literals.py,sha256=mvPH3_TyuF5GTYKfIGk9SpxbuW7uYhSlp20SZJ7l6BY,5658
42
42
  kerykeion/schemas/kr_models.py,sha256=e1PQxUqC437IWIVKrOi71NVR5bEV7KGhF_JAaxs9zN0,21945
43
43
  kerykeion/schemas/settings_models.py,sha256=NlOW9T7T5kD5Dzna1UOdb7XPQeQnzMOu0y4-ApERxPw,17619
44
44
  kerykeion/settings/__init__.py,sha256=IJUqkYTpvmbKecVeCbsiL1qU_4xWc78u4OrvN_T3ZAI,624
45
45
  kerykeion/settings/chart_defaults.py,sha256=TSm2hXCxym3I7XpIqIMBLWM253Z0dCGbhPi2MpWbD_0,12490
46
46
  kerykeion/settings/config_constants.py,sha256=D1PApNT7Zldlv8QINmaKnyLovY5UJ2FsAqHNTTPd5ps,3717
47
- kerykeion/settings/kerykeion_settings.py,sha256=Aoi_9SeHlgmTaopMdC4Ojb9V0kw-iKZKqJopZBiTikM,1884
47
+ kerykeion/settings/kerykeion_settings.py,sha256=aeRlxvy2GwC-mxoD_DFMXMoO4yPQp1XQbEAGVQat0Wo,1947
48
48
  kerykeion/settings/translation_strings.py,sha256=UYxiZT5aiNZ4tpQuTauIlXwMm3tzfz6QE9DPNVtlHdo,56412
49
49
  kerykeion/settings/translations.py,sha256=A_Zwr-ujvxBLvE4a87-28pM3KifdUaAvsR3XvMZNg0s,2377
50
50
  kerykeion/sweph/README.md,sha256=L7FtNAJTWtrZNGKa8MX87SjduFYPYxwWhaI5fmtzNZo,73
@@ -57,7 +57,7 @@ kerykeion/sweph/ast28/se28978s.se1,sha256=nU2Qp-ELc_tzFnRc1QT6uVueWXEipvhYDgfQRX
57
57
  kerykeion/sweph/ast50/se50000s.se1,sha256=9jTrPlIrZMOBWC9cNgwzcfz0KBHdXFZoY9-NZ_HtECo,15748
58
58
  kerykeion/sweph/ast90/se90377s.se1,sha256=bto2x4LtBv8b1ej1XhVFYq-kfHO9cczbKV9U1f9UVu4,10288
59
59
  kerykeion/sweph/ast90/se90482s.se1,sha256=uHxz6bP4K8zgtQFrlWFwxrYfmqm5kXxsg6OYhAIUbAA,16173
60
- kerykeion-5.1.5.dist-info/METADATA,sha256=G32MUXKcYQ0CO9XgyEd3ku5749Aqc9PoifJ5ptJI0fU,61445
61
- kerykeion-5.1.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
62
- kerykeion-5.1.5.dist-info/licenses/LICENSE,sha256=UTLH8EdbAsgQei4PA2PnBCPGLSZkq5J-dhkyJuXgWQU,34273
63
- kerykeion-5.1.5.dist-info/RECORD,,
60
+ kerykeion-5.1.7.dist-info/METADATA,sha256=w1pDxVeTBbyLbZ-m6qCBi9-gSx8eI1fXUUHm1_g1Gf0,61756
61
+ kerykeion-5.1.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
62
+ kerykeion-5.1.7.dist-info/licenses/LICENSE,sha256=UTLH8EdbAsgQei4PA2PnBCPGLSZkq5J-dhkyJuXgWQU,34273
63
+ kerykeion-5.1.7.dist-info/RECORD,,