mcnoaa-tides/tests/conftest.py
Ryan Malloy 66ec2ba9e7 Add visualization tools: tide charts and conditions dashboards
New visualize_tides and visualize_conditions MCP tools that generate
PNG (inline via MCP ImageContent) or interactive HTML (Plotly) charts.
Optional dependency group [viz] keeps the base install lightweight.

- charts/ package: rendering logic separated from MCP tool wiring
- Shared marine color palette (ocean blue, teal, slate, sand, coral)
- 14 new tests (parsing, PNG/HTML rendering, tool registration)
- Example chart images in README with realistic synthetic tidal data
2026-02-22 16:51:00 -07:00

155 lines
4.2 KiB
Python

"""Test fixtures with mock NOAAClient injected via lifespan."""
from contextlib import asynccontextmanager
from unittest.mock import AsyncMock
import pytest
from fastmcp import Client, FastMCP
from fastmcp.client.transports import StreamableHttpTransport
from fastmcp.utilities.tests import run_server_async
from noaa_tides import prompts, resources
from noaa_tides.client import NOAAClient
from noaa_tides.tools import charts, conditions, meteorological, stations, tides
# Realistic station fixtures
MOCK_STATIONS_RAW = [
{
"id": "8454000",
"name": "Providence",
"state": "RI",
"lat": 41.8071,
"lng": -71.4012,
"tidal": True,
"greatlakes": False,
"shefcode": "PRVD1",
},
{
"id": "8452660",
"name": "Newport",
"state": "RI",
"lat": 41.5043,
"lng": -71.3261,
"tidal": True,
"greatlakes": False,
"shefcode": "NWPR1",
},
{
"id": "8447930",
"name": "Woods Hole",
"state": "MA",
"lat": 41.5236,
"lng": -70.6714,
"tidal": True,
"greatlakes": False,
"shefcode": "WHOM3",
},
]
MOCK_PREDICTIONS = {
"predictions": [
{"t": "2026-02-21 04:30", "v": "4.521", "type": "H"},
{"t": "2026-02-21 10:42", "v": "-0.123", "type": "L"},
{"t": "2026-02-21 16:55", "v": "5.012", "type": "H"},
{"t": "2026-02-21 23:08", "v": "0.234", "type": "L"},
]
}
MOCK_WATER_LEVEL = {
"data": [
{"t": "2026-02-21 00:00", "v": "2.34", "s": "0.003", "f": "0,0,0,0", "q": "p"},
{"t": "2026-02-21 00:06", "v": "2.38", "s": "0.003", "f": "0,0,0,0", "q": "p"},
]
}
MOCK_WIND = {
"data": [
{
"t": "2026-02-21 00:00",
"s": "12.5",
"d": "225.00",
"dr": "SW",
"g": "18.2",
"f": "0,0",
}
]
}
MOCK_AIR_TEMP = {"data": [{"t": "2026-02-21 00:00", "v": "42.3", "f": "0,0,0"}]}
MOCK_WATER_TEMP = {"data": [{"t": "2026-02-21 00:00", "v": "38.7", "f": "0,0,0"}]}
MOCK_PRESSURE = {"data": [{"t": "2026-02-21 00:00", "v": "1013.2", "f": "0,0,0"}]}
MOCK_METADATA = {
"stations": [
{
"id": "8454000",
"name": "Providence",
"state": "RI",
"lat": 41.8071,
"lng": -71.4012,
"tidal": True,
"sensors": [{"name": "Water Level"}, {"name": "Air Temperature"}],
"products": {"self": "...", "tidePredictions": "..."},
"datums": {"datums": [{"name": "MLLW", "value": "0.0"}]},
}
]
}
def _build_mock_client() -> NOAAClient:
"""Build a NOAAClient with mocked HTTP but real search/nearest logic."""
from noaa_tides.models import Station
client = NOAAClient()
client._stations = [Station(**s) for s in MOCK_STATIONS_RAW]
client._cache_time = float("inf") # Never expires
client._http = AsyncMock()
async def mock_get_data(station_id, product, **kwargs):
responses = {
"predictions": MOCK_PREDICTIONS,
"water_level": MOCK_WATER_LEVEL,
"wind": MOCK_WIND,
"air_temperature": MOCK_AIR_TEMP,
"water_temperature": MOCK_WATER_TEMP,
"air_pressure": MOCK_PRESSURE,
}
if product in responses:
return responses[product]
raise ValueError(f"No data was found for product: {product}")
client.get_data = mock_get_data
async def mock_get_station_metadata(station_id):
return MOCK_METADATA["stations"][0]
client.get_station_metadata = mock_get_station_metadata
return client
@asynccontextmanager
async def _test_lifespan(server: FastMCP):
client = _build_mock_client()
yield {"noaa_client": client}
def _build_test_server() -> FastMCP:
mcp = FastMCP("noaa-tides-test", lifespan=_test_lifespan)
stations.register(mcp)
tides.register(mcp)
meteorological.register(mcp)
conditions.register(mcp)
charts.register(mcp)
resources.register(mcp)
prompts.register(mcp)
return mcp
@pytest.fixture
async def mcp_client():
server = _build_test_server()
async with run_server_async(server) as url:
async with Client(StreamableHttpTransport(url)) as client:
yield client