added tests
This commit is contained in:
@@ -53,7 +53,9 @@ async def run_fce(
|
||||
|
||||
status = flight_status.get("status", "")
|
||||
delay_minutes = flight_status.get("delay_minutes", 0)
|
||||
should_notify = status in ("DELAYED", "CANCELLED", "DIVERTED") and delay_minutes >= 10
|
||||
should_notify = status in ("CANCELLED", "DIVERTED") or (
|
||||
status == "DELAYED" and delay_minutes >= 10
|
||||
)
|
||||
|
||||
await emit("node_exit", node="triage", result={"should_notify": should_notify})
|
||||
|
||||
|
||||
@@ -185,6 +185,31 @@ CREW: list[CrewMember] = [
|
||||
CrewMember(crew_id="CR-1012", name="FA Garcia", role=CrewRole.FA,
|
||||
duty_hours_elapsed=11.5, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=10.0, next_scheduled_flight="UA881", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-1013", name="FA Herrera", role=CrewRole.FA,
|
||||
duty_hours_elapsed=9.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=11.0, next_scheduled_flight="UA881", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-1014", name="FA Novak", role=CrewRole.FA,
|
||||
duty_hours_elapsed=7.5, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=15.0, next_scheduled_flight="UA233", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-1015", name="FA Park", role=CrewRole.FA,
|
||||
duty_hours_elapsed=6.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=17.0, next_scheduled_flight="UA094", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-1016", name="FO Santos", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=4.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=20.0, next_scheduled_flight=None, base_hub="ORD"),
|
||||
# On-time flight crew
|
||||
CrewMember(crew_id="CR-1017", name="Capt. Walsh", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=3.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=22.0, next_scheduled_flight="UA1220", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-1018", name="FO Lindgren", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=3.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=21.0, next_scheduled_flight="UA1220", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-1019", name="Capt. Rivera", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=2.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=24.0, next_scheduled_flight="UA788", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-1020", name="FO Cheng", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=2.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=22.0, next_scheduled_flight="UA788", base_hub="ORD"),
|
||||
# Backup crew
|
||||
CrewMember(crew_id="CR-4421", name="Capt. Okafor", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=0.0, duty_hours_limit=14.0,
|
||||
|
||||
@@ -22,6 +22,7 @@ dependencies = [
|
||||
dev = [
|
||||
"pytest",
|
||||
"pytest-asyncio",
|
||||
"httpx",
|
||||
"ruff",
|
||||
]
|
||||
|
||||
@@ -34,3 +35,6 @@ select = ["E", "F", "I"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
markers = [
|
||||
"live: tests that hit external APIs (OpenMeteo, FAA)",
|
||||
]
|
||||
|
||||
82
tests/base.py
Normal file
82
tests/base.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Test base — two execution modes from one test suite.
|
||||
|
||||
Modes (set via CONTRACT_TEST_MODE env var):
|
||||
|
||||
inprocess (default) — httpx.AsyncClient with ASGI transport, full FastAPI
|
||||
stack in-process. Fast, no server needed.
|
||||
|
||||
live — httpx.AsyncClient against CONTRACT_TEST_URL.
|
||||
Tests against a running deployment (Kind, EC2, etc).
|
||||
|
||||
Usage:
|
||||
# In-process (default)
|
||||
pytest tests/
|
||||
|
||||
# Against live server
|
||||
CONTRACT_TEST_MODE=live CONTRACT_TEST_URL=http://unt.local.ar pytest tests/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import httpx
|
||||
import pytest_asyncio
|
||||
|
||||
|
||||
def get_mode() -> str:
|
||||
return os.getenv("CONTRACT_TEST_MODE", "inprocess")
|
||||
|
||||
|
||||
def get_base_url() -> str:
|
||||
return os.getenv("CONTRACT_TEST_URL", "http://localhost:8040")
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def client() -> httpx.AsyncClient:
|
||||
"""Async HTTP client for API tests."""
|
||||
if get_mode() == "live":
|
||||
async with httpx.AsyncClient(base_url=get_base_url(), timeout=30.0) as c:
|
||||
yield c
|
||||
else:
|
||||
from httpx import ASGITransport
|
||||
from api.main import app
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
|
||||
yield c
|
||||
|
||||
|
||||
# Alias for agent tests
|
||||
agent_client = client
|
||||
|
||||
|
||||
class ContractHelpers:
|
||||
"""Reusable assertion and utility methods."""
|
||||
|
||||
@staticmethod
|
||||
def assert_status(response: httpx.Response, expected: int):
|
||||
assert response.status_code == expected, (
|
||||
f"Expected {expected}, got {response.status_code}: {response.text[:200]}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def assert_has_fields(data: dict, *fields: str):
|
||||
for f in fields:
|
||||
assert f in data, f"Missing field: {f}. Keys: {list(data.keys())}"
|
||||
|
||||
@staticmethod
|
||||
def assert_is_list(data, min_length: int = 0):
|
||||
assert isinstance(data, list), f"Expected list, got {type(data)}"
|
||||
assert len(data) >= min_length, f"Expected >= {min_length} items, got {len(data)}"
|
||||
|
||||
@staticmethod
|
||||
async def poll_run(client: httpx.AsyncClient, run_id: str, timeout: int = 60) -> dict:
|
||||
"""Poll an agent run until completion."""
|
||||
deadline = asyncio.get_event_loop().time() + timeout
|
||||
while asyncio.get_event_loop().time() < deadline:
|
||||
res = await client.get(f"/agents/runs/{run_id}")
|
||||
data = res.json()
|
||||
if data.get("status") in ("completed", "error"):
|
||||
return data
|
||||
await asyncio.sleep(1)
|
||||
raise TimeoutError(f"Run {run_id} did not complete in {timeout}s")
|
||||
3
tests/conftest.py
Normal file
3
tests/conftest.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Root conftest — exports async client fixtures."""
|
||||
|
||||
from tests.base import client, agent_client # noqa: F401
|
||||
35
tests/endpoints.py
Normal file
35
tests/endpoints.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Centralized API endpoint paths — single source of truth."""
|
||||
|
||||
|
||||
class Endpoints:
|
||||
# Scenarios
|
||||
SCENARIOS = "/scenarios"
|
||||
SCENARIO_ACTIVE = "/scenarios/active"
|
||||
|
||||
# Scenario data
|
||||
DATA_FLIGHTS = "/scenarios/data/flights"
|
||||
DATA_CREW = "/scenarios/data/crew"
|
||||
DATA_CREW_NOTES = "/scenarios/data/crew-notes"
|
||||
DATA_MAINTENANCE = "/scenarios/data/maintenance"
|
||||
DATA_REBOOKINGS = "/scenarios/data/rebookings"
|
||||
|
||||
@staticmethod
|
||||
def flight(flight_id: str) -> str:
|
||||
return f"/scenarios/data/flights/{flight_id}"
|
||||
|
||||
@staticmethod
|
||||
def crew(crew_id: str) -> str:
|
||||
return f"/scenarios/data/crew/{crew_id}"
|
||||
|
||||
@staticmethod
|
||||
def crew_notes(flight_id: str) -> str:
|
||||
return f"/scenarios/data/crew-notes/{flight_id}"
|
||||
|
||||
# Agents
|
||||
AGENT_FCE = "/agents/fce"
|
||||
AGENT_HANDOVER = "/agents/handover"
|
||||
AGENT_RUNS = "/agents/runs"
|
||||
|
||||
@staticmethod
|
||||
def run(run_id: str) -> str:
|
||||
return f"/agents/runs/{run_id}"
|
||||
39
tests/helpers.py
Normal file
39
tests/helpers.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Reusable test data and setup helpers."""
|
||||
|
||||
import httpx
|
||||
|
||||
from tests.endpoints import Endpoints as E
|
||||
|
||||
|
||||
async def set_scenario(client: httpx.AsyncClient, scenario_id: str) -> dict:
|
||||
res = await client.put(E.SCENARIO_ACTIVE, json={"scenario_id": scenario_id})
|
||||
return res.json()
|
||||
|
||||
|
||||
async def get_flights(client: httpx.AsyncClient) -> list[dict]:
|
||||
return (await client.get(E.DATA_FLIGHTS)).json()
|
||||
|
||||
|
||||
async def get_disrupted_flights(client: httpx.AsyncClient) -> list[dict]:
|
||||
return [f for f in await get_flights(client) if f["status"] != "ON_TIME"]
|
||||
|
||||
|
||||
async def get_first_disrupted_flight_id(client: httpx.AsyncClient) -> str:
|
||||
flights = await get_disrupted_flights(client)
|
||||
assert len(flights) > 0, "No disrupted flights in current scenario"
|
||||
return flights[0]["flight_id"]
|
||||
|
||||
|
||||
async def trigger_fce(client: httpx.AsyncClient, flight_id: str) -> str:
|
||||
res = await client.post(E.AGENT_FCE, json={"flight_id": flight_id})
|
||||
data = res.json()
|
||||
assert "run_id" in data, f"FCE trigger failed: {data}"
|
||||
return data["run_id"]
|
||||
|
||||
|
||||
async def trigger_handover(client: httpx.AsyncClient, hubs: list[str] | None = None) -> str:
|
||||
body = {"hubs": hubs} if hubs else {}
|
||||
res = await client.post(E.AGENT_HANDOVER, json=body)
|
||||
data = res.json()
|
||||
assert "run_id" in data, f"Handover trigger failed: {data}"
|
||||
return data["run_id"]
|
||||
101
tests/test_agents.py
Normal file
101
tests/test_agents.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""Contract tests — agent execution via API."""
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.base import ContractHelpers as H
|
||||
from tests.endpoints import Endpoints as E
|
||||
from tests.helpers import set_scenario, trigger_fce, trigger_handover
|
||||
|
||||
|
||||
class TestFCEAgent:
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_returns_run_id(self, agent_client):
|
||||
await set_scenario(agent_client, "weather_disruption_ord")
|
||||
res = await agent_client.post(E.AGENT_FCE, json={"flight_id": "UA432"})
|
||||
H.assert_status(res, 200)
|
||||
H.assert_has_fields(res.json(), "run_id", "status")
|
||||
assert res.json()["status"] == "running"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_completes_with_notification(self, agent_client):
|
||||
await set_scenario(agent_client, "weather_disruption_ord")
|
||||
run_id = await trigger_fce(agent_client, "UA432")
|
||||
result = await H.poll_run(agent_client, run_id)
|
||||
|
||||
assert result["status"] == "completed"
|
||||
r = result["result"]
|
||||
H.assert_has_fields(r, "type", "status", "notification_text", "data_sources", "duration_ms")
|
||||
assert r["type"].endswith("_NOTIFICATION")
|
||||
assert len(r["notification_text"]) > 0
|
||||
assert r["duration_ms"] > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_includes_live_data(self, agent_client):
|
||||
await set_scenario(agent_client, "weather_disruption_ord")
|
||||
run_id = await trigger_fce(agent_client, "UA432")
|
||||
result = await H.poll_run(agent_client, run_id)
|
||||
|
||||
sources = result["result"]["data_sources"]
|
||||
assert "weather_live" in sources
|
||||
assert "faa_status_live" in sources
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notification_mentions_flight(self, agent_client):
|
||||
await set_scenario(agent_client, "weather_disruption_ord")
|
||||
run_id = await trigger_fce(agent_client, "UA432")
|
||||
result = await H.poll_run(agent_client, run_id)
|
||||
|
||||
text = result["result"]["notification_text"]
|
||||
assert "UA432" in text
|
||||
assert "ORD" in text or "SFO" in text
|
||||
|
||||
|
||||
class TestHandoverAgent:
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_returns_run_id(self, agent_client):
|
||||
res = await agent_client.post(E.AGENT_HANDOVER, json={})
|
||||
H.assert_status(res, 200)
|
||||
H.assert_has_fields(res.json(), "run_id", "status")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_completes_with_brief(self, agent_client):
|
||||
await set_scenario(agent_client, "weather_disruption_ord")
|
||||
run_id = await trigger_handover(agent_client, hubs=["ORD"])
|
||||
result = await H.poll_run(agent_client, run_id)
|
||||
|
||||
assert result["status"] == "completed"
|
||||
r = result["result"]
|
||||
H.assert_has_fields(r, "type", "brief_text", "summary", "items", "duration_ms")
|
||||
assert r["type"] == "HANDOVER_BRIEF"
|
||||
assert len(r["brief_text"]) > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_identifies_disruptions(self, agent_client):
|
||||
await set_scenario(agent_client, "weather_disruption_ord")
|
||||
run_id = await trigger_handover(agent_client, hubs=["ORD"])
|
||||
result = await H.poll_run(agent_client, run_id)
|
||||
|
||||
items = result["result"]["items"]
|
||||
assert len(items["immediate"]) > 0, "Should find immediate items in disruption scenario"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hub_filtering(self, agent_client):
|
||||
await set_scenario(agent_client, "weather_disruption_ord")
|
||||
run_id = await trigger_handover(agent_client, hubs=["ORD"])
|
||||
result = await H.poll_run(agent_client, run_id)
|
||||
|
||||
assert result["result"]["hubs"] == ["ORD"]
|
||||
|
||||
|
||||
class TestAgentRuns:
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_runs(self, agent_client):
|
||||
res = await agent_client.get(E.AGENT_RUNS)
|
||||
H.assert_status(res, 200)
|
||||
H.assert_is_list(res.json())
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unknown_run(self, agent_client):
|
||||
res = await agent_client.get(E.run("nonexistent"))
|
||||
H.assert_status(res, 200)
|
||||
assert "error" in res.json()
|
||||
172
tests/test_data_clients.py
Normal file
172
tests/test_data_clients.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""Tests for real data clients (OpenMeteo, FAA).
|
||||
|
||||
These hit live APIs — marked with pytest.mark for selective running.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp_servers.data.real.openmeteo import (
|
||||
HUB_COORDS,
|
||||
WMO_CODES,
|
||||
_interpolate_waypoints,
|
||||
_interpret_weather,
|
||||
get_weather_along_route,
|
||||
get_weather_forecast_hubs,
|
||||
)
|
||||
from mcp_servers.data.real.faa import get_airport_status, _parse_status_xml
|
||||
|
||||
|
||||
class TestOpenMeteoHelpers:
|
||||
def test_interpolate_waypoints_count(self):
|
||||
origin = (41.97, -87.91)
|
||||
dest = (37.62, -122.38)
|
||||
points = _interpolate_waypoints(origin, dest, n=2)
|
||||
assert len(points) == 2
|
||||
|
||||
def test_interpolate_waypoints_between(self):
|
||||
origin = (0.0, 0.0)
|
||||
dest = (30.0, 60.0)
|
||||
points = _interpolate_waypoints(origin, dest, n=2)
|
||||
for lat, lon in points:
|
||||
assert 0 < lat < 30
|
||||
assert 0 < lon < 60
|
||||
|
||||
def test_interpret_weather_clear(self):
|
||||
result = _interpret_weather(0)
|
||||
assert result["condition"] == "Clear sky"
|
||||
assert result["is_significant"] is False
|
||||
|
||||
def test_interpret_weather_thunderstorm(self):
|
||||
result = _interpret_weather(95)
|
||||
assert result["condition"] == "Thunderstorm"
|
||||
assert result["is_significant"] is True
|
||||
|
||||
def test_interpret_weather_unknown(self):
|
||||
result = _interpret_weather(999)
|
||||
assert "Unknown" in result["condition"]
|
||||
|
||||
def test_hub_coords_complete(self):
|
||||
for hub in ["ORD", "EWR", "IAH", "SFO", "DEN"]:
|
||||
assert hub in HUB_COORDS
|
||||
lat, lon = HUB_COORDS[hub]
|
||||
assert -90 <= lat <= 90
|
||||
assert -180 <= lon <= 180
|
||||
|
||||
|
||||
class TestFAAHelpers:
|
||||
def test_parse_status_xml_ground_stop(self):
|
||||
xml = """
|
||||
<AIRPORT_STATUS_INFORMATION>
|
||||
<Delay_type>
|
||||
<Name>Ground Stop Programs</Name>
|
||||
<Ground_Stop_List>
|
||||
<Program>
|
||||
<ARPT>SFO</ARPT>
|
||||
<Reason>thunderstorms</Reason>
|
||||
<End_Time>6:45 pm PDT</End_Time>
|
||||
</Program>
|
||||
</Ground_Stop_List>
|
||||
</Delay_type>
|
||||
</AIRPORT_STATUS_INFORMATION>
|
||||
"""
|
||||
result = _parse_status_xml(xml)
|
||||
assert "SFO" in result
|
||||
assert len(result["SFO"]) == 1
|
||||
assert result["SFO"][0]["type"] == "ground_stop"
|
||||
assert result["SFO"][0]["reason"] == "thunderstorms"
|
||||
|
||||
def test_parse_status_xml_gdp(self):
|
||||
xml = """
|
||||
<AIRPORT_STATUS_INFORMATION>
|
||||
<Delay_type>
|
||||
<Name>Ground Delay Programs</Name>
|
||||
<Ground_Delay_List>
|
||||
<Ground_Delay>
|
||||
<ARPT>EWR</ARPT>
|
||||
<Reason>wind</Reason>
|
||||
<Avg>45 minutes</Avg>
|
||||
<Max>1 hour</Max>
|
||||
</Ground_Delay>
|
||||
</Ground_Delay_List>
|
||||
</Delay_type>
|
||||
</AIRPORT_STATUS_INFORMATION>
|
||||
"""
|
||||
result = _parse_status_xml(xml)
|
||||
assert "EWR" in result
|
||||
assert result["EWR"][0]["type"] == "ground_delay_program"
|
||||
assert result["EWR"][0]["average_delay"] == "45 minutes"
|
||||
|
||||
def test_parse_status_xml_empty(self):
|
||||
xml = "<AIRPORT_STATUS_INFORMATION></AIRPORT_STATUS_INFORMATION>"
|
||||
result = _parse_status_xml(xml)
|
||||
assert result == {}
|
||||
|
||||
def test_parse_status_xml_multiple_airports(self):
|
||||
xml = """
|
||||
<AIRPORT_STATUS_INFORMATION>
|
||||
<Delay_type>
|
||||
<Name>Ground Stop Programs</Name>
|
||||
<Ground_Stop_List>
|
||||
<Program><ARPT>SFO</ARPT><Reason>weather</Reason></Program>
|
||||
<Program><ARPT>ORD</ARPT><Reason>volume</Reason></Program>
|
||||
</Ground_Stop_List>
|
||||
</Delay_type>
|
||||
</AIRPORT_STATUS_INFORMATION>
|
||||
"""
|
||||
result = _parse_status_xml(xml)
|
||||
assert "SFO" in result
|
||||
assert "ORD" in result
|
||||
|
||||
|
||||
@pytest.mark.live
|
||||
class TestOpenMeteoLive:
|
||||
"""Tests that hit the live OpenMeteo API."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_weather_along_route(self):
|
||||
result = await get_weather_along_route("ORD", "SFO")
|
||||
assert result["origin"] == "ORD"
|
||||
assert result["destination"] == "SFO"
|
||||
assert "waypoints" in result
|
||||
assert "origin" in result["waypoints"]
|
||||
assert "destination" in result["waypoints"]
|
||||
# Check a waypoint has weather data
|
||||
wp = result["waypoints"]["origin"]
|
||||
assert "temperature_c" in wp
|
||||
assert "weather" in wp
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_weather_unknown_airport(self):
|
||||
result = await get_weather_along_route("ORD", "FAKE")
|
||||
assert "error" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hub_forecasts(self):
|
||||
result = await get_weather_forecast_hubs()
|
||||
assert "hubs" in result
|
||||
for hub in ["ORD", "EWR", "IAH", "SFO", "DEN"]:
|
||||
assert hub in result["hubs"]
|
||||
hub_data = result["hubs"][hub]
|
||||
if "error" not in hub_data:
|
||||
assert "forecast" in hub_data
|
||||
assert len(hub_data["forecast"]) > 0
|
||||
|
||||
|
||||
@pytest.mark.live
|
||||
class TestFAALive:
|
||||
"""Tests that hit the live FAA API."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_airport_status_known(self):
|
||||
result = await get_airport_status("ORD")
|
||||
assert result["airport"] == "ORD"
|
||||
assert result["source"] == "faa_nasstatus_live"
|
||||
assert result["status"] in ("normal_operations", "delays_active", "status_unavailable")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_airport_status_no_delays(self):
|
||||
"""Small airport unlikely to have delays."""
|
||||
result = await get_airport_status("BTV")
|
||||
assert result["airport"] == "BTV"
|
||||
# Either normal or unavailable, but should not crash
|
||||
assert "status" in result
|
||||
206
tests/test_mcp_servers.py
Normal file
206
tests/test_mcp_servers.py
Normal file
@@ -0,0 +1,206 @@
|
||||
"""Tests for MCP servers — tools, resources, prompts via the MCP protocol."""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from fastmcp import Client
|
||||
|
||||
|
||||
def _client(module: str) -> Client:
|
||||
return Client({"mcpServers": {"default": {
|
||||
"command": "uv",
|
||||
"args": ["run", "python", "-m", module],
|
||||
}}})
|
||||
|
||||
|
||||
def _parse_result(result) -> any:
|
||||
"""Parse a fastmcp CallToolResult into Python data."""
|
||||
if hasattr(result, "content"):
|
||||
texts = [c.text for c in result.content if hasattr(c, "text")]
|
||||
elif isinstance(result, list):
|
||||
texts = [c.text for c in result if hasattr(c, "text")]
|
||||
else:
|
||||
return result
|
||||
|
||||
if not texts:
|
||||
return None
|
||||
combined = texts[0] if len(texts) == 1 else "[" + ",".join(texts) + "]"
|
||||
try:
|
||||
return json.loads(combined)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return combined
|
||||
|
||||
|
||||
class TestSharedServer:
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_tools(self):
|
||||
async with _client("mcp_servers.shared") as c:
|
||||
tools = await c.list_tools()
|
||||
names = [t.name for t in tools]
|
||||
assert "get_flight_status" in names
|
||||
assert "get_flight_details" in names
|
||||
assert "get_irregular_ops" in names
|
||||
assert "get_route_weather" in names
|
||||
assert "get_hub_forecasts" in names
|
||||
assert "get_airport_status" in names
|
||||
assert "get_airport_congestion" in names
|
||||
assert "get_maintenance_flags" in names
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flight_status_found(self):
|
||||
async with _client("mcp_servers.shared") as c:
|
||||
result = await c.call_tool("get_flight_status", {"flight_id": "UA432"})
|
||||
data = _parse_result(result)
|
||||
assert data["flight_id"] == "UA432"
|
||||
assert data["status"] == "DELAYED"
|
||||
assert data["delay_minutes"] == 55
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flight_status_not_found(self):
|
||||
async with _client("mcp_servers.shared") as c:
|
||||
result = await c.call_tool("get_flight_status", {"flight_id": "FAKE999"})
|
||||
data = _parse_result(result)
|
||||
assert "error" in data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_irregular_ops(self):
|
||||
async with _client("mcp_servers.shared") as c:
|
||||
result = await c.call_tool("get_irregular_ops", {"hub": "ORD"})
|
||||
data = _parse_result(result)
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 4
|
||||
for f in data:
|
||||
assert f["irrop_type"] in ("DELAYED", "CANCELLED", "DIVERTED")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_maintenance_flags(self):
|
||||
async with _client("mcp_servers.shared") as c:
|
||||
result = await c.call_tool("get_maintenance_flags", {"aircraft_tail": "N78501"})
|
||||
data = _parse_result(result)
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 1
|
||||
assert "system" in data[0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_maintenance_flags_empty(self):
|
||||
async with _client("mcp_servers.shared") as c:
|
||||
result = await c.call_tool("get_maintenance_flags", {"aircraft_tail": "FAKE"})
|
||||
data = _parse_result(result)
|
||||
# Empty list may come back as [] or as "[]" string
|
||||
if isinstance(data, list):
|
||||
assert len(data) == 0
|
||||
else:
|
||||
assert data == "[]" or data is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_prompts(self):
|
||||
async with _client("mcp_servers.shared") as c:
|
||||
prompts = await c.list_prompts()
|
||||
names = [p.name for p in prompts]
|
||||
assert "delay_explainer" in names
|
||||
|
||||
|
||||
class TestOpsServer:
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_tools(self):
|
||||
async with _client("mcp_servers.ops") as c:
|
||||
tools = await c.list_tools()
|
||||
names = [t.name for t in tools]
|
||||
assert "get_crew_notes" in names
|
||||
assert "get_crew_duty_status" in names
|
||||
assert "get_pending_rebookings" in names
|
||||
assert "generate_narrative" in names
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_crew_notes(self):
|
||||
async with _client("mcp_servers.ops") as c:
|
||||
result = await c.call_tool("get_crew_notes", {"flight_id": "UA432"})
|
||||
data = _parse_result(result)
|
||||
assert isinstance(data, list)
|
||||
assert len(data) > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_crew_duty_status(self):
|
||||
async with _client("mcp_servers.ops") as c:
|
||||
result = await c.call_tool(
|
||||
"get_crew_duty_status",
|
||||
{"crew_ids": ["CR-1001", "CR-1003"]}
|
||||
)
|
||||
data = _parse_result(result)
|
||||
assert isinstance(data, list)
|
||||
assert len(data) == 2
|
||||
for c_data in data:
|
||||
assert "hours_until_limit" in c_data
|
||||
assert "at_risk" in c_data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_crew_at_risk_detection(self):
|
||||
async with _client("mcp_servers.ops") as c:
|
||||
result = await c.call_tool(
|
||||
"get_crew_duty_status",
|
||||
{"crew_ids": ["CR-1003"]}
|
||||
)
|
||||
data = _parse_result(result)
|
||||
crew = data[0] if isinstance(data, list) else data
|
||||
assert crew["at_risk"] is True
|
||||
assert crew["hours_until_limit"] <= 2.0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pending_rebookings(self):
|
||||
async with _client("mcp_servers.ops") as c:
|
||||
result = await c.call_tool("get_pending_rebookings", {"hub": "ORD"})
|
||||
data = _parse_result(result)
|
||||
assert isinstance(data, list)
|
||||
assert len(data) > 0
|
||||
urgencies = [r["urgency"] for r in data]
|
||||
assert urgencies[0] == "HIGH"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_resources(self):
|
||||
async with _client("mcp_servers.ops") as c:
|
||||
resources = await c.list_resources()
|
||||
uris = [str(r.uri) for r in resources]
|
||||
assert "ops://crew/roster" in uris
|
||||
assert "ops://handover/latest" in uris
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_prompts(self):
|
||||
async with _client("mcp_servers.ops") as c:
|
||||
prompts = await c.list_prompts()
|
||||
names = [p.name for p in prompts]
|
||||
assert "handover_brief" in names
|
||||
|
||||
|
||||
class TestPassengerServer:
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_tools(self):
|
||||
async with _client("mcp_servers.passenger") as c:
|
||||
tools = await c.list_tools()
|
||||
names = [t.name for t in tools]
|
||||
assert "generate_notification" in names
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_notification(self):
|
||||
async with _client("mcp_servers.passenger") as c:
|
||||
result = await c.call_tool("generate_notification", {
|
||||
"context": {
|
||||
"flight_id": "UA432",
|
||||
"origin": "ORD",
|
||||
"destination": "SFO",
|
||||
"status": "DELAYED",
|
||||
"delay_minutes": 55,
|
||||
"delay_cause": "WEATHER",
|
||||
"gate": "H14",
|
||||
}
|
||||
})
|
||||
text = _parse_result(result)
|
||||
assert "UA432" in text
|
||||
assert "DELAYED" in text or "delayed" in text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_prompts(self):
|
||||
async with _client("mcp_servers.passenger") as c:
|
||||
prompts = await c.list_prompts()
|
||||
names = [p.name for p in prompts]
|
||||
assert "passenger_notification" in names
|
||||
150
tests/test_models.py
Normal file
150
tests/test_models.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""Tests for data models and scenario manager."""
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp_servers.data.models import (
|
||||
HUBS,
|
||||
CrewMember,
|
||||
CrewRole,
|
||||
DelayCause,
|
||||
FlightData,
|
||||
FlightStatus,
|
||||
HubInfo,
|
||||
MELItem,
|
||||
MPStatus,
|
||||
Passenger,
|
||||
RebookingCase,
|
||||
)
|
||||
from mcp_servers.data.scenarios.manager import ScenarioManager, SCENARIO_MODULES
|
||||
|
||||
|
||||
class TestModels:
|
||||
def test_flight_data_serializes(self):
|
||||
from datetime import datetime, timezone
|
||||
|
||||
f = FlightData(
|
||||
flight_id="UA100",
|
||||
origin="ORD",
|
||||
destination="SFO",
|
||||
scheduled_departure=datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc),
|
||||
scheduled_arrival=datetime(2026, 1, 1, 15, 0, tzinfo=timezone.utc),
|
||||
status=FlightStatus.DELAYED,
|
||||
delay_minutes=30,
|
||||
delay_cause=DelayCause.WEATHER,
|
||||
aircraft_tail="N12345",
|
||||
gate="A1",
|
||||
)
|
||||
d = f.model_dump(mode="json")
|
||||
assert d["flight_id"] == "UA100"
|
||||
assert d["status"] == "DELAYED"
|
||||
assert d["delay_cause"] == "WEATHER"
|
||||
|
||||
def test_crew_member_serializes(self):
|
||||
c = CrewMember(
|
||||
crew_id="CR-1",
|
||||
name="Test Pilot",
|
||||
role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=10.0,
|
||||
duty_hours_limit=14.0,
|
||||
rest_hours_since_last=12.0,
|
||||
base_hub="ORD",
|
||||
)
|
||||
d = c.model_dump(mode="json")
|
||||
assert d["role"] == "CAPTAIN"
|
||||
assert d["duty_hours_elapsed"] == 10.0
|
||||
|
||||
def test_mp_status_values(self):
|
||||
assert MPStatus.GLOBAL_SERVICES.value == "GLOBAL_SERVICES"
|
||||
assert MPStatus.K1.value == "1K"
|
||||
|
||||
def test_hubs_reference_data(self):
|
||||
assert "ORD" in HUBS
|
||||
assert "EWR" in HUBS
|
||||
assert "IAH" in HUBS
|
||||
assert "SFO" in HUBS
|
||||
assert "DEN" in HUBS
|
||||
assert len(HUBS) == 5
|
||||
|
||||
ord = HUBS["ORD"]
|
||||
assert ord.code == "ORD"
|
||||
assert ord.city == "Chicago"
|
||||
assert ord.latitude == pytest.approx(41.97, abs=0.01)
|
||||
assert ord.gates > 0
|
||||
assert ord.runways > 0
|
||||
|
||||
|
||||
class TestScenarioManager:
|
||||
def test_list_scenarios(self):
|
||||
mgr = ScenarioManager()
|
||||
scenarios = mgr.list_scenarios()
|
||||
assert len(scenarios) == len(SCENARIO_MODULES)
|
||||
ids = [s["scenario_id"] for s in scenarios]
|
||||
assert "normal_ops" in ids
|
||||
assert "weather_disruption_ord" in ids
|
||||
assert "maintenance_delay_sfo" in ids
|
||||
assert "crew_swap_ewr" in ids
|
||||
|
||||
def test_default_active_scenario(self):
|
||||
mgr = ScenarioManager()
|
||||
assert mgr.active_id == "weather_disruption_ord"
|
||||
|
||||
def test_switch_scenario(self):
|
||||
mgr = ScenarioManager()
|
||||
result = mgr.set_active("normal_ops")
|
||||
assert mgr.active_id == "normal_ops"
|
||||
assert result["scenario_id"] == "normal_ops"
|
||||
|
||||
def test_switch_invalid_scenario(self):
|
||||
mgr = ScenarioManager()
|
||||
with pytest.raises(ValueError, match="Unknown scenario"):
|
||||
mgr.set_active("nonexistent")
|
||||
|
||||
def test_flights_per_scenario(self):
|
||||
mgr = ScenarioManager()
|
||||
for sid in SCENARIO_MODULES:
|
||||
mgr.set_active(sid)
|
||||
assert len(mgr.flights) > 0, f"No flights in {sid}"
|
||||
for f in mgr.flights:
|
||||
assert f.flight_id, f"Empty flight_id in {sid}"
|
||||
assert f.origin, f"Empty origin in {sid}"
|
||||
|
||||
def test_crew_per_scenario(self):
|
||||
mgr = ScenarioManager()
|
||||
for sid in SCENARIO_MODULES:
|
||||
mgr.set_active(sid)
|
||||
assert len(mgr.crew) > 0, f"No crew in {sid}"
|
||||
for c in mgr.crew:
|
||||
assert c.crew_id
|
||||
assert c.duty_hours_limit > 0
|
||||
|
||||
def test_scenario_internal_consistency(self):
|
||||
"""Crew IDs on flights should exist in the crew roster."""
|
||||
mgr = ScenarioManager()
|
||||
for sid in SCENARIO_MODULES:
|
||||
mgr.set_active(sid)
|
||||
crew_ids = {c.crew_id for c in mgr.crew}
|
||||
for f in mgr.flights:
|
||||
for cid in f.crew_ids:
|
||||
assert cid in crew_ids, (
|
||||
f"Flight {f.flight_id} in {sid} references crew {cid} "
|
||||
f"not in roster"
|
||||
)
|
||||
|
||||
def test_disrupted_flights_have_cause(self):
|
||||
mgr = ScenarioManager()
|
||||
for sid in SCENARIO_MODULES:
|
||||
mgr.set_active(sid)
|
||||
for f in mgr.flights:
|
||||
if f.status != FlightStatus.ON_TIME:
|
||||
assert f.delay_cause is not None, (
|
||||
f"Flight {f.flight_id} in {sid} is {f.status} "
|
||||
f"but has no delay_cause"
|
||||
)
|
||||
|
||||
def test_metadata_counts(self):
|
||||
mgr = ScenarioManager()
|
||||
mgr.set_active("weather_disruption_ord")
|
||||
meta = mgr.get_metadata()
|
||||
assert meta["flight_count"] == len(mgr.flights)
|
||||
disrupted = sum(1 for f in mgr.flights if f.status != FlightStatus.ON_TIME)
|
||||
assert meta["disrupted_flights"] == disrupted
|
||||
133
tests/test_scenario_data.py
Normal file
133
tests/test_scenario_data.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""Contract tests — scenario data CRUD."""
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.base import ContractHelpers as H
|
||||
from tests.endpoints import Endpoints as E
|
||||
from tests.helpers import set_scenario
|
||||
|
||||
|
||||
class TestFlights:
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_flights(self, client):
|
||||
await set_scenario(client, "weather_disruption_ord")
|
||||
res = await client.get(E.DATA_FLIGHTS)
|
||||
H.assert_status(res, 200)
|
||||
data = res.json()
|
||||
H.assert_is_list(data, min_length=1)
|
||||
H.assert_has_fields(data[0], "flight_id", "origin", "destination", "status")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flights_change_with_scenario(self, client):
|
||||
await set_scenario(client, "weather_disruption_ord")
|
||||
ord_ids = {f["flight_id"] for f in (await client.get(E.DATA_FLIGHTS)).json()}
|
||||
|
||||
await set_scenario(client, "crew_swap_ewr")
|
||||
ewr_ids = {f["flight_id"] for f in (await client.get(E.DATA_FLIGHTS)).json()}
|
||||
|
||||
assert ord_ids != ewr_ids, "Different scenarios should have different flights"
|
||||
await set_scenario(client, "weather_disruption_ord")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_flight_delay(self, client):
|
||||
await set_scenario(client, "weather_disruption_ord")
|
||||
res = await client.patch(E.flight("UA432"), json={"delay_minutes": 120})
|
||||
H.assert_status(res, 200)
|
||||
assert res.json()["delay_minutes"] == 120
|
||||
|
||||
flights = (await client.get(E.DATA_FLIGHTS)).json()
|
||||
ua432 = next(f for f in flights if f["flight_id"] == "UA432")
|
||||
assert ua432["delay_minutes"] == 120
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_flight_status(self, client):
|
||||
await set_scenario(client, "weather_disruption_ord")
|
||||
res = await client.patch(E.flight("UA432"), json={"status": "CANCELLED"})
|
||||
H.assert_status(res, 200)
|
||||
assert res.json()["status"] == "CANCELLED"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_unknown_flight(self, client):
|
||||
res = await client.patch(E.flight("FAKE"), json={"delay_minutes": 10})
|
||||
H.assert_status(res, 200)
|
||||
assert "error" in res.json()
|
||||
|
||||
|
||||
class TestCrew:
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_crew(self, client):
|
||||
await set_scenario(client, "weather_disruption_ord")
|
||||
res = await client.get(E.DATA_CREW)
|
||||
H.assert_status(res, 200)
|
||||
data = res.json()
|
||||
H.assert_is_list(data, min_length=1)
|
||||
H.assert_has_fields(data[0], "crew_id", "name", "role", "hours_until_limit", "at_risk")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_crew_duty_hours(self, client):
|
||||
await set_scenario(client, "weather_disruption_ord")
|
||||
res = await client.patch(E.crew("CR-1001"), json={"duty_hours_elapsed": 13.5})
|
||||
H.assert_status(res, 200)
|
||||
data = res.json()
|
||||
assert data["duty_hours_elapsed"] == 13.5
|
||||
assert data["at_risk"] is True
|
||||
assert data["hours_until_limit"] == 0.5
|
||||
|
||||
|
||||
class TestCrewNotes:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_notes(self, client):
|
||||
await set_scenario(client, "weather_disruption_ord")
|
||||
res = await client.get(E.DATA_CREW_NOTES)
|
||||
H.assert_status(res, 200)
|
||||
data = res.json()
|
||||
assert isinstance(data, dict)
|
||||
assert "UA432" in data
|
||||
assert len(data["UA432"]) > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_notes(self, client):
|
||||
await set_scenario(client, "weather_disruption_ord")
|
||||
new_notes = ["Test note alpha", "Test note beta"]
|
||||
res = await client.put(E.crew_notes("UA432"), json={"notes": new_notes})
|
||||
H.assert_status(res, 200)
|
||||
assert res.json()["notes"] == new_notes
|
||||
|
||||
all_notes = (await client.get(E.DATA_CREW_NOTES)).json()
|
||||
assert all_notes["UA432"] == new_notes
|
||||
|
||||
|
||||
class TestMaintenance:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_maintenance(self, client):
|
||||
await set_scenario(client, "weather_disruption_ord")
|
||||
res = await client.get(E.DATA_MAINTENANCE)
|
||||
H.assert_status(res, 200)
|
||||
assert isinstance(res.json(), dict)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_maintenance_has_mel_items(self, client):
|
||||
await set_scenario(client, "maintenance_delay_sfo")
|
||||
data = (await client.get(E.DATA_MAINTENANCE)).json()
|
||||
assert len(data) > 0
|
||||
for tail, items in data.items():
|
||||
for item in items:
|
||||
H.assert_has_fields(item, "mel_id", "system", "description")
|
||||
|
||||
|
||||
class TestRebookings:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_rebookings(self, client):
|
||||
await set_scenario(client, "weather_disruption_ord")
|
||||
res = await client.get(E.DATA_REBOOKINGS)
|
||||
H.assert_status(res, 200)
|
||||
data = res.json()
|
||||
H.assert_is_list(data, min_length=1)
|
||||
H.assert_has_fields(data[0], "pax_id", "name", "urgency", "original_flight")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_in_normal_ops(self, client):
|
||||
await set_scenario(client, "normal_ops")
|
||||
data = (await client.get(E.DATA_REBOOKINGS)).json()
|
||||
assert len(data) == 0
|
||||
await set_scenario(client, "weather_disruption_ord")
|
||||
53
tests/test_scenarios.py
Normal file
53
tests/test_scenarios.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Contract tests — scenario management."""
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.base import ContractHelpers as H
|
||||
from tests.endpoints import Endpoints as E
|
||||
|
||||
|
||||
class TestListScenarios:
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_list(self, client):
|
||||
res = await client.get(E.SCENARIOS)
|
||||
H.assert_status(res, 200)
|
||||
H.assert_is_list(res.json(), min_length=4)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_each_has_metadata(self, client):
|
||||
data = (await client.get(E.SCENARIOS)).json()
|
||||
for s in data:
|
||||
H.assert_has_fields(s, "scenario_id", "name", "description", "hubs", "flight_count")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_known_scenarios_present(self, client):
|
||||
ids = [s["scenario_id"] for s in (await client.get(E.SCENARIOS)).json()]
|
||||
assert "normal_ops" in ids
|
||||
assert "weather_disruption_ord" in ids
|
||||
assert "maintenance_delay_sfo" in ids
|
||||
assert "crew_swap_ewr" in ids
|
||||
|
||||
|
||||
class TestActiveScenario:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_active(self, client):
|
||||
res = await client.get(E.SCENARIO_ACTIVE)
|
||||
H.assert_status(res, 200)
|
||||
H.assert_has_fields(res.json(), "scenario_id", "name")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_switch_scenario(self, client):
|
||||
res = await client.put(E.SCENARIO_ACTIVE, json={"scenario_id": "normal_ops"})
|
||||
H.assert_status(res, 200)
|
||||
assert res.json()["scenario_id"] == "normal_ops"
|
||||
|
||||
res = await client.get(E.SCENARIO_ACTIVE)
|
||||
assert res.json()["scenario_id"] == "normal_ops"
|
||||
|
||||
await client.put(E.SCENARIO_ACTIVE, json={"scenario_id": "weather_disruption_ord"})
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_switch_invalid(self, client):
|
||||
res = await client.put(E.SCENARIO_ACTIVE, json={"scenario_id": "nonexistent"})
|
||||
H.assert_status(res, 200)
|
||||
assert "error" in res.json()
|
||||
98
uv.lock
generated
98
uv.lock
generated
@@ -1880,6 +1880,56 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stellar-ops"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "anthropic" },
|
||||
{ name = "boto3" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "fastmcp" },
|
||||
{ name = "httpx" },
|
||||
{ name = "langchain-anthropic" },
|
||||
{ name = "langchain-aws" },
|
||||
{ name = "langfuse" },
|
||||
{ name = "langgraph" },
|
||||
{ name = "mcp", extra = ["cli"] },
|
||||
{ name = "pydantic" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "httpx" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "anthropic" },
|
||||
{ name = "boto3" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "fastmcp", specifier = ">=3.0" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx", marker = "extra == 'dev'" },
|
||||
{ name = "langchain-anthropic" },
|
||||
{ name = "langchain-aws" },
|
||||
{ name = "langfuse" },
|
||||
{ name = "langgraph" },
|
||||
{ name = "mcp", extras = ["cli"] },
|
||||
{ name = "pydantic", specifier = ">=2.0" },
|
||||
{ name = "pytest", marker = "extra == 'dev'" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'dev'" },
|
||||
{ name = "ruff", marker = "extra == 'dev'" },
|
||||
{ name = "uvicorn", extras = ["standard"] },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
[[package]]
|
||||
name = "tenacity"
|
||||
version = "9.1.4"
|
||||
@@ -1934,54 +1984,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/e1/7ec67882ad8fc9f86384bef6421fa252c9cbe5744f8df6ce77afc9eca1f5/uncalled_for-0.3.1-py3-none-any.whl", hash = "sha256:074cdc92da8356278f93d0ded6f2a66dd883dbecaf9bc89437646ee2289cc200", size = 11361, upload-time = "2026-04-07T13:05:05.341Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "united-ops"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "anthropic" },
|
||||
{ name = "boto3" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "fastmcp" },
|
||||
{ name = "httpx" },
|
||||
{ name = "langchain-anthropic" },
|
||||
{ name = "langchain-aws" },
|
||||
{ name = "langfuse" },
|
||||
{ name = "langgraph" },
|
||||
{ name = "mcp", extra = ["cli"] },
|
||||
{ name = "pydantic" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "anthropic" },
|
||||
{ name = "boto3" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "fastmcp", specifier = ">=3.0" },
|
||||
{ name = "httpx" },
|
||||
{ name = "langchain-anthropic" },
|
||||
{ name = "langchain-aws" },
|
||||
{ name = "langfuse" },
|
||||
{ name = "langgraph" },
|
||||
{ name = "mcp", extras = ["cli"] },
|
||||
{ name = "pydantic", specifier = ">=2.0" },
|
||||
{ name = "pytest", marker = "extra == 'dev'" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'dev'" },
|
||||
{ name = "ruff", marker = "extra == 'dev'" },
|
||||
{ name = "uvicorn", extras = ["standard"] },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
|
||||
Reference in New Issue
Block a user