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
4 changes: 4 additions & 0 deletions src/runloop_api_client/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)

self._default_stream_cls = Stream

self.blueprints = resources.BlueprintsResource(self)
self.deployments = resources.DeploymentsResource(self)
self.devboxes = resources.DevboxesResource(self)
Expand Down Expand Up @@ -284,6 +286,8 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)

self._default_stream_cls = AsyncStream

self.blueprints = resources.AsyncBlueprintsResource(self)
self.deployments = resources.AsyncDeploymentsResource(self)
self.devboxes = resources.AsyncDevboxesResource(self)
Expand Down
48 changes: 46 additions & 2 deletions src/runloop_api_client/_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,29 @@ def __stream__(self) -> Iterator[_T]:
iterator = self._iter_events()

for sse in iterator:
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
if sse.event == "completion":
yield process_data(data=sse.json(), cast_to=cast_to, response=response)

if sse.event == "message_start" or sse.event == "content_block_stop":
yield process_data(data=sse.json(), cast_to=cast_to, response=response)

if sse.event == "ping":
continue

if sse.event == "error":
body = sse.data

try:
body = sse.json()
err_msg = f"{body}"
except Exception:
err_msg = sse.data or f"Error code: {response.status_code}"

raise self._client._make_status_error(
err_msg,
body=body,
response=self.response,
)

# Ensure the entire stream is consumed
for _sse in iterator:
Expand Down Expand Up @@ -119,7 +141,29 @@ async def __stream__(self) -> AsyncIterator[_T]:
iterator = self._iter_events()

async for sse in iterator:
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
if sse.event == "completion":
yield process_data(data=sse.json(), cast_to=cast_to, response=response)

if sse.event == "message_start" or sse.event == "content_block_stop":
yield process_data(data=sse.json(), cast_to=cast_to, response=response)

if sse.event == "ping":
continue

if sse.event == "error":
body = sse.data

try:
body = sse.json()
err_msg = f"{body}"
except Exception:
err_msg = sse.data or f"Error code: {response.status_code}"

raise self._client._make_status_error(
err_msg,
body=body,
response=self.response,
)

