kerykeion 4.26.3__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.

Files changed (76) hide show
  1. kerykeion/__init__.py +54 -11
  2. kerykeion/aspects/__init__.py +5 -2
  3. kerykeion/aspects/aspects_factory.py +569 -0
  4. kerykeion/aspects/aspects_utils.py +81 -8
  5. kerykeion/astrological_subject_factory.py +1897 -0
  6. kerykeion/backword.py +773 -0
  7. kerykeion/chart_data_factory.py +549 -0
  8. kerykeion/charts/chart_drawer.py +2601 -0
  9. kerykeion/charts/charts_utils.py +948 -177
  10. kerykeion/charts/draw_planets.py +602 -351
  11. kerykeion/charts/templates/aspect_grid_only.xml +328 -202
  12. kerykeion/charts/templates/chart.xml +432 -272
  13. kerykeion/charts/templates/wheel_only.xml +350 -214
  14. kerykeion/charts/themes/black-and-white.css +148 -0
  15. kerykeion/charts/themes/classic.css +107 -76
  16. kerykeion/charts/themes/dark-high-contrast.css +145 -107
  17. kerykeion/charts/themes/dark.css +146 -107
  18. kerykeion/charts/themes/light.css +146 -103
  19. kerykeion/charts/themes/strawberry.css +158 -0
  20. kerykeion/composite_subject_factory.py +253 -51
  21. kerykeion/ephemeris_data_factory.py +434 -0
  22. kerykeion/fetch_geonames.py +27 -8
  23. kerykeion/house_comparison/__init__.py +6 -0
  24. kerykeion/house_comparison/house_comparison_factory.py +103 -0
  25. kerykeion/house_comparison/house_comparison_utils.py +126 -0
  26. kerykeion/kr_types/__init__.py +66 -6
  27. kerykeion/kr_types/chart_template_model.py +20 -0
  28. kerykeion/kr_types/kerykeion_exception.py +15 -9
  29. kerykeion/kr_types/kr_literals.py +14 -132
  30. kerykeion/kr_types/kr_models.py +14 -318
  31. kerykeion/kr_types/settings_models.py +15 -203
  32. kerykeion/planetary_return_factory.py +805 -0
  33. kerykeion/relationship_score_factory.py +301 -0
  34. kerykeion/report.py +751 -64
  35. kerykeion/schemas/__init__.py +106 -0
  36. kerykeion/schemas/chart_template_model.py +367 -0
  37. kerykeion/schemas/kerykeion_exception.py +20 -0
  38. kerykeion/schemas/kr_literals.py +181 -0
  39. kerykeion/schemas/kr_models.py +605 -0
  40. kerykeion/schemas/settings_models.py +180 -0
  41. kerykeion/settings/__init__.py +20 -1
  42. kerykeion/settings/chart_defaults.py +444 -0
  43. kerykeion/settings/config_constants.py +117 -12
  44. kerykeion/settings/kerykeion_settings.py +31 -73
  45. kerykeion/settings/translation_strings.py +1479 -0
  46. kerykeion/settings/translations.py +74 -0
  47. kerykeion/sweph/ast136/s136108s.se1 +0 -0
  48. kerykeion/sweph/ast136/s136199s.se1 +0 -0
  49. kerykeion/sweph/ast136/s136472s.se1 +0 -0
  50. kerykeion/sweph/ast28/se28978s.se1 +0 -0
  51. kerykeion/sweph/ast50/se50000s.se1 +0 -0
  52. kerykeion/sweph/ast90/se90377s.se1 +0 -0
  53. kerykeion/sweph/ast90/se90482s.se1 +0 -0
  54. kerykeion/sweph/sefstars.txt +1602 -0
  55. kerykeion/transits_time_range_factory.py +302 -0
  56. kerykeion/utilities.py +393 -114
  57. kerykeion-5.0.0.dist-info/METADATA +1176 -0
  58. kerykeion-5.0.0.dist-info/RECORD +63 -0
  59. {kerykeion-4.26.3.dist-info → kerykeion-5.0.0.dist-info}/WHEEL +1 -1
  60. kerykeion/aspects/natal_aspects.py +0 -172
  61. kerykeion/aspects/synastry_aspects.py +0 -124
  62. kerykeion/aspects/transits_time_range.py +0 -41
  63. kerykeion/astrological_subject.py +0 -841
  64. kerykeion/charts/kerykeion_chart_svg.py +0 -1219
  65. kerykeion/enums.py +0 -57
  66. kerykeion/ephemeris_data.py +0 -242
  67. kerykeion/kr_types/chart_types.py +0 -95
  68. kerykeion/relationship_score/__init__.py +0 -2
  69. kerykeion/relationship_score/relationship_score.py +0 -175
  70. kerykeion/relationship_score/relationship_score_factory.py +0 -230
  71. kerykeion/settings/kr.config.json +0 -1258
  72. kerykeion/transits_time_range.py +0 -124
  73. kerykeion-4.26.3.dist-info/METADATA +0 -634
  74. kerykeion-4.26.3.dist-info/RECORD +0 -45
  75. kerykeion-4.26.3.dist-info/entry_points.txt +0 -3
  76. {kerykeion-4.26.3.dist-info → kerykeion-5.0.0.dist-info/licenses}/LICENSE +0 -0
