kerykeion 4.26.2__py3-none-any.whl → 5.0.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (77) hide show
  1. kerykeion/__init__.py +54 -11
  2. kerykeion/aspects/__init__.py +5 -2
  3. kerykeion/aspects/aspects_factory.py +569 -0
  4. kerykeion/aspects/aspects_utils.py +81 -8
  5. kerykeion/astrological_subject_factory.py +1897 -0
  6. kerykeion/backword.py +773 -0
  7. kerykeion/chart_data_factory.py +549 -0
  8. kerykeion/charts/chart_drawer.py +2601 -0
  9. kerykeion/charts/charts_utils.py +948 -177
  10. kerykeion/charts/draw_planets.py +602 -351
  11. kerykeion/charts/templates/aspect_grid_only.xml +328 -202
  12. kerykeion/charts/templates/chart.xml +432 -272
  13. kerykeion/charts/templates/wheel_only.xml +350 -214
  14. kerykeion/charts/themes/black-and-white.css +148 -0
  15. kerykeion/charts/themes/classic.css +107 -76
  16. kerykeion/charts/themes/dark-high-contrast.css +145 -107
  17. kerykeion/charts/themes/dark.css +146 -107
  18. kerykeion/charts/themes/light.css +146 -103
  19. kerykeion/charts/themes/strawberry.css +158 -0
  20. kerykeion/composite_subject_factory.py +253 -51
  21. kerykeion/ephemeris_data_factory.py +434 -0
  22. kerykeion/fetch_geonames.py +27 -8
  23. kerykeion/house_comparison/__init__.py +6 -0
  24. kerykeion/house_comparison/house_comparison_factory.py +103 -0
  25. kerykeion/house_comparison/house_comparison_utils.py +126 -0
  26. kerykeion/kr_types/__init__.py +66 -6
  27. kerykeion/kr_types/chart_template_model.py +20 -0
  28. kerykeion/kr_types/kerykeion_exception.py +15 -9
  29. kerykeion/kr_types/kr_literals.py +14 -132
  30. kerykeion/kr_types/kr_models.py +14 -318
  31. kerykeion/kr_types/settings_models.py +15 -203
  32. kerykeion/planetary_return_factory.py +805 -0
  33. kerykeion/relationship_score_factory.py +301 -0
  34. kerykeion/report.py +751 -64
  35. kerykeion/schemas/__init__.py +106 -0
  36. kerykeion/schemas/chart_template_model.py +367 -0
  37. kerykeion/schemas/kerykeion_exception.py +20 -0
  38. kerykeion/schemas/kr_literals.py +181 -0
  39. kerykeion/schemas/kr_models.py +605 -0
  40. kerykeion/schemas/settings_models.py +180 -0
  41. kerykeion/settings/__init__.py +20 -1
  42. kerykeion/settings/chart_defaults.py +444 -0
  43. kerykeion/settings/config_constants.py +117 -12
  44. kerykeion/settings/kerykeion_settings.py +31 -73
  45. kerykeion/settings/translation_strings.py +1479 -0
  46. kerykeion/settings/translations.py +74 -0
  47. kerykeion/sweph/ast136/s136108s.se1 +0 -0
  48. kerykeion/sweph/ast136/s136199s.se1 +0 -0
  49. kerykeion/sweph/ast136/s136472s.se1 +0 -0
  50. kerykeion/sweph/ast28/se28978s.se1 +0 -0
  51. kerykeion/sweph/ast50/se50000s.se1 +0 -0
  52. kerykeion/sweph/ast90/se90377s.se1 +0 -0
  53. kerykeion/sweph/ast90/se90482s.se1 +0 -0
  54. kerykeion/sweph/sefstars.txt +1602 -0
  55. kerykeion/transits_time_range_factory.py +302 -0
  56. kerykeion/utilities.py +393 -114
  57. kerykeion-5.0.0.dist-info/METADATA +1176 -0
  58. kerykeion-5.0.0.dist-info/RECORD +63 -0
  59. {kerykeion-4.26.2.dist-info → kerykeion-5.0.0.dist-info}/WHEEL +1 -1
  60. kerykeion/aspects/natal_aspects.py +0 -172
  61. kerykeion/aspects/synastry_aspects.py +0 -124
  62. kerykeion/aspects/transits_time_range.py +0 -41
  63. kerykeion/astrological_subject.py +0 -841
  64. kerykeion/charts/kerykeion_chart_svg.py +0 -1219
  65. kerykeion/enums.py +0 -57
  66. kerykeion/ephemeris_data.py +0 -242
  67. kerykeion/kr_types/chart_types.py +0 -95
  68. kerykeion/relationship_score/__init__.py +0 -2
  69. kerykeion/relationship_score/relationship_score.py +0 -175
  70. kerykeion/relationship_score/relationship_score_factory.py +0 -230
  71. kerykeion/settings/kr.config.json +0 -1258
  72. kerykeion/transits_time_range.py +0 -124
  73. kerykeion-4.26.2.dist-info/LICENSE +0 -661
  74. kerykeion-4.26.2.dist-info/METADATA +0 -629
  75. kerykeion-4.26.2.dist-info/RECORD +0 -46
  76. kerykeion-4.26.2.dist-info/entry_points.txt +0 -3
  77. /LICENSE → /kerykeion-5.0.0.dist-info/licenses/LICENSE +0 -0
