kerykeion 4.18.3__py3-none-any.whl → 5.1.9__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 +56 -11
  2. kerykeion/aspects/__init__.py +7 -4
  3. kerykeion/aspects/aspects_factory.py +568 -0
  4. kerykeion/aspects/aspects_utils.py +86 -13
  5. kerykeion/astrological_subject_factory.py +1901 -0
  6. kerykeion/backword.py +820 -0
  7. kerykeion/chart_data_factory.py +552 -0
  8. kerykeion/charts/__init__.py +2 -2
  9. kerykeion/charts/chart_drawer.py +2794 -0
  10. kerykeion/charts/charts_utils.py +1066 -309
  11. kerykeion/charts/draw_planets.py +602 -351
  12. kerykeion/charts/templates/aspect_grid_only.xml +337 -193
  13. kerykeion/charts/templates/chart.xml +441 -240
  14. kerykeion/charts/templates/wheel_only.xml +365 -211
  15. kerykeion/charts/themes/black-and-white.css +148 -0
  16. kerykeion/charts/themes/classic.css +107 -76
  17. kerykeion/charts/themes/dark-high-contrast.css +145 -107
  18. kerykeion/charts/themes/dark.css +146 -107
  19. kerykeion/charts/themes/light.css +146 -103
  20. kerykeion/charts/themes/strawberry.css +158 -0
  21. kerykeion/composite_subject_factory.py +408 -0
  22. kerykeion/ephemeris_data_factory.py +443 -0
  23. kerykeion/fetch_geonames.py +81 -21
  24. kerykeion/house_comparison/__init__.py +6 -0
  25. kerykeion/house_comparison/house_comparison_factory.py +103 -0
  26. kerykeion/house_comparison/house_comparison_utils.py +126 -0
  27. kerykeion/kr_types/__init__.py +66 -6
  28. kerykeion/kr_types/chart_template_model.py +20 -0
  29. kerykeion/kr_types/kerykeion_exception.py +15 -9
  30. kerykeion/kr_types/kr_literals.py +14 -106
  31. kerykeion/kr_types/kr_models.py +14 -179
  32. kerykeion/kr_types/settings_models.py +15 -152
  33. kerykeion/planetary_return_factory.py +805 -0
  34. kerykeion/relationship_score_factory.py +301 -0
  35. kerykeion/report.py +750 -65
  36. kerykeion/schemas/__init__.py +106 -0
  37. kerykeion/schemas/chart_template_model.py +367 -0
  38. kerykeion/schemas/kerykeion_exception.py +20 -0
  39. kerykeion/schemas/kr_literals.py +181 -0
  40. kerykeion/schemas/kr_models.py +603 -0
  41. kerykeion/schemas/settings_models.py +188 -0
  42. kerykeion/settings/__init__.py +20 -1
  43. kerykeion/settings/chart_defaults.py +444 -0
  44. kerykeion/settings/config_constants.py +152 -0
  45. kerykeion/settings/kerykeion_settings.py +36 -61
  46. kerykeion/settings/translation_strings.py +1499 -0
  47. kerykeion/settings/translations.py +74 -0
  48. kerykeion/sweph/ast136/s136108s.se1 +0 -0
  49. kerykeion/sweph/ast136/s136199s.se1 +0 -0
  50. kerykeion/sweph/ast136/s136472s.se1 +0 -0
  51. kerykeion/sweph/ast28/se28978s.se1 +0 -0
  52. kerykeion/sweph/ast50/se50000s.se1 +0 -0
  53. kerykeion/sweph/ast90/se90377s.se1 +0 -0
  54. kerykeion/sweph/ast90/se90482s.se1 +0 -0
  55. kerykeion/sweph/sefstars.txt +1602 -0
  56. kerykeion/transits_time_range_factory.py +302 -0
  57. kerykeion/utilities.py +626 -125
  58. kerykeion-5.1.9.dist-info/METADATA +1793 -0
  59. kerykeion-5.1.9.dist-info/RECORD +63 -0
  60. {kerykeion-4.18.3.dist-info → kerykeion-5.1.9.dist-info}/WHEEL +1 -1
  61. kerykeion/aspects/natal_aspects.py +0 -143
  62. kerykeion/aspects/synastry_aspects.py +0 -113
  63. kerykeion/astrological_subject.py +0 -818
  64. kerykeion/charts/kerykeion_chart_svg.py +0 -894
  65. kerykeion/enums.py +0 -51
  66. kerykeion/ephemeris_data.py +0 -178
  67. kerykeion/kr_types/chart_types.py +0 -88
  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 -275
  71. kerykeion/settings/kr.config.json +0 -721
  72. kerykeion-4.18.3.dist-info/LICENSE +0 -661
  73. kerykeion-4.18.3.dist-info/METADATA +0 -396
  74. kerykeion-4.18.3.dist-info/RECORD +0 -42
  75. kerykeion-4.18.3.dist-info/entry_points.txt +0 -3
  76. /LICENSE → /kerykeion-5.1.9.dist-info/licenses/LICENSE +0 -0
