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.

Files changed (77) 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.2.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.2.dist-info/LICENSE +0 -661
  74. kerykeion-4.26.2.dist-info/METADATA +0 -629
  75. kerykeion-4.26.2.dist-info/RECORD +0 -46
  76. kerykeion-4.26.2.dist-info/entry_points.txt +0 -3
  77. /LICENSE → /kerykeion-5.0.0.dist-info/licenses/LICENSE +0 -0
kerykeion/__init__.py CHANGED
@@ -6,15 +6,58 @@ This is part of Kerykeion (C) 2025 Giacomo Battaglia
6
6
  """
7
7
 
8
8
  # Local
9
- from .astrological_subject import AstrologicalSubject
10
- from .charts.kerykeion_chart_svg import KerykeionChartSVG
11
- from .kr_types import *
12
- from .relationship_score.relationship_score import RelationshipScore
13
- from .relationship_score.relationship_score_factory import RelationshipScoreFactory
14
- from .aspects import SynastryAspects, NatalAspects
15
- from .report import Report
16
- from .settings import KerykeionSettingsModel, get_settings
17
- from .enums import Planets, Aspects, Signs
18
- from .ephemeris_data import EphemerisDataFactory
9
+ from .aspects import AspectsFactory
10
+ from .astrological_subject_factory import AstrologicalSubjectFactory
11
+ from .chart_data_factory import ChartDataFactory
12
+ from .schemas import KerykeionException
13
+ from .schemas.kr_models import (
14
+ ChartDataModel,
15
+ SingleChartDataModel,
16
+ DualChartDataModel,
17
+ ElementDistributionModel,
18
+ QualityDistributionModel,
19
+ HouseComparisonModel,
20
+ )
21
+ from .charts.chart_drawer import ChartDrawer
19
22
  from .composite_subject_factory import CompositeSubjectFactory
20
- from .transits_time_range import TransitsTimeRangeFactory
23
+ from .ephemeris_data_factory import EphemerisDataFactory
24
+ from .house_comparison.house_comparison_factory import HouseComparisonFactory
25
+ from .planetary_return_factory import PlanetaryReturnFactory, PlanetReturnModel
26
+ from .relationship_score_factory import RelationshipScoreFactory
27
+ from .report import ReportGenerator
28
+ from .settings import KerykeionSettingsModel
29
+ from .transits_time_range_factory import TransitsTimeRangeFactory
30
+ from .backword import (
31
+ AstrologicalSubject, # Legacy wrapper
32
+ KerykeionChartSVG, # Legacy wrapper
33
+ NatalAspects, # Legacy wrapper
34
+ SynastryAspects, # Legacy wrapper
35
+ )
36
+
37
+ __all__ = [
38
+ "AspectsFactory",
39
+ "AstrologicalSubjectFactory",
40
+ "ChartDataFactory",
41
+ "ChartDataModel",
42
+ "SingleChartDataModel",
43
+ "DualChartDataModel",
44
+ "ElementDistributionModel",
45
+ "QualityDistributionModel",
46
+ "ChartDrawer",
47
+ "CompositeSubjectFactory",
48
+ "EphemerisDataFactory",
49
+ "HouseComparisonFactory",
50
+ "HouseComparisonModel",
51
+ "KerykeionException",
52
+ "PlanetaryReturnFactory",
53
+ "PlanetReturnModel",
54
+ "RelationshipScoreFactory",
55
+ "ReportGenerator",
56
+ "KerykeionSettingsModel",
57
+ "TransitsTimeRangeFactory",
58
+ # Legacy (v4) exported names for backward compatibility
59
+ "AstrologicalSubject",
60
+ "KerykeionChartSVG",
61
+ "NatalAspects",
62
+ "SynastryAspects",
63
+ ]
@@ -7,5 +7,8 @@ The aspects module contains the classes and functions for calculating
7
7
  """
8
8
 
9
9
 
