Skip to content

Commit fda97da

Browse files
committed
Fix VCRConfig after restack: remove filter_headers and fix match_on
1 parent 25f2bba commit fda97da

File tree

80 files changed

+12557
-9721
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+12557
-9721
lines changed

python/tests/e2e/conftest.py

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@
66
from __future__ import annotations
77

88
import hashlib
9-
from collections.abc import Generator
9+
import inspect
10+
from collections.abc import Awaitable, Callable, Generator
1011
from copy import deepcopy
11-
from typing import Any, TypeAlias, TypedDict, get_args
12-
from collections.abc import Callable
13-
from copy import deepcopy
14-
from typing import Any, TypedDict, get_args
12+
from typing import TypedDict, get_args
13+
from typing_extensions import TypeIs
1514

1615
import httpx
1716
import pytest
1817
from anthropic.lib.bedrock import _auth as bedrock_auth
1918
from anthropic.lib.bedrock._client import AnthropicBedrock, AsyncAnthropicBedrock
19+
from vcr.cassette import Cassette as VCRCassette
20+
from vcr.request import Request as VCRRequest
2021
from vcr.stubs import httpx_stubs
2122

2223
from mirascope import llm
@@ -74,17 +75,28 @@ class VCRConfig(TypedDict, total=False):
7475
- 'uri': Request URI/URL
7576
- 'body': Request body content (use 'raw_body' for exact binary matching)
7677
- 'headers': Request headers
78+
- 'scheme', 'host', 'port', 'path', 'query': URL components
7779
"""
7880

79-
before_record_request: Callable[[Any], Any]
80-
"""Callback to sanitize requests before saving to cassette.
81+
filter_headers: list[str]
82+
"""Headers to filter out from recordings for security/privacy.
8183
82-
This function is called AFTER the real HTTP request is sent (with valid auth),
83-
but BEFORE it's written to the cassette file. Use this to sanitize sensitive
84-
headers without affecting the actual HTTP requests.
84+
DEPRECATED: Use before_record_request instead for better control.
85+
These headers will be removed from both recorded cassettes and
86+
when matching requests during playback. Commonly used for:
87+
- Authentication tokens
88+
- API keys
89+
- Organization identifiers
90+
"""
91+
92+
filter_post_data_parameters: list[str]
93+
"""POST data parameters to filter out from recordings.
94+
95+
Similar to filter_headers but for form data and request body parameters.
96+
Useful for removing sensitive data from request bodies.
8597
"""
8698

87-
before_record_request: Any
99+
before_record_request: Callable[[VCRRequest], VCRRequest]
88100
"""Callback to sanitize requests before saving to cassette.
89101
90102
This function is called AFTER the real HTTP request is sent (with valid auth),
@@ -101,17 +113,15 @@ class VCRConfig(TypedDict, total=False):
101113
"""
102114

103115

104-
105-
def sanitize_request(request: Any) -> Any: # noqa: ANN401
116+
def sanitize_request(request: VCRRequest) -> VCRRequest:
106117
"""Sanitize sensitive headers in VCR request before recording to cassette.
107118
108119
This hook is called AFTER the real HTTP request is sent (with valid auth),
109120
but BEFORE it's written to the cassette file. We deep copy the request
110121
and replace sensitive headers with placeholders.
111122
112123
Args:
113-
request: VCR request object to sanitize (Any type since VCR doesn't
114-
provide typed request objects)
124+
request: VCR request object to sanitize
115125
116126
Returns:
117127
Sanitized copy of the request safe for cassette storage
@@ -151,17 +161,14 @@ def vcr_config() -> VCRConfig:
151161
"""
152162
return {
153163
"record_mode": "once",
154-
"match_on": ["method", "scheme", "host", "port", "path", "query", "raw_body"],
164+
"match_on": ["method", "uri", "body"],
155165
"filter_headers": [], # Don't filter here; use before_record_request
156166
"filter_post_data_parameters": [],
157167
"before_record_request": sanitize_request,
158168
"decode_compressed_response": False, # Preserve exact response bytes
159169
}
160170

