kerykeion 5.0.0a9__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.
- kerykeion/__init__.py +50 -9
- kerykeion/aspects/__init__.py +5 -2
- kerykeion/aspects/aspects_factory.py +568 -0
- kerykeion/aspects/aspects_utils.py +78 -11
- kerykeion/astrological_subject_factory.py +1032 -275
- kerykeion/backword.py +820 -0
- kerykeion/chart_data_factory.py +552 -0
- kerykeion/charts/chart_drawer.py +2661 -0
- kerykeion/charts/charts_utils.py +652 -399
- kerykeion/charts/draw_planets.py +603 -353
- kerykeion/charts/templates/aspect_grid_only.xml +326 -198
- kerykeion/charts/templates/chart.xml +306 -256
- kerykeion/charts/templates/wheel_only.xml +330 -200
- kerykeion/charts/themes/black-and-white.css +148 -0
- kerykeion/charts/themes/classic.css +11 -0
- kerykeion/charts/themes/dark-high-contrast.css +11 -0
- kerykeion/charts/themes/dark.css +11 -0
- kerykeion/charts/themes/light.css +11 -0
- kerykeion/charts/themes/strawberry.css +10 -0
- kerykeion/composite_subject_factory.py +232 -13
- kerykeion/ephemeris_data_factory.py +443 -0
- kerykeion/fetch_geonames.py +78 -21
- kerykeion/house_comparison/__init__.py +4 -1
- kerykeion/house_comparison/house_comparison_factory.py +52 -19
- kerykeion/house_comparison/house_comparison_utils.py +37 -9
- kerykeion/kr_types/__init__.py +66 -6
- kerykeion/kr_types/chart_template_model.py +20 -0
- kerykeion/kr_types/kerykeion_exception.py +15 -9
- kerykeion/kr_types/kr_literals.py +14 -160
- kerykeion/kr_types/kr_models.py +14 -291
- kerykeion/kr_types/settings_models.py +15 -167
- kerykeion/planetary_return_factory.py +545 -40
- kerykeion/relationship_score_factory.py +137 -63
- kerykeion/report.py +749 -64
- kerykeion/schemas/__init__.py +106 -0
- kerykeion/schemas/chart_template_model.py +367 -0
- kerykeion/schemas/kerykeion_exception.py +20 -0
- kerykeion/schemas/kr_literals.py +181 -0
- kerykeion/schemas/kr_models.py +603 -0
- kerykeion/schemas/settings_models.py +188 -0
- kerykeion/settings/__init__.py +20 -1
- kerykeion/settings/chart_defaults.py +444 -0
- kerykeion/settings/config_constants.py +88 -12
- kerykeion/settings/kerykeion_settings.py +32 -75
- kerykeion/settings/translation_strings.py +1499 -0
- kerykeion/settings/translations.py +74 -0
- kerykeion/sweph/ast136/s136108s.se1 +0 -0
- kerykeion/sweph/ast136/s136199s.se1 +0 -0
- kerykeion/sweph/ast136/s136472s.se1 +0 -0
- kerykeion/sweph/ast28/se28978s.se1 +0 -0
- kerykeion/sweph/ast50/se50000s.se1 +0 -0
- kerykeion/sweph/ast90/se90377s.se1 +0 -0
- kerykeion/sweph/ast90/se90482s.se1 +0 -0
- kerykeion/sweph/sefstars.txt +1602 -0
- kerykeion/transits_time_range_factory.py +302 -0
- kerykeion/utilities.py +289 -204
- kerykeion-5.1.8.dist-info/METADATA +1793 -0
- kerykeion-5.1.8.dist-info/RECORD +63 -0
- kerykeion/aspects/natal_aspects.py +0 -181
- kerykeion/aspects/synastry_aspects.py +0 -141
- kerykeion/aspects/transits_time_range.py +0 -41
- kerykeion/charts/draw_planets_v2.py +0 -649
- kerykeion/charts/draw_planets_v3.py +0 -679
- kerykeion/charts/kerykeion_chart_svg.py +0 -2038
- kerykeion/enums.py +0 -57
- kerykeion/ephemeris_data.py +0 -238
- kerykeion/house_comparison/house_comparison_models.py +0 -38
- kerykeion/kr_types/chart_types.py +0 -106
- kerykeion/settings/kr.config.json +0 -1304
- kerykeion/settings/legacy/__init__.py +0 -0
- kerykeion/settings/legacy/legacy_celestial_points_settings.py +0 -299
- kerykeion/settings/legacy/legacy_chart_aspects_settings.py +0 -71
- kerykeion/settings/legacy/legacy_color_settings.py +0 -42
- kerykeion/transits_time_range.py +0 -128
- kerykeion-5.0.0a9.dist-info/METADATA +0 -636
- kerykeion-5.0.0a9.dist-info/RECORD +0 -55
- kerykeion-5.0.0a9.dist-info/entry_points.txt +0 -2
- {kerykeion-5.0.0a9.dist-info → kerykeion-5.1.8.dist-info}/WHEEL +0 -0
- {kerykeion-5.0.0a9.dist-info → kerykeion-5.1.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,28 +1,51 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""
|
|
3
|
-
|
|
3
|
+
Astrological Subject Factory Module
|
|
4
|
+
|
|
5
|
+
This module provides factory classes for creating astrological subjects with comprehensive
|
|
6
|
+
astrological calculations including planetary positions, house cusps, aspects, and various
|
|
7
|
+
astrological points.
|
|
8
|
+
|
|
9
|
+
The main factory class AstrologicalSubjectFactory offers multiple creation methods for
|
|
10
|
+
different initialization scenarios, supporting both online and offline calculation modes,
|
|
11
|
+
various zodiac systems (Tropical/Sidereal), house systems, and coordinate perspectives.
|
|
12
|
+
|
|
13
|
+
Key Features:
|
|
14
|
+
- Planetary position calculations for all traditional and modern planets
|
|
15
|
+
- House cusp calculations with multiple house systems
|
|
16
|
+
- Lunar nodes, Lilith points, asteroids, and trans-Neptunian objects
|
|
17
|
+
- Arabic parts (lots) calculations
|
|
18
|
+
- Fixed star positions
|
|
19
|
+
- Automatic location data fetching via GeoNames API
|
|
20
|
+
- Comprehensive timezone and coordinate handling
|
|
21
|
+
- Flexible point selection for performance optimization
|
|
22
|
+
|
|
23
|
+
Classes:
|
|
24
|
+
ChartConfiguration: Configuration settings for astrological calculations
|
|
25
|
+
LocationData: Geographical location information and utilities
|
|
26
|
+
AstrologicalSubjectFactory: Main factory for creating astrological subjects
|
|
27
|
+
|
|
28
|
+
Author: Giacomo Battaglia
|
|
29
|
+
Copyright: (C) 2025 Kerykeion Project
|
|
30
|
+
License: AGPL-3.0
|
|
4
31
|
"""
|
|
5
32
|
|
|
6
33
|
import pytz
|
|
7
34
|
import swisseph as swe
|
|
8
35
|
import logging
|
|
9
|
-
import warnings
|
|
10
36
|
import math
|
|
11
37
|
from datetime import datetime
|
|
12
38
|
from pathlib import Path
|
|
13
|
-
from typing import
|
|
14
|
-
from functools import cached_property, lru_cache
|
|
39
|
+
from typing import Optional, List, Dict, Any, get_args, cast
|
|
15
40
|
from dataclasses import dataclass, field
|
|
16
|
-
from
|
|
41
|
+
from contextlib import contextmanager
|
|
17
42
|
|
|
18
43
|
|
|
19
44
|
from kerykeion.fetch_geonames import FetchGeonames
|
|
20
|
-
from kerykeion.
|
|
45
|
+
from kerykeion.schemas import (
|
|
21
46
|
KerykeionException,
|
|
22
47
|
ZodiacType,
|
|
23
48
|
AstrologicalSubjectModel,
|
|
24
|
-
LunarPhaseModel,
|
|
25
|
-
KerykeionPointModel,
|
|
26
49
|
PointType,
|
|
27
50
|
SiderealMode,
|
|
28
51
|
HousesSystemIdentifier,
|
|
@@ -31,13 +54,13 @@ from kerykeion.kr_types import (
|
|
|
31
54
|
Houses,
|
|
32
55
|
)
|
|
33
56
|
from kerykeion.utilities import (
|
|
34
|
-
get_number_from_name,
|
|
35
57
|
get_kerykeion_point_from_degree,
|
|
36
58
|
get_planet_house,
|
|
37
59
|
check_and_adjust_polar_latitude,
|
|
38
60
|
calculate_moon_phase,
|
|
39
61
|
datetime_to_julian,
|
|
40
|
-
get_house_number
|
|
62
|
+
get_house_number,
|
|
63
|
+
normalize_zodiac_type,
|
|
41
64
|
)
|
|
42
65
|
from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS
|
|
43
66
|
|
|
@@ -45,7 +68,7 @@ from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS
|
|
|
45
68
|
DEFAULT_GEONAMES_USERNAME = "century.boy"
|
|
46
69
|
DEFAULT_SIDEREAL_MODE: SiderealMode = "FAGAN_BRADLEY"
|
|
47
70
|
DEFAULT_HOUSES_SYSTEM_IDENTIFIER: HousesSystemIdentifier = "P"
|
|
48
|
-
DEFAULT_ZODIAC_TYPE: ZodiacType = "
|
|
71
|
+
DEFAULT_ZODIAC_TYPE: ZodiacType = "Tropical"
|
|
49
72
|
DEFAULT_PERSPECTIVE_TYPE: PerspectiveType = "Apparent Geocentric"
|
|
50
73
|
DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS = 30
|
|
51
74
|
|
|
@@ -60,28 +83,133 @@ GEONAMES_DEFAULT_USERNAME_WARNING = (
|
|
|
60
83
|
"********"
|
|
61
84
|
)
|
|
62
85
|
|
|
63
|
-
|
|
64
|
-
|
|
86
|
+
@contextmanager
|
|
87
|
+
def ephemeris_context(
|
|
88
|
+
ephe_path: str,
|
|
89
|
+
config: "ChartConfiguration",
|
|
90
|
+
lng: float,
|
|
91
|
+
lat: float,
|
|
92
|
+
alt: Optional[float] = None,
|
|
93
|
+
):
|
|
94
|
+
"""Context manager that isolates Swiss Ephemeris configuration.
|
|
95
|
+
|
|
96
|
+
Responsibilities:
|
|
97
|
+
- Set ephemeris path and calculation flags
|
|
98
|
+
- Configure perspective (true geo / helio / topo)
|
|
99
|
+
- Configure sidereal mode when needed
|
|
100
|
+
- Apply topocentric observer only inside the with-block
|
|
101
|
+
- Yield iflag for calculations
|
|
102
|
+
- Reset topocentric coordinates afterward (defensive)
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
ephe_path: Path containing Swiss Ephemeris data files.
|
|
106
|
+
config: Validated chart configuration.
|
|
107
|
+
lng: Observer longitude (used for topocentric charts).
|
|
108
|
+
lat: Observer latitude (used for topocentric charts).
|
|
109
|
+
alt: Observer altitude (meters) for topocentric charts.
|
|
110
|
+
|
|
111
|
+
Yields:
|
|
112
|
+
int: iflag to be passed to swe.calc_ut / swe.fixstar_ut.
|
|
113
|
+
"""
|
|
114
|
+
swe.set_ephe_path(ephe_path)
|
|
115
|
+
iflag = swe.FLG_SWIEPH | swe.FLG_SPEED
|
|
116
|
+
|
|
117
|
+
topo_used = False
|
|
118
|
+
|
|
119
|
+
# Perspective configuration
|
|
120
|
+
if config.perspective_type == "True Geocentric":
|
|
121
|
+
iflag |= swe.FLG_TRUEPOS
|
|
122
|
+
elif config.perspective_type == "Heliocentric":
|
|
123
|
+
iflag |= swe.FLG_HELCTR
|
|
124
|
+
elif config.perspective_type == "Topocentric":
|
|
125
|
+
iflag |= swe.FLG_TOPOCTR
|
|
126
|
+
swe.set_topo(lng, lat, alt or 0.0)
|
|
127
|
+
topo_used = True
|
|
128
|
+
|
|
129
|
+
# Sidereal configuration
|
|
130
|
+
if config.zodiac_type == "Sidereal":
|
|
131
|
+
iflag |= swe.FLG_SIDEREAL
|
|
132
|
+
swe.set_sid_mode(getattr(swe, f"SIDM_{config.sidereal_mode}"))
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
yield iflag
|
|
136
|
+
finally:
|
|
137
|
+
# Defensive cleanup: reset topo if it was set
|
|
138
|
+
if topo_used:
|
|
139
|
+
swe.set_topo(0.0, 0.0, 0.0)
|
|
65
140
|
|
|
66
141
|
@dataclass
|
|
67
142
|
class ChartConfiguration:
|
|
68
|
-
"""
|
|
143
|
+
"""
|
|
144
|
+
Configuration settings for astrological chart calculations.
|
|
145
|
+
|
|
146
|
+
This class encapsulates all the configuration parameters needed for astrological
|
|
147
|
+
calculations, including zodiac type, coordinate systems, house systems, and
|
|
148
|
+
calculation perspectives. It provides validation to ensure compatible settings
|
|
149
|
+
combinations.
|
|
150
|
+
|
|
151
|
+
Attributes:
|
|
152
|
+
zodiac_type (ZodiacType): The zodiac system to use ('Tropical' or 'Sidereal').
|
|
153
|
+
Defaults to 'Tropical'.
|
|
154
|
+
sidereal_mode (Optional[SiderealMode]): The sidereal calculation mode when using
|
|
155
|
+
sidereal zodiac. Only required/used when zodiac_type is 'Sidereal'.
|
|
156
|
+
Defaults to None (auto-set to FAGAN_BRADLEY for sidereal).
|
|
157
|
+
houses_system_identifier (HousesSystemIdentifier): The house system to use for
|
|
158
|
+
house cusp calculations. Defaults to 'P' (Placidus).
|
|
159
|
+
perspective_type (PerspectiveType): The coordinate perspective for calculations.
|
|
160
|
+
Options include 'Apparent Geocentric', 'True Geocentric', 'Heliocentric',
|
|
161
|
+
or 'Topocentric'. Defaults to 'Apparent Geocentric'.
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
KerykeionException: When invalid configuration combinations are detected,
|
|
165
|
+
such as setting sidereal_mode with tropical zodiac, or using invalid
|
|
166
|
+
enumeration values.
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
>>> config = ChartConfiguration(
|
|
170
|
+
... zodiac_type="Sidereal",
|
|
171
|
+
... sidereal_mode="LAHIRI",
|
|
172
|
+
... houses_system_identifier="K",
|
|
173
|
+
... perspective_type="Topocentric"
|
|
174
|
+
... )
|
|
175
|
+
"""
|
|
69
176
|
zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE
|
|
70
177
|
sidereal_mode: Optional[SiderealMode] = None
|
|
71
178
|
houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER
|
|
72
179
|
perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE
|
|
73
180
|
|
|
181
|
+
def __post_init__(self) -> None:
|
|
182
|
+
self.validate()
|
|
183
|
+
|
|
74
184
|
def validate(self) -> None:
|
|
75
|
-
"""
|
|
185
|
+
"""
|
|
186
|
+
Validate configuration settings for internal consistency.
|
|
187
|
+
|
|
188
|
+
Performs comprehensive validation of all configuration parameters to ensure
|
|
189
|
+
they form a valid, compatible combination. This includes checking enumeration
|
|
190
|
+
values, zodiac/sidereal mode compatibility, and setting defaults where needed.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
KerykeionException: If any configuration parameter is invalid or if
|
|
194
|
+
incompatible parameter combinations are detected.
|
|
195
|
+
|
|
196
|
+
Side Effects:
|
|
197
|
+
- Sets default sidereal_mode to FAGAN_BRADLEY if zodiac_type is Sidereal
|
|
198
|
+
and no sidereal_mode is specified
|
|
199
|
+
- Logs informational message when setting default sidereal mode
|
|
200
|
+
"""
|
|
76
201
|
# Validate zodiac type
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
)
|
|
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
|
|
81
209
|
|
|
82
210
|
# Validate sidereal mode settings
|
|
83
|
-
if self.sidereal_mode and self.zodiac_type == "
|
|
84
|
-
raise KerykeionException("You can't set a sidereal mode with a
|
|
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!")
|
|
85
213
|
|
|
86
214
|
if self.zodiac_type == "Sidereal":
|
|
87
215
|
if not self.sidereal_mode:
|
|
@@ -107,7 +235,39 @@ class ChartConfiguration:
|
|
|
107
235
|
|
|
108
236
|
@dataclass
|
|
109
237
|
class LocationData:
|
|
110
|
-
"""
|
|
238
|
+
"""
|
|
239
|
+
Information about a geographical location for astrological calculations.
|
|
240
|
+
|
|
241
|
+
This class handles all location-related data including coordinates, timezone
|
|
242
|
+
information, and interaction with the GeoNames API for automatic location
|
|
243
|
+
data retrieval. It provides methods for fetching location data online and
|
|
244
|
+
preparing coordinates for astrological calculations.
|
|
245
|
+
|
|
246
|
+
Attributes:
|
|
247
|
+
city (str): Name of the city or location. Defaults to "Greenwich".
|
|
248
|
+
nation (str): ISO country code (2-letter). Defaults to "GB" (United Kingdom).
|
|
249
|
+
lat (float): Latitude in decimal degrees. Positive for North, negative for South.
|
|
250
|
+
Defaults to 51.5074 (Greenwich).
|
|
251
|
+
lng (float): Longitude in decimal degrees. Positive for East, negative for West.
|
|
252
|
+
Defaults to 0.0 (Greenwich).
|
|
253
|
+
tz_str (str): IANA timezone identifier (e.g., 'Europe/London', 'America/New_York').
|
|
254
|
+
Defaults to "Etc/GMT".
|
|
255
|
+
altitude (Optional[float]): Altitude above sea level in meters. Used for
|
|
256
|
+
topocentric calculations. Defaults to None (sea level assumed).
|
|
257
|
+
city_data (Dict[str, str]): Raw data retrieved from GeoNames API. Used internally
|
|
258
|
+
for caching and validation. Defaults to empty dictionary.
|
|
259
|
+
|
|
260
|
+
Note:
|
|
261
|
+
When using online mode, the initial coordinate and timezone values may be
|
|
262
|
+
overridden by data fetched from the GeoNames API based on city and nation.
|
|
263
|
+
For polar regions, latitude values are automatically adjusted to prevent
|
|
264
|
+
calculation errors.
|
|
265
|
+
|
|
266
|
+
Example:
|
|
267
|
+
>>> location = LocationData(city="Rome", nation="IT")
|
|
268
|
+
>>> location.fetch_from_geonames("your_username", 30)
|
|
269
|
+
>>> location.prepare_for_calculation()
|
|
270
|
+
"""
|
|
111
271
|
city: str = "Greenwich"
|
|
112
272
|
nation: str = "GB"
|
|
113
273
|
lat: float = 51.5074
|
|
@@ -119,7 +279,32 @@ class LocationData:
|
|
|
119
279
|
city_data: Dict[str, str] = field(default_factory=dict)
|
|
120
280
|
|
|
121
281
|
def fetch_from_geonames(self, username: str, cache_expire_after_days: int) -> None:
|
|
122
|
-
"""
|
|
282
|
+
"""
|
|
283
|
+
Fetch location data from GeoNames API.
|
|
284
|
+
|
|
285
|
+
Retrieves accurate coordinates, timezone, and country code information
|
|
286
|
+
for the specified city and country from the GeoNames web service.
|
|
287
|
+
Updates the instance attributes with the fetched data.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
username (str): GeoNames API username. Must be registered at geonames.org.
|
|
291
|
+
Free accounts are limited to 2000 requests per hour.
|
|
292
|
+
cache_expire_after_days (int): Number of days to cache the location data
|
|
293
|
+
locally before refreshing from the API.
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
KerykeionException: If required data is missing from the API response,
|
|
297
|
+
typically due to network issues, invalid location names, or API limits.
|
|
298
|
+
|
|
299
|
+
Side Effects:
|
|
300
|
+
- Updates city_data with raw API response
|
|
301
|
+
- Updates nation, lng, lat, and tz_str with fetched values
|
|
302
|
+
- May create or update local cache files
|
|
303
|
+
|
|
304
|
+
Note:
|
|
305
|
+
The method validates that all required fields (countryCode, timezonestr,
|
|
306
|
+
lat, lng) are present in the API response before updating instance attributes.
|
|
307
|
+
"""
|
|
123
308
|
logging.info(f"Fetching timezone/coordinates for {self.city}, {self.nation} from geonames")
|
|
124
309
|
|
|
125
310
|
geonames = FetchGeonames(
|
|
@@ -148,29 +333,101 @@ class LocationData:
|
|
|
148
333
|
self.tz_str = self.city_data["timezonestr"]
|
|
149
334
|
|
|
150
335
|
def prepare_for_calculation(self) -> None:
|
|
151
|
-
"""
|
|
336
|
+
"""
|
|
337
|
+
Prepare location data for astrological calculations.
|
|
338
|
+
|
|
339
|
+
Performs final adjustments to location data to ensure compatibility
|
|
340
|
+
with Swiss Ephemeris calculations. This includes handling special cases
|
|
341
|
+
like polar regions where extreme latitudes can cause calculation errors.
|
|
342
|
+
|
|
343
|
+
Side Effects:
|
|
344
|
+
- Adjusts latitude values for polar regions (beyond ±66.5°) to
|
|
345
|
+
prevent Swiss Ephemeris calculation failures
|
|
346
|
+
- May log warnings about latitude adjustments
|
|
347
|
+
|
|
348
|
+
Note:
|
|
349
|
+
This method should be called after all location data has been set,
|
|
350
|
+
either manually or via fetch_from_geonames(), and before performing
|
|
351
|
+
any astrological calculations.
|
|
352
|
+
"""
|
|
152
353
|
# Adjust latitude for polar regions
|
|
153
354
|
self.lat = check_and_adjust_polar_latitude(self.lat)
|
|
154
355
|
|
|
155
356
|
|
|
156
357
|
class AstrologicalSubjectFactory:
|
|
157
358
|
"""
|
|
158
|
-
Factory class for creating astrological subjects
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
multiple
|
|
359
|
+
Factory class for creating comprehensive astrological subjects.
|
|
360
|
+
|
|
361
|
+
This factory creates AstrologicalSubjectModel instances with complete astrological
|
|
362
|
+
information including planetary positions, house cusps, aspects, lunar phases, and
|
|
363
|
+
various specialized astrological points. It provides multiple class methods for
|
|
364
|
+
different initialization scenarios and supports both online and offline calculation modes.
|
|
365
|
+
|
|
366
|
+
The factory handles complex astrological calculations using the Swiss Ephemeris library,
|
|
367
|
+
supports multiple coordinate systems and house systems, and can automatically fetch
|
|
368
|
+
location data from online sources.
|
|
369
|
+
|
|
370
|
+
Supported Astrological Points:
|
|
371
|
+
- Traditional Planets: Sun through Pluto
|
|
372
|
+
- Lunar Nodes: Mean and True North/South Nodes
|
|
373
|
+
- Lilith Points: Mean and True Black Moon
|
|
374
|
+
- Asteroids: Ceres, Pallas, Juno, Vesta
|
|
375
|
+
- Centaurs: Chiron, Pholus
|
|
376
|
+
- Trans-Neptunian Objects: Eris, Sedna, Haumea, Makemake, Ixion, Orcus, Quaoar
|
|
377
|
+
- Fixed Stars: Regulus, Spica (extensible)
|
|
378
|
+
- Arabic Parts: Pars Fortunae, Pars Spiritus, Pars Amoris, Pars Fidei
|
|
379
|
+
- Special Points: Vertex, Anti-Vertex, Earth (for heliocentric charts)
|
|
380
|
+
- House Cusps: All 12 houses with configurable house systems
|
|
381
|
+
- Angles: Ascendant, Medium Coeli, Descendant, Imum Coeli
|
|
382
|
+
|
|
383
|
+
Supported Features:
|
|
384
|
+
- Multiple zodiac systems (Tropical/Sidereal with various ayanamshas)
|
|
385
|
+
- Multiple house systems (Placidus, Koch, Equal, Whole Sign, etc.)
|
|
386
|
+
- Multiple coordinate perspectives (Geocentric, Heliocentric, Topocentric)
|
|
387
|
+
- Automatic timezone and coordinate resolution via GeoNames API
|
|
388
|
+
- Lunar phase calculations
|
|
389
|
+
- Day/night chart detection for Arabic parts
|
|
390
|
+
- Performance optimization through selective point calculation
|
|
391
|
+
- Comprehensive error handling and validation
|
|
392
|
+
|
|
393
|
+
Class Methods:
|
|
394
|
+
from_birth_data: Create subject from standard birth data (most flexible)
|
|
395
|
+
from_iso_utc_time: Create subject from ISO UTC timestamp
|
|
396
|
+
from_current_time: Create subject for current moment
|
|
397
|
+
|
|
398
|
+
Example:
|
|
399
|
+
>>> # Create natal chart
|
|
400
|
+
>>> subject = AstrologicalSubjectFactory.from_birth_data(
|
|
401
|
+
... name="John Doe",
|
|
402
|
+
... year=1990, month=6, day=15,
|
|
403
|
+
... hour=14, minute=30,
|
|
404
|
+
... city="Rome", nation="IT",
|
|
405
|
+
... online=True
|
|
406
|
+
... )
|
|
407
|
+
>>> print(f"Sun: {subject.sun.sign} {subject.sun.abs_pos}°")
|
|
408
|
+
>>> print(f"Active points: {len(subject.active_points)}")
|
|
409
|
+
|
|
410
|
+
>>> # Create chart for current time
|
|
411
|
+
>>> now_subject = AstrologicalSubjectFactory.from_current_time(
|
|
412
|
+
... name="Current Moment",
|
|
413
|
+
... city="London", nation="GB"
|
|
414
|
+
... )
|
|
415
|
+
|
|
416
|
+
Thread Safety:
|
|
417
|
+
This factory is not thread-safe due to its use of the Swiss Ephemeris library
|
|
418
|
+
which maintains global state. Use separate instances in multi-threaded applications
|
|
419
|
+
or implement appropriate locking mechanisms.
|
|
163
420
|
"""
|
|
164
421
|
|
|
165
422
|
@classmethod
|
|
166
423
|
def from_birth_data(
|
|
167
424
|
cls,
|
|
168
425
|
name: str = "Now",
|
|
169
|
-
year: int =
|
|
170
|
-
month: int =
|
|
171
|
-
day: int =
|
|
172
|
-
hour: int =
|
|
173
|
-
minute: int =
|
|
426
|
+
year: Optional[int] = None,
|
|
427
|
+
month: Optional[int] = None,
|
|
428
|
+
day: Optional[int] = None,
|
|
429
|
+
hour: Optional[int] = None,
|
|
430
|
+
minute: Optional[int] = None,
|
|
174
431
|
city: Optional[str] = None,
|
|
175
432
|
nation: Optional[str] = None,
|
|
176
433
|
lng: Optional[float] = None,
|
|
@@ -185,48 +442,146 @@ class AstrologicalSubjectFactory:
|
|
|
185
442
|
cache_expire_after_days: int = DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS,
|
|
186
443
|
is_dst: Optional[bool] = None,
|
|
187
444
|
altitude: Optional[float] = None,
|
|
188
|
-
active_points: List[AstrologicalPoint] =
|
|
445
|
+
active_points: Optional[List[AstrologicalPoint]] = None,
|
|
189
446
|
calculate_lunar_phase: bool = True,
|
|
190
447
|
*,
|
|
191
448
|
seconds: int = 0,
|
|
449
|
+
suppress_geonames_warning: bool = False,
|
|
192
450
|
|
|
193
451
|
) -> AstrologicalSubjectModel:
|
|
194
452
|
"""
|
|
195
|
-
Create an astrological subject from standard birth
|
|
453
|
+
Create an astrological subject from standard birth or event data.
|
|
454
|
+
|
|
455
|
+
This is the most flexible and commonly used factory method. It creates a complete
|
|
456
|
+
astrological subject with planetary positions, house cusps, and specialized points
|
|
457
|
+
for a specific date, time, and location. Supports both online location resolution
|
|
458
|
+
and offline calculation modes.
|
|
196
459
|
|
|
197
460
|
Args:
|
|
198
|
-
name:
|
|
199
|
-
year
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
461
|
+
name (str, optional): Name or identifier for the subject. Defaults to "Now".
|
|
462
|
+
year (int, optional): Year of birth/event. Defaults to current year.
|
|
463
|
+
month (int, optional): Month of birth/event (1-12). Defaults to current month.
|
|
464
|
+
day (int, optional): Day of birth/event (1-31). Defaults to current day.
|
|
465
|
+
hour (int, optional): Hour of birth/event (0-23). Defaults to current hour.
|
|
466
|
+
minute (int, optional): Minute of birth/event (0-59). Defaults to current minute.
|
|
467
|
+
seconds (int, optional): Seconds of birth/event (0-59). Defaults to 0.
|
|
468
|
+
city (str, optional): City name for location lookup. Used with online=True.
|
|
469
|
+
Defaults to None (Greenwich if not specified).
|
|
470
|
+
nation (str, optional): ISO country code (e.g., 'US', 'GB', 'IT'). Used with
|
|
471
|
+
online=True. Defaults to None ('GB' if not specified).
|
|
472
|
+
lng (float, optional): Longitude in decimal degrees. East is positive, West
|
|
473
|
+
is negative. If not provided and online=True, fetched from GeoNames.
|
|
474
|
+
lat (float, optional): Latitude in decimal degrees. North is positive, South
|
|
475
|
+
is negative. If not provided and online=True, fetched from GeoNames.
|
|
476
|
+
tz_str (str, optional): IANA timezone identifier (e.g., 'Europe/London').
|
|
477
|
+
If not provided and online=True, fetched from GeoNames.
|
|
478
|
+
geonames_username (str, optional): Username for GeoNames API. Required for
|
|
479
|
+
online location lookup. Get one free at geonames.org.
|
|
480
|
+
online (bool, optional): Whether to fetch location data online. If False,
|
|
481
|
+
lng, lat, and tz_str must be provided. Defaults to True.
|
|
482
|
+
zodiac_type (ZodiacType, optional): Zodiac system - 'Tropical' or 'Sidereal'.
|
|
483
|
+
Defaults to 'Tropical'.
|
|
484
|
+
sidereal_mode (SiderealMode, optional): Sidereal calculation mode (e.g.,
|
|
485
|
+
'FAGAN_BRADLEY', 'LAHIRI'). Only used with zodiac_type='Sidereal'.
|
|
486
|
+
houses_system_identifier (HousesSystemIdentifier, optional): House system
|
|
487
|
+
for cusp calculations (e.g., 'P'=Placidus, 'K'=Koch, 'E'=Equal).
|
|
488
|
+
Defaults to 'P' (Placidus).
|
|
489
|
+
perspective_type (PerspectiveType, optional): Calculation perspective:
|
|
490
|
+
- 'Apparent Geocentric': Standard geocentric with light-time correction
|
|
491
|
+
- 'True Geocentric': Geometric geocentric positions
|
|
492
|
+
- 'Heliocentric': Sun-centered coordinates
|
|
493
|
+
- 'Topocentric': Earth surface perspective (requires altitude)
|
|
494
|
+
Defaults to 'Apparent Geocentric'.
|
|
495
|
+
cache_expire_after_days (int, optional): Days to cache GeoNames data locally.
|
|
496
|
+
Defaults to 30.
|
|
497
|
+
is_dst (bool, optional): Daylight Saving Time flag for ambiguous times.
|
|
498
|
+
If None, pytz attempts automatic detection. Set explicitly for
|
|
499
|
+
times during DST transitions.
|
|
500
|
+
altitude (float, optional): Altitude above sea level in meters. Used for
|
|
501
|
+
topocentric calculations and atmospheric corrections. Defaults to None
|
|
502
|
+
(sea level assumed).
|
|
503
|
+
active_points (Optional[List[AstrologicalPoint]], optional): List of astrological
|
|
504
|
+
points to calculate. Omitting points can improve performance for
|
|
505
|
+
specialized applications. If None, uses DEFAULT_ACTIVE_POINTS.
|
|
506
|
+
calculate_lunar_phase (bool, optional): Whether to calculate lunar phase.
|
|
507
|
+
Requires Sun and Moon in active_points. Defaults to True.
|
|
508
|
+
suppress_geonames_warning (bool, optional): If True, suppresses the warning
|
|
509
|
+
message when using the default GeoNames username. Useful for testing
|
|
510
|
+
or automated processes. Defaults to False.
|
|
215
511
|
|
|
216
512
|
Returns:
|
|
217
|
-
|
|
513
|
+
AstrologicalSubjectModel: Complete astrological subject with calculated
|
|
514
|
+
positions, houses, and metadata. Access planetary positions via
|
|
515
|
+
attributes like .sun, .moon, .mercury, etc.
|
|
516
|
+
|
|
517
|
+
Raises:
|
|
518
|
+
KerykeionException:
|
|
519
|
+
- If offline mode is used without required location data
|
|
520
|
+
- If invalid zodiac/sidereal mode combinations are specified
|
|
521
|
+
- If GeoNames data is missing or invalid
|
|
522
|
+
- If timezone localization fails (ambiguous DST times)
|
|
523
|
+
|
|
524
|
+
Examples:
|
|
525
|
+
>>> # Basic natal chart with online location lookup
|
|
526
|
+
>>> chart = AstrologicalSubjectFactory.from_birth_data(
|
|
527
|
+
... name="Jane Doe",
|
|
528
|
+
... year=1985, month=3, day=21,
|
|
529
|
+
... hour=15, minute=30,
|
|
530
|
+
... city="Paris", nation="FR",
|
|
531
|
+
... geonames_username="your_username"
|
|
532
|
+
... )
|
|
533
|
+
|
|
534
|
+
>>> # Offline calculation with manual coordinates
|
|
535
|
+
>>> chart = AstrologicalSubjectFactory.from_birth_data(
|
|
536
|
+
... name="John Smith",
|
|
537
|
+
... year=1990, month=12, day=25,
|
|
538
|
+
... hour=0, minute=0,
|
|
539
|
+
... lng=-74.006, lat=40.7128, tz_str="America/New_York",
|
|
540
|
+
... online=False
|
|
541
|
+
... )
|
|
542
|
+
|
|
543
|
+
>>> # Sidereal chart with specific points
|
|
544
|
+
>>> chart = AstrologicalSubjectFactory.from_birth_data(
|
|
545
|
+
... name="Vedic Chart",
|
|
546
|
+
... year=2000, month=6, day=15, hour=12,
|
|
547
|
+
... city="Mumbai", nation="IN",
|
|
548
|
+
... zodiac_type="Sidereal",
|
|
549
|
+
... sidereal_mode="LAHIRI",
|
|
550
|
+
... active_points=["Sun", "Moon", "Mercury", "Venus", "Mars",
|
|
551
|
+
... "Jupiter", "Saturn", "Ascendant"]
|
|
552
|
+
... )
|
|
553
|
+
|
|
554
|
+
Note:
|
|
555
|
+
- For high-precision calculations, consider providing seconds parameter
|
|
556
|
+
- Use topocentric perspective for observer-specific calculations
|
|
557
|
+
- Some Arabic parts automatically activate required base points
|
|
558
|
+
- The method handles polar regions by adjusting extreme latitudes
|
|
559
|
+
- Time zones are handled with full DST awareness via pytz
|
|
218
560
|
"""
|
|
561
|
+
# Resolve time defaults using current time
|
|
562
|
+
if year is None or month is None or day is None or hour is None or minute is None or seconds is None:
|
|
563
|
+
now = datetime.now()
|
|
564
|
+
year = year if year is not None else now.year
|
|
565
|
+
month = month if month is not None else now.month
|
|
566
|
+
day = day if day is not None else now.day
|
|
567
|
+
hour = hour if hour is not None else now.hour
|
|
568
|
+
minute = minute if minute is not None else now.minute
|
|
569
|
+
seconds = seconds if seconds is not None else now.second
|
|
570
|
+
|
|
219
571
|
# Create a calculation data container
|
|
220
|
-
calc_data = {}
|
|
572
|
+
calc_data: Dict[str, Any] = {}
|
|
221
573
|
|
|
222
574
|
# Basic identity
|
|
223
575
|
calc_data["name"] = name
|
|
224
576
|
calc_data["json_dir"] = str(Path.home())
|
|
225
577
|
|
|
226
578
|
# Create a deep copy of active points to avoid modifying the original list
|
|
227
|
-
active_points
|
|
579
|
+
if active_points is None:
|
|
580
|
+
active_points_list: List[AstrologicalPoint] = list(DEFAULT_ACTIVE_POINTS)
|
|
581
|
+
else:
|
|
582
|
+
active_points_list = list(active_points)
|
|
228
583
|
|
|
229
|
-
calc_data["active_points"] =
|
|
584
|
+
calc_data["active_points"] = active_points_list
|
|
230
585
|
|
|
231
586
|
# Initialize configuration
|
|
232
587
|
config = ChartConfiguration(
|
|
@@ -235,7 +590,6 @@ class AstrologicalSubjectFactory:
|
|
|
235
590
|
houses_system_identifier=houses_system_identifier,
|
|
236
591
|
perspective_type=perspective_type,
|
|
237
592
|
)
|
|
238
|
-
config.validate()
|
|
239
593
|
|
|
240
594
|
# Add configuration data to calculation data
|
|
241
595
|
calc_data["zodiac_type"] = config.zodiac_type
|
|
@@ -245,7 +599,8 @@ class AstrologicalSubjectFactory:
|
|
|
245
599
|
|
|
246
600
|
# Set up geonames username if needed
|
|
247
601
|
if geonames_username is None and online and (not lat or not lng or not tz_str):
|
|
248
|
-
|
|
602
|
+
if not suppress_geonames_warning:
|
|
603
|
+
logging.warning(GEONAMES_DEFAULT_USERNAME_WARNING)
|
|
249
604
|
geonames_username = DEFAULT_GEONAMES_USERNAME
|
|
250
605
|
|
|
251
606
|
# Initialize location data
|
|
@@ -292,20 +647,37 @@ class AstrologicalSubjectFactory:
|
|
|
292
647
|
calc_data["is_dst"] = is_dst
|
|
293
648
|
|
|
294
649
|
# Calculate time conversions
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
650
|
+
AstrologicalSubjectFactory._calculate_time_conversions(calc_data, location)
|
|
651
|
+
# Initialize Swiss Ephemeris and calculate houses and planets with context manager
|
|
652
|
+
ephe_path = str(Path(__file__).parent.absolute() / "sweph")
|
|
653
|
+
with ephemeris_context(
|
|
654
|
+
ephe_path=ephe_path,
|
|
655
|
+
config=config,
|
|
656
|
+
lng=calc_data["lng"],
|
|
657
|
+
lat=calc_data["lat"],
|
|
658
|
+
alt=calc_data["altitude"],
|
|
659
|
+
) as iflag:
|
|
660
|
+
calc_data["_iflag"] = iflag
|
|
661
|
+
# House system name (previously set in _setup_ephemeris)
|
|
662
|
+
calc_data["houses_system_name"] = swe.house_name(
|
|
663
|
+
config.houses_system_identifier.encode("ascii")
|
|
664
|
+
)
|
|
665
|
+
calculated_axial_cusps = AstrologicalSubjectFactory._calculate_houses(
|
|
666
|
+
calc_data, active_points_list
|
|
667
|
+
)
|
|
668
|
+
AstrologicalSubjectFactory._calculate_planets(
|
|
669
|
+
calc_data, active_points_list, calculated_axial_cusps
|
|
670
|
+
)
|
|
671
|
+
AstrologicalSubjectFactory._calculate_day_of_week(calc_data)
|
|
302
672
|
|
|
303
673
|
# Calculate lunar phase (optional - only if requested and Sun and Moon are available)
|
|
304
674
|
if calculate_lunar_phase and "moon" in calc_data and "sun" in calc_data:
|
|
305
675
|
calc_data["lunar_phase"] = calculate_moon_phase(
|
|
306
|
-
calc_data["moon"].abs_pos,
|
|
307
|
-
calc_data["sun"].abs_pos
|
|
676
|
+
calc_data["moon"].abs_pos, # type: ignore[attr-defined,union-attr]
|
|
677
|
+
calc_data["sun"].abs_pos # type: ignore[attr-defined,union-attr]
|
|
308
678
|
)
|
|
679
|
+
else:
|
|
680
|
+
calc_data["lunar_phase"] = None
|
|
309
681
|
|
|
310
682
|
# Create and return the AstrologicalSubjectModel
|
|
311
683
|
return AstrologicalSubjectModel(**calc_data)
|
|
@@ -327,31 +699,83 @@ class AstrologicalSubjectFactory:
|
|
|
327
699
|
houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
|
|
328
700
|
perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE,
|
|
329
701
|
altitude: Optional[float] = None,
|
|
330
|
-
active_points: List[AstrologicalPoint] =
|
|
331
|
-
calculate_lunar_phase: bool = True
|
|
702
|
+
active_points: Optional[List[AstrologicalPoint]] = None,
|
|
703
|
+
calculate_lunar_phase: bool = True,
|
|
704
|
+
suppress_geonames_warning: bool = False
|
|
332
705
|
) -> AstrologicalSubjectModel:
|
|
333
706
|
"""
|
|
334
|
-
Create an astrological subject from an ISO formatted UTC
|
|
707
|
+
Create an astrological subject from an ISO formatted UTC timestamp.
|
|
708
|
+
|
|
709
|
+
This method is ideal for creating astrological subjects from standardized
|
|
710
|
+
time formats, such as those stored in databases or received from APIs.
|
|
711
|
+
It automatically handles timezone conversion from UTC to the specified
|
|
712
|
+
local timezone.
|
|
335
713
|
|
|
336
714
|
Args:
|
|
337
|
-
name:
|
|
338
|
-
iso_utc_time: ISO formatted UTC
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
715
|
+
name (str): Name or identifier for the subject.
|
|
716
|
+
iso_utc_time (str): ISO 8601 formatted UTC timestamp. Supported formats:
|
|
717
|
+
- "2023-06-15T14:30:00Z" (with Z suffix)
|
|
718
|
+
- "2023-06-15T14:30:00+00:00" (with UTC offset)
|
|
719
|
+
- "2023-06-15T14:30:00.123Z" (with milliseconds)
|
|
720
|
+
city (str, optional): City name for location. Defaults to "Greenwich".
|
|
721
|
+
nation (str, optional): ISO country code. Defaults to "GB".
|
|
722
|
+
tz_str (str, optional): IANA timezone identifier for result conversion.
|
|
723
|
+
The ISO time is assumed to be in UTC and will be converted to this
|
|
724
|
+
timezone. Defaults to "Etc/GMT".
|
|
725
|
+
online (bool, optional): Whether to fetch coordinates online. If True,
|
|
726
|
+
coordinates are fetched via GeoNames API. Defaults to True.
|
|
727
|
+
lng (float, optional): Longitude in decimal degrees. Used when online=False
|
|
728
|
+
or as fallback. Defaults to 0.0 (Greenwich).
|
|
729
|
+
lat (float, optional): Latitude in decimal degrees. Used when online=False
|
|
730
|
+
or as fallback. Defaults to 51.5074 (Greenwich).
|
|
731
|
+
geonames_username (str, optional): GeoNames API username. Required when
|
|
732
|
+
online=True. Defaults to DEFAULT_GEONAMES_USERNAME.
|
|
733
|
+
zodiac_type (ZodiacType, optional): Zodiac system. Defaults to 'Tropical'.
|
|
734
|
+
sidereal_mode (SiderealMode, optional): Sidereal mode when zodiac_type
|
|
735
|
+
is 'Sidereal'. Defaults to None.
|
|
736
|
+
houses_system_identifier (HousesSystemIdentifier, optional): House system.
|
|
737
|
+
Defaults to 'P' (Placidus).
|
|
738
|
+
perspective_type (PerspectiveType, optional): Calculation perspective.
|
|
739
|
+
Defaults to 'Apparent Geocentric'.
|
|
740
|
+
altitude (float, optional): Altitude in meters for topocentric calculations.
|
|
741
|
+
Defaults to None (sea level).
|
|
742
|
+
active_points (Optional[List[AstrologicalPoint]], optional): Points to calculate.
|
|
743
|
+
If None, uses DEFAULT_ACTIVE_POINTS.
|
|
744
|
+
calculate_lunar_phase (bool, optional): Whether to calculate lunar phase.
|
|
745
|
+
Defaults to True.
|
|
352
746
|
|
|
353
747
|
Returns:
|
|
354
|
-
AstrologicalSubjectModel
|
|
748
|
+
AstrologicalSubjectModel: Astrological subject with positions calculated
|
|
749
|
+
for the specified UTC time converted to local timezone.
|
|
750
|
+
|
|
751
|
+
Raises:
|
|
752
|
+
ValueError: If the ISO timestamp format is invalid or cannot be parsed.
|
|
753
|
+
KerykeionException: If location data cannot be fetched or is invalid.
|
|
754
|
+
|
|
755
|
+
Examples:
|
|
756
|
+
>>> # From API timestamp with online location lookup
|
|
757
|
+
>>> subject = AstrologicalSubjectFactory.from_iso_utc_time(
|
|
758
|
+
... name="Event Chart",
|
|
759
|
+
... iso_utc_time="2023-12-25T12:00:00Z",
|
|
760
|
+
... city="Tokyo", nation="JP",
|
|
761
|
+
... tz_str="Asia/Tokyo",
|
|
762
|
+
... geonames_username="your_username"
|
|
763
|
+
... )
|
|
764
|
+
|
|
765
|
+
>>> # From database timestamp with manual coordinates
|
|
766
|
+
>>> subject = AstrologicalSubjectFactory.from_iso_utc_time(
|
|
767
|
+
... name="Historical Event",
|
|
768
|
+
... iso_utc_time="1969-07-20T20:17:00Z",
|
|
769
|
+
... lng=-95.0969, lat=37.4419, # Houston
|
|
770
|
+
... tz_str="America/Chicago",
|
|
771
|
+
... online=False
|
|
772
|
+
... )
|
|
773
|
+
|
|
774
|
+
Note:
|
|
775
|
+
- The method assumes the input timestamp is in UTC
|
|
776
|
+
- Local time conversion respects DST rules for the target timezone
|
|
777
|
+
- Milliseconds in the timestamp are supported but truncated to seconds
|
|
778
|
+
- When online=True, the city/nation parameters override lng/lat
|
|
355
779
|
"""
|
|
356
780
|
# Parse the ISO time
|
|
357
781
|
dt = datetime.fromisoformat(iso_utc_time.replace('Z', '+00:00'))
|
|
@@ -359,7 +783,8 @@ class AstrologicalSubjectFactory:
|
|
|
359
783
|
# Get location data if online mode is enabled
|
|
360
784
|
if online:
|
|
361
785
|
if geonames_username == DEFAULT_GEONAMES_USERNAME:
|
|
362
|
-
|
|
786
|
+
if not suppress_geonames_warning:
|
|
787
|
+
logging.warning(GEONAMES_DEFAULT_USERNAME_WARNING)
|
|
363
788
|
|
|
364
789
|
geonames = FetchGeonames(
|
|
365
790
|
city,
|
|
@@ -397,7 +822,8 @@ class AstrologicalSubjectFactory:
|
|
|
397
822
|
perspective_type=perspective_type,
|
|
398
823
|
altitude=altitude,
|
|
399
824
|
active_points=active_points,
|
|
400
|
-
calculate_lunar_phase=calculate_lunar_phase
|
|
825
|
+
calculate_lunar_phase=calculate_lunar_phase,
|
|
826
|
+
suppress_geonames_warning=suppress_geonames_warning
|
|
401
827
|
)
|
|
402
828
|
|
|
403
829
|
@classmethod
|
|
@@ -415,29 +841,84 @@ class AstrologicalSubjectFactory:
|
|
|
415
841
|
sidereal_mode: Optional[SiderealMode] = None,
|
|
416
842
|
houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
|
|
417
843
|
perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE,
|
|
418
|
-
active_points: List[AstrologicalPoint] =
|
|
419
|
-
calculate_lunar_phase: bool = True
|
|
844
|
+
active_points: Optional[List[AstrologicalPoint]] = None,
|
|
845
|
+
calculate_lunar_phase: bool = True,
|
|
846
|
+
suppress_geonames_warning: bool = False
|
|
420
847
|
) -> AstrologicalSubjectModel:
|
|
421
848
|
"""
|
|
422
|
-
Create an astrological subject for the current time.
|
|
849
|
+
Create an astrological subject for the current moment in time.
|
|
850
|
+
|
|
851
|
+
This convenience method creates a "now" chart, capturing the current
|
|
852
|
+
astrological conditions at the moment of execution. Useful for horary
|
|
853
|
+
astrology, electional astrology, or real-time astrological monitoring.
|
|
423
854
|
|
|
424
855
|
Args:
|
|
425
|
-
name:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
856
|
+
name (str, optional): Name for the current moment chart.
|
|
857
|
+
Defaults to "Now".
|
|
858
|
+
city (str, optional): City name for location lookup. If not provided
|
|
859
|
+
and online=True, defaults to Greenwich.
|
|
860
|
+
nation (str, optional): ISO country code. If not provided and
|
|
861
|
+
online=True, defaults to 'GB'.
|
|
862
|
+
lng (float, optional): Longitude in decimal degrees. If not provided
|
|
863
|
+
and online=True, fetched from GeoNames API.
|
|
864
|
+
lat (float, optional): Latitude in decimal degrees. If not provided
|
|
865
|
+
and online=True, fetched from GeoNames API.
|
|
866
|
+
tz_str (str, optional): IANA timezone identifier. If not provided
|
|
867
|
+
and online=True, fetched from GeoNames API.
|
|
868
|
+
geonames_username (str, optional): GeoNames API username for location
|
|
869
|
+
lookup. Required when online=True and location is not fully specified.
|
|
870
|
+
online (bool, optional): Whether to fetch location data online.
|
|
871
|
+
Defaults to True.
|
|
872
|
+
zodiac_type (ZodiacType, optional): Zodiac system to use.
|
|
873
|
+
Defaults to 'Tropical'.
|
|
874
|
+
sidereal_mode (SiderealMode, optional): Sidereal calculation mode.
|
|
875
|
+
Only used when zodiac_type is 'Sidereal'. Defaults to None.
|
|
876
|
+
houses_system_identifier (HousesSystemIdentifier, optional): House
|
|
877
|
+
system for calculations. Defaults to 'P' (Placidus).
|
|
878
|
+
perspective_type (PerspectiveType, optional): Calculation perspective.
|
|
879
|
+
Defaults to 'Apparent Geocentric'.
|
|
880
|
+
active_points (Optional[List[AstrologicalPoint]], optional): Astrological points
|
|
881
|
+
to calculate. If None, uses DEFAULT_ACTIVE_POINTS.
|
|
882
|
+
calculate_lunar_phase (bool, optional): Whether to calculate lunar phase.
|
|
883
|
+
Defaults to True.
|
|
438
884
|
|
|
439
885
|
Returns:
|
|
440
|
-
AstrologicalSubjectModel
|
|
886
|
+
AstrologicalSubjectModel: Astrological subject representing current
|
|
887
|
+
astrological conditions at the specified or default location.
|
|
888
|
+
|
|
889
|
+
Raises:
|
|
890
|
+
KerykeionException: If online location lookup fails or if offline mode
|
|
891
|
+
is used without sufficient location data.
|
|
892
|
+
|
|
893
|
+
Examples:
|
|
894
|
+
>>> # Current moment for your location
|
|
895
|
+
>>> now_chart = AstrologicalSubjectFactory.from_current_time(
|
|
896
|
+
... name="Current Transits",
|
|
897
|
+
... city="New York", nation="US",
|
|
898
|
+
... geonames_username="your_username"
|
|
899
|
+
... )
|
|
900
|
+
|
|
901
|
+
>>> # Horary chart with specific coordinates
|
|
902
|
+
>>> horary = AstrologicalSubjectFactory.from_current_time(
|
|
903
|
+
... name="Horary Question",
|
|
904
|
+
... lng=-0.1278, lat=51.5074, # London
|
|
905
|
+
... tz_str="Europe/London",
|
|
906
|
+
... online=False
|
|
907
|
+
... )
|
|
908
|
+
|
|
909
|
+
>>> # Current sidereal positions
|
|
910
|
+
>>> sidereal_now = AstrologicalSubjectFactory.from_current_time(
|
|
911
|
+
... name="Sidereal Now",
|
|
912
|
+
... city="Mumbai", nation="IN",
|
|
913
|
+
... zodiac_type="Sidereal",
|
|
914
|
+
... sidereal_mode="LAHIRI"
|
|
915
|
+
... )
|
|
916
|
+
|
|
917
|
+
Note:
|
|
918
|
+
- The exact time is captured at method execution, including seconds
|
|
919
|
+
- For horary astrology, consider the moment of understanding the question
|
|
920
|
+
- System clock accuracy affects precision; ensure accurate system time
|
|
921
|
+
- Time zone detection is automatic when using online location lookup
|
|
441
922
|
"""
|
|
442
923
|
now = datetime.now()
|
|
443
924
|
|
|
@@ -461,12 +942,38 @@ class AstrologicalSubjectFactory:
|
|
|
461
942
|
houses_system_identifier=houses_system_identifier,
|
|
462
943
|
perspective_type=perspective_type,
|
|
463
944
|
active_points=active_points,
|
|
464
|
-
calculate_lunar_phase=calculate_lunar_phase
|
|
945
|
+
calculate_lunar_phase=calculate_lunar_phase,
|
|
946
|
+
suppress_geonames_warning=suppress_geonames_warning
|
|
465
947
|
)
|
|
466
948
|
|
|
467
|
-
@
|
|
468
|
-
def _calculate_time_conversions(
|
|
469
|
-
"""
|
|
949
|
+
@staticmethod
|
|
950
|
+
def _calculate_time_conversions(data: Dict[str, Any], location: LocationData) -> None:
|
|
951
|
+
"""
|
|
952
|
+
Calculate time conversions between local time, UTC, and Julian Day Number.
|
|
953
|
+
|
|
954
|
+
Handles timezone-aware conversion from local civil time to UTC and astronomical
|
|
955
|
+
Julian Day Number, including proper DST handling and timezone localization.
|
|
956
|
+
|
|
957
|
+
Args:
|
|
958
|
+
data (Dict[str, Any]): Calculation data dictionary containing time components
|
|
959
|
+
(year, month, day, hour, minute, seconds) and optional DST flag.
|
|
960
|
+
location (LocationData): Location data containing timezone information.
|
|
961
|
+
|
|
962
|
+
Raises:
|
|
963
|
+
KerykeionException: If DST ambiguity occurs during timezone transitions
|
|
964
|
+
and is_dst parameter is not explicitly set to resolve the ambiguity.
|
|
965
|
+
|
|
966
|
+
Side Effects:
|
|
967
|
+
Updates data dictionary with:
|
|
968
|
+
- iso_formatted_utc_datetime: ISO 8601 UTC timestamp
|
|
969
|
+
- iso_formatted_local_datetime: ISO 8601 local timestamp
|
|
970
|
+
- julian_day: Julian Day Number for astronomical calculations
|
|
971
|
+
|
|
972
|
+
Note:
|
|
973
|
+
During DST transitions, times may be ambiguous (fall back) or non-existent
|
|
974
|
+
(spring forward). The method raises an exception for ambiguous times unless
|
|
975
|
+
the is_dst parameter is explicitly set to True or False.
|
|
976
|
+
"""
|
|
470
977
|
# Convert local time to UTC
|
|
471
978
|
local_timezone = pytz.timezone(location.tz_str)
|
|
472
979
|
naive_datetime = datetime(
|
|
@@ -481,6 +988,11 @@ class AstrologicalSubjectFactory:
|
|
|
481
988
|
"Ambiguous time error! The time falls during a DST transition. "
|
|
482
989
|
"Please specify is_dst=True or is_dst=False to clarify."
|
|
483
990
|
)
|
|
991
|
+
except pytz.exceptions.NonExistentTimeError:
|
|
992
|
+
raise KerykeionException(
|
|
993
|
+
"Non-existent time error! The time does not exist due to DST transition (spring forward). "
|
|
994
|
+
"Please specify a valid time."
|
|
995
|
+
)
|
|
484
996
|
|
|
485
997
|
# Store formatted times
|
|
486
998
|
utc_datetime = local_datetime.astimezone(pytz.utc)
|
|
@@ -490,46 +1002,50 @@ class AstrologicalSubjectFactory:
|
|
|
490
1002
|
# Calculate Julian day
|
|
491
1003
|
data["julian_day"] = datetime_to_julian(utc_datetime)
|
|
492
1004
|
|
|
493
|
-
@classmethod
|
|
494
|
-
def _setup_ephemeris(cls, data: Dict[str, Any], config: ChartConfiguration) -> None:
|
|
495
|
-
"""Set up Swiss Ephemeris with appropriate flags"""
|
|
496
|
-
# Set ephemeris path
|
|
497
|
-
swe.set_ephe_path(str(Path(__file__).parent.absolute() / "sweph"))
|
|
498
|
-
|
|
499
|
-
# Base flags
|
|
500
|
-
iflag = swe.FLG_SWIEPH + swe.FLG_SPEED
|
|
501
|
-
|
|
502
|
-
# Add perspective flags
|
|
503
|
-
if config.perspective_type == "True Geocentric":
|
|
504
|
-
iflag += swe.FLG_TRUEPOS
|
|
505
|
-
elif config.perspective_type == "Heliocentric":
|
|
506
|
-
iflag += swe.FLG_HELCTR
|
|
507
|
-
elif config.perspective_type == "Topocentric":
|
|
508
|
-
iflag += swe.FLG_TOPOCTR
|
|
509
|
-
# Set topocentric coordinates
|
|
510
|
-
swe.set_topo(data["lng"], data["lat"], data["altitude"] or 0)
|
|
511
|
-
|
|
512
|
-
# Add sidereal flag if needed
|
|
513
|
-
if config.zodiac_type == "Sidereal":
|
|
514
|
-
iflag += swe.FLG_SIDEREAL
|
|
515
|
-
# Set sidereal mode
|
|
516
|
-
mode = f"SIDM_{config.sidereal_mode}"
|
|
517
|
-
swe.set_sid_mode(getattr(swe, mode))
|
|
518
|
-
logging.debug(f"Using sidereal mode: {mode}")
|
|
519
|
-
|
|
520
|
-
# Save house system name and iflag for later use
|
|
521
|
-
data["houses_system_name"] = swe.house_name(
|
|
522
|
-
config.houses_system_identifier.encode('ascii')
|
|
523
|
-
)
|
|
524
|
-
data["_iflag"] = iflag
|
|
525
1005
|
|
|
526
|
-
@
|
|
527
|
-
def _calculate_houses(
|
|
528
|
-
"""
|
|
1006
|
+
@staticmethod
|
|
1007
|
+
def _calculate_houses(data: Dict[str, Any], active_points: Optional[List[AstrologicalPoint]]) -> List[AstrologicalPoint]:
|
|
1008
|
+
"""
|
|
1009
|
+
Calculate house cusps and angular points (Ascendant, MC, etc.).
|
|
1010
|
+
|
|
1011
|
+
Computes the 12 house cusps using the specified house system and calculates
|
|
1012
|
+
the four main angles of the chart. Only calculates angular points that are
|
|
1013
|
+
included in the active_points list for performance optimization.
|
|
1014
|
+
|
|
1015
|
+
Args:
|
|
1016
|
+
data (Dict[str, Any]): Calculation data dictionary containing configuration
|
|
1017
|
+
and location information. Updated with calculated house and angle data.
|
|
1018
|
+
active_points (Optional[List[AstrologicalPoint]]): List of points to calculate.
|
|
1019
|
+
If None, all points are calculated. Angular points not in this list
|
|
1020
|
+
are skipped for performance.
|
|
1021
|
+
|
|
1022
|
+
Side Effects:
|
|
1023
|
+
Updates data dictionary with:
|
|
1024
|
+
- House cusp objects: first_house through twelfth_house
|
|
1025
|
+
- Angular points: ascendant, medium_coeli, descendant, imum_coeli
|
|
1026
|
+
- houses_names_list: List of all house names
|
|
1027
|
+
- _houses_degree_ut: Raw house cusp degrees for internal use
|
|
1028
|
+
|
|
1029
|
+
House Systems Supported:
|
|
1030
|
+
All systems supported by Swiss Ephemeris including Placidus, Koch,
|
|
1031
|
+
Equal House, Whole Sign, Regiomontanus, Campanus, Topocentric, etc.
|
|
1032
|
+
|
|
1033
|
+
Angular Points Calculated:
|
|
1034
|
+
- Ascendant: Eastern horizon point (1st house cusp)
|
|
1035
|
+
- Medium Coeli (Midheaven): Southern meridian point (10th house cusp)
|
|
1036
|
+
- Descendant: Western horizon point (opposite Ascendant)
|
|
1037
|
+
- Imum Coeli: Northern meridian point (opposite Medium Coeli)
|
|
1038
|
+
|
|
1039
|
+
Note:
|
|
1040
|
+
House calculations respect the zodiac type (Tropical/Sidereal) and use
|
|
1041
|
+
the appropriate Swiss Ephemeris function. Angular points include house
|
|
1042
|
+
position and retrograde status (always False for angles).
|
|
1043
|
+
"""
|
|
529
1044
|
# Skip calculation if point is not in active_points
|
|
530
|
-
should_calculate:
|
|
1045
|
+
def should_calculate(point: AstrologicalPoint) -> bool:
|
|
1046
|
+
return not active_points or point in active_points
|
|
531
1047
|
# Track which axial cusps are actually calculated
|
|
532
|
-
calculated_axial_cusps = []
|
|
1048
|
+
calculated_axial_cusps: List[AstrologicalPoint] = []
|
|
533
1049
|
|
|
534
1050
|
# Calculate houses based on zodiac type
|
|
535
1051
|
if data["zodiac_type"] == "Sidereal":
|
|
@@ -602,9 +1118,10 @@ class AstrologicalSubjectFactory:
|
|
|
602
1118
|
data["imum_coeli"].retrograde = False
|
|
603
1119
|
calculated_axial_cusps.append("Imum_Coeli")
|
|
604
1120
|
|
|
605
|
-
|
|
1121
|
+
return calculated_axial_cusps
|
|
1122
|
+
|
|
1123
|
+
@staticmethod
|
|
606
1124
|
def _calculate_single_planet(
|
|
607
|
-
cls,
|
|
608
1125
|
data: Dict[str, Any],
|
|
609
1126
|
planet_name: AstrologicalPoint,
|
|
610
1127
|
planet_id: int,
|
|
@@ -612,30 +1129,61 @@ class AstrologicalSubjectFactory:
|
|
|
612
1129
|
iflag: int,
|
|
613
1130
|
houses_degree_ut: List[float],
|
|
614
1131
|
point_type: PointType,
|
|
615
|
-
calculated_planets: List[
|
|
1132
|
+
calculated_planets: List[AstrologicalPoint],
|
|
616
1133
|
active_points: List[AstrologicalPoint]
|
|
617
1134
|
) -> None:
|
|
618
1135
|
"""
|
|
619
|
-
Calculate a single
|
|
1136
|
+
Calculate a single celestial body's position with comprehensive error handling.
|
|
1137
|
+
|
|
1138
|
+
Computes the position of a single planet, asteroid, or other celestial object
|
|
1139
|
+
using Swiss Ephemeris, creates a Kerykeion point object, determines house
|
|
1140
|
+
position, and assesses retrograde status. Handles calculation errors gracefully
|
|
1141
|
+
by logging and removing failed points from the active list.
|
|
620
1142
|
|
|
621
1143
|
Args:
|
|
622
|
-
data:
|
|
623
|
-
planet_name: Name
|
|
624
|
-
planet_id: Swiss Ephemeris
|
|
625
|
-
julian_day: Julian
|
|
626
|
-
iflag: Swiss Ephemeris calculation flags
|
|
627
|
-
houses_degree_ut: House degrees for house
|
|
628
|
-
point_type:
|
|
629
|
-
calculated_planets
|
|
630
|
-
active_points
|
|
1144
|
+
data (Dict[str, Any]): Main calculation data dictionary to store results.
|
|
1145
|
+
planet_name (AstrologicalPoint): Name identifier for the celestial body.
|
|
1146
|
+
planet_id (int): Swiss Ephemeris numerical identifier for the object.
|
|
1147
|
+
julian_day (float): Julian Day Number for the calculation moment.
|
|
1148
|
+
iflag (int): Swiss Ephemeris calculation flags (perspective, zodiac, etc.).
|
|
1149
|
+
houses_degree_ut (List[float]): House cusp degrees for house determination.
|
|
1150
|
+
point_type (PointType): Classification of the point type for the object.
|
|
1151
|
+
calculated_planets (List[str]): Running list of successfully calculated objects.
|
|
1152
|
+
active_points (List[AstrologicalPoint]): Active points list (modified on error).
|
|
1153
|
+
|
|
1154
|
+
Side Effects:
|
|
1155
|
+
- Adds calculated object to data dictionary using lowercase planet_name as key
|
|
1156
|
+
- Appends planet_name to calculated_planets list on success
|
|
1157
|
+
- Removes planet_name from active_points list on calculation failure
|
|
1158
|
+
- Logs error messages for calculation failures
|
|
1159
|
+
|
|
1160
|
+
Calculated Properties:
|
|
1161
|
+
- Zodiacal position (longitude) in degrees
|
|
1162
|
+
- House position based on house cusp positions
|
|
1163
|
+
- Retrograde status based on velocity (negative = retrograde)
|
|
1164
|
+
- Sign, degree, and minute components
|
|
1165
|
+
|
|
1166
|
+
Error Handling:
|
|
1167
|
+
If Swiss Ephemeris calculation fails (e.g., for distant asteroids outside
|
|
1168
|
+
ephemeris range), the method logs the error and removes the object from
|
|
1169
|
+
active_points to prevent cascade failures.
|
|
1170
|
+
|
|
1171
|
+
Note:
|
|
1172
|
+
The method uses the Swiss Ephemeris calc_ut function which returns position
|
|
1173
|
+
and velocity data. Retrograde determination is based on the velocity
|
|
1174
|
+
component being negative (element index 3).
|
|
631
1175
|
"""
|
|
632
1176
|
try:
|
|
633
|
-
# Calculate planet position using Swiss Ephemeris
|
|
1177
|
+
# Calculate planet position using Swiss Ephemeris (ecliptic coordinates)
|
|
634
1178
|
planet_calc = swe.calc_ut(julian_day, planet_id, iflag)[0]
|
|
635
1179
|
|
|
1180
|
+
# Get declination from equatorial coordinates
|
|
1181
|
+
planet_eq = swe.calc_ut(julian_day, planet_id, iflag | swe.FLG_EQUATORIAL)[0]
|
|
1182
|
+
declination = planet_eq[1] # Declination from equatorial coordinates
|
|
1183
|
+
|
|
636
1184
|
# Create Kerykeion point from degree
|
|
637
1185
|
data[planet_name.lower()] = get_kerykeion_point_from_degree(
|
|
638
|
-
planet_calc[0], planet_name, point_type=point_type
|
|
1186
|
+
planet_calc[0], planet_name, point_type=point_type, speed=planet_calc[3], declination=declination
|
|
639
1187
|
)
|
|
640
1188
|
|
|
641
1189
|
# Calculate house position
|
|
@@ -652,11 +1200,93 @@ class AstrologicalSubjectFactory:
|
|
|
652
1200
|
if planet_name in active_points:
|
|
653
1201
|
active_points.remove(planet_name)
|
|
654
1202
|
|
|
655
|
-
@
|
|
656
|
-
def _calculate_planets(
|
|
657
|
-
"""
|
|
1203
|
+
@staticmethod
|
|
1204
|
+
def _calculate_planets(data: Dict[str, Any], active_points: List[AstrologicalPoint], calculated_axial_cusps: Optional[List[AstrologicalPoint]] = None) -> None:
|
|
1205
|
+
"""
|
|
1206
|
+
Calculate positions for all requested celestial bodies and special points.
|
|
1207
|
+
|
|
1208
|
+
This comprehensive method calculates positions for a wide range of astrological
|
|
1209
|
+
points including traditional planets, lunar nodes, asteroids, trans-Neptunian
|
|
1210
|
+
objects, fixed stars, Arabic parts, and specialized points like Vertex.
|
|
1211
|
+
|
|
1212
|
+
The calculation is performed selectively based on the active_points list for
|
|
1213
|
+
performance optimization. Some Arabic parts automatically activate their
|
|
1214
|
+
prerequisite points if needed.
|
|
1215
|
+
|
|
1216
|
+
Args:
|
|
1217
|
+
data (Dict[str, Any]): Main calculation data dictionary. Updated with all
|
|
1218
|
+
calculated planetary positions and related metadata.
|
|
1219
|
+
active_points (List[AstrologicalPoint]): Mutable list of points to calculate.
|
|
1220
|
+
Modified during execution to remove failed calculations and add
|
|
1221
|
+
automatically required points for Arabic parts.
|
|
1222
|
+
|
|
1223
|
+
Celestial Bodies Calculated:
|
|
1224
|
+
Traditional Planets:
|
|
1225
|
+
- Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn
|
|
1226
|
+
- Uranus, Neptune, Pluto
|
|
1227
|
+
|
|
1228
|
+
Lunar Nodes:
|
|
1229
|
+
- Mean Node, True Node (North nodes)
|
|
1230
|
+
- Mean South Node, True South Node (calculated as opposites)
|
|
1231
|
+
|
|
1232
|
+
Lilith Points:
|
|
1233
|
+
- Mean Lilith (Mean Black Moon Lilith)
|
|
1234
|
+
- True Lilith (Osculating Black Moon Lilith)
|
|
1235
|
+
|
|
1236
|
+
Asteroids:
|
|
1237
|
+
- Ceres, Pallas, Juno, Vesta (main belt asteroids)
|
|
1238
|
+
|
|
1239
|
+
Centaurs:
|
|
1240
|
+
- Chiron, Pholus
|
|
1241
|
+
|
|
1242
|
+
Trans-Neptunian Objects:
|
|
1243
|
+
- Eris, Sedna, Haumea, Makemake
|
|
1244
|
+
- Ixion, Orcus, Quaoar
|
|
1245
|
+
|
|
1246
|
+
Fixed Stars:
|
|
1247
|
+
- Regulus, Spica (examples, extensible)
|
|
1248
|
+
|
|
1249
|
+
Arabic Parts (Lots):
|
|
1250
|
+
- Pars Fortunae (Part of Fortune)
|
|
1251
|
+
- Pars Spiritus (Part of Spirit)
|
|
1252
|
+
- Pars Amoris (Part of Love/Eros)
|
|
1253
|
+
- Pars Fidei (Part of Faith)
|
|
1254
|
+
|
|
1255
|
+
Special Points:
|
|
1256
|
+
- Earth (for heliocentric perspectives)
|
|
1257
|
+
- Vertex and Anti-Vertex
|
|
1258
|
+
|
|
1259
|
+
Side Effects:
|
|
1260
|
+
- Updates data dictionary with all calculated positions
|
|
1261
|
+
- Modifies active_points list by removing failed calculations
|
|
1262
|
+
- Adds prerequisite points for Arabic parts calculations
|
|
1263
|
+
- Updates data["active_points"] with successfully calculated objects
|
|
1264
|
+
|
|
1265
|
+
Error Handling:
|
|
1266
|
+
Individual calculation failures (e.g., asteroids outside ephemeris range)
|
|
1267
|
+
are handled gracefully with logging and removal from active_points.
|
|
1268
|
+
This prevents cascade failures while maintaining calculation integrity.
|
|
1269
|
+
|
|
1270
|
+
Arabic Parts Logic:
|
|
1271
|
+
- Day/night birth detection based on Sun's house position
|
|
1272
|
+
- Automatic activation of required base points (Sun, Moon, Ascendant, etc.)
|
|
1273
|
+
- Classical formulae with day/night variations where applicable
|
|
1274
|
+
- All parts marked as non-retrograde (conceptual points)
|
|
1275
|
+
|
|
1276
|
+
Performance Notes:
|
|
1277
|
+
- Only points in active_points are calculated (selective computation)
|
|
1278
|
+
- Failed calculations are removed to prevent repeated attempts
|
|
1279
|
+
- Some expensive calculations (like distant TNOs) may timeout
|
|
1280
|
+
- Fixed stars use different calculation methods than planets
|
|
1281
|
+
|
|
1282
|
+
Note:
|
|
1283
|
+
The method maintains a running list of successfully calculated planets
|
|
1284
|
+
and updates the active_points list to reflect actual availability.
|
|
1285
|
+
This ensures that dependent calculations and aspects only use valid data.
|
|
1286
|
+
"""
|
|
658
1287
|
# Skip calculation if point is not in active_points
|
|
659
|
-
should_calculate:
|
|
1288
|
+
def should_calculate(point: AstrologicalPoint) -> bool:
|
|
1289
|
+
return not active_points or point in active_points
|
|
660
1290
|
|
|
661
1291
|
point_type: PointType = "AstrologicalPoint"
|
|
662
1292
|
julian_day = data["julian_day"]
|
|
@@ -664,7 +1294,7 @@ class AstrologicalSubjectFactory:
|
|
|
664
1294
|
houses_degree_ut = data["_houses_degree_ut"]
|
|
665
1295
|
|
|
666
1296
|
# Track which planets are actually calculated
|
|
667
|
-
calculated_planets = []
|
|
1297
|
+
calculated_planets: List[AstrologicalPoint] = []
|
|
668
1298
|
|
|
669
1299
|
# ==================
|
|
670
1300
|
# MAIN PLANETS
|
|
@@ -672,75 +1302,87 @@ class AstrologicalSubjectFactory:
|
|
|
672
1302
|
|
|
673
1303
|
# Calculate Sun
|
|
674
1304
|
if should_calculate("Sun"):
|
|
675
|
-
|
|
1305
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Sun", 0, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
676
1306
|
|
|
677
1307
|
# Calculate Moon
|
|
678
1308
|
if should_calculate("Moon"):
|
|
679
|
-
|
|
1309
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Moon", 1, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
680
1310
|
|
|
681
1311
|
# Calculate Mercury
|
|
682
1312
|
if should_calculate("Mercury"):
|
|
683
|
-
|
|
1313
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Mercury", 2, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
684
1314
|
|
|
685
1315
|
# Calculate Venus
|
|
686
1316
|
if should_calculate("Venus"):
|
|
687
|
-
|
|
1317
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Venus", 3, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
688
1318
|
|
|
689
1319
|
# Calculate Mars
|
|
690
1320
|
if should_calculate("Mars"):
|
|
691
|
-
|
|
1321
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Mars", 4, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
692
1322
|
|
|
693
1323
|
# Calculate Jupiter
|
|
694
1324
|
if should_calculate("Jupiter"):
|
|
695
|
-
|
|
1325
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Jupiter", 5, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
696
1326
|
|
|
697
1327
|
# Calculate Saturn
|
|
698
1328
|
if should_calculate("Saturn"):
|
|
699
|
-
|
|
1329
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Saturn", 6, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
700
1330
|
|
|
701
1331
|
# Calculate Uranus
|
|
702
1332
|
if should_calculate("Uranus"):
|
|
703
|
-
|
|
1333
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Uranus", 7, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
704
1334
|
|
|
705
1335
|
# Calculate Neptune
|
|
706
1336
|
if should_calculate("Neptune"):
|
|
707
|
-
|
|
1337
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Neptune", 8, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
708
1338
|
|
|
709
1339
|
# Calculate Pluto
|
|
710
1340
|
if should_calculate("Pluto"):
|
|
711
|
-
|
|
1341
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Pluto", 9, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
712
1342
|
|
|
713
1343
|
# ==================
|
|
714
1344
|
# LUNAR NODES
|
|
715
1345
|
# ==================
|
|
716
1346
|
|
|
717
|
-
# Calculate Mean Lunar Node
|
|
718
|
-
if should_calculate("
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
# Calculate
|
|
726
|
-
if should_calculate("
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
1347
|
+
# Calculate Mean North Lunar Node
|
|
1348
|
+
if should_calculate("Mean_North_Lunar_Node"):
|
|
1349
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Mean_North_Lunar_Node", 10, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
1350
|
+
# Get correct declination using equatorial coordinates
|
|
1351
|
+
if "mean_north_lunar_node" in data:
|
|
1352
|
+
mean_north_lunar_node_eq = swe.calc_ut(julian_day, 10, iflag | swe.FLG_EQUATORIAL)[0]
|
|
1353
|
+
data["mean_north_lunar_node"].declination = mean_north_lunar_node_eq[1] # Declination from equatorial coordinates
|
|
1354
|
+
|
|
1355
|
+
# Calculate True North Lunar Node
|
|
1356
|
+
if should_calculate("True_North_Lunar_Node"):
|
|
1357
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "True_North_Lunar_Node", 11, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
1358
|
+
# Get correct declination using equatorial coordinates
|
|
1359
|
+
if "true_north_lunar_node" in data:
|
|
1360
|
+
true_north_lunar_node_eq = swe.calc_ut(julian_day, 11, iflag | swe.FLG_EQUATORIAL)[0]
|
|
1361
|
+
data["true_north_lunar_node"].declination = true_north_lunar_node_eq[1] # Declination from equatorial coordinates
|
|
1362
|
+
|
|
1363
|
+
# Calculate Mean South Lunar Node (opposite to Mean North Lunar Node)
|
|
1364
|
+
if should_calculate("Mean_South_Lunar_Node") and "mean_north_lunar_node" in data:
|
|
1365
|
+
mean_south_lunar_node_deg = math.fmod(data["mean_north_lunar_node"].abs_pos + 180, 360)
|
|
1366
|
+
data["mean_south_lunar_node"] = get_kerykeion_point_from_degree(
|
|
1367
|
+
mean_south_lunar_node_deg, "Mean_South_Lunar_Node", point_type=point_type,
|
|
1368
|
+
speed=-data["mean_north_lunar_node"].speed if data["mean_north_lunar_node"].speed is not None else None,
|
|
1369
|
+
declination=-data["mean_north_lunar_node"].declination if data["mean_north_lunar_node"].declination is not None else None
|
|
730
1370
|
)
|
|
731
|
-
data["
|
|
732
|
-
data["
|
|
733
|
-
calculated_planets.append("
|
|
734
|
-
|
|
735
|
-
# Calculate True South Node (opposite to True North Node)
|
|
736
|
-
if should_calculate("
|
|
737
|
-
|
|
738
|
-
data["
|
|
739
|
-
|
|
1371
|
+
data["mean_south_lunar_node"].house = get_planet_house(mean_south_lunar_node_deg, houses_degree_ut)
|
|
1372
|
+
data["mean_south_lunar_node"].retrograde = data["mean_north_lunar_node"].retrograde
|
|
1373
|
+
calculated_planets.append("Mean_South_Lunar_Node")
|
|
1374
|
+
|
|
1375
|
+
# Calculate True South Lunar Node (opposite to True North Lunar Node)
|
|
1376
|
+
if should_calculate("True_South_Lunar_Node") and "true_north_lunar_node" in data:
|
|
1377
|
+
true_south_lunar_node_deg = math.fmod(data["true_north_lunar_node"].abs_pos + 180, 360)
|
|
1378
|
+
data["true_south_lunar_node"] = get_kerykeion_point_from_degree(
|
|
1379
|
+
true_south_lunar_node_deg, "True_South_Lunar_Node", point_type=point_type,
|
|
1380
|
+
speed=-data["true_north_lunar_node"].speed if data["true_north_lunar_node"].speed is not None else None,
|
|
1381
|
+
declination=-data["true_north_lunar_node"].declination if data["true_north_lunar_node"].declination is not None else None
|
|
740
1382
|
)
|
|
741
|
-
data["
|
|
742
|
-
data["
|
|
743
|
-
calculated_planets.append("
|
|
1383
|
+
data["true_south_lunar_node"].house = get_planet_house(true_south_lunar_node_deg, houses_degree_ut)
|
|
1384
|
+
data["true_south_lunar_node"].retrograde = data["true_north_lunar_node"].retrograde
|
|
1385
|
+
calculated_planets.append("True_South_Lunar_Node")
|
|
744
1386
|
|
|
745
1387
|
# ==================
|
|
746
1388
|
# LILITH POINTS
|
|
@@ -748,14 +1390,14 @@ class AstrologicalSubjectFactory:
|
|
|
748
1390
|
|
|
749
1391
|
# Calculate Mean Lilith (Mean Black Moon)
|
|
750
1392
|
if should_calculate("Mean_Lilith"):
|
|
751
|
-
|
|
1393
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
752
1394
|
data, "Mean_Lilith", 12, julian_day, iflag, houses_degree_ut,
|
|
753
1395
|
point_type, calculated_planets, active_points
|
|
754
1396
|
)
|
|
755
1397
|
|
|
756
1398
|
# Calculate True Lilith (Osculating Black Moon)
|
|
757
1399
|
if should_calculate("True_Lilith"):
|
|
758
|
-
|
|
1400
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
759
1401
|
data, "True_Lilith", 13, julian_day, iflag, houses_degree_ut,
|
|
760
1402
|
point_type, calculated_planets, active_points
|
|
761
1403
|
)
|
|
@@ -766,21 +1408,21 @@ class AstrologicalSubjectFactory:
|
|
|
766
1408
|
|
|
767
1409
|
# Calculate Earth - useful for heliocentric charts
|
|
768
1410
|
if should_calculate("Earth"):
|
|
769
|
-
|
|
1411
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
770
1412
|
data, "Earth", 14, julian_day, iflag, houses_degree_ut,
|
|
771
1413
|
point_type, calculated_planets, active_points
|
|
772
1414
|
)
|
|
773
1415
|
|
|
774
1416
|
# Calculate Chiron
|
|
775
1417
|
if should_calculate("Chiron"):
|
|
776
|
-
|
|
1418
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
777
1419
|
data, "Chiron", 15, julian_day, iflag, houses_degree_ut,
|
|
778
1420
|
point_type, calculated_planets, active_points
|
|
779
1421
|
)
|
|
780
1422
|
|
|
781
1423
|
# Calculate Pholus
|
|
782
1424
|
if should_calculate("Pholus"):
|
|
783
|
-
|
|
1425
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
784
1426
|
data, "Pholus", 16, julian_day, iflag, houses_degree_ut,
|
|
785
1427
|
point_type, calculated_planets, active_points
|
|
786
1428
|
)
|
|
@@ -791,28 +1433,28 @@ class AstrologicalSubjectFactory:
|
|
|
791
1433
|
|
|
792
1434
|
# Calculate Ceres
|
|
793
1435
|
if should_calculate("Ceres"):
|
|
794
|
-
|
|
1436
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
795
1437
|
data, "Ceres", 17, julian_day, iflag, houses_degree_ut,
|
|
796
1438
|
point_type, calculated_planets, active_points
|
|
797
1439
|
)
|
|
798
1440
|
|
|
799
1441
|
# Calculate Pallas
|
|
800
1442
|
if should_calculate("Pallas"):
|
|
801
|
-
|
|
1443
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
802
1444
|
data, "Pallas", 18, julian_day, iflag, houses_degree_ut,
|
|
803
1445
|
point_type, calculated_planets, active_points
|
|
804
1446
|
)
|
|
805
1447
|
|
|
806
1448
|
# Calculate Juno
|
|
807
1449
|
if should_calculate("Juno"):
|
|
808
|
-
|
|
1450
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
809
1451
|
data, "Juno", 19, julian_day, iflag, houses_degree_ut,
|
|
810
1452
|
point_type, calculated_planets, active_points
|
|
811
1453
|
)
|
|
812
1454
|
|
|
813
1455
|
# Calculate Vesta
|
|
814
1456
|
if should_calculate("Vesta"):
|
|
815
|
-
|
|
1457
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
816
1458
|
data, "Vesta", 20, julian_day, iflag, houses_degree_ut,
|
|
817
1459
|
point_type, calculated_planets, active_points
|
|
818
1460
|
)
|
|
@@ -825,7 +1467,7 @@ class AstrologicalSubjectFactory:
|
|
|
825
1467
|
if should_calculate("Eris"):
|
|
826
1468
|
try:
|
|
827
1469
|
eris_calc = swe.calc_ut(julian_day, swe.AST_OFFSET + 136199, iflag)[0]
|
|
828
|
-
data["eris"] = get_kerykeion_point_from_degree(eris_calc[0], "Eris", point_type=point_type)
|
|
1470
|
+
data["eris"] = get_kerykeion_point_from_degree(eris_calc[0], "Eris", point_type=point_type, speed=eris_calc[3], declination=eris_calc[1])
|
|
829
1471
|
data["eris"].house = get_planet_house(eris_calc[0], houses_degree_ut)
|
|
830
1472
|
data["eris"].retrograde = eris_calc[3] < 0
|
|
831
1473
|
calculated_planets.append("Eris")
|
|
@@ -837,7 +1479,7 @@ class AstrologicalSubjectFactory:
|
|
|
837
1479
|
if should_calculate("Sedna"):
|
|
838
1480
|
try:
|
|
839
1481
|
sedna_calc = swe.calc_ut(julian_day, swe.AST_OFFSET + 90377, iflag)[0]
|
|
840
|
-
data["sedna"] = get_kerykeion_point_from_degree(sedna_calc[0], "Sedna", point_type=point_type)
|
|
1482
|
+
data["sedna"] = get_kerykeion_point_from_degree(sedna_calc[0], "Sedna", point_type=point_type, speed=sedna_calc[3], declination=sedna_calc[1])
|
|
841
1483
|
data["sedna"].house = get_planet_house(sedna_calc[0], houses_degree_ut)
|
|
842
1484
|
data["sedna"].retrograde = sedna_calc[3] < 0
|
|
843
1485
|
calculated_planets.append("Sedna")
|
|
@@ -849,7 +1491,7 @@ class AstrologicalSubjectFactory:
|
|
|
849
1491
|
if should_calculate("Haumea"):
|
|
850
1492
|
try:
|
|
851
1493
|
haumea_calc = swe.calc_ut(julian_day, swe.AST_OFFSET + 136108, iflag)[0]
|
|
852
|
-
data["haumea"] = get_kerykeion_point_from_degree(haumea_calc[0], "Haumea", point_type=point_type)
|
|
1494
|
+
data["haumea"] = get_kerykeion_point_from_degree(haumea_calc[0], "Haumea", point_type=point_type, speed=haumea_calc[3], declination=haumea_calc[1])
|
|
853
1495
|
data["haumea"].house = get_planet_house(haumea_calc[0], houses_degree_ut)
|
|
854
1496
|
data["haumea"].retrograde = haumea_calc[3] < 0
|
|
855
1497
|
calculated_planets.append("Haumea")
|
|
@@ -861,7 +1503,7 @@ class AstrologicalSubjectFactory:
|
|
|
861
1503
|
if should_calculate("Makemake"):
|
|
862
1504
|
try:
|
|
863
1505
|
makemake_calc = swe.calc_ut(julian_day, swe.AST_OFFSET + 136472, iflag)[0]
|
|
864
|
-
data["makemake"] = get_kerykeion_point_from_degree(makemake_calc[0], "Makemake", point_type=point_type)
|
|
1506
|
+
data["makemake"] = get_kerykeion_point_from_degree(makemake_calc[0], "Makemake", point_type=point_type, speed=makemake_calc[3], declination=makemake_calc[1])
|
|
865
1507
|
data["makemake"].house = get_planet_house(makemake_calc[0], houses_degree_ut)
|
|
866
1508
|
data["makemake"].retrograde = makemake_calc[3] < 0
|
|
867
1509
|
calculated_planets.append("Makemake")
|
|
@@ -873,7 +1515,7 @@ class AstrologicalSubjectFactory:
|
|
|
873
1515
|
if should_calculate("Ixion"):
|
|
874
1516
|
try:
|
|
875
1517
|
ixion_calc = swe.calc_ut(julian_day, swe.AST_OFFSET + 28978, iflag)[0]
|
|
876
|
-
data["ixion"] = get_kerykeion_point_from_degree(ixion_calc[0], "Ixion", point_type=point_type)
|
|
1518
|
+
data["ixion"] = get_kerykeion_point_from_degree(ixion_calc[0], "Ixion", point_type=point_type, speed=ixion_calc[3], declination=ixion_calc[1])
|
|
877
1519
|
data["ixion"].house = get_planet_house(ixion_calc[0], houses_degree_ut)
|
|
878
1520
|
data["ixion"].retrograde = ixion_calc[3] < 0
|
|
879
1521
|
calculated_planets.append("Ixion")
|
|
@@ -885,7 +1527,7 @@ class AstrologicalSubjectFactory:
|
|
|
885
1527
|
if should_calculate("Orcus"):
|
|
886
1528
|
try:
|
|
887
1529
|
orcus_calc = swe.calc_ut(julian_day, swe.AST_OFFSET + 90482, iflag)[0]
|
|
888
|
-
data["orcus"] = get_kerykeion_point_from_degree(orcus_calc[0], "Orcus", point_type=point_type)
|
|
1530
|
+
data["orcus"] = get_kerykeion_point_from_degree(orcus_calc[0], "Orcus", point_type=point_type, speed=orcus_calc[3], declination=orcus_calc[1])
|
|
889
1531
|
data["orcus"].house = get_planet_house(orcus_calc[0], houses_degree_ut)
|
|
890
1532
|
data["orcus"].retrograde = orcus_calc[3] < 0
|
|
891
1533
|
calculated_planets.append("Orcus")
|
|
@@ -897,7 +1539,7 @@ class AstrologicalSubjectFactory:
|
|
|
897
1539
|
if should_calculate("Quaoar"):
|
|
898
1540
|
try:
|
|
899
1541
|
quaoar_calc = swe.calc_ut(julian_day, swe.AST_OFFSET + 50000, iflag)[0]
|
|
900
|
-
data["quaoar"] = get_kerykeion_point_from_degree(quaoar_calc[0], "Quaoar", point_type=point_type)
|
|
1542
|
+
data["quaoar"] = get_kerykeion_point_from_degree(quaoar_calc[0], "Quaoar", point_type=point_type, speed=quaoar_calc[3], declination=quaoar_calc[1])
|
|
901
1543
|
data["quaoar"].house = get_planet_house(quaoar_calc[0], houses_degree_ut)
|
|
902
1544
|
data["quaoar"].retrograde = quaoar_calc[3] < 0
|
|
903
1545
|
calculated_planets.append("Quaoar")
|
|
@@ -912,10 +1554,12 @@ class AstrologicalSubjectFactory:
|
|
|
912
1554
|
# Calculate Regulus (example fixed star)
|
|
913
1555
|
if should_calculate("Regulus"):
|
|
914
1556
|
try:
|
|
915
|
-
star_name =
|
|
916
|
-
swe.fixstar_ut(star_name, julian_day, iflag)
|
|
917
|
-
regulus_deg =
|
|
918
|
-
|
|
1557
|
+
star_name = "Regulus"
|
|
1558
|
+
pos = swe.fixstar_ut(star_name, julian_day, iflag)[0]
|
|
1559
|
+
regulus_deg = pos[0]
|
|
1560
|
+
regulus_speed = pos[3] if len(pos) > 3 else 0.0 # Fixed stars have very slow speed
|
|
1561
|
+
regulus_dec = pos[1] if len(pos) > 1 else None # Declination
|
|
1562
|
+
data["regulus"] = get_kerykeion_point_from_degree(regulus_deg, "Regulus", point_type=point_type, speed=regulus_speed, declination=regulus_dec)
|
|
919
1563
|
data["regulus"].house = get_planet_house(regulus_deg, houses_degree_ut)
|
|
920
1564
|
data["regulus"].retrograde = False # Fixed stars are never retrograde
|
|
921
1565
|
calculated_planets.append("Regulus")
|
|
@@ -926,10 +1570,12 @@ class AstrologicalSubjectFactory:
|
|
|
926
1570
|
# Calculate Spica (example fixed star)
|
|
927
1571
|
if should_calculate("Spica"):
|
|
928
1572
|
try:
|
|
929
|
-
star_name =
|
|
930
|
-
swe.fixstar_ut(star_name, julian_day, iflag)
|
|
931
|
-
spica_deg =
|
|
932
|
-
|
|
1573
|
+
star_name = "Spica"
|
|
1574
|
+
pos = swe.fixstar_ut(star_name, julian_day, iflag)[0]
|
|
1575
|
+
spica_deg = pos[0]
|
|
1576
|
+
spica_speed = pos[3] if len(pos) > 3 else 0.0 # Fixed stars have very slow speed
|
|
1577
|
+
spica_dec = pos[1] if len(pos) > 1 else None # Declination
|
|
1578
|
+
data["spica"] = get_kerykeion_point_from_degree(spica_deg, "Spica", point_type=point_type, speed=spica_speed, declination=spica_dec)
|
|
933
1579
|
data["spica"].house = get_planet_house(spica_deg, houses_degree_ut)
|
|
934
1580
|
data["spica"].retrograde = False # Fixed stars are never retrograde
|
|
935
1581
|
calculated_planets.append("Spica")
|
|
@@ -944,21 +1590,41 @@ class AstrologicalSubjectFactory:
|
|
|
944
1590
|
# Calculate Pars Fortunae (Part of Fortune)
|
|
945
1591
|
if should_calculate("Pars_Fortunae"):
|
|
946
1592
|
# Auto-activate required points with notification
|
|
947
|
-
|
|
948
|
-
missing_points = [point for point in
|
|
1593
|
+
pars_fortunae_required_points: List[AstrologicalPoint] = ["Ascendant", "Sun", "Moon"]
|
|
1594
|
+
missing_points = [point for point in pars_fortunae_required_points if point not in active_points]
|
|
949
1595
|
if missing_points:
|
|
950
1596
|
logging.info(f"Automatically adding required points for Pars_Fortunae: {missing_points}")
|
|
951
1597
|
active_points.extend(cast(List[AstrologicalPoint], missing_points))
|
|
952
1598
|
# Recalculate the missing points
|
|
953
1599
|
for point in missing_points:
|
|
954
|
-
if point == "
|
|
1600
|
+
if point == "Ascendant" and "ascendant" not in data:
|
|
1601
|
+
# Calculate Ascendant from houses data if needed
|
|
1602
|
+
if data["zodiac_type"] == "Sidereal":
|
|
1603
|
+
cusps, ascmc = swe.houses_ex(
|
|
1604
|
+
tjdut=data["julian_day"],
|
|
1605
|
+
lat=data["lat"],
|
|
1606
|
+
lon=data["lng"],
|
|
1607
|
+
hsys=str.encode(data["houses_system_identifier"]),
|
|
1608
|
+
flags=swe.FLG_SIDEREAL
|
|
1609
|
+
)
|
|
1610
|
+
else:
|
|
1611
|
+
cusps, ascmc = swe.houses(
|
|
1612
|
+
tjdut=data["julian_day"],
|
|
1613
|
+
lat=data["lat"],
|
|
1614
|
+
lon=data["lng"],
|
|
1615
|
+
hsys=str.encode(data["houses_system_identifier"])
|
|
1616
|
+
)
|
|
1617
|
+
data["ascendant"] = get_kerykeion_point_from_degree(ascmc[0], "Ascendant", point_type=point_type)
|
|
1618
|
+
data["ascendant"].house = get_planet_house(ascmc[0], houses_degree_ut)
|
|
1619
|
+
data["ascendant"].retrograde = False
|
|
1620
|
+
elif point == "Sun" and "sun" not in data:
|
|
955
1621
|
sun_calc = swe.calc_ut(julian_day, 0, iflag)[0]
|
|
956
|
-
data["sun"] = get_kerykeion_point_from_degree(sun_calc[0], "Sun", point_type=point_type)
|
|
1622
|
+
data["sun"] = get_kerykeion_point_from_degree(sun_calc[0], "Sun", point_type=point_type, speed=sun_calc[3], declination=sun_calc[1])
|
|
957
1623
|
data["sun"].house = get_planet_house(sun_calc[0], houses_degree_ut)
|
|
958
1624
|
data["sun"].retrograde = sun_calc[3] < 0
|
|
959
|
-
elif point == "Moon" and
|
|
1625
|
+
elif point == "Moon" and "moon" not in data:
|
|
960
1626
|
moon_calc = swe.calc_ut(julian_day, 1, iflag)[0]
|
|
961
|
-
data["moon"] = get_kerykeion_point_from_degree(moon_calc[0], "Moon", point_type=point_type)
|
|
1627
|
+
data["moon"] = get_kerykeion_point_from_degree(moon_calc[0], "Moon", point_type=point_type, speed=moon_calc[3], declination=moon_calc[1])
|
|
962
1628
|
data["moon"].house = get_planet_house(moon_calc[0], houses_degree_ut)
|
|
963
1629
|
data["moon"].retrograde = moon_calc[3] < 0
|
|
964
1630
|
|
|
@@ -985,21 +1651,41 @@ class AstrologicalSubjectFactory:
|
|
|
985
1651
|
# Calculate Pars Spiritus (Part of Spirit)
|
|
986
1652
|
if should_calculate("Pars_Spiritus"):
|
|
987
1653
|
# Auto-activate required points with notification
|
|
988
|
-
|
|
989
|
-
missing_points = [point for point in
|
|
1654
|
+
pars_spiritus_required_points: List[AstrologicalPoint] = ["Ascendant", "Sun", "Moon"]
|
|
1655
|
+
missing_points = [point for point in pars_spiritus_required_points if point not in active_points]
|
|
990
1656
|
if missing_points:
|
|
991
1657
|
logging.info(f"Automatically adding required points for Pars_Spiritus: {missing_points}")
|
|
992
1658
|
active_points.extend(cast(List[AstrologicalPoint], missing_points))
|
|
993
1659
|
# Recalculate the missing points
|
|
994
1660
|
for point in missing_points:
|
|
995
|
-
if point == "
|
|
1661
|
+
if point == "Ascendant" and "ascendant" not in data:
|
|
1662
|
+
# Calculate Ascendant from houses data if needed
|
|
1663
|
+
if data["zodiac_type"] == "Sidereal":
|
|
1664
|
+
cusps, ascmc = swe.houses_ex(
|
|
1665
|
+
tjdut=data["julian_day"],
|
|
1666
|
+
lat=data["lat"],
|
|
1667
|
+
lon=data["lng"],
|
|
1668
|
+
hsys=str.encode(data["houses_system_identifier"]),
|
|
1669
|
+
flags=swe.FLG_SIDEREAL
|
|
1670
|
+
)
|
|
1671
|
+
else:
|
|
1672
|
+
cusps, ascmc = swe.houses(
|
|
1673
|
+
tjdut=data["julian_day"],
|
|
1674
|
+
lat=data["lat"],
|
|
1675
|
+
lon=data["lng"],
|
|
1676
|
+
hsys=str.encode(data["houses_system_identifier"])
|
|
1677
|
+
)
|
|
1678
|
+
data["ascendant"] = get_kerykeion_point_from_degree(ascmc[0], "Ascendant", point_type=point_type)
|
|
1679
|
+
data["ascendant"].house = get_planet_house(ascmc[0], houses_degree_ut)
|
|
1680
|
+
data["ascendant"].retrograde = False
|
|
1681
|
+
elif point == "Sun" and "sun" not in data:
|
|
996
1682
|
sun_calc = swe.calc_ut(julian_day, 0, iflag)[0]
|
|
997
|
-
data["sun"] = get_kerykeion_point_from_degree(sun_calc[0], "Sun", point_type=point_type)
|
|
1683
|
+
data["sun"] = get_kerykeion_point_from_degree(sun_calc[0], "Sun", point_type=point_type, speed=sun_calc[3], declination=sun_calc[1])
|
|
998
1684
|
data["sun"].house = get_planet_house(sun_calc[0], houses_degree_ut)
|
|
999
1685
|
data["sun"].retrograde = sun_calc[3] < 0
|
|
1000
|
-
elif point == "Moon" and
|
|
1686
|
+
elif point == "Moon" and "moon" not in data:
|
|
1001
1687
|
moon_calc = swe.calc_ut(julian_day, 1, iflag)[0]
|
|
1002
|
-
data["moon"] = get_kerykeion_point_from_degree(moon_calc[0], "Moon", point_type=point_type)
|
|
1688
|
+
data["moon"] = get_kerykeion_point_from_degree(moon_calc[0], "Moon", point_type=point_type, speed=moon_calc[3], declination=moon_calc[1])
|
|
1003
1689
|
data["moon"].house = get_planet_house(moon_calc[0], houses_degree_ut)
|
|
1004
1690
|
data["moon"].retrograde = moon_calc[3] < 0
|
|
1005
1691
|
|
|
@@ -1025,21 +1711,41 @@ class AstrologicalSubjectFactory:
|
|
|
1025
1711
|
# Calculate Pars Amoris (Part of Eros/Love)
|
|
1026
1712
|
if should_calculate("Pars_Amoris"):
|
|
1027
1713
|
# Auto-activate required points with notification
|
|
1028
|
-
|
|
1029
|
-
missing_points = [point for point in
|
|
1714
|
+
pars_amoris_required_points: List[AstrologicalPoint] = ["Ascendant", "Venus", "Sun"]
|
|
1715
|
+
missing_points = [point for point in pars_amoris_required_points if point not in active_points]
|
|
1030
1716
|
if missing_points:
|
|
1031
1717
|
logging.info(f"Automatically adding required points for Pars_Amoris: {missing_points}")
|
|
1032
1718
|
active_points.extend(cast(List[AstrologicalPoint], missing_points))
|
|
1033
1719
|
# Recalculate the missing points
|
|
1034
1720
|
for point in missing_points:
|
|
1035
|
-
if point == "
|
|
1721
|
+
if point == "Ascendant" and "ascendant" not in data:
|
|
1722
|
+
# Calculate Ascendant from houses data if needed
|
|
1723
|
+
if data["zodiac_type"] == "Sidereal":
|
|
1724
|
+
cusps, ascmc = swe.houses_ex(
|
|
1725
|
+
tjdut=data["julian_day"],
|
|
1726
|
+
lat=data["lat"],
|
|
1727
|
+
lon=data["lng"],
|
|
1728
|
+
hsys=str.encode(data["houses_system_identifier"]),
|
|
1729
|
+
flags=swe.FLG_SIDEREAL
|
|
1730
|
+
)
|
|
1731
|
+
else:
|
|
1732
|
+
cusps, ascmc = swe.houses(
|
|
1733
|
+
tjdut=data["julian_day"],
|
|
1734
|
+
lat=data["lat"],
|
|
1735
|
+
lon=data["lng"],
|
|
1736
|
+
hsys=str.encode(data["houses_system_identifier"])
|
|
1737
|
+
)
|
|
1738
|
+
data["ascendant"] = get_kerykeion_point_from_degree(ascmc[0], "Ascendant", point_type=point_type)
|
|
1739
|
+
data["ascendant"].house = get_planet_house(ascmc[0], houses_degree_ut)
|
|
1740
|
+
data["ascendant"].retrograde = False
|
|
1741
|
+
elif point == "Sun" and "sun" not in data:
|
|
1036
1742
|
sun_calc = swe.calc_ut(julian_day, 0, iflag)[0]
|
|
1037
|
-
data["sun"] = get_kerykeion_point_from_degree(sun_calc[0], "Sun", point_type=point_type)
|
|
1743
|
+
data["sun"] = get_kerykeion_point_from_degree(sun_calc[0], "Sun", point_type=point_type, speed=sun_calc[3], declination=sun_calc[1])
|
|
1038
1744
|
data["sun"].house = get_planet_house(sun_calc[0], houses_degree_ut)
|
|
1039
1745
|
data["sun"].retrograde = sun_calc[3] < 0
|
|
1040
|
-
elif point == "Venus" and
|
|
1746
|
+
elif point == "Venus" and "venus" not in data:
|
|
1041
1747
|
venus_calc = swe.calc_ut(julian_day, 3, iflag)[0]
|
|
1042
|
-
data["venus"] = get_kerykeion_point_from_degree(venus_calc[0], "Venus", point_type=point_type)
|
|
1748
|
+
data["venus"] = get_kerykeion_point_from_degree(venus_calc[0], "Venus", point_type=point_type, speed=venus_calc[3], declination=venus_calc[1])
|
|
1043
1749
|
data["venus"].house = get_planet_house(venus_calc[0], houses_degree_ut)
|
|
1044
1750
|
data["venus"].retrograde = venus_calc[3] < 0
|
|
1045
1751
|
|
|
@@ -1056,21 +1762,41 @@ class AstrologicalSubjectFactory:
|
|
|
1056
1762
|
# Calculate Pars Fidei (Part of Faith)
|
|
1057
1763
|
if should_calculate("Pars_Fidei"):
|
|
1058
1764
|
# Auto-activate required points with notification
|
|
1059
|
-
|
|
1060
|
-
missing_points = [point for point in
|
|
1765
|
+
pars_fidei_required_points: List[AstrologicalPoint] = ["Ascendant", "Jupiter", "Saturn"]
|
|
1766
|
+
missing_points = [point for point in pars_fidei_required_points if point not in active_points]
|
|
1061
1767
|
if missing_points:
|
|
1062
1768
|
logging.info(f"Automatically adding required points for Pars_Fidei: {missing_points}")
|
|
1063
1769
|
active_points.extend(cast(List[AstrologicalPoint], missing_points))
|
|
1064
1770
|
# Recalculate the missing points
|
|
1065
1771
|
for point in missing_points:
|
|
1066
|
-
if point == "
|
|
1772
|
+
if point == "Ascendant" and "ascendant" not in data:
|
|
1773
|
+
# Calculate Ascendant from houses data if needed
|
|
1774
|
+
if data["zodiac_type"] == "Sidereal":
|
|
1775
|
+
cusps, ascmc = swe.houses_ex(
|
|
1776
|
+
tjdut=data["julian_day"],
|
|
1777
|
+
lat=data["lat"],
|
|
1778
|
+
lon=data["lng"],
|
|
1779
|
+
hsys=str.encode(data["houses_system_identifier"]),
|
|
1780
|
+
flags=swe.FLG_SIDEREAL
|
|
1781
|
+
)
|
|
1782
|
+
else:
|
|
1783
|
+
cusps, ascmc = swe.houses(
|
|
1784
|
+
tjdut=data["julian_day"],
|
|
1785
|
+
lat=data["lat"],
|
|
1786
|
+
lon=data["lng"],
|
|
1787
|
+
hsys=str.encode(data["houses_system_identifier"])
|
|
1788
|
+
)
|
|
1789
|
+
data["ascendant"] = get_kerykeion_point_from_degree(ascmc[0], "Ascendant", point_type=point_type)
|
|
1790
|
+
data["ascendant"].house = get_planet_house(ascmc[0], houses_degree_ut)
|
|
1791
|
+
data["ascendant"].retrograde = False
|
|
1792
|
+
elif point == "Jupiter" and "jupiter" not in data:
|
|
1067
1793
|
jupiter_calc = swe.calc_ut(julian_day, 5, iflag)[0]
|
|
1068
|
-
data["jupiter"] = get_kerykeion_point_from_degree(jupiter_calc[0], "Jupiter", point_type=point_type)
|
|
1794
|
+
data["jupiter"] = get_kerykeion_point_from_degree(jupiter_calc[0], "Jupiter", point_type=point_type, speed=jupiter_calc[3], declination=jupiter_calc[1])
|
|
1069
1795
|
data["jupiter"].house = get_planet_house(jupiter_calc[0], houses_degree_ut)
|
|
1070
1796
|
data["jupiter"].retrograde = jupiter_calc[3] < 0
|
|
1071
|
-
elif point == "Saturn" and
|
|
1797
|
+
elif point == "Saturn" and "saturn" not in data:
|
|
1072
1798
|
saturn_calc = swe.calc_ut(julian_day, 6, iflag)[0]
|
|
1073
|
-
data["saturn"] = get_kerykeion_point_from_degree(saturn_calc[0], "Saturn", point_type=point_type)
|
|
1799
|
+
data["saturn"] = get_kerykeion_point_from_degree(saturn_calc[0], "Saturn", point_type=point_type, speed=saturn_calc[3], declination=saturn_calc[1])
|
|
1074
1800
|
data["saturn"].house = get_planet_house(saturn_calc[0], houses_degree_ut)
|
|
1075
1801
|
data["saturn"].retrograde = saturn_calc[3] < 0
|
|
1076
1802
|
|
|
@@ -1084,8 +1810,8 @@ class AstrologicalSubjectFactory:
|
|
|
1084
1810
|
data["pars_fidei"].retrograde = False
|
|
1085
1811
|
calculated_planets.append("Pars_Fidei")
|
|
1086
1812
|
|
|
1087
|
-
# Calculate Vertex
|
|
1088
|
-
if should_calculate("Vertex"):
|
|
1813
|
+
# Calculate Vertex and/or Anti-Vertex
|
|
1814
|
+
if should_calculate("Vertex") or should_calculate("Anti_Vertex"):
|
|
1089
1815
|
try:
|
|
1090
1816
|
# Vertex is at ascmc[3] in Swiss Ephemeris
|
|
1091
1817
|
if data["zodiac_type"] == "Sidereal":
|
|
@@ -1105,40 +1831,71 @@ class AstrologicalSubjectFactory:
|
|
|
1105
1831
|
)
|
|
1106
1832
|
|
|
1107
1833
|
vertex_deg = ascmc[3]
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1834
|
+
|
|
1835
|
+
# Calculate Vertex if requested
|
|
1836
|
+
if should_calculate("Vertex"):
|
|
1837
|
+
data["vertex"] = get_kerykeion_point_from_degree(vertex_deg, "Vertex", point_type=point_type)
|
|
1838
|
+
data["vertex"].house = get_planet_house(vertex_deg, houses_degree_ut)
|
|
1839
|
+
data["vertex"].retrograde = False
|
|
1840
|
+
calculated_planets.append("Vertex")
|
|
1841
|
+
|
|
1842
|
+
# Calculate Anti-Vertex if requested
|
|
1843
|
+
if should_calculate("Anti_Vertex"):
|
|
1844
|
+
anti_vertex_deg = math.fmod(vertex_deg + 180, 360)
|
|
1845
|
+
data["anti_vertex"] = get_kerykeion_point_from_degree(anti_vertex_deg, "Anti_Vertex", point_type=point_type)
|
|
1846
|
+
data["anti_vertex"].house = get_planet_house(anti_vertex_deg, houses_degree_ut)
|
|
1847
|
+
data["anti_vertex"].retrograde = False
|
|
1848
|
+
calculated_planets.append("Anti_Vertex")
|
|
1849
|
+
|
|
1119
1850
|
except Exception as e:
|
|
1120
|
-
logging.warning("Could not calculate Vertex position, error: %s", e)
|
|
1121
|
-
|
|
1851
|
+
logging.warning("Could not calculate Vertex/Anti-Vertex position, error: %s", e)
|
|
1852
|
+
if "Vertex" in active_points:
|
|
1853
|
+
active_points.remove("Vertex")
|
|
1854
|
+
if "Anti_Vertex" in active_points:
|
|
1855
|
+
active_points.remove("Anti_Vertex")
|
|
1122
1856
|
|
|
1123
1857
|
# Store only the planets that were actually calculated
|
|
1124
|
-
|
|
1858
|
+
all_calculated_points = calculated_planets.copy()
|
|
1859
|
+
if calculated_axial_cusps:
|
|
1860
|
+
all_calculated_points.extend(calculated_axial_cusps)
|
|
1861
|
+
data["active_points"] = all_calculated_points
|
|
1125
1862
|
|
|
1126
|
-
@
|
|
1127
|
-
def _calculate_day_of_week(
|
|
1128
|
-
"""
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1863
|
+
@staticmethod
|
|
1864
|
+
def _calculate_day_of_week(data: Dict[str, Any]) -> None:
|
|
1865
|
+
"""
|
|
1866
|
+
Calculate the day of the week for the given astronomical event.
|
|
1867
|
+
|
|
1868
|
+
Determines the day of the week corresponding to the local datetime
|
|
1869
|
+
using the standard library for consistency.
|
|
1870
|
+
|
|
1871
|
+
Args:
|
|
1872
|
+
data (Dict[str, Any]): Calculation data dictionary containing
|
|
1873
|
+
iso_formatted_local_datetime. Updated with the calculated day_of_week string.
|
|
1874
|
+
|
|
1875
|
+
Side Effects:
|
|
1876
|
+
Updates data dictionary with:
|
|
1877
|
+
- day_of_week: Human-readable day name (e.g., "Monday", "Tuesday")
|
|
1878
|
+
"""
|
|
1879
|
+
dt = datetime.fromisoformat(data["iso_formatted_local_datetime"])
|
|
1880
|
+
data["day_of_week"] = dt.strftime("%A")
|
|
1134
1881
|
|
|
1135
1882
|
if __name__ == "__main__":
|
|
1883
|
+
from kerykeion.schemas.kr_literals import AstrologicalPoint
|
|
1884
|
+
|
|
1136
1885
|
# Example usage
|
|
1137
|
-
|
|
1886
|
+
new_active_points: List[AstrologicalPoint] = ['Sun', 'Moon', 'Mercury', 'Venus', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', 'Pluto', 'Mean_North_Lunar_Node', 'True_North_Lunar_Node', 'Mean_South_Lunar_Node', 'True_South_Lunar_Node', 'Chiron', 'Mean_Lilith', 'True_Lilith', 'Earth', 'Pholus', 'Ceres', 'Pallas', 'Juno', 'Vesta', 'Eris', 'Sedna', 'Haumea', 'Makemake', 'Ixion', 'Orcus', 'Quaoar', 'Regulus', 'Spica', 'Pars_Fortunae', 'Pars_Spiritus', 'Pars_Amoris', 'Pars_Fidei', 'Vertex', 'Anti_Vertex', 'Ascendant', 'Medium_Coeli', 'Descendant', 'Imum_Coeli']
|
|
1887
|
+
subject = AstrologicalSubjectFactory.from_current_time(name="Test Subject", active_points=new_active_points)
|
|
1138
1888
|
print(subject.sun)
|
|
1139
1889
|
print(subject.pars_amoris)
|
|
1140
1890
|
print(subject.eris)
|
|
1141
1891
|
print(subject.active_points)
|
|
1892
|
+
print(subject.pars_fidei)
|
|
1893
|
+
print("----")
|
|
1894
|
+
print(subject.anti_vertex)
|
|
1142
1895
|
|
|
1143
1896
|
# Create JSON output
|
|
1144
1897
|
json_string = subject.model_dump_json(exclude_none=True, indent=2)
|
|
1898
|
+
|
|
1899
|
+
# Write JSON to home
|
|
1900
|
+
with open(Path.home() / "kerykeion_subject_example.json", "w", encoding="utf-8") as f:
|
|
1901
|
+
f.write(json_string)
|