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