kerykeion/backword.py ADDED
@@ -0,0 +1,773 @@
1
+ """Backward compatibility shims for legacy Kerykeion v4 public API.
2
+
3
+ This module provides wrapper classes and aliases that emulate the old
4
+ (v4 and earlier) interfaces while internally delegating to the new v5
5
+ factory/data oriented architecture.
6
+
7
+ Import pattern supported (legacy):
8
+ from kerykeion import AstrologicalSubject, KerykeionChartSVG, SynastryAspects
9
+
10
+ New architecture summary:
11
+ - AstrologicalSubjectFactory.from_birth_data(...) returns an AstrologicalSubjectModel
12
+ - ChartDataFactory + ChartDrawer replace KerykeionChartSVG direct chart building
13
+ - AspectsFactory provides both single and dual chart aspects
14
+
15
+ Classes provided here:
16
+ AstrologicalSubject (wrapper around AstrologicalSubjectFactory)
17
+ KerykeionChartSVG (wrapper producing SVGs via ChartDataFactory + ChartDrawer)
18
+ SynastryAspects (wrapper over AspectsFactory.dual_chart_aspects)
19
+
20
+ Deprecation: Each class issues a DeprecationWarning guiding users to the
21
+ replacement APIs. They are intentionally minimal; only the most used
22
+ attributes / methods from the README master branch examples are reproduced.
23
+
24
+ Note: This file name is intentionally spelled 'backword.py' per user request.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ from typing import Any, Iterable, List, Mapping, Optional, Sequence, Union, Literal, cast
29
+ import logging
30
+ import warnings
31
+ from datetime import datetime
32
+ from functools import cached_property
33
+
34
+ from .astrological_subject_factory import AstrologicalSubjectFactory
35
+ from .chart_data_factory import ChartDataFactory
36
+ from .charts.chart_drawer import ChartDrawer
37
+ from .aspects import AspectsFactory
38
+ from .settings.config_constants import DEFAULT_ACTIVE_POINTS, DEFAULT_ACTIVE_ASPECTS
39
+ from .schemas.kr_models import (
40
+ AstrologicalSubjectModel,
41
+ CompositeSubjectModel,
42
+ ActiveAspect,
43
+ SingleChartDataModel,
44
+ DualChartDataModel,
45
+ )
46
+ from .schemas.kr_literals import (
47
+ KerykeionChartLanguage,
48
+ KerykeionChartTheme,
49
+ ChartType,
50
+ AstrologicalPoint,
51
+ )
52
+ from .schemas import ZodiacType, SiderealMode, HousesSystemIdentifier, PerspectiveType
53
+ from pathlib import Path
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Helpers
57
+ # ---------------------------------------------------------------------------
58
+
59
+ def _deprecated(old: str, new: str) -> None:
60
+ warnings.warn(
61
+ f"'{old}' is deprecated and will be removed in a future major release. "
62
+ f"Please migrate to: {new}",
63
+ DeprecationWarning,
64
+ stacklevel=2,
65
+ )
66
+
67
+
68
+ # Legacy node name mapping for backward compatibility
69
+ LEGACY_NODE_NAMES_MAP = {
70
+ "Mean_Node": "Mean_North_Lunar_Node",
71
+ "True_Node": "True_North_Lunar_Node",
72
+ "Mean_South_Node": "Mean_South_Lunar_Node",
73
+ "True_South_Node": "True_South_Lunar_Node",
74
+ }
75
+
76
+
77
+ def _normalize_active_points(points: Optional[Iterable[Union[str, AstrologicalPoint]]]) -> Optional[List[AstrologicalPoint]]:
78
+ """Best-effort normalization of legacy string active points list.
79
+
80
+ - Accepts None -> None
81
+ - Accepts iterable of strings / AstrologicalPoint literals
82
+ - Filters only those present in DEFAULT_ACTIVE_POINTS to avoid invalid entries
83
+ - Returns None if result would be empty (to let downstream use defaults)
84
+ - Maps old lunar node names to new names with deprecation warning
85
+ """
86
+ if points is None:
87
+ return None
88
+ normalized: List[AstrologicalPoint] = []
89
+ valid: Sequence[AstrologicalPoint] = DEFAULT_ACTIVE_POINTS # type: ignore[assignment]
90
+ for p in points:
91
+ if isinstance(p, str):
92
+ # Check if this is a legacy node name and map it
93
+ if p in LEGACY_NODE_NAMES_MAP:
94
+ warnings.warn(
95
+ f"Active point '{p}' is deprecated in Kerykeion v5. "
96
+ f"Use '{LEGACY_NODE_NAMES_MAP[p]}' instead.",
97
+ DeprecationWarning,
98
+ stacklevel=3,
99
+ )
100
+ p = LEGACY_NODE_NAMES_MAP[p]
101
+
102
+ # Match case-insensitive exact name in default list
103
+ match = next((vp for vp in valid if vp.lower() == p.lower()), None) # type: ignore[attr-defined]
104
+ if match:
105
+ normalized.append(match) # type: ignore[arg-type]
106
+ else:
107
+ if p in valid:
108
+ normalized.append(p)
109
+ return normalized or None
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # Legacy AstrologicalSubject wrapper
113
+ # ---------------------------------------------------------------------------
114
+ class AstrologicalSubject:
115
+ """Backward compatible wrapper implementing the requested __init__ signature."""
116
+
117
+ from datetime import datetime as _dt
118
+ NOW = _dt.utcnow()
119
+
120
+ def __init__(
121
+ self,
122
+ name: str = "Now",
123
+ year: int = NOW.year, # type: ignore[misc]
124
+ month: int = NOW.month, # type: ignore[misc]
125
+ day: int = NOW.day, # type: ignore[misc]
126
+ hour: int = NOW.hour, # type: ignore[misc]
127
+ minute: int = NOW.minute, # type: ignore[misc]
128
+ city: Union[str, None] = None,
129
+ nation: Union[str, None] = None,
130
+ lng: Union[int, float, None] = None,
131
+ lat: Union[int, float, None] = None,
132
+ tz_str: Union[str, None] = None,
133
+ geonames_username: Union[str, None] = None,
134
+ zodiac_type: Union[ZodiacType, None] = None, # default resolved below
135
+ online: bool = True,
136
+ disable_chiron: Union[None, bool] = None, # deprecated
137
+ sidereal_mode: Union[SiderealMode, None] = None,
138
+ houses_system_identifier: Union[HousesSystemIdentifier, None] = None,
139
+ perspective_type: Union[PerspectiveType, None] = None,
140
+ cache_expire_after_days: Union[int, None] = None,
141
+ is_dst: Union[None, bool] = None,
142
+ disable_chiron_and_lilith: bool = False, # currently not forwarded (not in factory)
143
+ ) -> None:
144
+ from .astrological_subject_factory import (
145
+ DEFAULT_ZODIAC_TYPE,
146
+ DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
147
+ DEFAULT_PERSPECTIVE_TYPE,
148
+ DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS,
149
+ )
150
+
151
+ _deprecated("AstrologicalSubject", "AstrologicalSubjectFactory.from_birth_data")
152
+
153
+ if disable_chiron is not None:
154
+ warnings.warn("'disable_chiron' è deprecato e ignorato.", DeprecationWarning, stacklevel=2)
155
+ if disable_chiron_and_lilith:
156
+ warnings.warn(
157
+ "'disable_chiron_and_lilith' non è supportato da from_birth_data in questa versione ed è ignorato.",
158
+ UserWarning,
159
+ stacklevel=2,
160
+ )
161
+
162
+ zodiac_type = DEFAULT_ZODIAC_TYPE if zodiac_type is None else zodiac_type
163
+ houses_system_identifier = (
164
+ DEFAULT_HOUSES_SYSTEM_IDENTIFIER if houses_system_identifier is None else houses_system_identifier
165
+ )
166
+ perspective_type = DEFAULT_PERSPECTIVE_TYPE if perspective_type is None else perspective_type
167
+ cache_expire_after_days = (
168
+ DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS if cache_expire_after_days is None else cache_expire_after_days
169
+ )
170
+
171
+ self._model = AstrologicalSubjectFactory.from_birth_data(
172
+ name=name,
173
+ year=year,
174
+ month=month,
175
+ day=day,
176
+ hour=hour,
177
+ minute=minute,
178
+ seconds=0,
179
+ city=city,
180
+ nation=nation,
181
+ lng=float(lng) if lng is not None else None,
182
+ lat=float(lat) if lat is not None else None,
183
+ tz_str=tz_str,
184
+ geonames_username=geonames_username,
185
+ online=online,
186
+ zodiac_type=zodiac_type, # type: ignore[arg-type]
187
+ sidereal_mode=sidereal_mode, # type: ignore[arg-type]
188
+ houses_system_identifier=houses_system_identifier, # type: ignore[arg-type]
189
+ perspective_type=perspective_type, # type: ignore[arg-type]
190
+ cache_expire_after_days=cache_expire_after_days,
191
+ is_dst=is_dst, # type: ignore[arg-type]
192
+ )
193
+
194
+ # Legacy filesystem attributes
195
+ self.json_dir = Path.home()
196
+
197
+ # Backward compatibility properties for v4 lunar node names
198
+ @property
199
+ def mean_node(self):
200
+ """Deprecated: Use mean_north_lunar_node instead."""
201
+ warnings.warn(
202
+ "'mean_node' is deprecated in Kerykeion v5. Use 'mean_north_lunar_node' instead.",
203
+ DeprecationWarning,
204
+ stacklevel=2,
205
+ )
206
+ return self._model.mean_north_lunar_node
207
+
208
+ @property
209
+ def true_node(self):
210
+ """Deprecated: Use true_north_lunar_node instead."""
211
+ warnings.warn(
212
+ "'true_node' is deprecated in Kerykeion v5. Use 'true_north_lunar_node' instead.",
213
+ DeprecationWarning,
214
+ stacklevel=2,
215
+ )
216
+ return self._model.true_north_lunar_node
217
+
218
+ @property
219
+ def mean_south_node(self):
220
+ """Deprecated: Use mean_south_lunar_node instead."""
221
+ warnings.warn(
222
+ "'mean_south_node' is deprecated in Kerykeion v5. Use 'mean_south_lunar_node' instead.",
223
+ DeprecationWarning,
224
+ stacklevel=2,
225
+ )
226
+ return self._model.mean_south_lunar_node
227
+
228
+ @property
229
+ def true_south_node(self):
230
+ """Deprecated: Use true_south_lunar_node instead."""
231
+ warnings.warn(
232
+ "'true_south_node' is deprecated in Kerykeion v5. Use 'true_south_lunar_node' instead.",
233
+ DeprecationWarning,
234
+ stacklevel=2,
235
+ )
236
+ return self._model.true_south_lunar_node
237
+
238
+ # Provide attribute passthrough for planetary points / houses used in README
239
+ def __getattr__(self, item: str) -> Any: # pragma: no cover - dynamic proxy
240
+ try:
241
+ return getattr(self._model, item)
242
+ except AttributeError:
243
+ raise AttributeError(f"AstrologicalSubject has no attribute '{item}'") from None
244
+
245
+ def __repr__(self) -> str:
246
+ return self.__str__()
247
+
248
+ # Provide json() similar convenience
249
+ def json(
250
+ self,
251
+ dump: bool = False,
252
+ destination_folder: Optional[Union[str, Path]] = None,
253
+ indent: Optional[int] = None,
254
+ ) -> str:
255
+ """Replicate legacy json() behaviour returning a JSON string and optionally dumping to disk."""
256
+
257
+ json_string = self._model.model_dump_json(exclude_none=True, indent=indent)
258
+
259
+ if not dump:
260
+ return json_string
261
+
262
+ if destination_folder is not None:
263
+ target_dir = Path(destination_folder)
264
+ else:
265
+ target_dir = self.json_dir
266
+
267
+ target_dir.mkdir(parents=True, exist_ok=True)
268
+ json_path = target_dir / f"{self._model.name}_kerykeion.json"
269
+
270
+ with open(json_path, "w", encoding="utf-8") as file:
271
+ file.write(json_string)
272
+ logging.info("JSON file dumped in %s.", json_path)
273
+
274
+ return json_string
275
+
276
+ # Legacy helpers -----------------------------------------------------
277
+ @staticmethod
278
+ def _parse_iso_datetime(value: str) -> datetime:
279
+ if value.endswith("Z"):
280
+ value = value[:-1] + "+00:00"
281
+ return datetime.fromisoformat(value)
282
+
283
+ def model(self) -> AstrologicalSubjectModel:
284
+ """Return the underlying Pydantic model (legacy compatibility)."""
285
+
286
+ return self._model
287
+
288
+ def __getitem__(self, item: str) -> Any:
289
+ return getattr(self, item)
290
+
291
+ def get(self, item: str, default: Any = None) -> Any:
292
+ return getattr(self, item, default)
293
+
294
+ def __str__(self) -> str:
295
+ return (
296
+ f"Astrological data for: {self._model.name}, {self._model.iso_formatted_utc_datetime} UTC\n"
297
+ f"Birth location: {self._model.city}, Lat {self._model.lat}, Lon {self._model.lng}"
298
+ )
299
+
300
+ @cached_property
301
+ def utc_time(self) -> float:
302
+ """Backwards-compatible float UTC time value."""
303
+
304
+ dt = self._parse_iso_datetime(self._model.iso_formatted_utc_datetime)
305
+ return dt.hour + dt.minute / 60 + dt.second / 3600 + dt.microsecond / 3_600_000_000
306
+
307
+ @cached_property
308
+ def local_time(self) -> float:
309
+ """Backwards-compatible float local time value."""
310
+
311
+ dt = self._parse_iso_datetime(self._model.iso_formatted_local_datetime)
312
+ return dt.hour + dt.minute / 60 + dt.second / 3600 + dt.microsecond / 3_600_000_000
313
+
314
+ # Factory method compatibility (class method in old API)
315
+ @classmethod
316
+ def get_from_iso_utc_time(
317
+ cls,
318
+ name: str,
319
+ iso_utc_time: str,
320
+ city: str = "Greenwich",
321
+ nation: str = "GB",
322
+ tz_str: str = "Etc/GMT",
323
+ online: bool = False,
324
+ lng: Union[int, float] = 0.0,
325
+ lat: Union[int, float] = 51.5074,
326
+ geonames_username: Optional[str] = None,
327
+ zodiac_type: Optional[ZodiacType] = None,
328
+ disable_chiron_and_lilith: bool = False,
329
+ sidereal_mode: Optional[SiderealMode] = None,
330
+ houses_system_identifier: Optional[HousesSystemIdentifier] = None,
331
+ perspective_type: Optional[PerspectiveType] = None,
332
+ **kwargs: Any,
333
+ ) -> "AstrologicalSubject":
334
+ from .astrological_subject_factory import (
335
+ DEFAULT_ZODIAC_TYPE,
336
+ DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
337
+ DEFAULT_PERSPECTIVE_TYPE,
338
+ DEFAULT_GEONAMES_USERNAME,
339
+ GEONAMES_DEFAULT_USERNAME_WARNING,
340
+ )
341
+
342
+ _deprecated("AstrologicalSubject.get_from_iso_utc_time", "AstrologicalSubjectFactory.from_iso_utc_time")
343
+
344
+ if disable_chiron_and_lilith:
345
+ warnings.warn(
346
+ "'disable_chiron_and_lilith' is ignored by the new factory pipeline.",
347
+ UserWarning,
348
+ stacklevel=2,
349
+ )
350
+
351
+ resolved_geonames = geonames_username or DEFAULT_GEONAMES_USERNAME
352
+ if online and resolved_geonames == DEFAULT_GEONAMES_USERNAME:
353
+ warnings.warn(GEONAMES_DEFAULT_USERNAME_WARNING, UserWarning, stacklevel=2)
354
+
355
+ model = AstrologicalSubjectFactory.from_iso_utc_time(
356
+ name=name,
357
+ iso_utc_time=iso_utc_time,
358
+ city=city,
359
+ nation=nation,
360
+ tz_str=tz_str,
361
+ online=online,
362
+ lng=float(lng),
363
+ lat=float(lat),
364
+ geonames_username=resolved_geonames,
365
+ zodiac_type=(zodiac_type or DEFAULT_ZODIAC_TYPE), # type: ignore[arg-type]
366
+ sidereal_mode=sidereal_mode,
367
+ houses_system_identifier=(houses_system_identifier or DEFAULT_HOUSES_SYSTEM_IDENTIFIER), # type: ignore[arg-type]
368
+ perspective_type=(perspective_type or DEFAULT_PERSPECTIVE_TYPE), # type: ignore[arg-type]
369
+ **kwargs,
370
+ )
371
+
372
+ obj = cls.__new__(cls)
373
+ obj._model = model
374
+ obj.json_dir = Path.home()
375
+ return obj
376
+
377
+ # ---------------------------------------------------------------------------
378
+ # Legacy KerykeionChartSVG wrapper
379
+ # ---------------------------------------------------------------------------
380
+ class KerykeionChartSVG:
381
+ """Wrapper emulating the v4 chart generation interface.
382
+
383
+ Old usage:
384
+ chart = KerykeionChartSVG(subject, chart_type="ExternalNatal", second_subject)
385
+ chart.makeSVG(minify_svg=True, remove_css_variables=True)
386
+
387
+ Replaced by ChartDataFactory + ChartDrawer.
388
+ """
389
+
390
+ def __init__(
391
+ self,
392
+ first_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel],
393
+ chart_type: ChartType = "Natal",
394
+ second_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, None] = None,
395
+ new_output_directory: Union[str, None] = None,
396
+ new_settings_file: Union[Path, None, dict] = None, # retained for signature compatibility (unused)
397
+ theme: Union[KerykeionChartTheme, None] = "classic",
398
+ double_chart_aspect_grid_type: Literal["list", "table"] = "list",
399
+ chart_language: KerykeionChartLanguage = "EN",
400
+ active_points: List[AstrologicalPoint] = DEFAULT_ACTIVE_POINTS, # type: ignore[assignment]
401
+ active_aspects: Optional[List[ActiveAspect]] = None,
402
+ *,
403
+ language_pack: Optional[Mapping[str, Any]] = None,
404
+
405
+ ) -> None:
406
+ _deprecated("KerykeionChartSVG", "ChartDataFactory + ChartDrawer")
407
+
408
+ if new_settings_file is not None:
409
+ warnings.warn(
410
+ "'new_settings_file' is deprecated and ignored in Kerykeion v5. Use language_pack instead.",
411
+ DeprecationWarning,
412
+ stacklevel=2,
413
+ )
414
+
415
+ if isinstance(first_obj, AstrologicalSubject):
416
+ subject_model: Union[AstrologicalSubjectModel, CompositeSubjectModel] = first_obj.model()
417
+ else:
418
+ subject_model = first_obj
419
+
420
+ if isinstance(second_obj, AstrologicalSubject):
421
+ second_model: Optional[Union[AstrologicalSubjectModel, CompositeSubjectModel]] = second_obj.model()
422
+ else:
423
+ second_model = second_obj
424
+
425
+ if active_aspects is None:
426
+ active_aspects = list(DEFAULT_ACTIVE_ASPECTS)
427
+ else:
428
+ active_aspects = list(active_aspects)
429
+
430
+ self.chart_type = chart_type
431
+ self.language_pack = language_pack
432
+ self.theme = theme # type: ignore[assignment]
433
+ self.double_chart_aspect_grid_type = double_chart_aspect_grid_type
434
+ self.chart_language = chart_language # type: ignore[assignment]
435
+
436
+ self._subject_model = subject_model
437
+ self._second_model = second_model
438
+ self.user = subject_model
439
+ self.first_obj = subject_model
440
+ self.t_user = second_model
441
+ self.second_obj = second_model
442
+
443
+ self.active_points = list(active_points) if active_points is not None else list(DEFAULT_ACTIVE_POINTS) # type: ignore[list-item]
444
+ self._active_points = _normalize_active_points(self.active_points)
445
+ self.active_aspects = active_aspects
446
+ self._active_aspects = active_aspects
447
+
448
+ self.output_directory = Path(new_output_directory) if new_output_directory else Path.home()
449
+ self._output_directory = self.output_directory
450
+
451
+ self.template = ""
452
+ self.aspects_list: list[dict[str, Any]] = []
453
+ self.available_planets_setting: list[dict[str, Any]] = []
454
+ self.t_available_kerykeion_celestial_points = None
455
+ self.available_kerykeion_celestial_points: list[dict[str, Any]] = []
456
+ self.chart_colors_settings: dict[str, Any] = {}
457
+ self.planets_settings: list[dict[str, Any]] = []
458
+ self.aspects_settings: list[dict[str, Any]] = []
459
+ self.language_settings: dict[str, Any] = {}
460
+ self.height = None
461
+ self.width = None
462
+ self.location = None
463
+ self.geolat = None
464
+ self.geolon = None
465
+
466
+ self._chart_drawer: Optional[ChartDrawer] = None
467
+ self._chart_data: Optional[Union[SingleChartDataModel, DualChartDataModel]] = None
468
+ self._external_view = False
469
+
470
+ def _ensure_chart(self) -> None:
471
+ if self._chart_drawer is not None:
472
+ return
473
+
474
+ if self._subject_model is None:
475
+ raise ValueError("First object is required to build charts.")
476
+
477
+ chart_type_normalized = str(self.chart_type).lower()
478
+ active_points = self._active_points
479
+ active_aspects = self._active_aspects
480
+ external_view = False
481
+
482
+ if chart_type_normalized in ("natal", "birth", "externalnatal", "external_natal"):
483
+ data = ChartDataFactory.create_natal_chart_data(
484
+ self._subject_model, active_points=active_points, active_aspects=active_aspects
485
+ )
486
+ if chart_type_normalized in ("externalnatal", "external_natal"):
487
+ external_view = True
488
+ elif chart_type_normalized == "synastry":
489
+ if self._second_model is None:
490
+ raise ValueError("Second object is required for Synastry charts.")
491
+ if not isinstance(self._subject_model, AstrologicalSubjectModel) or not isinstance(
492
+ self._second_model, AstrologicalSubjectModel
493
+ ):
494
+ raise ValueError("Synastry charts require two AstrologicalSubject instances.")
495
+ data = ChartDataFactory.create_synastry_chart_data(
496
+ cast(AstrologicalSubjectModel, self._subject_model),
497
+ cast(AstrologicalSubjectModel, self._second_model),
498
+ active_points=active_points,
499
+ active_aspects=active_aspects,
500
+ )
501
+ elif chart_type_normalized == "transit":
502
+ if self._second_model is None:
503
+ raise ValueError("Second object is required for Transit charts.")
504
+ if not isinstance(self._subject_model, AstrologicalSubjectModel) or not isinstance(
505
+ self._second_model, AstrologicalSubjectModel
506
+ ):
507
+ raise ValueError("Transit charts require natal and transit AstrologicalSubject instances.")
508
+ data = ChartDataFactory.create_transit_chart_data(
509
+ cast(AstrologicalSubjectModel, self._subject_model),
510
+ cast(AstrologicalSubjectModel, self._second_model),
511
+ active_points=active_points,
512
+ active_aspects=active_aspects,
513
+ )
514
+ elif chart_type_normalized == "composite":
515
+ if not isinstance(self._subject_model, CompositeSubjectModel):
516
+ raise ValueError("First object must be a CompositeSubjectModel instance for composite charts.")
517
+ data = ChartDataFactory.create_composite_chart_data(
518
+ self._subject_model, active_points=active_points, active_aspects=active_aspects
519
+ )
520
+ else:
521
+ raise ValueError(f"Unsupported or improperly configured chart_type '{self.chart_type}'")
522
+
523
+ self._external_view = external_view
524
+ self._chart_data = data
525
+ self.chart_data = data
526
+ self._chart_drawer = ChartDrawer(
527
+ chart_data=data,
528
+ theme=cast(Optional[KerykeionChartTheme], self.theme),
529
+ double_chart_aspect_grid_type=cast(Literal["list", "table"], self.double_chart_aspect_grid_type),
530
+ chart_language=cast(KerykeionChartLanguage, self.chart_language),
531
+ language_pack=self.language_pack,
532
+ external_view=external_view,
533
+ )
534
+
535
+ # Mirror commonly accessed attributes from legacy class
536
+ drawer = self._chart_drawer
537
+ self.available_planets_setting = getattr(drawer, "available_planets_setting", [])
538
+ self.available_kerykeion_celestial_points = getattr(drawer, "available_kerykeion_celestial_points", [])
539
+ self.aspects_list = getattr(drawer, "aspects_list", [])
540
+ if hasattr(drawer, "t_available_kerykeion_celestial_points"):
541
+ self.t_available_kerykeion_celestial_points = getattr(drawer, "t_available_kerykeion_celestial_points")
542
+ self.chart_colors_settings = getattr(drawer, "chart_colors_settings", {})
543
+ self.planets_settings = getattr(drawer, "planets_settings", [])
544
+ self.aspects_settings = getattr(drawer, "aspects_settings", [])
545
+ self.language_settings = getattr(drawer, "language_settings", {})
546
+ self.height = getattr(drawer, "height", self.height)
547
+ self.width = getattr(drawer, "width", self.width)
548
+ self.location = getattr(drawer, "location", self.location)
549
+ self.geolat = getattr(drawer, "geolat", self.geolat)
550
+ self.geolon = getattr(drawer, "geolon", self.geolon)
551
+ for attr in ["main_radius", "first_circle_radius", "second_circle_radius", "third_circle_radius"]:
552
+ if hasattr(drawer, attr):
553
+ setattr(self, attr, getattr(drawer, attr))
554
+
555
+ # Legacy method names --------------------------------------------------
556
+ def makeTemplate(self, minify: bool = False, remove_css_variables: bool = False) -> str:
557
+ self._ensure_chart()
558
+ assert self._chart_drawer is not None
559
+ template = self._chart_drawer.generate_svg_string(minify=minify, remove_css_variables=remove_css_variables)
560
+ self.template = template
561
+ return template
562
+
563
+ def makeSVG(self, minify: bool = False, remove_css_variables: bool = False) -> None:
564
+ self._ensure_chart()
565
+ assert self._chart_drawer is not None
566
+ self._chart_drawer.save_svg(
567
+ output_path=self.output_directory,
568
+ minify=minify,
569
+ remove_css_variables=remove_css_variables,
570
+ )
571
+ self.template = getattr(self._chart_drawer, "template", self.template)
572
+
573
+ def makeWheelOnlyTemplate(self, minify: bool = False, remove_css_variables: bool = False) -> str:
574
+ self._ensure_chart()
575
+ assert self._chart_drawer is not None
576
+ template = self._chart_drawer.generate_wheel_only_svg_string(
577
+ minify=minify,
578
+ remove_css_variables=remove_css_variables,
579
+ )
580
+ self.template = template
581
+ return template
582
+
583
+ def makeWheelOnlySVG(self, minify: bool = False, remove_css_variables: bool = False) -> None:
584
+ self._ensure_chart()
585
+ assert self._chart_drawer is not None
586
+ self._chart_drawer.save_wheel_only_svg_file(
587
+ output_path=self.output_directory,
588
+ minify=minify,
589
+ remove_css_variables=remove_css_variables,
590
+ )
591
+ self.template = getattr(self._chart_drawer, "template", self.template)
592
+
593
+ def makeAspectGridOnlyTemplate(self, minify: bool = False, remove_css_variables: bool = False) -> str:
594
+ self._ensure_chart()
595
+ assert self._chart_drawer is not None
596
+ template = self._chart_drawer.generate_aspect_grid_only_svg_string(
597
+ minify=minify,
598
+ remove_css_variables=remove_css_variables,
599
+ )
600
+ self.template = template
601
+ return template
602
+
603
+ def makeAspectGridOnlySVG(self, minify: bool = False, remove_css_variables: bool = False) -> None:
604
+ self._ensure_chart()
605
+ assert self._chart_drawer is not None
606
+ self._chart_drawer.save_aspect_grid_only_svg_file(
607
+ output_path=self.output_directory,
608
+ minify=minify,
609
+ remove_css_variables=remove_css_variables,
610
+ )
611
+ self.template = getattr(self._chart_drawer, "template", self.template)
612
+
613
+ # Aliases for new naming in README next (optional convenience)
614
+ save_svg = makeSVG
615
+ save_wheel_only_svg_file = makeWheelOnlySVG
616
+ save_aspect_grid_only_svg_file = makeAspectGridOnlySVG
617
+ makeGridOnlySVG = makeAspectGridOnlySVG
618
+
619
+ # ---------------------------------------------------------------------------
620
+ # Legacy NatalAspects wrapper
621
+ # ---------------------------------------------------------------------------
622
+ class NatalAspects:
623
+ """Wrapper replicating the master branch NatalAspects interface.
624
+
625
+ Replacement: AspectsFactory.single_subject_aspects(subject)
626
+ """
627
+
628
+ def __init__(
629
+ self,
630
+ user: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel],
631
+ new_settings_file: Union[Path, None, dict] = None,
632
+ active_points: Iterable[Union[str, AstrologicalPoint]] = DEFAULT_ACTIVE_POINTS,
633
+ active_aspects: Optional[List[ActiveAspect]] = None,
634
+ *,
635
+ language_pack: Optional[Mapping[str, Any]] = None,
636
+ axis_orb_limit: Optional[float] = None,
637
+ ) -> None:
638
+ _deprecated("NatalAspects", "AspectsFactory.single_chart_aspects")
639
+
640
+ if new_settings_file is not None:
641
+ warnings.warn(
642
+ "'new_settings_file' is deprecated and ignored in Kerykeion v5. Use language_pack instead.",
643
+ DeprecationWarning,
644
+ stacklevel=2,
645
+ )
646
+
647
+ self.user = user.model() if isinstance(user, AstrologicalSubject) else user
648
+ self.new_settings_file = new_settings_file
649
+
650
+ self.language_pack = language_pack
651
+ self.celestial_points: list[Any] = []
652
+ self.aspects_settings: list[Any] = []
653
+ self.axes_orbit_settings = axis_orb_limit
654
+
655
+ self.active_points = list(active_points)
656
+ self._active_points = _normalize_active_points(self.active_points)
657
+ if active_aspects is None:
658
+ active_aspects = list(DEFAULT_ACTIVE_ASPECTS)
659
+ else:
660
+ active_aspects = list(active_aspects)
661
+ self.active_aspects = active_aspects
662
+
663
+ self._aspects_model = None
664
+ self._all_aspects_cache = None
665
+ self._relevant_aspects_cache = None
666
+
667
+ def _build_aspects_model(self):
668
+ if self._aspects_model is None:
669
+ self._aspects_model = AspectsFactory.single_chart_aspects(
670
+ self.user,
671
+ active_points=self._active_points,
672
+ active_aspects=self.active_aspects,
673
+ axis_orb_limit=self.axes_orbit_settings,
674
+ )
675
+ return self._aspects_model
676
+
677
+ @cached_property
678
+ def all_aspects(self):
679
+ if self._all_aspects_cache is None:
680
+ self._all_aspects_cache = list(self._build_aspects_model().all_aspects)
681
+ return self._all_aspects_cache
682
+
683
+ @cached_property
684
+ def relevant_aspects(self):
685
+ if self._relevant_aspects_cache is None:
686
+ self._relevant_aspects_cache = list(self._build_aspects_model().relevant_aspects)
687
+ return self._relevant_aspects_cache
688
+
689
+ # ---------------------------------------------------------------------------
690
+ # Legacy SynastryAspects wrapper
691
+ # ---------------------------------------------------------------------------
692
+ class SynastryAspects:
693
+ """Wrapper replicating the master branch synastry aspects interface."""
694
+
695
+ def __init__(
696
+ self,
697
+ kr_object_one: Union[AstrologicalSubject, AstrologicalSubjectModel],
698
+ kr_object_two: Union[AstrologicalSubject, AstrologicalSubjectModel],
699
+ new_settings_file: Union[Path, None, dict] = None,
700
+ active_points: Iterable[Union[str, AstrologicalPoint]] = DEFAULT_ACTIVE_POINTS,
701
+ active_aspects: Optional[List[ActiveAspect]] = None,
702
+ *,
703
+ language_pack: Optional[Mapping[str, Any]] = None,
704
+ axis_orb_limit: Optional[float] = None,
705
+ ) -> None:
706
+ _deprecated("SynastryAspects", "AspectsFactory.dual_chart_aspects")
707
+
708
+ if new_settings_file is not None:
709
+ warnings.warn(
710
+ "'new_settings_file' is deprecated and ignored in Kerykeion v5. Use language_pack instead.",
711
+ DeprecationWarning,
712
+ stacklevel=2,
713
+ )
714
+
715
+ self.first_user = kr_object_one.model() if isinstance(kr_object_one, AstrologicalSubject) else kr_object_one
716
+ self.second_user = kr_object_two.model() if isinstance(kr_object_two, AstrologicalSubject) else kr_object_two
717
+ self.new_settings_file = new_settings_file
718
+
719
+ self.language_pack = language_pack
720
+ self.celestial_points: list[Any] = []
721
+ self.aspects_settings: list[Any] = []
722
+ self.axes_orbit_settings = axis_orb_limit
723
+
724
+ self.active_points = list(active_points)
725
+ self._active_points = _normalize_active_points(self.active_points)
726
+ if active_aspects is None:
727
+ active_aspects = list(DEFAULT_ACTIVE_ASPECTS)
728
+ else:
729
+ active_aspects = list(active_aspects)
730
+ self.active_aspects = active_aspects
731
+
732
+ self._dual_model = None
733
+ self._all_aspects_cache = None
734
+ self._relevant_aspects_cache = None
735
+ self._all_aspects: Union[list, None] = None
736
+ self._relevant_aspects: Union[list, None] = None
737
+
738
+ def _build_dual_model(self):
739
+ if self._dual_model is None:
740
+ self._dual_model = AspectsFactory.dual_chart_aspects(
741
+ self.first_user,
742
+ self.second_user,
743
+ active_points=self._active_points,
744
+ active_aspects=self.active_aspects,
745
+ axis_orb_limit=self.axes_orbit_settings,
746
+ )
747
+ return self._dual_model
748
+
749
+ @cached_property
750
+ def all_aspects(self):
751
+ if self._all_aspects_cache is None:
752
+ self._all_aspects_cache = list(self._build_dual_model().all_aspects)
753
+ return self._all_aspects_cache
754
+
755
+ @cached_property
756
+ def relevant_aspects(self):
757
+ if self._relevant_aspects_cache is None:
758
+ self._relevant_aspects_cache = list(self._build_dual_model().relevant_aspects)
759
+ return self._relevant_aspects_cache
760
+
761
+ def get_relevant_aspects(self):
762
+ """Legacy method for compatibility with master branch."""
763
+ return self.relevant_aspects
764
+
765
+ # ---------------------------------------------------------------------------
766
+ # Convenience exports (mirroring old implicit surface API)
767
+ # ---------------------------------------------------------------------------
768
+ __all__ = [
769
+ "AstrologicalSubject",
770
+ "KerykeionChartSVG",
771
+ "NatalAspects",
772
+ "SynastryAspects",
773
+ ]