Skip to content

Commit 14efeb5

Browse files
feat: Improve interface, testing, defaults, add make docker-build and docker-test
1 parent 8dcc9e2 commit 14efeb5

13 files changed

Lines changed: 583 additions & 72 deletions

Dockerfile

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,23 @@ FROM python:3.11-slim AS builder
33

44
WORKDIR /app
55

6+
# Install build dependencies for pyswisseph (C extension)
7+
RUN apt-get update && apt-get install -y --no-install-recommends \
8+
gcc \
9+
g++ \
10+
make \
11+
&& rm -rf /var/lib/apt/lists/*
12+
613
# Install uv
714
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
815

9-
# Copy dependency files first (cache layer)
10-
COPY pyproject.toml uv.lock ./
16+
# Copy dependency files and source (needed for hatchling to build)
17+
COPY pyproject.toml uv.lock README.md ./
18+
COPY src/ src/
1119

1220
# Install dependencies with frozen lockfile (reproducible)
1321
RUN uv sync --frozen --no-dev --no-editable
1422

15-
# Copy source code
16-
COPY src/ src/
17-
1823
# Stage 2: Runtime
1924
FROM python:3.11-slim
2025

Makefile

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ help:
1919
@echo "make test - Run tests with coverage"
2020
@echo "make validate-dev - Run development validation (install, lint, format-check, test)"
2121
@echo "make clean - Remove artifacts and cache"
22-
@echo "make docker-build - Build the Docker image"
23-
@echo "make docker-run - Run the Docker container"
22+
@echo "make docker-build - Build the Docker image"
23+
@echo "make docker-run - Run the Docker container interactively"
24+
@echo "make docker-happycase - Test the Docker image with a simple query"
25+
@echo "make docker-dev - Full Docker dev workflow (validate, build, test)"
26+
@echo "make docker-clean - Remove Docker images"
27+
@echo "make validate-happycase - Test engine locally with a simple query"
2428
@echo "make act - Run GitHub Actions locally (requires 'act')"
2529
@echo "make release-dry-run - Dry-run semantic release"
2630
@echo "make audit - Audit dependencies"
@@ -64,6 +68,12 @@ validate-dev:
6468
$(MAKE) test
6569
@echo "✅ Development simple validation complete!"
6670

71+
validate-happycase:
72+
@echo "🧪 Testing engine locally with planetary positions query..."
73+
@echo "📍 Querying: 2025-12-16T15:28:00Z at New York (40.7128, -74.0060)"
74+
@uv run python -c "from astro_mcp.engine import calculate_chart; result = calculate_chart('2025-12-16T15:28:00Z', 40.7128, -74.0060); print('✅ Engine working!\n'); print('🌟 BODIES:'); [print(f' {name:18} {data[\"sign\"]:10} {data[\"sign_degrees\"]:6.2f}° ({data[\"motion\"]})') for name, data in result['bodies'].items()]; print('\n🏠 HOUSES:'); [print(f' {name:18} {data[\"sign\"]:10} {data[\"sign_degrees\"]:6.2f}°') for name, data in result['houses'].items()]"
75+
@echo "\n✅ Happy case test passed!"
76+
6777
validate-dev-full: validate-dev
6878
@echo "🔍 Running full development validation..."
6979
$(MAKE) release-dry-run
@@ -85,11 +95,28 @@ docker-build:
8595
@echo "🐳 Building Docker image..."
8696
docker build -t $(IMAGE_NAME):$(VERSION) .
8797
docker tag $(IMAGE_NAME):$(VERSION) $(IMAGE_NAME):latest
98+
@echo "✅ Docker image built: $(IMAGE_NAME):$(VERSION) and $(IMAGE_NAME):latest"
8899

89100
docker-run:
90-
@echo "🚀 Running container..."
101+
@echo "🚀 Running container interactively..."
91102
docker run -it --rm $(IMAGE_NAME):latest
92103

104+
docker-happycase:
105+
@echo "🧪 Testing Docker image with planetary positions query..."
106+
@echo "📍 Querying: 2025-12-16T15:28:00Z at New York (40.7128, -74.0060)"
107+
@docker run --rm $(IMAGE_NAME):latest python -c "from astro_mcp.engine import calculate_chart; import json; result = calculate_chart('2025-12-16T15:28:00Z', 40.7128, -74.0060); print('✅ Docker image working!\n'); print('🌟 BODIES:'); [print(f' {name:18} {data[\"sign\"]:10} {data[\"sign_degrees\"]:6.2f}° ({data[\"motion\"]})') for name, data in result['bodies'].items()]; print('\n🏠 HOUSES:'); [print(f' {name:18} {data[\"sign\"]:10} {data[\"sign_degrees\"]:6.2f}°') for name, data in result['houses'].items()]"
108+
@echo "\n✅ Docker test passed!"
109+
110+
docker-dev: validate-dev docker-build docker-happycase
111+
@echo "✅ Full Docker dev workflow complete!"
112+
@echo "📦 Image ready: $(IMAGE_NAME):$(VERSION)"
113+
@echo "🚀 To run: make docker-run"
114+
115+
docker-clean:
116+
@echo "🧹 Removing Docker images..."
117+
docker rmi $(IMAGE_NAME):$(VERSION) $(IMAGE_NAME):latest || true
118+
@echo "✅ Docker images removed"
119+
93120
# --- CI/CD (Local) ---
94121

95122
act:
@@ -111,4 +138,4 @@ act-ci:
111138

112139
act-release:
113140
@echo "🎬 Running Release workflow locally with act (dry-run)..."
114-
act push --container-architecture linux/amd64 -W .github/workflows/cd-release.yml --dryrun
141+
act push --container-architecture linux/amd64 -W .github/workflows/cd-release.yml --dryrun

docs/getting-started.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ docker run -i astro-mcp:latest
3838
AstroMCP exposes a single MCP tool: `get_planetary_positions`
3939

4040
**Parameters:**
41-
- `iso_time`: ISO-8601 timestamp (e.g., `2026-01-10T15:00:00`)
42-
- `latitude`: Observer latitude (default: 43.8)
43-
- `longitude`: Observer longitude (default: -84.7)
41+
- `iso_time`: ISO-8601 timestamp (e.g., `2025-12-16T15:28:00`)
42+
- `latitude`: Observer latitude (default: 42.3314, Detroit, MI)
43+
- `longitude`: Observer longitude (default: -83.0458, Detroit, MI)
4444

4545
**Returns:** JSON with planetary positions in Tropical Zodiac coordinates.
4646

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ requires-python = ">=3.10, <3.12"
88
dependencies = [
99
"flatlib",
1010
"mcp>=0.1.0",
11-
"pydantic"
11+
"pydantic",
12+
"structlog>=24.0.0"
1213
]
1314

1415
# This tells uv to install these tools

scaffolding.sh

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,14 @@ from astro_mcp.engine import calculate_chart
9898
mcp = FastMCP("AstroMCP")
9999
100100
@mcp.tool()
101-
def get_planetary_positions(iso_time: str, latitude: float = 43.8, longitude: float = -84.7) -> str:
101+
def get_planetary_positions(iso_time: str, latitude: float = 42.3314, longitude: float = -83.0458) -> str:
102102
"""
103103
Returns precise astrological positions (Tropical Zodiac) for a given time/place.
104-
104+
105105
Args:
106-
iso_time: ISO-8601 string (e.g., '2026-01-10T15:00:00')
107-
latitude: Observer latitude (default: Clare County, MI)
108-
longitude: Observer longitude (default: Clare County, MI)
106+
iso_time: ISO-8601 string (e.g., '2025-12-16T15:28:00')
107+
latitude: Observer latitude (default: Detroit, MI)
108+
longitude: Observer longitude (default: Detroit, MI)
109109
"""
110110
try:
111111
data = calculate_chart(iso_time, latitude, longitude)
@@ -130,7 +130,7 @@ from astro_mcp.engine import calculate_chart
130130
131131
def test_calculate_chart_structure():
132132
"""Ensure the JSON structure matches the ADR spec."""
133-
result = calculate_chart("2026-01-01T12:00:00", 43.8, -84.7)
133+
result = calculate_chart("2025-12-16T15:28:00", 42.3314, -83.0458)
134134
135135
assert "meta" in result
136136
assert "bodies" in result

src/astro_mcp/engine.py

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import datetime
2-
import logging
32

3+
import structlog
44
from flatlib import const
55
from flatlib.chart import Chart
66
from flatlib.datetime import Datetime
77
from flatlib.geopos import GeoPos
88

9-
# Configure logging
10-
logging.basicConfig(level=logging.INFO)
11-
logger = logging.getLogger(__name__)
9+
logger = structlog.get_logger(__name__)
1210

1311

1412
def calculate_chart(iso_date: str, lat: float, lon: float) -> dict:
@@ -29,7 +27,7 @@ def calculate_chart(iso_date: str, lat: float, lon: float) -> dict:
2927
pos = GeoPos(lat, lon)
3028

3129
# Calculate the chart with all planets including outer planets
32-
# Note: Angles (ASC, MC, NORTH_NODE) cannot be included in IDs parameter
30+
# Note: Angles (ASC, MC) are calculated automatically and accessed via chart.get()
3331
chart = Chart(
3432
date,
3533
pos,
@@ -44,12 +42,16 @@ def calculate_chart(iso_date: str, lat: float, lon: float) -> dict:
4442
const.URANUS,
4543
const.NEPTUNE,
4644
const.PLUTO,
45+
const.NORTH_NODE,
46+
const.SOUTH_NODE,
47+
const.CHIRON,
4748
],
4849
)
4950

5051
output = {
5152
"meta": {"timestamp": iso_date, "lat": lat, "lon": lon, "system": "Geocentric Tropical Zodiac"},
5253
"bodies": {},
54+
"houses": {},
5355
}
5456

5557
# Define bodies to track
@@ -64,17 +66,38 @@ def calculate_chart(iso_date: str, lat: float, lon: float) -> dict:
6466
(const.URANUS, "Uranus"),
6567
(const.NEPTUNE, "Neptune"),
6668
(const.PLUTO, "Pluto"),
69+
(const.NORTH_NODE, "North Node"),
70+
(const.SOUTH_NODE, "South Node"),
71+
(const.CHIRON, "Chiron"),
72+
]
73+
74+
# Define angles to track
75+
angles = [
6776
(const.ASC, "Ascendant"),
6877
(const.MC, "Midheaven"),
69-
(const.NORTH_NODE, "North Node"),
78+
(const.DESC, "Descendant"),
79+
(const.IC, "Imum Coeli"),
80+
]
81+
82+
# Define house cusps to track
83+
houses = [
84+
(const.HOUSE1, "House 1"),
85+
(const.HOUSE2, "House 2"),
86+
(const.HOUSE3, "House 3"),
87+
(const.HOUSE4, "House 4"),
88+
(const.HOUSE5, "House 5"),
89+
(const.HOUSE6, "House 6"),
90+
(const.HOUSE7, "House 7"),
91+
(const.HOUSE8, "House 8"),
92+
(const.HOUSE9, "House 9"),
93+
(const.HOUSE10, "House 10"),
94+
(const.HOUSE11, "House 11"),
95+
(const.HOUSE12, "House 12"),
7096
]
7197

7298
for body_id, friendly_name in bodies:
7399
try:
74100
obj = chart.get(body_id)
75-
76-
# Access speed directly from the object attribute
77-
# Angles (ASC/MC) might not have speed, default to 0.0
78101
speed = getattr(obj, "lonspeed", 0.0)
79102

80103
output["bodies"][friendly_name] = {
@@ -85,13 +108,64 @@ def calculate_chart(iso_date: str, lat: float, lon: float) -> dict:
85108
"motion": "retrograde" if speed < 0 else "direct",
86109
"speed": float(f"{speed:.4f}"),
87110
}
88-
except KeyError:
89-
# Skip planets that couldn't be calculated
90-
logger.warning(f"Could not calculate {friendly_name}")
111+
except (KeyError, AttributeError) as e:
112+
logger.warning(
113+
"Could not calculate body",
114+
body=friendly_name,
115+
error=str(e),
116+
error_type=type(e).__name__,
117+
)
118+
continue
119+
120+
# Process angles (ASC, MC, DESC, IC)
121+
for angle_id, friendly_name in angles:
122+
try:
123+
obj = chart.get(angle_id)
124+
output["bodies"][friendly_name] = {
125+
"longitude": float(f"{obj.lon:.4f}"),
126+
"sign": str(obj.sign),
127+
"sign_degrees": float(f"{obj.signlon:.4f}"),
128+
"declination": float(f"{obj.lat:.4f}"),
129+
"motion": "direct",
130+
"speed": 0.0,
131+
}
132+
except (KeyError, AttributeError) as e:
133+
logger.warning(
134+
"Could not calculate angle",
135+
angle=friendly_name,
136+
error=str(e),
137+
error_type=type(e).__name__,
138+
)
139+
continue
140+
141+
# Process house cusps
142+
for house_id, friendly_name in houses:
143+
try:
144+
obj = chart.get(house_id)
145+
output["houses"][friendly_name] = {
146+
"longitude": float(f"{obj.lon:.4f}"),
147+
"sign": str(obj.sign),
148+
"sign_degrees": float(f"{obj.signlon:.4f}"),
149+
}
150+
except (KeyError, AttributeError) as e:
151+
logger.warning(
152+
"Could not calculate house",
153+
house=friendly_name,
154+
error=str(e),
155+
error_type=type(e).__name__,
156+
)
91157
continue
92158

93159
return output
94160

95161
except Exception as e:
96-
logger.error(f"Calculation failed: {e}")
162+
logger.error(
163+
"Calculation failed",
164+
error=str(e),
165+
error_type=type(e).__name__,
166+
iso_date=iso_date,
167+
lat=lat,
168+
lon=lon,
169+
exc_info=True,
170+
)
97171
raise ValueError(f"Physics Engine Error: {str(e)}")

src/astro_mcp/logging_config.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# See AGENTS.md for project context and conventions
2+
3+
"""Logging configuration for AstroMCP.
4+
5+
This module provides structured logging setup using structlog:
6+
- JSON-serializable structured events
7+
- Context binding and processors
8+
- Integration with stdlib logging for compatibility
9+
"""
10+
11+
import logging
12+
import sys
13+
14+
import structlog
15+
16+
17+
def setup_logging(level: int = logging.INFO) -> None:
18+
"""Configure application-wide logging with structlog.
19+
20+
Args:
21+
level: Logging level (default: INFO)
22+
"""
23+
# Configure structlog processors
24+
structlog.configure(
25+
processors=[
26+
structlog.contextvars.merge_contextvars,
27+
structlog.processors.add_log_level,
28+
structlog.processors.StackInfoRenderer(),
29+
structlog.dev.set_exc_info,
30+
structlog.processors.TimeStamper(fmt="iso", utc=True),
31+
structlog.dev.ConsoleRenderer(), # Human-readable for development
32+
],
33+
wrapper_class=structlog.make_filtering_bound_logger(level),
34+
context_class=dict,
35+
logger_factory=structlog.PrintLoggerFactory(file=sys.stderr),
36+
cache_logger_on_first_use=True,
37+
)
38+
39+
# Also configure stdlib logging for compatibility
40+
logging.basicConfig(
41+
format="%(message)s",
42+
level=level,
43+
stream=sys.stderr,
44+
)

src/astro_mcp/server.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
from mcp.server.fastmcp import FastMCP
22

33
from astro_mcp.engine import calculate_chart
4+
from astro_mcp.logging_config import setup_logging
5+
6+
# Initialize logging
7+
setup_logging()
48

59
mcp = FastMCP("AstroMCP")
610

711

812
@mcp.tool()
9-
def get_planetary_positions(iso_time: str, latitude: float = 43.8, longitude: float = -84.7) -> dict:
13+
def get_planetary_positions(iso_time: str, latitude: float = 42.3314, longitude: float = -83.0458) -> dict:
1014
"""
1115
Returns precise astrological positions (Tropical Zodiac) for a given time/place.
1216
1317
Args:
14-
iso_time: ISO-8601 string (e.g., '2026-01-10T15:00:00')
15-
latitude: Observer latitude (default: Clare County, MI)
16-
longitude: Observer longitude (default: Clare County, MI)
18+
iso_time: ISO-8601 string (e.g., '2025-12-16T15:28:00')
19+
latitude: Observer latitude (default: Detroit, MI)
20+
longitude: Observer longitude (default: Detroit, MI)
1721
"""
1822
data = calculate_chart(iso_date=iso_time, lat=latitude, lon=longitude)
1923
return data

0 commit comments

Comments
 (0)