kerykeion/utilities.py CHANGED
@@ -1,16 +1,39 @@
1
- from kerykeion.kr_types import KerykeionPointModel, KerykeionException, ZodiacSignModel, AstrologicalSubjectModel, LunarPhaseModel
2
- from kerykeion.kr_types.kr_literals import LunarPhaseEmoji, LunarPhaseName, PointType, Planet, Houses, AxialCusps
3
- from typing import Union, get_args, TYPE_CHECKING
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
- def get_number_from_name(name: Planet) -> int:
13
- """Utility function, gets planet id from the name."""
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 == "Mean_Node":
58
+ elif name == "Mean_North_Lunar_Node":
36
59
  return 10
37
- elif name == "True_Node":
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 == "Mean_South_Node":
63
+ elif name == "Mean_South_Lunar_Node":
41
64
  return 1000
42
- elif name == "True_South_Node":
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": # TODO: Is this needed?
71
+ elif name == "Ascendant": # TODO: Is this needed?
49
72
  return 9900
50
- elif name == "Descendant": # TODO: Is this needed?
73
+ elif name == "Descendant": # TODO: Is this needed?
51
74
  return 9901
52
- elif name == "Medium_Coeli": # TODO: Is this needed?
75
+ elif name == "Medium_Coeli": # TODO: Is this needed?
53
76
  return 9902
54
- elif name == "Imum_Coeli": # TODO: Is this needed?
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[Planet, Houses, AxialCusps], point_type: PointType
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
- Returns a KerykeionPointModel object based on the given degree.
87
+ Create a KerykeionPointModel from a degree position.
65
88
 
66
89
  Args:
67
- degree (Union[int, float]): The degree of the celestial point.
68
- name (str): The name of the celestial point.
69
- point_type (PointType): The type of the celestial point.
70
-
71
- Raises:
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: The model representing the celestial point.
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 < 0 or degree >= 360:
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
- Setup logging for testing.
145
+ Configure logging for the application.
115
146
 
116
147
  Args:
117
- level: Log level as a string, options: debug, info, warning, error
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
- Determines if a point is between two others on a circle, with additional rules:
138
- - If evaluated_point == start_point, it is considered between.
139
- - If evaluated_point == end_point, it is NOT considered between.
140
- - The range between start_point and end_point must not exceed 180°.
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
- - start_point: The first point on the circle.
144
- - end_point: The second point on the circle.
145
- - evaluated_point: The point to check.
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
- - True if evaluated_point is between start_point and end_point, False otherwise.
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(f"The angle between start and end point is not allowed to exceed 180°, yet is: {angular_difference}")
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
- Determines the house in which a planet is located based on its position in degrees.
219
+ Determine which house contains a planet based on its degree position.
185
220
 
