From 3e1eb6284f5f3859eb776215f6dc24a49a3ec7b7 Mon Sep 17 00:00:00 2001 From: Abdulrahman Amer Date: Fri, 5 Jun 2026 10:15:13 -0400 Subject: [PATCH] perf(rfapi): use module-level requests.Session for connection pooling 57 bare requests.get/post/put/delete calls each opened a new TCP connection and TLS handshake. A module-level _session reuses connections via keep-alive; urllib3 pools by host so api.roboflow.com and GCS signed-URL calls pool independently. The two functions that accept a caller-provided session (get_version_export, get_search_export) are unchanged. Updated mock targets in tests from requests.get/post to _session.get/post to reflect the new call path. --- roboflow/adapters/rfapi.py | 112 +++++++++-------- tests/adapters/test_rfapi_model_evals.py | 40 +++--- tests/adapters/test_rfapi_phase2.py | 150 +++++++++++------------ 3 files changed, 153 insertions(+), 149 deletions(-) diff --git a/roboflow/adapters/rfapi.py b/roboflow/adapters/rfapi.py index eef2e2a7..c7dd77f5 100644 --- a/roboflow/adapters/rfapi.py +++ b/roboflow/adapters/rfapi.py @@ -11,6 +11,10 @@ from roboflow.config import API_URL, DEFAULT_BATCH_NAME, DEFAULT_JOB_NAME +# Module-level session pools TCP connections across all API calls (keep-alive). +# Each unique host (api.roboflow.com, GCS) gets its own pool automatically. +_session = requests.Session() + class RoboflowError(Exception): """Generic API error. @@ -43,7 +47,7 @@ def __init__(self, message, status_code=None): def get_workspace(api_key, workspace_url): url = f"{API_URL}/{workspace_url}?api_key={api_key}" - response = requests.get(url) + response = _session.get(url) if response.status_code != 200: raise RoboflowError(response.text) result = response.json() @@ -52,7 +56,7 @@ def get_workspace(api_key, workspace_url): def get_project(api_key, workspace_url, project_url): url = f"{API_URL}/{workspace_url}/{project_url}?api_key={api_key}" - response = requests.get(url) + response = _session.get(url) if response.status_code != 200: raise RoboflowError(response.text) result = response.json() @@ -64,7 +68,7 @@ def get_project_health(api_key, workspace_url, project_url, regenerate=False): url = f"{API_URL}/{workspace_url}/{project_url}/health?api_key={api_key}" if regenerate: url += "®enerate=true" - response = requests.get(url) + response = _session.get(url) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -99,7 +103,7 @@ def start_version_training( if epochs is not None: data["epochs"] = epochs - response = requests.post(url, json=data) + response = _session.post(url, json=data) if not response.ok: raise RoboflowError(response.text) return True @@ -122,7 +126,7 @@ def cancel_version_training( body: Dict[str, Union[str, int, bool]] = {} if continue_if_no_refund: body["continueIfNoRefund"] = True - response = requests.post(url, json=body) + response = _session.post(url, json=body) if not response.ok: raise RoboflowError(response.text) return response.json() if response.content else {"success": True} @@ -135,7 +139,7 @@ def stop_version_training(api_key: str, workspace_url: str, project_url: str, ve phase gracefully (mining or training). """ url = f"{API_URL}/{workspace_url}/{project_url}/{version}/train/stop?api_key={api_key}" - response = requests.post(url, json={}) + response = _session.post(url, json={}) if not response.ok: raise RoboflowError(response.text) return response.json() if response.content else {"success": True} @@ -149,7 +153,7 @@ def get_training_results(api_key: str, workspace_url: str, project_url: str, ver returns a minimal bundle with the produced model(s). """ url = f"{API_URL}/{workspace_url}/{project_url}/{version}/training/results?api_key={api_key}" - response = requests.get(url) + response = _session.get(url) if not response.ok: raise RoboflowError(response.text) return response.json() @@ -166,7 +170,7 @@ def list_project_models( url = f"{API_URL}/{workspace_url}/{project_url}/models?api_key={api_key}" if group: url += f"&group={urllib.parse.quote(group, safe='')}" - response = requests.get(url) + response = _session.get(url) if not response.ok: raise RoboflowError(response.text) return response.json() @@ -176,7 +180,7 @@ def get_model_by_url(api_key: str, workspace_url: str, model_url: str): """Fetch a single model by its URL slug.""" encoded = urllib.parse.quote(model_url, safe="/") url = f"{API_URL}/models/{workspace_url}/{encoded}?api_key={api_key}" - response = requests.get(url) + response = _session.get(url) if not response.ok: raise RoboflowError(response.text) return response.json() @@ -192,7 +196,7 @@ def favorite_nas_model(api_key: str, workspace_url: str, model_id: str, *, starr """ encoded = urllib.parse.quote(model_id, safe="") url = f"{API_URL}/{workspace_url}/models/{encoded}/favorite?api_key={api_key}" - response = requests.post(url, json={"starred": bool(starred)}) + response = _session.post(url, json={"starred": bool(starred)}) if not response.ok: raise RoboflowError(response.text) return response.json() @@ -219,7 +223,7 @@ def get_version(api_key: str, workspace_url: str, project_url: str, version: str if nocache: url += "&nocache=true" - response = requests.get(url) + response = _session.get(url) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -242,7 +246,7 @@ def get_version_export( Raises RoboflowError on non-200/202 statuses or invalid/missing JSON when 200/202. """ url = f"{API_URL}/{workspace_url}/{project_url}/{version}/{format}?api_key={api_key}&nocache=true" - response = requests.get(url) + response = _session.get(url) # Non-success codes other than 202 are errors if response.status_code not in (200, 202): @@ -347,7 +351,7 @@ def workspace_search( if continuation_token is not None: payload["continuationToken"] = continuation_token - response = requests.post(url, json=payload) + response = _session.post(url, json=payload) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -372,7 +376,7 @@ def workspace_delete_images( RoboflowError: On non-200 response status codes. """ url = f"{API_URL}/{workspace_url}/images?api_key={api_key}" - response = requests.delete(url, json={"images": image_ids}) + response = _session.delete(url, json={"images": image_ids}) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -416,7 +420,7 @@ def update_image_metadata( if remove_tags is not None: body["removeTags"] = remove_tags - response = requests.post(url, params={"api_key": api_key}, json=body) + response = _session.post(url, params={"api_key": api_key}, json=body) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -442,7 +446,7 @@ def batch_update_image_metadata( RoboflowError: On non-202 response. """ url = f"{API_URL}/{workspace_url}/images/metadata" - response = requests.post(url, params={"api_key": api_key}, json={"updates": updates}) + response = _session.post(url, params={"api_key": api_key}, json={"updates": updates}) if response.status_code != 202: raise RoboflowError(response.text) return response.json() @@ -496,7 +500,7 @@ def upload_image( m = MultipartEncoder(fields=fields) try: - response = requests.post(upload_url, data=m, headers={"Content-Type": m.content_type}, timeout=(300, 300)) + response = _session.post(upload_url, data=m, headers={"Content-Type": m.content_type}, timeout=(300, 300)) except RequestException as e: raise ImageUploadError(str(e)) from e @@ -511,7 +515,7 @@ def upload_image( try: # Get response - response = requests.post(upload_url, timeout=(300, 300)) + response = _session.post(upload_url, timeout=(300, 300)) except RequestException as e: raise ImageUploadError(str(e)) from e @@ -569,7 +573,7 @@ def save_annotation( ) try: - response = requests.post( + response = _session.post( upload_url, data=json.dumps({"annotationFile": annotation_string, "labelmap": annotation_labelmap}), headers={"Content-Type": "application/json"}, @@ -682,7 +686,7 @@ def init_zip_upload(api_key, workspace_url, project_url, split=None, tags=None, body["tags"] = tags if batch_name is not None: body["batchName"] = batch_name - response = requests.post(url, params={"api_key": api_key}, json=body) + response = _session.post(url, params={"api_key": api_key}, json=body) if response.status_code not in (200, 201): raise RoboflowError(response.text) return response.json() @@ -691,7 +695,7 @@ def init_zip_upload(api_key, workspace_url, project_url, split=None, tags=None, def upload_zip_to_signed_url(signed_url, zip_path) -> None: """PUT the zip file to the GCS signed URL returned by init_zip_upload.""" with open(zip_path, "rb") as fh: - response = requests.put( + response = _session.put( signed_url, data=fh, headers={"Content-Type": "application/zip"}, @@ -704,7 +708,7 @@ def upload_zip_to_signed_url(signed_url, zip_path) -> None: def get_zip_upload_status(api_key, workspace_url, task_id) -> dict: """GET /{ws}/upload/zip/{task_id} — poll status of an async zip upload.""" url = f"{API_URL}/{workspace_url}/upload/zip/{task_id}" - response = requests.get(url, params={"api_key": api_key}) + response = _session.get(url, params={"api_key": api_key}) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -717,7 +721,7 @@ def get_zip_upload_status(api_key, workspace_url, task_id) -> dict: def list_batches(api_key, workspace_url, project_url): """GET /{ws}/{proj}/batches — list annotation batches.""" - response = requests.get(f"{API_URL}/{workspace_url}/{project_url}/batches", params={"api_key": api_key}) + response = _session.get(f"{API_URL}/{workspace_url}/{project_url}/batches", params={"api_key": api_key}) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -725,7 +729,7 @@ def list_batches(api_key, workspace_url, project_url): def get_batch(api_key, workspace_url, project_url, batch_id): """GET /{ws}/{proj}/batches/{batch_id} — get batch details.""" - response = requests.get(f"{API_URL}/{workspace_url}/{project_url}/batches/{batch_id}", params={"api_key": api_key}) + response = _session.get(f"{API_URL}/{workspace_url}/{project_url}/batches/{batch_id}", params={"api_key": api_key}) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -733,7 +737,7 @@ def get_batch(api_key, workspace_url, project_url, batch_id): def list_annotation_jobs(api_key, workspace_url, project_url): """GET /{ws}/{proj}/jobs — list annotation jobs.""" - response = requests.get(f"{API_URL}/{workspace_url}/{project_url}/jobs", params={"api_key": api_key}) + response = _session.get(f"{API_URL}/{workspace_url}/{project_url}/jobs", params={"api_key": api_key}) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -741,7 +745,7 @@ def list_annotation_jobs(api_key, workspace_url, project_url): def get_annotation_job(api_key, workspace_url, project_url, job_id): """GET /{ws}/{proj}/jobs/{job_id} — get annotation job details.""" - response = requests.get(f"{API_URL}/{workspace_url}/{project_url}/jobs/{job_id}", params={"api_key": api_key}) + response = _session.get(f"{API_URL}/{workspace_url}/{project_url}/jobs/{job_id}", params={"api_key": api_key}) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -754,7 +758,7 @@ def create_annotation_job(api_key, workspace_url, project_url, *, name, batch_id payload["batchId"] = batch_id if assignees: payload["assignees"] = assignees - response = requests.post( + response = _session.post( f"{API_URL}/{workspace_url}/{project_url}/jobs", params={"api_key": api_key}, json=payload, @@ -771,7 +775,7 @@ def create_annotation_job(api_key, workspace_url, project_url, *, name, batch_id def list_folders(api_key, workspace_url): """GET /{ws}/groups — list project folders.""" - response = requests.get(f"{API_URL}/{workspace_url}/groups", params={"api_key": api_key}) + response = _session.get(f"{API_URL}/{workspace_url}/groups", params={"api_key": api_key}) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -779,7 +783,7 @@ def list_folders(api_key, workspace_url): def get_folder(api_key, workspace_url, group_id): """GET /{ws}/groups?groupId={id} — get folder details.""" - response = requests.get( + response = _session.get( f"{API_URL}/{workspace_url}/groups", params={"api_key": api_key, "groupId": group_id}, ) @@ -795,7 +799,7 @@ def create_folder(api_key, workspace_url, name, *, parent_id=None, project_ids=N payload["parent_id"] = parent_id if project_ids: payload["projects"] = project_ids - response = requests.post( + response = _session.post( f"{API_URL}/{workspace_url}/groups", params={"api_key": api_key}, json=payload, @@ -810,7 +814,7 @@ def update_folder(api_key, workspace_url, group_id, *, name=None): payload: Dict[str, Optional[str]] = {} if name: payload["name"] = name - response = requests.post( + response = _session.post( f"{API_URL}/{workspace_url}/groups/{group_id}", params={"api_key": api_key}, json=payload, @@ -822,7 +826,7 @@ def update_folder(api_key, workspace_url, group_id, *, name=None): def delete_folder(api_key, workspace_url, group_id): """DELETE /{ws}/groups/{id} — delete a project folder.""" - response = requests.delete( + response = _session.delete( f"{API_URL}/{workspace_url}/groups/{group_id}", params={"api_key": api_key}, ) @@ -835,7 +839,7 @@ def delete_folder(api_key, workspace_url, group_id): def add_projects_to_folder(api_key, workspace_url, group_id, project_ids): """PATCH /{ws}/groups/{id}/projects — add projects to a folder.""" - response = requests.patch( + response = _session.patch( f"{API_URL}/{workspace_url}/groups/{group_id}/projects", params={"api_key": api_key}, json={"projects": project_ids}, @@ -846,7 +850,7 @@ def add_projects_to_folder(api_key, workspace_url, group_id, project_ids): def remove_projects_from_folder(api_key, workspace_url, group_id, project_ids): """DELETE /{ws}/groups/{id}/projects — remove projects from a folder.""" - response = requests.delete( + response = _session.delete( f"{API_URL}/{workspace_url}/groups/{group_id}/projects", params={"api_key": api_key}, json={"projects": project_ids}, @@ -909,7 +913,7 @@ def _normalize_workflow_config(config): def list_workflows(api_key, workspace_url): """GET /{ws}/workflows — list workflows.""" - response = requests.get(f"{API_URL}/{workspace_url}/workflows", params={"api_key": api_key}) + response = _session.get(f"{API_URL}/{workspace_url}/workflows", params={"api_key": api_key}) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -917,7 +921,7 @@ def list_workflows(api_key, workspace_url): def get_workflow(api_key, workspace_url, workflow_url): """GET /{ws}/workflows/{url} — get workflow details.""" - response = requests.get( + response = _session.get( f"{API_URL}/{workspace_url}/workflows/{workflow_url}", params={"api_key": api_key}, ) @@ -956,7 +960,7 @@ def create_workflow(api_key, workspace_url, *, name, url=None, config=None, temp "template": template, "config": config, } - response = requests.post( + response = _session.post( f"{API_URL}/{workspace_url}/createWorkflow", params=params, ) @@ -987,7 +991,7 @@ def update_workflow(api_key, workspace_url, *, workflow_id, workflow_name, workf "url": workflow_url, "config": config, } - response = requests.post( + response = _session.post( f"{API_URL}/{workspace_url}/updateWorkflow", params={"api_key": api_key}, json=payload, @@ -999,7 +1003,7 @@ def update_workflow(api_key, workspace_url, *, workflow_id, workflow_name, workf def list_workflow_versions(api_key, workspace_url, workflow_url): """GET /{ws}/workflows/{url}/versions — list workflow versions.""" - response = requests.get( + response = _session.get( f"{API_URL}/{workspace_url}/workflows/{workflow_url}/versions", params={"api_key": api_key}, ) @@ -1026,7 +1030,7 @@ def fork_project( payload["url"] = url if source_project_slug: payload["source_project"] = source_project_slug - response = requests.post( + response = _session.post( f"{API_URL}/{dest_workspace}/projects/fork", params={"api_key": api_key}, json=payload, @@ -1049,7 +1053,7 @@ def get_async_task(api_key, workspace_url, task_id): # ``/``, ``?`` or ``#`` cannot mutate the request path (and still send # the api_key with it). encoded_task_id = quote(task_id, safe="") - response = requests.get( + response = _session.get( f"{API_URL}/{workspace_url}/asynctasks/{encoded_task_id}", params={"api_key": api_key}, ) @@ -1067,7 +1071,7 @@ def get_async_task_at(api_key, polling_url): only attach the api_key. Falls back to ``get_async_task`` callers when no server-supplied URL is available. """ - response = requests.get(polling_url, params={"api_key": api_key}) + response = _session.get(polling_url, params={"api_key": api_key}) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -1091,7 +1095,7 @@ def fork_workflow(api_key, workspace_url, *, source_workspace, source_workflow, payload["name"] = name if url: payload["url"] = url - response = requests.post( + response = _session.post( f"{API_URL}/{workspace_url}/forkWorkflow", params={"api_key": api_key}, json=payload, @@ -1108,7 +1112,7 @@ def fork_workflow(api_key, workspace_url, *, source_workspace, source_workflow, def get_billing_usage(api_key, workspace_url): """POST /{ws}/billing-usage-report — get billing usage report.""" - response = requests.post( + response = _session.post( f"{API_URL}/{workspace_url}/billing-usage-report", params={"api_key": api_key}, ) @@ -1119,7 +1123,7 @@ def get_billing_usage(api_key, workspace_url): def get_plan_info(api_key): """GET /usage/plan — get workspace plan info and limits.""" - response = requests.get(f"{API_URL}/usage/plan", params={"api_key": api_key}) + response = _session.get(f"{API_URL}/usage/plan", params={"api_key": api_key}) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -1132,7 +1136,7 @@ def get_labeling_stats(api_key, workspace_url, *, start_date=None, end_date=None params["startDate"] = start_date if end_date: params["endDate"] = end_date - response = requests.get(f"{API_URL}/{workspace_url}/stats", params=params) + response = _session.get(f"{API_URL}/{workspace_url}/stats", params=params) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -1145,7 +1149,7 @@ def get_labeling_stats(api_key, workspace_url, *, start_date=None, end_date=None def get_video_job_status(api_key, job_id): """GET /videoinfer?jobId={id} — check video inference job status.""" - response = requests.get(f"{API_URL}/videoinfer", params={"api_key": api_key, "job_id": job_id}) + response = _session.get(f"{API_URL}/videoinfer", params={"api_key": api_key, "job_id": job_id}) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -1163,7 +1167,7 @@ def search_universe(query, *, api_key=None, project_type=None, limit=12, page=1) params["api_key"] = api_key if project_type: params["type"] = project_type - response = requests.get(f"{API_URL}/universe/search", params=params) + response = _session.get(f"{API_URL}/universe/search", params=params) if response.status_code != 200: raise RoboflowError(response.text) return response.json() @@ -1207,7 +1211,7 @@ def delete_project(api_key, workspace_url, project_url): window; after 30 days the cleanup cron permanently removes it. """ url = f"{API_URL}/{workspace_url}/{project_url}?api_key={api_key}" - response = requests.delete(url) + response = _session.delete(url) if response.status_code != 200: _raise_for_trash_response(response) return response.json() @@ -1219,7 +1223,7 @@ def delete_version(api_key, workspace_url, project_url, version): Any in-flight training on the version will be cancelled automatically. """ url = f"{API_URL}/{workspace_url}/{project_url}/{version}?api_key={api_key}" - response = requests.delete(url) + response = _session.delete(url) if response.status_code != 200: _raise_for_trash_response(response) return response.json() @@ -1230,7 +1234,7 @@ def delete_workflow(api_key, workspace_url, workflow_url): (30-day retention). Restore via `restore_trash_item(..., "workflow", ...)`. """ url = f"{API_URL}/{workspace_url}/workflows/{workflow_url}?api_key={api_key}" - response = requests.delete(url) + response = _session.delete(url) if response.status_code != 200: _raise_for_trash_response(response) return response.json() @@ -1244,7 +1248,7 @@ def list_trash(api_key, workspace_url): `name`, `deletedAt`, `scheduledCleanupAt`, and (for versions) `parentId`. """ url = f"{API_URL}/{workspace_url}/trash?api_key={api_key}" - response = requests.get(url) + response = _session.get(url) if response.status_code != 200: _raise_for_trash_response(response) return response.json() @@ -1260,7 +1264,7 @@ def restore_trash_item(api_key, workspace_url, item_type, item_id, parent_id=Non payload = {"type": item_type, "id": item_id} if parent_id is not None: payload["parentId"] = parent_id - response = requests.post(url, json=payload) + response = _session.post(url, json=payload) if response.status_code != 200: _raise_for_trash_response(response) return response.json() @@ -1340,7 +1344,7 @@ def _eval_get(api_key, workspace_url, path, params=None): if value is not None: query[key] = value url = f"{API_URL}/{workspace_url}/model-evals{path}" - response = requests.get(url, params=query) + response = _session.get(url, params=query) if response.status_code != 200: raise _model_eval_error_for(response) return response.json() diff --git a/tests/adapters/test_rfapi_model_evals.py b/tests/adapters/test_rfapi_model_evals.py index 41cc6942..66c7a16a 100644 --- a/tests/adapters/test_rfapi_model_evals.py +++ b/tests/adapters/test_rfapi_model_evals.py @@ -18,7 +18,7 @@ def _resp(status: int, body): class TestListModelEvals(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success_no_filters(self, mock_get): mock_get.return_value = _resp(200, {"evals": [{"id": "e1", "status": "done"}]}) @@ -30,7 +30,7 @@ def test_success_no_filters(self, mock_get): self.assertEqual(url, f"{API_URL}/ws/model-evals") self.assertEqual(params, {"api_key": "k"}) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success_with_filters(self, mock_get): mock_get.return_value = _resp(200, {"evals": []}) @@ -49,7 +49,7 @@ def test_success_with_filters(self, mock_get): }, ) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_omits_none_filters(self, mock_get): mock_get.return_value = _resp(200, {"evals": []}) @@ -59,7 +59,7 @@ def test_omits_none_filters(self, mock_get): self.assertNotIn("limit", params) self.assertEqual(params["status"], "done") - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_404_raises_not_found(self, mock_get): mock_get.return_value = _resp(404, {"error": "model_eval_not_found", "message": "nope"}) @@ -69,7 +69,7 @@ def test_404_raises_not_found(self, mock_get): class TestGetModelEval(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success(self, mock_get): mock_get.return_value = _resp(200, {"id": "e1", "status": "done", "summary": {"mAP": 0.9}}) @@ -83,7 +83,7 @@ def test_success(self, mock_get): class TestPanelEndpoints(unittest.TestCase): """Each panel endpoint forwards path + params correctly.""" - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_map_results_url(self, mock_get): mock_get.return_value = _resp(200, {"splits": {}}) @@ -92,7 +92,7 @@ def test_map_results_url(self, mock_get): url = mock_get.call_args[0][0] self.assertEqual(url, f"{API_URL}/ws/model-evals/e1/map-results") - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_confidence_sweep_url(self, mock_get): mock_get.return_value = _resp(200, {"splits": {}}) @@ -101,7 +101,7 @@ def test_confidence_sweep_url(self, mock_get): url = mock_get.call_args[0][0] self.assertEqual(url, f"{API_URL}/ws/model-evals/e1/confidence-sweep") - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_performance_by_class_passes_split(self, mock_get): mock_get.return_value = _resp(200, {"split": "valid", "classes": []}) @@ -110,7 +110,7 @@ def test_performance_by_class_passes_split(self, mock_get): params = mock_get.call_args.kwargs["params"] self.assertEqual(params["split"], "valid") - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_confusion_matrix_passes_params(self, mock_get): mock_get.return_value = _resp(200, {"matrix": []}) @@ -120,7 +120,7 @@ def test_confusion_matrix_passes_params(self, mock_get): self.assertEqual(params["split"], "test") self.assertEqual(params["confidence"], 30) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_image_predictions_pagination(self, mock_get): mock_get.return_value = _resp(200, {"images": []}) @@ -130,7 +130,7 @@ def test_image_predictions_pagination(self, mock_get): self.assertEqual(params["limit"], 50) self.assertEqual(params["offset"], 100) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_recommendations_url(self, mock_get): mock_get.return_value = _resp(200, {"recommendations": []}) @@ -139,7 +139,7 @@ def test_recommendations_url(self, mock_get): url = mock_get.call_args[0][0] self.assertEqual(url, f"{API_URL}/ws/model-evals/e1/recommendations") - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_vector_analysis_passes_confidence(self, mock_get): mock_get.return_value = _resp(200, {"clusters": []}) @@ -152,7 +152,7 @@ def test_vector_analysis_passes_confidence(self, mock_get): class TestErrorMapping(unittest.TestCase): """Typed errors are routed to the right exception subclass.""" - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_404_flat_envelope(self, mock_get): # Server returns the flat shape: {"error": "code", "message": "..."} mock_get.return_value = _resp(404, {"error": "model_eval_not_found", "message": "Eval 'x' not found"}) @@ -161,7 +161,7 @@ def test_404_flat_envelope(self, mock_get): rfapi.get_model_eval("k", "ws", "x") self.assertIn("Eval 'x' not found", str(ctx.exception)) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_404_status_code_fallback(self, mock_get): # No `error` field at all — fall back to the status code mapping. mock_get.return_value = _resp(404, {"message": "something went wrong"}) @@ -169,28 +169,28 @@ def test_404_status_code_fallback(self, mock_get): with self.assertRaises(rfapi.ModelEvalNotFoundError): rfapi.get_model_eval("k", "ws", "x") - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_409_not_done(self, mock_get): mock_get.return_value = _resp(409, {"error": "model_eval_not_done", "message": "Eval still running"}) with self.assertRaises(rfapi.ModelEvalNotDoneError): rfapi.get_model_eval_map_results("k", "ws", "x") - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_400_invalid_split(self, mock_get): mock_get.return_value = _resp(400, {"error": "invalid_split", "message": "Invalid split"}) with self.assertRaises(rfapi.InvalidSplitError): rfapi.get_model_eval_performance_by_class("k", "ws", "x", split="all") - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_400_invalid_confidence(self, mock_get): mock_get.return_value = _resp(400, {"error": "invalid_confidence", "message": "out of range"}) with self.assertRaises(rfapi.InvalidConfidenceError): rfapi.get_model_eval_confusion_matrix("k", "ws", "x", confidence=200) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_unknown_404_falls_back_to_not_found(self, mock_get): # 404 without a recognised code still maps by status code (forward-compat). mock_get.return_value = _resp(404, {"error": "some_new_code", "message": "?"}) @@ -198,7 +198,7 @@ def test_unknown_404_falls_back_to_not_found(self, mock_get): with self.assertRaises(rfapi.ModelEvalNotFoundError): rfapi.get_model_eval("k", "ws", "x") - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_unknown_500_raises_generic_roboflow_error(self, mock_get): mock_get.return_value = _resp(500, {"error": "server_oops", "message": "boom"}) @@ -208,7 +208,7 @@ def test_unknown_500_raises_generic_roboflow_error(self, mock_get): self.assertNotIsInstance(ctx.exception, rfapi.ModelEvalNotFoundError) self.assertNotIsInstance(ctx.exception, rfapi.ModelEvalNotDoneError) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_non_json_body_falls_back_to_text(self, mock_get): # Some misbehaving proxies return HTML 502s — make sure we don't crash. bad = MagicMock(status_code=502, text="Bad Gateway") diff --git a/tests/adapters/test_rfapi_phase2.py b/tests/adapters/test_rfapi_phase2.py index dc03e800..07c17afa 100644 --- a/tests/adapters/test_rfapi_phase2.py +++ b/tests/adapters/test_rfapi_phase2.py @@ -8,7 +8,7 @@ class TestListBatches(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success(self, mock_get): from roboflow.adapters.rfapi import list_batches @@ -18,7 +18,7 @@ def test_success(self, mock_get): mock_get.assert_called_once() self.assertIn("/ws/proj/batches", mock_get.call_args[0][0]) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_error(self, mock_get): from roboflow.adapters.rfapi import RoboflowError, list_batches @@ -28,7 +28,7 @@ def test_error(self, mock_get): class TestGetBatch(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success(self, mock_get): from roboflow.adapters.rfapi import get_batch @@ -37,7 +37,7 @@ def test_success(self, mock_get): self.assertEqual(result, {"batch": {"id": "b1"}}) self.assertIn("/ws/proj/batches/b1", mock_get.call_args[0][0]) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_error(self, mock_get): from roboflow.adapters.rfapi import RoboflowError, get_batch @@ -47,7 +47,7 @@ def test_error(self, mock_get): class TestListAnnotationJobs(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success(self, mock_get): from roboflow.adapters.rfapi import list_annotation_jobs @@ -56,7 +56,7 @@ def test_success(self, mock_get): self.assertEqual(result, {"jobs": []}) self.assertIn("/ws/proj/jobs", mock_get.call_args[0][0]) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_error(self, mock_get): from roboflow.adapters.rfapi import RoboflowError, list_annotation_jobs @@ -66,7 +66,7 @@ def test_error(self, mock_get): class TestGetAnnotationJob(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success(self, mock_get): from roboflow.adapters.rfapi import get_annotation_job @@ -75,7 +75,7 @@ def test_success(self, mock_get): self.assertEqual(result["job"]["id"], "j1") self.assertIn("/ws/proj/jobs/j1", mock_get.call_args[0][0]) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_error(self, mock_get): from roboflow.adapters.rfapi import RoboflowError, get_annotation_job @@ -85,7 +85,7 @@ def test_error(self, mock_get): class TestCreateAnnotationJob(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_success(self, mock_post): from roboflow.adapters.rfapi import create_annotation_job @@ -99,7 +99,7 @@ def test_success(self, mock_post): self.assertEqual(payload["name"], "my-job") self.assertEqual(payload["batchId"], "b1") - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_success_200(self, mock_post): from roboflow.adapters.rfapi import create_annotation_job @@ -107,7 +107,7 @@ def test_success_200(self, mock_post): result = create_annotation_job("key", "ws", "proj", name="my-job") self.assertEqual(result["job"]["id"], "j3") - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_with_assignees(self, mock_post): from roboflow.adapters.rfapi import create_annotation_job @@ -116,7 +116,7 @@ def test_with_assignees(self, mock_post): payload = mock_post.call_args[1]["json"] self.assertEqual(payload["assignees"], ["a@b.com"]) - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_error(self, mock_post): from roboflow.adapters.rfapi import RoboflowError, create_annotation_job @@ -126,7 +126,7 @@ def test_error(self, mock_post): class TestListFolders(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success(self, mock_get): from roboflow.adapters.rfapi import list_folders @@ -136,7 +136,7 @@ def test_success(self, mock_get): mock_get.assert_called_once() self.assertIn("/ws/groups", mock_get.call_args[0][0]) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_error(self, mock_get): from roboflow.adapters.rfapi import RoboflowError, list_folders @@ -146,7 +146,7 @@ def test_error(self, mock_get): class TestGetFolder(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success(self, mock_get): from roboflow.adapters.rfapi import get_folder @@ -156,7 +156,7 @@ def test_success(self, mock_get): call_kwargs = mock_get.call_args[1] self.assertEqual(call_kwargs["params"]["groupId"], "g1") - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_error(self, mock_get): from roboflow.adapters.rfapi import RoboflowError, get_folder @@ -166,7 +166,7 @@ def test_error(self, mock_get): class TestCreateFolder(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_success(self, mock_post): from roboflow.adapters.rfapi import create_folder @@ -176,7 +176,7 @@ def test_success(self, mock_post): payload = mock_post.call_args[1]["json"] self.assertEqual(payload["name"], "NewFolder") - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_with_parent_and_projects(self, mock_post): from roboflow.adapters.rfapi import create_folder @@ -186,7 +186,7 @@ def test_with_parent_and_projects(self, mock_post): self.assertEqual(payload["parent_id"], "g1") self.assertEqual(payload["projects"], ["p1", "p2"]) - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_error(self, mock_post): from roboflow.adapters.rfapi import RoboflowError, create_folder @@ -196,7 +196,7 @@ def test_error(self, mock_post): class TestUpdateFolder(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_success(self, mock_post): from roboflow.adapters.rfapi import update_folder @@ -207,7 +207,7 @@ def test_success(self, mock_post): payload = mock_post.call_args[1]["json"] self.assertEqual(payload["name"], "Renamed") - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_error(self, mock_post): from roboflow.adapters.rfapi import RoboflowError, update_folder @@ -217,7 +217,7 @@ def test_error(self, mock_post): class TestDeleteFolder(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.delete") + @patch("roboflow.adapters.rfapi._session.delete") def test_success(self, mock_delete): from roboflow.adapters.rfapi import delete_folder @@ -226,7 +226,7 @@ def test_success(self, mock_delete): self.assertEqual(result["status"], "deleted") self.assertIn("/ws/groups/g1", mock_delete.call_args[0][0]) - @patch("roboflow.adapters.rfapi.requests.delete") + @patch("roboflow.adapters.rfapi._session.delete") def test_error(self, mock_delete): from roboflow.adapters.rfapi import RoboflowError, delete_folder @@ -236,7 +236,7 @@ def test_error(self, mock_delete): class TestListWorkflows(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success(self, mock_get): from roboflow.adapters.rfapi import list_workflows @@ -245,7 +245,7 @@ def test_success(self, mock_get): self.assertEqual(len(result["workflows"]), 1) self.assertIn("/ws/workflows", mock_get.call_args[0][0]) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_error(self, mock_get): from roboflow.adapters.rfapi import RoboflowError, list_workflows @@ -255,7 +255,7 @@ def test_error(self, mock_get): class TestGetWorkflow(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success(self, mock_get): from roboflow.adapters.rfapi import get_workflow @@ -264,7 +264,7 @@ def test_success(self, mock_get): self.assertEqual(result["workflow"]["url"], "wf1") self.assertIn("/ws/workflows/wf1", mock_get.call_args[0][0]) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_error(self, mock_get): from roboflow.adapters.rfapi import RoboflowError, get_workflow @@ -274,7 +274,7 @@ def test_error(self, mock_get): class TestCreateWorkflow(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_success(self, mock_post): from roboflow.adapters.rfapi import create_workflow @@ -286,7 +286,7 @@ def test_success(self, mock_post): params = mock_post.call_args[1]["params"] self.assertEqual(params["name"], "New Workflow") - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_auto_generates_url_slug(self, mock_post): from roboflow.adapters.rfapi import create_workflow @@ -295,7 +295,7 @@ def test_auto_generates_url_slug(self, mock_post): params = mock_post.call_args[1]["params"] self.assertEqual(params["url"], "my-workflow") - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_with_config_and_template(self, mock_post): from roboflow.adapters.rfapi import create_workflow @@ -306,7 +306,7 @@ def test_with_config_and_template(self, mock_post): self.assertEqual(params["config"], '{"a":1}') self.assertEqual(params["template"], '{"b":2}') - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_config_dict_serialized_to_string(self, mock_post): from roboflow.adapters.rfapi import create_workflow @@ -317,7 +317,7 @@ def test_config_dict_serialized_to_string(self, mock_post): self.assertIsInstance(params["config"], str) self.assertIsInstance(params["template"], str) - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_defaults_config_and_template(self, mock_post): from roboflow.adapters.rfapi import create_workflow @@ -327,7 +327,7 @@ def test_defaults_config_and_template(self, mock_post): self.assertEqual(params["config"], "{}") self.assertEqual(params["template"], "{}") - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_error(self, mock_post): from roboflow.adapters.rfapi import RoboflowError, create_workflow @@ -335,7 +335,7 @@ def test_error(self, mock_post): with self.assertRaises(RoboflowError): create_workflow("key", "ws", name="Bad") - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_bare_spec_dict_is_auto_wrapped(self, mock_post): """Docs-shaped workflow definitions get wrapped in {"specification": ...} so they match the backend's stored format and the inference server's @@ -350,7 +350,7 @@ def test_bare_spec_dict_is_auto_wrapped(self, mock_post): sent_config = _json.loads(mock_post.call_args[1]["params"]["config"]) self.assertEqual(sent_config, {"specification": bare}) - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_already_wrapped_config_is_not_double_wrapped(self, mock_post): import json as _json @@ -362,7 +362,7 @@ def test_already_wrapped_config_is_not_double_wrapped(self, mock_post): sent_config = _json.loads(mock_post.call_args[1]["params"]["config"]) self.assertEqual(sent_config, wrapped) - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_bare_spec_json_string_is_auto_wrapped(self, mock_post): """JSON strings are parsed, wrapped if bare, and re-serialized.""" import json as _json @@ -375,7 +375,7 @@ def test_bare_spec_json_string_is_auto_wrapped(self, mock_post): sent_config = _json.loads(mock_post.call_args[1]["params"]["config"]) self.assertEqual(sent_config, {"specification": {"version": "1.0", "steps": []}}) - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_non_workflow_dict_is_not_wrapped(self, mock_post): """Dicts that don't look like a workflow spec (no version/inputs/steps/outputs) are passed through unchanged to avoid second-guessing custom payloads.""" @@ -471,7 +471,7 @@ def test_wrapped_output_uses_compact_separators(self): class TestUpdateWorkflow(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_success(self, mock_post): from roboflow.adapters.rfapi import update_workflow @@ -488,7 +488,7 @@ def test_success(self, mock_post): # config dict should be serialized to string self.assertIsInstance(payload["config"], str) - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_config_string_passthrough(self, mock_post): from roboflow.adapters.rfapi import update_workflow @@ -497,7 +497,7 @@ def test_config_string_passthrough(self, mock_post): payload = mock_post.call_args[1]["json"] self.assertEqual(payload["config"], '{"a":1}') - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_bare_spec_dict_is_auto_wrapped_on_update(self, mock_post): import json as _json @@ -509,7 +509,7 @@ def test_bare_spec_dict_is_auto_wrapped_on_update(self, mock_post): sent_config = _json.loads(mock_post.call_args[1]["json"]["config"]) self.assertEqual(sent_config, {"specification": bare}) - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_error(self, mock_post): from roboflow.adapters.rfapi import RoboflowError, update_workflow @@ -519,7 +519,7 @@ def test_error(self, mock_post): class TestListWorkflowVersions(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success(self, mock_get): from roboflow.adapters.rfapi import list_workflow_versions @@ -528,7 +528,7 @@ def test_success(self, mock_get): self.assertEqual(len(result["versions"]), 1) self.assertIn("/ws/workflows/wf1/versions", mock_get.call_args[0][0]) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_error(self, mock_get): from roboflow.adapters.rfapi import RoboflowError, list_workflow_versions @@ -538,7 +538,7 @@ def test_error(self, mock_get): class TestForkProject(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_success_with_url(self, mock_post): from roboflow.adapters.rfapi import fork_project @@ -551,7 +551,7 @@ def test_success_with_url(self, mock_post): payload = mock_post.call_args[1]["json"] self.assertEqual(payload, {"url": "source-ws/source-project"}) - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_success_with_explicit_source_slug(self, mock_post): from roboflow.adapters.rfapi import fork_project @@ -566,7 +566,7 @@ def test_success_with_explicit_source_slug(self, mock_post): payload = mock_post.call_args[1]["json"] self.assertEqual(payload, {"source_project": "source-project"}) - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_error(self, mock_post): from roboflow.adapters.rfapi import RoboflowError, fork_project @@ -574,7 +574,7 @@ def test_error(self, mock_post): with self.assertRaises(RoboflowError): fork_project("key", "ws", url="source-ws/source-project") - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_any_2xx_accepted(self, mock_post): """#8 — accept any 2xx so the SDK doesn't break if the backend ever returns 200 (sync result) or 201 (created) instead of 202. @@ -592,7 +592,7 @@ def test_any_2xx_accepted(self, mock_post): class TestGetAsyncTask(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success(self, mock_get): from roboflow.adapters.rfapi import get_async_task @@ -603,7 +603,7 @@ def test_success(self, mock_get): self.assertEqual(result["status"], "running") self.assertIn("/ws/asynctasks/task-1", mock_get.call_args[0][0]) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_malformed_task_id_is_url_encoded(self, mock_get): """A task_id containing path/query/fragment characters must not silently mutate the request path. Each unsafe char is percent-encoded @@ -629,7 +629,7 @@ def test_malformed_task_id_is_url_encoded(self, mock_get): self.assertNotIn("/asynctasks/../task", called_url) self.assertNotIn("?secret=1", called_url.split("/asynctasks/", 1)[1]) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_get_async_task_at_uses_supplied_url(self, mock_get): """``get_async_task_at`` hits the server-supplied polling URL verbatim (modulo the api_key query param), so polling stays on the @@ -642,7 +642,7 @@ def test_get_async_task_at_uses_supplied_url(self, mock_get): self.assertEqual(mock_get.call_args[0][0], "https://other.host/ws/asynctasks/task-1") self.assertEqual(mock_get.call_args[1]["params"], {"api_key": "key"}) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_error(self, mock_get): from roboflow.adapters.rfapi import RoboflowError, get_async_task @@ -652,7 +652,7 @@ def test_error(self, mock_get): class TestGetAsyncTaskAt(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_polling_url_used_verbatim(self, mock_get): """When the server returns a fully-qualified polling URL, the SDK must hit it as-is (potentially on a different host than ``API_URL``) and @@ -671,7 +671,7 @@ def test_polling_url_used_verbatim(self, mock_get): # api_key tacked on as a param. self.assertEqual(mock_get.call_args[1]["params"], {"api_key": "api-key"}) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_error_on_non_200(self, mock_get): from roboflow.adapters.rfapi import RoboflowError, get_async_task_at @@ -681,7 +681,7 @@ def test_error_on_non_200(self, mock_get): class TestForkWorkflow(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_success(self, mock_post): from roboflow.adapters.rfapi import fork_workflow @@ -693,7 +693,7 @@ def test_success(self, mock_post): self.assertEqual(payload["source_workspace"], "src-ws") self.assertEqual(payload["source_workflow"], "wf1") - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_success_200(self, mock_post): from roboflow.adapters.rfapi import fork_workflow @@ -701,7 +701,7 @@ def test_success_200(self, mock_post): result = fork_workflow("key", "ws", source_workspace="src-ws", source_workflow="wf2") self.assertEqual(result["workflow"]["url"], "forked2") - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_with_name_and_url(self, mock_post): from roboflow.adapters.rfapi import fork_workflow @@ -713,7 +713,7 @@ def test_with_name_and_url(self, mock_post): self.assertEqual(payload["name"], "Custom Fork") self.assertEqual(payload["url"], "custom-fork") - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_error(self, mock_post): from roboflow.adapters.rfapi import RoboflowError, fork_workflow @@ -723,7 +723,7 @@ def test_error(self, mock_post): class TestGetBillingUsage(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_success(self, mock_post): from roboflow.adapters.rfapi import get_billing_usage @@ -732,7 +732,7 @@ def test_success(self, mock_post): self.assertEqual(result["usage"]["credits"], 100) self.assertIn("/ws/billing-usage-report", mock_post.call_args[0][0]) - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_error(self, mock_post): from roboflow.adapters.rfapi import RoboflowError, get_billing_usage @@ -742,7 +742,7 @@ def test_error(self, mock_post): class TestGetPlanInfo(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success(self, mock_get): from roboflow.adapters.rfapi import get_plan_info @@ -751,7 +751,7 @@ def test_success(self, mock_get): self.assertEqual(result["plan"], "starter") self.assertIn("/usage/plan", mock_get.call_args[0][0]) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_error(self, mock_get): from roboflow.adapters.rfapi import RoboflowError, get_plan_info @@ -761,7 +761,7 @@ def test_error(self, mock_get): class TestGetLabelingStats(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success(self, mock_get): from roboflow.adapters.rfapi import get_labeling_stats @@ -770,7 +770,7 @@ def test_success(self, mock_get): self.assertEqual(result["stats"]["labeled"], 50) self.assertIn("/ws/stats", mock_get.call_args[0][0]) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_error(self, mock_get): from roboflow.adapters.rfapi import RoboflowError, get_labeling_stats @@ -780,7 +780,7 @@ def test_error(self, mock_get): class TestGetVideoJobStatus(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success(self, mock_get): from roboflow.adapters.rfapi import get_video_job_status @@ -790,7 +790,7 @@ def test_success(self, mock_get): call_kwargs = mock_get.call_args[1] self.assertEqual(call_kwargs["params"]["job_id"], "job-123") - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_error(self, mock_get): from roboflow.adapters.rfapi import RoboflowError, get_video_job_status @@ -800,7 +800,7 @@ def test_error(self, mock_get): class TestSearchUniverse(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_success(self, mock_get): from roboflow.adapters.rfapi import search_universe @@ -812,7 +812,7 @@ def test_success(self, mock_get): call_kwargs = mock_get.call_args[1] self.assertEqual(call_kwargs["params"]["q"], "cats") - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_with_type_and_limit(self, mock_get): from roboflow.adapters.rfapi import search_universe @@ -823,7 +823,7 @@ def test_with_type_and_limit(self, mock_get): self.assertEqual(call_kwargs["params"]["limit"], 5) self.assertEqual(call_kwargs["params"]["page"], 2) - @patch("roboflow.adapters.rfapi.requests.get") + @patch("roboflow.adapters.rfapi._session.get") def test_error(self, mock_get): from roboflow.adapters.rfapi import RoboflowError, search_universe @@ -833,7 +833,7 @@ def test_error(self, mock_get): class TestUpdateImageMetadata(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_success(self, mock_post): from roboflow.adapters.rfapi import update_image_metadata @@ -846,7 +846,7 @@ def test_success(self, mock_post): self.assertEqual(payload["addTags"], ["tag1"]) self.assertEqual(payload["removeTags"], ["old"]) - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_only_sends_provided_fields(self, mock_post): from roboflow.adapters.rfapi import update_image_metadata @@ -855,7 +855,7 @@ def test_only_sends_provided_fields(self, mock_post): payload = mock_post.call_args[1]["json"] self.assertEqual(payload, {"addTags": ["foo"]}) - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_metadata_and_tags(self, mock_post): from roboflow.adapters.rfapi import update_image_metadata @@ -865,7 +865,7 @@ def test_metadata_and_tags(self, mock_post): self.assertEqual(payload["metadata"], {"cam": "1"}) self.assertEqual(payload["addTags"], ["review"]) - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_error_404(self, mock_post): from roboflow.adapters.rfapi import RoboflowError, update_image_metadata @@ -873,7 +873,7 @@ def test_error_404(self, mock_post): with self.assertRaises(RoboflowError): update_image_metadata("key", "ws", "img-1", add_tags=["x"]) - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_image_id_url_encoded(self, mock_post): from roboflow.adapters.rfapi import update_image_metadata @@ -885,7 +885,7 @@ def test_image_id_url_encoded(self, mock_post): class TestBatchUpdateImageMetadata(unittest.TestCase): - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_success(self, mock_post): from roboflow.adapters.rfapi import batch_update_image_metadata @@ -898,7 +898,7 @@ def test_success(self, mock_post): payload = mock_post.call_args[1]["json"] self.assertEqual(payload, {"updates": updates}) - @patch("roboflow.adapters.rfapi.requests.post") + @patch("roboflow.adapters.rfapi._session.post") def test_error_400(self, mock_post): from roboflow.adapters.rfapi import RoboflowError, batch_update_image_metadata