render_test_charts.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. #!/usr/bin/env python3
  2. """Test script for render_natal_chart_by_id via MCP client.
  3. Calls the astro-mcp server via MCP SSE to render test charts for
  4. celebrities stored in the database.
  5. Usage:
  6. python3 render_test_charts.py [--format svg|png|jpg] [--size N]
  7. Requires:
  8. - astro-mcp server running (e.g., on thinkcenter-2:7016)
  9. """
  10. from __future__ import annotations
  11. import argparse
  12. import asyncio
  13. import json
  14. import os
  15. from datetime import timedelta
  16. from typing import Any
  17. from mcp import ClientSession
  18. from mcp.client.sse import sse_client
  19. ASTRO_MCP_URL = os.environ.get(
  20. "ASTRO_MCP_URL", "http://192.168.0.249:7016/mcp/sse"
  21. )
  22. # Person nicknames in the database (as used in test_live_charts.py)
  23. PERSON_IDS = [
  24. "trump", # Donald Trump
  25. "chaka", # Chaka Khan
  26. "lucky"
  27. ]
  28. def _payload_from_result(result: Any) -> dict[str, Any]:
  29. """Extract a dict payload from an MCP CallToolResult."""
  30. payload = getattr(result, "structuredContent", None)
  31. if isinstance(payload, dict):
  32. return payload
  33. content_items = getattr(result, "content", []) or []
  34. for item in content_items:
  35. text = getattr(item, "text", None)
  36. if not isinstance(text, str) or not text.strip():
  37. continue
  38. try:
  39. decoded = json.loads(text)
  40. except Exception:
  41. continue
  42. if isinstance(decoded, dict):
  43. return decoded
  44. error_texts = []
  45. for item in content_items:
  46. text = getattr(item, "text", None)
  47. if isinstance(text, str) and text.strip():
  48. error_texts.append(text.strip())
  49. if error_texts:
  50. return {"error": error_texts[0]}
  51. return {}
  52. async def call_astro_tool(
  53. session: ClientSession, tool_name: str, arguments: dict[str, Any]
  54. ) -> dict[str, Any]:
  55. """Call a tool on the astro-mcp server via MCP session."""
  56. result = await session.call_tool(tool_name, arguments)
  57. return _payload_from_result(result)
  58. async def render_person_chart(person_id: str, fmt: str, size: int, color_mode: str, timeout: float = 30.0) -> dict[str, Any] | None:
  59. """Render a natal chart for one person by ID via MCP."""
  60. url = ASTRO_MCP_URL
  61. if not url.endswith("/mcp/sse"):
  62. url = url.rstrip("/") + "/mcp/sse"
  63. async with sse_client(url, timeout=timeout, sse_read_timeout=timeout) as streams:
  64. async with ClientSession(
  65. *streams, read_timeout_seconds=timedelta(seconds=timeout)
  66. ) as session:
  67. await session.initialize()
  68. # Get chart data first (for moon info)
  69. chart_args = {"person_id": person_id}
  70. chart_result = await call_astro_tool(session, "calculate_natal_chart_by_id", chart_args)
  71. if "error" in chart_result:
  72. print(f" ERROR calculating {person_id}: {chart_result['error']}")
  73. return None
  74. # Then render
  75. render_args = {"person_id": person_id, "color_mode": color_mode, "size": size, "format": fmt}
  76. render_result = await call_astro_tool(session, "render_natal_chart_by_id", render_args)
  77. if "error" in render_result:
  78. print(f" ERROR rendering {person_id}: {render_result['error']}")
  79. return None
  80. # Save to output directory
  81. content = render_result.get("content", "")
  82. ext = fmt
  83. filename = f"{person_id}_natal.{ext}"
  84. outdir = "/home/shared/astro"
  85. os.makedirs(outdir, exist_ok=True)
  86. outpath = os.path.join(outdir, filename)
  87. mode = "w" if isinstance(content, str) else "wb"
  88. with open(outpath, mode) as f:
  89. f.write(content)
  90. # Extract moon info from chart data
  91. moon_info = None
  92. for p in chart_result.get("planets", []):
  93. if p.get("body") == "moon":
  94. moon_info = {
  95. "absolute_lon": p.get("absolute_lon"),
  96. "degree_within_sign": p.get("degree_within_sign"),
  97. "sign": p.get("sign"),
  98. "house": p.get("house"),
  99. "retrograde": p.get("retrograde"),
  100. }
  101. break
  102. return {
  103. "person_id": person_id,
  104. "output": outpath,
  105. "format": render_result.get("format"),
  106. "content_type": render_result.get("content_type"),
  107. "size_bytes": len(content) if isinstance(content, bytes) else len(content.encode("utf-8")),
  108. "moon": moon_info,
  109. "all_planets": [
  110. {"body": p.get("body"), "absolute_lon": p.get("absolute_lon"),
  111. "degree_within_sign": p.get("degree_within_sign"),
  112. "sign": p.get("sign"), "house": p.get("house"),
  113. "retrograde": p.get("retrograde")}
  114. for p in chart_result.get("planets", [])
  115. ],
  116. "houses_count": len(chart_result.get("houses", [])),
  117. "aspects_count": len(chart_result.get("aspects", [])),
  118. }
  119. async def main():
  120. parser = argparse.ArgumentParser(description="Render test natal charts via MCP client")
  121. parser.add_argument("--format", default="svg", choices=["svg", "png", "jpg"],
  122. help="Output format (default: svg)")
  123. parser.add_argument("--size", type=int, default=600,
  124. help="Canvas size in pixels (default: 600)")
  125. parser.add_argument("--color-mode", default="color", choices=["color", "bw", "dark"],
  126. help="Color theme (default: color)")
  127. args = parser.parse_args()
  128. print(f"Rendering test charts — format={args.format}, size={args.size}px, theme={args.color_mode}")
  129. print(f"Output directory: /home/shared/astro/")
  130. print(f"Persons: {', '.join(PERSON_IDS)}")
  131. print()
  132. results = []
  133. for person_id in PERSON_IDS:
  134. print(f"Processing {person_id}...")
  135. result = await render_person_chart(person_id, args.format, args.size, args.color_mode)
  136. if result:
  137. results.append(result)
  138. print(f" Saved: {result['output']} ({result['size_bytes']:,} bytes)")
  139. if result["moon"]:
  140. m = result["moon"]
  141. print(f" MOON: {m['sign']} {m['degree_within_sign']:.1f}° (lon={m['absolute_lon']:.2f}°, house {m['house']})")
  142. else:
  143. print(f" MOON: not found in data!")
  144. print(f" Planets: {len(result['all_planets'])}, Houses: {result['houses_count']}, Aspects: {result['aspects_count']}")
  145. else:
  146. print(f" FAILED")
  147. print()
  148. # Summary comparison
  149. print("=" * 60)
  150. print("MOON COMPARISON")
  151. print("=" * 60)
  152. for r in results:
  153. m = r.get("moon", {})
  154. if m:
  155. print(f" {r['person_id']:20s} | Moon in {m.get('sign','?'):12s} {m.get('degree_within_sign',0):5.1f}° | House {m.get('house','?')} | Retrograde: {m.get('retrograde',False)}")
  156. else:
  157. print(f" {r['person_id']:20s} | Moon data MISSING")
  158. print()
  159. # Save JSON summary
  160. summary_path = "/home/shared/astro/test_charts_summary.json"
  161. with open(summary_path, "w") as f:
  162. json.dump(results, f, indent=2, default=str)
  163. print(f"Summary saved to: {summary_path}")
  164. if __name__ == "__main__":
  165. asyncio.run(main())