kerykeion 5.0.0a9__py3-none-any.whl → 5.1.8__py3-none-any.whl

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

Potentially problematic release.


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

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