10
- from .synastry_aspects import SynastryAspects
11
- from .natal_aspects import NatalAspects
10
+ from .aspects_factory import AspectsFactory
11
+
12
+ __all__ = [
13
+ "AspectsFactory",
14
+ ]
@@ -0,0 +1,569 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ This is part of Kerykeion (C) 2025 Giacomo Battaglia
4
+ """
5
+
6
+ import logging
7
+ from typing import Union, List, Optional
8
+
9
+ from kerykeion.astrological_subject_factory import AstrologicalSubjectFactory
10
+ from kerykeion.aspects.aspects_utils import get_aspect_from_two_points, get_active_points_list, calculate_aspect_movement
11
+ from kerykeion.schemas.kr_models import (
12
+ AstrologicalSubjectModel,
13
+ AspectModel,
14
+ ActiveAspect,
15
+ CompositeSubjectModel,
16
+ PlanetReturnModel,
17
+ SingleChartAspectsModel,
18
+ DualChartAspectsModel,
19
+ # Legacy aliases for backward compatibility
20
+ NatalAspectsModel,
21
+ SynastryAspectsModel
22
+ )
23
+ from kerykeion.schemas.kr_literals import AstrologicalPoint
24
+ from kerykeion.settings.config_constants import DEFAULT_ACTIVE_ASPECTS
25
+ from kerykeion.settings.chart_defaults import (
26
+ DEFAULT_CELESTIAL_POINTS_SETTINGS,
27
+ DEFAULT_CHART_ASPECTS_SETTINGS,
28
+ )
29
+ from kerykeion.utilities import find_common_active_points
30
+
31
+ # Axes constants for orb filtering
32
+ AXES_LIST = [
33
+ "Ascendant",
34
+ "Medium_Coeli",
35
+ "Descendant",
36
+ "Imum_Coeli",
37
+ ]
38
+
39
+
40
+ class AspectsFactory:
41
+ """
42
+ Unified factory class for creating both single chart and dual chart aspects analysis.
43
+
44
+ This factory provides methods to calculate all aspects within a single chart or
45
+ between two charts. It consolidates the common functionality between different
46
+ types of aspect calculations while providing specialized methods for each type.
47
+
48
+ The factory provides both comprehensive and filtered aspect lists based on orb settings
49
+ and relevance criteria.
50
+
51
+ Key Features:
52
+ - Calculates aspects within a single chart (natal, returns, composite, etc.)
53
+ - Calculates aspects between two charts (synastry, transits, comparisons, etc.)
54
+ - Filters aspects based on orb thresholds
55
+ - Applies stricter orb limits for chart axes (ASC, MC, DSC, IC)
56
+ - Supports multiple subject types (natal, composite, planetary returns)
57
+
58
+ Example:
59
+ >>> # For single chart aspects (natal, returns, etc.)
60
+ >>> johnny = AstrologicalSubjectFactory.from_birth_data("Johnny", 1963, 6, 9, 0, 0, "Owensboro", "US")
61
+ >>> single_chart_aspects = AspectsFactory.single_chart_aspects(johnny)
62
+ >>>
63
+ >>> # For dual chart aspects (synastry, comparisons, etc.)
64
+ >>> john = AstrologicalSubjectFactory.from_birth_data("John", 1990, 1, 1, 12, 0, "London", "GB")
65
+ >>> jane = AstrologicalSubjectFactory.from_birth_data("Jane", 1992, 6, 15, 14, 30, "Paris", "FR")
66
+ >>> dual_chart_aspects = AspectsFactory.dual_chart_aspects(john, jane)
67
+ """
68
+
69
+ @staticmethod
70
+ def single_chart_aspects(
71
+ subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
72
+ *,
73
+ active_points: Optional[List[AstrologicalPoint]] = None,
74
+ active_aspects: Optional[List[ActiveAspect]] = None,
75
+ axis_orb_limit: Optional[float] = None,
76
+ ) -> SingleChartAspectsModel:
77
+ """
78
+ Create aspects analysis for a single astrological chart.
79
+
80
+ This method calculates all astrological aspects (angular relationships)
81
+ within a single chart. Can be used for any type of chart including:
82
+ - Natal charts
83
+ - Planetary return charts
84
+ - Composite charts
85
+ - Any other single chart type
86
+
87
+ Args:
88
+ subject: The astrological subject for aspect calculation
89
+
90
+ Kwargs:
91
+ active_points: List of points to include in calculations
92
+ active_aspects: List of aspects with their orb settings
93
+ axis_orb_limit: Optional orb threshold applied to chart axes; when None, no special axis filter
94
+
95
+ Returns:
96
+ SingleChartAspectsModel containing all calculated aspects data
97
+
98
+ Example:
99
+ >>> johnny = AstrologicalSubjectFactory.from_birth_data("Johnny", 1963, 6, 9, 0, 0, "Owensboro", "US")
100
+ >>> chart_aspects = AspectsFactory.single_chart_aspects(johnny)
101
+ >>> print(f"Found {len(chart_aspects.relevant_aspects)} relevant aspects")
102
+ """
103
+ # Initialize settings and configurations
104
+ celestial_points = DEFAULT_CELESTIAL_POINTS_SETTINGS
105
+ aspects_settings = DEFAULT_CHART_ASPECTS_SETTINGS
106
+ # Set active aspects with default fallback
107
+ active_aspects_resolved = active_aspects if active_aspects is not None else DEFAULT_ACTIVE_ASPECTS
108
+
109
+ # Determine active points to use
110
+ if active_points is None:
111
+ active_points_resolved = subject.active_points
112
+ else:
113
+ active_points_resolved = find_common_active_points(
114
+ subject.active_points,
115
+ active_points,
116
+ )
117
+
118
+ return AspectsFactory._create_single_chart_aspects_model(
119
+ subject,
120
+ active_points_resolved,
121
+ active_aspects_resolved,
122
+ aspects_settings,
123
+ axis_orb_limit,
124
+ celestial_points,
125
+ )
126
+
127
+ @staticmethod
128
+ def dual_chart_aspects(
129
+ first_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
130
+ second_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
131
+ *,
132
+ active_points: Optional[List[AstrologicalPoint]] = None,
133
+ active_aspects: Optional[List[ActiveAspect]] = None,
134
+ axis_orb_limit: Optional[float] = None,
135
+ ) -> DualChartAspectsModel:
136
+ """
137
+ Create aspects analysis between two astrological charts.
138
+
139
+ This method calculates all astrological aspects (angular relationships)
140
+ between planets and points in two different charts. Can be used for:
141
+ - Synastry (relationship compatibility)
142
+ - Transit comparisons
143
+ - Composite vs natal comparisons
144
+ - Any other dual chart analysis
145
+
146
+ Args:
147
+ first_subject: The first astrological subject
148
+ second_subject: The second astrological subject to compare with the first
149
+
150
+ Kwargs:
151
+ active_points: Optional list of celestial points to include in calculations.
152
+ If None, uses common points between both subjects.
153
+ active_aspects: Optional list of aspect types with their orb settings.
154
+ If None, uses default aspect configuration.
155
+ axis_orb_limit: Optional orb threshold for chart axes (applied to single chart calculations only)
156
+
157
+ Returns:
158
+ DualChartAspectsModel: Complete model containing all calculated aspects data,
159
+ including both comprehensive and filtered relevant aspects.
160
+
161
+ Example:
162
+ >>> john = AstrologicalSubjectFactory.from_birth_data("John", 1990, 1, 1, 12, 0, "London", "GB")
163
+ >>> jane = AstrologicalSubjectFactory.from_birth_data("Jane", 1992, 6, 15, 14, 30, "Paris", "FR")
164
+ >>> synastry = AspectsFactory.dual_chart_aspects(john, jane)
165
+ >>> print(f"Found {len(synastry.relevant_aspects)} relevant aspects")
166
+ """
167
+ # Initialize settings and configurations
168
+ celestial_points = DEFAULT_CELESTIAL_POINTS_SETTINGS
169
+ aspects_settings = DEFAULT_CHART_ASPECTS_SETTINGS
170
+ # Set active aspects with default fallback
171
+ active_aspects_resolved = active_aspects if active_aspects is not None else DEFAULT_ACTIVE_ASPECTS
172
+
173
+ # Determine active points to use - find common points between both subjects
174
+ if active_points is None:
175
+ active_points_resolved = first_subject.active_points
176
+ else:
177
+ active_points_resolved = find_common_active_points(
178
+ first_subject.active_points,
179
+ active_points,
180
+ )
181
+
182
+ # Further filter with second subject's active points
183
+ active_points_resolved = find_common_active_points(
184
+ second_subject.active_points,
185
+ active_points_resolved,
186
+ )
187
+
188
+ return AspectsFactory._create_dual_chart_aspects_model(
189
+ first_subject,
190
+ second_subject,
191
+ active_points_resolved,
192
+ active_aspects_resolved,
193
+ aspects_settings,
194
+ axis_orb_limit,
195
+ celestial_points,
196
+ )
197
+
198
+ @staticmethod
199
+ def _create_single_chart_aspects_model(
200
+ subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
201
+ active_points_resolved: List[AstrologicalPoint],
202
+ active_aspects_resolved: List[ActiveAspect],
203
+ aspects_settings: List[dict],
204
+ axis_orb_limit: Optional[float],
205
+ celestial_points: List[dict]
206
+ ) -> SingleChartAspectsModel:
207
+ """
208
+ Create the complete single chart aspects model with all calculations.
209
+
210
+ Returns:
211
+ SingleChartAspectsModel containing all aspects data
212
+ """
213
+ all_aspects = AspectsFactory._calculate_single_chart_aspects(
214
+ subject, active_points_resolved, active_aspects_resolved, aspects_settings, celestial_points
215
+ )
216
+ relevant_aspects = AspectsFactory._filter_relevant_aspects(
217
+ all_aspects,
218
+ axis_orb_limit,
219
+ apply_axis_orb_filter=axis_orb_limit is not None,
220
+ )
221
+
222
+ return SingleChartAspectsModel(
223
+ subject=subject,
224
+ all_aspects=all_aspects,
225
+ relevant_aspects=relevant_aspects,
226
+ active_points=active_points_resolved,
227
+ active_aspects=active_aspects_resolved,
228
+ )
229
+
230
+ @staticmethod
231
+ def _create_dual_chart_aspects_model(
232
+ first_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
233
+ second_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
234
+ active_points_resolved: List[AstrologicalPoint],
235
+ active_aspects_resolved: List[ActiveAspect],
236
+ aspects_settings: List[dict],
237
+ axis_orb_limit: Optional[float],
238
+ celestial_points: List[dict]
239
+ ) -> DualChartAspectsModel:
240
+ """
241
+ Create the complete dual chart aspects model with all calculations.
242
+
243
+ Args:
244
+ first_subject: First astrological subject
245
+ second_subject: Second astrological subject
246
+ active_points_resolved: Resolved list of active celestial points
247
+ active_aspects_resolved: Resolved list of active aspects with orbs
248
+ aspects_settings: Chart aspect configuration settings
249
+ axis_orb_limit: Orb threshold for chart axes
250
+ celestial_points: Celestial points configuration
251
+
252
+ Returns:
253
+ DualChartAspectsModel: Complete model containing all aspects data
254
+ """
255
+ all_aspects = AspectsFactory._calculate_dual_chart_aspects(
256
+ first_subject, second_subject, active_points_resolved, active_aspects_resolved,
257
+ aspects_settings, celestial_points
258
+ )
259
+ relevant_aspects = AspectsFactory._filter_relevant_aspects(
260
+ all_aspects,
261
+ axis_orb_limit,
262
+ apply_axis_orb_filter=False,
263
+ )
264
+
265
+ return DualChartAspectsModel(
266
+ first_subject=first_subject,
267
+ second_subject=second_subject,
268
+ all_aspects=all_aspects,
269
+ relevant_aspects=relevant_aspects,
270
+ active_points=active_points_resolved,
271
+ active_aspects=active_aspects_resolved,
272
+ )
273
+
274
+ @staticmethod
275
+ def _calculate_single_chart_aspects(
276
+ subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
277
+ active_points: List[AstrologicalPoint],
278
+ active_aspects: List[ActiveAspect],
279
+ aspects_settings: List[dict],
280
+ celestial_points: List[dict]
281
+ ) -> List[AspectModel]:
282
+ """
283
+ Calculate all aspects within a single chart.
284
+
285
+ This method handles all aspect calculations including settings updates,
286
+ opposite pair filtering, and planet ID resolution for single charts.
287
+ Works with any chart type (natal, return, composite, etc.).
288
+
289
+ Returns:
290
+ List of all calculated AspectModel instances
291
+ """
292
+ active_points_list = get_active_points_list(subject, active_points)
293
+
294
+ # Update aspects settings with active aspects orbs
295
+ filtered_settings = AspectsFactory._update_aspect_settings(aspects_settings, active_aspects)
296
+
297
+ # Create a lookup dictionary for planet IDs to optimize performance
298
+ planet_id_lookup = {planet["name"]: planet["id"] for planet in celestial_points}
299
+
300
+ # Define opposite pairs that should be skipped for single chart aspects
301
+ opposite_pairs = {
302
+ ("Ascendant", "Descendant"),
303
+ ("Descendant", "Ascendant"),
304
+ ("Medium_Coeli", "Imum_Coeli"),
305
+ ("Imum_Coeli", "Medium_Coeli"),
306
+ ("True_North_Lunar_Node", "True_South_Lunar_Node"),
307
+ ("Mean_North_Lunar_Node", "Mean_South_Lunar_Node"),
308
+ ("True_South_Lunar_Node", "True_North_Lunar_Node"),
309
+ ("Mean_South_Lunar_Node", "Mean_North_Lunar_Node"),
310
+ }
311
+
312
+ all_aspects_list = []
313
+
314
+ for first in range(len(active_points_list)):
315
+ # Generate aspects list without repetitions (single chart - same chart)
316
+ for second in range(first + 1, len(active_points_list)):
317
+ # Skip predefined opposite pairs (AC/DC, MC/IC, North/South nodes)
318
+ first_name = active_points_list[first]["name"]
319
+ second_name = active_points_list[second]["name"]
320
+
321
+ if (first_name, second_name) in opposite_pairs:
322
+ continue
323
+
324
+ aspect = get_aspect_from_two_points(
325
+ filtered_settings,
326
+ active_points_list[first]["abs_pos"],
327
+ active_points_list[second]["abs_pos"]
328
+ )
329
+
330
+ if aspect["verdict"]:
331
+ # Get planet IDs using lookup dictionary for better performance
332
+ first_planet_id = planet_id_lookup.get(first_name, 0)
333
+ second_planet_id = planet_id_lookup.get(second_name, 0)
334
+
335
+ # Calculate aspect movement (applying/separating/exact)
336
+ aspect_movement = calculate_aspect_movement(
337
+ active_points_list[first]["abs_pos"],
338
+ active_points_list[second]["abs_pos"],
339
+ aspect["aspect_degrees"]
340
+ )
341
+
342
+ aspect_model = AspectModel(
343
+ p1_name=first_name,
344
+ p1_owner=subject.name,
345
+ p1_abs_pos=active_points_list[first]["abs_pos"],
346
+ p2_name=second_name,
347
+ p2_owner=subject.name,
348
+ p2_abs_pos=active_points_list[second]["abs_pos"],
349
+ aspect=aspect["name"],
350
+ orbit=aspect["orbit"],
351
+ aspect_degrees=aspect["aspect_degrees"],
352
+ diff=aspect["diff"],
353
+ p1=first_planet_id,
354
+ p2=second_planet_id,
355
+ aspect_movement=aspect_movement,
356
+ )
357
+ all_aspects_list.append(aspect_model)
358
+
359
+ return all_aspects_list
360
+
361
+ @staticmethod
362
+ def _calculate_dual_chart_aspects(
363
+ first_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
364
+ second_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
365
+ active_points: List[AstrologicalPoint],
366
+ active_aspects: List[ActiveAspect],
367
+ aspects_settings: List[dict],
368
+ celestial_points: List[dict]
369
+ ) -> List[AspectModel]:
370
+ """
371
+ Calculate all aspects between two charts.
372
+
373
+ This method performs comprehensive aspect calculations between all active points
374
+ of both subjects, applying the specified orb settings and creating detailed
375
+ aspect models with planet IDs and positional information.
376
+ Works with any chart types (synastry, transits, comparisons, etc.).
377
+
378
+ Args:
379
+ first_subject: First astrological subject
380
+ second_subject: Second astrological subject
381
+ active_points: List of celestial points to include in calculations
382
+ active_aspects: List of aspect types with their orb settings
383
+ aspects_settings: Base aspect configuration settings
384
+ celestial_points: Celestial points configuration with IDs
385
+
386
+ Returns:
387
+ List[AspectModel]: Complete list of all calculated aspect instances
388
+ """
389
+ # Get active points lists for both subjects
390
+ first_active_points_list = get_active_points_list(first_subject, active_points)
391
+ second_active_points_list = get_active_points_list(second_subject, active_points)
392
+
393
+ # Create a lookup dictionary for planet IDs to optimize performance
394
+ planet_id_lookup = {planet["name"]: planet["id"] for planet in celestial_points}
395
+
396
+ # Update aspects settings with active aspects orbs
397
+ filtered_settings = AspectsFactory._update_aspect_settings(aspects_settings, active_aspects)
398
+
399
+ all_aspects_list = []
400
+ for first in range(len(first_active_points_list)):
401
+ # Generate aspects list between all points of first and second subjects
402
+ for second in range(len(second_active_points_list)):
403
+ aspect = get_aspect_from_two_points(
404
+ filtered_settings,
405
+ first_active_points_list[first]["abs_pos"],
406
+ second_active_points_list[second]["abs_pos"],
407
+ )
408
+
409
+ if aspect["verdict"]:
410
+ first_name = first_active_points_list[first]["name"]
411
+ second_name = second_active_points_list[second]["name"]
412
+
413
+ # Get planet IDs using lookup dictionary for better performance
414
+ first_planet_id = planet_id_lookup.get(first_name, 0)
415
+ second_planet_id = planet_id_lookup.get(second_name, 0)
416
+
417
+ # Calculate aspect movement (applying/separating/exact)
418
+ aspect_movement = calculate_aspect_movement(
419
+ first_active_points_list[first]["abs_pos"],
420
+ second_active_points_list[second]["abs_pos"],
421
+ aspect["aspect_degrees"]
422
+ )
423
+
424
+ aspect_model = AspectModel(
425
+ p1_name=first_name,
426
+ p1_owner=first_subject.name,
427
+ p1_abs_pos=first_active_points_list[first]["abs_pos"],
428
+ p2_name=second_name,
429
+ p2_owner=second_subject.name,
430
+ p2_abs_pos=second_active_points_list[second]["abs_pos"],
431
+ aspect=aspect["name"],
432
+ orbit=aspect["orbit"],
433
+ aspect_degrees=aspect["aspect_degrees"],
434
+ diff=aspect["diff"],
435
+ p1=first_planet_id,
436
+ p2=second_planet_id,
437
+ aspect_movement=aspect_movement,
438
+ )
439
+ all_aspects_list.append(aspect_model)
440
+
441
+ return all_aspects_list
442
+
443
+ @staticmethod
444
+ def _update_aspect_settings(
445
+ aspects_settings: List[dict],
446
+ active_aspects: List[ActiveAspect]
447
+ ) -> List[dict]:
448
+ """
449
+ Update aspects settings with active aspects orbs.
450
+
451
+ This is a common utility method used by both single chart and dual chart calculations.
452
+
453
+ Args:
454
+ aspects_settings: Base aspect settings
455
+ active_aspects: Active aspects with their orb configurations
456
+
457
+ Returns:
458
+ List of filtered and updated aspect settings
459
+ """
460
+ filtered_settings = []
461
+ for aspect_setting in aspects_settings:
462
+ for active_aspect in active_aspects:
463
+ if aspect_setting["name"] == active_aspect["name"]:
464
+ aspect_setting = aspect_setting.copy() # Don't modify original
465
+ aspect_setting["orb"] = active_aspect["orb"]
466
+ filtered_settings.append(aspect_setting)
467
+ break
468
+ return filtered_settings
469
+
470
+ @staticmethod
471
+ def _filter_relevant_aspects(
472
+ all_aspects: List[AspectModel],
473
+ axis_orb_limit: Optional[float],
474
+ *,
475
+ apply_axis_orb_filter: bool,
476
+ ) -> List[AspectModel]:
477
+ """
478
+ Filter aspects based on orb thresholds for axes and comprehensive criteria.
479
+
480
+ This method consolidates all filtering logic including axes checks and orb thresholds
481
+ for both single chart and dual chart aspects in a single comprehensive filtering method.
482
+
483
+ Args:
484
+ all_aspects: Complete list of calculated aspects
485
+ axis_orb_limit: Optional orb threshold for axes aspects
486
+ apply_axis_orb_filter: Whether to apply the axis-specific orb filtering logic
487
+
488
+ Returns:
489
+ Filtered list of relevant aspects
490
+ """
491
+ logging.debug("Calculating relevant aspects by filtering orbs...")
492
+
493
+ relevant_aspects = []
494
+
495
+ if not apply_axis_orb_filter or axis_orb_limit is None:
496
+ return list(all_aspects)
497
+
498
+ for aspect in all_aspects:
499
+ # Check if aspect involves any of the chart axes and apply stricter orb limits
500
+ aspect_involves_axes = (aspect.p1_name in AXES_LIST or aspect.p2_name in AXES_LIST)
501
+
502
+ if aspect_involves_axes and abs(aspect.orbit) >= axis_orb_limit:
503
+ continue
504
+
505
+ relevant_aspects.append(aspect)
506
+
507
+ return relevant_aspects
508
+
509
+ # Legacy methods for temporary backward compatibility
510
+ @staticmethod
511
+ def natal_aspects(
512
+ subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
513
+ *,
514
+ active_points: Optional[List[AstrologicalPoint]] = None,
515
+ active_aspects: Optional[List[ActiveAspect]] = None,
516
+ axis_orb_limit: Optional[float] = None,
517
+ ) -> NatalAspectsModel:
518
+ """
519
+ Legacy method - use single_chart_aspects() instead.
520
+
521
+ ⚠️ DEPRECATION WARNING ⚠️
522
+ This method is deprecated. Use AspectsFactory.single_chart_aspects() instead.
523
+ """
524
+ return AspectsFactory.single_chart_aspects(
525
+ subject,
526
+ active_points=active_points,
527
+ active_aspects=active_aspects,
528
+ axis_orb_limit=axis_orb_limit,
529
+ )
530
+
531
+ @staticmethod
532
+ def synastry_aspects(
533
+ first_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
534
+ second_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
535
+ *,
536
+ active_points: Optional[List[AstrologicalPoint]] = None,
537
+ active_aspects: Optional[List[ActiveAspect]] = None,
538
+ axis_orb_limit: Optional[float] = None,
539
+ ) -> SynastryAspectsModel:
540
+ """
541
+ Legacy method - use dual_chart_aspects() instead.
542
+
543
+ ⚠️ DEPRECATION WARNING ⚠️
544
+ This method is deprecated. Use AspectsFactory.dual_chart_aspects() instead.
545
+ """
546
+ return AspectsFactory.dual_chart_aspects(
547
+ first_subject,
548
+ second_subject,
549
+ active_points=active_points,
550
+ active_aspects=active_aspects,
551
+ axis_orb_limit=axis_orb_limit,
552
+ )
553
+
554
+
555
+ if __name__ == "__main__":
556
+ from kerykeion.utilities import setup_logging
557
+
558
+ setup_logging(level="debug")
559
+
560
+ # Test single chart aspects (replaces natal aspects)
561
+ johnny = AstrologicalSubjectFactory.from_birth_data("Johnny Depp", 1963, 6, 9, 0, 0, city="Owensboro", nation="US")
562
+ 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)}")
564
+
565
+ # Test dual chart aspects (replaces synastry aspects)
566
+ john = AstrologicalSubjectFactory.from_birth_data("John", 1940, 10, 9, 10, 30, "Liverpool", "GB")
567
+ yoko = AstrologicalSubjectFactory.from_birth_data("Yoko", 1933, 2, 18, 10, 30, "Tokyo", "JP")
568
+ 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)}")