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.
- vedic_ghadi-1.0.0/PKG-INFO +43 -0
- vedic_ghadi-1.0.0/README.md +16 -0
- vedic_ghadi-1.0.0/pyproject.toml +48 -0
- vedic_ghadi-1.0.0/setup.cfg +4 -0
- vedic_ghadi-1.0.0/tests/test_api.py +64 -0
- vedic_ghadi-1.0.0/tests/test_parity.py +92 -0
- vedic_ghadi-1.0.0/tests/test_substrate.py +189 -0
- vedic_ghadi-1.0.0/vedic_ghadi/__init__.py +42 -0
- vedic_ghadi-1.0.0/vedic_ghadi/__main__.py +6 -0
- vedic_ghadi-1.0.0/vedic_ghadi/api.py +129 -0
- vedic_ghadi-1.0.0/vedic_ghadi/cli.py +74 -0
- vedic_ghadi-1.0.0/vedic_ghadi/ghadi.py +143 -0
- vedic_ghadi-1.0.0/vedic_ghadi/substrate.py +380 -0
- vedic_ghadi-1.0.0/vedic_ghadi.egg-info/PKG-INFO +43 -0
- vedic_ghadi-1.0.0/vedic_ghadi.egg-info/SOURCES.txt +17 -0
- vedic_ghadi-1.0.0/vedic_ghadi.egg-info/dependency_links.txt +1 -0
- vedic_ghadi-1.0.0/vedic_ghadi.egg-info/entry_points.txt +2 -0
- vedic_ghadi-1.0.0/vedic_ghadi.egg-info/requires.txt +9 -0
- vedic_ghadi-1.0.0/vedic_ghadi.egg-info/top_level.txt +1 -0
|
@@ -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,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,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ḍī · 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> · current moment</li>
|
|
68
|
+
<li><a href="/at?date=2026-05-17T16:30:00">GET /at?date=…</a> · any moment</li>
|
|
69
|
+
<li><a href="/stream">GET /stream</a> · SSE feed (one event per prāṇa = 4 sec)</li>
|
|
70
|
+
<li><a href="/docs">GET /docs</a> · OpenAPI / Swagger</li>
|
|
71
|
+
<li><a href="/healthz">GET /healthz</a> · liveness</li>
|
|
72
|
+
</ul>
|
|
73
|
+
<p style="opacity:.7">ॐ कालाय नमः · 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
|
+
)
|