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.

@@ -1023,65 +1023,34 @@ def draw_transit_aspect_list(
1023
1023
 
1024
1024
 
1025
1025
  def calculate_moon_phase_chart_params(
1026
- degrees_between_sun_and_moon: float,
1027
- latitude: float
1026
+ degrees_between_sun_and_moon: float
1028
1027
  ) -> dict:
1029
1028
  """
1030
- Calculate the parameters for the moon phase chart.
1029
+ Calculate normalized parameters used by the moon phase icon.
1031
1030
 
1032
1031
  Parameters:
1033
- - degrees_between_sun_and_moon (float): The degrees between the sun and the moon.
1034
- - latitude (float): The latitude for rotation calculation.
1032
+ - degrees_between_sun_and_moon (float): The elongation between the sun and moon.
1035
1033
 
1036
1034
  Returns:
1037
- - dict: The moon phase chart parameters.
1038
- """
1039
- deg = degrees_between_sun_and_moon
1040
-
1041
- # Initialize variables for lunar phase properties
1042
- circle_center_x = None
1043
- circle_radius = None
1044
-
1045
- # Determine lunar phase properties based on the degree
1046
- if deg < 90.0:
1047
- max_radius = deg
1048
- if deg > 80.0:
1049
- max_radius = max_radius * max_radius
1050
- circle_center_x = 20.0 + (deg / 90.0) * (max_radius + 10.0)
1051
- circle_radius = 10.0 + (deg / 90.0) * max_radius
1052
-
1053
- elif deg < 180.0:
1054
- max_radius = 180.0 - deg
1055
- if deg < 100.0:
1056
- max_radius = max_radius * max_radius
1057
- circle_center_x = 20.0 + ((deg - 90.0) / 90.0 * (max_radius + 10.0)) - (max_radius + 10.0)
1058
- circle_radius = 10.0 + max_radius - ((deg - 90.0) / 90.0 * max_radius)
1059
-
1060
- elif deg < 270.0:
1061
- max_radius = deg - 180.0
1062
- if deg > 260.0:
1063
- max_radius = max_radius * max_radius
1064
- circle_center_x = 20.0 + ((deg - 180.0) / 90.0 * (max_radius + 10.0))
1065
- circle_radius = 10.0 + ((deg - 180.0) / 90.0 * max_radius)
1066
-
1067
- elif deg < 361.0:
1068
- max_radius = 360.0 - deg
1069
- if deg < 280.0:
1070
- max_radius = max_radius * max_radius
1071
- circle_center_x = 20.0 + ((deg - 270.0) / 90.0 * (max_radius + 10.0)) - (max_radius + 10.0)
1072
- circle_radius = 10.0 + max_radius - ((deg - 270.0) / 90.0 * max_radius)
1073
-
1074
- else:
1075
- raise KerykeionException(f"Invalid degree value: {deg}")
1035
+ - dict: Normalized phase data (angle, illuminated fraction, shadow ellipse radius).
1036
+ """
1037
+ if not math.isfinite(degrees_between_sun_and_moon):
1038
+ raise KerykeionException(
1039
+ f"Invalid degree value: {degrees_between_sun_and_moon}"
1040
+ )
1076
1041
 
1042
+ phase_angle = degrees_between_sun_and_moon % 360.0
1043
+ radians = math.radians(phase_angle)
1044
+ cosine = math.cos(radians)
1045
+ illuminated_fraction = (1.0 - cosine) / 2.0
1077
1046
 
1078
- # Calculate rotation based on latitude
1079
- lunar_phase_rotate = -90.0 - latitude
1047
+ # Guard against floating point spillover outside [0, 1].
1048
+ illuminated_fraction = max(0.0, min(1.0, illuminated_fraction))
1080
1049
 
1081
1050
  return {
1082
- "circle_center_x": circle_center_x,
1083
- "circle_radius": circle_radius,
1084
- "lunar_phase_rotate": lunar_phase_rotate,
1051
+ "phase_angle": phase_angle,
1052
+ "illuminated_fraction": illuminated_fraction,
1053
+ "shadow_ellipse_rx": 10.0 * cosine,
1085
1054
  }
1086
1055
 
1087
1056
 
@@ -1697,34 +1666,92 @@ def makeLunarPhase(degrees_between_sun_and_moon: float, latitude: float) -> str:
1697
1666
 
1698
1667
  Parameters:
1699
1668
  - degrees_between_sun_and_moon (float): Angle between sun and moon in degrees
1700
- - latitude (float): Observer's latitude for correct orientation
1669
+ - latitude (float): Observer's latitude (no longer used, kept for backward compatibility)
1701
1670
 
1702
1671
  Returns:
1703
1672
  - str: SVG representation of lunar phase
1704
1673
  """
1705
- # Calculate parameters for the lunar phase visualization
1706
- params = calculate_moon_phase_chart_params(degrees_between_sun_and_moon, latitude)
1707
-
1708
- # Extract the calculated values
1709
- lunar_phase_circle_center_x = params["circle_center_x"]
1710
- lunar_phase_circle_radius = params["circle_radius"]
1711
- lunar_phase_rotate = params["lunar_phase_rotate"]
1712
-
1713
- # Generate the SVG for the lunar phase
1714
- svg = (
1715
- f'<g transform="rotate({lunar_phase_rotate} 20 10)">\n'
1716
- f' <defs>\n'
1717
- f' <clipPath id="moonPhaseCutOffCircle">\n'
1718
- f' <circle cx="20" cy="10" r="10" />\n'
1719
- f' </clipPath>\n'
1720
- f' </defs>\n'
1721
- f' <circle cx="20" cy="10" r="10" style="fill: var(--kerykeion-chart-color-lunar-phase-0)" />\n'
1722
- f' <circle cx="{lunar_phase_circle_center_x}" cy="10" r="{lunar_phase_circle_radius}" style="fill: var(--kerykeion-chart-color-lunar-phase-1)" clip-path="url(#moonPhaseCutOffCircle)" />\n'
1723
- f' <circle cx="20" cy="10" r="10" style="fill: none; stroke: var(--kerykeion-chart-color-lunar-phase-0); stroke-width: 0.5px; stroke-opacity: 0.5" />\n'
1724
- f'</g>'
1674
+ params = calculate_moon_phase_chart_params(degrees_between_sun_and_moon)
1675
+
1676
+ phase_angle = params["phase_angle"]
1677
+ illuminated_fraction = 1.0 - params["illuminated_fraction"]
1678
+ shadow_ellipse_rx = abs(params["shadow_ellipse_rx"])
1679
+
1680
+ radius = 10.0
1681
+ center_x = 20.0
1682
+ center_y = 10.0
1683
+
1684
+ bright_color = "var(--kerykeion-chart-color-lunar-phase-1)"
1685
+ shadow_color = "var(--kerykeion-chart-color-lunar-phase-0)"
1686
+
1687
+ is_waxing = phase_angle < 180.0
1688
+
1689
+ if illuminated_fraction <= 1e-6:
1690
+ base_fill = shadow_color
1691
+ overlay_path = ""
1692
+ overlay_fill = ""
1693
+ elif 1.0 - illuminated_fraction <= 1e-6:
1694
+ base_fill = bright_color
1695
+ overlay_path = ""
1696
+ overlay_fill = ""
1697
+ else:
1698
+ is_lit_major = illuminated_fraction >= 0.5
1699
+ if is_lit_major:
1700
+ base_fill = bright_color
1701
+ overlay_fill = shadow_color
1702
+ overlay_side = "left" if is_waxing else "right"
1703
+ else:
1704
+ base_fill = shadow_color
1705
+ overlay_fill = bright_color
1706
+ overlay_side = "right" if is_waxing else "left"
1707
+
1708
+ # The illuminated limb is the orthographic projection of the lunar terminator;
1709
+ # it appears as an ellipse with vertical radius equal to the lunar radius and
1710
+ # horizontal radius scaled by |cos(phase)|.
1711
+ def build_lune_path(side: str, ellipse_rx: float) -> str:
1712
+ ellipse_rx = max(0.0, min(radius, ellipse_rx))
1713
+ top_y = center_y - radius
1714
+ bottom_y = center_y + radius
1715
+ circle_sweep = 1 if side == "right" else 0
1716
+
1717
+ if ellipse_rx <= 1e-6:
1718
+ return (
1719
+ f"M {center_x:.4f} {top_y:.4f}"
1720
+ f" A {radius:.4f} {radius:.4f} 0 0 {circle_sweep} {center_x:.4f} {bottom_y:.4f}"
1721
+ f" L {center_x:.4f} {top_y:.4f}"
1722
+ " Z"
1723
+ )
1724
+
1725
+ return (
1726
+ f"M {center_x:.4f} {top_y:.4f}"
1727
+ f" A {radius:.4f} {radius:.4f} 0 0 {circle_sweep} {center_x:.4f} {bottom_y:.4f}"
1728
+ f" A {ellipse_rx:.4f} {radius:.4f} 0 0 {circle_sweep} {center_x:.4f} {top_y:.4f}"
1729
+ " Z"
1730
+ )
1731
+
1732
+ overlay_path = build_lune_path(overlay_side, shadow_ellipse_rx)
1733
+
1734
+ svg_lines = [
1735
+ '<g transform="rotate(0 20 10)">',
1736
+ ' <defs>',
1737
+ ' <clipPath id="moonPhaseCutOffCircle">',
1738
+ ' <circle cx="20" cy="10" r="10" />',
1739
+ ' </clipPath>',
1740
+ ' </defs>',
1741
+ f' <circle cx="20" cy="10" r="10" style="fill: {base_fill}" />',
1742
+ ]
1743
+
1744
+ if overlay_path:
1745
+ svg_lines.append(
1746
+ f' <path d="{overlay_path}" style="fill: {overlay_fill}" clip-path="url(#moonPhaseCutOffCircle)" />'
1747
+ )
1748
+
1749
+ svg_lines.append(
1750
+ ' <circle cx="20" cy="10" r="10" style="fill: none; stroke: var(--kerykeion-chart-color-lunar-phase-0); stroke-width: 0.5px; stroke-opacity: 0.5" />'
1725
1751
  )
1752
+ svg_lines.append('</g>')
1726
1753
 
1727
- return svg
1754
+ return "\n".join(svg_lines)
1728
1755
 
1729
1756
 
1730
1757
  def calculate_quality_points(
@@ -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,11 +6,21 @@ 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
+ from os import getenv
12
+ from pathlib import Path
13
+ from typing import Optional, Union
14
+
11
15
  from requests import Request
12
16
  from requests_cache import CachedSession
13
- from typing import Union
17
+
18
+
19
+ logger = getLogger(__name__)
20
+
21
+
22
+ DEFAULT_GEONAMES_CACHE_NAME = Path("cache") / "kerykeion_geonames_cache"
23
+ GEONAMES_CACHE_ENV_VAR = "KERYKEION_GEONAMES_CACHE_NAME"
14
24
 
15
25
 
16
26
  class FetchGeonames:
@@ -25,17 +35,24 @@ class FetchGeonames:
25
35
  country_code: Two-letter country code (ISO 3166-1 alpha-2).
26
36
  username: GeoNames username for API access, defaults to "century.boy".
27
37
  cache_expire_after_days: Number of days to cache responses, defaults to 30.
38
+ cache_name: Optional path (directory or filename stem) used by requests-cache.
39
+ Defaults to "cache/kerykeion_geonames_cache" and may also be overridden
40
+ via the environment variable ``KERYKEION_GEONAMES_CACHE_NAME`` or by
41
+ calling :meth:`FetchGeonames.set_default_cache_name`.
28
42
  """
29
43
 
44
+ default_cache_name: Path = DEFAULT_GEONAMES_CACHE_NAME
45
+
30
46
  def __init__(
31
47
  self,
32
48
  city_name: str,
33
49
  country_code: str,
34
50
  username: str = "century.boy",
35
51
  cache_expire_after_days=30,
52
+ cache_name: Optional[Union[str, Path]] = None,
36
53
  ):
37
54
  self.session = CachedSession(
38
- cache_name="cache/kerykeion_geonames_cache",
55
+ cache_name=str(self._resolve_cache_name(cache_name)),
39
56
  backend="sqlite",
40
57
  expire_after=timedelta(days=cache_expire_after_days),
41
58
  )
@@ -46,6 +63,25 @@ class FetchGeonames:
46
63
  self.base_url = "http://api.geonames.org/searchJSON"
47
64
  self.timezone_url = "http://api.geonames.org/timezoneJSON"
48
65
 
66
+ @classmethod
67
+ def set_default_cache_name(cls, cache_name: Union[str, Path]) -> None:
68
+ """Override the default cache name used when none is provided."""
69
+
70
+ cls.default_cache_name = Path(cache_name)
71
+
72
+ @classmethod
73
+ def _resolve_cache_name(cls, cache_name: Optional[Union[str, Path]]) -> Path:
74
+ """Return the resolved cache name applying overrides in priority order."""
75
+
76
+ if cache_name is not None:
77
+ return Path(cache_name)
78
+
79
+ env_override = getenv(GEONAMES_CACHE_ENV_VAR)
80
+ if env_override:
81
+ return Path(env_override)
82
+
83
+ return cls.default_cache_name
84
+
49
85
  def __get_timezone(self, lat: Union[str, float, int], lon: Union[str, float, int]) -> dict[str, str]:
50
86
  """
51
87
  Get timezone information for a given latitude and longitude.
@@ -63,21 +99,21 @@ class FetchGeonames:
63
99
  params = {"lat": lat, "lng": lon, "username": self.username}
64
100
 
65
101
  prepared_request = Request("GET", self.timezone_url, params=params).prepare()
66
- logging.debug(f"Requesting data from GeoName timezones: {prepared_request.url}")
102
+ logger.debug("GeoNames timezone lookup url=%s", prepared_request.url)
67
103
 
68
104
  try:
69
105
  response = self.session.send(prepared_request)
70
106
  response_json = response.json()
71
107
 
72
108
  except Exception as e:
73
- logging.error(f"Error fetching {self.timezone_url}: {e}")
109
+ logger.error("GeoNames timezone request failed for %s: %s", self.timezone_url, e)
74
110
  return {}
75
111
 
76
112
  try:
77
113
  timezone_data["timezonestr"] = response_json["timezoneId"]
78
114
 
79
115
  except Exception as e:
80
- logging.error(f"Error serializing data maybe wrong username? Details: {e}")
116
+ logger.error("GeoNames timezone payload missing expected keys: %s", e)
81
117
  return {}
82
118
 
83
119
  if hasattr(response, "from_cache"):
@@ -109,15 +145,16 @@ class FetchGeonames:
109
145
  }
110
146
 
111
147
  prepared_request = Request("GET", self.base_url, params=params).prepare()
112
- logging.debug(f"Requesting data from geonames basic: {prepared_request.url}")
148
+ logger.debug("GeoNames search url=%s", prepared_request.url)
113
149
 
114
150
  try:
115
151
  response = self.session.send(prepared_request)
152
+ response.raise_for_status()
116
153
  response_json = response.json()
117
- logging.debug(f"Response from GeoNames: {response_json}")
154
+ logger.debug("GeoNames search response: %s", response_json)
118
155
 
119
156
  except Exception as e:
120
- logging.error(f"Error in fetching {self.base_url}: {e}")
157
+ logger.error("GeoNames search request failed for %s: %s", self.base_url, e)
121
158
  return {}
122
159
 
123
160
  try:
@@ -127,7 +164,7 @@ class FetchGeonames:
127
164
  city_data_whitout_tz["countryCode"] = response_json["geonames"][0]["countryCode"]
128
165
 
129
166
  except Exception as e:
130
- logging.error(f"Error serializing data maybe wrong username? Details: {e}")
167
+ logger.error("GeoNames search payload missing expected keys: %s", e)
131
168
  return {}
132
169
 
133
170
  if hasattr(response, "from_cache"):
@@ -147,15 +184,16 @@ class FetchGeonames:
147
184
  timezone_response = self.__get_timezone(city_data_response["lat"], city_data_response["lng"])
148
185
 
149
186
  except Exception as e:
150
- logging.error(f"Error in fetching timezone: {e}")
187
+ logger.error("Unable to fetch timezone details: %s", e)
151
188
  return {}
152
189
 
153
190
  return {**timezone_response, **city_data_response}
154
191
 
155
192
 
156
193
  if __name__ == "__main__":
157
- from kerykeion.utilities import setup_logging
158
- setup_logging(level="debug")
194
+ """Run a tiny demonstration when executing the module directly."""
195
+ from kerykeion.utilities import setup_logging as configure_logging
159
196
 
197
+ configure_logging("debug")
160
198
  geonames = FetchGeonames("Montichiari", "IT")
161
199
  print(geonames.get_serialized_data())
@@ -117,7 +117,7 @@ class RelationshipScoreFactory:
117
117
  self.first_subject,
118
118
  self.second_subject,
119
119
  axis_orb_limit=axis_orb_limit,
120
- ).all_aspects
120
+ ).aspects
121
121
 
