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.

@@ -98,7 +98,7 @@ class AspectsFactory:
98
98
  Example:
99
99
  >>> johnny = AstrologicalSubjectFactory.from_birth_data("Johnny", 1963, 6, 9, 0, 0, "Owensboro", "US")
100
100
  >>> chart_aspects = AspectsFactory.single_chart_aspects(johnny)
101
- >>> print(f"Found {len(chart_aspects.relevant_aspects)} relevant aspects")
101
+ >>> print(f"Found {len(chart_aspects.aspects)} aspects")
102
102
  """
103
103
  # Initialize settings and configurations
104
104
  celestial_points = DEFAULT_CELESTIAL_POINTS_SETTINGS
@@ -162,7 +162,7 @@ class AspectsFactory:
162
162
  >>> john = AstrologicalSubjectFactory.from_birth_data("John", 1990, 1, 1, 12, 0, "London", "GB")
163
163
  >>> jane = AstrologicalSubjectFactory.from_birth_data("Jane", 1992, 6, 15, 14, 30, "Paris", "FR")
164
164
  >>> synastry = AspectsFactory.dual_chart_aspects(john, jane)
165
- >>> print(f"Found {len(synastry.relevant_aspects)} relevant aspects")
165
+ >>> print(f"Found {len(synastry.aspects)} aspects")
166
166
  """
167
167
  # Initialize settings and configurations
168
168
  celestial_points = DEFAULT_CELESTIAL_POINTS_SETTINGS
@@ -208,12 +208,12 @@ class AspectsFactory:
208
208
  Create the complete single chart aspects model with all calculations.
209
209
 
210
210
  Returns:
211
- SingleChartAspectsModel containing all aspects data
211
+ SingleChartAspectsModel containing filtered aspects data
212
212
  """
213
213
  all_aspects = AspectsFactory._calculate_single_chart_aspects(
214
214
  subject, active_points_resolved, active_aspects_resolved, aspects_settings, celestial_points
215
215
  )
216
- relevant_aspects = AspectsFactory._filter_relevant_aspects(
216
+ filtered_aspects = AspectsFactory._filter_relevant_aspects(
217
217
  all_aspects,
218
218
  axis_orb_limit,
219
219
  apply_axis_orb_filter=axis_orb_limit is not None,
@@ -221,8 +221,7 @@ class AspectsFactory:
221
221
 
222
222
  return SingleChartAspectsModel(
223
223
  subject=subject,
224
- all_aspects=all_aspects,
225
- relevant_aspects=relevant_aspects,
224
+ aspects=filtered_aspects,
226
225
  active_points=active_points_resolved,
227
226
  active_aspects=active_aspects_resolved,
228
227
  )
@@ -250,13 +249,13 @@ class AspectsFactory:
250
249
  celestial_points: Celestial points configuration
251
250
 
252
251
  Returns:
253
- DualChartAspectsModel: Complete model containing all aspects data
252
+ DualChartAspectsModel: Complete model containing filtered aspects data
254
253
  """
255
254
  all_aspects = AspectsFactory._calculate_dual_chart_aspects(
256
255
  first_subject, second_subject, active_points_resolved, active_aspects_resolved,
257
256
  aspects_settings, celestial_points
258
257
  )