# Ensure the entire stream is consumed
async for _sse in iterator:
Expand Down
8 changes: 8 additions & 0 deletions tests/api_resources/devboxes/test_executions.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ def test_path_params_logs(self, client: Runloop) -> None:
id="id",
)

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
def test_method_tail(self, client: Runloop) -> None:
execution = client.devboxes.executions.tail(
Expand All @@ -274,6 +275,7 @@ def test_method_tail(self, client: Runloop) -> None:
)
assert execution is None

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
def test_raw_response_tail(self, client: Runloop) -> None:
response = client.devboxes.executions.with_raw_response.tail(
Expand All @@ -286,6 +288,7 @@ def test_raw_response_tail(self, client: Runloop) -> None:
execution = response.parse()
assert execution is None

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
def test_streaming_response_tail(self, client: Runloop) -> None:
with client.devboxes.executions.with_streaming_response.tail(
Expand All @@ -300,6 +303,7 @@ def test_streaming_response_tail(self, client: Runloop) -> None:

assert cast(Any, response.is_closed) is True

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
def test_path_params_tail(self, client: Runloop) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
Expand Down Expand Up @@ -563,6 +567,7 @@ async def test_path_params_logs(self, async_client: AsyncRunloop) -> None:
id="id",
)

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
async def test_method_tail(self, async_client: AsyncRunloop) -> None:
execution = await async_client.devboxes.executions.tail(
Expand All @@ -571,6 +576,7 @@ async def test_method_tail(self, async_client: AsyncRunloop) -> None:
)
assert execution is None

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
async def test_raw_response_tail(self, async_client: AsyncRunloop) -> None:
response = await async_client.devboxes.executions.with_raw_response.tail(
Expand All @@ -583,6 +589,7 @@ async def test_raw_response_tail(self, async_client: AsyncRunloop) -> None:
execution = await response.parse()
assert execution is None

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
async def test_streaming_response_tail(self, async_client: AsyncRunloop) -> None:
async with async_client.devboxes.executions.with_streaming_response.tail(
Expand All @@ -597,6 +604,7 @@ async def test_streaming_response_tail(self, async_client: AsyncRunloop) -> None

assert cast(Any, response.is_closed) is True

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
async def test_path_params_tail(self, async_client: AsyncRunloop) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
Expand Down
8 changes: 8 additions & 0 deletions tests/api_resources/devboxes/test_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,15 @@ def test_path_params_list(self, client: Runloop) -> None:
"",
)

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
def test_method_tail(self, client: Runloop) -> None:
log = client.devboxes.logs.tail(
"id",
)
assert log is None

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
def test_raw_response_tail(self, client: Runloop) -> None:
response = client.devboxes.logs.with_raw_response.tail(
Expand All @@ -73,6 +75,7 @@ def test_raw_response_tail(self, client: Runloop) -> None:
log = response.parse()
assert log is None

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
def test_streaming_response_tail(self, client: Runloop) -> None:
with client.devboxes.logs.with_streaming_response.tail(
Expand All @@ -86,6 +89,7 @@ def test_streaming_response_tail(self, client: Runloop) -> None:

assert cast(Any, response.is_closed) is True

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
def test_path_params_tail(self, client: Runloop) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
Expand Down Expand Up @@ -135,13 +139,15 @@ async def test_path_params_list(self, async_client: AsyncRunloop) -> None:
"",
)

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
async def test_method_tail(self, async_client: AsyncRunloop) -> None:
log = await async_client.devboxes.logs.tail(
"id",
)
assert log is None

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
async def test_raw_response_tail(self, async_client: AsyncRunloop) -> None:
response = await async_client.devboxes.logs.with_raw_response.tail(
Expand All @@ -153,6 +159,7 @@ async def test_raw_response_tail(self, async_client: AsyncRunloop) -> None:
log = await response.parse()
assert log is None

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
async def test_streaming_response_tail(self, async_client: AsyncRunloop) -> None:
async with async_client.devboxes.logs.with_streaming_response.tail(
Expand All @@ -166,6 +173,7 @@ async def test_streaming_response_tail(self, async_client: AsyncRunloop) -> None

assert cast(Any, response.is_closed) is True

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
async def test_path_params_tail(self, async_client: AsyncRunloop) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
Expand Down
8 changes: 8 additions & 0 deletions tests/api_resources/test_deployments.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,15 @@ def test_path_params_redeploy(self, client: Runloop) -> None:
"",
)

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
def test_method_tail(self, client: Runloop) -> None:
deployment = client.deployments.tail(
"deployment_id",
)
assert_matches_type(DeploymentTailResponse, deployment, path=["response"])

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
def test_raw_response_tail(self, client: Runloop) -> None:
response = client.deployments.with_raw_response.tail(
Expand All @@ -188,6 +190,7 @@ def test_raw_response_tail(self, client: Runloop) -> None:
deployment = response.parse()
assert_matches_type(DeploymentTailResponse, deployment, path=["response"])

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
def test_streaming_response_tail(self, client: Runloop) -> None:
with client.deployments.with_streaming_response.tail(
Expand All @@ -201,6 +204,7 @@ def test_streaming_response_tail(self, client: Runloop) -> None:

assert cast(Any, response.is_closed) is True

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
def test_path_params_tail(self, client: Runloop) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `deployment_id` but received ''"):
Expand Down Expand Up @@ -359,13 +363,15 @@ async def test_path_params_redeploy(self, async_client: AsyncRunloop) -> None:
"",
)

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
async def test_method_tail(self, async_client: AsyncRunloop) -> None:
deployment = await async_client.deployments.tail(
"deployment_id",
)
assert_matches_type(DeploymentTailResponse, deployment, path=["response"])

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
async def test_raw_response_tail(self, async_client: AsyncRunloop) -> None:
response = await async_client.deployments.with_raw_response.tail(
Expand All @@ -377,6 +383,7 @@ async def test_raw_response_tail(self, async_client: AsyncRunloop) -> None:
deployment = await response.parse()
assert_matches_type(DeploymentTailResponse, deployment, path=["response"])

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
async def test_streaming_response_tail(self, async_client: AsyncRunloop) -> None:
async with async_client.deployments.with_streaming_response.tail(
Expand All @@ -390,6 +397,7 @@ async def test_streaming_response_tail(self, async_client: AsyncRunloop) -> None

assert cast(Any, response.is_closed) is True

@pytest.mark.skip(reason="cannot test text/event-stream")
@parametrize
async def test_path_params_tail(self, async_client: AsyncRunloop) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `deployment_id` but received ''"):
Expand Down
24 changes: 24 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from runloop_api_client._types import Omit
from runloop_api_client._models import BaseModel, FinalRequestOptions
from runloop_api_client._constants import RAW_RESPONSE_HEADER
from runloop_api_client._streaming import Stream, AsyncStream
from runloop_api_client._exceptions import RunloopError, APIStatusError, APITimeoutError, APIResponseValidationError
from runloop_api_client._base_client import (
DEFAULT_TIMEOUT,
Expand Down Expand Up @@ -685,6 +686,17 @@ def test_client_max_retries_validation(self) -> None:
max_retries=cast(Any, None),
)

@pytest.mark.respx(base_url=base_url)
def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
class Model(BaseModel):
name: str

respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))

stream = self.client.post("/foo", cast_to=Model, stream=True, stream_cls=Stream[Model])
assert isinstance(stream, Stream)
stream.response.close()

@pytest.mark.respx(base_url=base_url)
def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
class Model(BaseModel):
Expand Down Expand Up @@ -1420,6 +1432,18 @@ async def test_client_max_retries_validation(self) -> None:
max_retries=cast(Any, None),
)

@pytest.mark.respx(base_url=base_url)
@pytest.mark.asyncio
async def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
class Model(BaseModel):
name: str

respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))

stream = await self.client.post("/foo", cast_to=Model, stream=True, stream_cls=AsyncStream[Model])
assert isinstance(stream, AsyncStream)
await stream.response.aclose()

@pytest.mark.respx(base_url=base_url)
@pytest.mark.asyncio
async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
Expand Down