Wire VedAstro official snapshot priority
This commit is contained in:
@@ -57,6 +57,7 @@ def _attach_vedastro_main_entry_overview(chart_result, birth_payload):
|
||||
|
||||
try:
|
||||
orchestrator = _load_local_module('vedastro_evidence_orchestrator')
|
||||
priority = _load_local_module('vedastro_priority')
|
||||
except Exception:
|
||||
return chart_result
|
||||
|
||||
@@ -65,7 +66,7 @@ def _attach_vedastro_main_entry_overview(chart_result, birth_payload):
|
||||
or birth_payload.get('today')
|
||||
or datetime.utcnow().strftime('%Y-%m-%d')
|
||||
)[:10]
|
||||
modules['vedastro_range_scan_result'] = orchestrator.orchestrate_vedastro_evidence({
|
||||
vedastro_evidence = orchestrator.orchestrate_vedastro_evidence({
|
||||
'year': birth_payload.get('year'),
|
||||
'month': birth_payload.get('month'),
|
||||
'day': birth_payload.get('day'),
|
||||
@@ -78,6 +79,17 @@ def _attach_vedastro_main_entry_overview(chart_result, birth_payload):
|
||||
'ayanamsa_policy': birth_payload.get('ayanamsa') or 'lahiri',
|
||||
'node_policy': birth_payload.get('node_mode') or birth_payload.get('nodeMode') or 'mean',
|
||||
}, route='overview', reference_date=reference_date, case_id='api_chart')
|
||||
if isinstance(vedastro_evidence, dict):
|
||||
metadata = vedastro_evidence.get('source_metadata')
|
||||
if not isinstance(metadata, dict):
|
||||
metadata = {}
|
||||
metadata.setdefault('ingestion_profile', 'main_entry_overview')
|
||||
metadata.setdefault('reference_date', reference_date)
|
||||
vedastro_evidence['source_metadata'] = metadata
|
||||
modules['vedastro_range_scan_result'] = vedastro_evidence
|
||||
official_snapshot = vedastro_evidence.get('official_full_snapshot') if isinstance(vedastro_evidence, dict) else None
|
||||
if isinstance(official_snapshot, dict):
|
||||
priority.apply_vedastro_source_priority(chart_result, official_snapshot=official_snapshot)
|
||||
return chart_result
|
||||
|
||||
|
||||
@@ -134,6 +146,39 @@ def _build_vedastro_overview_payload_from_chart(chart):
|
||||
'visibility': 'user_visible_overview_only',
|
||||
}
|
||||
|
||||
|
||||
def _build_vedastro_official_full_snapshot_payload_from_chart(chart):
|
||||
modules = chart.get('modules') if isinstance(chart, dict) else {}
|
||||
snapshot = modules.get('vedastro_official_full_snapshot') if isinstance(modules, dict) else {}
|
||||
if not isinstance(snapshot, dict) or not snapshot:
|
||||
return {
|
||||
'status': 'blocked',
|
||||
'available': False,
|
||||
'operation': 'official_full_snapshot',
|
||||
'primary_source': 'vedastro_official',
|
||||
'boundary_note': 'VedAstro official full snapshot is not attached.',
|
||||
}
|
||||
manifest = snapshot.get('request_manifest') if isinstance(snapshot.get('request_manifest'), dict) else {}
|
||||
requests = manifest.get('requests') if isinstance(manifest.get('requests'), list) else []
|
||||
sections = snapshot.get('snapshot_sections') if isinstance(snapshot.get('snapshot_sections'), dict) else {}
|
||||
return {
|
||||
'status': snapshot.get('status') or 'blocked',
|
||||
'available': bool(snapshot.get('available')),
|
||||
'operation': snapshot.get('operation') or 'official_full_snapshot',
|
||||
'primary_source': snapshot.get('primary_source') or 'vedastro_official',
|
||||
'section_statuses': snapshot.get('section_statuses') or {},
|
||||
'snapshot_section_keys': sorted(sections.keys()),
|
||||
'request_section_count': len(requests),
|
||||
'request_sections': [item.get('section') for item in requests if isinstance(item, dict)],
|
||||
'method_catalog': manifest.get('method_catalog') or {},
|
||||
'user_visibility': snapshot.get('user_visibility') or 'backend_raw_evidence_not_direct_user_report',
|
||||
'source_metadata': snapshot.get('source_metadata') or {},
|
||||
'boundary_note': (
|
||||
snapshot.get('reason')
|
||||
or 'VedAstro official full snapshot is the primary raw evidence layer; user reports consume selected slices only.'
|
||||
),
|
||||
}
|
||||
|
||||
SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
|
||||
'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
|
||||
|
||||
@@ -2300,6 +2345,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
shadbala = chart.get('shadbala') or {}
|
||||
functional_layer = self._functional_benefic_malefic_snapshot(planets, ascendant)
|
||||
vedastro_overview = _build_vedastro_overview_payload_from_chart(chart)
|
||||
vedastro_official_full_snapshot = _build_vedastro_official_full_snapshot_payload_from_chart(chart)
|
||||
_attach_guided_topics(chart)
|
||||
modules = chart.get('modules') if isinstance(chart.get('modules'), dict) else {}
|
||||
guided_topics = modules.get('guided_topics') if isinstance(modules.get('guided_topics'), list) else []
|
||||
@@ -2328,10 +2374,13 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
)[:7]
|
||||
core_planets = {
|
||||
planet: {
|
||||
'source': pdata.get('source'),
|
||||
'sign': pdata.get('sign'),
|
||||
'degree': pdata.get('degree'),
|
||||
'degree_in_sign': pdata.get('degree_in_sign'),
|
||||
'house': pdata.get('house'),
|
||||
'lon': pdata.get('lon'),
|
||||
'vargas': pdata.get('vargas'),
|
||||
}
|
||||
for planet, pdata in planets.items()
|
||||
if planet in {'Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'}
|
||||
@@ -2345,6 +2394,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
f'本盘使用 {ayanamsa_display} ayanamsa,节点口径为 {node_mode}。',
|
||||
'不要仅凭单一配置下结论;核心判断至少交叉 D1、D9、Dasha、Shadbala/Ashtakavarga 或 Transit 中的两个证据层。',
|
||||
'必须显式标注置信度和边界:Dasha/PDF 起点差异、Shadbala 外部绝对值 oracle 尚未完成时,不得声称已经完全校准。',
|
||||
'VedAstro 官方全量快照是第一原始证据层;若该层 blocked,必须把本地结果标记为 fallback。',
|
||||
]
|
||||
oracle_progress = {
|
||||
'scope': 'external_oracle_evidence_validation',
|
||||
@@ -2382,6 +2432,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
'shadbala_ranking': top_strength,
|
||||
},
|
||||
'functional_benefic_malefic': functional_layer,
|
||||
'vedastro_official_full_snapshot': vedastro_official_full_snapshot,
|
||||
'vedastro_overview': vedastro_overview,
|
||||
'guided_topics': guided_topics,
|
||||
'capability_evidence_pool': capability_evidence_pool,
|
||||
|
||||
@@ -822,10 +822,14 @@ def _planet_snapshot(planets, planet_name):
|
||||
if not isinstance(pdata, dict):
|
||||
return {}
|
||||
return {
|
||||
'source': pdata.get('source'),
|
||||
'sign': pdata.get('sign'),
|
||||
'sign_cn': pdata.get('sign_cn'),
|
||||
'house': pdata.get('house'),
|
||||
'degree': pdata.get('degree'),
|
||||
'lon': pdata.get('lon'),
|
||||
'degree_in_sign': pdata.get('degree_in_sign'),
|
||||
'vargas': pdata.get('vargas'),
|
||||
'nakshatra': pdata.get('nakshatra'),
|
||||
'nakshatra_pada': pdata.get('nakshatra_pada'),
|
||||
'status': pdata.get('status'),
|
||||
@@ -1146,6 +1150,38 @@ def _build_vedastro_overview_payload(modules):
|
||||
}
|
||||
|
||||
|
||||
def _build_vedastro_official_full_snapshot_payload(modules):
|
||||
snapshot = modules.get('vedastro_official_full_snapshot') if isinstance(modules, dict) else {}
|
||||
if not isinstance(snapshot, dict) or not snapshot:
|
||||
return {
|
||||
'status': 'blocked',
|
||||
'available': False,
|
||||
'operation': 'official_full_snapshot',
|
||||
'primary_source': 'vedastro_official',
|
||||
'boundary_note': 'VedAstro official full snapshot is not attached.',
|
||||
}
|
||||
manifest = snapshot.get('request_manifest') if isinstance(snapshot.get('request_manifest'), dict) else {}
|
||||
requests = manifest.get('requests') if isinstance(manifest.get('requests'), list) else []
|
||||
snapshot_sections = snapshot.get('snapshot_sections') if isinstance(snapshot.get('snapshot_sections'), dict) else {}
|
||||
return {
|
||||
'status': snapshot.get('status') or 'blocked',
|
||||
'available': bool(snapshot.get('available')),
|
||||
'operation': snapshot.get('operation') or 'official_full_snapshot',
|
||||
'primary_source': snapshot.get('primary_source') or 'vedastro_official',
|
||||
'section_statuses': snapshot.get('section_statuses') or {},
|
||||
'snapshot_section_keys': sorted(snapshot_sections.keys()),
|
||||
'request_section_count': len(requests),
|
||||
'request_sections': [item.get('section') for item in requests if isinstance(item, dict)],
|
||||
'method_catalog': manifest.get('method_catalog') or {},
|
||||
'user_visibility': snapshot.get('user_visibility') or 'backend_raw_evidence_not_direct_user_report',
|
||||
'source_metadata': snapshot.get('source_metadata') or {},
|
||||
'boundary_note': (
|
||||
snapshot.get('reason')
|
||||
or 'VedAstro official full snapshot is the primary raw evidence layer; user reports consume selected slices only.'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _build_ai_prompt_pack(report):
|
||||
"""Build a compact, evidence-first prompt pack for downstream AI/RAG reading."""
|
||||
modules = report.get('modules', {}) if isinstance(report, dict) else {}
|
||||
@@ -1167,6 +1203,7 @@ def _build_ai_prompt_pack(report):
|
||||
relationship_narrative = _build_relationship_narrative_payload(modules.get('relationship_strict_evidence'))
|
||||
vimsopaka_semantic_summary = _build_vimsopaka_semantic_summary(modules.get('vimsopaka'))
|
||||
vedastro_overview = _build_vedastro_overview_payload(modules)
|
||||
vedastro_official_full_snapshot = _build_vedastro_official_full_snapshot_payload(modules)
|
||||
guided_topics = modules.get('guided_topics') if isinstance(modules.get('guided_topics'), list) else build_guided_topics(report)
|
||||
capability_evidence_pool = build_capability_evidence_pool_summary()
|
||||
|
||||
@@ -1239,6 +1276,7 @@ def _build_ai_prompt_pack(report):
|
||||
},
|
||||
'oracle_progress': oracle_progress,
|
||||
'functional_benefic_malefic': functional_layer,
|
||||
'vedastro_official_full_snapshot': vedastro_official_full_snapshot,
|
||||
'vedastro_overview': vedastro_overview,
|
||||
'guided_topics': guided_topics,
|
||||
'capability_evidence_pool': capability_evidence_pool,
|
||||
@@ -1256,6 +1294,7 @@ def _build_ai_prompt_pack(report):
|
||||
"输出结构建议:参数声明、核心星盘、关系/事业/财富/健康分主题、当前时机、证据表、风险边界、可行动建议。",
|
||||
"若引用经典法则,请优先检索 retrieval_plan.local_reference_docs;需要外部断语时再做 web/source verification。",
|
||||
"若 evidence_snapshot.vedastro_overview.status 为 ok,请把它作为用户可见外部概览证据明确写出,但不要把 overview-only 结果误当作长周期精扫结论。",
|
||||
"VedAstro 官方全量快照是第一原始证据层;若 evidence_snapshot.vedastro_official_full_snapshot.status 不是 ok/partial,必须说明官方全量资料 blocked,并把本地结果标记为 fallback。",
|
||||
"若 evidence_snapshot.capability_evidence_pool 存在,请把 89 项视为后台备选证据池;不要把所有能力条目平铺成结论,也不要让 audit_only/alias 条目影响占星判断。",
|
||||
]
|
||||
|
||||
@@ -1327,6 +1366,11 @@ def _attach_vedastro_main_entry_overview(report, args):
|
||||
'tz': getattr(args, 'tz', None),
|
||||
'ayanamsa_policy': getattr(args, 'ayanamsa', None) or _current_ayanamsa_name(args),
|
||||
'node_policy': getattr(args, 'node_mode', 'mean'),
|
||||
'reference_date': (
|
||||
getattr(args, 'transit_date', None)
|
||||
or getattr(args, 'today', None)
|
||||
or datetime.now().strftime('%Y-%m-%d')
|
||||
),
|
||||
}
|
||||
|
||||
def _scan_domain(domain: str):
|
||||
@@ -1411,6 +1455,51 @@ def _attach_vedastro_main_entry_overview(report, args):
|
||||
return report
|
||||
|
||||
|
||||
def _attach_vedastro_official_full_snapshot(report, args):
|
||||
if not isinstance(report, dict):
|
||||
return report
|
||||
modules = report.setdefault('modules', {})
|
||||
if not isinstance(modules, dict):
|
||||
return report
|
||||
if modules.get('vedastro_official_full_snapshot'):
|
||||
return report
|
||||
|
||||
try:
|
||||
from vedastro_service_adapter import run_official_full_snapshot_for_case
|
||||
from vedastro_priority import apply_vedastro_source_priority
|
||||
except Exception as exc: # pragma: no cover - import guard
|
||||
report.setdefault('warnings', []).append(f"vedastro-official-full-snapshot-import: {exc}")
|
||||
return report
|
||||
|
||||
case = {
|
||||
'year': getattr(args, 'year', None),
|
||||
'month': getattr(args, 'month', None),
|
||||
'day': getattr(args, 'day', None),
|
||||
'hour': getattr(args, 'hour', None),
|
||||
'minute': getattr(args, 'minute', None),
|
||||
'second': _arg_second(args),
|
||||
'lat': getattr(args, 'lat', None),
|
||||
'lon': getattr(args, 'lon', None),
|
||||
'tz': getattr(args, 'tz', None),
|
||||
'ayanamsa_policy': getattr(args, 'ayanamsa', None) or _current_ayanamsa_name(args),
|
||||
'node_policy': getattr(args, 'node_mode', 'mean'),
|
||||
'reference_date': (
|
||||
getattr(args, 'transit_date', None)
|
||||
or getattr(args, 'today', None)
|
||||
or datetime.now().strftime('%Y-%m-%d')
|
||||
),
|
||||
}
|
||||
modules['vedastro_official_full_snapshot'] = run_official_full_snapshot_for_case(
|
||||
case,
|
||||
case_id='full_reading_official_primary',
|
||||
)
|
||||
apply_vedastro_source_priority(
|
||||
report,
|
||||
official_snapshot=modules['vedastro_official_full_snapshot'],
|
||||
)
|
||||
return report
|
||||
|
||||
|
||||
def _load_relationship_strict_collector():
|
||||
try:
|
||||
from mcp_server import _collect_strict_evidence as collector
|
||||
@@ -5132,6 +5221,11 @@ def cmd_full_reading(args):
|
||||
except Exception as e:
|
||||
report['errors'].append(f"relationship-strict-evidence: {e}")
|
||||
|
||||
try:
|
||||
_attach_vedastro_official_full_snapshot(report, args)
|
||||
except Exception as e:
|
||||
report['warnings'].append(f"vedastro-official-full-snapshot: {e}")
|
||||
|
||||
try:
|
||||
_attach_vedastro_main_entry_overview(report, args)
|
||||
except Exception as e:
|
||||
|
||||
@@ -14,10 +14,15 @@ from typing import Any
|
||||
try:
|
||||
from scripts.vedastro_service_adapter import (
|
||||
VEDASTRO_CALCULATION_COVERAGE,
|
||||
run_official_full_snapshot_for_case,
|
||||
run_range_scan_for_case,
|
||||
)
|
||||
except ModuleNotFoundError: # pragma: no cover - script execution path
|
||||
from vedastro_service_adapter import VEDASTRO_CALCULATION_COVERAGE, run_range_scan_for_case
|
||||
from vedastro_service_adapter import (
|
||||
VEDASTRO_CALCULATION_COVERAGE,
|
||||
run_official_full_snapshot_for_case,
|
||||
run_range_scan_for_case,
|
||||
)
|
||||
|
||||
|
||||
ROUTE_DOMAIN_MAP = {
|
||||
@@ -75,6 +80,7 @@ def orchestrate_vedastro_evidence(
|
||||
domains = ROUTE_DOMAIN_MAP.get(route, ROUTE_DOMAIN_MAP["general"])
|
||||
window_start, window_end = (start_date, end_date) if start_date and end_date else _default_window(reference_date)
|
||||
case = _normalize_case(birth_payload)
|
||||
case["reference_date"] = str(reference_date or window_start)[:10]
|
||||
domain_reports: dict[str, Any] = {}
|
||||
evidence_ledger: list[dict[str, Any]] = []
|
||||
top_events_by_domain: dict[str, Any] = {}
|
||||
@@ -82,6 +88,10 @@ def orchestrate_vedastro_evidence(
|
||||
domain_event_counts: dict[str, int] = {}
|
||||
available = False
|
||||
first_reason = None
|
||||
official_full_snapshot = run_official_full_snapshot_for_case(
|
||||
case,
|
||||
case_id=f"{case_id}_official_full_snapshot",
|
||||
)
|
||||
|
||||
for domain in domains:
|
||||
report = run_range_scan_for_case(
|
||||
@@ -115,11 +125,13 @@ def orchestrate_vedastro_evidence(
|
||||
"top_events_by_domain": top_events_by_domain,
|
||||
"evidence_ledger": evidence_ledger,
|
||||
"reason": None if status == "ok" else first_reason,
|
||||
"official_full_snapshot": official_full_snapshot,
|
||||
"domain_reports": domain_reports,
|
||||
"source_metadata": {
|
||||
"auto_ingested_by": "VedAstroEvidenceOrchestrator",
|
||||
"strategy": "minimal_route_scoped_orchestration",
|
||||
"strategy": "official_full_snapshot_first_then_route_scoped_range_scan",
|
||||
"node_coverage": {
|
||||
"official_full_snapshot_first": True,
|
||||
"strategy": "domain_scoped_range_scan",
|
||||
"official_calculation_coverage": VEDASTRO_CALCULATION_COVERAGE,
|
||||
"selected_domains": domains,
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
"""Shared VedAstro-first source priority helpers.
|
||||
|
||||
This module keeps the user-facing data order identical across the CLI engine,
|
||||
API server, and MCP strict workflow:
|
||||
|
||||
1. VedAstro official full snapshot when it contains an official chart.
|
||||
2. Local modules as supplemental evidence and cross-checks.
|
||||
3. Local chart as fallback only when the official snapshot is blocked.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
|
||||
SOURCE_PRIORITY = [
|
||||
"vedastro_official_snapshot",
|
||||
"local_supplemental_modules",
|
||||
"local_engine_fallback_when_official_blocked",
|
||||
]
|
||||
|
||||
|
||||
def official_chart_available(official_snapshot: dict[str, Any] | None) -> bool:
|
||||
if not isinstance(official_snapshot, dict):
|
||||
return False
|
||||
official_chart = official_snapshot.get("official_chart")
|
||||
if not isinstance(official_chart, dict):
|
||||
return False
|
||||
return (
|
||||
isinstance(official_chart.get("planets"), dict)
|
||||
and bool(official_chart.get("planets"))
|
||||
and isinstance(official_chart.get("ascendant"), dict)
|
||||
and bool(official_chart.get("ascendant"))
|
||||
)
|
||||
|
||||
|
||||
def _local_chart_from(report: dict[str, Any], modules: dict[str, Any]) -> dict[str, Any]:
|
||||
chart = report.get("chart")
|
||||
if isinstance(chart, dict) and chart:
|
||||
return chart
|
||||
chart = modules.get("chart")
|
||||
return chart if isinstance(chart, dict) else {}
|
||||
|
||||
|
||||
def _blocked_reason(official_snapshot: dict[str, Any] | None) -> str:
|
||||
status = "missing"
|
||||
if isinstance(official_snapshot, dict):
|
||||
status = str(official_snapshot.get("status") or "blocked")
|
||||
return f"VedAstro official snapshot blocked: {status}"
|
||||
|
||||
|
||||
def build_source_priority_metadata(
|
||||
official_snapshot: dict[str, Any] | None,
|
||||
*,
|
||||
official_primary: bool,
|
||||
) -> dict[str, Any]:
|
||||
status = official_snapshot.get("status") if isinstance(official_snapshot, dict) else "missing"
|
||||
return {
|
||||
"mode": "vedastro_official_primary" if official_primary else "local_fallback_official_blocked",
|
||||
"priority": list(SOURCE_PRIORITY),
|
||||
"official_snapshot_first": True,
|
||||
"official_snapshot_status": status or "blocked",
|
||||
"local_engine_role": (
|
||||
"supplemental_crosscheck_or_fallback"
|
||||
if official_primary
|
||||
else "fallback_only_because_official_blocked"
|
||||
),
|
||||
"user_visible_policy": (
|
||||
"show_vedastro_verified_when_official_chart_available"
|
||||
if official_primary
|
||||
else "show_local_fallback_with_official_blocked_boundary"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def apply_vedastro_source_priority(
|
||||
report: dict[str, Any],
|
||||
*,
|
||||
official_snapshot: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
if not isinstance(report, dict):
|
||||
return report
|
||||
modules = report.setdefault("modules", {})
|
||||
if not isinstance(modules, dict):
|
||||
modules = {}
|
||||
report["modules"] = modules
|
||||
|
||||
if isinstance(official_snapshot, dict):
|
||||
modules["vedastro_official_full_snapshot"] = official_snapshot
|
||||
|
||||
local_chart = _local_chart_from(report, modules)
|
||||
|
||||
if official_chart_available(official_snapshot):
|
||||
official_chart = official_snapshot.get("official_chart") # type: ignore[union-attr]
|
||||
modules["local_engine_chart_fallback"] = deepcopy(local_chart)
|
||||
public_chart = {
|
||||
**deepcopy(local_chart),
|
||||
"source": "vedastro_official_primary",
|
||||
"primary_source": "vedastro_official",
|
||||
"fallback_source": "local_engine",
|
||||
"local_engine_role": "supplemental_crosscheck_or_fallback",
|
||||
"local_crosscheck_status": "pending",
|
||||
"source_priority": list(SOURCE_PRIORITY),
|
||||
"planets": official_chart.get("planets", {}),
|
||||
"ascendant": official_chart.get("ascendant", {}),
|
||||
"houses": official_chart.get("houses", {}),
|
||||
"birth_info": local_chart.get("birth_info", report.get("birth_info", report.get("birth", {}))),
|
||||
"official_coverage": official_chart.get("coverage", {}),
|
||||
}
|
||||
report["chart"] = public_chart
|
||||
modules["chart"] = public_chart
|
||||
for key in ("source", "primary_source", "fallback_source", "planets", "ascendant", "houses"):
|
||||
if key in public_chart:
|
||||
report[key] = public_chart[key]
|
||||
modules["source_priority"] = build_source_priority_metadata(
|
||||
official_snapshot,
|
||||
official_primary=True,
|
||||
)
|
||||
return report
|
||||
|
||||
if local_chart:
|
||||
fallback_chart = {
|
||||
**deepcopy(local_chart),
|
||||
"source": "local_engine_fallback",
|
||||
"primary_source": "local_engine",
|
||||
"fallback_reason": _blocked_reason(official_snapshot),
|
||||
"local_engine_role": "fallback_only_because_official_blocked",
|
||||
"source_priority": list(SOURCE_PRIORITY),
|
||||
}
|
||||
report["chart"] = fallback_chart
|
||||
modules["chart"] = fallback_chart
|
||||
for key in ("source", "primary_source", "planets", "ascendant", "houses"):
|
||||
if key in fallback_chart:
|
||||
report[key] = fallback_chart[key]
|
||||
|
||||
modules["source_priority"] = build_source_priority_metadata(
|
||||
official_snapshot,
|
||||
official_primary=False,
|
||||
)
|
||||
return report
|
||||
|
||||
|
||||
def official_snapshot_evidence(modules: dict[str, Any]) -> dict[str, Any]:
|
||||
snapshot = modules.get("vedastro_official_full_snapshot") if isinstance(modules, dict) else {}
|
||||
source_priority = modules.get("source_priority") if isinstance(modules, dict) else {}
|
||||
if not isinstance(snapshot, dict) or not snapshot:
|
||||
return {
|
||||
"level": "blocked",
|
||||
"source": "vedastro_official",
|
||||
"status": "missing",
|
||||
"operation": "official_full_snapshot",
|
||||
"source_priority": source_priority if isinstance(source_priority, dict) else {},
|
||||
"reason": "VedAstro official full snapshot is not attached.",
|
||||
}
|
||||
level = "primary" if official_chart_available(snapshot) else "blocked"
|
||||
return {
|
||||
"level": level,
|
||||
"source": "vedastro_official",
|
||||
"status": snapshot.get("status") or "blocked",
|
||||
"available": bool(snapshot.get("available")),
|
||||
"operation": snapshot.get("operation") or "official_full_snapshot",
|
||||
"source_priority": source_priority if isinstance(source_priority, dict) else {},
|
||||
"section_statuses": snapshot.get("section_statuses") or {},
|
||||
"chart_available": official_chart_available(snapshot),
|
||||
"reason": snapshot.get("reason"),
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import json
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib import request, error
|
||||
@@ -86,6 +87,62 @@ SUPPORTED_EXTERNAL_TECHNIQUE_DOMAINS = {"marriage", "wealth", "career", "general
|
||||
OFFICIAL_SEARCH_EVENTS_ENDPOINT_PATH = "/Calculate/SearchEvents"
|
||||
OFFICIAL_SEARCH_EVENTS_METHOD = "POST"
|
||||
OFFICIAL_SEARCH_EVENTS_PROFILE_VERSION = "official_builder_search_events_v1"
|
||||
OFFICIAL_FULL_SNAPSHOT_PROFILE_VERSION = "official_full_snapshot_v1"
|
||||
OFFICIAL_METHOD_CATALOG_URL = "https://vedastro.org/Complete-List-VedAstro-API-Methods-Calculators.html"
|
||||
OFFICIAL_FULL_SNAPSHOT_METHODS = [
|
||||
{
|
||||
"section": "chart_core",
|
||||
"endpoint_path": "/Calculate/AllPlanetData",
|
||||
"calculator_name": "AllPlanetData",
|
||||
"role": "core_chart_raw_evidence",
|
||||
"description": "Core planet, ascendant, house, nakshatra, ayanamsa and node-mode evidence when supported by the official service.",
|
||||
"fanout": "planetName",
|
||||
},
|
||||
{
|
||||
"section": "house_core",
|
||||
"endpoint_path": "/Calculate/AllHouseData",
|
||||
"calculator_name": "AllHouseData",
|
||||
"role": "core_house_raw_evidence",
|
||||
"description": "Official house data snapshot when supported by the official service.",
|
||||
"fanout": "houseName",
|
||||
},
|
||||
{
|
||||
"section": "dasha_all",
|
||||
"endpoint_path": "/Calculate/DasaAtRange",
|
||||
"calculator_name": "DasaAtRange",
|
||||
"role": "all_dasha_raw_evidence",
|
||||
"description": "Official dasha timeline snapshot where available.",
|
||||
},
|
||||
{
|
||||
"section": "events_overview",
|
||||
"endpoint_path": OFFICIAL_SEARCH_EVENTS_ENDPOINT_PATH,
|
||||
"calculator_name": "SearchEvents",
|
||||
"role": "life_event_raw_evidence",
|
||||
"description": "Official event radar using SearchEvents for career, marriage and wealth tags.",
|
||||
},
|
||||
]
|
||||
OFFICIAL_FULL_SNAPSHOT_BACKLOG_SECTIONS = [
|
||||
{
|
||||
"section": "varga_all",
|
||||
"role": "all_varga_raw_evidence",
|
||||
"status": "catalog_pending",
|
||||
"description": "Awaiting official method mapping for all divisional charts; local varga remains fallback until mapped.",
|
||||
},
|
||||
{
|
||||
"section": "shadbala",
|
||||
"role": "strength_raw_evidence",
|
||||
"status": "catalog_pending",
|
||||
"description": "Awaiting official method mapping for Shadbala; local Shadbala remains fallback until mapped.",
|
||||
},
|
||||
{
|
||||
"section": "ashtakavarga",
|
||||
"role": "ashtakavarga_raw_evidence",
|
||||
"status": "catalog_pending",
|
||||
"description": "Awaiting official method mapping for Ashtakavarga; local Ashtakavarga remains fallback until mapped.",
|
||||
},
|
||||
]
|
||||
OFFICIAL_SNAPSHOT_PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu", "Ascendant"]
|
||||
OFFICIAL_SNAPSHOT_HOUSES = [f"House{i}" for i in range(1, 13)]
|
||||
OFFICIAL_RANGE_SCAN_EVENT_TAGS = {
|
||||
"marriage": ["Marriage", "Personal", "General"],
|
||||
"wealth": ["LendingMoney", "BorrowingMoney", "BuyingSelling", "General"],
|
||||
@@ -418,7 +475,40 @@ def schema() -> dict[str, Any]:
|
||||
"AtTime | StartTime + EndTime + PrecisionHours",
|
||||
],
|
||||
},
|
||||
"official_full_snapshot_request_contract": {
|
||||
"profile_version": OFFICIAL_FULL_SNAPSHOT_PROFILE_VERSION,
|
||||
"primary_source": "vedastro_official",
|
||||
"method_catalog_url": OFFICIAL_METHOD_CATALOG_URL,
|
||||
"strategy": "fetch_official_raw_sections_first_then_local_crosscheck",
|
||||
"common_body_fields": [
|
||||
"BirthTime",
|
||||
"Ayanamsa",
|
||||
"NodeMode",
|
||||
"CalculationPreferences",
|
||||
],
|
||||
"request_sections": [
|
||||
{
|
||||
"section": item["section"],
|
||||
"endpoint_path": item["endpoint_path"],
|
||||
"calculator_name": item.get("calculator_name"),
|
||||
"role": item["role"],
|
||||
}
|
||||
for item in OFFICIAL_FULL_SNAPSHOT_METHODS
|
||||
],
|
||||
"backlog_sections": OFFICIAL_FULL_SNAPSHOT_BACKLOG_SECTIONS,
|
||||
"user_visibility": "backend_raw_evidence_not_direct_user_report",
|
||||
},
|
||||
"vedastro_calculation_coverage": VEDASTRO_CALCULATION_COVERAGE,
|
||||
"official_full_snapshot_response_contract": [
|
||||
"backend",
|
||||
"available",
|
||||
"status",
|
||||
"operation",
|
||||
"primary_source",
|
||||
"snapshot_sections",
|
||||
"request_manifest",
|
||||
"source_metadata",
|
||||
],
|
||||
"external_technique_request_contract": [
|
||||
"operation",
|
||||
"role",
|
||||
@@ -515,9 +605,20 @@ def _format_std_time(date_text: str, hour: Any, minute: Any, tz: Any) -> str:
|
||||
return f"{hour_int:02d}:{minute_int:02d} {day}/{month}/{year} {tz}"
|
||||
|
||||
|
||||
def _time_json_from_case(case: dict[str, Any], date_text: str) -> dict[str, Any]:
|
||||
def _time_json_from_case(
|
||||
case: dict[str, Any],
|
||||
date_text: str,
|
||||
*,
|
||||
hour: Any | None = None,
|
||||
minute: Any | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"StdTime": _format_std_time(date_text, case.get("hour", 0), case.get("minute", 0), case.get("tz", "+00:00")),
|
||||
"StdTime": _format_std_time(
|
||||
date_text,
|
||||
case.get("hour", 0) if hour is None else hour,
|
||||
case.get("minute", 0) if minute is None else minute,
|
||||
case.get("tz", "+00:00"),
|
||||
),
|
||||
"Location": {
|
||||
"Name": case.get("case_id") or "UserLocation",
|
||||
"Latitude": case.get("lat"),
|
||||
@@ -590,6 +691,108 @@ def _build_live_sampling_search_events_profile(request_preview: dict[str, Any])
|
||||
}
|
||||
|
||||
|
||||
def _official_common_body(case: dict[str, Any]) -> dict[str, Any]:
|
||||
normalized = dict(case)
|
||||
normalized["tz"] = _normalize_tz(normalized)
|
||||
return {
|
||||
"time": _time_json_from_case(
|
||||
normalized,
|
||||
f"{int(normalized['year']):04d}-{int(normalized['month']):02d}-{int(normalized['day']):02d}",
|
||||
),
|
||||
"Ayanamsa": str(normalized.get("ayanamsa_policy") or "lahiri"),
|
||||
"NodeMode": str(normalized.get("node_policy") or "mean"),
|
||||
"CalculationPreferences": {
|
||||
"scope": "all_supported_official_calculations",
|
||||
"user_visibility": "backend_raw_evidence_not_direct_user_report",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _official_snapshot_reference_date(case: dict[str, Any]) -> str:
|
||||
for key in ("reference_date", "today", "transit_date", "current_date"):
|
||||
value = case.get(key)
|
||||
if not value:
|
||||
continue
|
||||
raw = str(value)[:10]
|
||||
try:
|
||||
datetime.strptime(raw, "%Y-%m-%d")
|
||||
return raw
|
||||
except ValueError:
|
||||
continue
|
||||
return datetime.utcnow().strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def _official_dasha_range_body(case: dict[str, Any], common_body: dict[str, Any]) -> dict[str, Any]:
|
||||
normalized = dict(case)
|
||||
normalized["tz"] = _normalize_tz(normalized)
|
||||
reference = datetime.strptime(_official_snapshot_reference_date(normalized), "%Y-%m-%d").date()
|
||||
start_date = reference.replace(month=1, day=1)
|
||||
end_date = reference.replace(month=12, day=31)
|
||||
return {
|
||||
"birthTime": common_body["time"],
|
||||
"startTime": _time_json_from_case(normalized, start_date.isoformat(), hour=0, minute=0),
|
||||
"endTime": _time_json_from_case(normalized, end_date.isoformat(), hour=23, minute=59),
|
||||
"levels": int(normalized.get("dasha_levels") or 3),
|
||||
"precisionHours": int(normalized.get("dasha_precision_hours") or 100),
|
||||
"Ayanamsa": common_body["Ayanamsa"],
|
||||
}
|
||||
|
||||
|
||||
def _official_full_snapshot_manifest(case: dict[str, Any], case_id: str = "user_chart") -> dict[str, Any]:
|
||||
common_body = _official_common_body(case)
|
||||
reference_date = _official_snapshot_reference_date(case)
|
||||
headers: dict[str, str] = {"Content-Type": "application/json"}
|
||||
api_key = os.environ.get("VEDASTRO_API_KEY", "").strip()
|
||||
if api_key:
|
||||
headers["x-api-key"] = api_key
|
||||
requests = []
|
||||
for item in OFFICIAL_FULL_SNAPSHOT_METHODS:
|
||||
body = dict(common_body)
|
||||
if item["section"] == "events_overview":
|
||||
body = {
|
||||
"BirthTime": common_body["time"],
|
||||
"Ayanamsa": common_body["Ayanamsa"],
|
||||
"EventTagList": sorted({tag for tags in OFFICIAL_RANGE_SCAN_EVENT_TAGS.values() for tag in tags}),
|
||||
"AtTime": common_body["time"],
|
||||
}
|
||||
if item["section"] == "dasha_all":
|
||||
body = _official_dasha_range_body(case, common_body)
|
||||
fanout_values = []
|
||||
if item.get("fanout") == "planetName":
|
||||
fanout_values = OFFICIAL_SNAPSHOT_PLANETS
|
||||
elif item.get("fanout") == "houseName":
|
||||
fanout_values = OFFICIAL_SNAPSHOT_HOUSES
|
||||
requests.append(
|
||||
{
|
||||
"section": item["section"],
|
||||
"role": item["role"],
|
||||
"calculator_name": item.get("calculator_name"),
|
||||
"endpoint_path": item["endpoint_path"],
|
||||
"method": "POST",
|
||||
"headers": headers,
|
||||
"body": body,
|
||||
"fanout_parameter": item.get("fanout"),
|
||||
"fanout_values": fanout_values,
|
||||
"description": item["description"],
|
||||
}
|
||||
)
|
||||
return {
|
||||
"operation": "official_full_snapshot",
|
||||
"profile_version": OFFICIAL_FULL_SNAPSHOT_PROFILE_VERSION,
|
||||
"source_role": "primary_official_raw_evidence",
|
||||
"primary_source": "vedastro_official",
|
||||
"case_id": case_id,
|
||||
"reference_date": reference_date,
|
||||
"method_catalog": {
|
||||
"url": OFFICIAL_METHOD_CATALOG_URL,
|
||||
"declared_coverage": VEDASTRO_CALCULATION_COVERAGE,
|
||||
"catalog_role": "all_supported_method_reference_not_user_visible_output",
|
||||
"backlog_sections": OFFICIAL_FULL_SNAPSHOT_BACKLOG_SECTIONS,
|
||||
},
|
||||
"requests": requests,
|
||||
}
|
||||
|
||||
|
||||
def _external_technique_preview(
|
||||
case: dict[str, Any],
|
||||
domain: str,
|
||||
@@ -1114,6 +1317,385 @@ def run_case(case_id: str) -> dict[str, Any]:
|
||||
return _normalize_success(payload, endpoint, request_preview)
|
||||
|
||||
|
||||
def _official_full_snapshot_metadata(endpoint: str | None, manifest: dict[str, Any]) -> dict[str, Any]:
|
||||
metadata = {
|
||||
"transport": "http_json_service_boundary",
|
||||
"operation": "official_full_snapshot",
|
||||
"primary_source": "vedastro_official",
|
||||
"provenance_mode": "vedastro_official_primary_candidate",
|
||||
"timeout_seconds": _timeout_seconds(),
|
||||
"retry_policy": {**RETRY_POLICY, "backoff_seconds": _backoff_seconds()},
|
||||
"network_execution_env": ALLOW_NETWORK_ENV,
|
||||
"method_catalog_url": OFFICIAL_METHOD_CATALOG_URL,
|
||||
"reference_date": manifest.get("reference_date"),
|
||||
"request_hash": _hash_payload(manifest),
|
||||
}
|
||||
if endpoint:
|
||||
metadata["endpoint"] = endpoint
|
||||
metadata["endpoint_host"] = _endpoint_host(endpoint)
|
||||
return metadata
|
||||
|
||||
|
||||
def _payload_status(payload: dict[str, Any]) -> str:
|
||||
if not isinstance(payload, dict):
|
||||
return "invalid"
|
||||
if str(payload.get("Status") or "").lower() == "fail":
|
||||
failure_text = json.dumps(payload.get("Payload"), ensure_ascii=False).lower()
|
||||
if "rate limit" in failure_text or "calls/minute" in failure_text or "too many requests" in failure_text:
|
||||
return "rate_limited"
|
||||
return "ok" if payload.get("Status") == "Pass" else "fail"
|
||||
|
||||
|
||||
def _aggregate_section_status(statuses: list[str]) -> str:
|
||||
if statuses and all(status == "ok" for status in statuses):
|
||||
return "ok"
|
||||
if any(status == "rate_limited" for status in statuses):
|
||||
return "rate_limited"
|
||||
return "partial"
|
||||
|
||||
|
||||
def _degrees_from_sign_payload(value: Any) -> float | None:
|
||||
if not isinstance(value, dict):
|
||||
return None
|
||||
degrees = value.get("DegreesIn") if isinstance(value.get("DegreesIn"), dict) else {}
|
||||
raw = degrees.get("TotalDegrees")
|
||||
try:
|
||||
return float(raw)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _sign_position(value: Any) -> dict[str, Any] | None:
|
||||
if not isinstance(value, dict):
|
||||
return None
|
||||
sign = value.get("Name")
|
||||
degree = _degrees_from_sign_payload(value)
|
||||
if not sign:
|
||||
return None
|
||||
return {
|
||||
"sign": sign,
|
||||
"degree_in_sign": degree,
|
||||
}
|
||||
|
||||
|
||||
def _extract_all_planet_data(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
if not isinstance(payload, dict):
|
||||
return {}
|
||||
body = payload.get("Payload") if isinstance(payload.get("Payload"), dict) else {}
|
||||
data = body.get("AllPlanetData") if isinstance(body.get("AllPlanetData"), dict) else {}
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def _extract_all_house_data(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
if not isinstance(payload, dict):
|
||||
return {}
|
||||
body = payload.get("Payload") if isinstance(payload.get("Payload"), dict) else {}
|
||||
data = body.get("AllHouseData") if isinstance(body.get("AllHouseData"), dict) else {}
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def _official_planet_snapshot(planet_name: str, data: dict[str, Any]) -> dict[str, Any]:
|
||||
d1 = _sign_position(data.get("PlanetRasiD1Sign")) or {}
|
||||
raw_lon = None
|
||||
nirayana = data.get("PlanetNirayanaLongitude")
|
||||
if isinstance(nirayana, dict):
|
||||
try:
|
||||
raw_lon = float(nirayana.get("TotalDegrees"))
|
||||
except (TypeError, ValueError):
|
||||
raw_lon = None
|
||||
house_text = data.get("HousePlanetOccupiesBasedOnSign") or data.get("HousePlanetOccupiesBasedOnLongitudes")
|
||||
house = None
|
||||
if isinstance(house_text, str) and house_text.lower().startswith("house"):
|
||||
try:
|
||||
house = int("".join(ch for ch in house_text if ch.isdigit()))
|
||||
except ValueError:
|
||||
house = None
|
||||
return {
|
||||
"source": "vedastro_official",
|
||||
"name": planet_name,
|
||||
"sign": d1.get("sign"),
|
||||
"degree_in_sign": d1.get("degree_in_sign"),
|
||||
"degree": raw_lon,
|
||||
"lon": raw_lon,
|
||||
"house": house,
|
||||
"vargas": {
|
||||
"D1": d1,
|
||||
"D2": _sign_position(data.get("PlanetHoraD2Signs")),
|
||||
"D3": _sign_position(data.get("PlanetDrekkanaD3Sign")),
|
||||
"D4": _sign_position(data.get("PlanetChaturthamshaD4Sign")),
|
||||
"D7": _sign_position(data.get("PlanetSaptamshaD7Sign")),
|
||||
"D9": _sign_position(data.get("PlanetNavamshaD9Sign")),
|
||||
"D10": _sign_position(data.get("PlanetDashamamshaD10Sign")),
|
||||
"D12": _sign_position(data.get("PlanetDwadashamshaD12Sign")),
|
||||
"D16": _sign_position(data.get("PlanetShodashamshaD16Sign")),
|
||||
"D20": _sign_position(data.get("PlanetVimshamshaD20Sign")),
|
||||
"D24": _sign_position(data.get("PlanetChaturvimshamshaD24Sign")),
|
||||
"D27": _sign_position(data.get("PlanetBhamshaD27Sign")),
|
||||
"D30": _sign_position(data.get("PlanetTrimshamshaD30Sign")),
|
||||
"D40": _sign_position(data.get("PlanetKhavedamshaD40Sign")),
|
||||
"D45": _sign_position(data.get("PlanetAkshavedamshaD45Sign")),
|
||||
"D60": _sign_position(data.get("PlanetShashtyamshaD60Sign")),
|
||||
},
|
||||
"nakshatra": data.get("PlanetConstellation"),
|
||||
"raw_source_keys": sorted(data.keys()),
|
||||
}
|
||||
|
||||
|
||||
def _official_house_snapshot(house_name: str, data: dict[str, Any]) -> dict[str, Any]:
|
||||
d1 = _sign_position(data.get("HouseRasiD1Sign") or data.get("HouseBhavaChalitSign")) or {}
|
||||
return {
|
||||
"source": "vedastro_official",
|
||||
"name": house_name,
|
||||
"sign": d1.get("sign"),
|
||||
"degree_in_sign": d1.get("degree_in_sign"),
|
||||
"vargas": {
|
||||
"D1": d1,
|
||||
"D2": _sign_position(data.get("HouseHoraD2Sign") or data.get("HouseHoraD2Signs")),
|
||||
"D3": _sign_position(data.get("HouseDrekkanaD3Sign")),
|
||||
"D4": _sign_position(data.get("HouseChaturthamshaD4Sign")),
|
||||
"D7": _sign_position(data.get("HouseSaptamshaD7Sign")),
|
||||
"D9": _sign_position(data.get("HouseNavamshaD9Sign") or data.get("HouseNavamsaD9Sign")),
|
||||
"D10": _sign_position(data.get("HouseDashamamshaD10Sign")),
|
||||
"D12": _sign_position(data.get("HouseDwadashamshaD12Sign")),
|
||||
"D16": _sign_position(data.get("HouseShodashamshaD16Sign")),
|
||||
"D20": _sign_position(data.get("HouseVimshamshaD20Sign")),
|
||||
"D24": _sign_position(data.get("HouseChaturvimshamshaD24Sign")),
|
||||
"D27": _sign_position(data.get("HouseBhamshaD27Sign")),
|
||||
"D30": _sign_position(data.get("HouseTrimshamshaD30Sign")),
|
||||
"D40": _sign_position(data.get("HouseKhavedamshaD40Sign")),
|
||||
"D45": _sign_position(data.get("HouseAkshavedamshaD45Sign")),
|
||||
"D60": _sign_position(data.get("HouseShashtyamshaD60Sign")),
|
||||
},
|
||||
"nakshatra": data.get("HouseConstellation"),
|
||||
"raw_source_keys": sorted(data.keys()),
|
||||
}
|
||||
|
||||
|
||||
def _build_official_chart_from_snapshot(sections: dict[str, Any]) -> dict[str, Any]:
|
||||
chart_core = sections.get("chart_core") if isinstance(sections.get("chart_core"), dict) else {}
|
||||
house_core = sections.get("house_core") if isinstance(sections.get("house_core"), dict) else {}
|
||||
planets: dict[str, Any] = {}
|
||||
for planet_name, payload in chart_core.items():
|
||||
data = _extract_all_planet_data(payload)
|
||||
if data:
|
||||
planets[planet_name] = _official_planet_snapshot(planet_name, data)
|
||||
houses: dict[str, Any] = {}
|
||||
for house_name, payload in house_core.items():
|
||||
data = _extract_all_house_data(payload)
|
||||
if data:
|
||||
houses[house_name] = _official_house_snapshot(house_name, data)
|
||||
ascendant = houses.get("House1") or {}
|
||||
return {
|
||||
"source": "vedastro_official",
|
||||
"primary_source": "vedastro_official",
|
||||
"planets": planets,
|
||||
"houses": houses,
|
||||
"ascendant": ascendant,
|
||||
"coverage": {
|
||||
"planet_count": len(planets),
|
||||
"house_count": len(houses),
|
||||
"varga_keys": ["D1", "D2", "D3", "D4", "D7", "D9", "D10", "D12", "D16", "D20", "D24", "D27", "D30", "D40", "D45", "D60"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _post_official_snapshot_section(endpoint: str, request_item: dict[str, Any]) -> tuple[dict[str, Any], int, list[int]]:
|
||||
body = dict(request_item["body"])
|
||||
fanout_parameter = request_item.get("fanout_parameter")
|
||||
fanout_value = request_item.get("fanout_value")
|
||||
if fanout_parameter and fanout_value is not None:
|
||||
if fanout_parameter == "planetName":
|
||||
body[fanout_parameter] = {"Name": str(fanout_value)}
|
||||
else:
|
||||
body[fanout_parameter] = str(fanout_value)
|
||||
section_preview = {
|
||||
"operation": "official_full_snapshot",
|
||||
"section": request_item["section"],
|
||||
"official_request_profile": {
|
||||
"profile_version": OFFICIAL_FULL_SNAPSHOT_PROFILE_VERSION,
|
||||
"endpoint_path": request_item["endpoint_path"],
|
||||
"method": request_item["method"],
|
||||
"headers": request_item["headers"],
|
||||
"body": body,
|
||||
},
|
||||
}
|
||||
return _post_json_with_retry(endpoint, section_preview)
|
||||
|
||||
|
||||
def _normalize_official_full_snapshot_success(
|
||||
endpoint: str,
|
||||
manifest: dict[str, Any],
|
||||
sections: dict[str, Any],
|
||||
section_statuses: dict[str, str],
|
||||
attempt_count: int,
|
||||
retry_error_codes: list[int],
|
||||
) -> dict[str, Any]:
|
||||
primary_sections = [item["section"] for item in manifest["requests"]]
|
||||
ok_count = sum(1 for section in primary_sections if section_statuses.get(section) == "ok")
|
||||
status = "ok" if ok_count == len(primary_sections) else "partial"
|
||||
rate_limited_sections = [
|
||||
section
|
||||
for section in primary_sections
|
||||
if section_statuses.get(section) == "rate_limited"
|
||||
]
|
||||
metadata = {
|
||||
**_official_full_snapshot_metadata(endpoint, manifest),
|
||||
"called_at": _utc_timestamp(),
|
||||
"section_statuses": section_statuses,
|
||||
"section_count": len(primary_sections),
|
||||
"section_ok_count": ok_count,
|
||||
"rate_limited_sections": rate_limited_sections,
|
||||
"attempt_count": attempt_count,
|
||||
"retry_error_codes": retry_error_codes,
|
||||
"response_hash": _hash_payload({"sections": sections, "section_statuses": section_statuses}),
|
||||
}
|
||||
if rate_limited_sections:
|
||||
metadata["production_hint"] = "configure_vedastro_api_key_or_self_host_official_api"
|
||||
result = {
|
||||
"backend": "vedastro_service_adapter_candidate",
|
||||
"available": ok_count > 0,
|
||||
"status": status,
|
||||
"operation": "official_full_snapshot",
|
||||
"primary_source": "vedastro_official",
|
||||
"snapshot_sections": sections,
|
||||
"official_chart": _build_official_chart_from_snapshot(sections),
|
||||
"section_statuses": section_statuses,
|
||||
"request_manifest": manifest,
|
||||
"user_visibility": "backend_raw_evidence_not_direct_user_report",
|
||||
"source_metadata": metadata,
|
||||
}
|
||||
result["source_metadata"]["artifact_path"] = _write_artifact(result)
|
||||
return result
|
||||
|
||||
|
||||
def _run_official_full_snapshot_case(case: dict[str, Any], case_id: str = "user_chart") -> dict[str, Any]:
|
||||
user_case = {
|
||||
"case_id": case_id,
|
||||
"year": case.get("year"),
|
||||
"month": case.get("month"),
|
||||
"day": case.get("day"),
|
||||
"hour": case.get("hour"),
|
||||
"minute": case.get("minute"),
|
||||
"second": case.get("second", 0),
|
||||
"lat": case.get("lat"),
|
||||
"lon": case.get("lon"),
|
||||
"tz": case.get("tz"),
|
||||
"ayanamsa_policy": case.get("ayanamsa_policy") or case.get("ayanamsa") or "lahiri",
|
||||
"node_policy": case.get("node_policy") or case.get("node_mode") or "mean",
|
||||
"reference_date": case.get("reference_date") or case.get("today") or case.get("transit_date") or case.get("current_date"),
|
||||
"dasha_levels": case.get("dasha_levels"),
|
||||
"dasha_precision_hours": case.get("dasha_precision_hours"),
|
||||
}
|
||||
manifest = _official_full_snapshot_manifest(user_case, case_id)
|
||||
endpoint = os.environ.get("VEDASTRO_API_ENDPOINT", "").strip()
|
||||
if not endpoint:
|
||||
return {
|
||||
"backend": "vedastro_service_adapter_candidate",
|
||||
"available": False,
|
||||
"status": "service_endpoint_not_configured",
|
||||
"operation": "official_full_snapshot",
|
||||
"primary_source": "vedastro_official",
|
||||
"reason": "VEDASTRO_API_ENDPOINT is not configured; official full snapshot stops before network access.",
|
||||
"snapshot_sections": {},
|
||||
"request_manifest": manifest,
|
||||
"user_visibility": "backend_raw_evidence_not_direct_user_report",
|
||||
"source_metadata": _official_full_snapshot_metadata(None, manifest),
|
||||
}
|
||||
|
||||
if os.environ.get(ALLOW_NETWORK_ENV, "").strip().lower() not in {"1", "true", "yes"}:
|
||||
return {
|
||||
"backend": "vedastro_service_adapter_candidate",
|
||||
"available": False,
|
||||
"status": "network_execution_disabled",
|
||||
"operation": "official_full_snapshot",
|
||||
"primary_source": "vedastro_official",
|
||||
"reason": f"{ALLOW_NETWORK_ENV} is not enabled; official full snapshot stops after building request manifest.",
|
||||
"snapshot_sections": {},
|
||||
"request_manifest": manifest,
|
||||
"user_visibility": "backend_raw_evidence_not_direct_user_report",
|
||||
"source_metadata": _official_full_snapshot_metadata(endpoint, manifest),
|
||||
}
|
||||
|
||||
sections: dict[str, Any] = {}
|
||||
section_statuses: dict[str, str] = {}
|
||||
attempt_count = 0
|
||||
retry_error_codes: list[int] = []
|
||||
for request_item in manifest["requests"]:
|
||||
section = request_item["section"]
|
||||
fanout_values = request_item.get("fanout_values") if isinstance(request_item.get("fanout_values"), list) else []
|
||||
if fanout_values:
|
||||
section_payloads: dict[str, Any] = {}
|
||||
fanout_statuses: dict[str, str] = {}
|
||||
for value in fanout_values:
|
||||
fanout_request = {**request_item, "fanout_value": value}
|
||||
try:
|
||||
payload, attempts, retries = _post_official_snapshot_section(endpoint, fanout_request)
|
||||
section_payloads[str(value)] = payload
|
||||
fanout_statuses[str(value)] = _payload_status(payload)
|
||||
attempt_count += attempts
|
||||
retry_error_codes.extend(retries)
|
||||
except error.HTTPError as exc:
|
||||
fanout_statuses[str(value)] = f"http_error:{exc.code}"
|
||||
except (error.URLError, http.client.RemoteDisconnected) as exc:
|
||||
fanout_statuses[str(value)] = f"network_error:{getattr(exc, 'reason', str(exc))}"
|
||||
except (TimeoutError, socket.timeout):
|
||||
fanout_statuses[str(value)] = "timeout"
|
||||
except json.JSONDecodeError:
|
||||
fanout_statuses[str(value)] = "invalid_json"
|
||||
sections[section] = section_payloads
|
||||
section_statuses[section] = _aggregate_section_status(list(fanout_statuses.values()))
|
||||
section_statuses[f"{section}_fanout"] = fanout_statuses
|
||||
continue
|
||||
|
||||
try:
|
||||
payload, attempts, retries = _post_official_snapshot_section(endpoint, request_item)
|
||||
sections[section] = payload
|
||||
section_statuses[section] = _payload_status(payload)
|
||||
attempt_count += attempts
|
||||
retry_error_codes.extend(retries)
|
||||
except error.HTTPError as exc:
|
||||
section_statuses[section] = f"http_error:{exc.code}"
|
||||
except (error.URLError, http.client.RemoteDisconnected) as exc:
|
||||
section_statuses[section] = f"network_error:{getattr(exc, 'reason', str(exc))}"
|
||||
except (TimeoutError, socket.timeout):
|
||||
section_statuses[section] = "timeout"
|
||||
except json.JSONDecodeError:
|
||||
section_statuses[section] = "invalid_json"
|
||||
|
||||
return _normalize_official_full_snapshot_success(
|
||||
endpoint,
|
||||
manifest,
|
||||
sections,
|
||||
section_statuses,
|
||||
attempt_count or 1,
|
||||
retry_error_codes,
|
||||
)
|
||||
|
||||
|
||||
def run_official_full_snapshot(case_id: str, reference_date: str | None = None) -> dict[str, Any]:
|
||||
if case_id not in PARITY_CASES:
|
||||
return {
|
||||
"backend": "vedastro_service_adapter_candidate",
|
||||
"available": False,
|
||||
"status": "unknown_case_id",
|
||||
"operation": "official_full_snapshot",
|
||||
"primary_source": "vedastro_official",
|
||||
"reason": f"Unknown parity case: {case_id}",
|
||||
}
|
||||
case = dict(PARITY_CASES[case_id])
|
||||
if reference_date:
|
||||
case["reference_date"] = reference_date
|
||||
return _run_official_full_snapshot_case(case, case_id=case_id)
|
||||
|
||||
|
||||
def run_official_full_snapshot_for_case(
|
||||
case: dict[str, Any],
|
||||
*,
|
||||
case_id: str = "user_chart",
|
||||
) -> dict[str, Any]:
|
||||
return _run_official_full_snapshot_case(case, case_id=case_id)
|
||||
|
||||
|
||||
def _run_range_scan_case(case: dict[str, Any], domain: str, start_date: str, end_date: str) -> dict[str, Any]:
|
||||
if domain not in SUPPORTED_RANGE_SCAN_DOMAINS:
|
||||
return {
|
||||
@@ -1330,9 +1912,11 @@ def main() -> int:
|
||||
parser.add_argument("--print-schema", action="store_true")
|
||||
parser.add_argument("--case", default="beijing_first_use_demo")
|
||||
parser.add_argument("--range-scan", action="store_true")
|
||||
parser.add_argument("--official-full-snapshot", action="store_true")
|
||||
parser.add_argument("--domain", choices=sorted(SUPPORTED_EXTERNAL_TECHNIQUE_DOMAINS), default="marriage")
|
||||
parser.add_argument("--start-date", default="2026-01-01")
|
||||
parser.add_argument("--end-date", default="2031-01-01")
|
||||
parser.add_argument("--reference-date", default=None)
|
||||
parser.add_argument("--external-technique", action="store_true")
|
||||
parser.add_argument("--method", default="")
|
||||
parser.add_argument("--api-endpoint", default="")
|
||||
@@ -1340,6 +1924,8 @@ def main() -> int:
|
||||
|
||||
if args.print_schema:
|
||||
result = schema()
|
||||
elif args.official_full_snapshot:
|
||||
result = run_official_full_snapshot(args.case, reference_date=args.reference_date)
|
||||
elif args.external_technique:
|
||||
result = run_external_technique(args.case, args.domain, args.method, args.api_endpoint)
|
||||
elif args.range_scan:
|
||||
|
||||
Reference in New Issue
Block a user