Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f9cd760
chore: update SDK settings
stainless-app[bot] Mar 5, 2026
9580045
chore: update SDK settings
stainless-app[bot] Mar 5, 2026
2240862
chore: update SDK settings
stainless-app[bot] Mar 5, 2026
972c929
chore(ci): skip uploading artifacts on stainless-internal branches
stainless-app[bot] Mar 7, 2026
b8e57a4
chore: update placeholder string
stainless-app[bot] Mar 7, 2026
706fa9e
chore(api): resolving merge conflicts
stainless-app[bot] Mar 10, 2026
113574f
fix(pydantic): do not pass `by_alias` unless set
stainless-app[bot] Mar 16, 2026
7016283
fix(deps): bump minimum typing-extensions version
stainless-app[bot] Mar 16, 2026
c8ce919
chore(internal): tweak CI branches
stainless-app[bot] Mar 16, 2026
e6391a9
feat(api): stable beta
stainless-app[bot] Mar 18, 2026
21bb2b7
fix(api): update flags
stainless-app[bot] Mar 18, 2026
181c06d
codegen metadata
stainless-app[bot] Mar 18, 2026
00d1f1a
chore: update SDK settings
stainless-app[bot] Mar 18, 2026
4ceefe7
chore: update SDK settings
stainless-app[bot] Mar 18, 2026
b7e80bb
chore: update SDK settings
stainless-app[bot] Mar 18, 2026
01a998a
chore: update SDK settings
stainless-app[bot] Mar 18, 2026
1dbbf1a
codegen metadata
stainless-app[bot] Mar 18, 2026
bfe4bcd
Merge remote-tracking branch 'origin/main' into next
stainless-app[bot] Mar 18, 2026
8efa47d
chore(api): update homebrew tap and code samples
stainless-app[bot] Mar 18, 2026
f5c935c
fix(api): consolidate pagination & disable websockets
stainless-app[bot] Mar 18, 2026
e904fd4
Merge remote-tracking branch 'origin/main' into next
stainless-app[bot] Mar 18, 2026
ddb7735
fix: sanitize endpoint path params
stainless-app[bot] Mar 19, 2026
1766adf
refactor(tests): switch from prism to steady
stainless-app[bot] Mar 19, 2026
c6813af
chore(tests): bump steady to v0.19.4
stainless-app[bot] Mar 20, 2026
b377c15
fix(api): add stream_status SSE endpoint, websocket terminals, websoc…
stainless-app[bot] Mar 20, 2026
6b3f3df
chore(tests): bump steady to v0.19.5
stainless-app[bot] Mar 20, 2026
7031c73
release: 0.0.2
stainless-app[bot] Mar 20, 2026
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
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.0.1"
".": "0.0.2"
}
8 changes: 4 additions & 4 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 26
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/dedalus-labs%2Fdedalus-2412fc9544367895508dc1f8377c71b177df7bbfdf0888087edb84e8067f62d9.yml
openapi_spec_hash: c3563b6ed3c5a0f8ff3a09f8c8725bc4
config_hash: d7156d9b45faaeba89feda6169c2c86a
configured_endpoints: 27
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/dedalus-labs%2Fdedalus-a2b38be63dcddaea1a314843f9685b8e26c1f584b1696712f6a9668014afc0a7.yml
openapi_spec_hash: ba6a5b38ed5fa9d49b03b154e3b99b53
config_hash: a71704446fb82d83c7357258c182bdb5
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## 0.0.2 (2026-03-20)