122
122
  def _evaluate_destiny_sign(self):
123
123
  """
kerykeion/report.py CHANGED
@@ -510,15 +510,14 @@ class ReportGenerator:
510
510
  if not self._chart_data or not getattr(self._chart_data, "aspects", None):
511
511
  return ""
512
512
 
513
- aspects_model = self._chart_data.aspects
514
- relevant_aspects = list(getattr(aspects_model, "relevant_aspects", []))
513
+ aspects_list = list(self._chart_data.aspects)
515
514
 
516
- if not relevant_aspects:
515
+ if not aspects_list:
517
516
  return "No aspects data available."
518
517
 
519
- total_aspects = len(relevant_aspects)
518
+ total_aspects = len(aspects_list)
520
519
  if max_aspects is not None:
521
- relevant_aspects = relevant_aspects[:max_aspects]
520
+ aspects_list = aspects_list[:max_aspects]
522
521
 
523
522
  is_dual = isinstance(self._chart_data, DualChartDataModel)
524
523
  if is_dual:
@@ -527,7 +526,7 @@ class ReportGenerator:
527
526
  table_header = ["Point 1", "Aspect", "Point 2", "Orb", "Movement"]
528
527
 
529
528
  aspects_table: List[List[str]] = [table_header]
530
- for aspect in relevant_aspects:
529
+ for aspect in aspects_list:
531
530
  aspect_name = str(aspect.aspect)
532
531
  symbol = ASPECT_SYMBOLS.get(aspect_name.lower(), aspect_name)
533
532
  movement_symbol = MOVEMENT_SYMBOLS.get(aspect.aspect_movement, "")
@@ -552,7 +551,7 @@ class ReportGenerator:
552
551
  movement,
553
552
  ])
554
553
 
555
- suffix = f" (showing {len(relevant_aspects)} of {total_aspects})" if max_aspects is not None else ""
554
+ suffix = f" (showing {len(aspects_list)} of {total_aspects})" if max_aspects is not None else ""
556
555
  title = f"Aspects{suffix}"
557
556
  return AsciiTable(aspects_table, title=title).table
558
557
 
@@ -728,8 +727,7 @@ if __name__ == "__main__":
728
727
  natal_subject.iso_formatted_local_datetime,
729
728
  "Solar",
730
729
  )
731
-
732
- # Composite chart subject
730
+ # Derive a composite subject representing the pair's midpoint configuration
733
731
  composite_subject = CompositeSubjectFactory(
734
732
  natal_subject,
735
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
 
@@ -353,12 +353,11 @@ class SingleChartAspectsModel(SubscriptableBaseModel):
353
353
  - Composite charts
354
354
  - Any other single chart type
355
355
 
356
- Contains both all calculated aspects and the filtered relevant aspects
357
- for the astrological subject.
356
+ Contains the filtered and relevant aspects for the astrological subject
357
+ based on configured orb settings.
358
358
  """
