ddapm-test-agent 1.32.0__py3-none-any.whl → 1.33.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,21 +1,38 @@
1
1
  import hashlib
2
2
  import json
3
+ import logging
3
4
  import os
4
5
  import re
6
+ from typing import Any
7
+ from typing import Dict
5
8
  from typing import Optional
6
9
  from urllib.parse import urljoin
7
10
 
8
11
  from aiohttp.web import Request
9
12
  from aiohttp.web import Response
10
13
  import requests
14
+ from requests_aws4auth import AWS4Auth
11
15
  import vcr
12
16
 
13
17
 
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ # Used for AWS signature recalculation for aws services initial proxying
22
+ AWS_REGION = os.environ.get("AWS_REGION", "us-east-1")
23
+ AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")
24
+
25
+
14
26
  def url_path_join(base_url: str, path: str) -> str:
15
27
  """Join a base URL with a path, handling slashes automatically."""
16
28
  return urljoin(base_url.rstrip("/") + "/", path.lstrip("/"))
17
29
 
18
30
 
31
+ AWS_SERVICES = {
32
+ "bedrock-runtime": "bedrock",
33
+ }
34
+
35
+
19
36
  PROVIDER_BASE_URLS = {
20
37
  "openai": "https://api.openai.com/v1",
21
38
  "azure-openai": "https://dd.openai.azure.com/",
@@ -23,8 +40,25 @@ PROVIDER_BASE_URLS = {
23
40
  "anthropic": "https://api.anthropic.com/",
24
41
  "datadog": "https://api.datadoghq.com/",
25
42
  "genai": "https://generativelanguage.googleapis.com/",
43
+ "bedrock-runtime": f"https://bedrock-runtime.{AWS_REGION}.amazonaws.com",
26
44
  }
27
45
 
46
+ CASSETTE_FILTER_HEADERS = [
47
+ "authorization",
48
+ "OpenAI-Organization",
49
+ "api-key",
50
+ "x-api-key",
51
+ "dd-api-key",
52
+ "dd-application-key",
53
+ "x-goog-api-key",
54
+ "x-amz-security-token",
55
+ "x-amz-content-sha256",
56
+ "x-amz-date",
57
+ "x-amz-user-agent",
58
+ "amz-sdk-invocation-id",
59
+ "amz-sdk-request",
60
+ ]
61
+
28
62
  NORMALIZERS = [
29
63
  (
30
64
  r"--form-data-boundary-[^\r\n]+",
@@ -65,6 +99,21 @@ def normalize_multipart_body(body: bytes) -> str:
65
99
  return f"[binary_data_{hex_digest}]"
66
100
 
67
101
 
102
+ def parse_authorization_header(auth_header: str) -> Dict[str, str]:
103
+ """Parse AWS Authorization header to extract components"""
104
+ if not auth_header.startswith("AWS4-HMAC-SHA256 "):
105
+ return {}
106
+
107
+ auth_parts = auth_header[len("AWS4-HMAC-SHA256 ") :].split(",")
108
+ parsed = {}
109
+
110
+ for part in auth_parts:
111
+ key, value = part.split("=", 1)
112
+ parsed[key.strip()] = value.strip()
113
+
114
+ return parsed
115
+
116
+
68
117
  def get_vcr(subdirectory: str, vcr_cassettes_directory: str) -> vcr.VCR:
69
118
  cassette_dir = os.path.join(vcr_cassettes_directory, subdirectory)
70
119
 
@@ -72,15 +121,7 @@ def get_vcr(subdirectory: str, vcr_cassettes_directory: str) -> vcr.VCR:
72
121
  cassette_library_dir=cassette_dir,
73
122
  record_mode="once",
74
123
  match_on=["path", "method"],
75
- filter_headers=[
76
- "authorization",
77
- "OpenAI-Organization",
78
- "api-key",
79
- "x-api-key",
80
- "dd-api-key",
81
- "dd-application-key",
82
- "x-goog-api-key",
83
- ],
124
+ filter_headers=CASSETTE_FILTER_HEADERS,
84
125
  )
85
126
 
86
127
 
@@ -125,31 +166,47 @@ async def proxy_request(request: Request, vcr_cassettes_directory: str) -> Respo
125
166
  body_bytes = await request.read()
126
167
 
127
168
  vcr_cassette_prefix = request.pop("vcr_cassette_prefix", None)
128
-
129
169
  cassette_name = generate_cassette_name(path, request.method, body_bytes, vcr_cassette_prefix)
170
+
171
+ request_kwargs: Dict[str, Any] = {
172
+ "method": request.method,
173
+ "url": target_url,
174
+ "headers": headers,
175
+ "data": body_bytes,
176
+ "cookies": dict(request.cookies),
177
+ "allow_redirects": False,
178
+ "stream": True,
179
+ }
180
+
181
+ if provider in AWS_SERVICES and not os.path.exists(os.path.join(vcr_cassettes_directory, provider, cassette_name)):
182
+ if not AWS_SECRET_ACCESS_KEY:
183
+ return Response(
184
+ body="AWS_SECRET_ACCESS_KEY environment variable not set for aws signature recalculation",
185
+ status=400,
186
+ )
187
+
188
+ auth_header = request.headers.get("Authorization", "")
189
+ auth_parts = parse_authorization_header(auth_header)
190
+ aws_access_key = auth_parts.get("Credential", "").split("/")[0]
191
+
192
+ auth = AWS4Auth(aws_access_key, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_SERVICES[provider])
193
+ request_kwargs["auth"] = auth
194
+
130
195
  with get_vcr(provider, vcr_cassettes_directory).use_cassette(f"{cassette_name}.yaml"):
131
- oai_response = requests.request(
132
- method=request.method,
133
- url=target_url,
134
- headers=headers,
135
- data=body_bytes,
136
- cookies=dict(request.cookies),
137
- allow_redirects=False,
138
- stream=True,
139
- )
196
+ provider_response = requests.request(**request_kwargs)
140
197
 
141
198
  # Extract content type without charset
142
- content_type = oai_response.headers.get("content-type", "")
199
+ content_type = provider_response.headers.get("content-type", "")
143
200
  if ";" in content_type:
144
201
  content_type = content_type.split(";")[0].strip()
145
202
 
146
203
  response = Response(
147
- body=oai_response.content,
148
- status=oai_response.status_code,
204
+ body=provider_response.content,
205
+ status=provider_response.status_code,
149
206
  content_type=content_type,
150
207
  )
151
208
 
152
- for key, value in oai_response.headers.items():
209
+ for key, value in provider_response.headers.items():
153
210
  if key.lower() not in (
154
211
  "content-length",
155
212
  "transfer-encoding",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddapm-test-agent
3
- Version: 1.32.0
3
+ Version: 1.33.0
4
4
  Summary: Test agent for Datadog APM client libraries
5
5
  Home-page: https://github.com/Datadog/dd-apm-test-agent
6
6
  Author: Kyle Verhoog
@@ -24,6 +24,7 @@ Requires-Dist: requests
24
24
  Requires-Dist: typing_extensions
25
25
  Requires-Dist: yarl
26
26
  Requires-Dist: vcrpy
27
+ Requires-Dist: requests-aws4auth
27
28
  Requires-Dist: opentelemetry-proto<1.37.0,>1.33.0
28
29
  Requires-Dist: protobuf>=3.19.0
29
30
  Requires-Dist: grpcio<2.0,>=1.66.2
@@ -192,6 +193,13 @@ The cassettes are matched based on the path, method, and body of the request. To
192
193
 
193
194
  Optionally specifying whatever mounted path is used for the cassettes directory. The test agent comes with a default set of cassettes for OpenAI, Azure OpenAI, and DeepSeek.
194
195
 
196
+ #### AWS Services
197
+ AWS service proxying, specifically recording cassettes for the first time, requires a `AWS_SECRET_ACCESS_KEY` environment variable to be set for the container running the test agent. This is used to recalculate the AWS signature for the request, as the one generated client-side likely used `{test-agent-host}:{test-agent-port}/vcr/{aws-service}` as the host, and the signature will mismatch that on the actual AWS service.
198
+
199
+ Additionally, the `AWS_REGION` environment variable can be set, defaulting to `us-east-1`.
200
+
201
+ To add a new AWS service to proxy, add an entry in the `PROVIDER_BASE_URLS` for its provider url, and an entry in the `AWS_SERVICES` dictionary for the service name, since they are not always a one-to-one mapping with the implied provider url (e.g, `https://bedrock-runtime.{AWS_REGION}.amazonaws.com` is the provider url, but the service name is `bedrock`, as `bedrock` also has multiple sub services, like `converse`).
202
+
195
203
  #### Usage in clients
196
204
 
197
205
  To use this feature in your client, you can use the `/vcr/{provider}` endpoint to proxy requests to the provider API.
@@ -16,11 +16,11 @@ ddapm_test_agent/trace_snapshot.py,sha256=g2MhKi8UE-Wsf6PtuzPoXymcW-cYRUnvj63SP9
16
16
  ddapm_test_agent/tracerflare.py,sha256=uoSjhPCOKZflgJn5JLv1Unh4gUdAR1-YbC9_1n1iH9w,954
17
17
  ddapm_test_agent/tracestats.py,sha256=q_WQZnh2kXSSN3fRIBe_0jMYCBQHcaS3fZmJTge4lWc,2073
18
18
  ddapm_test_agent/tracestats_snapshot.py,sha256=VsB6MVnHPjPWHVWnnDdCXJcVKL_izKXEf9lvJ0qbjNQ,3609
19
- ddapm_test_agent/vcr_proxy.py,sha256=g6ix7laiS8Hqq9p14nkTMARhj5KMZmyRZjZpfFEMxOM,4973
20
- ddapm_test_agent-1.32.0.dist-info/licenses/LICENSE.BSD3,sha256=J9S_Tq-hhvteDV2W8R0rqht5DZHkmvgdx3gnLZg4j6Q,1493
21
- ddapm_test_agent-1.32.0.dist-info/licenses/LICENSE.apache2,sha256=5V2RruBHZQIcPyceiv51DjjvdvhgsgS4pnXAOHDuZkQ,11342
22
- ddapm_test_agent-1.32.0.dist-info/METADATA,sha256=mf922XgdhlbHNJk3P5jCiw5HBkmzfwpex-FDvqjHXa4,27493
23
- ddapm_test_agent-1.32.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
- ddapm_test_agent-1.32.0.dist-info/entry_points.txt,sha256=ulayVs6YJ-0Ej2kxbwn39wOHDVXbyQgFgsbRQmXydcs,250
25
- ddapm_test_agent-1.32.0.dist-info/top_level.txt,sha256=A9jiKOrrg6VjFAk-mtlSVYN4wr0VsZe58ehGR6IW47U,17
26
- ddapm_test_agent-1.32.0.dist-info/RECORD,,
19
+ ddapm_test_agent/vcr_proxy.py,sha256=qnzLD5f1LcKIcLFmqU0n5fzcfWTIt0m173pbpBfeXK4,6745
20
+ ddapm_test_agent-1.33.0.dist-info/licenses/LICENSE.BSD3,sha256=J9S_Tq-hhvteDV2W8R0rqht5DZHkmvgdx3gnLZg4j6Q,1493
21
+ ddapm_test_agent-1.33.0.dist-info/licenses/LICENSE.apache2,sha256=5V2RruBHZQIcPyceiv51DjjvdvhgsgS4pnXAOHDuZkQ,11342
22
+ ddapm_test_agent-1.33.0.dist-info/METADATA,sha256=hxIxnr5yrlh06q1YfaKgeX7RcQUx0XzC7nM2pgHQQQ4,28483
23
+ ddapm_test_agent-1.33.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
+ ddapm_test_agent-1.33.0.dist-info/entry_points.txt,sha256=ulayVs6YJ-0Ej2kxbwn39wOHDVXbyQgFgsbRQmXydcs,250
25
+ ddapm_test_agent-1.33.0.dist-info/top_level.txt,sha256=A9jiKOrrg6VjFAk-mtlSVYN4wr0VsZe58ehGR6IW47U,17
26
+ ddapm_test_agent-1.33.0.dist-info/RECORD,,