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,277 @@
1
+ """Gochara: Vedic transit analysis — how transiting planets affect natal houses."""
2
+
3
+ from kundali_lib.vedic.constants import RASHIS
4
+
5
+ # ---------------------------------------------------------------------------
6
+ # Gochara effect tables (house counted FROM natal Moon sign)
7
+ # ---------------------------------------------------------------------------
8
+
9
+ GOCHARA_EFFECTS = {
10
+ "Sun": {"good": [3, 6, 10, 11], "bad": [1, 2, 4, 5, 7, 8, 9, 12]},
11
+ "Moon": {"good": [1, 3, 6, 7, 10, 11], "bad": [2, 4, 5, 8, 9, 12]},
12
+ "Mars": {"good": [3, 6, 11], "bad": [1, 2, 4, 5, 7, 8, 9, 10, 12]},
13
+ "Mercury": {"good": [2, 4, 6, 8, 10, 11], "bad": [1, 3, 5, 7, 9, 12]},
14
+ "Jupiter": {"good": [2, 5, 7, 9, 11], "bad": [1, 3, 4, 6, 8, 10, 12]},
15
+ "Venus": {"good": [1, 2, 3, 4, 5, 8, 9, 11, 12], "bad": [6, 7, 10]},
16
+ "Saturn": {"good": [3, 6, 11], "bad": [1, 2, 4, 5, 7, 8, 9, 10, 12]},
17
+ "Rahu": {"good": [3, 6, 11], "bad": [1, 2, 4, 5, 7, 8, 9, 10, 12]},
18
+ "Ketu": {"good": [3, 6, 11], "bad": [1, 2, 4, 5, 7, 8, 9, 10, 12]},
19
+ }
20
+
21
+ # Vedha obstruction points: {planet: {good_house: vedha_house}}
22
+ # If a planet is in good_house AND another planet is in vedha_house → good effect cancelled.
23
+ VEDHA_POINTS = {
24
+ "Sun": {3: 9, 6: 12, 10: 4, 11: 5},
25
+ "Moon": {1: 5, 3: 9, 6: 12, 7: 2, 10: 4, 11: 8},
26
+ "Mars": {3: 12, 6: 9, 11: 5},
27
+ "Mercury": {2: 5, 4: 3, 6: 9, 8: 1, 10: 8, 11: 12},
28
+ "Jupiter": {2: 12, 5: 4, 7: 3, 9: 10, 11: 8},
29
+ "Venus": {1: 8, 2: 7, 3: 1, 4: 10, 5: 9, 8: 5, 9: 1, 11: 8, 12: 6},
30
+ "Saturn": {3: 12, 6: 9, 11: 5},
31
+ "Rahu": {3: 12, 6: 9, 11: 5},
32
+ "Ketu": {3: 12, 6: 9, 11: 5},
33
+ }
34
+
35
+ # Short descriptions for each effect category
36
+ _EFFECT_DESCRIPTIONS = {
37
+ "Sun": {
38
+ "good": "Sun in {h}th from Moon: favours authority, health, career visibility.",
39
+ "bad": "Sun in {h}th from Moon: ego conflicts, health strain, obstacles from officials.",
40
+ },
41
+ "Moon": {
42
+ "good": "Moon in {h}th from Moon: emotional harmony, public success, good relationships.",
43
+ "bad": "Moon in {h}th from Moon: emotional turbulence, domestic issues, mental stress.",
44
+ },
45
+ "Mars": {
46
+ "good": "Mars in {h}th from Moon: courage, competition success, energy channelled well.",
47
+ "bad": "Mars in {h}th from Moon: accidents, arguments, impulsiveness, sibling friction.",
48
+ },
49
+ "Mercury": {
50
+ "good": "Mercury in {h}th from Moon: business gains, sharp intellect, communication flows.",
51
+ "bad": "Mercury in {h}th from Moon: miscommunication, travel problems, nervous tension.",
52
+ },
53
+ "Jupiter": {
54
+ "good": "Jupiter in {h}th from Moon: wisdom, wealth, expansion, spiritual growth.",
55
+ "bad": "Jupiter in {h}th from Moon: over-expansion, false optimism, financial missteps.",
56
+ },
57
+ "Venus": {
58
+ "good": "Venus in {h}th from Moon: romance, artistic gains, comforts, social pleasure.",
59
+ "bad": "Venus in {h}th from Moon: relationship strain, extravagance, health of women.",
60
+ },
61
+ "Saturn": {
62
+ "good": "Saturn in {h}th from Moon: disciplined gains through hard work and perseverance.",
63
+ "bad": "Saturn in {h}th from Moon: delays, burdens, health issues, melancholy.",
64
+ },
65
+ "Rahu": {
66
+ "good": "Rahu in {h}th from Moon: unconventional gains, foreign connections, ambition.",
67
+ "bad": "Rahu in {h}th from Moon: confusion, deception, hidden enemies, anxiety.",
68
+ },
69
+ "Ketu": {
70
+ "good": "Ketu in {h}th from Moon: spiritual insight, liberation from past karma.",
71
+ "bad": "Ketu in {h}th from Moon: losses, separations, health issues, disconnection.",
72
+ },
73
+ }
74
+
75
+
76
+ def _house_from_moon(transit_rashi: str, moon_rashi: str) -> int:
77
+ """Calculate transit house number (1-12) counted from natal Moon sign."""
78
+ moon_i = RASHIS.index(moon_rashi)
79
+ transit_i = RASHIS.index(transit_rashi)
80
+ return (transit_i - moon_i) % 12 + 1
81
+
82
+
83
+ def _sade_sati_info(saturn_house_from_moon: int) -> dict | None:
84
+ """Return Sade Sati / Kantaka Shani details if applicable."""
85
+ if saturn_house_from_moon == 12:
86
+ return {
87
+ "type": "Sade Sati — Rising Phase",
88
+ "phase": "first",
89
+ "description": (
90
+ "Saturn is in the 12th sign from natal Moon, marking the start of the 7.5-year "
91
+ "Sade Sati. Losses, expenses, sleep issues, and preparation for major life changes. "
92
+ "Foreign travel or spiritual inclinations may increase."
93
+ ),
94
+ }
95
+ elif saturn_house_from_moon == 1:
96
+ return {
97
+ "type": "Sade Sati — Peak Phase",
98
+ "phase": "second",
99
+ "description": (
100
+ "Saturn transits natal Moon sign — the most intense phase of Sade Sati. "
101
+ "Health, relationships, and career may all be tested simultaneously. "
102
+ "Strong discipline and introspection are essential. Avoid major irreversible decisions."
103
+ ),
104
+ }
105
+ elif saturn_house_from_moon == 2:
106
+ return {
107
+ "type": "Sade Sati — Setting Phase",
108
+ "phase": "third",
109
+ "description": (
110
+ "Saturn moves past the Moon sign — final phase of Sade Sati. Financial pressures "
111
+ "and family tensions linger. Gradual relief begins but demands continued patience."
112
+ ),
113
+ }
114
+ elif saturn_house_from_moon == 4:
115
+ return {
116
+ "type": "Kantaka Shani (4th house from Moon)",
117
+ "phase": "kantaka",
118
+ "description": (
119
+ "Saturn in 4th from natal Moon causes domestic unrest, property disputes, "
120
+ "mother's health concerns, and career instability. Known as Ardhashtama Shani."
121
+ ),
122
+ }
123
+ elif saturn_house_from_moon == 8:
124
+ return {
125
+ "type": "Kantaka Shani (8th house from Moon)",
126
+ "phase": "kantaka",
127
+ "description": (
128
+ "Saturn in 8th from natal Moon — Ashtama Shani. A highly challenging transit "
129
+ "involving chronic health issues, sudden obstacles, transformation through crisis, "
130
+ "and karmic reckoning. Requires patience and surrender."
131
+ ),
132
+ }
133
+ return None
134
+
135
+
136
+ def get_gochara(natal_chart: dict, transit_positions: list) -> dict:
137
+ """Analyse Gochara (transit) effects of current planets on the natal chart.
138
+
139
+ Args:
140
+ natal_chart: Output of ``build_chart()``. Must contain ``moon_sign``
141
+ and ``planetary_positions``.
142
+ transit_positions: List of current planet position dicts (same shape as
143
+ ``planetary_positions`` entries in a built chart).
144
+
145
+ Returns:
146
+ A dict with ``natal_moon_rashi``, ``transit_analysis`` (per planet),
147
+ ``overall_period``, and optional ``special_conditions``.
148
+ """
149
+ moon_rashi: str = natal_chart["moon_sign"]
150
+
151
+ # Build quick lookup: {planet_name: house_from_moon} for Vedha checks
152
+ transit_house_map: dict[str, int] = {}
153
+ transit_rashi_map: dict[str, str] = {}
154
+ for tp in transit_positions:
155
+ name = tp["name"]
156
+ rashi = tp["rashi"]
157
+ transit_rashi_map[name] = rashi
158
+ transit_house_map[name] = _house_from_moon(rashi, moon_rashi)
159
+
160
+ transit_analysis: dict[str, dict] = {}
161
+ benefic_count = 0
162
+ malefic_count = 0
163
+ special_conditions: list[dict] = []
164
+
165
+ for tp in transit_positions:
166
+ planet = tp["name"]
167
+ if planet not in GOCHARA_EFFECTS:
168
+ continue
169
+
170
+ house = transit_house_map[planet]
171
+ effects = GOCHARA_EFFECTS[planet]
172
+
173
+ if house in effects["good"]:
174
+ base_effect = "Beneficial"
175
+ elif house in effects["bad"]:
176
+ base_effect = "Harmful"
177
+ else:
178
+ base_effect = "Neutral"
179
+
180
+ # --- Vedha check ---
181
+ vedha_present = False
182
+ vedha_planet_name = None
183
+ if base_effect == "Beneficial" and planet in VEDHA_POINTS:
184
+ vedha_house = VEDHA_POINTS[planet].get(house)
185
+ if vedha_house is not None:
186
+ for other_planet, other_house in transit_house_map.items():
187
+ if other_planet != planet and other_house == vedha_house:
188
+ vedha_present = True
189
+ vedha_planet_name = other_planet
190
+ break
191
+
192
+ if vedha_present:
193
+ final_effect = "Cancelled"
194
+ else:
195
+ final_effect = base_effect
196
+
197
+ # Build description
198
+ desc_tmpl = _EFFECT_DESCRIPTIONS.get(planet, {})
199
+ key = "good" if base_effect == "Beneficial" else "bad"
200
+ description = desc_tmpl.get(key, f"{planet} in {house}th from Moon.").format(
201
+ h=house
202
+ )
203
+ if vedha_present:
204
+ description += (
205
+ f" However, {vedha_planet_name} in the Vedha house ({VEDHA_POINTS[planet][house]}th) "
206
+ "cancels this good effect."
207
+ )
208
+
209
+ transit_analysis[planet] = {
210
+ "transit_rashi": transit_rashi_map[planet],
211
+ "house_from_moon": house,
212
+ "effect": base_effect,
213
+ "vedha_present": vedha_present,
214
+ "vedha_planet": vedha_planet_name,
215
+ "final_effect": final_effect,
216
+ "description": description,
217
+ }
218
+
219
+ if final_effect == "Beneficial":
220
+ benefic_count += 1
221
+ elif final_effect in ("Harmful", "Cancelled"):
222
+ malefic_count += 1
223
+
224
+ # Saturn special conditions
225
+ if planet == "Saturn":
226
+ sade_sati = _sade_sati_info(house)
227
+ if sade_sati:
228
+ special_conditions.append(sade_sati)
229
+
230
+ # Overall summary
231
+ total = benefic_count + malefic_count
232
+ if total == 0:
233
+ summary = "No significant Gochara data available."
234
+ recommendation = "Proceed with caution and mindfulness."
235
+ elif benefic_count > malefic_count:
236
+ summary = (
237
+ f"Predominantly favourable transit period with {benefic_count} benefic "
238
+ f"and {malefic_count} malefic planetary influences."
239
+ )
240
+ recommendation = (
241
+ "Good time to initiate new ventures, make important decisions, and strengthen "
242
+ "relationships. Leverage benefic planetary support."
243
+ )
244
+ elif malefic_count > benefic_count:
245
+ summary = (
246
+ f"Challenging transit period with {malefic_count} malefic "
247
+ f"and {benefic_count} benefic planetary influences."
248
+ )
249
+ recommendation = (
250
+ "Exercise caution, avoid major irreversible decisions, focus on health and "
251
+ "consolidation. Perform recommended remedies."
252
+ )
253
+ else:
254
+ summary = (
255
+ f"Mixed transit period — {benefic_count} benefic and {malefic_count} malefic "
256
+ "influences balance each other."
257
+ )
258
+ recommendation = (
259
+ "Proceed carefully. Balance opportunities with risks; seek guidance before "
260
+ "major commitments."
261
+ )
262
+
263
+ result: dict = {
264
+ "natal_moon_rashi": moon_rashi,
265
+ "transit_analysis": transit_analysis,
266
+ "overall_period": {
267
+ "benefic_count": benefic_count,
268
+ "malefic_count": malefic_count,
269
+ "summary": summary,
270
+ "recommendation": recommendation,
271
+ },
272
+ }
273
+
274
+ if special_conditions:
275
+ result["special_conditions"] = special_conditions
276
+
277
+ return result
@@ -0,0 +1,263 @@
1
+ """Hora: Planetary hours — each hour of the day/night ruled by a planet."""
2
+
3
+ from datetime import datetime, timedelta, timezone
4
+
5
+ import swisseph as swe
6
+
7
+ # ---------------------------------------------------------------------------
8
+ # Chaldean order and weekday mappings
9
+ # ---------------------------------------------------------------------------
10
+
11
+ CHALDEAN_ORDER = ["Saturn", "Jupiter", "Mars", "Sun", "Venus", "Mercury", "Moon"]
12
+
13
+ # Index into CHALDEAN_ORDER for the first (day-break) hour of each weekday.
14
+ # weekday() returns 0=Monday … 6=Sunday (Python convention).
15
+ WEEKDAY_FIRST_HOUR: dict[int, int] = {
16
+ 6: 3, # Sunday → Sun (index 3)
17
+ 0: 6, # Monday → Moon (index 6)
18
+ 1: 2, # Tuesday → Mars (index 2)
19
+ 2: 5, # Wednesday → Mercury (index 5)
20
+ 3: 1, # Thursday → Jupiter (index 1)
21
+ 4: 4, # Friday → Venus (index 4)
22
+ 5: 0, # Saturday → Saturn (index 0)
23
+ }
24
+
25
+ WEEKDAY_NAMES = [
26
+ "Monday",
27
+ "Tuesday",
28
+ "Wednesday",
29
+ "Thursday",
30
+ "Friday",
31
+ "Saturday",
32
+ "Sunday",
33
+ ]
34
+
35
+ HORA_SIGNIFICANCE: dict[str, str] = {
36
+ "Sun": "Leadership, government work, father, authority, health",
37
+ "Moon": "Travel, dealing with public, women, emotions, water matters",
38
+ "Mars": "Surgery, arguments, sports, enemies, property, courage",
39
+ "Mercury": "Communication, business, writing, learning, trade",
40
+ "Jupiter": "Religious work, education, wealth, children, law, spirituality",
41
+ "Venus": "Love, marriage, arts, luxury, entertainment, beauty",
42
+ "Saturn": "Hard labor, construction, agriculture, service, old people",
43
+ }
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Sunrise / Sunset helpers
48
+ # ---------------------------------------------------------------------------
49
+
50
+
51
+ def _jd_from_utc(dt: datetime) -> float:
52
+ """Convert a UTC-aware (or naive-UTC) datetime to Julian Day."""
53
+ if dt.tzinfo is None:
54
+ dt = dt.replace(tzinfo=timezone.utc)
55
+ h = dt.hour + dt.minute / 60.0 + dt.second / 3600.0
56
+ return swe.julday(dt.year, dt.month, dt.day, h)
57
+
58
+
59
+ def _get_sunrise_sunset(
60
+ dt_utc: datetime, lat: float, lon: float
61
+ ) -> tuple[float, float]:
62
+ """Return (sunrise_jd, sunset_jd) for the calendar day of dt_utc at (lat, lon).
63
+
64
+ Uses ``swe.rise_trans``. Falls back to 06:00 / 18:00 local solar noon
65
+ approximation if the call fails (e.g. extreme latitudes or ephemeris
66
+ data unavailable).
67
+ """
68
+ jd_noon = _jd_from_utc(dt_utc.replace(hour=12, minute=0, second=0, microsecond=0))
69
+ geopos = (lon, lat, 0.0) # (longitude, latitude, altitude) – swe convention
70
+ ephe_flag = swe.FLG_SWIEPH
71
+
72
+ try:
73
+ # Sunrise
74
+ ret_rise, tret_rise = swe.rise_trans(
75
+ jd_noon - 0.5, swe.SUN, "", ephe_flag, swe.CALC_RISE, geopos, 0.0, 0.0
76
+ )
77
+ sunrise_jd = tret_rise[0]
78
+
79
+ # Sunset – search forward from sunrise
80
+ ret_set, tret_set = swe.rise_trans(
81
+ sunrise_jd, swe.SUN, "", ephe_flag, swe.CALC_SET, geopos, 0.0, 0.0
82
+ )
83
+ sunset_jd = tret_set[0]
84
+
85
+ if sunrise_jd < sunset_jd:
86
+ return sunrise_jd, sunset_jd
87
+ except Exception:
88
+ pass
89
+
90
+ # Fallback: approximate as 06:00 and 18:00 UTC (naive, ignore timezone)
91
+ approx_rise = dt_utc.replace(hour=6, minute=0, second=0, microsecond=0)
92
+ approx_set = dt_utc.replace(hour=18, minute=0, second=0, microsecond=0)
93
+ return _jd_from_utc(approx_rise), _jd_from_utc(approx_set)
94
+
95
+
96
+ def _jd_to_utc(jd: float) -> datetime:
97
+ """Convert a Julian Day number back to a UTC datetime."""
98
+ y, m, d, h_frac = swe.revjul(jd)
99
+ hours = int(h_frac)
100
+ rem = (h_frac - hours) * 60.0
101
+ minutes = int(rem)
102
+ seconds = int((rem - minutes) * 60.0)
103
+ return datetime(y, m, d, hours, minutes, seconds, tzinfo=timezone.utc)
104
+
105
+
106
+ def _next_sunrise(sunset_jd: float, lat: float, lon: float) -> float:
107
+ """Return the JD of the next sunrise after sunset_jd."""
108
+ geopos = (lon, lat, 0.0)
109
+ try:
110
+ _, tret = swe.rise_trans(
111
+ sunset_jd, swe.SUN, "", swe.FLG_SWIEPH, swe.CALC_RISE, geopos, 0.0, 0.0
112
+ )
113
+ return tret[0]
114
+ except Exception:
115
+ return sunset_jd + 0.5 # +12 hours as fallback
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Hour-slot builders
120
+ # ---------------------------------------------------------------------------
121
+
122
+
123
+ def _build_hour_slots(
124
+ period_start_jd: float,
125
+ period_end_jd: float,
126
+ first_planet_index: int,
127
+ slot_offset: int,
128
+ ) -> list[dict]:
129
+ """Divide a period into 12 equal planetary hours and return slot metadata.
130
+
131
+ Args:
132
+ period_start_jd: Start of the period (JD).
133
+ period_end_jd: End of the period (JD).
134
+ first_planet_index: Index into CHALDEAN_ORDER for the first hour.
135
+ slot_offset: Running offset so night hours continue from daytime's
136
+ last planet index.
137
+ Returns:
138
+ List of 12 dicts with hour number, start/end times, and ruling planet.
139
+ """
140
+ duration_jd = (period_end_jd - period_start_jd) / 12.0
141
+ slots: list[dict] = []
142
+ for i in range(12):
143
+ planet_idx = (first_planet_index + slot_offset + i) % 7
144
+ start_jd = period_start_jd + i * duration_jd
145
+ end_jd = period_start_jd + (i + 1) * duration_jd
146
+ slots.append(
147
+ {
148
+ "hour": i + 1,
149
+ "start_time": _jd_to_utc(start_jd).strftime("%H:%M:%S UTC"),
150
+ "end_time": _jd_to_utc(end_jd).strftime("%H:%M:%S UTC"),
151
+ "planet": CHALDEAN_ORDER[planet_idx],
152
+ }
153
+ )
154
+ return slots
155
+
156
+
157
+ def _compute_hora(dt_utc: datetime, lat: float, lon: float) -> dict:
158
+ """Core computation shared by the public API functions."""
159
+ sunrise_jd, sunset_jd = _get_sunrise_sunset(dt_utc, lat, lon)
160
+ next_sunrise_jd = _next_sunrise(sunset_jd, lat, lon)
161
+
162
+ current_jd = _jd_from_utc(dt_utc)
163
+ weekday = _jd_to_utc(sunrise_jd).weekday() # weekday of the sunrise for this day
164
+ weekday_name = WEEKDAY_NAMES[weekday]
165
+ day_ruler_idx = WEEKDAY_FIRST_HOUR[weekday]
166
+ day_ruler = CHALDEAN_ORDER[day_ruler_idx]
167
+
168
+ # Build day hours (sunrise → sunset), starting at day_ruler_idx
169
+ day_hours = _build_hour_slots(sunrise_jd, sunset_jd, day_ruler_idx, slot_offset=0)
170
+ # Night hours continue from where day hours left off (12 slots consumed = +12)
171
+ night_hours = _build_hour_slots(
172
+ sunset_jd, next_sunrise_jd, day_ruler_idx, slot_offset=12
173
+ )
174
+
175
+ # Determine which hour the current moment falls in
176
+ is_day = sunrise_jd <= current_jd < sunset_jd
177
+ hour_number = 1
178
+ planetary_hour_lord = day_ruler
179
+
180
+ if is_day:
181
+ day_slot_jd = (sunset_jd - sunrise_jd) / 12.0
182
+ slot_i = int((current_jd - sunrise_jd) / day_slot_jd)
183
+ slot_i = max(0, min(11, slot_i))
184
+ hour_number = slot_i + 1
185
+ planetary_hour_lord = day_hours[slot_i]["planet"]
186
+ else:
187
+ # Normalise current_jd into night window
188
+ if current_jd < sunrise_jd:
189
+ # Pre-sunrise: part of previous day's night — treat as night hour
190
+ effective_start = sunset_jd - (next_sunrise_jd - sunset_jd)
191
+ # This is an edge case; fall back to last night slot
192
+ night_slot_jd = (next_sunrise_jd - sunset_jd) / 12.0
193
+ elapsed = current_jd - sunset_jd
194
+ if elapsed < 0:
195
+ elapsed += next_sunrise_jd - sunset_jd
196
+ else:
197
+ night_slot_jd = (next_sunrise_jd - sunset_jd) / 12.0
198
+ elapsed = current_jd - sunset_jd
199
+
200
+ slot_i = int(elapsed / night_slot_jd)
201
+ slot_i = max(0, min(11, slot_i))
202
+ hour_number = slot_i + 1
203
+ planetary_hour_lord = night_hours[slot_i]["planet"]
204
+
205
+ return {
206
+ "datetime_utc": dt_utc.strftime("%Y-%m-%d %H:%M:%S UTC"),
207
+ "weekday": weekday_name,
208
+ "sunrise_approx": _jd_to_utc(sunrise_jd).strftime("%H:%M:%S UTC"),
209
+ "sunset_approx": _jd_to_utc(sunset_jd).strftime("%H:%M:%S UTC"),
210
+ "is_day": is_day,
211
+ "hour_number": hour_number,
212
+ "planetary_hour_lord": planetary_hour_lord,
213
+ "day_ruler": day_ruler,
214
+ "all_day_hours": day_hours,
215
+ "all_night_hours": night_hours,
216
+ "significance": HORA_SIGNIFICANCE.get(planetary_hour_lord, ""),
217
+ }
218
+
219
+
220
+ # ---------------------------------------------------------------------------
221
+ # Public API
222
+ # ---------------------------------------------------------------------------
223
+
224
+
225
+ def get_planetary_hour(dt_utc: datetime, latitude: float, longitude: float) -> dict:
226
+ """Return the planetary hour ruling the given UTC moment at (latitude, longitude).
227
+
228
+ Args:
229
+ dt_utc: A datetime object representing the moment of interest (UTC).
230
+ May be timezone-naive (treated as UTC) or tz-aware.
231
+ latitude: Geographic latitude in decimal degrees (north positive).
232
+ longitude: Geographic longitude in decimal degrees (east positive).
233
+
234
+ Returns:
235
+ A dict with weekday, sunrise/sunset times, day/night flag, hour number,
236
+ ruling planet, all 12 day hours, all 12 night hours, and significance text.
237
+ """
238
+ swe.set_ephe_path(None)
239
+ return _compute_hora(dt_utc, latitude, longitude)
240
+
241
+
242
+ def get_hora_chart(
243
+ birth_datetime_utc: datetime,
244
+ birth_lat: float,
245
+ birth_lon: float,
246
+ ) -> dict:
247
+ """Return the planetary hour ruling the birth moment.
248
+
249
+ Args:
250
+ birth_datetime_utc: Birth datetime (UTC).
251
+ birth_lat: Birth latitude.
252
+ birth_lon: Birth longitude.
253
+
254
+ Returns:
255
+ Same structure as ``get_planetary_hour`` — the Hora at the time of birth.
256
+ """
257
+ swe.set_ephe_path(None)
258
+ result = _compute_hora(birth_datetime_utc, birth_lat, birth_lon)
259
+ result["context"] = (
260
+ "Hora chart at birth: the planetary hour lord at birth colours the native's "
261
+ "temperament and early instincts, supplementing the Lagna lord."
262
+ )
263
+ return result
@@ -0,0 +1,30 @@
1
+ """House cusps (sidereal, configurable house system)."""
2
+
3
+ import swisseph as swe
4
+
5
+ from kundali_lib.vedic.ayanamsa import DEFAULT_HOUSE_SYSTEM
6
+ from kundali_lib.vedic.zodiac import get_rashi
7
+
8
+
9
+ def calc_houses_sidereal_with_system(
10
+ julian_day: float, lat: float, lon: float, house_system: str | None = None
11
+ ) -> tuple[list[float], list[float]]:
12
+ """Returns (cusps[12], ascmc[8]).
13
+
14
+ house_system: single char code like 'P' (Placidus), 'K' (Koch), 'E' (Equal), etc.
15
+ """
16
+ hs = (house_system or DEFAULT_HOUSE_SYSTEM).upper()[:1]
17
+ cusps, ascmc = swe.houses_ex(
18
+ julian_day, lat, lon, hs.encode("ascii"), swe.FLG_SIDEREAL
19
+ )
20
+ return list(cusps), list(ascmc)
21
+
22
+
23
+ def houses_dict_and_degrees(cusps: list[float]) -> tuple[dict[str, str], list[float]]:
24
+ """From cusps, return ({House_1: rashi, ...}, [deg1, ...])."""
25
+ houses = {}
26
+ degrees = []
27
+ for i, cusp in enumerate(cusps[:12]):
28
+ houses[f"House_{i + 1}"] = get_rashi(cusp)
29
+ degrees.append(round(cusp % 30, 4))
30
+ return houses, degrees