kundali-chart-mcp 0.2.1

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.
Files changed (172) hide show
  1. package/README.md +67 -0
  2. package/azure-function/function_app.py +93 -0
  3. package/azure-function/host.json +15 -0
  4. package/azure-function/kundali_bridge.py +952 -0
  5. package/azure-function/python/kundali_lib/__init__.py +1 -0
  6. package/azure-function/python/kundali_lib/__pycache__/__init__.cpython-313.pyc +0 -0
  7. package/azure-function/python/kundali_lib/__pycache__/ephemeris.cpython-313.pyc +0 -0
  8. package/azure-function/python/kundali_lib/__pycache__/geocoder.cpython-313.pyc +0 -0
  9. package/azure-function/python/kundali_lib/__pycache__/vedicastro_bridge.cpython-313.pyc +0 -0
  10. package/azure-function/python/kundali_lib/ephemeris.py +30 -0
  11. package/azure-function/python/kundali_lib/geocoder.py +82 -0
  12. package/azure-function/python/kundali_lib/vedic/__init__.py +1 -0
  13. package/azure-function/python/kundali_lib/vedic/__pycache__/__init__.cpython-313.pyc +0 -0
  14. package/azure-function/python/kundali_lib/vedic/__pycache__/arishta.cpython-313.pyc +0 -0
  15. package/azure-function/python/kundali_lib/vedic/__pycache__/ashtakavarga.cpython-313.pyc +0 -0
  16. package/azure-function/python/kundali_lib/vedic/__pycache__/avasthas.cpython-313.pyc +0 -0
  17. package/azure-function/python/kundali_lib/vedic/__pycache__/ayanamsa.cpython-313.pyc +0 -0
  18. package/azure-function/python/kundali_lib/vedic/__pycache__/bhava_chalit.cpython-313.pyc +0 -0
  19. package/azure-function/python/kundali_lib/vedic/__pycache__/char_dasha.cpython-313.pyc +0 -0
  20. package/azure-function/python/kundali_lib/vedic/__pycache__/chart.cpython-313.pyc +0 -0
  21. package/azure-function/python/kundali_lib/vedic/__pycache__/chart_types.cpython-313.pyc +0 -0
  22. package/azure-function/python/kundali_lib/vedic/__pycache__/compatibility.cpython-313.pyc +0 -0
  23. package/azure-function/python/kundali_lib/vedic/__pycache__/constants.cpython-313.pyc +0 -0
  24. package/azure-function/python/kundali_lib/vedic/__pycache__/dasha_extended.cpython-313.pyc +0 -0
  25. package/azure-function/python/kundali_lib/vedic/__pycache__/dasha_systems.cpython-313.pyc +0 -0
  26. package/azure-function/python/kundali_lib/vedic/__pycache__/doshas.cpython-313.pyc +0 -0
  27. package/azure-function/python/kundali_lib/vedic/__pycache__/gandanta.cpython-313.pyc +0 -0
  28. package/azure-function/python/kundali_lib/vedic/__pycache__/gochara.cpython-313.pyc +0 -0
  29. package/azure-function/python/kundali_lib/vedic/__pycache__/hora.cpython-313.pyc +0 -0
  30. package/azure-function/python/kundali_lib/vedic/__pycache__/houses.cpython-313.pyc +0 -0
  31. package/azure-function/python/kundali_lib/vedic/__pycache__/jaimini.cpython-313.pyc +0 -0
  32. package/azure-function/python/kundali_lib/vedic/__pycache__/kalachakra.cpython-313.pyc +0 -0
  33. package/azure-function/python/kundali_lib/vedic/__pycache__/kartari.cpython-313.pyc +0 -0
  34. package/azure-function/python/kundali_lib/vedic/__pycache__/kurmachakra.cpython-313.pyc +0 -0
  35. package/azure-function/python/kundali_lib/vedic/__pycache__/lunar_return.cpython-313.pyc +0 -0
  36. package/azure-function/python/kundali_lib/vedic/__pycache__/muhurta.cpython-313.pyc +0 -0
  37. package/azure-function/python/kundali_lib/vedic/__pycache__/nabhasha.cpython-313.pyc +0 -0
  38. package/azure-function/python/kundali_lib/vedic/__pycache__/nakshatra_details.cpython-313.pyc +0 -0
  39. package/azure-function/python/kundali_lib/vedic/__pycache__/panchanga.cpython-313.pyc +0 -0
  40. package/azure-function/python/kundali_lib/vedic/__pycache__/planets.cpython-313.pyc +0 -0
  41. package/azure-function/python/kundali_lib/vedic/__pycache__/shadbala.cpython-313.pyc +0 -0
  42. package/azure-function/python/kundali_lib/vedic/__pycache__/special_conditions.cpython-313.pyc +0 -0
  43. package/azure-function/python/kundali_lib/vedic/__pycache__/sudarshana.cpython-313.pyc +0 -0
  44. package/azure-function/python/kundali_lib/vedic/__pycache__/tajaka.cpython-313.pyc +0 -0
  45. package/azure-function/python/kundali_lib/vedic/__pycache__/upagrahas.cpython-313.pyc +0 -0
  46. package/azure-function/python/kundali_lib/vedic/__pycache__/varshaphal.cpython-313.pyc +0 -0
  47. package/azure-function/python/kundali_lib/vedic/__pycache__/yogas.cpython-313.pyc +0 -0
  48. package/azure-function/python/kundali_lib/vedic/__pycache__/zodiac.cpython-313.pyc +0 -0
  49. package/azure-function/python/kundali_lib/vedic/arishta.py +465 -0
  50. package/azure-function/python/kundali_lib/vedic/ashtakavarga.py +213 -0
  51. package/azure-function/python/kundali_lib/vedic/avasthas.py +292 -0
  52. package/azure-function/python/kundali_lib/vedic/ayanamsa.py +106 -0
  53. package/azure-function/python/kundali_lib/vedic/bhava_chalit.py +137 -0
  54. package/azure-function/python/kundali_lib/vedic/char_dasha.py +308 -0
  55. package/azure-function/python/kundali_lib/vedic/chart.py +126 -0
  56. package/azure-function/python/kundali_lib/vedic/chart_types.py +338 -0
  57. package/azure-function/python/kundali_lib/vedic/compatibility.py +705 -0
  58. package/azure-function/python/kundali_lib/vedic/constants.py +108 -0
  59. package/azure-function/python/kundali_lib/vedic/dasha_extended.py +262 -0
  60. package/azure-function/python/kundali_lib/vedic/dasha_systems.py +439 -0
  61. package/azure-function/python/kundali_lib/vedic/doshas.py +453 -0
  62. package/azure-function/python/kundali_lib/vedic/gandanta.py +213 -0
  63. package/azure-function/python/kundali_lib/vedic/gochara.py +277 -0
  64. package/azure-function/python/kundali_lib/vedic/hora.py +263 -0
  65. package/azure-function/python/kundali_lib/vedic/houses.py +30 -0
  66. package/azure-function/python/kundali_lib/vedic/jaimini.py +361 -0
  67. package/azure-function/python/kundali_lib/vedic/kalachakra.py +226 -0
  68. package/azure-function/python/kundali_lib/vedic/kartari.py +243 -0
  69. package/azure-function/python/kundali_lib/vedic/kurmachakra.py +383 -0
  70. package/azure-function/python/kundali_lib/vedic/lunar_return.py +402 -0
  71. package/azure-function/python/kundali_lib/vedic/muhurta.py +414 -0
  72. package/azure-function/python/kundali_lib/vedic/nabhasha.py +349 -0
  73. package/azure-function/python/kundali_lib/vedic/nakshatra_details.py +945 -0
  74. package/azure-function/python/kundali_lib/vedic/panchanga.py +297 -0
  75. package/azure-function/python/kundali_lib/vedic/planets.py +55 -0
  76. package/azure-function/python/kundali_lib/vedic/shadbala.py +500 -0
  77. package/azure-function/python/kundali_lib/vedic/special_conditions.py +319 -0
  78. package/azure-function/python/kundali_lib/vedic/sudarshana.py +232 -0
  79. package/azure-function/python/kundali_lib/vedic/tajaka.py +482 -0
  80. package/azure-function/python/kundali_lib/vedic/upagrahas.py +229 -0
  81. package/azure-function/python/kundali_lib/vedic/varshaphal.py +185 -0
  82. package/azure-function/python/kundali_lib/vedic/yogas.py +935 -0
  83. package/azure-function/python/kundali_lib/vedic/zodiac.py +42 -0
  84. package/azure-function/python/kundali_lib/vedicastro_bridge.py +198 -0
  85. package/azure-function/requirements.txt +9 -0
  86. package/index.js +747 -0
  87. package/kundali-chart-mcp.js +159 -0
  88. package/kundali_bridge.py +952 -0
  89. package/package.json +41 -0
  90. package/python/kundali_lib/__init__.py +1 -0
  91. package/python/kundali_lib/__pycache__/__init__.cpython-313.pyc +0 -0
  92. package/python/kundali_lib/__pycache__/ephemeris.cpython-313.pyc +0 -0
  93. package/python/kundali_lib/__pycache__/geocoder.cpython-313.pyc +0 -0
  94. package/python/kundali_lib/__pycache__/vedicastro_bridge.cpython-313.pyc +0 -0
  95. package/python/kundali_lib/ephemeris.py +30 -0
  96. package/python/kundali_lib/geocoder.py +82 -0
  97. package/python/kundali_lib/vedic/__init__.py +1 -0
  98. package/python/kundali_lib/vedic/__pycache__/__init__.cpython-313.pyc +0 -0
  99. package/python/kundali_lib/vedic/__pycache__/arishta.cpython-313.pyc +0 -0
  100. package/python/kundali_lib/vedic/__pycache__/ashtakavarga.cpython-313.pyc +0 -0
  101. package/python/kundali_lib/vedic/__pycache__/avasthas.cpython-313.pyc +0 -0
  102. package/python/kundali_lib/vedic/__pycache__/ayanamsa.cpython-313.pyc +0 -0
  103. package/python/kundali_lib/vedic/__pycache__/bhava_chalit.cpython-313.pyc +0 -0
  104. package/python/kundali_lib/vedic/__pycache__/char_dasha.cpython-313.pyc +0 -0
  105. package/python/kundali_lib/vedic/__pycache__/chart.cpython-313.pyc +0 -0
  106. package/python/kundali_lib/vedic/__pycache__/chart_types.cpython-313.pyc +0 -0
  107. package/python/kundali_lib/vedic/__pycache__/compatibility.cpython-313.pyc +0 -0
  108. package/python/kundali_lib/vedic/__pycache__/constants.cpython-313.pyc +0 -0
  109. package/python/kundali_lib/vedic/__pycache__/dasha_extended.cpython-313.pyc +0 -0
  110. package/python/kundali_lib/vedic/__pycache__/dasha_systems.cpython-313.pyc +0 -0
  111. package/python/kundali_lib/vedic/__pycache__/doshas.cpython-313.pyc +0 -0
  112. package/python/kundali_lib/vedic/__pycache__/gandanta.cpython-313.pyc +0 -0
  113. package/python/kundali_lib/vedic/__pycache__/gochara.cpython-313.pyc +0 -0
  114. package/python/kundali_lib/vedic/__pycache__/hora.cpython-313.pyc +0 -0
  115. package/python/kundali_lib/vedic/__pycache__/houses.cpython-313.pyc +0 -0
  116. package/python/kundali_lib/vedic/__pycache__/jaimini.cpython-313.pyc +0 -0
  117. package/python/kundali_lib/vedic/__pycache__/kalachakra.cpython-313.pyc +0 -0
  118. package/python/kundali_lib/vedic/__pycache__/kartari.cpython-313.pyc +0 -0
  119. package/python/kundali_lib/vedic/__pycache__/kurmachakra.cpython-313.pyc +0 -0
  120. package/python/kundali_lib/vedic/__pycache__/lunar_return.cpython-313.pyc +0 -0
  121. package/python/kundali_lib/vedic/__pycache__/muhurta.cpython-313.pyc +0 -0
  122. package/python/kundali_lib/vedic/__pycache__/nabhasha.cpython-313.pyc +0 -0
  123. package/python/kundali_lib/vedic/__pycache__/nakshatra_details.cpython-313.pyc +0 -0
  124. package/python/kundali_lib/vedic/__pycache__/panchanga.cpython-313.pyc +0 -0
  125. package/python/kundali_lib/vedic/__pycache__/planets.cpython-313.pyc +0 -0
  126. package/python/kundali_lib/vedic/__pycache__/shadbala.cpython-313.pyc +0 -0
  127. package/python/kundali_lib/vedic/__pycache__/special_conditions.cpython-313.pyc +0 -0
  128. package/python/kundali_lib/vedic/__pycache__/sudarshana.cpython-313.pyc +0 -0
  129. package/python/kundali_lib/vedic/__pycache__/tajaka.cpython-313.pyc +0 -0
  130. package/python/kundali_lib/vedic/__pycache__/upagrahas.cpython-313.pyc +0 -0
  131. package/python/kundali_lib/vedic/__pycache__/varshaphal.cpython-313.pyc +0 -0
  132. package/python/kundali_lib/vedic/__pycache__/yogas.cpython-313.pyc +0 -0
  133. package/python/kundali_lib/vedic/__pycache__/zodiac.cpython-313.pyc +0 -0
  134. package/python/kundali_lib/vedic/arishta.py +465 -0
  135. package/python/kundali_lib/vedic/ashtakavarga.py +213 -0
  136. package/python/kundali_lib/vedic/avasthas.py +292 -0
  137. package/python/kundali_lib/vedic/ayanamsa.py +106 -0
  138. package/python/kundali_lib/vedic/bhava_chalit.py +137 -0
  139. package/python/kundali_lib/vedic/char_dasha.py +308 -0
  140. package/python/kundali_lib/vedic/chart.py +126 -0
  141. package/python/kundali_lib/vedic/chart_types.py +338 -0
  142. package/python/kundali_lib/vedic/compatibility.py +705 -0
  143. package/python/kundali_lib/vedic/constants.py +108 -0
  144. package/python/kundali_lib/vedic/dasha_extended.py +262 -0
  145. package/python/kundali_lib/vedic/dasha_systems.py +439 -0
  146. package/python/kundali_lib/vedic/doshas.py +453 -0
  147. package/python/kundali_lib/vedic/gandanta.py +213 -0
  148. package/python/kundali_lib/vedic/gochara.py +277 -0
  149. package/python/kundali_lib/vedic/hora.py +263 -0
  150. package/python/kundali_lib/vedic/houses.py +30 -0
  151. package/python/kundali_lib/vedic/jaimini.py +361 -0
  152. package/python/kundali_lib/vedic/kalachakra.py +226 -0
  153. package/python/kundali_lib/vedic/kartari.py +243 -0
  154. package/python/kundali_lib/vedic/kurmachakra.py +383 -0
  155. package/python/kundali_lib/vedic/lunar_return.py +402 -0
  156. package/python/kundali_lib/vedic/muhurta.py +414 -0
  157. package/python/kundali_lib/vedic/nabhasha.py +349 -0
  158. package/python/kundali_lib/vedic/nakshatra_details.py +945 -0
  159. package/python/kundali_lib/vedic/panchanga.py +297 -0
  160. package/python/kundali_lib/vedic/planets.py +55 -0
  161. package/python/kundali_lib/vedic/shadbala.py +500 -0
  162. package/python/kundali_lib/vedic/special_conditions.py +319 -0
  163. package/python/kundali_lib/vedic/sudarshana.py +232 -0
  164. package/python/kundali_lib/vedic/tajaka.py +482 -0
  165. package/python/kundali_lib/vedic/upagrahas.py +229 -0
  166. package/python/kundali_lib/vedic/varshaphal.py +185 -0
  167. package/python/kundali_lib/vedic/yogas.py +935 -0
  168. package/python/kundali_lib/vedic/zodiac.py +42 -0
  169. package/python/kundali_lib/vedicastro_bridge.py +198 -0
  170. package/remote-server.js +590 -0
  171. package/requirements.txt +8 -0
  172. package/setup.sh +218 -0
