Complete implementation of all modules: - constants.py: GIBS API endpoints, projections, TileMatrixSet defs - models.py: Pydantic models for layers, colormaps, geocoding - geo.py: Nominatim geocoding with rate limiting and caching - capabilities.py: WMTS GetCapabilities XML parser with search - colormaps.py: Colormap v1.3 parser with natural-language summaries - client.py: Async GIBS HTTP client wrapping all API interactions - server.py: FastMCP 3.0 tools, resources, and prompts 11 MCP tools, 3 resources, 2 prompts. 47 tests, all passing.
185 lines
5.5 KiB
Python
185 lines
5.5 KiB
Python
"""Tests for GIBSClient using respx mocks — no real HTTP calls."""
|
|
|
|
from io import BytesIO
|
|
|
|
import httpx
|
|
import respx
|
|
from PIL import Image
|
|
|
|
from mcgibs.client import GIBSClient
|
|
from mcgibs.constants import WMTS_TILE_URL
|
|
|
|
|
|
@respx.mock
|
|
async def test_client_initialize(capabilities_xml):
|
|
"""Loading capabilities populates layer_index with all three sample layers."""
|
|
respx.get(url__regex=r".*WMTSCapabilities\.xml").mock(
|
|
return_value=httpx.Response(200, text=capabilities_xml)
|
|
)
|
|
|
|
client = GIBSClient()
|
|
await client.initialize()
|
|
|
|
assert len(client.layer_index) == 3
|
|
assert "MODIS_Terra_CorrectedReflectance_TrueColor" in client.layer_index
|
|
assert "AMSR2_Sea_Ice_Concentration_12km_Monthly" in client.layer_index
|
|
assert "AIRS_L3_Surface_Air_Temperature_Daily_Day" in client.layer_index
|
|
|
|
# Spot-check a parsed layer
|
|
modis = client.layer_index["MODIS_Terra_CorrectedReflectance_TrueColor"]
|
|
assert modis.title == "Corrected Reflectance (True Color, Terra/MODIS)"
|
|
assert "image/jpeg" in modis.formats
|
|
assert modis.time is not None
|
|
assert modis.time.start == "2000-02-24"
|
|
assert modis.has_colormap is False
|
|
|
|
await client.close()
|
|
|
|
|
|
@respx.mock
|
|
async def test_client_fetch_layer_metadata(capabilities_xml, layer_metadata_json):
|
|
"""Fetching layer metadata enriches the layer with instrument/platform fields."""
|
|
respx.get(url__regex=r".*WMTSCapabilities\.xml").mock(
|
|
return_value=httpx.Response(200, text=capabilities_xml)
|
|
)
|
|
layer_id = "MODIS_Terra_CorrectedReflectance_TrueColor"
|
|
respx.get(url__regex=rf".*layer-metadata.*/{layer_id}\.json").mock(
|
|
return_value=httpx.Response(200, text=layer_metadata_json)
|
|
)
|
|
|
|
client = GIBSClient()
|
|
await client.initialize()
|
|
|
|
data = await client.fetch_layer_metadata(layer_id)
|
|
|
|
assert data["instrument"] == "MODIS"
|
|
assert data["platform"] == "Terra"
|
|
assert data["ongoing"] is True
|
|
|
|
# Verify the layer_index entry was enriched
|
|
layer = client.get_layer(layer_id)
|
|
assert layer is not None
|
|
assert layer.instrument == "MODIS"
|
|
assert layer.platform == "Terra"
|
|
assert layer.measurement == "Corrected Reflectance"
|
|
assert layer.day_night == "Day"
|
|
|
|
# Second call should use cache
|
|
data2 = await client.fetch_layer_metadata(layer_id)
|
|
assert data2 is data
|
|
|
|
await client.close()
|
|
|
|
|
|
@respx.mock
|
|
async def test_client_fetch_colormap(capabilities_xml, colormap_xml):
|
|
"""Parsing colormap XML produces entries and legend data."""
|
|
respx.get(url__regex=r".*WMTSCapabilities\.xml").mock(
|
|
return_value=httpx.Response(200, text=capabilities_xml)
|
|
)
|
|
respx.get(url__regex=r".*colormaps/v1\.3/.*\.xml").mock(
|
|
return_value=httpx.Response(200, text=colormap_xml)
|
|
)
|
|
|
|
client = GIBSClient()
|
|
await client.initialize()
|
|
|
|
colormap_set = await client.fetch_colormap("AIRS_L3_Surface_Air_Temperature_Daily_Day")
|
|
|
|
assert colormap_set is not None
|
|
assert len(colormap_set.maps) == 2
|
|
|
|
data_map = colormap_set.data_map
|
|
assert data_map is not None
|
|
assert data_map.title == "Surface Air Temperature"
|
|
assert data_map.units == "K"
|
|
assert len(data_map.entries) == 14
|
|
assert data_map.legend_type == "continuous"
|
|
|
|
# Verify the nodata map
|
|
nodata_map = colormap_set.maps[1]
|
|
assert any(e.nodata for e in nodata_map.entries)
|
|
|
|
# Second fetch should be cached
|
|
cached = await client.fetch_colormap("AIRS_L3_Surface_Air_Temperature_Daily_Day")
|
|
assert cached is colormap_set
|
|
|
|
await client.close()
|
|
|
|
|
|
@respx.mock
|
|
async def test_client_get_wms_image(capabilities_xml):
|
|
"""WMS GetMap returns raw image bytes when content-type is image/*."""
|
|
respx.get(url__regex=r".*WMTSCapabilities\.xml").mock(
|
|
return_value=httpx.Response(200, text=capabilities_xml)
|
|
)
|
|
|
|
# Build a tiny valid JPEG to return as the mock response
|
|
buf = BytesIO()
|
|
Image.new("RGB", (10, 10), "blue").save(buf, format="JPEG")
|
|
fake_jpeg = buf.getvalue()
|
|
|
|
respx.get(url__regex=r".*wms\.cgi.*").mock(
|
|
return_value=httpx.Response(
|
|
200,
|
|
content=fake_jpeg,
|
|
headers={"content-type": "image/jpeg"},
|
|
)
|
|
)
|
|
|
|
client = GIBSClient()
|
|
await client.initialize()
|
|
|
|
from mcgibs.models import BBox
|
|
|
|
bbox = BBox(west=-120.0, south=30.0, east=-110.0, north=40.0)
|
|
result = await client.get_wms_image(
|
|
"MODIS_Terra_CorrectedReflectance_TrueColor",
|
|
"2025-06-01",
|
|
bbox,
|
|
)
|
|
|
|
assert isinstance(result, bytes)
|
|
assert len(result) > 0
|
|
# Verify the returned bytes are valid JPEG (starts with FFD8)
|
|
assert result[:2] == b"\xff\xd8"
|
|
|
|
await client.close()
|
|
|
|
|
|
def test_client_build_tile_url():
|
|
"""build_tile_url produces the expected WMTS REST URL format."""
|
|
client = GIBSClient()
|
|
|
|
url = client.build_tile_url(
|
|
layer_id="MODIS_Terra_CorrectedReflectance_TrueColor",
|
|
date="2025-06-01",
|
|
zoom=3,
|
|
row=2,
|
|
col=4,
|
|
tile_matrix_set="250m",
|
|
ext="jpg",
|
|
epsg="4326",
|
|
)
|
|
|
|
assert "MODIS_Terra_CorrectedReflectance_TrueColor" in url
|
|
assert "2025-06-01" in url
|
|
assert "/250m/" in url
|
|
assert "/3/" in url
|
|
assert "/2/" in url
|
|
assert "/4.jpg" in url
|
|
assert "epsg4326" in url
|
|
|
|
# Verify it matches the constant template
|
|
expected = WMTS_TILE_URL.format(
|
|
epsg="4326",
|
|
layer_id="MODIS_Terra_CorrectedReflectance_TrueColor",
|
|
date="2025-06-01",
|
|
tile_matrix_set="250m",
|
|
z=3,
|
|
row=2,
|
|
col=4,
|
|
ext="jpg",
|
|
)
|
|
assert url == expected
|