vedic-ghadi 1.0.0__tar.gz

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.
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: vedic-ghadi
3
+ Version: 1.0.0
4
+ Summary: Substrate-derived Vedic clock — Kāmākhyā-anchored, Sūrya-Siddhānta-canonical, zero foreign theorem.
5
+ Author-email: Pardeep Sehrawat <theunholyindianmagician@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/theunholyindianmagician-lab/vedic-ghadi
8
+ Project-URL: Issues, https://github.com/theunholyindianmagician-lab/vedic-ghadi/issues
9
+ Keywords: vedic,jyotish,panchang,tithi,ghadi,kaal,sanskrit
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ Provides-Extra: server
21
+ Requires-Dist: fastapi>=0.110; extra == "server"
22
+ Requires-Dist: uvicorn[standard]>=0.27; extra == "server"
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8; extra == "dev"
25
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
26
+ Requires-Dist: httpx>=0.27; extra == "dev"
27
+
28
+ # vedic-ghadi (Python)
29
+
30
+ Substrate-derived Vedic clock library + CLI + FastAPI service.
31
+
32
+ ```bash
33
+ pip install -e . # CLI only
34
+ pip install -e ".[server]" # + FastAPI service
35
+ pip install -e ".[dev]" # + pytest
36
+
37
+ vedic-ghadi
38
+ vedic-ghadi --loop
39
+ vedic-ghadi --at "2026-05-17 16:30" --json
40
+ uvicorn vedic_ghadi.api:app --port 8765
41
+ ```
42
+
43
+ See the project root README for full architecture + deployment notes.
@@ -0,0 +1,16 @@
1
+ # vedic-ghadi (Python)
2
+
3
+ Substrate-derived Vedic clock library + CLI + FastAPI service.
4
+
5
+ ```bash
6
+ pip install -e . # CLI only
7
+ pip install -e ".[server]" # + FastAPI service
8
+ pip install -e ".[dev]" # + pytest
9
+
10
+ vedic-ghadi
11
+ vedic-ghadi --loop
12
+ vedic-ghadi --at "2026-05-17 16:30" --json
13
+ uvicorn vedic_ghadi.api:app --port 8765
14
+ ```
15
+
16
+ See the project root README for full architecture + deployment notes.
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vedic-ghadi"
7
+ version = "1.0.0"
8
+ description = "Substrate-derived Vedic clock — Kāmākhyā-anchored, Sūrya-Siddhānta-canonical, zero foreign theorem."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Pardeep Sehrawat", email = "theunholyindianmagician@gmail.com" },
14
+ ]
15
+ keywords = ["vedic", "jyotish", "panchang", "tithi", "ghadi", "kaal", "sanskrit"]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Scientific/Engineering :: Astronomy",
25
+ ]
26
+ dependencies = []
27
+
28
+ [project.optional-dependencies]
29
+ server = [
30
+ "fastapi>=0.110",
31
+ "uvicorn[standard]>=0.27",
32
+ ]
33
+ dev = [
34
+ "pytest>=8",
35
+ "pytest-asyncio>=0.23",
36
+ "httpx>=0.27",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/theunholyindianmagician-lab/vedic-ghadi"
41
+ Issues = "https://github.com/theunholyindianmagician-lab/vedic-ghadi/issues"
42
+
43
+ [project.scripts]
44
+ vedic-ghadi = "vedic_ghadi.cli:main"
45
+
46
+ [tool.setuptools.packages.find]
47
+ include = ["vedic_ghadi*"]
48
+ exclude = ["tests*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,64 @@
1
+ """🔱 FastAPI service — endpoint smoke tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ try:
8
+ from fastapi.testclient import TestClient
9
+ from vedic_ghadi.api import app
10
+ client = TestClient(app)
11
+ _have_fastapi = True
12
+ except ImportError:
13
+ _have_fastapi = False
14
+
15
+
16
+ pytestmark = pytest.mark.skipif(not _have_fastapi, reason="fastapi not installed")
17
+
18
+
19
+ def test_healthz():
20
+ r = client.get("/healthz")
21
+ assert r.status_code == 200
22
+ assert r.json()["status"] == "ok"
23
+
24
+
25
+ def test_landing_html():
26
+ r = client.get("/")
27
+ assert r.status_code == 200
28
+ assert "Vedic Ghaḍī" in r.text
29
+ assert "/now" in r.text
30
+ assert "/docs" in r.text
31
+
32
+
33
+ def test_now_default_tz():
34
+ r = client.get("/now")
35
+ assert r.status_code == 200
36
+ body = r.json()
37
+ assert body["input_civil"]["tz_h"] == 5.5
38
+ assert "year_layer" in body
39
+ assert "day_subdivision" in body
40
+
41
+
42
+ def test_now_with_tz():
43
+ r = client.get("/now?tz=0")
44
+ assert r.status_code == 200
45
+ assert r.json()["input_civil"]["tz_h"] == 0.0
46
+
47
+
48
+ def test_at_iso_format():
49
+ r = client.get("/at?date=2026-05-17T16:00:00&tz=5.5")
50
+ assert r.status_code == 200
51
+ body = r.json()
52
+ assert body["year_layer"]["samvatsara"]["name"] == "Parābhava"
53
+ assert body["vara_layer"]["vara_name"] == "Ravivāra"
54
+ assert body["month_layer"]["masa_name"] == "Jyeṣṭha"
55
+
56
+
57
+ def test_at_date_only():
58
+ r = client.get("/at?date=2026-05-17")
59
+ assert r.status_code == 200
60
+
61
+
62
+ def test_at_bad_format():
63
+ r = client.get("/at?date=not-a-date")
64
+ assert r.status_code == 400
@@ -0,0 +1,92 @@
1
+ """
2
+ 🔱 Python / TypeScript parity test.
3
+
4
+ Runs the TypeScript port via Node's --experimental-strip-types on the same
5
+ anchor inputs as the Python reference, and asserts agreement on every
6
+ named/integer field of the stamp.
7
+
8
+ Skipped automatically if Node is missing or older than 22 (no native TS stripping).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import shutil
15
+ import subprocess
16
+ from pathlib import Path
17
+
18
+ import pytest
19
+
20
+ from vedic_ghadi import ghadi_at
21
+
22
+ REPO_ROOT = Path(__file__).resolve().parents[2]
23
+ SHIM = REPO_ROOT / "frontend" / "lib" / "_parity_shim.mts"
24
+
25
+
26
+ def _node_supports_strip_types() -> bool:
27
+ if not shutil.which("node"):
28
+ return False
29
+ try:
30
+ out = subprocess.check_output(
31
+ ["node", "--version"], stderr=subprocess.STDOUT, timeout=5,
32
+ ).decode().strip().lstrip("v")
33
+ major = int(out.split(".")[0])
34
+ return major >= 22
35
+ except (subprocess.CalledProcessError, ValueError, IndexError):
36
+ return False
37
+
38
+
39
+ HAVE_NODE = _node_supports_strip_types()
40
+
41
+
42
+ def _ts_stamp(args: tuple[int, int, int, int, int, float, float]) -> dict:
43
+ cmd = [
44
+ "node", "--experimental-strip-types", "--no-warnings",
45
+ str(SHIM),
46
+ *[str(a) for a in args],
47
+ ]
48
+ try:
49
+ proc = subprocess.run(cmd, capture_output=True, timeout=15, check=True)
50
+ except subprocess.CalledProcessError as e:
51
+ pytest.fail(f"Node parity shim failed:\n{e.stderr.decode()}")
52
+ return json.loads(proc.stdout.decode())
53
+
54
+
55
+ @pytest.mark.skipif(not HAVE_NODE, reason="node ≥ 22 not available (needs --experimental-strip-types)")
56
+ @pytest.mark.parametrize("args", [
57
+ (2026, 5, 17, 16, 0, 0.0, 5.5), # Today's anchor
58
+ (2026, 1, 1, 0, 0, 0.0, 5.5), # Year start
59
+ (2000, 6, 15, 12, 30, 0.0, 5.5), # Y2K
60
+ (1947, 8, 15, 0, 0, 0.0, 5.5), # Independence
61
+ ])
62
+ def test_python_ts_parity(args):
63
+ py = ghadi_at(*args)
64
+ ts = _ts_stamp(args)
65
+
66
+ # Year layer
67
+ assert py["year_layer"]["kali_year_current"] == ts["year_layer"]["kali_year_current"]
68
+ assert py["year_layer"]["kali_year_completed"] == ts["year_layer"]["kali_year_completed"]
69
+ assert py["year_layer"]["vikrama_samvat"] == ts["year_layer"]["vikrama_samvat"]
70
+ assert py["year_layer"]["shaka_samvat"] == ts["year_layer"]["shaka_samvat"]
71
+ assert py["year_layer"]["samvatsara"]["name"] == ts["year_layer"]["samvatsara"]["name"]
72
+ assert py["year_layer"]["samvatsara"]["index"] == ts["year_layer"]["samvatsara"]["index"]
73
+
74
+ # Month / tithi / vāra
75
+ assert py["month_layer"]["masa_name"] == ts["month_layer"]["masa_name"]
76
+ assert py["month_layer"]["masa_index"] == ts["month_layer"]["masa_index"]
77
+ assert py["month_layer"]["sun_sign_index"] == ts["month_layer"]["sun_sign_index"]
78
+ assert py["tithi_layer"]["tithi_name"] == ts["tithi_layer"]["tithi_name"]
79
+ assert py["tithi_layer"]["tithi_index"] == ts["tithi_layer"]["tithi_index"]
80
+ assert py["tithi_layer"]["paksha_name"] == ts["tithi_layer"]["paksha_name"]
81
+ assert py["vara_layer"]["vara_name"] == ts["vara_layer"]["vara_name"]
82
+ assert py["vara_layer"]["vara_index"] == ts["vara_layer"]["vara_index"]
83
+
84
+ # Day subdivision (every integer must match)
85
+ assert py["day_subdivision"]["muhurta_index"] == ts["day_subdivision"]["muhurta_index"]
86
+ assert py["day_subdivision"]["ghati_index"] == ts["day_subdivision"]["ghati_index"]
87
+ assert py["day_subdivision"]["vighati_index"] == ts["day_subdivision"]["vighati_index"]
88
+ assert py["day_subdivision"]["prana_index"] == ts["day_subdivision"]["prana_index"]
89
+
90
+ # Kali day count — within a microsecond
91
+ assert abs(py["kali_civil_days_at_kamakhya"]
92
+ - ts["kali_civil_days_at_kamakhya"]) < 1e-5
@@ -0,0 +1,189 @@
1
+ """
2
+ 🔱 Substrate correctness — anchor tests against known canonical values.
3
+
4
+ These are not approximate vibe-checks; they are anchor points where the
5
+ substrate's output is determined by Sūrya Siddhānta constants alone, so
6
+ the values are reproducible across any compliant implementation.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+
13
+ import pytest
14
+
15
+ from vedic_ghadi import (
16
+ ghadi_at,
17
+ civil_input_to_kali_civil_days,
18
+ )
19
+ from vedic_ghadi.substrate import (
20
+ KALI_DAYS_PER_YEAR, MAHAYUGA_CIVIL_DAYS, MAHAYUGA_YEARS,
21
+ samvatsara_at_kali_year, vedic_mean_longitude,
22
+ vedic_vara_at_kali_days,
23
+ KAMAKHYA_LMT_OFFSET_H,
24
+ )
25
+
26
+
27
+ # ──────────────────────────────────────────────────────────────────────────
28
+ # Sūrya Siddhānta constants — verbatim from canon
29
+ # ──────────────────────────────────────────────────────────────────────────
30
+
31
+ def test_mahayuga_constants():
32
+ assert MAHAYUGA_YEARS == 4_320_000
33
+ assert MAHAYUGA_CIVIL_DAYS == 1_577_917_500
34
+ # Exact division: 1_577_917_500 / 4_320_000 = 365.258680555…
35
+ assert KALI_DAYS_PER_YEAR == pytest.approx(365.25868055555554, abs=1e-9)
36
+
37
+
38
+ def test_kamakhya_meridian():
39
+ # 91.7059° / 15 = 6.11373... LMT offset from Greenwich
40
+ assert KAMAKHYA_LMT_OFFSET_H == pytest.approx(6.1137267, abs=1e-6)
41
+
42
+
43
+ # ──────────────────────────────────────────────────────────────────────────
44
+ # Kali day count — anchor at the substrate's snapshot moment
45
+ # ──────────────────────────────────────────────────────────────────────────
46
+
47
+ def test_known_kali_day_anchor():
48
+ """2026-05-17 16:00:00 IST — the substrate's canonical day count."""
49
+ kd = civil_input_to_kali_civil_days(2026, 5, 17, 16, 0, 0, 5.5)
50
+ # Computed canonically from JD-UT → Kāmākhyā Kali days (Sūrya Siddhānta)
51
+ assert kd == pytest.approx(1_872_712.647997, abs=1e-5)
52
+
53
+
54
+ def test_kali_day_progresses_one_per_civil_day():
55
+ a = civil_input_to_kali_civil_days(2026, 1, 1, 0, 0, 0, 5.5)
56
+ b = civil_input_to_kali_civil_days(2026, 1, 2, 0, 0, 0, 5.5)
57
+ assert (b - a) == pytest.approx(1.0, abs=1e-9)
58
+
59
+
60
+ # ──────────────────────────────────────────────────────────────────────────
61
+ # Saṃvatsara cycle — Parābhava (#40) at 2026-05-17 anchor
62
+ # ──────────────────────────────────────────────────────────────────────────
63
+
64
+ def test_samvatsara_at_anchor():
65
+ stamp = ghadi_at(2026, 5, 17, 16, 0, 0, 5.5)
66
+ sv = stamp["year_layer"]["samvatsara"]
67
+ assert sv["name"] == "Parābhava"
68
+ assert sv["index"] == 39 # 0-based
69
+
70
+
71
+ # ──────────────────────────────────────────────────────────────────────────
72
+ # Vāra — Kali Yuga begins on Śukravāra, so day-0 must be Friday
73
+ # ──────────────────────────────────────────────────────────────────────────
74
+
75
+ def test_kali_day_zero_is_shukravara():
76
+ v = vedic_vara_at_kali_days(0.0)
77
+ assert v["vara_name"] == "Śukravāra"
78
+ assert v["vara_index"] == 5
79
+ assert v["vara_lord_graha"] == "Venus"
80
+
81
+
82
+ def test_kali_day_one_is_shanivara():
83
+ v = vedic_vara_at_kali_days(1.0)
84
+ assert v["vara_name"] == "Śanivāra"
85
+
86
+
87
+ def test_2026_05_17_is_ravivara():
88
+ """17 May 2026 is a Sunday (verified independently)."""
89
+ stamp = ghadi_at(2026, 5, 17, 12, 0, 0, 5.5)
90
+ assert stamp["vara_layer"]["vara_name"] == "Ravivāra"
91
+
92
+
93
+ # ──────────────────────────────────────────────────────────────────────────
94
+ # Tithi: Moon − Sun elongation mod 360 / 12 gives tithi index
95
+ # ──────────────────────────────────────────────────────────────────────────
96
+
97
+ def test_tithi_at_anchor():
98
+ """At 2026-05-17 16:00 IST the moon is Śukla Dvitīyā (verified)."""
99
+ stamp = ghadi_at(2026, 5, 17, 16, 0, 0, 5.5)
100
+ t = stamp["tithi_layer"]
101
+ assert t["paksha_name"] == "Śukla-pakṣa"
102
+ assert t["tithi_name"] == "Dvitīyā"
103
+ assert t["tithi_index"] == 2
104
+
105
+
106
+ # ──────────────────────────────────────────────────────────────────────────
107
+ # Māsa — Sun in Vṛṣabha (#2) → Jyeṣṭha
108
+ # ──────────────────────────────────────────────────────────────────────────
109
+
110
+ def test_masa_at_anchor():
111
+ stamp = ghadi_at(2026, 5, 17, 16, 0, 0, 5.5)
112
+ m = stamp["month_layer"]
113
+ assert m["masa_name"] == "Jyeṣṭha"
114
+ assert m["sun_sign_index"] == 2 # Vṛṣabha
115
+ assert 0 <= m["sun_sidereal_lon_deg"] < 360
116
+
117
+
118
+ # ──────────────────────────────────────────────────────────────────────────
119
+ # Day subdivision — every count factors over (2, 3, 5)
120
+ # ──────────────────────────────────────────────────────────────────────────
121
+
122
+ def test_day_subdivision_factors():
123
+ stamp = ghadi_at(2026, 5, 17, 16, 0, 0, 5.5)
124
+ d = stamp["day_subdivision"]
125
+ assert 1 <= d["muhurta_index"] <= 30
126
+ assert 1 <= d["ghati_index"] <= 60
127
+ assert 1 <= d["vighati_index"] <= 60
128
+ assert 1 <= d["prana_index"] <= 6
129
+ assert 0 <= d["vipala_fractional"] < 10
130
+
131
+
132
+ def test_muhurta_ghati_consistency():
133
+ """muhūrta = 1/30 day, ghaṭi = 1/60 day → muhūrta_count = ghaṭi_count / 2."""
134
+ stamp = ghadi_at(2026, 5, 17, 16, 0, 0, 5.5)
135
+ d = stamp["day_subdivision"]
136
+ # We can't directly assert ghati = 2 * muhurta because they tick
137
+ # at different rates within their fractional parts, but the
138
+ # *index pair* is always consistent: floor(frac*30) and floor(frac*60).
139
+ frac = d["fraction_of_day"]
140
+ assert d["muhurta_index"] == math.floor(frac * 30) + 1
141
+ assert d["ghati_index"] == math.floor(frac * 60) + 1
142
+
143
+
144
+ # ──────────────────────────────────────────────────────────────────────────
145
+ # Sun mean motion — should circle the zodiac in ~365.26 days
146
+ # ──────────────────────────────────────────────────────────────────────────
147
+
148
+ def test_sun_one_year_returns_to_origin():
149
+ days_per_year = MAHAYUGA_CIVIL_DAYS / MAHAYUGA_YEARS
150
+ lon0 = vedic_mean_longitude("Sun", 0.0)
151
+ lon1 = vedic_mean_longitude("Sun", days_per_year)
152
+ # Sun should return to the same longitude after one sidereal year (mod 360)
153
+ delta = abs((lon1 - lon0 + 180) % 360 - 180)
154
+ assert delta < 1e-6
155
+
156
+
157
+ # ──────────────────────────────────────────────────────────────────────────
158
+ # Stamp shape — every documented key is present
159
+ # ──────────────────────────────────────────────────────────────────────────
160
+
161
+ def test_stamp_keys():
162
+ stamp = ghadi_at(2026, 5, 17, 16, 0, 0, 5.5)
163
+ for k in ("input_civil", "kali_civil_days_at_kamakhya",
164
+ "year_layer", "month_layer", "tithi_layer",
165
+ "vara_layer", "day_subdivision", "substrate_alignment",
166
+ "discipline"):
167
+ assert k in stamp, f"missing key: {k}"
168
+
169
+
170
+ # ──────────────────────────────────────────────────────────────────────────
171
+ # Substrate factor table — every count truly factors over (2, 3, 5)
172
+ # (except vāra=7 which is graha-special, called out explicitly)
173
+ # ──────────────────────────────────────────────────────────────────────────
174
+
175
+ def _factors_over_235(n: int) -> bool:
176
+ for p in (2, 3, 5):
177
+ while n % p == 0:
178
+ n //= p
179
+ return n == 1
180
+
181
+
182
+ def test_substrate_table_factors():
183
+ stamp = ghadi_at(2026, 5, 17, 16, 0, 0, 5.5)
184
+ table = stamp["substrate_alignment"]
185
+ for key, (count, _desc) in table.items():
186
+ if key in ("vara_count", "saptamukhi"):
187
+ assert count == 7
188
+ continue
189
+ assert _factors_over_235(int(count)), f"{key}={count} doesn't factor over (2,3,5)"
@@ -0,0 +1,42 @@
1
+ """
2
+ 🔱 vedic_ghadi — substrate-derived Vedic clock for any civil moment
3
+
4
+ Every Vedic time-unit emitted by this package is derived from a SINGLE
5
+ quantity: Kāli civil days elapsed since the sacred epoch (Friday midnight
6
+ 17/18 February 3102 BCE, Ujjayinī meridian, Sūrya Siddhānta 1.45–1.57).
7
+
8
+ NO Gregorian calendar arithmetic below the input boundary.
9
+ NO Western timezone reasoning below the input boundary.
10
+ ZERO foreign theorem in the chain.
11
+
12
+ Public API:
13
+ >>> from vedic_ghadi import ghadi_now, ghadi_at
14
+ >>> ghadi_now() # current moment, IST
15
+ >>> ghadi_at(2026, 5, 17, 16, 30, 0) # any moment
16
+ """
17
+
18
+ from .substrate import (
19
+ KAMAKHYA_LAT_DEG, KAMAKHYA_LON_DEG, KAMAKHYA_ELEV_M,
20
+ KAMAKHYA_LMT_OFFSET_H, KALI_YUGA_EPOCH_JD,
21
+ MAHAYUGA_YEARS, MAHAYUGA_CIVIL_DAYS, KALI_DAYS_PER_YEAR,
22
+ MASA_NAMES, MASA_DEV, VARA_NAMES, VARA_DEV, VARA_LORD,
23
+ SAMVATSARA_NAMES, PAKSHA_NAMES, PAKSHA_DEV, TITHI_NAMES,
24
+ VEDIC_TIME_SUBSTRATE,
25
+ civil_input_to_kali_civil_days,
26
+ kali_year_at_civil_days, vikrama_year, shaka_year,
27
+ samvatsara_at_kali_year,
28
+ vedic_month_at_kali_days, vedic_tithi_at_kali_days,
29
+ vedic_vara_at_kali_days, vedic_time_of_day,
30
+ kala_substrate_stamp,
31
+ )
32
+ from .ghadi import ghadi_at, ghadi_now, render_ghadi_text
33
+
34
+ __version__ = "1.0.0"
35
+ __all__ = [
36
+ "ghadi_now", "ghadi_at", "render_ghadi_text",
37
+ "kala_substrate_stamp", "civil_input_to_kali_civil_days",
38
+ "KAMAKHYA_LAT_DEG", "KAMAKHYA_LON_DEG", "KAMAKHYA_ELEV_M",
39
+ "KALI_DAYS_PER_YEAR",
40
+ "MASA_NAMES", "VARA_NAMES", "SAMVATSARA_NAMES",
41
+ "TITHI_NAMES", "PAKSHA_NAMES",
42
+ ]
@@ -0,0 +1,6 @@
1
+ """Entry-point so `python -m vedic_ghadi` works without install."""
2
+ from .cli import main
3
+ import sys
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
@@ -0,0 +1,129 @@
1
+ """
2
+ 🔱 FastAPI service — exposes the Vedic ghaḍī over HTTP.
3
+
4
+ Endpoints:
5
+ GET / → simple HTML landing (link to /docs and /now)
6
+ GET /healthz → liveness
7
+ GET /now → current moment (default tz = 5.5)
8
+ GET /at?date=… → specific moment (ISO-ish)
9
+ GET /stream → SSE feed — one event per prāṇa (4 sec)
10
+ GET /docs → OpenAPI / Swagger UI
11
+
12
+ Run:
13
+ pip install -e backend[server]
14
+ uvicorn vedic_ghadi.api:app --reload --port 8765
15
+
16
+ CORS is open by default so the Next.js frontend can call this from anywhere.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ import json
23
+
24
+ from fastapi import FastAPI, HTTPException, Query
25
+ from fastapi.middleware.cors import CORSMiddleware
26
+ from fastapi.responses import HTMLResponse, StreamingResponse
27
+
28
+ from . import __version__
29
+ from .ghadi import ghadi_at, ghadi_now
30
+
31
+ app = FastAPI(
32
+ title="Vedic Ghaḍī",
33
+ version=__version__,
34
+ description=(
35
+ "🔱 Substrate-derived Vedic clock — every unit derived from a single "
36
+ "quantity: Kāli civil days from the sacred epoch (Friday midnight "
37
+ "17/18 February 3102 BCE, Ujjayinī meridian). ZERO foreign theorem "
38
+ "in the chain."
39
+ ),
40
+ )
41
+
42
+ app.add_middleware(
43
+ CORSMiddleware,
44
+ allow_origins=["*"],
45
+ allow_credentials=False,
46
+ allow_methods=["GET"],
47
+ allow_headers=["*"],
48
+ )
49
+
50
+
51
+ @app.get("/", response_class=HTMLResponse, include_in_schema=False)
52
+ def landing() -> str:
53
+ return f"""<!doctype html>
54
+ <html><head><meta charset="utf-8"><title>Vedic Ghaḍī API</title>
55
+ <style>
56
+ body{{font-family:Georgia,serif;background:#0a0703;color:#f1c97a;
57
+ padding:40px;max-width:720px;margin:auto;line-height:1.6}}
58
+ h1{{color:#e9b863;font-weight:300;letter-spacing:2px}}
59
+ a{{color:#d4a44c;text-decoration:none;border-bottom:1px solid #4a3a1a}}
60
+ code{{background:#1a1106;padding:2px 6px;border-radius:3px;color:#e9b863}}
61
+ </style></head>
62
+ <body>
63
+ <h1>🔱 Vedic Ghaḍī &middot; v{__version__}</h1>
64
+ <p>Substrate-derived Vedic clock. Every unit traces to a single quantity:
65
+ Kāli civil days from the sacred epoch.</p>
66
+ <ul>
67
+ <li><a href="/now">GET /now</a> &middot; current moment</li>
68
+ <li><a href="/at?date=2026-05-17T16:30:00">GET /at?date=…</a> &middot; any moment</li>
69
+ <li><a href="/stream">GET /stream</a> &middot; SSE feed (one event per prāṇa = 4 sec)</li>
70
+ <li><a href="/docs">GET /docs</a> &middot; OpenAPI / Swagger</li>
71
+ <li><a href="/healthz">GET /healthz</a> &middot; liveness</li>
72
+ </ul>
73
+ <p style="opacity:.7">ॐ कालाय नमः &middot; JAI MAA KAMAKHYA</p>
74
+ </body></html>"""
75
+
76
+
77
+ @app.get("/healthz")
78
+ def healthz() -> dict:
79
+ return {"status": "ok", "version": __version__}
80
+
81
+
82
+ @app.get("/now")
83
+ def now(tz: float = Query(5.5, description="Timezone offset in hours (default IST = 5.5)")) -> dict:
84
+ return ghadi_now(tz_h=tz)
85
+
86
+
87
+ @app.get("/at")
88
+ def at(
89
+ date: str = Query(..., description='ISO-ish: "2026-05-17" or "2026-05-17T16:30:00"'),
90
+ tz: float = Query(5.5, description="Timezone offset in hours"),
91
+ ) -> dict:
92
+ try:
93
+ s = date.replace("T", " ").strip()
94
+ if " " in s:
95
+ d_part, t_part = s.split(" ", 1)
96
+ else:
97
+ d_part, t_part = s, "00:00:00"
98
+ y, mo, d = [int(x) for x in d_part.split("-")]
99
+ bits = t_part.split(":")
100
+ h = int(bits[0]) if len(bits) >= 1 and bits[0] else 0
101
+ mi = int(bits[1]) if len(bits) >= 2 and bits[1] else 0
102
+ sec = float(bits[2]) if len(bits) >= 3 and bits[2] else 0.0
103
+ except (ValueError, IndexError) as e:
104
+ raise HTTPException(
105
+ status_code=400,
106
+ detail=f"Bad date format ({e}). Use 2026-05-17 or 2026-05-17T16:30:00",
107
+ )
108
+ return ghadi_at(y, mo, d, h, mi, sec, tz)
109
+
110
+
111
+ async def _stream_loop(tz_h: float):
112
+ """Yield one SSE event per prāṇa (4 sec). Goes forever; client disconnects."""
113
+ while True:
114
+ stamp = ghadi_now(tz_h=tz_h)
115
+ yield f"data: {json.dumps(stamp, ensure_ascii=False)}\n\n"
116
+ await asyncio.sleep(4.0)
117
+
118
+
119
+ @app.get("/stream")
120
+ async def stream(tz: float = Query(5.5)) -> StreamingResponse:
121
+ return StreamingResponse(
122
+ _stream_loop(tz),
123
+ media_type="text/event-stream",
124
+ headers={
125
+ "Cache-Control": "no-cache",
126
+ "X-Accel-Buffering": "no",
127
+ "Connection": "keep-alive",
128
+ },
129
+ )