186
221
  Args:
187
- planet_position_degree (Union[int, float]): The position of the planet in degrees.
188
- houses_degree_ut_list (list): A list of the houses in degrees (0-360).
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
- str: The house in which the planet is located.
226
+ The house name containing the planet
192
227
 
193
228
  Raises:
194
- ValueError: If the planet's position does not fall within any house range.
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
- Returns the emoji of the moon phase.
248
+ Get the emoji representation of a lunar phase.
214
249
 
215
250
  Args:
216
- - phase: The phase of the moon (0-28)
251
+ phase: The lunar phase number (0-28)
217
252
 
218
253
  Returns:
219
- - The emoji of the moon phase
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
- Returns the name of the moon phase.
287
+ Get the name of a lunar phase from its numerical value.
249
288
 
250
289
  Args:
251
- - phase: The phase of the moon (0-28)
290
+ phase: The lunar phase number (0-28)
252
291
 
253
292
  Returns:
254
- - The name of the moon phase
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 = lunar_phase_names[1]
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
- Utility function to check if the location is in the polar circle.
285
- If it is, it sets the latitude to 66 or -66 degrees.
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(subject: Union["AstrologicalSubject", AstrologicalSubjectModel]) -> list[KerykeionPointModel]:
346
+ def get_houses_list(
347
+ subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel]
348
+ ) -> list[KerykeionPointModel]:
299
349
  """
300
- Return the names of the houses in the order of the houses.
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
- houses_absolute_position_list.append(subject[house.lower()])
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(subject: Union["AstrologicalSubject", AstrologicalSubjectModel]) -> list[KerykeionPointModel]:
365
+ def get_available_astrological_points_list(
366
+ subject: AstrologicalSubjectModel
367
+ ) -> list[KerykeionPointModel]:
310
368
  """
311
- Return the names of the planets in the order of the planets.
312
- The names can be used to access the planets from the AstrologicalSubject object with the __getitem__ method or the [] operator.
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.planets_names_list:
316
- planets_absolute_position_list.append(subject[planet.lower()])
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
- Computes the circular mean of two astrological positions (e.g., house cusps, planets).
386
+ Calculate the circular mean of two angular positions.
327
387
 
328
- This function ensures that positions crossing Aries (360°) are correctly averaged,
329
- avoiding errors that occur with simple linear means.
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
- position1 (Union[int, float]): First position in degrees (0-360).
333
- position2 (Union[int, float]): Second position in degrees (0-360).
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
- float: The circular mean position in degrees (0-360).
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 the lunar phase based on the positions of the moon and sun.
411
+ Calculate lunar phase information from Sun and Moon positions.
352
412
 
353
413
  Args:
354
- - moon_abs_pos (float): The absolute position of the moon.
355
- - sun_abs_pos (float): The absolute position of the sun.
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
- - dict: A dictionary containing the lunar phase information.
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
- # Define the sun phase steps
371
- sunstep = [
372
- 0, 30, 40, 50, 60, 70, 80, 90, 120, 130, 140, 150, 160, 170, 180,
373
- 210, 220, 230, 240, 250, 260, 270, 300, 310, 320, 330, 340, 350
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 a list of degrees in a circular manner, starting from the first element
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: A list of numeric values representing degrees
440
+ degrees: List of numeric degree values
403
441
 
404
442
  Returns:
405
- A list sorted based on circular clockwise progression from the first element
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
- Process an SVG string to inline all CSS custom properties.
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 (str): The original SVG string with CSS variables
492
+ svg_content: The original SVG string with CSS variables
452
493
 
453
494
  Returns:
454
- str: The modified SVG with all CSS variables replaced by their values
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