Full Changelog: [v0.0.1...v0.0.2](https://github.com/dedalus-labs/dedalus-python/compare/v0.0.1...v0.0.2)

### Bug Fixes

* **api:** add stream_status SSE endpoint, websocket terminals, websocket_base_url param ([b377c15](https://github.com/dedalus-labs/dedalus-python/commit/b377c15509fd024a3a26d24907ddb1a6426eca79))
* sanitize endpoint path params ([ddb7735](https://github.com/dedalus-labs/dedalus-python/commit/ddb7735faa56c28c05775dc625f2db7fede23c7c))


### Chores

* **tests:** bump steady to v0.19.4 ([c6813af](https://github.com/dedalus-labs/dedalus-python/commit/c6813af7f4f6c8995ca644160b30aac303160bee))
* **tests:** bump steady to v0.19.5 ([6b3f3df](https://github.com/dedalus-labs/dedalus-python/commit/6b3f3dff25d9416ebd57e1be69cbb6b94747018f))


### Refactors

* **tests:** switch from prism to steady ([1766adf](https://github.com/dedalus-labs/dedalus-python/commit/1766adfac04d3544abff0e45830865dca9fbec38))

## 0.0.1 (2026-03-18)

Full Changelog: [v0.0.1...v0.0.1](https://github.com/dedalus-labs/dedalus-python/compare/v0.0.1...v0.0.1)
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ $ pip install ./path-to-wheel-file.whl

## Running tests

Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests.
Most tests require you to [set up a mock server](https://github.com/dgellow/steady) against the OpenAPI spec to run the tests.

```sh
$ ./scripts/mock
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,36 @@ async def main() -> None:
asyncio.run(main())
```

## Streaming responses

We provide support for streaming responses using Server Side Events (SSE).

```python
from dedalus_sdk import Dedalus

client = Dedalus()

stream = client.workspaces.stream_status(
workspace_id="workspace_id",
)
for workspace in stream:
print(workspace.workspace_id)
```

The async client uses the exact same interface.

```python
from dedalus_sdk import AsyncDedalus

client = AsyncDedalus()

stream = await client.workspaces.stream_status(
workspace_id="workspace_id",
)
async for workspace in stream:
print(workspace.workspace_id)
```

## Using types

Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like:
Expand Down
1 change: 1 addition & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Methods:
- <code title="patch /v1/workspaces/{workspace_id}">client.workspaces.<a href="./src/dedalus_sdk/resources/workspaces/workspaces.py">update</a>(workspace_id, \*\*<a href="src/dedalus_sdk/types/workspace_update_params.py">params</a>) -> <a href="./src/dedalus_sdk/types/workspace.py">Workspace</a></code>
- <code title="get /v1/workspaces">client.workspaces.<a href="./src/dedalus_sdk/resources/workspaces/workspaces.py">list</a>(\*\*<a href="src/dedalus_sdk/types/workspace_list_params.py">params</a>) -> SyncCursorPage[Item]</code>
- <code title="delete /v1/workspaces/{workspace_id}">client.workspaces.<a href="./src/dedalus_sdk/resources/workspaces/workspaces.py">delete</a>(workspace_id) -> <a href="./src/dedalus_sdk/types/workspace.py">Workspace</a></code>
- <code title="get /v1/workspaces/{workspace_id}/status/stream">client.workspaces.<a href="./src/dedalus_sdk/resources/workspaces/workspaces.py">stream_status</a>(workspace_id) -> <a href="./src/dedalus_sdk/types/workspace.py">Workspace</a></code>

## Artifacts

Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "dedalus-sdk"
version = "0.0.1"
version = "0.0.2"
description = "The official Python library for the Dedalus API"
dynamic = ["readme"]
license = "MIT"
Expand Down Expand Up @@ -42,6 +42,7 @@ Repository = "https://github.com/dedalus-labs/dedalus-python"

[project.optional-dependencies]
aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"]
websockets = ["websockets >= 13, < 16"]

[tool.uv]
managed = true
Expand Down
26 changes: 13 additions & 13 deletions scripts/mock
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,34 @@ fi

echo "==> Starting mock server with URL ${URL}"

# Run prism mock on the given spec
# Run steady mock on the given spec
if [ "$1" == "--daemon" ]; then
# Pre-install the package so the download doesn't eat into the startup timeout
npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version
npm exec --package=@stdy/cli@0.19.5 -- steady --version

npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log &
npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log &

# Wait for server to come online (max 30s)
# Wait for server to come online via health endpoint (max 30s)
echo -n "Waiting for server"
attempts=0
while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do
while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do
if ! kill -0 $! 2>/dev/null; then
echo
cat .stdy.log
exit 1
fi
attempts=$((attempts + 1))
if [ "$attempts" -ge 300 ]; then
echo
echo "Timed out waiting for Prism server to start"
cat .prism.log
echo "Timed out waiting for Steady server to start"
cat .stdy.log
exit 1
fi
echo -n "."
sleep 0.1
done

if grep -q "✖ fatal" ".prism.log"; then
cat .prism.log
exit 1
fi

echo
else
npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL"
npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL"
fi
16 changes: 8 additions & 8 deletions scripts/test
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color

function prism_is_running() {
curl --silent "http://localhost:4010" >/dev/null 2>&1
function steady_is_running() {
curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1
}

kill_server_on_port() {
Expand All @@ -25,7 +25,7 @@ function is_overriding_api_base_url() {
[ -n "$TEST_API_BASE_URL" ]
}

if ! is_overriding_api_base_url && ! prism_is_running ; then
if ! is_overriding_api_base_url && ! steady_is_running ; then
# When we exit this script, make sure to kill the background mock server process
trap 'kill_server_on_port 4010' EXIT

Expand All @@ -36,19 +36,19 @@ fi
if is_overriding_api_base_url ; then
echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}"
echo
elif ! prism_is_running ; then
echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server"
elif ! steady_is_running ; then
echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Steady server"
echo -e "running against your OpenAPI spec."
echo
echo -e "To run the server, pass in the path or url of your OpenAPI"
echo -e "spec to the prism command:"
echo -e "spec to the steady command:"
echo
echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}"
echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.5 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}"
echo

exit 1
else
echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}"
echo -e "${GREEN}✔ Mock steady server is running with your OpenAPI spec${NC}"
echo
fi

Expand Down
30 changes: 30 additions & 0 deletions src/dedalus_sdk/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,22 @@ class Dedalus(SyncAPIClient):
x_api_key: str | None
dedalus_org_id: str | None

websocket_base_url: str | httpx.URL | None
"""Base URL for WebSocket connections.

If not specified, the default base URL will be used, with 'wss://' replacing the
'http://' or 'https://' scheme. For example: 'http://example.com' becomes
'wss://example.com'
"""

def __init__(
self,
*,
api_key: str | None = None,
x_api_key: str | None = None,
dedalus_org_id: str | None = None,
base_url: str | httpx.URL | None = None,
websocket_base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
max_retries: int = DEFAULT_MAX_RETRIES,
default_headers: Mapping[str, str] | None = None,
Expand Down Expand Up @@ -89,6 +98,8 @@ def __init__(
dedalus_org_id = os.environ.get("DEDALUS_ORG_ID")
self.dedalus_org_id = dedalus_org_id

self.websocket_base_url = websocket_base_url

if base_url is None:
base_url = os.environ.get("DEDALUS_BASE_URL")
if base_url is None:
Expand All @@ -107,6 +118,8 @@ def __init__(

self._idempotency_header = "Idempotency-Key"

self._default_stream_cls = Stream

@cached_property
def workspaces(self) -> WorkspacesResource:
from .resources.workspaces import WorkspacesResource
Expand Down Expand Up @@ -175,6 +188,7 @@ def copy(
api_key: str | None = None,
x_api_key: str | None = None,
dedalus_org_id: str | None = None,
websocket_base_url: str | httpx.URL | None = None,
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.Client | None = None,
Expand Down Expand Up @@ -211,6 +225,7 @@ def copy(
api_key=api_key or self.api_key,
x_api_key=x_api_key or self.x_api_key,
dedalus_org_id=dedalus_org_id or self.dedalus_org_id,
websocket_base_url=websocket_base_url or self.websocket_base_url,
base_url=base_url or self.base_url,
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
http_client=http_client,
Expand Down Expand Up @@ -264,13 +279,22 @@ class AsyncDedalus(AsyncAPIClient):
x_api_key: str | None
dedalus_org_id: str | None

websocket_base_url: str | httpx.URL | None
"""Base URL for WebSocket connections.

If not specified, the default base URL will be used, with 'wss://' replacing the
'http://' or 'https://' scheme. For example: 'http://example.com' becomes
'wss://example.com'
"""

def __init__(
self,
*,
api_key: str | None = None,
x_api_key: str | None = None,
dedalus_org_id: str | None = None,
base_url: str | httpx.URL | None = None,
websocket_base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
max_retries: int = DEFAULT_MAX_RETRIES,
default_headers: Mapping[str, str] | None = None,
Expand Down Expand Up @@ -308,6 +332,8 @@ def __init__(
dedalus_org_id = os.environ.get("DEDALUS_ORG_ID")
self.dedalus_org_id = dedalus_org_id

self.websocket_base_url = websocket_base_url

if base_url is None:
base_url = os.environ.get("DEDALUS_BASE_URL")
if base_url is None:
Expand All @@ -326,6 +352,8 @@ def __init__(

self._idempotency_header = "Idempotency-Key"

self._default_stream_cls = AsyncStream

@cached_property
def workspaces(self) -> AsyncWorkspacesResource:
from .resources.workspaces import AsyncWorkspacesResource
Expand Down Expand Up @@ -394,6 +422,7 @@ def copy(
api_key: str | None = None,
x_api_key: str | None = None,
dedalus_org_id: str | None = None,
websocket_base_url: str | httpx.URL | None = None,
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.AsyncClient | None = None,
Expand Down Expand Up @@ -430,6 +459,7 @@ def copy(
api_key=api_key or self.api_key,
x_api_key=x_api_key or self.x_api_key,
dedalus_org_id=dedalus_org_id or self.dedalus_org_id,
websocket_base_url=websocket_base_url or self.websocket_base_url,
base_url=base_url or self.base_url,
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
http_client=http_client,
Expand Down
42 changes: 40 additions & 2 deletions src/dedalus_sdk/_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,26 @@ def __stream__(self) -> Iterator[_T]:

try:
for sse in iterator:
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
if sse.event == "bookmark":
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,
)

if sse.event == "status":
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
finally:
# Ensure the response is closed even if the consumer doesn't read all data
response.close()
Expand Down Expand Up @@ -125,7 +144,26 @@ async def __stream__(self) -> AsyncIterator[_T]:

try:
async for sse in iterator:
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
if sse.event == "bookmark":
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,
)

if sse.event == "status":
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
finally:
# Ensure the response is closed even if the consumer doesn't read all data
await response.aclose()
Expand Down
1 change: 1 addition & 0 deletions src/dedalus_sdk/_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from ._path import path_template as path_template
from ._sync import asyncify as asyncify
from ._proxy import LazyProxy as LazyProxy
from ._utils import (
Expand Down
Loading
Loading