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
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ephemeris Data Factory Module
|
|
3
|
+
|
|
4
|
+
This module provides the EphemerisDataFactory class for generating time-series
|
|
5
|
+
astrological ephemeris data. It enables the creation of comprehensive astronomical
|
|
6
|
+
and astrological datasets across specified date ranges with flexible time intervals
|
|
7
|
+
and calculation parameters.
|
|
8
|
+
|
|
9
|
+
Key Features:
|
|
10
|
+
- Time-series ephemeris data generation
|
|
11
|
+
- Multiple time interval support (days, hours, minutes)
|
|
12
|
+
- Configurable astrological calculation systems
|
|
13
|
+
- Built-in performance safeguards and limits
|
|
14
|
+
- Multiple output formats (dictionaries or model instances)
|
|
15
|
+
- Complete AstrologicalSubject instance generation
|
|
16
|
+
|
|
17
|
+
The module supports both lightweight data extraction (via get_ephemeris_data)
|
|
18
|
+
and full-featured astrological analysis (via get_ephemeris_data_as_astrological_subjects),
|
|
19
|
+
making it suitable for various use cases from simple data collection to complex
|
|
20
|
+
astrological research and analysis applications.
|
|
21
|
+
|
|
22
|
+
Classes:
|
|
23
|
+
EphemerisDataFactory: Main factory class for generating ephemeris data
|
|
24
|
+
|
|
25
|
+
Dependencies:
|
|
26
|
+
- kerykeion.AstrologicalSubjectFactory: For creating astrological subjects
|
|
27
|
+
- kerykeion.utilities: For house and planetary data extraction
|
|
28
|
+
- kerykeion.schemas: For type definitions and model structures
|
|
29
|
+
- datetime: For date/time handling
|
|
30
|
+
- logging: For performance warnings
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
Basic usage for daily ephemeris data:
|
|
34
|
+
|
|
35
|
+
>>> from datetime import datetime
|
|
36
|
+
>>> from kerykeion.ephemeris_data_factory import EphemerisDataFactory
|
|
37
|
+
>>>
|
|
38
|
+
>>> start = datetime(2024, 1, 1)
|
|
39
|
+
>>> end = datetime(2024, 1, 31)
|
|
40
|
+
>>> factory = EphemerisDataFactory(start, end)
|
|
41
|
+
>>> data = factory.get_ephemeris_data()
|
|
42
|
+
>>> print(f"Generated {len(data)} data points")
|
|
43
|
+
|
|
44
|
+
Author: Giacomo Battaglia
|
|
45
|
+
Copyright: (C) 2025 Kerykeion Project
|
|
46
|
+
License: AGPL-3.0
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from kerykeion import AstrologicalSubjectFactory
|
|
50
|
+
from kerykeion.schemas.kr_models import AstrologicalSubjectModel
|
|
51
|
+
from kerykeion.utilities import (
|
|
52
|
+
get_houses_list,
|
|
53
|
+
get_available_astrological_points_list,
|
|
54
|
+
normalize_zodiac_type,
|
|
55
|
+
)
|
|
56
|
+
from kerykeion.astrological_subject_factory import DEFAULT_HOUSES_SYSTEM_IDENTIFIER, DEFAULT_PERSPECTIVE_TYPE, DEFAULT_ZODIAC_TYPE
|
|
57
|
+
from kerykeion.schemas import (
|
|
58
|
+
EphemerisDictModel,
|
|
59
|
+
SiderealMode,
|
|
60
|
+
HousesSystemIdentifier,
|
|
61
|
+
PerspectiveType,
|
|
62
|
+
ZodiacType,
|
|
63
|
+
)
|
|
64
|
+
from datetime import datetime, timedelta
|
|
65
|
+
from typing import Literal, Union, List
|
|
66
|
+
import logging
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EphemerisDataFactory:
|
|
70
|
+
"""
|
|
71
|
+
A factory class for generating ephemeris data over a specified date range.
|
|
72
|
+
|
|
73
|
+
This class calculates astrological ephemeris data (planetary positions and house cusps)
|
|
74
|
+
for a sequence of dates, allowing for detailed astronomical calculations across time periods.
|
|
75
|
+
It supports different time intervals (days, hours, or minutes) and various astrological
|
|
76
|
+
calculation systems.
|
|
77
|
+
|
|
78
|
+
The factory creates data points at regular intervals between start and end dates,
|
|
79
|
+
with built-in safeguards to prevent excessive computational loads through configurable
|
|
80
|
+
maximum limits.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
start_datetime (datetime): The starting date and time for ephemeris calculations.
|
|
84
|
+
end_datetime (datetime): The ending date and time for ephemeris calculations.
|
|
85
|
+
step_type (Literal["days", "hours", "minutes"], optional): The time interval unit
|
|
86
|
+
for data points. Defaults to "days".
|
|
87
|
+
step (int, optional): The number of units to advance for each data point.
|
|
88
|
+
For example, step=2 with step_type="days" creates data points every 2 days.
|
|
89
|
+
Defaults to 1.
|
|
90
|
+
lat (float, optional): Geographic latitude in decimal degrees for calculations.
|
|
91
|
+
Positive values for North, negative for South. Defaults to 51.4769 (Greenwich).
|
|
92
|
+
lng (float, optional): Geographic longitude in decimal degrees for calculations.
|
|
93
|
+
Positive values for East, negative for West. Defaults to 0.0005 (Greenwich).
|
|
94
|
+
tz_str (str, optional): Timezone identifier (e.g., "Europe/London", "America/New_York").
|
|
95
|
+
Defaults to "Etc/UTC".
|
|
96
|
+
is_dst (bool, optional): Whether daylight saving time is active for the location.
|
|
97
|
+
Only relevant for certain timezone calculations. Defaults to False.
|
|
98
|
+
zodiac_type (ZodiacType, optional): The zodiac system to use (tropical or sidereal).
|
|
99
|
+
Defaults to DEFAULT_ZODIAC_TYPE.
|
|
100
|
+
sidereal_mode (Union[SiderealMode, None], optional): The sidereal calculation mode
|
|
101
|
+
if using sidereal zodiac. Only applies when zodiac_type is sidereal.
|
|
102
|
+
Defaults to None.
|
|
103
|
+
houses_system_identifier (HousesSystemIdentifier, optional): The house system
|
|
104
|
+
for astrological house calculations (e.g., Placidus, Koch, Equal).
|
|
105
|
+
Defaults to DEFAULT_HOUSES_SYSTEM_IDENTIFIER.
|
|
106
|
+
perspective_type (PerspectiveType, optional): The calculation perspective
|
|
107
|
+
(geocentric, heliocentric, etc.). Defaults to DEFAULT_PERSPECTIVE_TYPE.
|
|
108
|
+
max_days (Union[int, None], optional): Maximum number of daily data points allowed.
|
|
109
|
+
Set to None to disable this safety check. Defaults to 730 (2 years).
|
|
110
|
+
max_hours (Union[int, None], optional): Maximum number of hourly data points allowed.
|
|
111
|
+
Set to None to disable this safety check. Defaults to 8760 (1 year).
|
|
112
|
+
max_minutes (Union[int, None], optional): Maximum number of minute-interval data points.
|
|
113
|
+
Set to None to disable this safety check. Defaults to 525600 (1 year).
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
ValueError: If step_type is not one of "days", "hours", or "minutes".
|
|
117
|
+
ValueError: If the calculated number of data points exceeds the respective maximum limit.
|
|
118
|
+
ValueError: If no valid dates are generated from the input parameters.
|
|
119
|
+
|
|
120
|
+
Examples:
|
|
121
|
+
Create daily ephemeris data for a month:
|
|
122
|
+
|
|
123
|
+
>>> from datetime import datetime
|
|
124
|
+
>>> start = datetime(2024, 1, 1)
|
|
125
|
+
>>> end = datetime(2024, 1, 31)
|
|
126
|
+
>>> factory = EphemerisDataFactory(start, end)
|
|
127
|
+
>>> data = factory.get_ephemeris_data()
|
|
128
|
+
|
|
129
|
+
Create hourly data for a specific location:
|
|
130
|
+
|
|
131
|
+
>>> factory = EphemerisDataFactory(
|
|
132
|
+
... start, end,
|
|
133
|
+
... step_type="hours",
|
|
134
|
+
... lat=40.7128, # New York
|
|
135
|
+
... lng=-74.0060,
|
|
136
|
+
... tz_str="America/New_York"
|
|
137
|
+
... )
|
|
138
|
+
>>> subjects = factory.get_ephemeris_data_as_astrological_subjects()
|
|
139
|
+
|
|
140
|
+
Note:
|
|
141
|
+
Large date ranges with small step intervals can generate thousands of data points,
|
|
142
|
+
which may require significant computation time and memory. The factory includes
|
|
143
|
+
warnings for calculations exceeding 1000 data points and enforces maximum limits
|
|
144
|
+
to prevent system overload.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def __init__(
|
|
148
|
+
self,
|
|
149
|
+
start_datetime: datetime,
|
|
150
|
+
end_datetime: datetime,
|
|
151
|
+
step_type: Literal["days", "hours", "minutes"] = "days",
|
|
152
|
+
step: int = 1,
|
|
153
|
+
lat: float = 51.4769,
|
|
154
|
+
lng: float = 0.0005,
|
|
155
|
+
tz_str: str = "Etc/UTC",
|
|
156
|
+
is_dst: bool = False,
|
|
157
|
+
zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE,
|
|
158
|
+
sidereal_mode: Union[SiderealMode, None] = None,
|
|
159
|
+
houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
|
|
160
|
+
perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE,
|
|
161
|
+
max_days: Union[int, None] = 730,
|
|
162
|
+
max_hours: Union[int, None] = 8760,
|
|
163
|
+
max_minutes: Union[int, None] = 525600,
|
|
164
|
+
):
|
|
165
|
+
self.start_datetime = start_datetime
|
|
166
|
+
self.end_datetime = end_datetime
|
|
167
|
+
self.step_type = step_type
|
|
168
|
+
self.step = step
|
|
169
|
+
self.lat = lat
|
|
170
|
+
self.lng = lng
|
|
171
|
+
self.tz_str = tz_str
|
|
172
|
+
self.is_dst = is_dst
|
|
173
|
+
self.zodiac_type = normalize_zodiac_type(zodiac_type)
|
|
174
|
+
self.sidereal_mode = sidereal_mode
|
|
175
|
+
self.houses_system_identifier = houses_system_identifier
|
|
176
|
+
self.perspective_type = perspective_type
|
|
177
|
+
self.max_days = max_days
|
|
178
|
+
self.max_hours = max_hours
|
|
179
|
+
self.max_minutes = max_minutes
|
|
180
|
+
|
|
181
|
+
self.dates_list = []
|
|
182
|
+
if self.step_type == "days":
|
|
183
|
+
self.dates_list = [self.start_datetime + timedelta(days=i * self.step) for i in range((self.end_datetime - self.start_datetime).days // self.step + 1)]
|
|
184
|
+
if max_days and (len(self.dates_list) > max_days):
|
|
185
|
+
raise ValueError(f"Too many days: {len(self.dates_list)} > {self.max_days}. To prevent this error, set max_days to a higher value or reduce the date range.")
|
|
186
|
+
|
|
187
|
+
elif self.step_type == "hours":
|
|
188
|
+
hours_diff = (self.end_datetime - self.start_datetime).total_seconds() / 3600
|
|
189
|
+
self.dates_list = [self.start_datetime + timedelta(hours=i * self.step) for i in range(int(hours_diff) // self.step + 1)]
|
|
190
|
+
if max_hours and (len(self.dates_list) > max_hours):
|
|
191
|
+
raise ValueError(f"Too many hours: {len(self.dates_list)} > {self.max_hours}. To prevent this error, set max_hours to a higher value or reduce the date range.")
|
|
192
|
+
|
|
193
|
+
elif self.step_type == "minutes":
|
|
194
|
+
minutes_diff = (self.end_datetime - self.start_datetime).total_seconds() / 60
|
|
195
|
+
self.dates_list = [self.start_datetime + timedelta(minutes=i * self.step) for i in range(int(minutes_diff) // self.step + 1)]
|
|
196
|
+
if max_minutes and (len(self.dates_list) > max_minutes):
|
|
197
|
+
raise ValueError(f"Too many minutes: {len(self.dates_list)} > {self.max_minutes}. To prevent this error, set max_minutes to a higher value or reduce the date range.")
|
|
198
|
+
|
|
199
|
+
else:
|
|
200
|
+
raise ValueError(f"Invalid step type: {self.step_type}")
|
|
201
|
+
|
|
202
|
+
if not self.dates_list:
|
|
203
|
+
raise ValueError("No dates found. Check the date range and step values.")
|
|
204
|
+
|
|
205
|
+
if len(self.dates_list) > 1000:
|
|
206
|
+
logging.warning(f"Large number of dates: {len(self.dates_list)}. The calculation may take a while.")
|
|
207
|
+
|
|
208
|
+
def get_ephemeris_data(self, as_model: bool = False) -> list:
|
|
209
|
+
"""
|
|
210
|
+
Generate ephemeris data for the specified date range.
|
|
211
|
+
|
|
212
|
+
This method creates a comprehensive dataset containing planetary positions and
|
|
213
|
+
astrological house cusps for each date in the configured time series. The data
|
|
214
|
+
is structured for easy consumption by astrological applications and analysis tools.
|
|
215
|
+
|
|
216
|
+
The returned data includes all available astrological points (planets, asteroids,
|
|
217
|
+
lunar nodes, etc.) as configured by the perspective type, along with complete
|
|
218
|
+
house cusp information for each calculated moment.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
as_model (bool, optional): If True, returns data as validated model instances
|
|
222
|
+
(EphemerisDictModel objects) which provide type safety and validation.
|
|
223
|
+
If False, returns raw dictionary data for maximum flexibility.
|
|
224
|
+
Defaults to False.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
list: A list of ephemeris data points, where each element represents one
|
|
228
|
+
calculated moment in time. The structure depends on the as_model parameter:
|
|
229
|
+
|
|
230
|
+
If as_model=False (default):
|
|
231
|
+
List of dictionaries with keys:
|
|
232
|
+
- "date" (str): ISO format datetime string (e.g., "2020-01-01T00:00:00")
|
|
233
|
+
- "planets" (list): List of dictionaries, each containing planetary data
|
|
234
|
+
with keys like 'name', 'abs_pos', 'lon', 'lat', 'dist', 'speed', etc.
|
|
235
|
+
- "houses" (list): List of dictionaries containing house cusp data
|
|
236
|
+
with keys like 'name', 'abs_pos', 'lon', etc.
|
|
237
|
+
|
|
238
|
+
If as_model=True:
|
|
239
|
+
List of EphemerisDictModel instances providing the same data
|
|
240
|
+
with type validation and structured access.
|
|
241
|
+
|
|
242
|
+
Examples:
|
|
243
|
+
Basic usage with dictionary output:
|
|
244
|
+
|
|
245
|
+
>>> factory = EphemerisDataFactory(start_date, end_date)
|
|
246
|
+
>>> data = factory.get_ephemeris_data()
|
|
247
|
+
>>> print(f"Sun position: {data[0]['planets'][0]['abs_pos']}")
|
|
248
|
+
>>> print(f"First house cusp: {data[0]['houses'][0]['abs_pos']}")
|
|
249
|
+
|
|
250
|
+
Using model instances for type safety:
|
|
251
|
+
|
|
252
|
+
>>> data_models = factory.get_ephemeris_data(as_model=True)
|
|
253
|
+
>>> first_point = data_models[0]
|
|
254
|
+
>>> print(f"Date: {first_point.date}")
|
|
255
|
+
>>> print(f"Number of planets: {len(first_point.planets)}")
|
|
256
|
+
|
|
257
|
+
Note:
|
|
258
|
+
- The calculation time is proportional to the number of data points
|
|
259
|
+
- For large datasets (>1000 points), consider using the method in batches
|
|
260
|
+
- Planet order and availability depend on the configured perspective type
|
|
261
|
+
- House system affects the house cusp calculations
|
|
262
|
+
- All positions are in the configured zodiac system (tropical/sidereal)
|
|
263
|
+
"""
|
|
264
|
+
ephemeris_data_list = []
|
|
265
|
+
for date in self.dates_list:
|
|
266
|
+
subject = AstrologicalSubjectFactory.from_birth_data(
|
|
267
|
+
year=date.year,
|
|
268
|
+
month=date.month,
|
|
269
|
+
day=date.day,
|
|
270
|
+
hour=date.hour,
|
|
271
|
+
minute=date.minute,
|
|
272
|
+
lng=self.lng,
|
|
273
|
+
lat=self.lat,
|
|
274
|
+
tz_str=self.tz_str,
|
|
275
|
+
city="Placeholder",
|
|
276
|
+
nation="Placeholder",
|
|
277
|
+
online=False,
|
|
278
|
+
zodiac_type=self.zodiac_type,
|
|
279
|
+
sidereal_mode=self.sidereal_mode,
|
|
280
|
+
houses_system_identifier=self.houses_system_identifier,
|
|
281
|
+
perspective_type=self.perspective_type,
|
|
282
|
+
is_dst=self.is_dst,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
houses_list = get_houses_list(subject)
|
|
286
|
+
available_planets = get_available_astrological_points_list(subject)
|
|
287
|
+
|
|
288
|
+
ephemeris_data_list.append({"date": date.isoformat(), "planets": available_planets, "houses": houses_list})
|
|
289
|
+
|
|
290
|
+
if as_model:
|
|
291
|
+
# Type narrowing: at this point, the dict structure matches EphemerisDictModel
|
|
292
|
+
return [EphemerisDictModel(date=data["date"], planets=data["planets"], houses=data["houses"]) for data in ephemeris_data_list] # type: ignore
|
|
293
|
+
|
|
294
|
+
return ephemeris_data_list
|
|
295
|
+
|
|
296
|
+
def get_ephemeris_data_as_astrological_subjects(self, as_model: bool = False) -> List[AstrologicalSubjectModel]:
|
|
297
|
+
"""
|
|
298
|
+
Generate ephemeris data as complete AstrologicalSubject instances.
|
|
299
|
+
|
|
300
|
+
This method creates fully-featured AstrologicalSubject objects for each date in the
|
|
301
|
+
configured time series, providing access to all astrological calculation methods
|
|
302
|
+
and properties. Unlike the dictionary-based approach of get_ephemeris_data(),
|
|
303
|
+
this method returns objects with the complete Kerykeion API available.
|
|
304
|
+
|
|
305
|
+
Each AstrologicalSubject instance represents a complete astrological chart for
|
|
306
|
+
the specified moment, location, and calculation settings. This allows direct
|
|
307
|
+
access to methods like get_sun(), get_all_points(), draw_chart(), calculate
|
|
308
|
+
aspects, and all other astrological analysis features.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
as_model (bool, optional): If True, returns AstrologicalSubjectModel instances
|
|
312
|
+
(Pydantic model versions) which provide serialization and validation features.
|
|
313
|
+
If False, returns raw AstrologicalSubject instances with full method access.
|
|
314
|
+
Defaults to False.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
List[AstrologicalSubjectModel]: A list of AstrologicalSubject or
|
|
318
|
+
AstrologicalSubjectModel instances (depending on as_model parameter).
|
|
319
|
+
Each element represents one calculated moment in time with full
|
|
320
|
+
astrological chart data and methods available.
|
|
321
|
+
|
|
322
|
+
Each subject contains:
|
|
323
|
+
- All planetary and astrological point positions
|
|
324
|
+
- Complete house system calculations
|
|
325
|
+
- Chart drawing capabilities
|
|
326
|
+
- Aspect calculation methods
|
|
327
|
+
- Access to all Kerykeion astrological features
|
|
328
|
+
|
|
329
|
+
Examples:
|
|
330
|
+
Basic usage for accessing individual chart features:
|
|
331
|
+
|
|
332
|
+
>>> factory = EphemerisDataFactory(start_date, end_date)
|
|
333
|
+
>>> subjects = factory.get_ephemeris_data_as_astrological_subjects()
|
|
334
|
+
>>>
|
|
335
|
+
>>> # Access specific planetary data
|
|
336
|
+
>>> sun_data = subjects[0].get_sun()
|
|
337
|
+
>>> moon_data = subjects[0].get_moon()
|
|
338
|
+
>>>
|
|
339
|
+
>>> # Get all astrological points
|
|
340
|
+
>>> all_points = subjects[0].get_all_points()
|
|
341
|
+
>>>
|
|
342
|
+
>>> # Generate chart visualization
|
|
343
|
+
>>> chart_svg = subjects[0].draw_chart()
|
|
344
|
+
|
|
345
|
+
Using model instances for serialization:
|
|
346
|
+
|
|
347
|
+
>>> subjects_models = factory.get_ephemeris_data_as_astrological_subjects(as_model=True)
|
|
348
|
+
>>> # Model instances can be easily serialized to JSON
|
|
349
|
+
>>> json_data = subjects_models[0].model_dump_json()
|
|
350
|
+
|
|
351
|
+
Batch processing for analysis:
|
|
352
|
+
|
|
353
|
+
>>> subjects = factory.get_ephemeris_data_as_astrological_subjects()
|
|
354
|
+
>>> sun_positions = [subj.sun['abs_pos'] for subj in subjects if subj.sun]
|
|
355
|
+
>>> # Analyze sun position changes over time
|
|
356
|
+
|
|
357
|
+
Use Cases:
|
|
358
|
+
- Time-series astrological analysis
|
|
359
|
+
- Planetary motion tracking
|
|
360
|
+
- Aspect pattern analysis over time
|
|
361
|
+
- Chart animation data generation
|
|
362
|
+
- Astrological research and statistics
|
|
363
|
+
- Progressive chart calculations
|
|
364
|
+
|
|
365
|
+
Performance Notes:
|
|
366
|
+
- More computationally intensive than get_ephemeris_data()
|
|
367
|
+
- Each subject performs full astrological calculations
|
|
368
|
+
- Memory usage scales with the number of data points
|
|
369
|
+
- Consider processing in batches for very large date ranges
|
|
370
|
+
- Ideal for comprehensive analysis requiring full chart features
|
|
371
|
+
|
|
372
|
+
See Also:
|
|
373
|
+
get_ephemeris_data(): For lightweight dictionary-based ephemeris data
|
|
374
|
+
AstrologicalSubject: For details on available methods and properties
|
|
375
|
+
"""
|
|
376
|
+
subjects_list = []
|
|
377
|
+
for date in self.dates_list:
|
|
378
|
+
subject = AstrologicalSubjectFactory.from_birth_data(
|
|
379
|
+
year=date.year,
|
|
380
|
+
month=date.month,
|
|
381
|
+
day=date.day,
|
|
382
|
+
hour=date.hour,
|
|
383
|
+
minute=date.minute,
|
|
384
|
+
lng=self.lng,
|
|
385
|
+
lat=self.lat,
|
|
386
|
+
tz_str=self.tz_str,
|
|
387
|
+
city="Placeholder",
|
|
388
|
+
nation="Placeholder",
|
|
389
|
+
online=False,
|
|
390
|
+
zodiac_type=self.zodiac_type,
|
|
391
|
+
sidereal_mode=self.sidereal_mode,
|
|
392
|
+
houses_system_identifier=self.houses_system_identifier,
|
|
393
|
+
perspective_type=self.perspective_type,
|
|
394
|
+
is_dst=self.is_dst,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
if as_model:
|
|
398
|
+
subjects_list.append(subject)
|
|
399
|
+
else:
|
|
400
|
+
subjects_list.append(subject)
|
|
401
|
+
|
|
402
|
+
return subjects_list
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
if __name__ == "__main__":
|
|
406
|
+
start_date = datetime.fromisoformat("2020-01-01")
|
|
407
|
+
end_date = datetime.fromisoformat("2020-01-03")
|
|
408
|
+
|
|
409
|
+
factory = EphemerisDataFactory(
|
|
410
|
+
start_datetime=start_date,
|
|
411
|
+
end_datetime=end_date,
|
|
412
|
+
step_type="minutes",
|
|
413
|
+
step=60, # One hour intervals to make the example more manageable
|
|
414
|
+
lat=37.9838,
|
|
415
|
+
lng=23.7275,
|
|
416
|
+
tz_str="Europe/Athens",
|
|
417
|
+
is_dst=False,
|
|
418
|
+
max_hours=None,
|
|
419
|
+
max_minutes=None,
|
|
420
|
+
max_days=None,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# Test original method
|
|
424
|
+
ephemeris_data = factory.get_ephemeris_data(as_model=True)
|
|
425
|
+
print(f"Number of ephemeris data points: {len(ephemeris_data)}")
|
|
426
|
+
print(f"First data point date: {ephemeris_data[0].date}")
|
|
427
|
+
|
|
428
|
+
# Test new method
|
|
429
|
+
subjects = factory.get_ephemeris_data_as_astrological_subjects()
|
|
430
|
+
print(f"Number of astrological subjects: {len(subjects)}")
|
|
431
|
+
print(f"First subject sun position: {subjects[0].sun}")
|
|
432
|
+
|
|
433
|
+
# Example of accessing more data from the first subject
|
|
434
|
+
first_subject = subjects[0]
|
|
435
|
+
if first_subject.sun is not None:
|
|
436
|
+
print(f"Sun sign: {first_subject.sun['sign']}")
|
|
437
|
+
|
|
438
|
+
# Compare sun positions from both methods
|
|
439
|
+
for i in range(min(3, len(subjects))):
|
|
440
|
+
print(f"Date: {ephemeris_data[i].date}")
|
|
441
|
+
if len(ephemeris_data[i].planets) > 0:
|
|
442
|
+
print(f"Sun position from dict: {ephemeris_data[i].planets[0]['abs_pos']}")
|
|
443
|
+
print("---")
|
kerykeion/fetch_geonames.py
CHANGED
|
@@ -1,36 +1,58 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""
|
|
3
|
-
|
|
3
|
+
Author: Giacomo Battaglia
|
|
4
|
+
Copyright: (C) 2025 Kerykeion Project
|
|
5
|
+
License: AGPL-3.0
|
|
4
6
|
"""
|
|
5
7
|
|
|
6
8
|
|
|
7
|
-
import
|
|
9
|
+
from logging import getLogger
|
|
8
10
|
from datetime import timedelta
|
|
11
|
+
from os import getenv
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional, Union
|
|
14
|
+
|
|
9
15
|
from requests import Request
|
|
10
16
|
from requests_cache import CachedSession
|
|
11
|
-
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
logger = getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
DEFAULT_GEONAMES_CACHE_NAME = Path("cache") / "kerykeion_geonames_cache"
|
|
23
|
+
GEONAMES_CACHE_ENV_VAR = "KERYKEION_GEONAMES_CACHE_NAME"
|
|
12
24
|
|
|
13
25
|
|
|
14
26
|
class FetchGeonames:
|
|
15
27
|
"""
|
|
16
|
-
Class to handle requests to the GeoNames API
|
|
28
|
+
Class to handle requests to the GeoNames API for location data and timezone information.
|
|
29
|
+
|
|
30
|
+
This class provides cached access to the GeoNames API to retrieve location coordinates,
|
|
31
|
+
timezone information, and other geographical data for astrological calculations.
|
|
17
32
|
|
|
18
33
|
Args:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
city_name: Name of the city to search for.
|
|
35
|
+
country_code: Two-letter country code (ISO 3166-1 alpha-2).
|
|
36
|
+
username: GeoNames username for API access, defaults to "century.boy".
|
|
37
|
+
cache_expire_after_days: Number of days to cache responses, defaults to 30.
|
|
38
|
+
cache_name: Optional path (directory or filename stem) used by requests-cache.
|
|
39
|
+
Defaults to "cache/kerykeion_geonames_cache" and may also be overridden
|
|
40
|
+
via the environment variable ``KERYKEION_GEONAMES_CACHE_NAME`` or by
|
|
41
|
+
calling :meth:`FetchGeonames.set_default_cache_name`.
|
|
23
42
|
"""
|
|
24
43
|
|
|
44
|
+
default_cache_name: Path = DEFAULT_GEONAMES_CACHE_NAME
|
|
45
|
+
|
|
25
46
|
def __init__(
|
|
26
47
|
self,
|
|
27
48
|
city_name: str,
|
|
28
49
|
country_code: str,
|
|
29
50
|
username: str = "century.boy",
|
|
30
51
|
cache_expire_after_days=30,
|
|
52
|
+
cache_name: Optional[Union[str, Path]] = None,
|
|
31
53
|
):
|
|
32
54
|
self.session = CachedSession(
|
|
33
|
-
cache_name=
|
|
55
|
+
cache_name=str(self._resolve_cache_name(cache_name)),
|
|
34
56
|
backend="sqlite",
|
|
35
57
|
expire_after=timedelta(days=cache_expire_after_days),
|
|
36
58
|
)
|
|
@@ -41,9 +63,35 @@ class FetchGeonames:
|
|
|
41
63
|
self.base_url = "http://api.geonames.org/searchJSON"
|
|
42
64
|
self.timezone_url = "http://api.geonames.org/timezoneJSON"
|
|
43
65
|
|
|
66
|
+
@classmethod
|
|
67
|
+
def set_default_cache_name(cls, cache_name: Union[str, Path]) -> None:
|
|
68
|
+
"""Override the default cache name used when none is provided."""
|
|
69
|
+
|
|
70
|
+
cls.default_cache_name = Path(cache_name)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def _resolve_cache_name(cls, cache_name: Optional[Union[str, Path]]) -> Path:
|
|
74
|
+
"""Return the resolved cache name applying overrides in priority order."""
|
|
75
|
+
|
|
76
|
+
if cache_name is not None:
|
|
77
|
+
return Path(cache_name)
|
|
78
|
+
|
|
79
|
+
env_override = getenv(GEONAMES_CACHE_ENV_VAR)
|
|
80
|
+
if env_override:
|
|
81
|
+
return Path(env_override)
|
|
82
|
+
|
|
83
|
+
return cls.default_cache_name
|
|
84
|
+
|
|
44
85
|
def __get_timezone(self, lat: Union[str, float, int], lon: Union[str, float, int]) -> dict[str, str]:
|
|
45
86
|
"""
|
|
46
|
-
Get
|
|
87
|
+
Get timezone information for a given latitude and longitude.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
lat: Latitude coordinate.
|
|
91
|
+
lon: Longitude coordinate.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
dict: Timezone data including timezone string and cache status.
|
|
47
95
|
"""
|
|
48
96
|
# Dictionary that will be returned:
|
|
49
97
|
timezone_data = {}
|
|
@@ -51,21 +99,21 @@ class FetchGeonames:
|
|
|
51
99
|
params = {"lat": lat, "lng": lon, "username": self.username}
|
|
52
100
|
|
|
53
101
|
prepared_request = Request("GET", self.timezone_url, params=params).prepare()
|
|
54
|
-
|
|
102
|
+
logger.debug("GeoNames timezone lookup url=%s", prepared_request.url)
|
|
55
103
|
|
|
56
104
|
try:
|
|
57
105
|
response = self.session.send(prepared_request)
|
|
58
106
|
response_json = response.json()
|
|
59
107
|
|
|
60
108
|
except Exception as e:
|
|
61
|
-
|
|
109
|
+
logger.error("GeoNames timezone request failed for %s: %s", self.timezone_url, e)
|
|
62
110
|
return {}
|
|
63
111
|
|
|
64
112
|
try:
|
|
65
113
|
timezone_data["timezonestr"] = response_json["timezoneId"]
|
|
66
114
|
|
|
67
115
|
except Exception as e:
|
|
68
|
-
|
|
116
|
+
logger.error("GeoNames timezone payload missing expected keys: %s", e)
|
|
69
117
|
return {}
|
|
70
118
|
|
|
71
119
|
if hasattr(response, "from_cache"):
|
|
@@ -75,7 +123,14 @@ class FetchGeonames:
|
|
|
75
123
|
|
|
76
124
|
def __get_contry_data(self, city_name: str, country_code: str) -> dict[str, str]:
|
|
77
125
|
"""
|
|
78
|
-
Get
|
|
126
|
+
Get city location data without timezone for a given city and country.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
city_name: Name of the city to search for.
|
|
130
|
+
country_code: Two-letter country code.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
dict: City location data excluding timezone information.
|
|
79
134
|
"""
|
|
80
135
|
# Dictionary that will be returned:
|
|
81
136
|
city_data_whitout_tz = {}
|
|
@@ -90,15 +145,16 @@ class FetchGeonames:
|
|
|
90
145
|
}
|
|
91
146
|
|
|
92
147
|
prepared_request = Request("GET", self.base_url, params=params).prepare()
|
|
93
|
-
|
|
148
|
+
logger.debug("GeoNames search url=%s", prepared_request.url)
|
|
94
149
|
|
|
95
150
|
try:
|
|
96
151
|
response = self.session.send(prepared_request)
|
|
152
|
+
response.raise_for_status()
|
|
97
153
|
response_json = response.json()
|
|
98
|
-
|
|
154
|
+
logger.debug("GeoNames search response: %s", response_json)
|
|
99
155
|
|
|
100
156
|
except Exception as e:
|
|
101
|
-
|
|
157
|
+
logger.error("GeoNames search request failed for %s: %s", self.base_url, e)
|
|
102
158
|
return {}
|
|
103
159
|
|
|
104
160
|
try:
|
|
@@ -108,7 +164,7 @@ class FetchGeonames:
|
|
|
108
164
|
city_data_whitout_tz["countryCode"] = response_json["geonames"][0]["countryCode"]
|
|
109
165
|
|
|
110
166
|
except Exception as e:
|
|
111
|
-
|
|
167
|
+
logger.error("GeoNames search payload missing expected keys: %s", e)
|
|
112
168
|
return {}
|
|
113
169
|
|
|
114
170
|
if hasattr(response, "from_cache"):
|
|
@@ -128,15 +184,16 @@ class FetchGeonames:
|
|
|
128
184
|
timezone_response = self.__get_timezone(city_data_response["lat"], city_data_response["lng"])
|
|
129
185
|
|
|
130
186
|
except Exception as e:
|
|
131
|
-
|
|
187
|
+
logger.error("Unable to fetch timezone details: %s", e)
|
|
132
188
|
return {}
|
|
133
189
|
|
|
134
190
|
return {**timezone_response, **city_data_response}
|
|
135
191
|
|
|
136
192
|
|
|
137
193
|
if __name__ == "__main__":
|
|
138
|
-
|
|
139
|
-
setup_logging
|
|
194
|
+
"""Run a tiny demonstration when executing the module directly."""
|
|
195
|
+
from kerykeion.utilities import setup_logging as configure_logging
|
|
140
196
|
|
|
197
|
+
configure_logging("debug")
|
|
141
198
|
geonames = FetchGeonames("Montichiari", "IT")
|
|
142
199
|
print(geonames.get_serialized_data())
|