kerykeion 4.26.2__py3-none-any.whl → 5.0.0__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/__init__.py +54 -11
- kerykeion/aspects/__init__.py +5 -2
- kerykeion/aspects/aspects_factory.py +569 -0
- kerykeion/aspects/aspects_utils.py +81 -8
- kerykeion/astrological_subject_factory.py +1897 -0
- kerykeion/backword.py +773 -0
- kerykeion/chart_data_factory.py +549 -0
- kerykeion/charts/chart_drawer.py +2601 -0
- kerykeion/charts/charts_utils.py +948 -177
- kerykeion/charts/draw_planets.py +602 -351
- kerykeion/charts/templates/aspect_grid_only.xml +328 -202
- kerykeion/charts/templates/chart.xml +432 -272
- kerykeion/charts/templates/wheel_only.xml +350 -214
- kerykeion/charts/themes/black-and-white.css +148 -0
- kerykeion/charts/themes/classic.css +107 -76
- kerykeion/charts/themes/dark-high-contrast.css +145 -107
- kerykeion/charts/themes/dark.css +146 -107
- kerykeion/charts/themes/light.css +146 -103
- kerykeion/charts/themes/strawberry.css +158 -0
- kerykeion/composite_subject_factory.py +253 -51
- kerykeion/ephemeris_data_factory.py +434 -0
- kerykeion/fetch_geonames.py +27 -8
- kerykeion/house_comparison/__init__.py +6 -0
- kerykeion/house_comparison/house_comparison_factory.py +103 -0
- kerykeion/house_comparison/house_comparison_utils.py +126 -0
- kerykeion/kr_types/__init__.py +66 -6
- kerykeion/kr_types/chart_template_model.py +20 -0
- kerykeion/kr_types/kerykeion_exception.py +15 -9
- kerykeion/kr_types/kr_literals.py +14 -132
- kerykeion/kr_types/kr_models.py +14 -318
- kerykeion/kr_types/settings_models.py +15 -203
- kerykeion/planetary_return_factory.py +805 -0
- kerykeion/relationship_score_factory.py +301 -0
- kerykeion/report.py +751 -64
- kerykeion/schemas/__init__.py +106 -0
- kerykeion/schemas/chart_template_model.py +367 -0
- kerykeion/schemas/kerykeion_exception.py +20 -0
- kerykeion/schemas/kr_literals.py +181 -0
- kerykeion/schemas/kr_models.py +605 -0
- kerykeion/schemas/settings_models.py +180 -0
- kerykeion/settings/__init__.py +20 -1
- kerykeion/settings/chart_defaults.py +444 -0
- kerykeion/settings/config_constants.py +117 -12
- kerykeion/settings/kerykeion_settings.py +31 -73
- kerykeion/settings/translation_strings.py +1479 -0
- kerykeion/settings/translations.py +74 -0
- kerykeion/sweph/ast136/s136108s.se1 +0 -0
- kerykeion/sweph/ast136/s136199s.se1 +0 -0
- kerykeion/sweph/ast136/s136472s.se1 +0 -0
- kerykeion/sweph/ast28/se28978s.se1 +0 -0
- kerykeion/sweph/ast50/se50000s.se1 +0 -0
- kerykeion/sweph/ast90/se90377s.se1 +0 -0
- kerykeion/sweph/ast90/se90482s.se1 +0 -0
- kerykeion/sweph/sefstars.txt +1602 -0
- kerykeion/transits_time_range_factory.py +302 -0
- kerykeion/utilities.py +393 -114
- kerykeion-5.0.0.dist-info/METADATA +1176 -0
- kerykeion-5.0.0.dist-info/RECORD +63 -0
- {kerykeion-4.26.2.dist-info → kerykeion-5.0.0.dist-info}/WHEEL +1 -1
- kerykeion/aspects/natal_aspects.py +0 -172
- kerykeion/aspects/synastry_aspects.py +0 -124
- kerykeion/aspects/transits_time_range.py +0 -41
- kerykeion/astrological_subject.py +0 -841
- kerykeion/charts/kerykeion_chart_svg.py +0 -1219
- kerykeion/enums.py +0 -57
- kerykeion/ephemeris_data.py +0 -242
- kerykeion/kr_types/chart_types.py +0 -95
- kerykeion/relationship_score/__init__.py +0 -2
- kerykeion/relationship_score/relationship_score.py +0 -175
- kerykeion/relationship_score/relationship_score_factory.py +0 -230
- kerykeion/settings/kr.config.json +0 -1258
- kerykeion/transits_time_range.py +0 -124
- kerykeion-4.26.2.dist-info/LICENSE +0 -661
- kerykeion-4.26.2.dist-info/METADATA +0 -629
- kerykeion-4.26.2.dist-info/RECORD +0 -46
- kerykeion-4.26.2.dist-info/entry_points.txt +0 -3
- /LICENSE → /kerykeion-5.0.0.dist-info/licenses/LICENSE +0 -0
kerykeion/utilities.py
CHANGED
|
@@ -1,16 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"""
|
|
2
|
+
Author: Giacomo Battaglia
|
|
3
|
+
Copyright: (C) 2025 Kerykeion Project
|
|
4
|
+
License: AGPL-3.0
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from kerykeion.schemas import (
|
|
8
|
+
KerykeionPointModel,
|
|
9
|
+
KerykeionException,
|
|
10
|
+
ZodiacSignModel,
|
|
11
|
+
AstrologicalSubjectModel,
|
|
12
|
+
LunarPhaseModel,
|
|
13
|
+
CompositeSubjectModel,
|
|
14
|
+
PlanetReturnModel,
|
|
15
|
+
)
|
|
16
|
+
from kerykeion.schemas.kr_literals import LunarPhaseEmoji, LunarPhaseName, PointType, AstrologicalPoint, Houses
|
|
17
|
+
from typing import Union, Optional, get_args
|
|
4
18
|
import logging
|
|
5
19
|
import math
|
|
6
20
|
import re
|
|
21
|
+
from datetime import datetime
|
|
7
22
|
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
from kerykeion import AstrologicalSubject
|
|
10
23
|
|
|
24
|
+
def get_number_from_name(name: AstrologicalPoint) -> int:
|
|
25
|
+
"""
|
|
26
|
+
Convert an astrological point name to its corresponding numerical identifier.
|
|
11
27
|
|
|
12
|
-
|
|
13
|
-
|
|
28
|
+
Args:
|
|
29
|
+
name: The name of the astrological point
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The numerical identifier used in Swiss Ephemeris calculations
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
KerykeionException: If the name is not recognized
|
|
36
|
+
"""
|
|
14
37
|
|
|
15
38
|
if name == "Sun":
|
|
16
39
|
return 0
|
|
@@ -32,50 +55,55 @@ def get_number_from_name(name: Planet) -> int:
|
|
|
32
55
|
return 8
|
|
33
56
|
elif name == "Pluto":
|
|
34
57
|
return 9
|
|
35
|
-
elif name == "
|
|
58
|
+
elif name == "Mean_North_Lunar_Node":
|
|
36
59
|
return 10
|
|
37
|
-
elif name == "
|
|
60
|
+
elif name == "True_North_Lunar_Node":
|
|
38
61
|
return 11
|
|
39
62
|
# Note: Swiss ephemeris library has no constants for south nodes. We're using integers >= 1000 for them.
|
|
40
|
-
elif name == "
|
|
63
|
+
elif name == "Mean_South_Lunar_Node":
|
|
41
64
|
return 1000
|
|
42
|
-
elif name == "
|
|
65
|
+
elif name == "True_South_Lunar_Node":
|
|
43
66
|
return 1100
|
|
44
67
|
elif name == "Chiron":
|
|
45
68
|
return 15
|
|
46
69
|
elif name == "Mean_Lilith":
|
|
47
70
|
return 12
|
|
48
|
-
elif name == "Ascendant":
|
|
71
|
+
elif name == "Ascendant": # TODO: Is this needed?
|
|
49
72
|
return 9900
|
|
50
|
-
elif name == "Descendant":
|
|
73
|
+
elif name == "Descendant": # TODO: Is this needed?
|
|
51
74
|
return 9901
|
|
52
|
-
elif name == "Medium_Coeli":
|
|
75
|
+
elif name == "Medium_Coeli": # TODO: Is this needed?
|
|
53
76
|
return 9902
|
|
54
|
-
elif name == "Imum_Coeli":
|
|
77
|
+
elif name == "Imum_Coeli": # TODO: Is this needed?
|
|
55
78
|
return 9903
|
|
56
79
|
else:
|
|
57
80
|
raise KerykeionException(f"Error in getting number from name! Name: {name}")
|
|
58
81
|
|
|
59
82
|
|
|
60
83
|
def get_kerykeion_point_from_degree(
|
|
61
|
-
degree: Union[int, float], name: Union[
|
|
84
|
+
degree: Union[int, float], name: Union[AstrologicalPoint, Houses], point_type: PointType, speed: Optional[float] = None, declination: Optional[float] = None
|
|
62
85
|
) -> KerykeionPointModel:
|
|
63
86
|
"""
|
|
64
|
-
|
|
87
|
+
Create a KerykeionPointModel from a degree position.
|
|
65
88
|
|
|
66
89
|
Args:
|
|
67
|
-
degree (
|
|
68
|
-
name
|
|
69
|
-
point_type
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
KerykeionException: If the degree is not within the valid range (0-360).
|
|
90
|
+
degree: The degree position (0-360, negative values are converted to positive)
|
|
91
|
+
name: The name of the celestial point or house
|
|
92
|
+
point_type: The type classification of the point
|
|
93
|
+
speed: The velocity/speed of the celestial point in degrees per day (optional)
|
|
94
|
+
declination: The declination of the celestial point in degrees (optional)
|
|
73
95
|
|
|
74
96
|
Returns:
|
|
75
|
-
KerykeionPointModel
|
|
97
|
+
A KerykeionPointModel with calculated zodiac sign, position, and properties
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
KerykeionException: If the degree is >= 360 after normalization
|
|
76
101
|
"""
|
|
102
|
+
# If - single degree is given, convert it to a positive degree
|
|
103
|
+
if degree < 0:
|
|
104
|
+
degree = degree % 360
|
|
77
105
|
|
|
78
|
-
if degree
|
|
106
|
+
if degree >= 360:
|
|
79
107
|
raise KerykeionException(f"Error in calculating positions! Degrees: {degree}")
|
|
80
108
|
|
|
81
109
|
ZODIAC_SIGNS = {
|
|
@@ -107,14 +135,17 @@ def get_kerykeion_point_from_degree(
|
|
|
107
135
|
abs_pos=degree,
|
|
108
136
|
emoji=zodiac_sign.emoji,
|
|
109
137
|
point_type=point_type,
|
|
138
|
+
speed=speed,
|
|
139
|
+
declination=declination,
|
|
110
140
|
)
|
|
111
141
|
|
|
142
|
+
|
|
112
143
|
def setup_logging(level: str) -> None:
|
|
113
144
|
"""
|
|
114
|
-
|
|
145
|
+
Configure logging for the application.
|
|
115
146
|
|
|
116
147
|
Args:
|
|
117
|
-
level: Log level as
|
|
148
|
+
level: Log level as string (debug, info, warning, error, critical)
|
|
118
149
|
"""
|
|
119
150
|
logging_options: dict[str, int] = {
|
|
120
151
|
"debug": logging.DEBUG,
|
|
@@ -129,23 +160,26 @@ def setup_logging(level: str) -> None:
|
|
|
129
160
|
|
|
130
161
|
|
|
131
162
|
def is_point_between(
|
|
132
|
-
start_point: Union[int, float],
|
|
133
|
-
end_point: Union[int, float],
|
|
134
|
-
evaluated_point: Union[int, float]
|
|
163
|
+
start_point: Union[int, float], end_point: Union[int, float], evaluated_point: Union[int, float]
|
|
135
164
|
) -> bool:
|
|
136
165
|
"""
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
-
|
|
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°
|
|
141
172
|
|
|
142
173
|
Args:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
146
177
|
|
|
147
178
|
Returns:
|
|
148
|
-
|
|
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°
|
|
149
183
|
"""
|
|
150
184
|
|
|
151
185
|
# Normalize angles to [0, 360)
|
|
@@ -159,7 +193,9 @@ def is_point_between(
|
|
|
159
193
|
# Ensure the range is not greater than 180°. Otherwise, it is not truly defined what
|
|
160
194
|
# being located in between two points on a circle actually means.
|
|
161
195
|
if angular_difference > 180:
|
|
162
|
-
raise KerykeionException(
|
|
196
|
+
raise KerykeionException(
|
|
197
|
+
f"The angle between start and end point is not allowed to exceed 180°, yet is: {angular_difference}"
|
|
198
|
+
)
|
|
163
199
|
|
|
164
200
|
# Handle explicitly when evaluated_point == start_point. Note: It may happen for mathematical
|
|
165
201
|
# reasons that evaluated_point and start_point deviate very slightly from each other, but
|
|
@@ -178,20 +214,19 @@ def is_point_between(
|
|
|
178
214
|
return (0 <= p1_p3) and (p1_p3 < angular_difference)
|
|
179
215
|
|
|
180
216
|
|
|
181
|
-
|
|
182
217
|
def get_planet_house(planet_position_degree: Union[int, float], houses_degree_ut_list: list) -> Houses:
|
|
183
218
|
"""
|
|
184
|
-
|
|
219
|
+
Determine which house contains a planet based on its degree position.
|
|
185
220
|
|
|
186
221
|
Args:
|
|
187
|
-
planet_position_degree
|
|
188
|
-
houses_degree_ut_list
|
|
222
|
+
planet_position_degree: The planet's position in degrees (0-360)
|
|
223
|
+
houses_degree_ut_list: List of house cusp degrees
|
|
189
224
|
|
|
190
225
|
Returns:
|
|
191
|
-
|
|
226
|
+
The house name containing the planet
|
|
192
227
|
|
|
193
228
|
Raises:
|
|
194
|
-
ValueError: If the planet's position
|
|
229
|
+
ValueError: If the planet's position doesn't fall within any house range
|
|
195
230
|
"""
|
|
196
231
|
|
|
197
232
|
house_names = get_args(Houses)
|
|
@@ -210,13 +245,16 @@ def get_planet_house(planet_position_degree: Union[int, float], houses_degree_ut
|
|
|
210
245
|
|
|
211
246
|
def get_moon_emoji_from_phase_int(phase: int) -> LunarPhaseEmoji:
|
|
212
247
|
"""
|
|
213
|
-
|
|
248
|
+
Get the emoji representation of a lunar phase.
|
|
214
249
|
|
|
215
250
|
Args:
|
|
216
|
-
|
|
251
|
+
phase: The lunar phase number (0-28)
|
|
217
252
|
|
|
218
253
|
Returns:
|
|
219
|
-
|
|
254
|
+
The corresponding emoji for the lunar phase
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
KerykeionException: If phase is outside valid range
|
|
220
258
|
"""
|
|
221
259
|
|
|
222
260
|
lunar_phase_emojis = get_args(LunarPhaseEmoji)
|
|
@@ -243,23 +281,26 @@ def get_moon_emoji_from_phase_int(phase: int) -> LunarPhaseEmoji:
|
|
|
243
281
|
|
|
244
282
|
return result
|
|
245
283
|
|
|
284
|
+
|
|
246
285
|
def get_moon_phase_name_from_phase_int(phase: int) -> LunarPhaseName:
|
|
247
286
|
"""
|
|
248
|
-
|
|
287
|
+
Get the name of a lunar phase from its numerical value.
|
|
249
288
|
|
|
250
289
|
Args:
|
|
251
|
-
|
|
290
|
+
phase: The lunar phase number (0-28)
|
|
252
291
|
|
|
253
292
|
Returns:
|
|
254
|
-
|
|
293
|
+
The corresponding name for the lunar phase
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
KerykeionException: If phase is outside valid range
|
|
255
297
|
"""
|
|
256
298
|
lunar_phase_names = get_args(LunarPhaseName)
|
|
257
299
|
|
|
258
|
-
|
|
259
300
|
if phase == 1:
|
|
260
301
|
result = lunar_phase_names[0]
|
|
261
302
|
elif phase < 7:
|
|
262
|
-
result =
|
|
303
|
+
result = lunar_phase_names[1]
|
|
263
304
|
elif 7 <= phase <= 9:
|
|
264
305
|
result = lunar_phase_names[2]
|
|
265
306
|
elif phase < 14:
|
|
@@ -281,8 +322,15 @@ def get_moon_phase_name_from_phase_int(phase: int) -> LunarPhaseName:
|
|
|
281
322
|
|
|
282
323
|
def check_and_adjust_polar_latitude(latitude: float) -> float:
|
|
283
324
|
"""
|
|
284
|
-
|
|
285
|
-
|
|
325
|
+
Adjust latitude values for polar regions to prevent calculation errors.
|
|
326
|
+
|
|
327
|
+
Latitudes beyond ±66° are clamped to ±66° for house calculations.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
latitude: The original latitude value
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
The adjusted latitude value, clamped between -66° and 66°
|
|
286
334
|
"""
|
|
287
335
|
if latitude > 66.0:
|
|
288
336
|
latitude = 66.0
|
|
@@ -295,45 +343,57 @@ def check_and_adjust_polar_latitude(latitude: float) -> float:
|
|
|
295
343
|
return latitude
|
|
296
344
|
|
|
297
345
|
|
|
298
|
-
def get_houses_list(
|
|
346
|
+
def get_houses_list(
|
|
347
|
+
subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel]
|
|
348
|
+
) -> list[KerykeionPointModel]:
|
|
299
349
|
"""
|
|
300
|
-
|
|
350
|
+
Get a list of house objects in order from the subject.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
subject: The astrological subject containing house data
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
List of KerykeionPointModel objects representing the houses
|
|
301
357
|
"""
|
|
302
358
|
houses_absolute_position_list = []
|
|
303
359
|
for house in subject.houses_names_list:
|
|
304
|
-
|
|
360
|
+
houses_absolute_position_list.append(subject[house.lower()])
|
|
305
361
|
|
|
306
362
|
return houses_absolute_position_list
|
|
307
363
|
|
|
308
364
|
|
|
309
|
-
def get_available_astrological_points_list(
|
|
365
|
+
def get_available_astrological_points_list(
|
|
366
|
+
subject: AstrologicalSubjectModel
|
|
367
|
+
) -> list[KerykeionPointModel]:
|
|
310
368
|
"""
|
|
311
|
-
|
|
312
|
-
|
|
369
|
+
Get a list of active astrological point objects from the subject.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
subject: The astrological subject containing point data
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
List of KerykeionPointModel objects for all active points
|
|
313
376
|
"""
|
|
314
377
|
planets_absolute_position_list = []
|
|
315
|
-
for planet in subject.
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
for axis in subject.axial_cusps_names_list:
|
|
319
|
-
planets_absolute_position_list.append(subject[axis.lower()])
|
|
378
|
+
for planet in subject.active_points:
|
|
379
|
+
planets_absolute_position_list.append(subject[planet.lower()])
|
|
320
380
|
|
|
321
381
|
return planets_absolute_position_list
|
|
322
382
|
|
|
323
383
|
|
|
324
384
|
def circular_mean(first_position: Union[int, float], second_position: Union[int, float]) -> float:
|
|
325
385
|
"""
|
|
326
|
-
|
|
386
|
+
Calculate the circular mean of two angular positions.
|
|
327
387
|
|
|
328
|
-
This
|
|
329
|
-
avoiding errors that occur with simple
|
|
388
|
+
This method correctly handles positions that cross the 0°/360° boundary,
|
|
389
|
+
avoiding errors that occur with simple arithmetic means.
|
|
330
390
|
|
|
331
391
|
Args:
|
|
332
|
-
|
|
333
|
-
|
|
392
|
+
first_position: First angular position in degrees (0-360)
|
|
393
|
+
second_position: Second angular position in degrees (0-360)
|
|
334
394
|
|
|
335
395
|
Returns:
|
|
336
|
-
|
|
396
|
+
The circular mean position in degrees (0-360)
|
|
337
397
|
"""
|
|
338
398
|
x = (math.cos(math.radians(first_position)) + math.cos(math.radians(second_position))) / 2
|
|
339
399
|
y = (math.sin(math.radians(first_position)) + math.sin(math.radians(second_position))) / 2
|
|
@@ -348,18 +408,15 @@ def circular_mean(first_position: Union[int, float], second_position: Union[int,
|
|
|
348
408
|
|
|
349
409
|
def calculate_moon_phase(moon_abs_pos: float, sun_abs_pos: float) -> LunarPhaseModel:
|
|
350
410
|
"""
|
|
351
|
-
Calculate
|
|
411
|
+
Calculate lunar phase information from Sun and Moon positions.
|
|
352
412
|
|
|
353
413
|
Args:
|
|
354
|
-
|
|
355
|
-
|
|
414
|
+
moon_abs_pos: Absolute position of the Moon in degrees
|
|
415
|
+
sun_abs_pos: Absolute position of the Sun in degrees
|
|
356
416
|
|
|
357
417
|
Returns:
|
|
358
|
-
|
|
418
|
+
LunarPhaseModel containing phase data, emoji, and name
|
|
359
419
|
"""
|
|
360
|
-
# Initialize moon_phase and sun_phase to None in case of an error
|
|
361
|
-
moon_phase, sun_phase = None, None
|
|
362
|
-
|
|
363
420
|
# Calculate the anti-clockwise degrees between the sun and moon
|
|
364
421
|
degrees_between = (moon_abs_pos - sun_abs_pos) % 360
|
|
365
422
|
|
|
@@ -367,42 +424,23 @@ def calculate_moon_phase(moon_abs_pos: float, sun_abs_pos: float) -> LunarPhaseM
|
|
|
367
424
|
step = 360.0 / 28.0
|
|
368
425
|
moon_phase = int(degrees_between // step) + 1
|
|
369
426
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
# Calculate the sun phase (1-28) based on the degrees between the sun and moon
|
|
377
|
-
for x in range(len(sunstep)):
|
|
378
|
-
low = sunstep[x]
|
|
379
|
-
high = sunstep[x + 1] if x < len(sunstep) - 1 else 360
|
|
380
|
-
if low <= degrees_between < high:
|
|
381
|
-
sun_phase = x + 1
|
|
382
|
-
break
|
|
383
|
-
|
|
384
|
-
# Create a dictionary with the lunar phase information
|
|
385
|
-
lunar_phase_dictionary = {
|
|
386
|
-
"degrees_between_s_m": degrees_between,
|
|
387
|
-
"moon_phase": moon_phase,
|
|
388
|
-
"sun_phase": sun_phase,
|
|
389
|
-
"moon_emoji": get_moon_emoji_from_phase_int(moon_phase),
|
|
390
|
-
"moon_phase_name": get_moon_phase_name_from_phase_int(moon_phase)
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return LunarPhaseModel(**lunar_phase_dictionary)
|
|
427
|
+
return LunarPhaseModel(
|
|
428
|
+
degrees_between_s_m=degrees_between,
|
|
429
|
+
moon_phase=moon_phase,
|
|
430
|
+
moon_emoji=get_moon_emoji_from_phase_int(moon_phase),
|
|
431
|
+
moon_phase_name=get_moon_phase_name_from_phase_int(moon_phase)
|
|
432
|
+
)
|
|
394
433
|
|
|
395
434
|
|
|
396
435
|
def circular_sort(degrees: list[Union[int, float]]) -> list[Union[int, float]]:
|
|
397
436
|
"""
|
|
398
|
-
Sort
|
|
399
|
-
and progressing clockwise around the circle.
|
|
437
|
+
Sort degrees in circular clockwise progression starting from the first element.
|
|
400
438
|
|
|
401
439
|
Args:
|
|
402
|
-
degrees:
|
|
440
|
+
degrees: List of numeric degree values
|
|
403
441
|
|
|
404
442
|
Returns:
|
|
405
|
-
|
|
443
|
+
List sorted by clockwise distance from the first element
|
|
406
444
|
|
|
407
445
|
Raises:
|
|
408
446
|
ValueError: If the list is empty or contains non-numeric values
|
|
@@ -445,14 +483,16 @@ def circular_sort(degrees: list[Union[int, float]]) -> list[Union[int, float]]:
|
|
|
445
483
|
|
|
446
484
|
def inline_css_variables_in_svg(svg_content: str) -> str:
|
|
447
485
|
"""
|
|
448
|
-
|
|
486
|
+
Replace CSS custom properties (variables) with their values in SVG content.
|
|
487
|
+
|
|
488
|
+
Extracts CSS variables from style blocks, replaces var() references with actual values,
|
|
489
|
+
and removes all style blocks from the SVG.
|
|
449
490
|
|
|
450
491
|
Args:
|
|
451
|
-
svg_content
|
|
492
|
+
svg_content: The original SVG string with CSS variables
|
|
452
493
|
|
|
453
494
|
Returns:
|
|
454
|
-
|
|
455
|
-
and all style blocks removed
|
|
495
|
+
Modified SVG with CSS variables inlined and style blocks removed
|
|
456
496
|
"""
|
|
457
497
|
# Find and extract CSS custom properties from style tags
|
|
458
498
|
css_variable_map = {}
|
|
@@ -473,6 +513,15 @@ def inline_css_variables_in_svg(svg_content: str) -> str:
|
|
|
473
513
|
|
|
474
514
|
# Function to replace var() references with their actual values
|
|
475
515
|
def replace_css_variable_reference(match):
|
|
516
|
+
"""
|
|
517
|
+
Replace CSS variable references with their actual values.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
match: Regular expression match object containing variable name and optional fallback.
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
str: The resolved CSS variable value or fallback value.
|
|
524
|
+
"""
|
|
476
525
|
variable_name = match.group(1).strip()
|
|
477
526
|
fallback_value = match.group(2) if match.group(2) else None
|
|
478
527
|
|
|
@@ -490,8 +539,238 @@ def inline_css_variables_in_svg(svg_content: str) -> str:
|
|
|
490
539
|
# This handles nested variables or variables that reference other variables
|
|
491
540
|
processed_svg = svg_without_style_blocks
|
|
492
541
|
while variable_usage_pattern.search(processed_svg):
|
|
493
|
-
processed_svg = variable_usage_pattern.sub(
|
|
494
|
-
lambda m: replace_css_variable_reference(m), processed_svg
|
|
495
|
-
)
|
|
542
|
+
processed_svg = variable_usage_pattern.sub(lambda m: replace_css_variable_reference(m), processed_svg)
|
|
496
543
|
|
|
497
544
|
return processed_svg
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def datetime_to_julian(dt: datetime) -> float:
|
|
548
|
+
"""
|
|
549
|
+
Convert a Python datetime object to Julian Day Number.
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
dt: The datetime object to convert
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
The corresponding Julian Day Number (JD) as a float
|
|
556
|
+
"""
|
|
557
|
+
# Extract year, month and day
|
|
558
|
+
year = dt.year
|
|
559
|
+
month = dt.month
|
|
560
|
+
day = dt.day
|
|
561
|
+
|
|
562
|
+
# Adjust month and year according to the conversion formula
|
|
563
|
+
if month <= 2:
|
|
564
|
+
year -= 1
|
|
565
|
+
month += 12
|
|
566
|
+
|
|
567
|
+
# Calculate century and year in century
|
|
568
|
+
a = year // 100
|
|
569
|
+
b = 2 - a + (a // 4)
|
|
570
|
+
|
|
571
|
+
# Calculate the Julian day
|
|
572
|
+
jd = int(365.25 * (year + 4716)) + int(30.6001 * (month + 1)) + day + b - 1524.5
|
|
573
|
+
|
|
574
|
+
# Add the time portion
|
|
575
|
+
hour = dt.hour
|
|
576
|
+
minute = dt.minute
|
|
577
|
+
second = dt.second
|
|
578
|
+
microsecond = dt.microsecond
|
|
579
|
+
|
|
580
|
+
jd += (hour + minute / 60 + second / 3600 + microsecond / 3600000000) / 24
|
|
581
|
+
|
|
582
|
+
return jd
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def julian_to_datetime(jd):
|
|
586
|
+
"""
|
|
587
|
+
Convert a Julian Day Number to a Python datetime object.
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
jd: Julian Day Number as a float
|
|
591
|
+
|
|
592
|
+
Returns:
|
|
593
|
+
The corresponding datetime object
|
|
594
|
+
"""
|
|
595
|
+
# Add 0.5 to the Julian day to adjust for noon-based Julian day
|
|
596
|
+
jd_plus = jd + 0.5
|
|
597
|
+
|
|
598
|
+
# Integer and fractional parts
|
|
599
|
+
Z = int(jd_plus)
|
|
600
|
+
F = jd_plus - Z
|
|
601
|
+
|
|
602
|
+
# Calculate alpha
|
|
603
|
+
if Z < 2299161:
|
|
604
|
+
A = Z # Julian calendar
|
|
605
|
+
else:
|
|
606
|
+
alpha = int((Z - 1867216.25) / 36524.25)
|
|
607
|
+
A = Z + 1 + alpha - int(alpha / 4) # Gregorian calendar
|
|
608
|
+
|
|
609
|
+
# Calculate B
|
|
610
|
+
B = A + 1524
|
|
611
|
+
|
|
612
|
+
# Calculate C
|
|
613
|
+
C = int((B - 122.1) / 365.25)
|
|
614
|
+
|
|
615
|
+
# Calculate D
|
|
616
|
+
D = int(365.25 * C)
|
|
617
|
+
|
|
618
|
+
# Calculate E
|
|
619
|
+
E = int((B - D) / 30.6001)
|
|
620
|
+
|
|
621
|
+
# Calculate day and month
|
|
622
|
+
day = B - D - int(30.6001 * E) + F
|
|
623
|
+
|
|
624
|
+
# Integer part of day
|
|
625
|
+
day_int = int(day)
|
|
626
|
+
|
|
627
|
+
# Fractional part converted to hours, minutes, seconds, microseconds
|
|
628
|
+
day_frac = day - day_int
|
|
629
|
+
hours = int(day_frac * 24)
|
|
630
|
+
minutes = int((day_frac * 24 - hours) * 60)
|
|
631
|
+
seconds = int((day_frac * 24 * 60 - hours * 60 - minutes) * 60)
|
|
632
|
+
microseconds = int(((day_frac * 24 * 60 - hours * 60 - minutes) * 60 - seconds) * 1000000)
|
|
633
|
+
|
|
634
|
+
# Calculate month
|
|
635
|
+
if E < 14:
|
|
636
|
+
month = E - 1
|
|
637
|
+
else:
|
|
638
|
+
month = E - 13
|
|
639
|
+
|
|
640
|
+
# Calculate year
|
|
641
|
+
if month > 2:
|
|
642
|
+
year = C - 4716
|
|
643
|
+
else:
|
|
644
|
+
year = C - 4715
|
|
645
|
+
|
|
646
|
+
# Create and return datetime object
|
|
647
|
+
return datetime(year, month, day_int, hours, minutes, seconds, microseconds)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def get_house_name(house_number: int) -> Houses:
|
|
651
|
+
"""
|
|
652
|
+
Convert a house number to its corresponding house name.
|
|
653
|
+
|
|
654
|
+
Args:
|
|
655
|
+
house_number: House number (1-12)
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
The house name
|
|
659
|
+
|
|
660
|
+
Raises:
|
|
661
|
+
ValueError: If house_number is not in range 1-12
|
|
662
|
+
"""
|
|
663
|
+
house_names: dict[int, Houses] = {
|
|
664
|
+
1: "First_House",
|
|
665
|
+
2: "Second_House",
|
|
666
|
+
3: "Third_House",
|
|
667
|
+
4: "Fourth_House",
|
|
668
|
+
5: "Fifth_House",
|
|
669
|
+
6: "Sixth_House",
|
|
670
|
+
7: "Seventh_House",
|
|
671
|
+
8: "Eighth_House",
|
|
672
|
+
9: "Ninth_House",
|
|
673
|
+
10: "Tenth_House",
|
|
674
|
+
11: "Eleventh_House",
|
|
675
|
+
12: "Twelfth_House",
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
name = house_names.get(house_number, None)
|
|
679
|
+
if name is None:
|
|
680
|
+
raise ValueError(f"Invalid house number: {house_number}")
|
|
681
|
+
|
|
682
|
+
return name
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def get_house_number(house_name: Houses) -> int:
|
|
686
|
+
"""
|
|
687
|
+
Convert a house name to its corresponding house number.
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
house_name: The house name
|
|
691
|
+
|
|
692
|
+
Returns:
|
|
693
|
+
House number (1-12)
|
|
694
|
+
|
|
695
|
+
Raises:
|
|
696
|
+
ValueError: If house_name is not recognized
|
|
697
|
+
"""
|
|
698
|
+
house_numbers: dict[Houses, int] = {
|
|
699
|
+
"First_House": 1,
|
|
700
|
+
"Second_House": 2,
|
|
701
|
+
"Third_House": 3,
|
|
702
|
+
"Fourth_House": 4,
|
|
703
|
+
"Fifth_House": 5,
|
|
704
|
+
"Sixth_House": 6,
|
|
705
|
+
"Seventh_House": 7,
|
|
706
|
+
"Eighth_House": 8,
|
|
707
|
+
"Ninth_House": 9,
|
|
708
|
+
"Tenth_House": 10,
|
|
709
|
+
"Eleventh_House": 11,
|
|
710
|
+
"Twelfth_House": 12,
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
number = house_numbers.get(house_name, None)
|
|
714
|
+
if number is None:
|
|
715
|
+
raise ValueError(f"Invalid house name: {house_name}")
|
|
716
|
+
|
|
717
|
+
return number
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def find_common_active_points(first_points: list[AstrologicalPoint], second_points: list[AstrologicalPoint]) -> list[AstrologicalPoint]:
|
|
721
|
+
"""
|
|
722
|
+
Find astrological points that appear in both input lists.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
first_points: First list of astrological points
|
|
726
|
+
second_points: Second list of astrological points
|
|
727
|
+
|
|
728
|
+
Returns:
|
|
729
|
+
List of points common to both input lists (without duplicates)
|
|
730
|
+
"""
|
|
731
|
+
common_points = list(set(first_points) & set(second_points))
|
|
732
|
+
|
|
733
|
+
return common_points
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def distribute_percentages_to_100(values: dict[str, float]) -> dict[str, int]:
|
|
737
|
+
"""
|
|
738
|
+
Distribute percentages so they sum to exactly 100.
|
|
739
|
+
|
|
740
|
+
This function uses a largest remainder method to ensure that
|
|
741
|
+
the percentage total equals 100 even after rounding.
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
values: Dictionary with keys and their raw percentage values
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
Dictionary with the same keys and integer percentages that sum to 100
|
|
748
|
+
"""
|
|
749
|
+
if not values:
|
|
750
|
+
return {}
|
|
751
|
+
|
|
752
|
+
total = sum(values.values())
|
|
753
|
+
if total == 0:
|
|
754
|
+
return {key: 0 for key in values.keys()}
|
|
755
|
+
|
|
756
|
+
# Calculate base percentages
|
|
757
|
+
percentages = {key: value * 100 / total for key, value in values.items()}
|
|
758
|
+
|
|
759
|
+
# Get integer parts and remainders
|
|
760
|
+
integer_parts = {key: int(value) for key, value in percentages.items()}
|
|
761
|
+
remainders = {key: percentages[key] - integer_parts[key] for key in percentages.keys()}
|
|
762
|
+
|
|
763
|
+
# Calculate how many we need to add to reach 100
|
|
764
|
+
current_sum = sum(integer_parts.values())
|
|
765
|
+
needed = 100 - current_sum
|
|
766
|
+
|
|
767
|
+
# Sort by remainder (largest first) and add 1 to the largest remainders
|
|
768
|
+
sorted_by_remainder = sorted(remainders.items(), key=lambda x: x[1], reverse=True)
|
|
769
|
+
|
|
770
|
+
result = integer_parts.copy()
|
|
771
|
+
for i in range(needed):
|
|
772
|
+
if i < len(sorted_by_remainder):
|
|
773
|
+
key = sorted_by_remainder[i][0]
|
|
774
|
+
result[key] += 1
|
|
775
|
+
|
|
776
|
+
return result
|