- Add lunar calendar astronomical calculation engine (lunar_calendar.py) - Add converter utilities for solar/lunar date conversion - Add historical era name conversions (chrono_converter.py, converter_with_eras.py) - Add FastAPI REST API for date conversion services - Add age calculator using lunar calendar - Add batch generation script for lunar calendar JSON data - Add project documentation (CLAUDE.md, pyproject.toml) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
3.7 KiB
Python
111 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
||
# 提供農曆-西曆轉換的 API 服務(FastAPI 版本)
|
||
# 依賴: fastapi, uvicorn, converter.py
|
||
|
||
# /// script
|
||
# requires-python = ">=3.12"
|
||
# dependencies = [
|
||
# "fastapi",
|
||
# "uvicorn",
|
||
# ]
|
||
# ///
|
||
|
||
import socket
|
||
from contextlib import closing
|
||
from typing import Any, Dict, List
|
||
|
||
from fastapi import FastAPI, Query # type: ignore[reportMissingImports]
|
||
from fastapi.responses import JSONResponse # type: ignore[reportMissingImports]
|
||
import uvicorn # type: ignore[reportMissingImports]
|
||
|
||
from converter import Converter
|
||
|
||
|
||
def find_free_port() -> int:
|
||
"""使用臨時 socket 綁定到 0 取得可用埠號"""
|
||
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
||
s.bind(("", 0))
|
||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||
return s.getsockname()[1]
|
||
|
||
|
||
app = FastAPI(title="Lunar Calendar Converter API")
|
||
conv = Converter()
|
||
|
||
|
||
@app.get("/solar2lunar")
|
||
def solar2lunar_endpoint(date: str = Query(None, description="YYYY-MM-DD")):
|
||
"""西曆轉農曆"""
|
||
if not date:
|
||
return JSONResponse({"error": "缺少參數 date,格式 YYYY-MM-DD"}, status_code=400)
|
||
try:
|
||
info_raw = conv.solar_to_lunar(date)
|
||
info = {
|
||
"gregorian": info_raw.get("西曆"),
|
||
"lunar": info_raw.get("農曆"),
|
||
"ganzhi": info_raw.get("干支"),
|
||
"zodiac": info_raw.get("生肖"),
|
||
"solar_term": info_raw.get("節氣"),
|
||
}
|
||
return info
|
||
except Exception as e:
|
||
return JSONResponse({"error": str(e)}, status_code=400)
|
||
|
||
|
||
@app.get("/lunar2solar")
|
||
def lunar2solar_endpoint(date: str = Query(None, description="YYYY-MM-DD")):
|
||
"""農曆轉西曆(自動處理閏月,回傳所有可能性)"""
|
||
if not date:
|
||
return JSONResponse({"error": "缺少參數 date,格式 YYYY-MM-DD"}, status_code=400)
|
||
|
||
try:
|
||
y_str, m_str, d_str = date.split("-", 2)
|
||
y, m, d = int(y_str), int(m_str), int(d_str)
|
||
except Exception:
|
||
return JSONResponse({"error": "農曆日期格式應為 YYYY-MM-DD"}, status_code=400)
|
||
|
||
possible_solar_dates: List[str] = conv.lunar_to_solar_all(y, m, d)
|
||
if not possible_solar_dates:
|
||
error_msg = f"找不到任何對應農曆 {y}年{m}月{d}日 的西曆日期"
|
||
return JSONResponse({"error": error_msg}, status_code=404)
|
||
|
||
results_list: List[Dict[str, Any]] = []
|
||
for solar_date in possible_solar_dates:
|
||
try:
|
||
info_raw = conv.solar_to_lunar(solar_date)
|
||
results_list.append({
|
||
"gregorian": info_raw.get("西曆"),
|
||
"lunar": info_raw.get("農曆"),
|
||
"ganzhi": info_raw.get("干支"),
|
||
"zodiac": info_raw.get("生肖"),
|
||
"solar_term": info_raw.get("節氣"),
|
||
})
|
||
except Exception:
|
||
continue
|
||
|
||
return results_list
|
||
|
||
|
||
@app.get("/leap-info")
|
||
def leap_info_endpoint(year: str = Query(None, description="西曆年份,數字")):
|
||
"""查詢指定西曆年份是否有農曆閏月,以及是閏幾月。"""
|
||
if not year:
|
||
return JSONResponse({"error": "缺少參數 year"}, status_code=400)
|
||
try:
|
||
y = int(year)
|
||
leap_month = conv.get_leap_month(y)
|
||
return {"year": y, "leap_month": leap_month}
|
||
except ValueError:
|
||
return JSONResponse({"error": "參數 year 必須是有效的整數"}, status_code=400)
|
||
except Exception as e:
|
||
return JSONResponse({"error": str(e)}, status_code=500)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
free_port = find_free_port()
|
||
print(f"找到一個可用的 port: {free_port}")
|
||
uvicorn.run(app, host="0.0.0.0", port=free_port, reload=False)
|
||
except Exception as e:
|
||
print(f"啟動服務失敗: {e}")
|