mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
* fix(mcp): keep the gateway handler off the SDK path when no tool is an MCP tool completion(), responses() and anthropic messages imported the MCP gateway handler whenever tools were passed, and responses() did so on every call. Since the handler began importing the MCP catalog at module level in #42568, that import needs the optional mcp package, so a base SDK install failed with ModuleNotFoundError on any function-tool call and on every responses() call A new import-safe pre-check skips the handler unless some tool is a dict of type mcp, which is the only shape the handler's gateway decision can accept. MCP gateway tools still reach the handler as before Adds in-process regression tests for the three entry points and a function-tool completion to the base SDK install check * fix(mcp): leave one-shot tool iterables unread in the gateway pre-check responses() declares tools as an Iterable and the handler scans the same object a second time, so a pre-check that read a generator would hand the handler one tool fewer. The pre-check now scans only collections and reports anything else as possibly naming the gateway, which keeps the previous path The Anthropic messages regression test now mocks the httpx layer with respx and turns off the aiohttp transport, instead of replacing AsyncHTTPHandler * fix(mcp): scan only lists and tuples in the gateway pre-check The pre-check read any Collection, but a Collection that yields its items only once would still be exhausted by the handler's second scan. Only the list and tuple builtins are guaranteed to iterate the same way twice, so they are the only inputs read now. Everything else keeps the previous path and is reported as possibly naming the gateway The unread-input test now covers a generator, an iterator, a one-shot collection and a deque * fix(mcp): scan only exact list and tuple types in the gateway pre-check isinstance accepted list subclasses, and a subclass that iterates only once would still be consumed by the handler's second scan. The fast path now reads only objects whose type is exactly list or tuple. Every other input, subclasses included, keeps the previous path and is reported as possibly naming the gateway The unread-input test gains a one-shot list subclass and a tuple subclass * fix(sdk): decide MCP gateway routing without importing the proxy handler The pre-check now holds the handler's exact gateway test, so remote MCP tools, mcp 1.x installs and any tools iterable stay off the handler, and the base SDK check calls responses() with a remote MCP tool. * fix(sdk): send /mcp/<name> remote MCP URLs to the provider when the gateway registry cannot load A path-only /mcp/<name> match now counts as a gateway tool only when the MCP server registry imports, so SDK installs without the proxy extras pass remote MCP servers like Zapier to the provider. Also drops a dead test patch. * test(sdk): send a /mcp/<name> remote MCP tool in the base SDK install check * style(sdk): drop docstrings from the MCP gateway pre-check and its tests * test(mcp): cover /mcp/<name> gateway routing on the proxy and Messages tools on a base SDK install --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
458 lines
20 KiB
Python
458 lines
20 KiB
Python
"""Check installed SDK HTTP behavior against a recording loopback upstream."""
|
|
|
|
import asyncio
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import struct
|
|
import zlib
|
|
from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, Sequence
|
|
from functools import partial
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from queue import Queue
|
|
from threading import Event, Thread
|
|
from typing import Final, cast
|
|
from unittest import SkipTest, skipIf
|
|
from unittest.mock import patch
|
|
|
|
from pydantic import BaseModel, JsonValue, TypeAdapter
|
|
|
|
ENVIRONMENT: Final = {"LITELLM_LOCAL_MODEL_COST_MAP": "True", "PYTHON_DOTENV_DISABLED": "1"}
|
|
|
|
with patch.dict(os.environ, ENVIRONMENT):
|
|
import litellm
|
|
|
|
RESPONSES: Final[Queue[tuple[int, bytes]]] = Queue()
|
|
REQUESTS: Final[Queue[tuple[str, dict[str, str], bytes]]] = Queue()
|
|
ARRIVED: Final = Event()
|
|
RELEASE: Final = Event()
|
|
JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
|
|
MESSAGES: Final = [{"role": "user", "content": "ping"}]
|
|
CHAT: Final = {
|
|
"id": "chat-http-check",
|
|
"object": "chat.completion",
|
|
"created": 1,
|
|
"model": "gpt-4o",
|
|
"choices": [{"index": 0, "message": {"role": "assistant", "content": "pong"}, "finish_reason": "stop"}],
|
|
"usage": {"prompt_tokens": 3, "completion_tokens": 2, "total_tokens": 5},
|
|
}
|
|
|
|
|
|
class RecordingUpstream(BaseHTTPRequestHandler):
|
|
def log_message(self, format: str, *args: object) -> None:
|
|
pass
|
|
|
|
def do_POST(self) -> None:
|
|
headers: Final = {name.lower(): value for name, value in self.headers.items()}
|
|
REQUESTS.put((self.path, headers, self.rfile.read(int(self.headers["Content-Length"]))))
|
|
status, body = RESPONSES.get(timeout=10)
|
|
ARRIVED.set()
|
|
if status == 0:
|
|
RELEASE.wait(timeout=10)
|
|
return
|
|
self.send_response(status)
|
|
self.send_header(
|
|
"Content-Type",
|
|
"text/event-stream"
|
|
if body.startswith((b"data:", b"event:"))
|
|
else "application/vnd.amazon.eventstream"
|
|
if body[:1] == b"\x00"
|
|
else "application/json",
|
|
)
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
|
|
def enqueue(body: object, status: int = 200) -> None:
|
|
RESPONSES.put((status, json.dumps(body).encode()))
|
|
|
|
|
|
def enqueue_stream() -> None:
|
|
chunk: Final = {**CHAT, "object": "chat.completion.chunk", "choices": [{"index": 0, "delta": {"content": "pong"}}]}
|
|
RESPONSES.put((200, f"data: {json.dumps(chunk)}\n\ndata: [DONE]\n\n".encode()))
|
|
|
|
|
|
def bedrock_event(event: str, payload: object) -> bytes:
|
|
def encode_header(name: str, value: str) -> bytes:
|
|
return bytes([len(name)]) + name.encode() + b"\x07" + struct.pack(">H", len(value)) + value.encode()
|
|
|
|
headers: Final = encode_header(":message-type", "event") + encode_header(":event-type", event)
|
|
content: Final = json.dumps(payload).encode()
|
|
prelude: Final = struct.pack(">II", len(headers) + len(content) + 16, len(headers))
|
|
frame: Final = prelude + struct.pack(">I", zlib.crc32(prelude)) + headers + content
|
|
return frame + struct.pack(">I", zlib.crc32(frame))
|
|
|
|
|
|
def payload(response: object) -> Mapping[str, JsonValue]:
|
|
assert isinstance(response, BaseModel), response
|
|
return JSON_OBJECT.validate_json(response.model_dump_json(exclude_none=True))
|
|
|
|
|
|
def check_tools(base: str, asynchronous: bool = False) -> None:
|
|
tools: Final = [
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "lookup",
|
|
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}},
|
|
},
|
|
}
|
|
]
|
|
call: Final = {
|
|
"id": "call-http-check",
|
|
"type": "function",
|
|
"function": {"name": "lookup", "arguments": '{"city":"Paris"}'},
|
|
}
|
|
arguments: Final = dict(model="openai/gpt-6", messages=MESSAGES, api_key="test-key", api_base=base + "/v1")
|
|
choices: Final = [
|
|
{"index": 0, "message": {"role": "assistant", "tool_calls": [call]}, "finish_reason": "tool_calls"}
|
|
]
|
|
enqueue({**CHAT, "choices": choices})
|
|
result: Final = (
|
|
asyncio.run(litellm.acompletion(**arguments, tools=tools))
|
|
if asynchronous
|
|
else litellm.completion(**arguments, tools=tools)
|
|
)
|
|
assert payload(result)["choices"] == [{**choices[0], "provider_specific_fields": {}}]
|
|
path, headers, body = REQUESTS.get(timeout=10)
|
|
assert path == "/v1/chat/completions"
|
|
assert headers["authorization"] == "Bearer test-key"
|
|
assert json.loads(body) == {"model": "gpt-6", "messages": MESSAGES, "tools": tools}
|
|
|
|
|
|
def check_responses(base: str, asynchronous: bool = False) -> None:
|
|
output: Final[Sequence[JsonValue]] = [
|
|
{
|
|
"id": "msg-http-check",
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"status": "completed",
|
|
"content": [{"type": "output_text", "text": "pong", "annotations": []}],
|
|
}
|
|
]
|
|
enqueue(
|
|
{
|
|
"id": "resp-http-check",
|
|
"object": "response",
|
|
"created_at": 1,
|
|
"status": "completed",
|
|
"model": "gpt-6",
|
|
"output": output,
|
|
"usage": {"input_tokens": 3, "output_tokens": 2, "total_tokens": 5},
|
|
}
|
|
)
|
|
tools: Final = [
|
|
{"type": "mcp", "server_label": "docs", "server_url": "https://mcp.example.com/sse"},
|
|
{"type": "mcp", "server_label": "repos", "server_url": "https://mcp.example.com/mcp/repos"},
|
|
]
|
|
arguments: Final = dict(model="openai/gpt-6", input="ping", tools=tools, api_key="test-key", api_base=base + "/v1")
|
|
response: Final = asyncio.run(litellm.aresponses(**arguments)) if asynchronous else litellm.responses(**arguments)
|
|
assert payload(response)["output"] == output
|
|
assert payload(response)["usage"] == {"input_tokens": 3, "output_tokens": 2, "total_tokens": 5}
|
|
path, headers, body = REQUESTS.get(timeout=10)
|
|
assert path == "/v1/responses"
|
|
assert headers["authorization"] == "Bearer test-key"
|
|
assert json.loads(body) == {"model": "gpt-6", "input": "ping", "tools": tools}
|
|
|
|
|
|
def check_messages_tools(base: str) -> None:
|
|
tools: Final = [
|
|
{
|
|
"name": "lookup",
|
|
"description": "Look up the forecast for a city",
|
|
"input_schema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
|
|
}
|
|
]
|
|
content: Final = [{"type": "tool_use", "id": "toolu-http-check", "name": "lookup", "input": {"city": "Paris"}}]
|
|
enqueue(
|
|
{
|
|
"id": "msg-tools-http-check",
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"model": "claude-sonnet-5-5",
|
|
"content": content,
|
|
"stop_reason": "tool_use",
|
|
"stop_sequence": None,
|
|
"usage": {"input_tokens": 3, "output_tokens": 2},
|
|
}
|
|
)
|
|
|
|
async def send() -> object:
|
|
create: Final = cast( # cast-ok: SDK facade exposes untyped kwargs
|
|
Callable[..., Awaitable[object]], litellm.anthropic.messages.acreate
|
|
)
|
|
return await create(
|
|
model="anthropic/claude-sonnet-5-5",
|
|
messages=MESSAGES,
|
|
tools=tools,
|
|
max_tokens=16,
|
|
api_key="test-key",
|
|
api_base=base,
|
|
)
|
|
|
|
response: Final = JSON_OBJECT.validate_python(asyncio.run(send()))
|
|
assert response["content"] == content, response
|
|
path, headers, body = REQUESTS.get(timeout=10)
|
|
assert path == "/v1/messages"
|
|
assert headers["x-api-key"] == "test-key"
|
|
sent: Final = JSON_OBJECT.validate_json(body)
|
|
assert (sent["model"], sent["messages"], sent["tools"]) == ("claude-sonnet-5-5", MESSAGES, tools), sent
|
|
|
|
|
|
@skipIf(importlib.util.find_spec("fastapi") is None, "BUG: LIT-9333 cache hits import proxy-only fastapi")
|
|
def check_cache_hit(base: str) -> None:
|
|
from litellm.caching.caching import Cache
|
|
from litellm.types.caching import LiteLLMCacheType
|
|
|
|
arguments: Final = dict(
|
|
model="openai/gpt-6",
|
|
messages=MESSAGES,
|
|
api_key="test-key",
|
|
api_base=base + "/v1",
|
|
caching=True,
|
|
timeout=3,
|
|
num_retries=0,
|
|
max_retries=0,
|
|
)
|
|
with patch.object(litellm, "cache", Cache(type=LiteLLMCacheType.LOCAL)):
|
|
enqueue(CHAT)
|
|
first: Final = litellm.completion(**arguments)
|
|
assert REQUESTS.get(timeout=10)[0] == "/v1/chat/completions"
|
|
cached: Final = litellm.completion(**arguments)
|
|
assert payload(cached) == payload(first)
|
|
assert payload(cached)["choices"] == [{
|
|
"index": 0, "message": {"role": "assistant", "content": "pong"},
|
|
"finish_reason": "stop", "provider_specific_fields": {},
|
|
}]
|
|
assert payload(cached)["usage"] == CHAT["usage"]
|
|
assert REQUESTS.empty(), "the second SDK call missed the response cache"
|
|
|
|
|
|
@skipIf(importlib.util.find_spec("fastapi") is None, "BUG: LIT-9333 Messages streaming imports proxy-only fastapi")
|
|
def check_messages_stream_and_callback(base: str) -> None:
|
|
from litellm.integrations.custom_logger import CustomLogger
|
|
from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER
|
|
|
|
successes: Final[Queue[Mapping[str, object]]] = Queue()
|
|
failures: Final[Queue[object]] = Queue()
|
|
|
|
class Probe(CustomLogger):
|
|
async def async_log_success_event(
|
|
self, kwargs: Mapping[str, object], response_obj: object, start_time: object, end_time: object
|
|
) -> None:
|
|
if kwargs.get("litellm_call_id") == "base-sdk-messages-check":
|
|
successes.put(kwargs)
|
|
|
|
async def async_log_failure_event(
|
|
self, kwargs: Mapping[str, object], response_obj: object, start_time: object, end_time: object
|
|
) -> None:
|
|
if kwargs.get("litellm_call_id") == "base-sdk-messages-check":
|
|
failures.put(response_obj)
|
|
|
|
frames: Final[tuple[Mapping[str, JsonValue], ...]] = (
|
|
{
|
|
"type": "message_start",
|
|
"message": {
|
|
"id": "msg-stream-http-check",
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"model": "claude-sonnet-5-5",
|
|
"content": [],
|
|
"stop_reason": None,
|
|
"stop_sequence": None,
|
|
"usage": {"input_tokens": 3, "output_tokens": 0},
|
|
},
|
|
},
|
|
{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}},
|
|
{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "pong"}},
|
|
{"type": "content_block_stop", "index": 0},
|
|
{
|
|
"type": "message_delta",
|
|
"delta": {"stop_reason": "end_turn", "stop_sequence": None},
|
|
"usage": {"output_tokens": 2},
|
|
},
|
|
{"type": "message_stop"},
|
|
)
|
|
RESPONSES.put(
|
|
(200, b"".join(f"event: {frame['type']}\ndata: {json.dumps(frame)}\n\n".encode() for frame in frames))
|
|
)
|
|
|
|
async def check_async() -> None:
|
|
with patch.object(litellm, "callbacks", [Probe()]):
|
|
create: Final = cast( # cast-ok: SDK facade exposes untyped kwargs and iterator items
|
|
Callable[..., Awaitable[object]], litellm.anthropic.messages.acreate
|
|
)
|
|
stream: Final = await create(
|
|
model="anthropic/claude-sonnet-5-5",
|
|
messages=MESSAGES,
|
|
api_key="test-key",
|
|
api_base=base,
|
|
max_tokens=16,
|
|
stream=True,
|
|
litellm_call_id="base-sdk-messages-check",
|
|
)
|
|
assert isinstance(stream, AsyncIterable), stream
|
|
iterator: Final = cast(AsyncIterable[object], stream) # cast-ok: SDK returns an unparameterized iterator
|
|
chunks: Final = tuple([frame async for frame in iterator])
|
|
assert all(isinstance(frame, bytes) for frame in chunks), chunks
|
|
received: Final = b"".join(frame for frame in chunks if isinstance(frame, bytes))
|
|
assert tuple(json.loads(line[6:]) for line in received.splitlines() if line.startswith(b"data: ")) == frames
|
|
success: Final = await asyncio.to_thread(successes.get, True, 10)
|
|
await GLOBAL_LOGGING_WORKER.flush()
|
|
logging: Final = success["standard_logging_object"]
|
|
assert isinstance(logging, dict)
|
|
assert (logging["prompt_tokens"], logging["completion_tokens"], logging["total_tokens"]) == (3, 2, 5)
|
|
assert successes.empty(), "a successful stream fired more than one success callback"
|
|
assert failures.empty(), "a successful stream fired a failure callback"
|
|
|
|
asyncio.run(check_async())
|
|
path, headers, body = REQUESTS.get(timeout=10)
|
|
assert path == "/v1/messages"
|
|
assert headers["x-api-key"] == "test-key"
|
|
assert json.loads(body) == {"model": "claude-sonnet-5-5", "messages": MESSAGES, "max_tokens": 16, "stream": True}
|
|
|
|
|
|
def check_http(base: str) -> None:
|
|
arguments: Final = dict(model="openai/gpt-4o", messages=MESSAGES, api_key="test-key", api_base=base + "/v1")
|
|
enqueue(CHAT)
|
|
response: Final = litellm.completion(**arguments)
|
|
assert response.choices[0].message.content == "pong"
|
|
assert response.usage.total_tokens == 5
|
|
path, headers, body = REQUESTS.get(timeout=10)
|
|
assert path == "/v1/chat/completions"
|
|
assert headers["authorization"] == "Bearer test-key"
|
|
assert json.loads(body)["messages"] == MESSAGES
|
|
enqueue_stream()
|
|
assert "".join(part.choices[0].delta.content or "" for part in litellm.completion(**arguments, stream=True)) == "pong"
|
|
REQUESTS.get(timeout=10)
|
|
for status, exception in ((401, litellm.AuthenticationError), (429, litellm.RateLimitError), (500, litellm.InternalServerError)):
|
|
enqueue({"error": {"message": "controlled upstream failure"}}, status)
|
|
try:
|
|
litellm.completion(**arguments, num_retries=0, max_retries=0)
|
|
except exception:
|
|
REQUESTS.get(timeout=10)
|
|
else:
|
|
raise AssertionError(f"HTTP {status} did not raise {exception.__name__}")
|
|
enqueue({"error": {"message": "retry once"}}, 429)
|
|
enqueue(CHAT)
|
|
assert litellm.completion(**arguments, num_retries=1).choices[0].message.content == "pong"
|
|
REQUESTS.get(timeout=10)
|
|
REQUESTS.get(timeout=10)
|
|
|
|
async def check_async() -> None:
|
|
enqueue(CHAT)
|
|
result: Final = await litellm.acompletion(**arguments)
|
|
assert result.choices[0].message.content == "pong"
|
|
REQUESTS.get(timeout=10)
|
|
enqueue_stream()
|
|
stream: Final = await litellm.acompletion(**arguments, stream=True)
|
|
assert "".join([part.choices[0].delta.content or "" async for part in stream]) == "pong"
|
|
REQUESTS.get(timeout=10)
|
|
ARRIVED.clear()
|
|
RESPONSES.put((0, b""))
|
|
task: Final = asyncio.create_task(litellm.acompletion(**arguments, num_retries=0, max_retries=0))
|
|
assert await asyncio.to_thread(ARRIVED.wait, 10), "request did not reach upstream"
|
|
task.cancel()
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
REQUESTS.get(timeout=10)
|
|
else:
|
|
raise AssertionError("cancellation did not propagate")
|
|
finally:
|
|
RELEASE.set()
|
|
|
|
asyncio.run(check_async())
|
|
enqueue({"object": "list", "model": "text-embedding-3-small",
|
|
"data": [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2]}],
|
|
"usage": {"prompt_tokens": 3, "total_tokens": 3}})
|
|
embedding: Final = litellm.embedding(model="text-embedding-3-small", input=["ping"], api_key="test-key", api_base=base + "/v1")
|
|
assert embedding.data[0]["embedding"] == [0.1, 0.2]
|
|
assert REQUESTS.get(timeout=10)[0] == "/v1/embeddings"
|
|
RELEASE.clear()
|
|
RESPONSES.put((0, b""))
|
|
try:
|
|
litellm.completion(**arguments, timeout=0.1, num_retries=0, max_retries=0)
|
|
except litellm.Timeout:
|
|
REQUESTS.get(timeout=10)
|
|
else:
|
|
raise AssertionError("stalled upstream did not time out")
|
|
finally:
|
|
RELEASE.set()
|
|
for signed in ((False, True) if importlib.util.find_spec("boto3") else (False,)):
|
|
for asynchronous in (False, True):
|
|
enqueue({
|
|
"output": {"message": {"role": "assistant", "content": [{"text": "pong"}]}},
|
|
"stopReason": "end_turn",
|
|
"usage": {"inputTokens": 3, "outputTokens": 2, "totalTokens": 5},
|
|
"metrics": {"latencyMs": 1},
|
|
})
|
|
bedrock: Final = dict(
|
|
model="bedrock/anthropic.claude-3-sonnet-20240229-v1:0", messages=MESSAGES,
|
|
aws_region_name="us-east-1", aws_bedrock_runtime_endpoint=base,
|
|
**({"aws_access_key_id": "test-key", "aws_secret_access_key": "test-secret", "api_key": ""}
|
|
if signed else {"api_key": "bearer-key"}),
|
|
)
|
|
result: Final = asyncio.run(litellm.acompletion(**bedrock)) if asynchronous else litellm.completion(**bedrock)
|
|
assert result.choices[0].message.content == "pong"
|
|
assert result.usage.total_tokens == 5
|
|
path, headers, body = REQUESTS.get(timeout=10)
|
|
assert path.endswith("/converse")
|
|
assert headers["authorization"].startswith(
|
|
"AWS4-HMAC-SHA256" if signed else "Bearer bearer-key"
|
|
)
|
|
assert json.loads(body)["messages"][0]["content"][0]["text"] == "ping"
|
|
if importlib.util.find_spec("botocore") is not None:
|
|
RESPONSES.put((200, b"".join((
|
|
bedrock_event("messageStart", {"role": "assistant"}),
|
|
bedrock_event("contentBlockDelta", {"contentBlockIndex": 0, "delta": {"text": "pong"}}),
|
|
bedrock_event("messageStop", {"stopReason": "end_turn"}),
|
|
bedrock_event("metadata", {"usage": {"inputTokens": 3, "outputTokens": 2, "totalTokens": 5}}),
|
|
))))
|
|
streamed: Final = litellm.completion(
|
|
model="bedrock/anthropic.claude-3-sonnet-20240229-v1:0", messages=MESSAGES,
|
|
api_key="bearer-key", aws_region_name="us-east-1", aws_bedrock_runtime_endpoint=base, stream=True,
|
|
)
|
|
assert "".join(part.choices[0].delta.content or "" for part in streamed) == "pong"
|
|
assert REQUESTS.get(timeout=10)[0].endswith("/converse-stream")
|
|
enqueue({"id": "msg-http-check", "type": "message", "role": "assistant", "model": "claude-3-sonnet-20240229",
|
|
"content": [{"type": "text", "text": "pong"}], "stop_reason": "end_turn", "stop_sequence": None,
|
|
"usage": {"input_tokens": 3, "output_tokens": 2}})
|
|
anthropic: Final = litellm.completion(model="anthropic/claude-3-sonnet-20240229", messages=MESSAGES,
|
|
api_key="test-key", api_base=base, max_tokens=16)
|
|
assert anthropic.choices[0].message.content == "pong"
|
|
assert anthropic.usage.total_tokens == 5
|
|
assert REQUESTS.get(timeout=10)[0] == "/v1/messages"
|
|
print("PASS installed HTTP: sync/async, streaming, usage, error mapping, retries, cancellation, Bedrock auth")
|
|
|
|
|
|
def main() -> None:
|
|
with ThreadingHTTPServer(("127.0.0.1", 0), RecordingUpstream) as server:
|
|
worker: Final = Thread(target=server.serve_forever, daemon=True)
|
|
worker.start()
|
|
try:
|
|
check_http(f"http://127.0.0.1:{server.server_port}")
|
|
for check in (
|
|
check_tools,
|
|
partial(check_tools, asynchronous=True),
|
|
check_responses,
|
|
partial(check_responses, asynchronous=True),
|
|
check_messages_tools,
|
|
check_cache_hit,
|
|
check_messages_stream_and_callback,
|
|
):
|
|
try:
|
|
check(f"http://127.0.0.1:{server.server_port}")
|
|
except SkipTest as skipped:
|
|
print(f"SKIP: {skipped}")
|
|
assert RESPONSES.empty() and REQUESTS.empty(), "unconsumed HTTP exchanges"
|
|
finally:
|
|
RELEASE.set()
|
|
server.shutdown()
|
|
worker.join(timeout=10)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
with patch.dict(os.environ, ENVIRONMENT):
|
|
main()
|