Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/domo_sdk/async_clients/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ async def _get(self, url: str, params: dict[str, Any] | None = None) -> Any:
async def _list(self, url: str, params: dict[str, Any] | None = None) -> Any:
return await self.transport.get(url, params=params)

async def _update(self, url: str, body: Any, method: str = "PUT") -> Any:
async def _update(self, url: str, body: Any, method: str = "PUT", params: dict[str, Any] | None = None) -> Any:
if method == "PATCH":
return await self.transport.patch(url, body=body)
return await self.transport.put(url, body=body)
return await self.transport.put(url, body=body, params=params)

async def _delete(self, url: str) -> None:
await self.transport.delete(url)
async def _delete(self, url: str, params: dict[str, Any] | None = None) -> Any:
return await self.transport.delete(url, params=params)

async def _upload_csv(self, url: str, csv_data: bytes | str) -> Any:
return await self.transport.put_csv(url, body=csv_data)
Expand Down
8 changes: 4 additions & 4 deletions src/domo_sdk/clients/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ def _get(self, url: str, params: dict[str, Any] | None = None) -> Any:
def _list(self, url: str, params: dict[str, Any] | None = None) -> Any:
return self.transport.get(url, params=params)

def _update(self, url: str, body: Any, method: str = "PUT") -> Any:
def _update(self, url: str, body: Any, method: str = "PUT", params: dict[str, Any] | None = None) -> Any:
if method == "PATCH":
return self.transport.patch(url, body=body)
return self.transport.put(url, body=body)
return self.transport.put(url, body=body, params=params)

def _delete(self, url: str) -> None:
self.transport.delete(url)
def _delete(self, url: str, params: dict[str, Any] | None = None) -> Any:
return self.transport.delete(url, params=params)