359
359
  subject: Union["AstrologicalSubjectModel", "CompositeSubjectModel", "PlanetReturnModel"] = Field(description="The astrological subject for which aspects were calculated.")
360
- all_aspects: List[AspectModel] = Field(description="Complete list of all calculated aspects within the chart.")
361
- relevant_aspects: List[AspectModel] = Field(description="Filtered list of relevant aspects based on orb settings.")
360
+ aspects: List[AspectModel] = Field(description="List of calculated aspects within the chart, filtered based on orb settings.")
362
361
  active_points: List[AstrologicalPoint] = Field(description="List of active points used in the calculation.")
363
362
  active_aspects: List["ActiveAspect"] = Field(description="List of active aspects with their orb settings.")
364
363
 
@@ -373,13 +372,12 @@ class DualChartAspectsModel(SubscriptableBaseModel):
373
372
  - Composite vs natal comparisons
374
373
  - Any other dual chart comparison
375
374
 
376
- Contains both all calculated aspects and the filtered relevant aspects
377
- between the two charts.
375
+ Contains the filtered and relevant aspects between the two charts
376
+ based on configured orb settings.
378
377
  """
379
378
  first_subject: Union["AstrologicalSubjectModel", "CompositeSubjectModel", "PlanetReturnModel"] = Field(description="The first astrological subject.")
380
379
  second_subject: Union["AstrologicalSubjectModel", "CompositeSubjectModel", "PlanetReturnModel"] = Field(description="The second astrological subject.")
381
- all_aspects: List[AspectModel] = Field(description="Complete list of all calculated aspects between the two charts.")
382
- relevant_aspects: List[AspectModel] = Field(description="Filtered list of relevant aspects based on orb settings.")
380
+ aspects: List[AspectModel] = Field(description="List of calculated aspects between the two charts, filtered based on orb settings.")
383
381
  active_points: List[AstrologicalPoint] = Field(description="List of active points used in the calculation.")
384
382
  active_aspects: List["ActiveAspect"] = Field(description="List of active aspects with their orb settings.")
385
383
 
@@ -538,7 +536,7 @@ class SingleChartDataModel(SubscriptableBaseModel):
538
536
  subject: Union["AstrologicalSubjectModel", "CompositeSubjectModel", "PlanetReturnModel"]
539
537
 
540
538
  # Internal aspects analysis
541
- aspects: "SingleChartAspectsModel"
539
+ aspects: List[AspectModel]
542
540
 
543
541
  # Element and quality distributions
544
542
  element_distribution: "ElementDistributionModel"
@@ -584,7 +582,7 @@ class DualChartDataModel(SubscriptableBaseModel):
584
582
  second_subject: Union["AstrologicalSubjectModel", "PlanetReturnModel"]
585
583
 
586
584
  # Inter-chart aspects analysis
587
- aspects: "DualChartAspectsModel"
585
+ aspects: List[AspectModel]
588
586
 
589
587
  # House comparison analysis
590
588
  house_comparison: Optional["HouseComparisonModel"] = None
@@ -170,6 +170,14 @@ class KerykeionLanguageModel(SubscriptableBaseModel):
170
170
  fixed: str = Field(title="Fixed", description="The fixed quality label in the chart, in the language")
171
171
  mutable: str = Field(title="Mutable", description="The mutable quality label in the chart, in the language")
172
172
  birth_chart: str = Field(title="Birth Chart", description="The birth chart label in the chart, in the language")
173
+ chart_info_natal_label: str = Field(
174
+ title="Chart Info Natal Label",
175
+ description="Short label for natal information panels",
176
+ )
177
+ chart_info_transit_label: str = Field(
178
+ title="Chart Info Transit Label",
179
+ description="Short label for transit information panels",
180
+ )
173
181
  weekdays: Optional[dict[str, str]] = Field(default=None, title="Weekdays", description="Mapping of English weekday names to localized names")
174
182
 
175
183
  # Settings Model
@@ -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"]
@@ -108,6 +108,8 @@ LANGUAGE_SETTINGS = {
108
108
  "fixed": "Fixed",
109
109
  "mutable": "Mutable",
110
110
  "birth_chart": "Birth Chart",
111
+ "chart_info_natal_label": "Natal",
112
+ "chart_info_transit_label": "Transit",
111
113
  "celestial_points": {
112
114
  "Sun": "Sun",
113
115
  "Moon": "Moon",
@@ -255,6 +257,8 @@ LANGUAGE_SETTINGS = {
255
257
  "fixed": "Fixe",
256
258
  "mutable": "Mutable",
257
259
  "birth_chart": "Thème Natal",
260
+ "chart_info_natal_label": "Natal",
261
+ "chart_info_transit_label": "Transit",
258
262
  "celestial_points": {
259
263
  "Sun": "Soleil",
260
264
  "Moon": "Lune",
@@ -402,6 +406,8 @@ LANGUAGE_SETTINGS = {
402
406
  "fixed": "Fixo",
403
407
  "mutable": "Mutável",
404
408
  "birth_chart": "Mapa Natal",
409
+ "chart_info_natal_label": "Natal",
410
+ "chart_info_transit_label": "Trânsito",
405
411
  "celestial_points": {
406
412
  "Sun": "Sol",
407
413
  "Moon": "Lua",
@@ -549,6 +555,8 @@ LANGUAGE_SETTINGS = {
549
555
  "fixed": "Fisso",
550
556
  "mutable": "Mutevole",
551
557
  "birth_chart": "Tema Natale",
558
+ "chart_info_natal_label": "Natale",
559
+ "chart_info_transit_label": "Transito",
552
560
  "celestial_points": {
553
561
  "Sun": "Sole",
554
562
  "Moon": "Luna",
@@ -696,6 +704,8 @@ LANGUAGE_SETTINGS = {
696
704
  "fixed": "固定",
697
705
  "mutable": "变动",
698
706
  "birth_chart": "出生图",
707
+ "chart_info_natal_label": "本命盤",
708
+ "chart_info_transit_label": "運行",
699
709
  "celestial_points": {
700
710
  "Sun": "太陽",
701
711
  "Moon": "月亮",
@@ -843,6 +853,8 @@ LANGUAGE_SETTINGS = {
843
853
  "fixed": "Fijo",
844
854
  "mutable": "Mutable",
845
855
  "birth_chart": "Carta Natal",
856
+ "chart_info_natal_label": "Natal",
857
+ "chart_info_transit_label": "Tránsito",
846
858
  "celestial_points": {
847
859
  "Sun": "Sol",
848
860
  "Moon": "Luna",
@@ -990,6 +1002,8 @@ LANGUAGE_SETTINGS = {
990
1002
  "fixed": "Фиксированный",
991
1003
  "mutable": "Мутабельный",
992
1004
  "birth_chart": "Натальная Карта",
1005
+ "chart_info_natal_label": "Натальная",
1006
+ "chart_info_transit_label": "Транзит",
993
1007
  "celestial_points": {
994
1008
  "Sun": "Солнце",
995
1009
  "Moon": "Луна",
@@ -1137,6 +1151,8 @@ LANGUAGE_SETTINGS = {
1137
1151
  "fixed": "Sabit",
1138
1152
  "mutable": "Değişken",
1139
1153
  "birth_chart": "Doğum Haritası",
1154
+ "chart_info_natal_label": "Doğum",
1155
+ "chart_info_transit_label": "Geçiş",
1140
1156
  "celestial_points": {
1141
1157
  "Sun": "Güneş",
1142
1158
  "Moon": "Ay",
@@ -1284,6 +1300,8 @@ LANGUAGE_SETTINGS = {
1284
1300
  "fixed": "Fix",
1285
1301
  "mutable": "Veränderlich",
1286
1302
  "birth_chart": "Geburtshoroskop",
1303
+ "chart_info_natal_label": "Radix",
1304
+ "chart_info_transit_label": "Transit",
1287
1305
  "celestial_points": {
1288
1306
  "Sun": "Sonne",
1289
1307
  "Moon": "Mond",
@@ -1431,6 +1449,8 @@ LANGUAGE_SETTINGS = {
1431
1449
  "fixed": "स्थिर",
1432
1450
  "mutable": "द्विस्वभाव",
1433
1451
  "birth_chart": "जन्म चार्ट",
1452
+ "chart_info_natal_label": "जन्म",
1453
+ "chart_info_transit_label": "गोचर",
1434
1454
  "celestial_points": {
1435
1455
  "Sun": "सूर्य",
1436
1456
  "Moon": "चंद्रमा",
@@ -247,7 +247,7 @@ class TransitsTimeRangeFactory:
247
247
  active_points=self.active_points,
248
248
  active_aspects=self.active_aspects,
249
249
  axis_orb_limit=self.axis_orb_limit,
250
- ).relevant_aspects
250
+ ).aspects
251
251
 
252
252
  # Create a transit moment for this point in time
253
253
  transit_moments.append(