161171

162-
Snapshot: TypeAlias = Any # Alias to avoid Ruff lint errors
163-
164-
165172
def _remove_auth_headers(headers: httpx.Headers) -> None:
166173
"""Remove stale AWS authentication headers before re-signing.
167174
@@ -286,7 +293,7 @@ async def _resign_async() -> None:
286293
original_sync_send = httpx_stubs._sync_vcr_send
287294
original_async_send = httpx_stubs._async_vcr_send
288295

289-
def _is_bedrock_request(request: httpx.Request | None) -> bool:
296+
def _is_bedrock_request(request: httpx.Request | None) -> TypeIs[httpx.Request]:
290297
"""Check if the request is targeting AWS Bedrock API.
291298
292299
We identify Bedrock requests by checking for 'bedrock-runtime' in the
@@ -338,7 +345,7 @@ def _handle_bedrock_resign_sync(real_request: httpx.Request) -> None:
338345
_reset_request_body(real_request, body_bytes)
339346

340347
resign_sync = real_request.extensions.get("mirascope_bedrock_resign")
341-
if callable(resign_sync):
348+
if inspect.isfunction(resign_sync):
342349
_remove_auth_headers(real_request.headers)
343350
resign_sync()
344351
_reset_request_body(real_request, body_bytes)
@@ -358,21 +365,27 @@ async def _handle_bedrock_resign_async(real_request: httpx.Request) -> None:
358365
resign_async = real_request.extensions.get("mirascope_bedrock_resign_async")
359366
resign_sync = real_request.extensions.get("mirascope_bedrock_resign")
360367

361-
if callable(resign_async):
368+
if inspect.iscoroutinefunction(resign_async):
362369
_remove_auth_headers(real_request.headers)
363370
await resign_async()
364371
_reset_request_body(real_request, body_bytes)
365-
elif callable(resign_sync):
372+
elif inspect.isfunction(resign_sync):
366373
_remove_auth_headers(real_request.headers)
367374
resign_sync()
368375
_reset_request_body(real_request, body_bytes)
369376

370-
def _patched_sync_send(cassette, real_send, *args, **kwargs) -> httpx.Response: # noqa: ANN001, ANN002, ANN003
377+
def _patched_sync_send(
378+
cassette: VCRCassette,
379+
real_send: Callable[..., httpx.Response],
380+
client: httpx.Client,
381+
request: httpx.Request,
382+
**kwargs: object,
383+
) -> httpx.Response:
384+
args = (client, request)
371385
vcr_request, response = original_shared_send(
372386
cassette, real_send, *args, **kwargs
373387
)
374-
client = args[0]
375-
real_request = args[1] if len(args) > 1 else None
388+
real_request: httpx.Request | None = request
376389

377390
if response is not None:
378391
client.cookies.extract_cookies(response)
@@ -392,16 +405,18 @@ def _patched_sync_send(cassette, real_send, *args, **kwargs) -> httpx.Response:
392405
return real_response
393406

394407
async def _patched_async_send(
395-
cassette,
396-
real_send,
397-
*args,
398-
**kwargs, # noqa: ANN001, ANN002, ANN003
408+
cassette: VCRCassette,
409+
real_send: Callable[..., Awaitable[httpx.Response]],
410+
client: httpx.AsyncClient,
411+
request: httpx.Request,
412+
**kwargs: object,
399413
) -> httpx.Response:
414+
# VCR expects args tuple, so reconstruct it for original_shared_send
415+
args = (client, request)
400416
vcr_request, response = original_shared_send(
401417
cassette, real_send, *args, **kwargs
402418
)
403-
client = args[0]
404-
real_request = args[1] if len(args) > 1 else None
419+
real_request: httpx.Request | None = request
405420

406421
if response is not None:
407422
client.cookies.extract_cookies(response)

0 commit comments

Comments
 (0)