kerykeion 5.0.1__py3-none-any.whl → 5.1.8__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.
- kerykeion/aspects/aspects_factory.py +11 -12
- kerykeion/aspects/aspects_utils.py +2 -2
- kerykeion/astrological_subject_factory.py +18 -14
- kerykeion/backword.py +52 -5
- kerykeion/chart_data_factory.py +9 -6
- kerykeion/charts/chart_drawer.py +96 -36
- kerykeion/charts/charts_utils.py +98 -71
- kerykeion/ephemeris_data_factory.py +13 -4
- kerykeion/fetch_geonames.py +51 -13
- kerykeion/relationship_score_factory.py +1 -1
- kerykeion/report.py +7 -9
- kerykeion/schemas/kr_literals.py +1 -1
- kerykeion/schemas/kr_models.py +8 -10
- kerykeion/schemas/settings_models.py +8 -0
- kerykeion/settings/kerykeion_settings.py +1 -1
- kerykeion/settings/translation_strings.py +20 -0
- kerykeion/transits_time_range_factory.py +1 -1
- kerykeion/utilities.py +121 -121
- {kerykeion-5.0.1.dist-info → kerykeion-5.1.8.dist-info}/METADATA +755 -138
- {kerykeion-5.0.1.dist-info → kerykeion-5.1.8.dist-info}/RECORD +22 -22
- {kerykeion-5.0.1.dist-info → kerykeion-5.1.8.dist-info}/WHEEL +0 -0
- {kerykeion-5.0.1.dist-info → kerykeion-5.1.8.dist-info}/licenses/LICENSE +0 -0
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
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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: {
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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,
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
341
|
+
logger.info("Latitude capped at -66° to keep house calculations stable.")
|
|
342
342
|
|
|
343
343
|
return latitude
|
|
344
344
|
|