kerykeion 4.18.3__py3-none-any.whl → 5.1.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kerykeion might be problematic. Click here for more details.

Files changed (76) hide show
  1. kerykeion/__init__.py +56 -11
  2. kerykeion/aspects/__init__.py +7 -4
  3. kerykeion/aspects/aspects_factory.py +568 -0
  4. kerykeion/aspects/aspects_utils.py +86 -13
  5. kerykeion/astrological_subject_factory.py +1901 -0
  6. kerykeion/backword.py +820 -0
  7. kerykeion/chart_data_factory.py +552 -0
  8. kerykeion/charts/__init__.py +2 -2
  9. kerykeion/charts/chart_drawer.py +2794 -0
  10. kerykeion/charts/charts_utils.py +1066 -309
  11. kerykeion/charts/draw_planets.py +602 -351
  12. kerykeion/charts/templates/aspect_grid_only.xml +337 -193
  13. kerykeion/charts/templates/chart.xml +441 -240
  14. kerykeion/charts/templates/wheel_only.xml +365 -211
  15. kerykeion/charts/themes/black-and-white.css +148 -0
  16. kerykeion/charts/themes/classic.css +107 -76
  17. kerykeion/charts/themes/dark-high-contrast.css +145 -107
  18. kerykeion/charts/themes/dark.css +146 -107
  19. kerykeion/charts/themes/light.css +146 -103
  20. kerykeion/charts/themes/strawberry.css +158 -0
  21. kerykeion/composite_subject_factory.py +408 -0
  22. kerykeion/ephemeris_data_factory.py +443 -0
  23. kerykeion/fetch_geonames.py +81 -21
  24. kerykeion/house_comparison/__init__.py +6 -0
  25. kerykeion/house_comparison/house_comparison_factory.py +103 -0
  26. kerykeion/house_comparison/house_comparison_utils.py +126 -0
  27. kerykeion/kr_types/__init__.py +66 -6
  28. kerykeion/kr_types/chart_template_model.py +20 -0
  29. kerykeion/kr_types/kerykeion_exception.py +15 -9
  30. kerykeion/kr_types/kr_literals.py +14 -106
  31. kerykeion/kr_types/kr_models.py +14 -179
  32. kerykeion/kr_types/settings_models.py +15 -152
  33. kerykeion/planetary_return_factory.py +805 -0
  34. kerykeion/relationship_score_factory.py +301 -0
  35. kerykeion/report.py +750 -65
  36. kerykeion/schemas/__init__.py +106 -0
  37. kerykeion/schemas/chart_template_model.py +367 -0
  38. kerykeion/schemas/kerykeion_exception.py +20 -0
  39. kerykeion/schemas/kr_literals.py +181 -0
  40. kerykeion/schemas/kr_models.py +603 -0
  41. kerykeion/schemas/settings_models.py +188 -0
  42. kerykeion/settings/__init__.py +20 -1
  43. kerykeion/settings/chart_defaults.py +444 -0
  44. kerykeion/settings/config_constants.py +152 -0
  45. kerykeion/settings/kerykeion_settings.py +36 -61
  46. kerykeion/settings/translation_strings.py +1499 -0
  47. kerykeion/settings/translations.py +74 -0
  48. kerykeion/sweph/ast136/s136108s.se1 +0 -0
  49. kerykeion/sweph/ast136/s136199s.se1 +0 -0
  50. kerykeion/sweph/ast136/s136472s.se1 +0 -0
  51. kerykeion/sweph/ast28/se28978s.se1 +0 -0
  52. kerykeion/sweph/ast50/se50000s.se1 +0 -0
  53. kerykeion/sweph/ast90/se90377s.se1 +0 -0
  54. kerykeion/sweph/ast90/se90482s.se1 +0 -0
  55. kerykeion/sweph/sefstars.txt +1602 -0
  56. kerykeion/transits_time_range_factory.py +302 -0
  57. kerykeion/utilities.py +626 -125
  58. kerykeion-5.1.9.dist-info/METADATA +1793 -0
  59. kerykeion-5.1.9.dist-info/RECORD +63 -0
  60. {kerykeion-4.18.3.dist-info → kerykeion-5.1.9.dist-info}/WHEEL +1 -1
  61. kerykeion/aspects/natal_aspects.py +0 -143
  62. kerykeion/aspects/synastry_aspects.py +0 -113
  63. kerykeion/astrological_subject.py +0 -818
  64. kerykeion/charts/kerykeion_chart_svg.py +0 -894
  65. kerykeion/enums.py +0 -51
  66. kerykeion/ephemeris_data.py +0 -178
  67. kerykeion/kr_types/chart_types.py +0 -88
  68. kerykeion/relationship_score/__init__.py +0 -2
  69. kerykeion/relationship_score/relationship_score.py +0 -175
  70. kerykeion/relationship_score/relationship_score_factory.py +0 -275
  71. kerykeion/settings/kr.config.json +0 -721
  72. kerykeion-4.18.3.dist-info/LICENSE +0 -661
  73. kerykeion-4.18.3.dist-info/METADATA +0 -396
  74. kerykeion-4.18.3.dist-info/RECORD +0 -42
  75. kerykeion-4.18.3.dist-info/entry_points.txt +0 -3
  76. /LICENSE → /kerykeion-5.1.9.dist-info/licenses/LICENSE +0 -0