@@ -0,0 +1,308 @@
1
+ """Char Dasha (Jaimini): Sign-based dasha system — one of the most important in Jaimini astrology."""
2
+
3
+ from datetime import datetime, timedelta
4
+
5
+ from kundali_lib.vedic.constants import RASHIS
6
+
7
+ # ── Constants ─────────────────────────────────────────────────────────────────
8
+
9
+ MOVABLE_SIGNS = ["Aries", "Cancer", "Libra", "Capricorn"]
10
+ FIXED_SIGNS = ["Taurus", "Leo", "Scorpio", "Aquarius"]
11
+ DUAL_SIGNS = ["Gemini", "Virgo", "Sagittarius", "Pisces"]
12
+
13
+ SIGN_LORD: dict[str, str] = {
14
+ "Aries": "Mars",
15
+ "Taurus": "Venus",
16
+ "Gemini": "Mercury",
17
+ "Cancer": "Moon",
18
+ "Leo": "Sun",
19
+ "Virgo": "Mercury",
20
+ "Libra": "Venus",
21
+ "Scorpio": "Mars",
22
+ "Sagittarius": "Jupiter",
23
+ "Capricorn": "Saturn",
24
+ "Aquarius": "Saturn",
25
+ "Pisces": "Jupiter",
26
+ }
27
+
28
+ # Planets eligible for Atmakaraka (7-karaka system, Rahu included)
29
+ _AK_PLANETS = {"Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu"}
30
+
31
+
32
+ # ── Private helpers ───────────────────────────────────────────────────────────
33
+
34
+
35
+ def _rotated_signs(start_sign: str) -> list:
36
+ """Return RASHIS rotated so it begins at start_sign."""
37
+ idx = RASHIS.index(start_sign)
38
+ return RASHIS[idx:] + RASHIS[:idx]
39
+
40
+
41
+ def _get_atmakaraka(planetary_positions: list) -> tuple:
42
+ """Return (ak_planet_name, ak_sign, degree_in_sign).
43
+
44
+ Atmakaraka = planet with the highest degree within its sign.
45
+ Rahu's degree is inverted (30 − degree) because it moves retrograde.
46
+ """
47
+ best_name = "Sun"
48
+ best_sign = "Aries"
49
+ best_raw_degree = 0.0
50
+ best_eff_degree = -1.0
51
+
52
+ for p in planetary_positions:
53
+ name = p.get("name", "")
54
+ if name not in _AK_PLANETS:
55
+ continue
56
+ raw_deg = p.get("degree", 0.0) # degree within sign (0–30)
57
+ eff_deg = (30.0 - raw_deg) if name == "Rahu" else raw_deg
58
+
59
+ if eff_deg > best_eff_degree:
60
+ best_eff_degree = eff_deg
61
+ best_name = name
62
+ best_sign = p.get("rashi", "Aries")
63
+ best_raw_degree = raw_deg
64
+
65
+ return best_name, best_sign, best_raw_degree
66
+
67
+
68
+ def _planet_sign_map(planetary_positions: list) -> dict[str, str]:
69
+ """Return {planet_name: rashi} for all planets in the chart."""
70
+ return {p["name"]: p.get("rashi", "") for p in planetary_positions}
71
+
72
+
73
+ def _sign_has_planet(planetary_positions: list) -> dict[str, bool]:
74
+ """Return {rashi: True} for every sign occupied by at least one planet."""
75
+ occupied: dict[str, bool] = {}
76
+ for p in planetary_positions:
77
+ rashi = p.get("rashi", "")
78
+ if rashi:
79
+ occupied[rashi] = True
80
+ return occupied
81
+
82
+
83
+ def _char_years_for_sign(
84
+ sign: str,
85
+ sign_idx: int,
86
+ lord_sign: str,
87
+ occupied: dict[str, bool],
88
+ forward: bool,
89
+ ) -> int:
90
+ """Calculate Char Dasha years for one sign.
91
+
92
+ Args:
93
+ sign: The sign being evaluated (e.g. "Aries").
94
+ sign_idx: 0-based index of sign in RASHIS (0 = Aries … 11 = Pisces).
95
+ lord_sign: The sign occupied by this sign's lord in the natal chart.
96
+ occupied: Mapping of which signs are occupied by planets.
97
+ forward: True → count forward (movable/dual/odd-Narayana); False → count backward.
98
+
99
+ Returns:
100
+ Clamped integer years in [1, 12].
101
+ """
102
+ lord_idx = RASHIS.index(lord_sign)
103
+
104
+ if forward:
105
+ distance = (lord_idx - sign_idx) % 12 + 1
106
+ count = 13 - distance # lord in same sign → distance=1 → count=12
107
+ else:
108
+ count = (sign_idx - lord_idx) % 12 + 1 # lord in same sign → 0%12+1=1
109
+
110
+ # Subtract 1 if the 7th sign from this sign has any planet
111
+ seventh_sign = RASHIS[(sign_idx + 6) % 12]
112
+ if occupied.get(seventh_sign, False):
113
+ count -= 1
114
+
115
+ return max(1, min(12, count))
116
+
117
+
118
+ def _compute_char_dasha_years(planetary_positions: list) -> dict[str, int]:
119
+ """Calculate Char Dasha years for each of the 12 signs.
120
+
121
+ Direction rules:
122
+ - Movable signs (Aries, Cancer, Libra, Capricorn): count forward
123
+ - Fixed signs (Taurus, Leo, Scorpio, Aquarius): count backward
124
+ - Dual signs (Gemini, Virgo, Sagittarius, Pisces): count forward
125
+ """
126
+ pmap = _planet_sign_map(planetary_positions)
127
+ occupied = _sign_has_planet(planetary_positions)
128
+ sign_years: dict[str, int] = {}
129
+
130
+ for i, sign in enumerate(RASHIS):
131
+ lord = SIGN_LORD[sign]
132
+ lord_sign = pmap.get(lord)
133
+
134
+ if lord_sign is None:
135
+ # Lord not found — assign a neutral default
136
+ sign_years[sign] = 7
137
+ continue
138
+
139
+ forward = sign in MOVABLE_SIGNS or sign in DUAL_SIGNS
140
+ sign_years[sign] = _char_years_for_sign(sign, i, lord_sign, occupied, forward)
141
+
142
+ return sign_years
143
+
144
+
145
+ def _compute_narayana_years(planetary_positions: list) -> dict[str, int]:
146
+ """Calculate Narayana Dasha years for each of the 12 signs.
147
+
148
+ Direction rules:
149
+ - Odd signs (1-based: Aries, Gemini, Leo, Libra, Sagittarius, Aquarius): count forward
150
+ - Even signs (1-based: Taurus, Cancer, Virgo, Scorpio, Capricorn, Pisces): count backward
151
+ """
152
+ pmap = _planet_sign_map(planetary_positions)
153
+ occupied = _sign_has_planet(planetary_positions)
154
+ sign_years: dict[str, int] = {}
155
+
156
+ for i, sign in enumerate(RASHIS):
157
+ lord = SIGN_LORD[sign]
158
+ lord_sign = pmap.get(lord)
159
+
160
+ if lord_sign is None:
161
+ sign_years[sign] = 7
162
+ continue
163
+
164
+ # 0-based even index → 1-based odd sign → forward
165
+ forward = i % 2 == 0
166
+ sign_years[sign] = _char_years_for_sign(sign, i, lord_sign, occupied, forward)
167
+
168
+ return sign_years
169
+
170
+
171
+ def _build_sign_dashas(
172
+ start_sign: str,
173
+ fraction_elapsed: float,
174
+ sign_years: dict[str, int],
175
+ birth_dt: datetime,
176
+ ) -> dict:
177
+ """Build mahadasha entries for all 12 signs with nested antardashas.
178
+
179
+ Args:
180
+ start_sign: First sign in the dasha sequence.
181
+ fraction_elapsed: Fraction of the first sign's period already elapsed at birth (0–1).
182
+ sign_years: {sign: years} map for all 12 signs.
183
+ birth_dt: Timezone-aware (or naive) birth datetime.
184
+
185
+ Returns:
186
+ dict: mahadashas structure keyed by sign name.
187
+ """
188
+ total_years = sum(sign_years.values())
189
+ sign_seq = _rotated_signs(start_sign)
190
+
191
+ result: dict = {}
192
+ # The first sign's period started (fraction_elapsed * first_sign_years) before birth
193
+ first_sign_years = sign_years[start_sign]
194
+ md_cursor = birth_dt - timedelta(days=fraction_elapsed * first_sign_years * 365.25)
195
+
196
+ for i, md_sign in enumerate(sign_seq):
197
+ md_years = sign_years[md_sign]
198
+ md_days = md_years * 365.25
199
+
200
+ md_start = md_cursor
201
+ md_end = md_start + timedelta(days=md_days)
202
+ md_cursor = md_end
203
+
204
+ display_duration: float = (
205
+ round((1.0 - fraction_elapsed) * md_years, 4) if i == 0 else float(md_years)
206
+ )
207
+
208
+ # ── Antardashas: 12 sub-signs starting from md_sign, zodiacal order ──
209
+ result_ads: dict = {}
210
+ ad_cursor = md_start
211
+ for ad_sign in _rotated_signs(md_sign):
212
+ ad_years = (sign_years[ad_sign] / total_years) * md_years
213
+ ad_days = ad_years * 365.25
214
+ ad_end = ad_cursor + timedelta(days=ad_days)
215
+
216
+ result_ads[ad_sign] = {
217
+ "start": ad_cursor.strftime("%d-%m-%Y"),
218
+ "end": ad_end.strftime("%d-%m-%Y"),
219
+ }
220
+ ad_cursor = ad_end
221
+
222
+ result[md_sign] = {
223
+ "start": md_start.strftime("%d-%m-%Y"),
224
+ "end": md_end.strftime("%d-%m-%Y"),
225
+ "duration_years": display_duration,
226
+ "antardashas": result_ads,
227
+ }
228
+
229
+ return result
230
+
231
+
232
+ # ── Public functions ──────────────────────────────────────────────────────────
233
+
234
+
235
+ def get_char_dasha(base_chart: dict, birth_dt: datetime) -> dict:
236
+ """Calculate Char Dasha (Jaimini sign-based dasha system).
237
+
238
+ Starting sign = sign occupied by the Atmakaraka (planet with highest
239
+ degree within its sign in the natal chart). The sequence then proceeds
240
+ through all 12 signs in zodiacal order.
241
+
242
+ Sub-periods (antardashas) within each sign-dasha rotate through all
243
+ 12 signs starting from the main sign:
244
+ antardasha_duration = (sub_sign_years / total_years) × main_sign_years
245
+
246
+ Args:
247
+ base_chart: Full birth chart dict from ``build_chart()``.
248
+ birth_dt: Timezone-aware birth datetime.
249
+
250
+ Returns:
251
+ dict with ``system``, ``atmakaraka``, ``atmakaraka_sign``,
252
+ ``starting_sign``, ``sign_dasha_years``, and ``mahadashas``.
253
+ """
254
+ positions: list = base_chart.get("planetary_positions", [])
255
+
256
+ ak_name, ak_sign, ak_degree = _get_atmakaraka(positions)
257
+ sign_years = _compute_char_dasha_years(positions)
258
+
259
+ # Fraction of the first sign's dasha elapsed: AK's degree within its sign / 30
260
+ fraction_elapsed = ak_degree / 30.0
261
+
262
+ mahadashas = _build_sign_dashas(ak_sign, fraction_elapsed, sign_years, birth_dt)
263
+
264
+ return {
265
+ "system": "Char Dasha (Jaimini)",
266
+ "atmakaraka": ak_name,
267
+ "atmakaraka_sign": ak_sign,
268
+ "starting_sign": ak_sign,
269
+ "sign_dasha_years": sign_years,
270
+ "mahadashas": mahadashas,
271
+ }
272
+
273
+
274
+ def get_narayana_dasha(base_chart: dict, birth_dt: datetime) -> dict:
275
+ """Calculate Narayana Dasha (sign-based dasha for worldly and outer circumstances).
276
+
277
+ Narayana Dasha uses the Lagna (Ascendant) sign as the starting sign rather
278
+ than the Atmakaraka's sign. Direction of counting for sign years depends on
279
+ whether the sign is odd (forward) or even (backward) in the natural zodiac.
280
+
281
+ Sub-periods follow the same proportional formula as Char Dasha.
282
+
283
+ Args:
284
+ base_chart: Full birth chart dict from ``build_chart()``.
285
+ birth_dt: Timezone-aware birth datetime.
286
+
287
+ Returns:
288
+ dict with ``system``, ``lagna_sign``, ``starting_sign``,
289
+ ``sign_dasha_years``, and ``mahadashas``.
290
+ """
291
+ positions: list = base_chart.get("planetary_positions", [])
292
+ ascendant: dict = base_chart.get("ascendant", {})
293
+
294
+ lagna_sign: str = ascendant.get("rashi", "Aries")
295
+ # Degree of Lagna within its sign determines fraction elapsed in first period
296
+ lagna_degree: float = ascendant.get("degree", 0.0) # 0–30
297
+ fraction_elapsed = lagna_degree / 30.0
298
+
299
+ sign_years = _compute_narayana_years(positions)
300
+ mahadashas = _build_sign_dashas(lagna_sign, fraction_elapsed, sign_years, birth_dt)
301
+
302
+ return {
303
+ "system": "Narayana Dasha",
304
+ "lagna_sign": lagna_sign,
305
+ "starting_sign": lagna_sign,
306
+ "sign_dasha_years": sign_years,
307
+ "mahadashas": mahadashas,
308
+ }
@@ -0,0 +1,126 @@
1
+ """Build full birth chart from datetime and location."""
2
+
3
+ from datetime import datetime
4
+
5
+ import pytz
6
+ import swisseph as swe
7
+
8
+ from kundali_lib.vedic.ayanamsa import (
9
+ _SIDM_MAP,
10
+ DEFAULT_AYANAMSA,
11
+ DEFAULT_HOUSE_SYSTEM,
12
+ )
13
+ from kundali_lib.vedic.constants import PLANET_IDS
14
+ from kundali_lib.vedic.houses import (
15
+ calc_houses_sidereal_with_system,
16
+ houses_dict_and_degrees,
17
+ )
18
+ from kundali_lib.vedic.planets import calc_planet_position
19
+ from kundali_lib.vedic.zodiac import (
20
+ get_nakshatra,
21
+ get_nakshatra_lord,
22
+ get_rashi,
23
+ get_rashi_lord,
24
+ position_dict,
25
+ )
26
+
27
+ # PLANET_NAMES is iteration order for planets
28
+ PLANET_NAMES = list(PLANET_IDS.keys())
29
+
30
+
31
+ def _jd_ut(birth_dt: datetime, timezone: str) -> float:
32
+ if timezone:
33
+ tz = pytz.timezone(timezone)
34
+ local = tz.localize(birth_dt)
35
+ utc = local.astimezone(pytz.UTC)
36
+ else:
37
+ utc = pytz.UTC.localize(birth_dt)
38
+ h = utc.hour + utc.minute / 60.0 + utc.second / 3600.0
39
+ return swe.julday(utc.year, utc.month, utc.day, h)
40
+
41
+
42
+ def _assign_houses(planetary_positions: list[dict], asc_lon: float) -> None:
43
+ asc_rashi_i = int(asc_lon / 30) % 12
44
+ for p in planetary_positions:
45
+ rashi_i = int(p["longitude"] / 30) % 12
46
+ p["house"] = (rashi_i - asc_rashi_i) % 12 + 1
47
+
48
+
49
+ def _ayanamsa_name(mode_key: str) -> str:
50
+ try:
51
+ const = _SIDM_MAP.get(mode_key, _SIDM_MAP["lahiri"])
52
+ name = (swe.get_ayanamsa_name(const) or mode_key.title()).strip()
53
+ return name or "Lahiri"
54
+ except Exception:
55
+ return "Lahiri"
56
+
57
+
58
+ def build_chart(
59
+ name: str,
60
+ birth_dt: datetime,
61
+ lat: float,
62
+ lon: float,
63
+ timezone: str,
64
+ ayanamsa_mode: str | None = None,
65
+ house_system: str | None = None,
66
+ ) -> dict:
67
+ """Full chart: planets, houses, ascendant, ayanamsa.
68
+
69
+ Args:
70
+ ayanamsa_mode: Key like 'lahiri', 'raman', 'krishnamurti', etc. Default 'lahiri'.
71
+ house_system: Single-char code like 'P' (Placidus), 'K' (Koch), 'E' (Equal), etc. Default 'P'.
72
+ """
73
+ swe.set_ephe_path(None)
74
+ mode_key = (ayanamsa_mode or DEFAULT_AYANAMSA).lower().replace("-", "_")
75
+ if mode_key not in _SIDM_MAP:
76
+ raise ValueError(
77
+ f"Unsupported ayanamsa_mode '{ayanamsa_mode}'. Use list_ayanamsa_modes to see options."
78
+ )
79
+ swe.set_sid_mode(_SIDM_MAP[mode_key])
80
+
81
+ hs = (house_system or DEFAULT_HOUSE_SYSTEM).upper()[:1]
82
+
83
+ jd = _jd_ut(birth_dt, timezone)
84
+ planetary_positions = [calc_planet_position(pname, jd) for pname in PLANET_NAMES]
85
+ cusps, ascmc = calc_houses_sidereal_with_system(jd, lat, lon, hs)
86
+ asc_lon = cusps[0]
87
+ mc_lon = ascmc[1] if len(ascmc) > 1 else cusps[9]
88
+
89
+ _assign_houses(planetary_positions, asc_lon)
90
+ houses, house_cusp_degrees = houses_dict_and_degrees(cusps)
91
+ ascendant = {"longitude": asc_lon, **position_dict(asc_lon)}
92
+ moon_pos = next(p for p in planetary_positions if p["name"] == "Moon")
93
+ sun_pos = next(p for p in planetary_positions if p["name"] == "Sun")
94
+ ayanamsa = swe.get_ayanamsa_ut(jd)
95
+
96
+ return {
97
+ "name": name,
98
+ "birth_details": {
99
+ "datetime": birth_dt.isoformat(),
100
+ "latitude": lat,
101
+ "longitude": lon,
102
+ "timezone": timezone,
103
+ },
104
+ "planetary_positions": planetary_positions,
105
+ "houses": houses,
106
+ "house_cusp_degrees": house_cusp_degrees,
107
+ "ascendant": ascendant,
108
+ "mc_longitude": round(mc_lon, 4),
109
+ "moon_sign": moon_pos["rashi"],
110
+ "sun_sign": sun_pos["rashi"],
111
+ "ayanamsa": round(ayanamsa, 4),
112
+ "ayanamsa_name": _ayanamsa_name(mode_key),
113
+ "ayanamsa_mode": mode_key,
114
+ "house_system": hs,
115
+ }
116
+
117
+
118
+ def get_transit_positions(dt_utc: datetime) -> list[dict]:
119
+ """Sidereal (Lahiri) planetary positions for a given UTC moment (e.g. 'now' for transits)."""
120
+ swe.set_ephe_path(None)
121
+ swe.set_sid_mode(_SIDM_MAP["lahiri"])
122
+ if dt_utc.tzinfo is None:
123
+ dt_utc = pytz.UTC.localize(dt_utc)
124
+ h = dt_utc.hour + dt_utc.minute / 60.0 + dt_utc.second / 3600.0
125
+ jd = swe.julday(dt_utc.year, dt_utc.month, dt_utc.day, h)
126
+ return [calc_planet_position(pname, jd) for pname in PLANET_NAMES]