Skip to content

Commit 3ec3d04

Browse files
iurisilvioclaude
andauthored
Send a roboflow-python User-Agent on every SDK request (#559)
* Add roboflow.util.http, which sends a roboflow-python User-Agent The helpers wrap requests' get, post, put, patch, delete, head and request and add `User-Agent: roboflow-python/<version>`; headers the caller passes take precedence. `http.Session` is a requests.Session that sends the same header. `__version__` moves to the top of roboflow/__init__.py, so the helper can read it while the package's own imports are still running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Send every SDK request through roboflow.util.http Every requests.<verb>(...) call in roboflow/, and the request functions the Batch Processing adapters pass by reference, now calls the roboflow.util.http helper of the same name. Workspace.search_export builds its session with http.Session, and the two urllib image fetches in roboflow.util.prediction send the header too. ruff's banned-api (TID251) rejects requests.get, post, put, patch, delete, head and request outside roboflow/util/http.py, so a new call can't skip the header. Tests that mocked roboflow.<module>.requests.<verb> now mock roboflow.util.http.requests.<verb>, where the call happens, and the three that compared the whole headers dict expect the User-Agent too. New tests check the header end to end on Roboflow() -> workspace -> project -> version and on an annotated image upload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent cbaaf5a commit 3ec3d04

38 files changed

Lines changed: 572 additions & 379 deletions

‎pyproject.toml‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ convention = "google"
9292
"tests/manual/*.py" = [
9393
"INP001", # Manual scripts don't need __init__.py
9494
]
95+
"roboflow/util/http.py" = [
96+
"TID251", # Wraps the requests functions that the rest of the SDK can't call
97+
]
9598

9699
[tool.ruff.lint.pyupgrade]
97100
# Preserve types, even if a file imports `from __future__ import annotations`.
@@ -104,6 +107,15 @@ banned-module-level-imports = [
104107
"numpy",
105108
]
106109

110+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
111+
"requests.delete".msg = "Use roboflow.util.http.delete, which sends the SDK's User-Agent."
112+
"requests.get".msg = "Use roboflow.util.http.get, which sends the SDK's User-Agent."
113+
"requests.head".msg = "Use roboflow.util.http.head, which sends the SDK's User-Agent."
114+
"requests.patch".msg = "Use roboflow.util.http.patch, which sends the SDK's User-Agent."
115+
"requests.post".msg = "Use roboflow.util.http.post, which sends the SDK's User-Agent."
116+
"requests.put".msg = "Use roboflow.util.http.put, which sends the SDK's User-Agent."
117+
"requests.request".msg = "Use roboflow.util.http.request, which sends the SDK's User-Agent."
118+
107119
[tool.mypy]
108120
python_version = "3.10"
109121
exclude = ["^build/"]

‎roboflow/__init__.py‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Set before the imports below, so roboflow.util.http can read it for the User-Agent while they run.
2+
__version__ = "1.7.1"
3+
14
import json
25
import os
36
import sys
@@ -6,11 +9,10 @@
69
from pathlib import Path
710
from urllib.parse import urlparse
811

9-
import requests
10-
1112
from roboflow.adapters import rfapi
1213
from roboflow.config import API_URL, APP_URL, DEMO_KEYS, load_roboflow_api_key
1314
from roboflow.core.workspace import Workspace
15+
from roboflow.util import http
1416
from roboflow.util.general import write_line
1517

1618
try:
@@ -21,8 +23,6 @@
2123
CLIPModel = None # type: ignore[assignment,misc]
2224
GazeModel = None # type: ignore[assignment,misc]
2325

24-
__version__ = "1.7.1"
25-
2626

2727
def check_key(api_key, model, notebook, num_retries=0):
2828
if not isinstance(api_key, str):
@@ -36,7 +36,7 @@ def check_key(api_key, model, notebook, num_retries=0):
3636
return api_key
3737
else:
3838
# validate key normally
39-
response = requests.post(API_URL + "/?api_key=" + api_key)
39+
response = http.post(API_URL + "/?api_key=" + api_key)
4040

4141
if response.status_code == 401:
4242
raise RuntimeError(response.text)
@@ -137,7 +137,7 @@ def login(workspace=None, force=False, region=None):
137137

138138
token = getpass("Paste the authentication token here: ")
139139

140-
r_login = requests.get(app_url + "/query/cliAuthToken/" + token)
140+
r_login = http.get(app_url + "/query/cliAuthToken/" + token)
141141

142142
if r_login.status_code == 200:
143143
r_login = r_login.json()
@@ -339,7 +339,7 @@ def project(self, project_name, the_workspace=None):
339339
else:
340340
the_workspace = self.current_workspace
341341

342-
dataset_info = requests.get(API_URL + "/" + the_workspace + "/" + project_name + "?api_key=" + self.api_key)
342+
dataset_info = http.get(API_URL + "/" + the_workspace + "/" + project_name + "?api_key=" + self.api_key)
343343

344344
# Throw error if dataset isn't valid/user doesn't have
345345
# permissions to access the dataset

‎roboflow/adapters/deploymentapi.py‎

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import urllib
22

3-
import requests
4-
53
from roboflow.config import DEDICATED_DEPLOYMENT_URL
4+
from roboflow.util import http
65

76

87
class DeploymentApiError(Exception):
@@ -24,23 +23,23 @@ def add_deployment(
2423
}
2524
if machine_type is not None:
2625
params["machine_type"] = machine_type
27-
response = requests.post(url, json=params)
26+
response = http.post(url, json=params)
2827
if response.status_code != 200:
2928
return response.status_code, response.text
3029
return response.status_code, response.json()
3130

3231

3332
def get_deployment(api_key, deployment_name):
3433
url = f"{DEDICATED_DEPLOYMENT_URL}/get?api_key={api_key}&deployment_name={deployment_name}"
35-
response = requests.get(url)
34+
response = http.get(url)
3635
if response.status_code != 200:
3736
return response.status_code, response.text
3837
return response.status_code, response.json()
3938

4039

4140
def list_deployment(api_key):
4241
url = f"{DEDICATED_DEPLOYMENT_URL}/list?api_key={api_key}"
43-
response = requests.get(url)
42+
response = http.get(url)
4443
if response.status_code != 200:
4544
return response.status_code, response.text
4645
return response.status_code, response.json()
@@ -53,7 +52,7 @@ def get_workspace_usage(api_key, from_timestamp, to_timestamp):
5352
if to_timestamp is not None:
5453
params["to_timestamp"] = to_timestamp.isoformat() # may contain + sign
5554
url = f"{DEDICATED_DEPLOYMENT_URL}/usage_workspace?{urllib.parse.urlencode(params)}"
56-
response = requests.get(url)
55+
response = http.get(url)
5756
if response.status_code != 200:
5857
return response.status_code, response.text
5958
return response.status_code, response.json()
@@ -66,39 +65,39 @@ def get_deployment_usage(api_key, deployment_name, from_timestamp, to_timestamp)
6665
if to_timestamp is not None:
6766
params["to_timestamp"] = to_timestamp.isoformat() # may contain + sign
6867
url = f"{DEDICATED_DEPLOYMENT_URL}/usage_deployment?{urllib.parse.urlencode(params)}"
69-
response = requests.get(url)
68+
response = http.get(url)
7069
if response.status_code != 200:
7170
return response.status_code, response.text
7271
return response.status_code, response.json()
7372

7473

7574
def pause_deployment(api_key, deployment_name):
7675
url = f"{DEDICATED_DEPLOYMENT_URL}/pause"
77-
response = requests.post(url, json={"api_key": api_key, "deployment_name": deployment_name})
76+
response = http.post(url, json={"api_key": api_key, "deployment_name": deployment_name})
7877
if response.status_code != 200:
7978
return response.status_code, response.text
8079
return response.status_code, response.json()
8180

8281

8382
def resume_deployment(api_key, deployment_name):
8483
url = f"{DEDICATED_DEPLOYMENT_URL}/resume"
85-
response = requests.post(url, json={"api_key": api_key, "deployment_name": deployment_name})
84+
response = http.post(url, json={"api_key": api_key, "deployment_name": deployment_name})
8685
if response.status_code != 200:
8786
return response.status_code, response.text
8887
return response.status_code, response.json()
8988

9089

9190
def delete_deployment(api_key, deployment_name):
9291
url = f"{DEDICATED_DEPLOYMENT_URL}/delete"
93-
response = requests.post(url, json={"api_key": api_key, "deployment_name": deployment_name})
92+
response = http.post(url, json={"api_key": api_key, "deployment_name": deployment_name})
9493
if response.status_code != 200:
9594
return response.status_code, response.text
9695
return response.status_code, response.json()
9796

9897

9998
def list_machine_types(api_key):
10099
url = f"{DEDICATED_DEPLOYMENT_URL}/machine_types?api_key={api_key}"
101-
response = requests.get(url)
100+
response = http.get(url)
102101
if response.status_code != 200:
103102
return response.status_code, response.text
104103
return response.status_code, response.json()
@@ -113,7 +112,7 @@ def get_deployment_log(api_key, deployment_name, from_timestamp=None, to_timesta
113112
if max_entries > 0:
114113
params["max_entries"] = max_entries
115114
url = f"{DEDICATED_DEPLOYMENT_URL}/get_log?{urllib.parse.urlencode(params)}"
116-
response = requests.get(url)
115+
response = http.get(url)
117116
if response.status_code != 200:
118117
return response.status_code, response.text
119118
return response.status_code, response.json()

‎roboflow/adapters/devicesapi.py‎

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010

1111
from __future__ import annotations
1212

13-
from typing import Any, Dict, List, Optional
13+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
1414
from urllib.parse import urlencode
1515

16-
import requests
17-
1816
from roboflow.adapters.rfapi import RoboflowError
1917
from roboflow.config import API_URL
18+
from roboflow.util import http
19+
20+
if TYPE_CHECKING:
21+
import requests
2022

2123
DEFAULT_TIMEOUT = (10, 60)
2224

@@ -111,7 +113,7 @@ def _raise_for_status(response: requests.Response) -> None:
111113

112114
def list_devices(api_key: str, workspace: str) -> Dict[str, Any]:
113115
"""``GET /:workspace/devices/v2`` — returns the parsed JSON response."""
114-
response = requests.get(_build_url(workspace, "", api_key), timeout=DEFAULT_TIMEOUT)
116+
response = http.get(_build_url(workspace, "", api_key), timeout=DEFAULT_TIMEOUT)
115117
_raise_for_status(response)
116118
return response.json()
117119

@@ -140,7 +142,7 @@ def create_device(
140142
if source_device_id is not None:
141143
# Body field is camelCase per docs/api/deployments/overview.md
142144
body["sourceDeviceId"] = source_device_id
143-
response = requests.post(
145+
response = http.post(
144146
_build_url(workspace, "", api_key),
145147
json=body,
146148
timeout=DEFAULT_TIMEOUT,
@@ -151,7 +153,7 @@ def create_device(
151153

152154
def get_device(api_key: str, workspace: str, device_id: str) -> Dict[str, Any]:
153155
"""``GET /:workspace/devices/v2/:deviceId``."""
154-
response = requests.get(_build_url(workspace, f"/{device_id}", api_key), timeout=DEFAULT_TIMEOUT)
156+
response = http.get(_build_url(workspace, f"/{device_id}", api_key), timeout=DEFAULT_TIMEOUT)
155157
_raise_for_status(response)
156158
return response.json()
157159

@@ -163,7 +165,7 @@ def get_device_config(api_key: str, workspace: str, device_id: str) -> Dict[str,
163165
The response can include ``environment_variables`` and integration
164166
credentials. Treat the returned dict as sensitive.
165167
"""
166-
response = requests.get(_build_url(workspace, f"/{device_id}/config", api_key), timeout=DEFAULT_TIMEOUT)
168+
response = http.get(_build_url(workspace, f"/{device_id}/config", api_key), timeout=DEFAULT_TIMEOUT)
167169
_raise_for_status(response)
168170
return response.json()
169171

@@ -177,7 +179,7 @@ def get_device_config_history(
177179
cursor: Optional[str] = None,
178180
) -> Dict[str, Any]:
179181
"""``GET /:workspace/devices/v2/:deviceId/config/history``."""
180-
response = requests.get(
182+
response = http.get(
181183
_build_url(
182184
workspace,
183185
f"/{device_id}/config/history",
@@ -192,14 +194,14 @@ def get_device_config_history(
192194

193195
def list_device_streams(api_key: str, workspace: str, device_id: str) -> Dict[str, Any]:
194196
"""``GET /:workspace/devices/v2/:deviceId/streams``."""
195-
response = requests.get(_build_url(workspace, f"/{device_id}/streams", api_key), timeout=DEFAULT_TIMEOUT)
197+
response = http.get(_build_url(workspace, f"/{device_id}/streams", api_key), timeout=DEFAULT_TIMEOUT)
196198
_raise_for_status(response)
197199
return response.json()
198200

199201

200202
def get_device_stream(api_key: str, workspace: str, device_id: str, stream_id: str) -> Dict[str, Any]:
201203
"""``GET /:workspace/devices/v2/:deviceId/streams/:streamId``."""
202-
response = requests.get(
204+
response = http.get(
203205
_build_url(workspace, f"/{device_id}/streams/{stream_id}", api_key),
204206
timeout=DEFAULT_TIMEOUT,
205207
)
@@ -220,7 +222,7 @@ def get_device_logs(
220222
cursor: Optional[str] = None,
221223
) -> Dict[str, Any]:
222224
"""``GET /:workspace/devices/v2/:deviceId/logs``. Rate limited 5/min/IP."""
223-
response = requests.get(
225+
response = http.get(
224226
_build_url(
225227
workspace,
226228
f"/{device_id}/logs",
@@ -248,7 +250,7 @@ def get_device_telemetry(
248250
time_period: Optional[str] = None,
249251
) -> Dict[str, Any]:
250252
"""``GET /:workspace/devices/v2/:deviceId/telemetry``. Rate limited 60/min."""
251-
response = requests.get(
253+
response = http.get(
252254
_build_url(
253255
workspace,
254256
f"/{device_id}/telemetry",
@@ -276,7 +278,7 @@ def get_device_events(
276278
direction: Optional[str] = None,
277279
) -> Dict[str, Any]:
278280
"""``GET /:workspace/devices/v2/:deviceId/events``."""
279-
response = requests.get(
281+
response = http.get(
280282
_build_url(
281283
workspace,
282284
f"/{device_id}/events",

0 commit comments

Comments
 (0)