def _upload_csv(self, url: str, csv_data: bytes | str) -> Any:
return self.transport.put_csv(url, body=csv_data)
Expand Down
11 changes: 7 additions & 4 deletions src/domo_sdk/transport/async_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,13 @@ async def post(self, url: str, body: Any = None, params: dict[str, Any] | None =
except httpx.ConnectError as err:
raise DomoConnectionError(url=url) from err

async def put(self, url: str, body: Any = None) -> Any:
async def put(self, url: str, body: Any = None, params: dict[str, Any] | None = None) -> Any:
headers = await self._get_headers(content_type="application/json")
full_url = self._build_url(url)
client = await self._get_client()
start = time.time()
try:
response = await client.put(full_url, headers=headers, json=body)
response = await client.put(full_url, headers=headers, params=params or {}, json=body)
self._log_timing("PUT", url, time.time() - start)
self._handle_response(response, url)
if response.status_code == 204 or not response.content:
Expand Down Expand Up @@ -171,15 +171,18 @@ async def patch(self, url: str, body: Any = None) -> Any:
except httpx.ConnectError as err:
raise DomoConnectionError(url=url) from err

async def delete(self, url: str) -> None:
async def delete(self, url: str, params: dict[str, Any] | None = None) -> Any:
headers = await self._get_headers()
full_url = self._build_url(url)
client = await self._get_client()
start = time.time()
try:
response = await client.delete(full_url, headers=headers)
response = await client.delete(full_url, headers=headers, params=params or {})
self._log_timing("DELETE", url, time.time() - start)
self._handle_response(response, url)
if response.status_code == 204 or not response.content:
return None
return response.json()
except httpx.TimeoutException as err:
raise DomoTimeoutError(url=url, timeout=self._timeout.read or DEFAULT_TIMEOUT) from err
except httpx.ConnectError as err:
Expand Down
13 changes: 9 additions & 4 deletions src/domo_sdk/transport/sync_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,15 @@ def post(self, url: str, body: Any = None, params: dict[str, Any] | None = None)
except requests.ConnectionError as err:
raise DomoConnectionError(url=url) from err

def put(self, url: str, body: Any = None) -> Any:
def put(self, url: str, body: Any = None, params: dict[str, Any] | None = None) -> Any:
headers = self._get_headers(content_type="application/json")
full_url = self._build_url(url)
data = json.dumps(body, default=str) if body is not None else None
start = time.time()
try:
response = self._session.put(full_url, headers=headers, data=data, timeout=self._timeout)
response = self._session.put(
full_url, headers=headers, params=params or {}, data=data, timeout=self._timeout,
)
self._log_timing("PUT", url, time.time() - start)
self._handle_response(response, url)
if response.status_code == 204 or not response.content:
Expand Down Expand Up @@ -149,14 +151,17 @@ def patch(self, url: str, body: Any = None) -> Any:
except requests.ConnectionError as err:
raise DomoConnectionError(url=url) from err

def delete(self, url: str) -> None:
def delete(self, url: str, params: dict[str, Any] | None = None) -> Any:
headers = self._get_headers()
full_url = self._build_url(url)
start = time.time()
try:
response = self._session.delete(full_url, headers=headers, timeout=self._timeout)
response = self._session.delete(full_url, headers=headers, params=params or {}, timeout=self._timeout)
self._log_timing("DELETE", url, time.time() - start)
self._handle_response(response, url)
if response.status_code == 204 or not response.content:
return None
return response.json()
except requests.Timeout as err:
raise DomoTimeoutError(url=url, timeout=self._timeout) from err
except requests.ConnectionError as err:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_clients/test_datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def test_delete_dataset(self) -> None:

client.delete("ds-123")

transport.delete.assert_called_once_with("/v1/datasets/ds-123")
transport.delete.assert_called_once_with("/v1/datasets/ds-123", params=None)


class TestDataSetClientQuery:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_clients/test_roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def test_delete_role(self) -> None:

client.delete(1)

transport.delete.assert_called_once_with("/authorization/v1/roles/1")
transport.delete.assert_called_once_with("/authorization/v1/roles/1", params=None)

def test_list_authorities(self) -> None:
"""GET /authorization/v1/roles/{id}/authorities."""
Expand Down
124 changes: 124 additions & 0 deletions tests/test_transport/test_async_transport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""Tests for AsyncTransport put/delete params support."""
from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest

from domo_sdk.transport.async_transport import AsyncTransport
from domo_sdk.transport.auth import AuthStrategy


def _make_transport() -> tuple[AsyncTransport, MagicMock]:
"""Create an AsyncTransport with a mocked auth strategy."""
auth = MagicMock(spec=AuthStrategy)
auth.get_base_url.return_value = "https://api.domo.com"
auth.get_headers_async = AsyncMock(return_value={"Authorization": "Bearer test"})
auth.auth_mode = "developer_token"
transport = AsyncTransport(auth)
return transport, auth


def _mock_response(status_code: int = 200, json_data: dict | list | None = None, content: bytes = b"{}") -> MagicMock:
"""Create a mock httpx.Response."""
response = MagicMock(spec=httpx.Response)
response.status_code = status_code
response.content = content
response.text = content.decode() if content else ""
if json_data is not None:
response.json.return_value = json_data
else:
response.json.return_value = {}
return response


class TestPutWithParams:
"""Tests for async put() with params support."""

@pytest.mark.asyncio
async def test_put_passes_params(self) -> None:
transport, _ = _make_transport()
mock_resp = _mock_response(200, {"ok": True})
mock_client = AsyncMock()
mock_client.put.return_value = mock_resp
mock_client.is_closed = False

with patch.object(transport, "_get_client", return_value=mock_client):
result = await transport.put("/test", body={"key": "val"}, params={"p": "1"})

_, kwargs = mock_client.put.call_args
assert kwargs["params"] == {"p": "1"}
assert result == {"ok": True}

@pytest.mark.asyncio
async def test_put_without_params_defaults_to_empty(self) -> None:
transport, _ = _make_transport()
mock_resp = _mock_response(200, {"ok": True})
mock_client = AsyncMock()
mock_client.put.return_value = mock_resp
mock_client.is_closed = False

with patch.object(transport, "_get_client", return_value=mock_client):
await transport.put("/test", body=None)

_, kwargs = mock_client.put.call_args
assert kwargs["params"] == {}


class TestDeleteWithParams:
"""Tests for async delete() with params and return value support."""

@pytest.mark.asyncio
async def test_delete_passes_params(self) -> None:
transport, _ = _make_transport()
mock_resp = _mock_response(200, {"Deleted": 3}, content=b'{"Deleted": 3}')
mock_client = AsyncMock()
mock_client.delete.return_value = mock_resp
mock_client.is_closed = False

with patch.object(transport, "_get_client", return_value=mock_client):
result = await transport.delete("/test", params={"ids": "1,2,3"})

_, kwargs = mock_client.delete.call_args
assert kwargs["params"] == {"ids": "1,2,3"}
assert result == {"Deleted": 3}

@pytest.mark.asyncio
async def test_delete_returns_none_on_204(self) -> None:
transport, _ = _make_transport()
mock_resp = _mock_response(204, content=b"")
mock_client = AsyncMock()
mock_client.delete.return_value = mock_resp
mock_client.is_closed = False

with patch.object(transport, "_get_client", return_value=mock_client):
result = await transport.delete("/test")

assert result is None

@pytest.mark.asyncio
async def test_delete_returns_json_body(self) -> None:
transport, _ = _make_transport()
mock_resp = _mock_response(200, {"Deleted": 5}, content=b'{"Deleted": 5}')
mock_client = AsyncMock()
mock_client.delete.return_value = mock_resp
mock_client.is_closed = False

with patch.object(transport, "_get_client", return_value=mock_client):
result = await transport.delete("/test")

assert result == {"Deleted": 5}

@pytest.mark.asyncio
async def test_delete_returns_none_on_empty_content(self) -> None:
transport, _ = _make_transport()
mock_resp = _mock_response(200, content=b"")
mock_client = AsyncMock()
mock_client.delete.return_value = mock_resp
mock_client.is_closed = False

with patch.object(transport, "_get_client", return_value=mock_client):
result = await transport.delete("/test")

assert result is None
120 changes: 120 additions & 0 deletions tests/test_transport/test_sync_transport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Tests for SyncTransport put/delete params support."""
from __future__ import annotations

from unittest.mock import MagicMock, patch

import requests

from domo_sdk.transport.auth import AuthStrategy
from domo_sdk.transport.sync_transport import SyncTransport


def _make_transport() -> tuple[SyncTransport, MagicMock]:
"""Create a SyncTransport with a mocked auth strategy and session."""
auth = MagicMock(spec=AuthStrategy)
auth.get_base_url.return_value = "https://api.domo.com"
auth.get_headers.return_value = {"Authorization": "Bearer test"}
auth.auth_mode = "developer_token"
transport = SyncTransport(auth)
return transport, auth


def _mock_response(status_code: int = 200, json_data: dict | list | None = None, content: bytes = b"{}") -> MagicMock:
"""Create a mock requests.Response."""
response = MagicMock(spec=requests.Response)
response.status_code = status_code
response.content = content
response.text = content.decode() if content else ""
if json_data is not None:
response.json.return_value = json_data
else:
response.json.return_value = {}
return response


class TestPutWithParams:
"""Tests for put() with params support."""

def test_put_passes_params(self) -> None:
transport, _ = _make_transport()
mock_resp = _mock_response(200, {"ok": True})

with patch.object(transport._session, "put", return_value=mock_resp) as mock_put:
result = transport.put("/test", body={"key": "val"}, params={"p": "1"})

_, kwargs = mock_put.call_args
assert kwargs["params"] == {"p": "1"}
assert result == {"ok": True}

def test_put_without_params_defaults_to_empty(self) -> None:
transport, _ = _make_transport()
mock_resp = _mock_response(200, {"ok": True})

with patch.object(transport._session, "put", return_value=mock_resp) as mock_put:
transport.put("/test", body=None)

_, kwargs = mock_put.call_args
assert kwargs["params"] == {}

def test_put_with_none_params_defaults_to_empty(self) -> None:
transport, _ = _make_transport()
mock_resp = _mock_response(200, {"ok": True})

with patch.object(transport._session, "put", return_value=mock_resp) as mock_put:
transport.put("/test", body=None, params=None)

_, kwargs = mock_put.call_args
assert kwargs["params"] == {}


class TestDeleteWithParams:
"""Tests for delete() with params and return value support."""

def test_delete_passes_params(self) -> None:
transport, _ = _make_transport()
mock_resp = _mock_response(200, {"Deleted": 3}, content=b'{"Deleted": 3}')

with patch.object(transport._session, "delete", return_value=mock_resp) as mock_delete:
result = transport.delete("/test", params={"ids": "1,2,3"})

_, kwargs = mock_delete.call_args
assert kwargs["params"] == {"ids": "1,2,3"}
assert result == {"Deleted": 3}

def test_delete_without_params_defaults_to_empty(self) -> None:
transport, _ = _make_transport()
mock_resp = _mock_response(204, content=b"")

with patch.object(transport._session, "delete", return_value=mock_resp) as mock_delete:
result = transport.delete("/test")

_, kwargs = mock_delete.call_args
assert kwargs["params"] == {}
assert result is None

def test_delete_returns_json_body(self) -> None:
transport, _ = _make_transport()
mock_resp = _mock_response(200, {"Created": 0, "Updated": 0, "Deleted": 5}, content=b'{"Created": 0}')

with patch.object(transport._session, "delete", return_value=mock_resp):
result = transport.delete("/test")

assert result == {"Created": 0, "Updated": 0, "Deleted": 5}

def test_delete_returns_none_on_204(self) -> None:
transport, _ = _make_transport()
mock_resp = _mock_response(204, content=b"")

with patch.object(transport._session, "delete", return_value=mock_resp):
result = transport.delete("/test")

assert result is None

def test_delete_returns_none_on_empty_content(self) -> None:
transport, _ = _make_transport()
mock_resp = _mock_response(200, content=b"")

with patch.object(transport._session, "delete", return_value=mock_resp):
result = transport.delete("/test")

assert result is None
Loading