@@ -0,0 +1,805 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Planetary Return Factory Module
4
+
5
+ This module provides the PlanetaryReturnFactory class for calculating and generating
6
+ comprehensive planetary return charts, specifically Solar and Lunar returns. It leverages
7
+ the Swiss Ephemeris library for precise astronomical calculations to determine exact
8
+ return moments and create complete astrological chart data.
9
+
10
+ Key Features:
11
+ - Solar Return calculations (Sun's annual return to natal position)
12
+ - Lunar Return calculations (Moon's monthly return to natal position)
13
+ - Multiple date input formats (ISO datetime, year-based, month/year-based)
14
+ - Flexible location handling (online geocoding or manual coordinates)
15
+ - Complete astrological chart generation for return moments
16
+ - Integration with Geonames service for location data
17
+ - Timezone-aware calculations with UTC precision
18
+
19
+ A planetary return occurs when a planet returns to the exact degree and minute
20
+ it occupied at the time of birth. Solar returns happen approximately once per year
21
+ and are widely used for annual forecasting, while Lunar returns occur roughly
22
+ every 27-29 days and are used for monthly analysis and timing.
23
+
24
+ The factory creates complete AstrologicalSubject instances for the calculated
25
+ return moments, enabling full chart analysis including planetary positions,
26
+ aspects, house cusps, and all other astrological features.
27
+
28
+ Classes:
29
+ PlanetaryReturnFactory: Main factory class for calculating planetary returns
30
+
31
+ Dependencies:
32
+ - swisseph: Swiss Ephemeris library for astronomical calculations
33
+ - kerykeion.AstrologicalSubjectFactory: For creating complete chart data
34
+ - kerykeion.fetch_geonames: For online location data retrieval
35
+ - kerykeion.utilities: For date/time conversions and astronomical functions
36
+ - kerykeion.schemas: For type definitions and model structures
37
+
38
+ Example:
39
+ Basic Solar Return calculation for a specific year:
40
+
41
+ >>> from kerykeion import AstrologicalSubjectFactory
42
+ >>> from kerykeion.planetary_return_factory import PlanetaryReturnFactory
43
+ >>>
44
+ >>> # Create natal chart
45
+ >>> subject = AstrologicalSubjectFactory.from_birth_data(
46
+ ... name="John Doe",
47
+ ... year=1990, month=6, day=15,
48
+ ... hour=12, minute=30,
49
+ ... lat=40.7128, lng=-74.0060,
50
+ ... tz_str="America/New_York"
51
+ ... )
52
+ >>>
53
+ >>> # Create return calculator for New York location
54
+ >>> calculator = PlanetaryReturnFactory(
55
+ ... subject,
56
+ ... city="New York",
57
+ ... nation="US",
58
+ ... online=True
59
+ ... )
60
+ >>>
61
+ >>> # Calculate Solar Return for 2024
62
+ >>> solar_return = calculator.next_return_from_year(2024, "Solar")
63
+ >>> print(f"Solar Return: {solar_return.iso_formatted_local_datetime}")
64
+ >>> print(f"Sun position: {solar_return.sun.abs_pos}°")
65
+
66
+ Author: Giacomo Battaglia
67
+ Copyright: (C) 2025 Kerykeion Project
68
+ License: AGPL-3.0
69
+ """
70
+ import logging
71
+ import swisseph as swe
72
+
73
+ from datetime import datetime, timezone
74
+ from typing import Union
75
+
76
+ from kerykeion.schemas import KerykeionException
77
+ from kerykeion.fetch_geonames import FetchGeonames
78
+ from kerykeion.utilities import julian_to_datetime, datetime_to_julian
79
+ from kerykeion.astrological_subject_factory import (
80
+ GEONAMES_DEFAULT_USERNAME_WARNING,
81
+ DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS,
82
+ DEFAULT_GEONAMES_USERNAME,
83
+ )
84
+ from kerykeion.astrological_subject_factory import AstrologicalSubjectFactory
85
+ from kerykeion.schemas.kr_literals import ReturnType
86
+ from kerykeion.schemas.kr_models import PlanetReturnModel, AstrologicalSubjectModel
87
+
88
+
89
+ class PlanetaryReturnFactory:
90
+ """
91
+ A factory class for calculating and generating planetary return charts.
92
+
93
+ This class specializes in computing precise planetary return moments using the Swiss
94
+ Ephemeris library and creating complete astrological charts for those calculated times.
95
+ It supports both Solar Returns (annual) and Lunar Returns (monthly), providing
96
+ comprehensive astrological analysis capabilities for timing and forecasting applications.
97
+
98
+ Planetary returns are fundamental concepts in predictive astrology:
99
+ - Solar Returns: Occur when the Sun returns to its exact natal position (~365.25 days)
100
+ - Lunar Returns: Occur when the Moon returns to its exact natal position (~27-29 days)
101
+
102
+ The factory handles complex astronomical calculations automatically, including:
103
+ - Precise celestial mechanics computations
104
+ - Timezone conversions and UTC coordination
105
+ - Location-based calculations for return chart casting
106
+ - Integration with online geocoding services
107
+ - Complete chart generation with all astrological points
108
+
109
+ Args:
110
+ subject (AstrologicalSubjectModel): The natal astrological subject for whom
111
+ returns are calculated. Must contain complete birth data including
112
+ planetary positions at birth.
113
+ city (Optional[str]): City name for return chart location. Required when
114
+ using online mode for location data retrieval.
115
+ nation (Optional[str]): Nation/country code for return chart location.
116
+ Required when using online mode (e.g., "US", "GB", "FR").
117
+ lng (Optional[Union[int, float]]): Geographic longitude in decimal degrees
118
+ for return chart location. Positive values for East, negative for West.
119
+ Required when using offline mode.
120
+ lat (Optional[Union[int, float]]): Geographic latitude in decimal degrees
121
+ for return chart location. Positive values for North, negative for South.
122
+ Required when using offline mode.
123
+ tz_str (Optional[str]): Timezone identifier for return chart location
124
+ (e.g., "America/New_York", "Europe/London", "Asia/Tokyo").
125
+ Required when using offline mode.
126
+ online (bool, optional): Whether to fetch location data online via Geonames
127
+ service. When True, requires city, nation, and geonames_username.
128
+ When False, requires lng, lat, and tz_str. Defaults to True.
129
+ geonames_username (Optional[str]): Username for Geonames API access.
130
+ Required when online=True and coordinates are not provided.
131
+ Register at http://www.geonames.org/login for free account.
132
+ cache_expire_after_days (int, optional): Number of days to cache Geonames
133
+ location data before refreshing. Defaults to system setting.
134
+ altitude (Optional[Union[float, int]]): Elevation above sea level in meters
135
+ for the return chart location. Reserved for future astronomical
136
+ calculations. Defaults to None.
137
+
138
+ Raises:
139
+ KerykeionException: If required location parameters are missing for the
140
+ chosen mode (online/offline).
141
+ KerykeionException: If Geonames API fails to retrieve location data.
142
+ KerykeionException: If online mode is used without proper API credentials.
143
+
144
+ Attributes:
145
+ subject (AstrologicalSubjectModel): The natal subject for calculations.
146
+ city (Optional[str]): Return chart city name.
147
+ nation (Optional[str]): Return chart nation code.
148
+ lng (float): Return chart longitude coordinate.
149
+ lat (float): Return chart latitude coordinate.
150
+ tz_str (str): Return chart timezone identifier.
151
+ online (bool): Location data retrieval mode.
152
+ city_data (Optional[dict]): Cached location data from Geonames.
153
+
154
+ Examples:
155
+ Online mode with automatic location lookup:
156
+
157
+ >>> subject = AstrologicalSubjectFactory.from_birth_data(
158
+ ... name="Alice", year=1985, month=3, day=21,
159
+ ... hour=14, minute=30, lat=51.5074, lng=-0.1278,
160
+ ... tz_str="Europe/London"
161
+ ... )
162
+ >>> factory = PlanetaryReturnFactory(
163
+ ... subject,
164
+ ... city="London",
165
+ ... nation="GB",
166
+ ... online=True,
167
+ ... geonames_username="your_username"
168
+ ... )
169
+
170
+ Offline mode with manual coordinates:
171
+
172
+ >>> factory = PlanetaryReturnFactory(
173
+ ... subject,
174
+ ... lng=-74.0060,
175
+ ... lat=40.7128,
176
+ ... tz_str="America/New_York",
177
+ ... online=False
178
+ ... )
179
+
180
+ Different location for return chart:
181
+
182
+ >>> # Calculate return as if living in a different city
183
+ >>> factory = PlanetaryReturnFactory(
184
+ ... natal_subject, # Born in London
185
+ ... city="Paris", # But living in Paris
186
+ ... nation="FR",
187
+ ... online=True
188
+ ... )
189
+
190
+ Use Cases:
191
+ - Annual Solar Return charts for yearly forecasting
192
+ - Monthly Lunar Return charts for timing analysis
193
+ - Relocation returns for different geographic locations
194
+ - Research into planetary cycle effects
195
+ - Astrological consultation and chart analysis
196
+ - Educational demonstrations of celestial mechanics
197
+
198
+ Note:
199
+ Return calculations use the exact degree and minute of natal planetary
200
+ positions. The resulting charts are cast for the precise moment when
201
+ the transiting planet reaches this position, which may not align with
202
+ calendar dates (especially for Solar Returns, which can occur on
203
+ different dates depending on leap years and location).
204
+ """
205
+
206
+ def __init__(
207
+ self,
208
+ subject: AstrologicalSubjectModel,
209
+ city: Union[str, None] = None,
210
+ nation: Union[str, None] = None,
211
+ lng: Union[int, float, None] = None,
212
+ lat: Union[int, float, None] = None,
213
+ tz_str: Union[str, None] = None,
214
+ online: bool = True,
215
+ geonames_username: Union[str, None] = None,
216
+ *,
217
+ cache_expire_after_days: int = DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS,
218
+ altitude: Union[float, int, None] = None,
219
+ ):
220
+
221
+ """
222
+ Initialize a PlanetaryReturnFactory instance with location and configuration settings.
223
+
224
+ This constructor sets up the factory with all necessary parameters for calculating
225
+ planetary returns at a specified location. It supports both online mode (with
226
+ automatic geocoding via Geonames) and offline mode (with manual coordinates).
227
+
228
+ The factory validates input parameters based on the chosen mode and automatically
229
+ retrieves missing location data when operating online. All location parameters
230
+ are stored and used for casting return charts at the exact calculated moments.
231
+
232
+ Args:
233
+ subject (AstrologicalSubjectModel): The natal astrological subject containing
234
+ birth data and planetary positions. This subject's natal planetary
235
+ positions serve as reference points for calculating returns.
236
+ city (Optional[str]): City name for the return chart location. Must be a
237
+ recognizable city name for Geonames geocoding when using online mode.
238
+ Examples: "New York", "London", "Tokyo", "Paris".
239
+ nation (Optional[str]): Country or nation code for the return chart location.
240
+ Use ISO country codes for best results (e.g., "US", "GB", "JP", "FR").
241
+ Required when online=True.
242
+ lng (Optional[Union[int, float]]): Geographic longitude coordinate in decimal
243
+ degrees for return chart location. Range: -180.0 to +180.0.
244
+ Positive values represent East longitude, negative values West longitude.
245
+ Required when online=False.
246
+ lat (Optional[Union[int, float]]): Geographic latitude coordinate in decimal
247
+ degrees for return chart location. Range: -90.0 to +90.0.
248
+ Positive values represent North latitude, negative values South latitude.
249
+ Required when online=False.
250
+ tz_str (Optional[str]): Timezone identifier string for return chart location.
251
+ Must be a valid timezone from the IANA Time Zone Database
252
+ (e.g., "America/New_York", "Europe/London", "Asia/Tokyo").
253
+ Required when online=False.
254
+ online (bool, optional): Location data retrieval mode. When True, uses
255
+ Geonames web service to automatically fetch coordinates and timezone
256
+ from city/nation parameters. When False, uses manually provided
257
+ coordinates and timezone. Defaults to True.
258
+ geonames_username (Optional[str]): Username for Geonames API access.
259
+ Required when online=True and coordinates are not manually provided.
260
+ Free accounts available at http://www.geonames.org/login.
261
+ If None and required, uses default username with warning.
262
+ cache_expire_after_days (int, optional): Number of days to cache Geonames
263
+ location data locally before requiring refresh. Helps reduce API
264
+ calls and improve performance for repeated calculations.
265
+ Defaults to system configuration value.
266
+ altitude (Optional[Union[float, int]]): Elevation above sea level in meters
267
+ for the return chart location. Currently reserved for future use in
268
+ advanced astronomical calculations. Defaults to None.
269
+
270
+ Raises:
271
+ KerykeionException: If city is not provided when online=True.
272
+ KerykeionException: If nation is not provided when online=True.
273
+ KerykeionException: If coordinates (lat/lng) are not provided when online=False.
274
+ KerykeionException: If timezone (tz_str) is not provided when online=False.
275
+ KerykeionException: If Geonames API fails to retrieve valid location data.
276
+ KerykeionException: If required parameters are missing for the chosen mode.
277
+
278
+ Examples:
279
+ Initialize with online geocoding:
280
+
281
+ >>> factory = PlanetaryReturnFactory(
282
+ ... subject,
283
+ ... city="San Francisco",
284
+ ... nation="US",
285
+ ... online=True,
286
+ ... geonames_username="your_username"
287
+ ... )
288
+
289
+ Initialize with manual coordinates:
290
+
291
+ >>> factory = PlanetaryReturnFactory(
292
+ ... subject,
293
+ ... lng=-122.4194,
294
+ ... lat=37.7749,
295
+ ... tz_str="America/Los_Angeles",
296
+ ... online=False
297
+ ... )
298
+
299
+ Initialize with mixed parameters (coordinates override online lookup):
300
+
301
+ >>> factory = PlanetaryReturnFactory(
302
+ ... subject,
303
+ ... city="Custom Location",
304
+ ... lng=-74.0060,
305
+ ... lat=40.7128,
306
+ ... tz_str="America/New_York",
307
+ ... online=False
308
+ ... )
309
+
310
+ Note:
311
+ - When both online and manual coordinates are provided, offline mode takes precedence
312
+ - Geonames cache helps reduce API calls for frequently used locations
313
+ - Timezone accuracy is crucial for precise return calculations
314
+ - Location parameters affect house cusps and angular positions in return charts
315
+ """
316
+ # Store basic configuration
317
+ self.subject = subject
318
+ self.online = online
319
+ self.cache_expire_after_days = cache_expire_after_days
320
+ self.altitude = altitude
321
+
322
+ # Geonames username
323
+ if geonames_username is None and online and (not lat or not lng or not tz_str):
324
+ logging.warning(GEONAMES_DEFAULT_USERNAME_WARNING)
325
+ self.geonames_username = DEFAULT_GEONAMES_USERNAME
326
+ else:
327
+ self.geonames_username = geonames_username # type: ignore
328
+
329
+ # City
330
+ if not city and online:
331
+ raise KerykeionException("You need to set the city if you want to use the online mode!")
332
+ else:
333
+ self.city = city
334
+
335
+ # Nation
336
+ if not nation and online:
337
+ raise KerykeionException("You need to set the nation if you want to use the online mode!")
338
+ else:
339
+ self.nation = nation
340
+
341
+ # Latitude
342
+ if not lat and not online:
343
+ raise KerykeionException("You need to set the coordinates and timezone if you want to use the offline mode!")
344
+ else:
345
+ self.lat = lat # type: ignore
346
+
347
+ # Longitude
348
+ if not lng and not online:
349
+ raise KerykeionException("You need to set the coordinates and timezone if you want to use the offline mode!")
350
+ else:
351
+ self.lng = lng # type: ignore
352
+
353
+ # Timezone
354
+ if (not online) and (not tz_str):
355
+ raise KerykeionException("You need to set the coordinates and timezone if you want to use the offline mode!")
356
+ else:
357
+ self.tz_str = tz_str # type: ignore
358
+
359
+ # Online mode
360
+ if (self.online) and (not self.tz_str) and (not self.lat) and (not self.lng):
361
+ logging.info("Fetching timezone/coordinates from geonames")
362
+
363
+ if not self.city or not self.nation or not self.geonames_username:
364
+ raise KerykeionException("You need to set the city and nation if you want to use the online mode!")
365
+
366
+ geonames = FetchGeonames(
367
+ self.city,
368
+ self.nation,
369
+ username=self.geonames_username,
370
+ cache_expire_after_days=self.cache_expire_after_days
371
+ )
372
+ self.city_data: dict[str, str] = geonames.get_serialized_data()
373
+
374
+ if (
375
+ "countryCode" not in self.city_data
376
+ or "timezonestr" not in self.city_data
377
+ or "lat" not in self.city_data
378
+ or "lng" not in self.city_data
379
+ ):
380
+ raise KerykeionException("No data found for this city, try again! Maybe check your connection?")
381
+
382
+ self.nation = self.city_data["countryCode"]
383
+ self.lng = float(self.city_data["lng"])
384
+ self.lat = float(self.city_data["lat"])
385
+ self.tz_str = self.city_data["timezonestr"]
386
+
387
+ def next_return_from_iso_formatted_time(
388
+ self,
389
+ iso_formatted_time: str,
390
+ return_type: ReturnType
391
+ ) -> PlanetReturnModel:
392
+ """
393
+ Calculate the next planetary return occurring after a specified ISO-formatted datetime.
394
+
395
+ This method computes the exact moment when the specified planet (Sun or Moon) returns
396
+ to its natal position, starting the search from the provided datetime. It uses precise
397
+ Swiss Ephemeris calculations to determine the exact return moment and generates a
398
+ complete astrological chart for that calculated time.
399
+
400
+ The calculation process:
401
+ 1. Converts the ISO datetime to Julian Day format for astronomical calculations
402
+ 2. Uses Swiss Ephemeris functions (solcross_ut/mooncross_ut) to find the exact
403
+ return moment when the planet reaches its natal degree and minute
404
+ 3. Creates a complete AstrologicalSubject instance for the calculated return time
405
+ 4. Returns a comprehensive PlanetReturnModel with all chart data
406
+
407
+ Args:
408
+ iso_formatted_time (str): Starting datetime in ISO format for the search.
409
+ Must be a valid ISO 8601 datetime string (e.g., "2024-01-15T10:30:00"
410
+ or "2024-01-15T10:30:00+00:00"). The method will find the next return
411
+ occurring after this moment.
412
+ return_type (ReturnType): Type of planetary return to calculate.
413
+ Must be either "Solar" for Sun returns or "Lunar" for Moon returns.
414
+ This determines which planet's return cycle to compute.
415
+
416
+ Returns:
417
+ PlanetReturnModel: A comprehensive Pydantic model containing complete
418
+ astrological chart data for the calculated return moment, including:
419
+ - Exact return datetime (UTC and local timezone)
420
+ - All planetary positions at the return moment
421
+ - House cusps and angles for the return location
422
+ - Complete astrological subject data with all calculated points
423
+ - Return type identifier and subject name
424
+ - Julian Day Number for the return moment
425
+
426
+ Raises:
427
+ KerykeionException: If return_type is not "Solar" or "Lunar".
428
+ ValueError: If iso_formatted_time is not a valid ISO datetime format.
429
+ SwissEphException: If Swiss Ephemeris calculations fail due to invalid
430
+ date ranges or astronomical calculation errors.
431
+
432
+ Examples:
433
+ Calculate next Solar Return after a specific date:
434
+
435
+ >>> factory = PlanetaryReturnFactory(subject, ...)
436
+ >>> solar_return = factory.next_return_from_iso_formatted_time(
437
+ ... "2024-06-15T12:00:00",
438
+ ... "Solar"
439
+ ... )
440
+ >>> print(f"Solar Return: {solar_return.iso_formatted_local_datetime}")
441
+ >>> print(f"Sun position: {solar_return.sun.abs_pos}°")
442
+
443
+ Calculate next Lunar Return with timezone:
444
+
445
+ >>> lunar_return = factory.next_return_from_iso_formatted_time(
446
+ ... "2024-01-01T00:00:00+00:00",
447
+ ... "Lunar"
448
+ ... )
449
+ >>> print(f"Moon return in {lunar_return.tz_str}")
450
+ >>> print(f"Return occurs: {lunar_return.iso_formatted_local_datetime}")
451
+
452
+ Access complete chart data from return:
453
+
454
+ >>> return_chart = factory.next_return_from_iso_formatted_time(
455
+ ... datetime.now().isoformat(),
456
+ ... "Solar"
457
+ ... )
458
+ >>> # Access all planetary positions
459
+ >>> for planet in return_chart.planets_list:
460
+ ... print(f"{planet.name}: {planet.abs_pos}° in {planet.sign}")
461
+ >>> # Access house cusps
462
+ >>> for house in return_chart.houses_list:
463
+ ... print(f"House {house.number}: {house.abs_pos}°")
464
+
465
+ Technical Notes:
466
+ - Solar returns typically occur within 1-2 days of the natal birthday
467
+ - Lunar returns occur approximately every 27.3 days (sidereal month)
468
+ - Return moments are calculated to the second for maximum precision
469
+ - The method accounts for leap years and varying orbital speeds
470
+ - Return charts use the factory's configured location, not the natal location
471
+
472
+ Use Cases:
473
+ - Annual birthday return chart calculations
474
+ - Monthly lunar return timing for astrological consultation
475
+ - Research into planetary cycle patterns and timing
476
+ - Forecasting and predictive astrology applications
477
+ - Educational demonstrations of astronomical cycles
478
+
479
+ See Also:
480
+ next_return_from_year(): Simplified interface for yearly calculations
481
+ next_return_from_month_and_year(): Monthly calculation interface
482
+ """
483
+
484
+ date = datetime.fromisoformat(iso_formatted_time)
485
+ julian_day = datetime_to_julian(date)
486
+
487
+ return_julian_date = None
488
+ if return_type == "Solar":
489
+ if self.subject.sun is None:
490
+ raise KerykeionException("Sun position is required for Solar return but is not available in the subject.")
491
+ return_julian_date = swe.solcross_ut(
492
+ self.subject.sun.abs_pos,
493
+ julian_day,
494
+ )
495
+ elif return_type == "Lunar":
496
+ if self.subject.moon is None:
497
+ raise KerykeionException("Moon position is required for Lunar return but is not available in the subject.")
498
+ return_julian_date = swe.mooncross_ut(
499
+ self.subject.moon.abs_pos,
500
+ julian_day,
501
+ )
502
+ else:
503
+ raise KerykeionException(f"Invalid return type {return_type}. Use 'Solar' or 'Lunar'.")
504
+
505
+ solar_return_date_utc = julian_to_datetime(return_julian_date)
506
+ solar_return_date_utc = solar_return_date_utc.replace(tzinfo=timezone.utc)
507
+
508
+ solar_return_astrological_subject = AstrologicalSubjectFactory.from_iso_utc_time(
509
+ name=self.subject.name,
510
+ iso_utc_time=solar_return_date_utc.isoformat(),
511
+ lng=self.lng, # type: ignore
512
+ lat=self.lat, # type: ignore
513
+ tz_str=self.tz_str, # type: ignore
514
+ city=self.city, # type: ignore
515
+ nation=self.nation, # type: ignore
516
+ online=False,
517
+ altitude=self.altitude,
518
+ active_points=self.subject.active_points,
519
+ )
520
+
521
+ model_data = solar_return_astrological_subject.model_dump()
522
+ model_data['name'] = f"{self.subject.name} {return_type} Return"
523
+ model_data['return_type'] = return_type
524
+
525
+ return PlanetReturnModel(
526
+ **model_data,
527
+ )
528
+
529
+ def next_return_from_year(
530
+ self,
531
+ year: int,
532
+ return_type: ReturnType
533
+ ) -> PlanetReturnModel:
534
+ """
535
+ Calculate the planetary return occurring within a specified year.
536
+
537
+ This is a convenience method that finds the first planetary return (Solar or Lunar)
538
+ that occurs in the given calendar year. It automatically searches from January 1st
539
+ of the specified year and returns the first return found, making it ideal for
540
+ annual forecasting and birthday return calculations.
541
+
542
+ For Solar Returns, this typically finds the return closest to the natal birthday
543
+ within that year. For Lunar Returns, it finds the first lunar return occurring
544
+ in January of the specified year.
545
+
546
+ The method internally uses next_return_from_iso_formatted_time() with a starting
547
+ point of January 1st at midnight UTC for the specified year.
548
+
549
+ Args:
550
+ year (int): The calendar year to search for the return. Must be a valid
551
+ year (typically between 1800-2200 for reliable ephemeris data).
552
+ Examples: 2024, 2025, 1990, 2050.
553
+ return_type (ReturnType): The type of planetary return to calculate.
554
+ Must be either "Solar" for Sun returns or "Lunar" for Moon returns.
555
+
556
+ Returns:
557
+ PlanetReturnModel: A comprehensive model containing the return chart data
558
+ for the first return found in the specified year. Includes:
559
+ - Exact return datetime in both UTC and local timezone
560
+ - Complete planetary positions at the return moment
561
+ - House cusps calculated for the factory's configured location
562
+ - All astrological chart features and calculated points
563
+ - Return type and subject identification
564
+
565
+ Raises:
566
+ KerykeionException: If return_type is not "Solar" or "Lunar".
567
+ ValueError: If year is outside the valid range for ephemeris calculations.
568
+ SwissEphException: If astronomical calculations fail for the given year.
569
+
570
+ Examples:
571
+ Calculate Solar Return for 2024:
572
+
573
+ >>> factory = PlanetaryReturnFactory(subject, ...)
574
+ >>> solar_return_2024 = factory.next_return_from_year(2024, "Solar")
575
+ >>> print(f"2024 Solar Return: {solar_return_2024.iso_formatted_local_datetime}")
576
+ >>> print(f"Birthday location: {solar_return_2024.city}, {solar_return_2024.nation}")
577
+
578
+ Calculate first Lunar Return of 2025:
579
+
580
+ >>> lunar_return = factory.next_return_from_year(2025, "Lunar")
581
+ >>> print(f"First 2025 Lunar Return: {lunar_return.iso_formatted_local_datetime}")
582
+
583
+ Compare multiple years:
584
+
585
+ >>> for year in [2023, 2024, 2025]:
586
+ ... solar_return = factory.next_return_from_year(year, "Solar")
587
+ ... print(f"{year}: {solar_return.iso_formatted_local_datetime}")
588
+
589
+ Practical Applications:
590
+ - Annual Solar Return chart casting for birthday forecasting
591
+ - Comparative analysis of return charts across multiple years
592
+ - Research into planetary return timing patterns
593
+ - Automated birthday return calculations for consultation
594
+ - Educational demonstrations of annual astrological cycles
595
+
596
+ Technical Notes:
597
+ - Solar returns in a given year occur near but not exactly on the birthday
598
+ - The exact date can vary by 1-2 days due to leap years and orbital mechanics
599
+ - Lunar returns occur approximately every 27.3 days throughout the year
600
+ - This method finds the chronologically first return in the year
601
+ - Return moment precision is calculated to the second
602
+
603
+ Use Cases:
604
+ - Birthday return chart interpretation
605
+ - Annual astrological forecasting
606
+ - Timing analysis for major life events
607
+ - Comparative return chart studies
608
+ - Astrological consultation preparation
609
+
610
+ See Also:
611
+ next_return_from_month_and_year(): For more specific monthly searches
612
+ next_return_from_iso_formatted_time(): For custom starting dates
613
+ """
614
+ # Create datetime for January 1st of the specified year (UTC)
615
+ start_date = datetime(year, 1, 1, 0, 0, tzinfo=timezone.utc)
616
+
617
+ # Get the return using the existing method
618
+ return self.next_return_from_iso_formatted_time(
619
+ start_date.isoformat(),
620
+ return_type
621
+ )
622
+
623
+ def next_return_from_month_and_year(
624
+ self,
625
+ year: int,
626
+ month: int,
627
+ return_type: ReturnType
628
+ ) -> PlanetReturnModel:
629
+ """
630
+ Calculate the first planetary return occurring in or after a specified month and year.
631
+
632
+ This method provides precise timing control for planetary return calculations by
633
+ searching from the first day of a specific month and year. It's particularly
634
+ useful for finding Lunar Returns in specific months or for Solar Return timing
635
+ when you need to focus on a particular time period within a year.
636
+
637
+ The method searches from the first moment (00:00:00 UTC) of the specified month
638
+ and year, finding the next return that occurs from that point forward. This is
639
+ especially valuable for Lunar Return work, where multiple returns occur per year
640
+ and you need to isolate specific monthly periods.
641
+
642
+ Args:
643
+ year (int): The calendar year to search within. Must be a valid year
644
+ within the ephemeris data range (typically 1800-2200).
645
+ Examples: 2024, 2025, 1990.
646
+ month (int): The month to start the search from. Must be between 1 and 12,
647
+ where 1=January, 2=February, ..., 12=December.
648
+ return_type (ReturnType): The type of planetary return to calculate.
649
+ Must be either "Solar" for Sun returns or "Lunar" for Moon returns.
650
+
651
+ Returns:
652
+ PlanetReturnModel: Comprehensive return chart data for the first return
653
+ found on or after the first day of the specified month and year.
654
+ Contains complete astrological chart information including:
655
+ - Precise return datetime in UTC and local timezone
656
+ - All planetary positions at the return moment
657
+ - House cusps for the factory's configured location
658
+ - Complete astrological subject data with all calculated features
659
+ - Return type identifier and naming information
660
+
661
+ Raises:
662
+ KerykeionException: If month is not between 1 and 12.
663
+ KerykeionException: If return_type is not "Solar" or "Lunar".
664
+ ValueError: If year is outside valid ephemeris calculation range.
665
+ SwissEphException: If astronomical calculations fail.
666
+
667
+ Examples:
668
+ Find Solar Return in birth month:
669
+
670
+ >>> factory = PlanetaryReturnFactory(subject, ...)
671
+ >>> # Subject born in June, find 2024 Solar Return in June
672
+ >>> solar_return = factory.next_return_from_month_and_year(
673
+ ... 2024, 6, "Solar"
674
+ ... )
675
+ >>> print(f"Solar Return: {solar_return.iso_formatted_local_datetime}")
676
+
677
+ Find specific Lunar Return:
678
+
679
+ >>> # Find first Lunar Return in March 2024
680
+ >>> lunar_return = factory.next_return_from_month_and_year(
681
+ ... 2024, 3, "Lunar"
682
+ ... )
683
+ >>> print(f"March 2024 Lunar Return: {lunar_return.iso_formatted_local_datetime}")
684
+
685
+ Monthly Lunar Return tracking:
686
+
687
+ >>> lunar_returns_2024 = []
688
+ >>> for month in range(1, 13):
689
+ ... lunar_return = factory.next_return_from_month_and_year(
690
+ ... 2024, month, "Lunar"
691
+ ... )
692
+ ... lunar_returns_2024.append(lunar_return)
693
+ ... print(f"Month {month}: {lunar_return.iso_formatted_local_datetime}")
694
+
695
+ Seasonal analysis:
696
+
697
+ >>> # Spring Solar Return (if birthday is in spring)
698
+ >>> spring_return = factory.next_return_from_month_and_year(
699
+ ... 2024, 3, "Solar"
700
+ ... )
701
+ >>> # Compare with autumn energy
702
+ >>> autumn_lunar = factory.next_return_from_month_and_year(
703
+ ... 2024, 9, "Lunar"
704
+ ... )
705
+
706
+ Practical Applications:
707
+ - Monthly Lunar Return consultation scheduling
708
+ - Seasonal astrological analysis and timing
709
+ - Comparative study of returns across different months
710
+ - Precise timing for astrological interventions
711
+ - Educational demonstrations of monthly astrological cycles
712
+ - Research into seasonal patterns in planetary returns
713
+
714
+ Technical Notes:
715
+ - Search begins at 00:00:00 UTC on the 1st day of the specified month
716
+ - For Solar Returns, may find the return in a subsequent month if
717
+ the birthday falls late in the specified month of the previous year
718
+ - Lunar Returns typically occur within the specified month due to
719
+ their ~27-day cycle
720
+ - Month validation prevents common input errors
721
+ - All calculations maintain second-level precision
722
+
723
+ Timing Considerations:
724
+ - Solar Returns: Usually occur within 1-2 days of the natal birthday
725
+ - Lunar Returns: Occur approximately every 27.3 days
726
+ - The method finds the chronologically first return from the start date
727
+ - Timezone differences can affect which calendar day the return occurs
728
+
729
+ Use Cases:
730
+ - Monthly return chart consultations
731
+ - Timing specific astrological work or rituals
732
+ - Research into monthly astrological patterns
733
+ - Educational calendar planning for astrological courses
734
+ - Comparative return chart analysis
735
+
736
+ See Also:
737
+ next_return_from_year(): For annual return calculations
738
+ next_return_from_iso_formatted_time(): For custom date searches
739
+ """
740
+ # Validate month input
741
+ if month < 1 or month > 12:
742
+ raise KerykeionException(f"Invalid month {month}. Month must be between 1 and 12.")
743
+
744
+ # Create datetime for the first day of the specified month and year (UTC)
745
+ start_date = datetime(year, month, 1, 0, 0, tzinfo=timezone.utc)
746
+
747
+ # Get the return using the existing method
748
+ return self.next_return_from_iso_formatted_time(
749
+ start_date.isoformat(),
750
+ return_type
751
+ )
752
+
753
+
754
+ if __name__ == "__main__":
755
+ import json
756
+ # Example usage
757
+ subject = AstrologicalSubjectFactory.from_birth_data(
758
+ name="Test Subject",
759
+ lng=-122.4194,
760
+ lat=37.7749,
761
+ tz_str="America/Los_Angeles",
762
+ )
763
+
764
+ print("=== Planet Return Calculator ===")
765
+ calculator = PlanetaryReturnFactory(
766
+ subject,
767
+ city="San Francisco",
768
+ nation="USA",
769
+ online=True,
770
+ geonames_username="century.boy",
771
+ )
772
+ date = datetime(2026, 1, 1, 0, 0, tzinfo=timezone.utc)
773
+ print(f"INITIAL DATE: {date.isoformat()}")
774
+ print(f"INITIAL DATE JULIAN: {datetime_to_julian(date)}")
775
+ print(f"INITIAL DATE REVERSED: {julian_to_datetime(datetime_to_julian(date)).isoformat()}")
776
+ solar_return = calculator.next_return_from_iso_formatted_time(
777
+ date.isoformat(),
778
+ return_type="Lunar",
779
+ )
780
+ print("--- After ---")
781
+ print(f"Solar Return Date UTC: {solar_return.iso_formatted_utc_datetime}")
782
+ print(f"Solar Return Date Local: {solar_return.iso_formatted_local_datetime}")
783
+ print(f"Solar Return JSON: {json.dumps(solar_return.model_dump(), indent=4)}")
784
+ print(f"Solar Return Julian Data: {solar_return.julian_day}")
785
+ print(f"ISO UTC: {solar_return.iso_formatted_utc_datetime}")
786
+
787
+ ## From Year
788
+ print("=== Planet Return Calculator ===")
789
+ solar_return = calculator.next_return_from_year(
790
+ 2026,
791
+ return_type="Lunar",
792
+ )
793
+ print("--- From Year ---")
794
+ print(f"Solar Return Julian Data: {solar_return.julian_day}")
795
+ print(f"Solar Return Date UTC: {solar_return.iso_formatted_utc_datetime}")
796
+ ## From Month and Year
797
+ print("=== Planet Return Calculator ===")
798
+ solar_return = calculator.next_return_from_month_and_year(
799
+ 2026,
800
+ 1,
801
+ return_type="Lunar",
802
+ )
803
+ print("--- From Month and Year ---")
804
+ print(f"Solar Return Julian Data: {solar_return.julian_day}")
805
+ print(f"Solar Return Date UTC: {solar_return.iso_formatted_utc_datetime}")