@@ -0,0 +1,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("---")
@@ -1,35 +1,60 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
- This is part of Kerykeion (C) 2024 Giacomo Battaglia
3
+ Author: Giacomo Battaglia
4
+ Copyright: (C) 2025 Kerykeion Project
5
+ License: AGPL-3.0
4
6
  """
5
7
 
6
8
 
7
- import logging
9
+ from logging import getLogger
10
+ from datetime import timedelta
11
+ from os import getenv
12
+ from pathlib import Path
13
+ from typing import Optional, Union
14
+
8
15
  from requests import Request
9
16
  from requests_cache import CachedSession
10
- from typing import Union
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"
11
24
 
12
25
 
13
26
  class FetchGeonames:
14
27
  """
15
- 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.
16
32
 
17
33
  Args:
18
- city_name (str): Name of the city
19
- country_code (str): Two letters country code
20
- username (str, optional): GeoNames username, defaults to "century.boy".
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`.
21
42
  """
22
43
 
44
+ default_cache_name: Path = DEFAULT_GEONAMES_CACHE_NAME
45
+
23
46
  def __init__(
24
47
  self,
25
48
  city_name: str,
26
49
  country_code: str,
27
50
  username: str = "century.boy",
51
+ cache_expire_after_days=30,
52
+ cache_name: Optional[Union[str, Path]] = None,
28
53
  ):
29
54
  self.session = CachedSession(
30
- cache_name="cache/kerykeion_geonames_cache",
55
+ cache_name=str(self._resolve_cache_name(cache_name)),
31
56
  backend="sqlite",
32
- expire_after=86400,
57
+ expire_after=timedelta(days=cache_expire_after_days),
33
58
  )
34
59
 
35
60
  self.username = username
@@ -38,9 +63,35 @@ class FetchGeonames:
38
63
  self.base_url = "http://api.geonames.org/searchJSON"
39
64
  self.timezone_url = "http://api.geonames.org/timezoneJSON"
40
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
+
41
85
  def __get_timezone(self, lat: Union[str, float, int], lon: Union[str, float, int]) -> dict[str, str]:
42
86
  """
43
- Get the timezone for a given latitude and longitude
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.
44
95
  """
45
96
  # Dictionary that will be returned:
46
97
  timezone_data = {}
@@ -48,21 +99,21 @@ class FetchGeonames:
48
99
  params = {"lat": lat, "lng": lon, "username": self.username}
49
100
 
50
101
  prepared_request = Request("GET", self.timezone_url, params=params).prepare()
51
- logging.debug(f"Requesting data from GeoName timezones: {prepared_request.url}")
102
+ logger.debug("GeoNames timezone lookup url=%s", prepared_request.url)
52
103
 
53
104
  try:
54
105
  response = self.session.send(prepared_request)
55
106
  response_json = response.json()
56
107
 
57
108
  except Exception as e:
58
- logging.error(f"Error fetching {self.timezone_url}: {e}")
109
+ logger.error("GeoNames timezone request failed for %s: %s", self.timezone_url, e)
59
110
  return {}
60
111
 
61
112
  try:
62
113
  timezone_data["timezonestr"] = response_json["timezoneId"]
63
114
 
64
115
  except Exception as e:
65
- logging.error(f"Error serializing data maybe wrong username? Details: {e}")
116
+ logger.error("GeoNames timezone payload missing expected keys: %s", e)
66
117
  return {}
67
118
 
68
119
  if hasattr(response, "from_cache"):
@@ -72,7 +123,14 @@ class FetchGeonames:
72
123
 
73
124
  def __get_contry_data(self, city_name: str, country_code: str) -> dict[str, str]:
74
125
  """
75
- Get the city data *whitout timezone* for a given city and country name
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.
76
134
  """
77
135
  # Dictionary that will be returned:
78
136
  city_data_whitout_tz = {}
@@ -87,15 +145,16 @@ class FetchGeonames:
87
145
  }
88
146
 
89
147
  prepared_request = Request("GET", self.base_url, params=params).prepare()
90
- logging.debug(f"Requesting data from geonames basic: {prepared_request.url}")
148
+ logger.debug("GeoNames search url=%s", prepared_request.url)
91
149
 
92
150
  try:
93
151
  response = self.session.send(prepared_request)
152
+ response.raise_for_status()
94
153
  response_json = response.json()
95
- logging.debug(f"Response from GeoNames: {response_json}")
154
+ logger.debug("GeoNames search response: %s", response_json)
96
155
 
97
156
  except Exception as e:
98
- logging.error(f"Error in fetching {self.base_url}: {e}")
157
+ logger.error("GeoNames search request failed for %s: %s", self.base_url, e)
99
158
  return {}
100
159
 
101
160
  try:
@@ -105,7 +164,7 @@ class FetchGeonames:
105
164
  city_data_whitout_tz["countryCode"] = response_json["geonames"][0]["countryCode"]
106
165
 
107
166
  except Exception as e:
108
- logging.error(f"Error serializing data maybe wrong username? Details: {e}")
167
+ logger.error("GeoNames search payload missing expected keys: %s", e)
109
168
  return {}
110
169
 
111
170
  if hasattr(response, "from_cache"):
@@ -125,15 +184,16 @@ class FetchGeonames:
125
184
  timezone_response = self.__get_timezone(city_data_response["lat"], city_data_response["lng"])
126
185
 
127
186
  except Exception as e:
128
- logging.error(f"Error in fetching timezone: {e}")
187
+ logger.error("Unable to fetch timezone details: %s", e)
129
188
  return {}
130
189
 
131
190
  return {**timezone_response, **city_data_response}
132
191
 
133
192
 
134
193
  if __name__ == "__main__":
135
- from kerykeion.utilities import setup_logging
136
- setup_logging(level="debug")
194
+ """Run a tiny demonstration when executing the module directly."""
195
+ from kerykeion.utilities import setup_logging as configure_logging
137
196
 
197
+ configure_logging("debug")
138
198
  geonames = FetchGeonames("Montichiari", "IT")
139
199
  print(geonames.get_serialized_data())
@@ -0,0 +1,6 @@
1
+
2
+ from .house_comparison_factory import HouseComparisonFactory
3
+
4
+ __all__ = [
5
+ "HouseComparisonFactory",
6
+ ]