259
- relevant_aspects = AspectsFactory._filter_relevant_aspects(
258
+ filtered_aspects = AspectsFactory._filter_relevant_aspects(
260
259
  all_aspects,
261
260
  axis_orb_limit,
262
261
  apply_axis_orb_filter=False,
@@ -265,8 +264,7 @@ class AspectsFactory:
265
264
  return DualChartAspectsModel(
266
265
  first_subject=first_subject,
267
266
  second_subject=second_subject,
268
- all_aspects=all_aspects,
269
- relevant_aspects=relevant_aspects,
267
+ aspects=filtered_aspects,
270
268
  active_points=active_points_resolved,
271
269
  active_aspects=active_aspects_resolved,
272
270
  )
@@ -560,10 +558,11 @@ if __name__ == "__main__":
560
558
  # Test single chart aspects (replaces natal aspects)
561
559
  johnny = AstrologicalSubjectFactory.from_birth_data("Johnny Depp", 1963, 6, 9, 0, 0, city="Owensboro", nation="US")
562
560
  single_chart_aspects = AspectsFactory.single_chart_aspects(johnny)
563
- print(f"Single chart aspects - All: {len(single_chart_aspects.all_aspects)}, Relevant: {len(single_chart_aspects.relevant_aspects)}")
561
+ print(f"Single chart aspects: {len(single_chart_aspects.aspects)}")
564
562
 
565
563
  # Test dual chart aspects (replaces synastry aspects)
566
564
  john = AstrologicalSubjectFactory.from_birth_data("John", 1940, 10, 9, 10, 30, "Liverpool", "GB")
567
565
  yoko = AstrologicalSubjectFactory.from_birth_data("Yoko", 1933, 2, 18, 10, 30, "Tokyo", "JP")
568
566
  dual_chart_aspects = AspectsFactory.dual_chart_aspects(john, yoko)
569
- print(f"Dual chart aspects - All: {len(dual_chart_aspects.all_aspects)}, Relevant: {len(dual_chart_aspects.relevant_aspects)}")
567
+ print(f"Dual chart aspects: {len(dual_chart_aspects.aspects)}")
568
+
@@ -49,8 +49,8 @@ def get_aspect_from_two_points(
49
49
  return {
50
50
  "verdict": verdict,
51
51
  "name": name,
52
- "orbit": distance - aspect_degrees,
53
- "distance": distance - aspect_degrees,
52
+ "orbit": abs(distance - aspect_degrees),
53
+ "distance": abs(distance - aspect_degrees),
54
54
  "aspect_degrees": aspect_degrees,
55
55
  "diff": diff,
56
56
  }
@@ -59,7 +59,8 @@ from kerykeion.utilities import (
59
59
  check_and_adjust_polar_latitude,
60
60
  calculate_moon_phase,
61
61
  datetime_to_julian,
62
- get_house_number
62
+ get_house_number,
63
+ normalize_zodiac_type,
63
64
  )
64
65
  from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS
65
66
 
@@ -67,7 +68,7 @@ from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS
67
68
  DEFAULT_GEONAMES_USERNAME = "century.boy"
68
69
  DEFAULT_SIDEREAL_MODE: SiderealMode = "FAGAN_BRADLEY"
69
70
  DEFAULT_HOUSES_SYSTEM_IDENTIFIER: HousesSystemIdentifier = "P"
70
- DEFAULT_ZODIAC_TYPE: ZodiacType = "Tropic"
71
+ DEFAULT_ZODIAC_TYPE: ZodiacType = "Tropical"
71
72
  DEFAULT_PERSPECTIVE_TYPE: PerspectiveType = "Apparent Geocentric"
72
73
  DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS = 30
73
74
 
@@ -148,8 +149,8 @@ class ChartConfiguration:
148
149
  combinations.
149
150
 
150
151
  Attributes:
151
- zodiac_type (ZodiacType): The zodiac system to use ('Tropic' or 'Sidereal').
152
- Defaults to 'Tropic'.
152
+ zodiac_type (ZodiacType): The zodiac system to use ('Tropical' or 'Sidereal').
153
+ Defaults to 'Tropical'.
153
154
  sidereal_mode (Optional[SiderealMode]): The sidereal calculation mode when using
154
155
  sidereal zodiac. Only required/used when zodiac_type is 'Sidereal'.
155
156
  Defaults to None (auto-set to FAGAN_BRADLEY for sidereal).
@@ -198,14 +199,17 @@ class ChartConfiguration:
198
199
  - Logs informational message when setting default sidereal mode
199
200
  """
200
201
  # Validate zodiac type
201
- if self.zodiac_type not in get_args(ZodiacType):
202
- raise KerykeionException(
203
- f"'{self.zodiac_type}' is not a valid zodiac type! Available types are: {get_args(ZodiacType)}"
204
- )
202
+ try:
203
+ normalized_zodiac_type = normalize_zodiac_type(self.zodiac_type)
204
+ except ValueError as exc:
205
+ raise KerykeionException(str(exc)) from exc
206
+ else:
207
+ if normalized_zodiac_type != self.zodiac_type:
208
+ self.zodiac_type = normalized_zodiac_type
205
209
 
206
210
  # Validate sidereal mode settings
207
- if self.sidereal_mode and self.zodiac_type == "Tropic":
208
- raise KerykeionException("You can't set a sidereal mode with a Tropic zodiac type!")
211
+ if self.sidereal_mode and self.zodiac_type == "Tropical":
212
+ raise KerykeionException("You can't set a sidereal mode with a Tropical zodiac type!")
209
213
 
210
214
  if self.zodiac_type == "Sidereal":
211
215
  if not self.sidereal_mode:
@@ -475,8 +479,8 @@ class AstrologicalSubjectFactory:
475
479
  online location lookup. Get one free at geonames.org.
476
480
  online (bool, optional): Whether to fetch location data online. If False,
477
481
  lng, lat, and tz_str must be provided. Defaults to True.
478
- zodiac_type (ZodiacType, optional): Zodiac system - 'Tropic' or 'Sidereal'.
479
- Defaults to 'Tropic'.
482
+ zodiac_type (ZodiacType, optional): Zodiac system - 'Tropical' or 'Sidereal'.
483
+ Defaults to 'Tropical'.
480
484
  sidereal_mode (SiderealMode, optional): Sidereal calculation mode (e.g.,
481
485
  'FAGAN_BRADLEY', 'LAHIRI'). Only used with zodiac_type='Sidereal'.
482
486
  houses_system_identifier (HousesSystemIdentifier, optional): House system
@@ -726,7 +730,7 @@ class AstrologicalSubjectFactory:
726
730
  or as fallback. Defaults to 51.5074 (Greenwich).
727
731
  geonames_username (str, optional): GeoNames API username. Required when
728
732
  online=True. Defaults to DEFAULT_GEONAMES_USERNAME.
729
- zodiac_type (ZodiacType, optional): Zodiac system. Defaults to 'Tropic'.
733
+ zodiac_type (ZodiacType, optional): Zodiac system. Defaults to 'Tropical'.
730
734
  sidereal_mode (SiderealMode, optional): Sidereal mode when zodiac_type
731
735
  is 'Sidereal'. Defaults to None.
732
736
  houses_system_identifier (HousesSystemIdentifier, optional): House system.
@@ -866,7 +870,7 @@ class AstrologicalSubjectFactory:
866
870
  online (bool, optional): Whether to fetch location data online.
867
871
  Defaults to True.
868
872
  zodiac_type (ZodiacType, optional): Zodiac system to use.
869
- Defaults to 'Tropic'.
873
+ Defaults to 'Tropical'.
870
874
  sidereal_mode (SiderealMode, optional): Sidereal calculation mode.
871
875
  Only used when zodiac_type is 'Sidereal'. Defaults to None.
872
876
  houses_system_identifier (HousesSystemIdentifier, optional): House
kerykeion/backword.py CHANGED
@@ -36,6 +36,7 @@ from .chart_data_factory import ChartDataFactory
36
36
  from .charts.chart_drawer import ChartDrawer
37
37
  from .aspects import AspectsFactory
38
38
  from .settings.config_constants import DEFAULT_ACTIVE_POINTS, DEFAULT_ACTIVE_ASPECTS
39
+ from .utilities import normalize_zodiac_type
39
40
  from .schemas.kr_models import (
40
41
  AstrologicalSubjectModel,
41
42
  CompositeSubjectModel,
@@ -74,6 +75,42 @@ LEGACY_NODE_NAMES_MAP = {
74
75
  }
75
76
 
76
77
 
78
+ def _normalize_zodiac_type_with_warning(zodiac_type: Optional[Union[str, ZodiacType]]) -> Optional[ZodiacType]:
79
+ """Normalize legacy zodiac type values with deprecation warning.
80
+
81
+ Wraps the utilities.normalize_zodiac_type function and adds a deprecation
82
+ warning for legacy formats like "tropic" or case-insensitive variants.
83
+
84
+ Args:
85
+ zodiac_type: Input zodiac type (may be legacy format)
86
+
87
+ Returns:
88
+ Normalized ZodiacType or None if input was None
89
+ """
90
+ if zodiac_type is None:
91
+ return None
92
+
93
+ zodiac_str = str(zodiac_type)
94
+
95
+ # Check if this is a legacy format (case-insensitive "tropic" or non-canonical case)
96
+ zodiac_lower = zodiac_str.lower()
97
+ if zodiac_lower in ("tropic", "tropical", "sidereal") and zodiac_str not in ("Tropical", "Sidereal"):
98
+ # Normalize using the utilities function
99
+ normalized = normalize_zodiac_type(zodiac_str)
100
+
101
+ # Emit deprecation warning for legacy usage
102
+ warnings.warn(
103
+ f"Zodiac type '{zodiac_str}' is deprecated in Kerykeion v5. "
104
+ f"Use '{normalized}' instead.",
105
+ DeprecationWarning,
106
+ stacklevel=4,
107
+ )
108
+ return normalized
109
+
110
+ # Already in correct format or will be normalized by utilities function
111
+ return cast(ZodiacType, normalize_zodiac_type(zodiac_str))
112
+
113
+
77
114
  def _normalize_active_points(points: Optional[Iterable[Union[str, AstrologicalPoint]]]) -> Optional[List[AstrologicalPoint]]:
78
115
  """Best-effort normalization of legacy string active points list.
79
116
 
@@ -159,7 +196,10 @@ class AstrologicalSubject:
159
196
  stacklevel=2,
160
197
  )
161
198
 
199
+ # Normalize legacy zodiac type values
200
+ zodiac_type = _normalize_zodiac_type_with_warning(zodiac_type)
162
201
  zodiac_type = DEFAULT_ZODIAC_TYPE if zodiac_type is None else zodiac_type
202
+
163
203
  houses_system_identifier = (
164
204
  DEFAULT_HOUSES_SYSTEM_IDENTIFIER if houses_system_identifier is None else houses_system_identifier
165
205
  )
@@ -352,6 +392,9 @@ class AstrologicalSubject:
352
392
  if online and resolved_geonames == DEFAULT_GEONAMES_USERNAME:
353
393
  warnings.warn(GEONAMES_DEFAULT_USERNAME_WARNING, UserWarning, stacklevel=2)
354
394
 
395
+ # Normalize legacy zodiac type values
396
+ normalized_zodiac_type = _normalize_zodiac_type_with_warning(zodiac_type)
397
+
355
398
  model = AstrologicalSubjectFactory.from_iso_utc_time(
356
399
  name=name,
357
400
  iso_utc_time=iso_utc_time,
@@ -362,7 +405,7 @@ class AstrologicalSubject:
362
405
  lng=float(lng),
363
406
  lat=float(lat),
364
407
  geonames_username=resolved_geonames,
365
- zodiac_type=(zodiac_type or DEFAULT_ZODIAC_TYPE), # type: ignore[arg-type]
408
+ zodiac_type=(normalized_zodiac_type or DEFAULT_ZODIAC_TYPE), # type: ignore[arg-type]
366
409
  sidereal_mode=sidereal_mode,
367
410
  houses_system_identifier=(houses_system_identifier or DEFAULT_HOUSES_SYSTEM_IDENTIFIER), # type: ignore[arg-type]
368
411
  perspective_type=(perspective_type or DEFAULT_PERSPECTIVE_TYPE), # type: ignore[arg-type]
@@ -676,14 +719,16 @@ class NatalAspects:
676
719
 
677
720
  @cached_property
678
721
  def all_aspects(self):
722
+ """Legacy property - returns the same as aspects for backwards compatibility."""
679
723
  if self._all_aspects_cache is None:
680
- self._all_aspects_cache = list(self._build_aspects_model().all_aspects)
724
+ self._all_aspects_cache = list(self._build_aspects_model().aspects)
681
725
  return self._all_aspects_cache
682
726
 
683
727
  @cached_property
684
728
  def relevant_aspects(self):
729
+ """Legacy property - returns the same as aspects for backwards compatibility."""
685
730
  if self._relevant_aspects_cache is None:
686
- self._relevant_aspects_cache = list(self._build_aspects_model().relevant_aspects)
731
+ self._relevant_aspects_cache = list(self._build_aspects_model().aspects)
687
732
  return self._relevant_aspects_cache
688
733
 
689
734
  # ---------------------------------------------------------------------------
@@ -748,14 +793,16 @@ class SynastryAspects:
748
793
 
749
794
  @cached_property
750
795
  def all_aspects(self):
796
+ """Legacy property - returns the same as aspects for backwards compatibility."""
751
797
  if self._all_aspects_cache is None:
752
- self._all_aspects_cache = list(self._build_dual_model().all_aspects)
798
+ self._all_aspects_cache = list(self._build_dual_model().aspects)
753
799
  return self._all_aspects_cache
754
800
 
755
801
  @cached_property
756
802
  def relevant_aspects(self):
803
+ """Legacy property - returns the same as aspects for backwards compatibility."""
757
804
  if self._relevant_aspects_cache is None:
758
- self._relevant_aspects_cache = list(self._build_dual_model().relevant_aspects)
805
+ self._relevant_aspects_cache = list(self._build_dual_model().aspects)
759
806
  return self._relevant_aspects_cache
760
807
 
761
808
  def get_relevant_aspects(self):
@@ -159,10 +159,10 @@ class ChartDataFactory:
159
159
  )
160
160
 
161
161
  # Calculate aspects based on chart type
162
- aspects: Union[SingleChartAspectsModel, DualChartAspectsModel]
162
+ aspects_model: Union[SingleChartAspectsModel, DualChartAspectsModel]
163
163
  if chart_type in ["Natal", "Composite", "SingleReturnChart"]:
164
164
  # Single chart aspects
165
- aspects = AspectsFactory.single_chart_aspects(
165
+ aspects_model = AspectsFactory.single_chart_aspects(
166
166
  first_subject,
167
167
  active_points=effective_active_points,
168
168
  active_aspects=active_aspects,
@@ -172,7 +172,7 @@ class ChartDataFactory:
172
172
  # Dual chart aspects - second_subject is guaranteed to exist here due to validation above
173
173
  if second_subject is None:
174
174
  raise KerykeionException(f"Second subject is required for {chart_type} charts.")
175
- aspects = AspectsFactory.dual_chart_aspects(
175
+ aspects_model = AspectsFactory.dual_chart_aspects(
176
176
  first_subject,
177
177
  second_subject,
178
178
  active_points=effective_active_points,
@@ -301,7 +301,7 @@ class ChartDataFactory:
301
301
  return SingleChartDataModel(
302
302
  chart_type=cast(Literal["Natal", "Composite", "SingleReturnChart"], chart_type),
303
303
  subject=first_subject,
304
- aspects=cast(SingleChartAspectsModel, aspects),
304
+ aspects=cast(SingleChartAspectsModel, aspects_model).aspects,
305
305
  element_distribution=element_distribution,
306
306
  quality_distribution=quality_distribution,
307
307
  active_points=effective_active_points,
@@ -315,7 +315,7 @@ class ChartDataFactory:
315
315
  chart_type=cast(Literal["Transit", "Synastry", "DualReturnChart"], chart_type),
316
316
  first_subject=first_subject,
317
317
  second_subject=second_subject,
318
- aspects=cast(DualChartAspectsModel, aspects),
318
+ aspects=cast(DualChartAspectsModel, aspects_model).aspects,
319
319
  house_comparison=house_comparison,
320
320
  relationship_score=relationship_score,
321
321
  element_distribution=element_distribution,
@@ -542,8 +542,11 @@ if __name__ == "__main__":
542
542
 
543
543
  print(f"Chart Type: {natal_data.chart_type}")
544
544
  print(f"Active Points: {len(natal_data.active_points)}")
545
- print(f"Aspects: {len(natal_data.aspects.relevant_aspects)}")
545
+ print(f"Aspects: {len(natal_data.aspects)}")
546
546
  print(f"Fire: {natal_data.element_distribution.fire_percentage}%")
547
547
  print(f"Earth: {natal_data.element_distribution.earth_percentage}%")
548
548
  print(f"Air: {natal_data.element_distribution.air_percentage}%")
549
549
  print(f"Water: {natal_data.element_distribution.water_percentage}%")
550
+
551
+ print("\n---\n")
552
+ print(natal_data.model_dump_json(indent=4))
@@ -78,6 +78,9 @@ from kerykeion.settings.chart_defaults import (
78
78
  from typing import List, Literal
79
79
 
80
80
 
81
+ logger = logging.getLogger(__name__)
82
+
83
+
81
84
  class ChartDrawer:
82
85
  """
83
86
  ChartDrawer generates astrological chart visualizations as SVG files from pre-computed chart data.
@@ -315,7 +318,7 @@ class ChartDrawer:
315
318
 
316
319
  active_points_count = len(self.available_planets_setting)
317
320
  if active_points_count > 24:
318
- logging.warning(
321
+ logger.warning(
319
322
  "ChartDrawer detected %s active celestial points; rendering may look crowded beyond 24.",
320
323
  active_points_count,
321
324
  )
@@ -343,7 +346,7 @@ class ChartDrawer:
343
346
  # --- NATAL CHART SETUP ---
344
347
 
345
348
  # Extract aspects from pre-computed chart data
346
- self.aspects_list = chart_data.aspects.relevant_aspects
349
+ self.aspects_list = chart_data.aspects
347
350
 
348
351
  # Screen size
349
352
  self.height = self._DEFAULT_HEIGHT
@@ -366,7 +369,7 @@ class ChartDrawer:
366
369
  # --- COMPOSITE CHART SETUP ---
367
370
 
368
371
  # Extract aspects from pre-computed chart data
369
- self.aspects_list = chart_data.aspects.relevant_aspects
372
+ self.aspects_list = chart_data.aspects
370
373
 
371
374
  # Screen size
372
375
  self.height = self._DEFAULT_HEIGHT
@@ -384,7 +387,7 @@ class ChartDrawer:
384
387
  # --- TRANSIT CHART SETUP ---
385
388
 
386
389
  # Extract aspects from pre-computed chart data
387
- self.aspects_list = chart_data.aspects.relevant_aspects
390
+ self.aspects_list = chart_data.aspects
388
391
 
389
392
  # Screen size
390
393
  self.height = self._DEFAULT_HEIGHT
@@ -405,7 +408,7 @@ class ChartDrawer:
405
408
  # --- SYNASTRY CHART SETUP ---
406
409
 
407
410
  # Extract aspects from pre-computed chart data
408
- self.aspects_list = chart_data.aspects.relevant_aspects
411
+ self.aspects_list = chart_data.aspects
409
412
 
410
413
  # Screen size
411
414
  self.height = self._DEFAULT_HEIGHT
@@ -423,7 +426,7 @@ class ChartDrawer:
423
426
  # --- RETURN CHART SETUP ---
424
427
 
425
428
  # Extract aspects from pre-computed chart data
426
- self.aspects_list = chart_data.aspects.relevant_aspects
429
+ self.aspects_list = chart_data.aspects
427
430
 
428
431
  # Screen size
429
432
  self.height = self._DEFAULT_HEIGHT
@@ -441,7 +444,7 @@ class ChartDrawer:
441
444
  # --- SINGLE WHEEL RETURN CHART SETUP ---
442
445
 
443
446
  # Extract aspects from pre-computed chart data
444
- self.aspects_list = chart_data.aspects.relevant_aspects
447
+ self.aspects_list = chart_data.aspects
445
448
 
446
449
  # Screen size
447
450
  self.height = self._DEFAULT_HEIGHT
@@ -856,7 +859,7 @@ class ChartDrawer:
856
859
  try:
857
860
  required_width = self._estimate_required_width_full()
858
861
  except Exception as e:
859
- logging.debug(f"Auto-size width calculation failed: {e}")
862
+ logger.debug("Auto-size width calculation failed: %s", e)
860
863
  return
861
864
 
862
865
  minimum_width = self._minimum_width_for_chart_type()
@@ -1071,24 +1074,28 @@ class ChartDrawer:
1071
1074
  return f"{synastry_label}: {name1} {and_word} {name2}"
1072
1075
 
1073
1076
  elif self.chart_type == "DualReturnChart":
1074
- year = datetime.fromisoformat(self.second_obj.iso_formatted_local_datetime).year # type: ignore
1077
+ return_datetime = datetime.fromisoformat(self.second_obj.iso_formatted_local_datetime) # type: ignore
1078
+ year = return_datetime.year
1079
+ month_year = return_datetime.strftime("%m/%Y")
1075
1080
  truncated_name = self._truncate_name(self.first_obj.name)
1076
1081
  if self.second_obj is not None and isinstance(self.second_obj, PlanetReturnModel) and self.second_obj.return_type == "Solar":
1077
1082
  solar_label = self._translate("solar_return", "Solar")
1078
1083
  return f"{truncated_name} - {solar_label} {year}"
1079
1084
  else:
1080
1085
  lunar_label = self._translate("lunar_return", "Lunar")
1081
- return f"{truncated_name} - {lunar_label} {year}"
1086
+ return f"{truncated_name} - {lunar_label} {month_year}"
1082
1087
 
1083
1088
  elif self.chart_type == "SingleReturnChart":
1084
- year = datetime.fromisoformat(self.first_obj.iso_formatted_local_datetime).year # type: ignore
1089
+ return_datetime = datetime.fromisoformat(self.first_obj.iso_formatted_local_datetime) # type: ignore
1090
+ year = return_datetime.year
1091
+ month_year = return_datetime.strftime("%m/%Y")
1085
1092
  truncated_name = self._truncate_name(self.first_obj.name)
1086
1093
  if isinstance(self.first_obj, PlanetReturnModel) and self.first_obj.return_type == "Solar":
1087
1094
  solar_label = self._translate("solar_return", "Solar")
1088
1095
  return f"{truncated_name} - {solar_label} {year}"
1089
1096
  else:
1090
1097
  lunar_label = self._translate("lunar_return", "Lunar")
1091
- return f"{truncated_name} - {lunar_label} {year}"
1098
+ return f"{truncated_name} - {lunar_label} {month_year}"
1092
1099
 
1093
1100
  # Fallback for unknown chart types
1094
1101
  return self._truncate_name(self.first_obj.name)
@@ -1267,7 +1274,7 @@ class ChartDrawer:
1267
1274
  template_dict["top_left_5"] = f"{self._translate('day_of_week', 'Day of Week')}: {localized_weekday}" # type: ignore
1268
1275
 
1269
1276
  # Bottom left section
1270
- if self.first_obj.zodiac_type == "Tropic":
1277
+ if self.first_obj.zodiac_type == "Tropical":
1271
1278
  zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
1272
1279
  else:
1273
1280
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
@@ -1429,7 +1436,7 @@ class ChartDrawer:
1429
1436
  template_dict["top_left_5"] = f"{latitude_string} / {longitude_string}"
1430
1437
 
1431
1438
  # Bottom left section
1432
- if self.first_obj.zodiac_type == "Tropic":
1439
+ if self.first_obj.zodiac_type == "Tropical":
1433
1440
  zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
1434
1441
  else:
1435
1442
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
@@ -1573,19 +1580,64 @@ class ChartDrawer:
1573
1580
 
1574
1581
  template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
1575
1582
 
1576
- # Top left section
1577
- latitude_string = convert_latitude_coordinate_to_string(self.geolat, self._translate("north", "North"), self._translate("south", "South"))
1578
- longitude_string = convert_longitude_coordinate_to_string(self.geolon, self._translate("east", "East"), self._translate("west", "West"))
1579
-
1580
- template_dict["top_left_0"] = f"{self.first_obj.name}"
1581
- template_dict["top_left_1"] = f"{format_location_string(self.first_obj.city)}, {self.first_obj.nation}" # type: ignore
1582
- template_dict["top_left_2"] = format_datetime_with_timezone(self.first_obj.iso_formatted_local_datetime) # type: ignore
1583
- template_dict["top_left_3"] = f"{self._translate('latitude', 'Latitude')}: {latitude_string}"
1584
- template_dict["top_left_4"] = f"{self._translate('longitude', 'Longitude')}: {longitude_string}"
1585
- template_dict["top_left_5"] = ""#f"{self._translate('type', 'Type')}: {self._translate(self.chart_type, self.chart_type)}"
1583
+ # Top left section (clear separation of Natal vs Transit details)
1584
+ natal_latitude_string = (
1585
+ convert_latitude_coordinate_to_string(
1586
+ self.first_obj.lat, # type: ignore[arg-type]
1587
+ self._translate("north_letter", "N"),
1588
+ self._translate("south_letter", "S"),
1589
+ )
1590
+ if getattr(self.first_obj, "lat", None) is not None
1591
+ else ""
1592
+ )
1593
+ natal_longitude_string = (
1594
+ convert_longitude_coordinate_to_string(
1595
+ self.first_obj.lng, # type: ignore[arg-type]
1596
+ self._translate("east_letter", "E"),
1597
+ self._translate("west_letter", "W"),
1598
+ )
1599
+ if getattr(self.first_obj, "lng", None) is not None
1600
+ else ""
1601
+ )
1602
+
1603
+ transit_latitude_string = ""
1604
+ transit_longitude_string = ""
1605
+ if self.second_obj is not None:
1606
+ if getattr(self.second_obj, "lat", None) is not None:
1607
+ transit_latitude_string = convert_latitude_coordinate_to_string(
1608
+ self.second_obj.lat, # type: ignore[arg-type]
1609
+ self._translate("north_letter", "N"),
1610
+ self._translate("south_letter", "S"),
1611
+ )
1612
+ if getattr(self.second_obj, "lng", None) is not None:
1613
+ transit_longitude_string = convert_longitude_coordinate_to_string(
1614
+ self.second_obj.lng, # type: ignore[arg-type]
1615
+ self._translate("east_letter", "E"),
1616
+ self._translate("west_letter", "W"),
1617
+ )
1618
+
1619
+ natal_dt = format_datetime_with_timezone(self.first_obj.iso_formatted_local_datetime) # type: ignore[arg-type]
1620
+ natal_place = f"{format_location_string(self.first_obj.city)}, {self.first_obj.nation}" # type: ignore[arg-type]
1621
+ transit_dt = (
1622
+ format_datetime_with_timezone(self.second_obj.iso_formatted_local_datetime) # type: ignore[arg-type]
1623
+ if self.second_obj is not None and getattr(self.second_obj, "iso_formatted_local_datetime", None) is not None
1624
+ else ""
1625
+ )
1626
+ transit_place = (
1627
+ f"{format_location_string(self.second_obj.city)}, {self.second_obj.nation}" # type: ignore[arg-type]
1628
+ if self.second_obj is not None
1629
+ else ""
1630
+ )
1631
+
1632
+ template_dict["top_left_0"] = f"{self._translate('chart_info_natal_label', 'Natal')}: {natal_dt}"
1633
+ template_dict["top_left_1"] = natal_place
1634
+ template_dict["top_left_2"] = f"{natal_latitude_string} · {natal_longitude_string}"
1635
+ template_dict["top_left_3"] = f"{self._translate('chart_info_transit_label', 'Transit')}: {transit_dt}"
1636
+ template_dict["top_left_4"] = transit_place
1637
+ template_dict["top_left_5"] = f"{transit_latitude_string} · {transit_longitude_string}"
1586
1638
 
1587
1639
  # Bottom left section
1588
- if self.first_obj.zodiac_type == "Tropic":
1640
+ if self.first_obj.zodiac_type == "Tropical":
1589
1641
  zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
1590
1642
  else:
1591
1643
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
@@ -1597,17 +1649,25 @@ class ChartDrawer:
1597
1649
 
1598
1650
  # Lunar phase information from second object (Transit) (optional)
1599
1651
  if self.second_obj is not None and hasattr(self.second_obj, 'lunar_phase') and self.second_obj.lunar_phase is not None:
1600
- template_dict["bottom_left_2"] = f'{self._translate("lunation_day", "Lunation Day")}: {self.second_obj.lunar_phase.get("moon_phase", "")}' # type: ignore
1601
- template_dict["bottom_left_3"] = f'{self._translate("lunar_phase", "Lunar Phase")}: {self._translate(self.second_obj.lunar_phase.moon_phase_name.lower().replace(" ", "_"), self.second_obj.lunar_phase.moon_phase_name)}'
1652
+ template_dict["bottom_left_3"] = (
1653
+ f"{self._translate('Transit', 'Transit')} "
1654
+ f"{self._translate('lunation_day', 'Lunation Day')}: "
1655
+ f"{self.second_obj.lunar_phase.get('moon_phase', '')}"
1656
+ ) # type: ignore
1657
+ template_dict["bottom_left_4"] = (
1658
+ f"{self._translate('Transit', 'Transit')} "
1659
+ f"{self._translate('lunar_phase', 'Lunar Phase')}: "
1660
+ f"{self._translate(self.second_obj.lunar_phase.moon_phase_name.lower().replace(' ', '_'), self.second_obj.lunar_phase.moon_phase_name)}"
1661
+ )
1602
1662
  else:
1603
- template_dict["bottom_left_2"] = ""
1604
1663
  template_dict["bottom_left_3"] = ""
1664
+ template_dict["bottom_left_4"] = ""
1605
1665
 
1606
- template_dict["bottom_left_4"] = f'{self._translate("perspective_type", "Perspective")}: {self._translate(self.second_obj.perspective_type.lower().replace(" ", "_"), self.second_obj.perspective_type)}' # type: ignore
1666
+ template_dict["bottom_left_2"] = f'{self._translate("perspective_type", "Perspective")}: {self._translate(self.second_obj.perspective_type.lower().replace(" ", "_"), self.second_obj.perspective_type)}' # type: ignore
1607
1667
 
1608
- # Moon phase section calculations - use first_obj for visualization
1609
- if self.first_obj.lunar_phase is not None:
1610
- template_dict["makeLunarPhase"] = makeLunarPhase(self.first_obj.lunar_phase["degrees_between_s_m"], self.geolat)
1668
+ # Moon phase section calculations - use transit subject data only
1669
+ if self.second_obj is not None and getattr(self.second_obj, "lunar_phase", None):
1670
+ template_dict["makeLunarPhase"] = makeLunarPhase(self.second_obj.lunar_phase["degrees_between_s_m"], self.geolat)
1611
1671
  else:
1612
1672
  template_dict["makeLunarPhase"] = ""
1613
1673
 
@@ -1763,7 +1823,7 @@ class ChartDrawer:
1763
1823
  template_dict["top_left_5"] = format_datetime_with_timezone(self.second_obj.iso_formatted_local_datetime) # type: ignore
1764
1824
 
1765
1825
  # Bottom left section
1766
- if self.first_obj.zodiac_type == "Tropic":
1826
+ if self.first_obj.zodiac_type == "Tropical":
1767
1827
  zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
1768
1828
  else:
1769
1829
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
@@ -1963,7 +2023,7 @@ class ChartDrawer:
1963
2023
  template_dict["top_left_5"] = f"{latitude_string} / {longitude_string}"
1964
2024
 
1965
2025
  # Bottom left section
1966
- if self.first_obj.zodiac_type == "Tropic":
2026
+ if self.first_obj.zodiac_type == "Tropical":
1967
2027
  zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
1968
2028
  else:
1969
2029
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
@@ -2136,7 +2196,7 @@ class ChartDrawer:
2136
2196
  template_dict["top_left_5"] = f"{self._translate('type', 'Type')}: {self._translate('lunar_return', 'Lunar Return')}"
2137
2197
 
2138
2198
  # Bottom left section
2139
- if self.first_obj.zodiac_type == "Tropic":
2199
+ if self.first_obj.zodiac_type == "Tropical":
2140
2200
  zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
2141
2201
  else:
2142
2202
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
@@ -2233,7 +2293,7 @@ class ChartDrawer:
2233
2293
 
2234
2294
  # return filename
2235
2295
 
2236
- logging.debug(f"Template dictionary has {len(td.model_dump())} fields")
2296
+ logger.debug("Template dictionary includes %s fields", len(td.model_dump()))
2237
2297
 
2238
2298
  self._create_template_dictionary()
2239
2299