From b016780a061a08941ca7d8e935e266cfecc4dabc Mon Sep 17 00:00:00 2001 From: scalarbot Date: Tue, 27 Oct 2026 22:07:02 +0000 Subject: [PATCH 1/3] feat(api): add auth scheme BrowserAuth (+1 more change) - Regenerated with generator `0.36.11` (previously `0.33.3`). - Added `oauth2` auth scheme `BrowserAuth`. Build: PQMDRmz8abbOlkWv4AVYp --- .claude/skills/dedalus-python-sdk/SKILL.md | 7 +- .github/workflows/release-please.yml | 40 ++++-- .github/workflows/release-title-edit.yml | 138 +++++++-------------- README.md | 13 +- SKILL.md | 7 +- VERSIONING.md | 7 +- api.md | 2 +- pyproject.toml | 1 - scalar-sdk.manifest.json | 21 +++- src/dedalus_sdk/_base_client.py | 15 ++- src/dedalus_sdk/_client.py | 34 ++++- src/dedalus_sdk/_utils/_transform.py | 17 ++- src/dedalus_sdk/_utils/_utils.py | 17 +-- src/dedalus_sdk/_version.py | 2 +- 14 files changed, 179 insertions(+), 142 deletions(-) diff --git a/.claude/skills/dedalus-python-sdk/SKILL.md b/.claude/skills/dedalus-python-sdk/SKILL.md index fc31a48..f71af89 100644 --- a/.claude/skills/dedalus-python-sdk/SKILL.md +++ b/.claude/skills/dedalus-python-sdk/SKILL.md @@ -21,7 +21,7 @@ import os from dedalus_sdk import Dedalus client = Dedalus( - x_api_key=os.environ.get("DEDALUS_X_API_KEY"), + api_key=os.environ.get("DEDALUS_API_KEY"), ) ``` @@ -29,6 +29,7 @@ Provide credentials using the options below. Environment variables are read auto - `x_api_key` (env: `DEDALUS_X_API_KEY`) — Dedalus API key. Alternative to Bearer token. - `api_key` (env: `DEDALUS_API_KEY`) — Dedalus API key or short-lived delegated access token in Authorization: Bearer . +- `browser_token` (env: `DEDALUS_BROWSER_TOKEN`) — Credential for the BrowserAuth scheme. ## Calling operations @@ -38,7 +39,7 @@ import os from dedalus_sdk import Dedalus client = Dedalus( - x_api_key=os.environ.get("DEDALUS_X_API_KEY"), + api_key=os.environ.get("DEDALUS_API_KEY"), ) machine = client.machines.create( @@ -84,7 +85,7 @@ except APIStatusError as err: ## Requirements -- Python 3.8 or newer +- Python 3.9 or newer ## Reference files diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 2edfec0..135ad43 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -51,18 +51,44 @@ jobs: # version bump and changelog must also land back on scalar-next, or the next # release PR would propose this release again. A real merge (never a plain # commit push) so a squash- or rebase-merged release PR syncs just as well; the - # non-conventional message keeps the sync commit out of future changelogs. Plain - # (non-force) push on purpose: losing a race against a concurrent regeneration - # push fails loudly here, and the platform's next sync re-converges. + # non-conventional message keeps the sync commit out of future changelogs. + # + # The Scalar platform merges main into scalar-next itself when the release + # PR merges, so this step routinely races it. The sync is done once the release commit + # is an ancestor of scalar-next, whoever merged it, so that is checked before every + # attempt. A rejected push retries from a fresh fetch rather than failing the release + # run. Plain (non-force) push on purpose: a concurrent regeneration push is never + # discarded, and a sync that still cannot land after the retries fails loudly. - name: Sync release back to scalar-next if: ${{ steps.release.outputs.release_created == 'true' }} + env: + RELEASE_SHA: ${{ steps.release.outputs.sha }} + RELEASE_TAG: ${{ steps.release.outputs.tag_name }} run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git fetch origin scalar-next - git checkout -B scalar-next origin/scalar-next - git merge --no-edit -m "Sync release ${{ steps.release.outputs.tag_name }} back to scalar-next" "${{ steps.release.outputs.sha }}" - git push origin scalar-next + # Explicit `|| return 1`s because `set -e` does not apply inside a function called + # as a condition, and a failed merge must never fall through to a no-op push. + sync_release() { + git fetch origin scalar-next || return 1 + if git merge-base --is-ancestor "$RELEASE_SHA" origin/scalar-next; then + echo "$RELEASE_TAG is already on scalar-next." + return 0 + fi + git checkout -B scalar-next origin/scalar-next || return 1 + if ! git merge --no-edit -m "Sync release $RELEASE_TAG back to scalar-next" "$RELEASE_SHA"; then + git merge --abort + return 1 + fi + git push origin scalar-next + } + for attempt in 1 2 3; do + if sync_release; then exit 0; fi + echo "Attempt $attempt of 3 to sync $RELEASE_TAG back to scalar-next failed." + if [ "$attempt" -lt 3 ]; then sleep $((attempt * 5)); fi + done + echo "::error::Could not sync $RELEASE_TAG back to scalar-next after 3 attempts." + exit 1 publish: needs: release-please # `!cancelled()` keeps publishing even when the back-sync to the integration branch diff --git a/.github/workflows/release-title-edit.yml b/.github/workflows/release-title-edit.yml index c836f65..08b0ba4 100644 --- a/.github/workflows/release-title-edit.yml +++ b/.github/workflows/release-title-edit.yml @@ -2,14 +2,17 @@ name: Release PR Version on: # `edited` is not one of the default pull_request types, but it is the point of this - # workflow: retitling the release PR is how a maintainer picks an exact version. The - # other types keep the consistency check attached to the PR as it evolves. + # workflow: retitling the release PR is how a maintainer picks an exact version, and the + # check has to re-run on the new title. The other types keep the consistency check attached + # to the PR as it evolves. pull_request: types: [opened, reopened, edited, synchronize] -# Read-only by default; only the job that pushes the Release-As commit widens this. +# Read-only: the check reads the committed version, and polls the pull request while it waits +# for a re-render. permissions: contents: read + pull-requests: read jobs: version-consistency: @@ -25,30 +28,39 @@ jobs: # typo'd into the title still reaches the script and fails there, rather than falling out # of the guard and leaving no check at all — a missing check is not a failing one, and this # is the check the release PR's footer tells maintainers to wait for before merging. - # - # It is also what keeps the workflow from failing silently if the platform ever opens - # release PRs from a different account: the bridge below stops firing, but this check still - # runs, the retitled version and the committed one disagree, and it turns red. The merge is - # blocked loudly instead of releasing the old version. if: >- ${{ github.event.pull_request.base.ref == 'main' && startsWith(github.event.pull_request.title, 'release: ') }} runs-on: ubuntu-latest + # Bounds the wait below with room to spare; a re-render normally lands within a minute. + timeout-minutes: 15 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # The version committed on the PR head is what merging would actually release. ref: ${{ github.event.pull_request.head.sha }} - # Fails while the title version and the committed version disagree — the window between - # a maintainer's retitle and the platform re-rendering the PR from it. Without this a - # merge in that window would tag a release whose own files self-report the old version, - # and a title release-please cannot parse would silently cut no release at all. + # A retitle is applied by the Scalar platform, which pushes a Release-As commit to the + # integration branch and re-renders the release PR at that version. Until it lands, the + # title and the committed version disagree, and a merge in that window would tag a release + # whose own files self-report the old version. + # + # So this waits rather than failing: the check stays pending, which blocks a merge just as + # a failure would where the check is required, but does not flash red on every retitle. It + # passes once the title and the committed version agree, and hands over once the head + # moves, since the re-render's push starts a run of its own on the new head. It never + # passes while the title still disagrees, even a newer title: the run started for that + # edit judges it, and a pass here would sit beside it on the same head. Only a re-render + # that never arrives, or a title that is not a version at all, fails it. - name: Compare the title version with the committed version env: # The title is human input, so it is bound through the environment (never # interpolated into the script) and matched against a strict semver pattern # before it is read. PR_TITLE: ${{ github.event.pull_request.title }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPOSITORY: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} run: | pattern='^release: ((0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?)$' if [[ ! "$PR_TITLE" =~ $pattern ]]; then @@ -57,87 +69,25 @@ jobs: fi title_version="${BASH_REMATCH[1]}" manifest_version="$(jq -r '.["."]' .release-please-manifest.json)" - if [[ "$title_version" != "$manifest_version" ]]; then - echo "::error::Release PR title says $title_version but the pull request is versioned $manifest_version. Wait for the release PR to be re-rendered at $title_version before merging." - exit 1 - fi - echo "Release PR title and committed version agree on $title_version." - - apply-title-version: - name: Apply edited release version - # A human retitle of the open release PR is bridged into the canonical explicit-version - # mechanism (a Release-As commit on scalar-next, which the platform re-renders the - # release PR from). `changes.title` is only set when the title itself changed, and bot - # senders are ignored so the platform's own retitles cannot bounce back into another - # commit. - # - # Both signals are required here, unlike the read-only check above, and each covers what the - # other cannot. The author, because this job pushes with `contents: write` and a title is - # free text — anyone able to open a pull request against main could otherwise - # name a version and have it committed to scalar-next. `user.login` is set by GitHub when - # the pull request is opened and cannot be forged by whoever edits the title afterwards. - # The title, because those accounts open pull requests other than release PRs, and retitling - # one of those must not push a release. - # - # One login per platform deployment, since a repo generated by staging carries staging's - # app. Parenthesised because the group is ANDed with the title clause below: without the - # parens that `&&` would bind to the last login alone, letting the other accounts push a - # release off any title. Listed rather than matched on the shared `scalar-docs` stem — a - # prefix test would also admit any future `scalar-docs-*[bot]`, including someone else's. - if: >- - ${{ github.event.action == 'edited' - && github.event.changes.title != null - && github.event.sender.type != 'Bot' - && github.event.pull_request.state == 'open' - && github.event.pull_request.base.ref == 'main' - && (github.event.pull_request.user.login == 'scalar-docs[bot]' - || github.event.pull_request.user.login == 'scalar-docs-staging[bot]' - || github.event.pull_request.user.login == 'scalar-docs-development[bot]') - && startsWith(github.event.pull_request.title, 'release: ') }} - # One bridge run at a time per pull request, newest retitle wins. Two retitles in quick - # succession (a version typo corrected seconds later) would otherwise start two runs that - # both read the version committed on the PR head, both get past the no-op guard, and both - # push — and since each fetches scalar-next immediately before committing, the second - # push fast-forwards instead of failing. release-please honours the newest Release-As - # footer, so the release would land on whichever run happened to push last, not on the - # title the maintainer actually left behind. - concurrency: - group: release-title-edit-${{ github.event.pull_request.number }} - cancel-in-progress: true - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - # The PR head, so the version the pull request currently carries can be read before - # deciding whether anything needs to change. - ref: ${{ github.event.pull_request.head.sha }} - - name: Push a Release-As commit for the edited version - env: - PR_TITLE: ${{ github.event.pull_request.title }} - run: | - set -euo pipefail - pattern='^release: ((0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?)$' - if [[ ! "$PR_TITLE" =~ $pattern ]]; then - echo "::error::Release PR title must be \"release: X.Y.Z\" with a full semver version, got: $PR_TITLE" - exit 1 - fi - # Only ever a validated semver from here on, so it is safe in a commit message. - version="${BASH_REMATCH[1]}" - manifest_version="$(jq -r '.["."]' .release-please-manifest.json)" - # The re-rendered PR is retitled to the version it now carries; stopping here keeps - # that from producing an endless chain of Release-As commits. - if [[ "$version" == "$manifest_version" ]]; then - echo "This release PR already carries $version; nothing to do." + if [[ "$title_version" == "$manifest_version" ]]; then + echo "Release PR title and committed version agree on $title_version." exit 0 fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git fetch origin scalar-next - git checkout -B scalar-next origin/scalar-next - git commit --allow-empty -m "chore: release $version" -m "Release-As: $version" - # Plain (non-force) push: losing a race against a regeneration push fails loudly - # rather than discarding it, and the retitle can simply be repeated. - git push origin scalar-next - echo "Pushed Release-As: $version to scalar-next. The release PR will be re-rendered at that version." + echo "Release PR title says $title_version but the pull request is versioned $manifest_version. Waiting for it to be re-rendered at $title_version." + # Up to ten minutes: 60 polls, 10 seconds apart. + for _ in $(seq 60); do + sleep 10 + # A failed read is retried on the next poll rather than failing the check. + current="$(gh api "repos/$REPOSITORY/pulls/$PR_NUMBER" --jq '[.head.sha, .title] | @tsv')" || continue + IFS=$'\t' read -r current_sha current_title <<< "$current" + if [[ "$current_sha" != "$HEAD_SHA" ]]; then + echo "The release PR has a new head; the run for that push checks it." + exit 0 + fi + if [[ "$current_title" =~ $pattern && "${BASH_REMATCH[1]}" == "$manifest_version" ]]; then + echo "Release PR title and committed version agree on $manifest_version." + exit 0 + fi + done + echo "::error::Release PR title says $title_version but the pull request is still versioned $manifest_version after 10 minutes. Rebuild the SDK from the Scalar dashboard to re-render it." + exit 1 diff --git a/README.md b/README.md index 0cd7cfb..294ffea 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ import os from dedalus_sdk import Dedalus client = Dedalus( - x_api_key=os.environ.get("DEDALUS_X_API_KEY"), + api_key=os.environ.get("DEDALUS_API_KEY"), ) machine = client.machines.create( @@ -91,13 +91,15 @@ Pass credentials to the generated client constructor. Environment variables are | Option | Type | Default | Description | | --- | --- | --- | --- | -| `x_api_key` | `string \| provider` | - | Dedalus API key. Alternative to Bearer token. Defaults to DEDALUS_X_API_KEY. | -| `api_key` | `string \| provider` | - | Dedalus API key or short-lived delegated access token in Authorization: Bearer . Defaults to DEDALUS_API_KEY. | +| `x_api_key` | `str \| None` | - | Dedalus API key. Alternative to Bearer token. Defaults to DEDALUS_X_API_KEY. | +| `api_key` | `str \| None` | - | Dedalus API key or short-lived delegated access token in Authorization: Bearer \. Defaults to DEDALUS_API_KEY. | +| `browser_token` | `str \| None` | - | Credential for the BrowserAuth scheme. Defaults to DEDALUS_BROWSER_TOKEN. | Declared schemes: - `ApiKeyAuth` API key in header `x-api-key` - `BearerAuth` bearer token +- `BrowserAuth` OAuth2/OpenID Connect
@@ -141,7 +143,8 @@ client = Dedalus( | Option | Type | Default | Description | | --- | --- | --- | --- | | `x_api_key` | `str \| None` | `os.environ.get("DEDALUS_X_API_KEY")` | Dedalus API key. Alternative to Bearer token. | -| `api_key` | `str \| None` | `os.environ.get("DEDALUS_API_KEY")` | Dedalus API key or short-lived delegated access token in Authorization: Bearer . | +| `api_key` | `str \| None` | `os.environ.get("DEDALUS_API_KEY")` | Dedalus API key or short-lived delegated access token in Authorization: Bearer \. | +| `browser_token` | `str \| None` | `os.environ.get("DEDALUS_BROWSER_TOKEN")` | Credential for the BrowserAuth scheme. | | `base_url` | `str \| httpx.URL \| None` | - | Override the default API base URL. | | `timeout` | `float \| Timeout \| None` | `60.0` | Maximum time in seconds to wait for a response before aborting a request. | | `max_retries` | `int` | `2` | Number of retries for temporary failures. | @@ -182,6 +185,6 @@ page = client.machines.list() ## Requirements -- Python 3.8 or newer +- Python 3.9 or newer Powered by Scalar. diff --git a/SKILL.md b/SKILL.md index 35cd7f7..8ab252a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -21,7 +21,7 @@ import os from dedalus_sdk import Dedalus client = Dedalus( - x_api_key=os.environ.get("DEDALUS_X_API_KEY"), + api_key=os.environ.get("DEDALUS_API_KEY"), ) ``` @@ -29,6 +29,7 @@ Provide credentials using the options below. Environment variables are read auto - `x_api_key` (env: `DEDALUS_X_API_KEY`) — Dedalus API key. Alternative to Bearer token. - `api_key` (env: `DEDALUS_API_KEY`) — Dedalus API key or short-lived delegated access token in Authorization: Bearer . +- `browser_token` (env: `DEDALUS_BROWSER_TOKEN`) — Credential for the BrowserAuth scheme. ## Calling operations @@ -38,7 +39,7 @@ import os from dedalus_sdk import Dedalus client = Dedalus( - x_api_key=os.environ.get("DEDALUS_X_API_KEY"), + api_key=os.environ.get("DEDALUS_API_KEY"), ) machine = client.machines.create( @@ -84,7 +85,7 @@ except APIStatusError as err: ## Requirements -- Python 3.8 or newer +- Python 3.9 or newer ## Reference files diff --git a/VERSIONING.md b/VERSIONING.md index f9cf7dd..dcde77a 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -36,12 +36,13 @@ not have picked — **edit the release PR title** to the version you want: release: 1.0.0 ``` -The `Release PR version` check turns red as soon as you save, because the version in the +The `Release PR version` check goes pending as soon as you save, because the version in the title no longer matches the version committed in the PR. The Scalar platform then re-renders the release PR at your version (changelog, manifest, and every version-bearing file), the title comes back as `release: 1.0.0`, and the check turns green. **Wait for it to be green before merging** — merging in between would tag a release whose own files still carry the -old version. +old version. The check only turns red if the title is not a full semver version, or if the +re-render has not arrived within 10 minutes. The git-native equivalent, if you would rather not touch the PR: push an empty commit with a `Release-As` footer to `scalar-next`. This is exactly what the title edit does for you. @@ -95,6 +96,6 @@ Releases work without this; it closes a window where one can go out at the wrong - **If this repository uses auto-merge, make the `Release PR version` check a required status check.** Auto-merge merges as soon as the checks that are *required* go green, with no - human present — so a check that is not required but still red at that moment stops + human present — so a check that is not required but still pending at that moment stops nothing, which is exactly the window between retitling a release PR and the platform re-rendering it. diff --git a/api.md b/api.md index badbc20..d8d66bf 100644 --- a/api.md +++ b/api.md @@ -45,7 +45,7 @@ import os from dedalus_sdk import Dedalus client = Dedalus( - x_api_key=os.environ.get("DEDALUS_X_API_KEY"), + api_key=os.environ.get("DEDALUS_API_KEY"), ) ``` diff --git a/pyproject.toml b/pyproject.toml index e60f3a0..ad468bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ dependencies = [ "pydantic>=1.9.0, <3", "typing-extensions>=4.14, <5", "anyio>=3.5.0, <5", - "distro>=1.7.0, <2", "sniffio", ] diff --git a/scalar-sdk.manifest.json b/scalar-sdk.manifest.json index 0bcba7b..9354cb6 100644 --- a/scalar-sdk.manifest.json +++ b/scalar-sdk.manifest.json @@ -1,7 +1,7 @@ { "name": "Dedalus", "slug": "dedalus", - "generatorVersion": "0.33.3", + "generatorVersion": "0.36.11", "servers": [ "https://dcs.dedaluslabs.ai" ], @@ -13,7 +13,8 @@ ], "auth": [ "apiKey", - "bearer" + "bearer", + "oauth2" ], "authDetails": [ { @@ -26,6 +27,22 @@ "kind": "bearer", "id": "BearerAuth", "bearerFormat": "Dedalus API key or delegated access token" + }, + { + "kind": "oauth2", + "id": "BrowserAuth", + "flows": [ + { + "kind": "authorizationCode", + "authorizationUrl": "https://clerk.dedaluslabs.ai/oauth/authorize", + "tokenUrl": "https://clerk.dedaluslabs.ai/oauth/token", + "refreshUrl": "https://clerk.dedaluslabs.ai/oauth/token", + "scopes": [ + "offline_access", + "user:org:read" + ] + } + ] } ], "clientHeaderParams": [], diff --git a/src/dedalus_sdk/_base_client.py b/src/dedalus_sdk/_base_client.py index 68e2617..305ee72 100644 --- a/src/dedalus_sdk/_base_client.py +++ b/src/dedalus_sdk/_base_client.py @@ -36,7 +36,6 @@ import anyio import httpx -import distro import pydantic from httpx import URL from pydantic import PrivateAttr @@ -2143,15 +2142,15 @@ def get_platform() -> Platform: # system is Linux and platform_name is a string like 'Linux-5.10.81-android12-9-00001-geba40aecb3b7-ab8534902-aarch64-with-libc' return "Android" - if system == "linux": - # https://distro.readthedocs.io/en/latest/#distro.id - distro_id = distro.id() - if distro_id == "freebsd": - return "FreeBSD" + # The BSDs are not Linux distributions: `platform.system()` reports them by name (`FreeBSD`, + # `OpenBSD`), so the kernel name alone tells them apart and no distribution lookup is needed. + if system == "freebsd": + return "FreeBSD" - if distro_id == "openbsd": - return "OpenBSD" + if system == "openbsd": + return "OpenBSD" + if system == "linux": return "Linux" if platform_name: diff --git a/src/dedalus_sdk/_client.py b/src/dedalus_sdk/_client.py index 6117352..61b007a 100644 --- a/src/dedalus_sdk/_client.py +++ b/src/dedalus_sdk/_client.py @@ -50,6 +50,7 @@ class Dedalus(SyncAPIClient): x_api_key: str | None as_base_url: str | None dedalus_org_id: str | None + browser_token: str | None def __init__( self, @@ -58,6 +59,7 @@ def __init__( x_api_key: str | None = None, as_base_url: str | None = None, dedalus_org_id: str | None = None, + browser_token: str | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, @@ -99,6 +101,9 @@ def __init__( if dedalus_org_id is None: dedalus_org_id = os.environ.get("DEDALUS_ORG_ID") self.dedalus_org_id = dedalus_org_id + if browser_token is None: + browser_token = os.environ.get("DEDALUS_BROWSER_TOKEN") + self.browser_token = browser_token if base_url is None: base_url = os.environ.get("DEDALUS_BASE_URL") if base_url is None: @@ -155,6 +160,7 @@ def auth_headers(self) -> dict[str, str]: return { **self._api_key_header_auth, **self._x_api_key_header_auth, + **self._browser_token_header_auth, } @override @@ -181,6 +187,13 @@ def _x_api_key_header_auth(self) -> dict[str, str]: return {} return {"x-api-key": value} + @property + def _browser_token_header_auth(self) -> dict[str, str]: + value = self.browser_token + if value is None: + return {} + return {"Authorization": f"Bearer {value}"} + @property @override def default_headers(self) -> dict[str, str | Omit]: @@ -210,7 +223,7 @@ def _validate_headers( if isinstance(custom_headers.get("x-api-key"), Omit): return raise TypeError( - '"Could not resolve authentication method. Expected either api_key or x_api_key to be set. Or for one of the `Authorization` or `x-api-key` headers to be explicitly omitted"' + '"Could not resolve authentication method. Expected either api_key, browser_token or x_api_key to be set. Or for one of the `Authorization` or `x-api-key` headers to be explicitly omitted"' ) def copy( @@ -220,6 +233,7 @@ def copy( x_api_key: str | None = None, as_base_url: str | None = None, dedalus_org_id: str | None = None, + browser_token: str | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.Client | None = None, @@ -251,6 +265,7 @@ def copy( x_api_key=x_api_key or self.x_api_key, as_base_url=as_base_url or self.as_base_url, dedalus_org_id=dedalus_org_id or self.dedalus_org_id, + browser_token=browser_token or self.browser_token, base_url=base_url or self.base_url, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, @@ -290,6 +305,7 @@ class AsyncDedalus(AsyncAPIClient): x_api_key: str | None as_base_url: str | None dedalus_org_id: str | None + browser_token: str | None def __init__( self, @@ -298,6 +314,7 @@ def __init__( x_api_key: str | None = None, as_base_url: str | None = None, dedalus_org_id: str | None = None, + browser_token: str | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, @@ -339,6 +356,9 @@ def __init__( if dedalus_org_id is None: dedalus_org_id = os.environ.get("DEDALUS_ORG_ID") self.dedalus_org_id = dedalus_org_id + if browser_token is None: + browser_token = os.environ.get("DEDALUS_BROWSER_TOKEN") + self.browser_token = browser_token if base_url is None: base_url = os.environ.get("DEDALUS_BASE_URL") if base_url is None: @@ -395,6 +415,7 @@ def auth_headers(self) -> dict[str, str]: return { **self._api_key_header_auth, **self._x_api_key_header_auth, + **self._browser_token_header_auth, } @override @@ -421,6 +442,13 @@ def _x_api_key_header_auth(self) -> dict[str, str]: return {} return {"x-api-key": value} + @property + def _browser_token_header_auth(self) -> dict[str, str]: + value = self.browser_token + if value is None: + return {} + return {"Authorization": f"Bearer {value}"} + @property @override def default_headers(self) -> dict[str, str | Omit]: @@ -450,7 +478,7 @@ def _validate_headers( if isinstance(custom_headers.get("x-api-key"), Omit): return raise TypeError( - '"Could not resolve authentication method. Expected either api_key or x_api_key to be set. Or for one of the `Authorization` or `x-api-key` headers to be explicitly omitted"' + '"Could not resolve authentication method. Expected either api_key, browser_token or x_api_key to be set. Or for one of the `Authorization` or `x-api-key` headers to be explicitly omitted"' ) def copy( @@ -460,6 +488,7 @@ def copy( x_api_key: str | None = None, as_base_url: str | None = None, dedalus_org_id: str | None = None, + browser_token: str | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.AsyncClient | None = None, @@ -491,6 +520,7 @@ def copy( x_api_key=x_api_key or self.x_api_key, as_base_url=as_base_url or self.as_base_url, dedalus_org_id=dedalus_org_id or self.dedalus_org_id, + browser_token=browser_token or self.browser_token, base_url=base_url or self.base_url, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, diff --git a/src/dedalus_sdk/_utils/_transform.py b/src/dedalus_sdk/_utils/_transform.py index 5207549..93e4bef 100644 --- a/src/dedalus_sdk/_utils/_transform.py +++ b/src/dedalus_sdk/_utils/_transform.py @@ -454,4 +454,19 @@ def get_type_hints( localns: Mapping[str, Any] | None = None, include_extras: bool = False, ) -> dict[str, Any]: - return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras) + # `typing` caches a subscripted generic by its arguments, so `Optional["Value"]` written in a request + # module and in a response module is one object holding one shared `ForwardRef("Value")`. Whenever the + # two namespaces it is evaluated with are the same mapping -- which a `TypedDict` field always gets -- + # `typing` returns whatever that reference resolved to first, wherever that was: usually the response + # model, built at import time, so the request `TypedDict` drops every wire alias and format below it. + # A `localns` distinct from the globals makes `typing` evaluate every reference again in this namespace. + # That re-evaluation still writes into the shared `ForwardRef` (Python 3.9-3.13). Pydantic v2 never reads + # that value back, and v1 does only while the package imports, before any request is transformed; what + # remains is a response model built for the first time on another thread at the same instant, which this + # call can race once per `TypedDict` before its `lru_cache` holds the result. + return _get_type_hints( + obj, + globalns=globalns, + localns={} if localns is None else localns, + include_extras=include_extras, + ) diff --git a/src/dedalus_sdk/_utils/_utils.py b/src/dedalus_sdk/_utils/_utils.py index 8b051e2..1af7aca 100644 --- a/src/dedalus_sdk/_utils/_utils.py +++ b/src/dedalus_sdk/_utils/_utils.py @@ -203,19 +203,14 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: return isinstance(obj, Iterable) -# copied from https://github.com/Rapptz/RoboDanny def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: - size = len(seq) - if size == 0: + """Joins `seq` into an English list: `"a"`, `"a or b"`, `"a, b or c"` (no comma before `final`).""" + if not seq: return "" - - if size == 1: - return seq[0] - - if size == 2: - return f"{seq[0]} {final} {seq[1]}" - - return delim.join(seq[:-1]) + f" {final} {seq[-1]}" + *head, last = seq + if not head: + return last + return f"{delim.join(head)} {final} {last}" def quote(string: str) -> str: diff --git a/src/dedalus_sdk/_version.py b/src/dedalus_sdk/_version.py index 6686d0b..d487cb2 100644 --- a/src/dedalus_sdk/_version.py +++ b/src/dedalus_sdk/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Scalar. See README.md for details. __title__ = "dedalus" -__version__ = "0.5.0" # x-release-please-version +__version__ = "0.6.0" # x-release-please-version From 61afcb0d0783d257542428550145370ffd04fce9 Mon Sep 17 00:00:00 2001 From: scalarbot Date: Wed, 28 Oct 2026 19:38:30 +0000 Subject: [PATCH 2/3] chore(api): update generated SDK content 5 generated files changed with no detected API surface change (documentation, examples, or file content). Build: pxWrsgoYpJcTSjZGUOhGW --- .claude/skills/dedalus-python-sdk/SKILL.md | 4 ++-- README.md | 2 +- SKILL.md | 4 ++-- api.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude/skills/dedalus-python-sdk/SKILL.md b/.claude/skills/dedalus-python-sdk/SKILL.md index f71af89..8566c43 100644 --- a/.claude/skills/dedalus-python-sdk/SKILL.md +++ b/.claude/skills/dedalus-python-sdk/SKILL.md @@ -21,7 +21,7 @@ import os from dedalus_sdk import Dedalus client = Dedalus( - api_key=os.environ.get("DEDALUS_API_KEY"), + browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"), ) ``` @@ -39,7 +39,7 @@ import os from dedalus_sdk import Dedalus client = Dedalus( - api_key=os.environ.get("DEDALUS_API_KEY"), + browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"), ) machine = client.machines.create( diff --git a/README.md b/README.md index 294ffea..735f413 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ import os from dedalus_sdk import Dedalus client = Dedalus( - api_key=os.environ.get("DEDALUS_API_KEY"), + browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"), ) machine = client.machines.create( diff --git a/SKILL.md b/SKILL.md index 8ab252a..bd7be7b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -21,7 +21,7 @@ import os from dedalus_sdk import Dedalus client = Dedalus( - api_key=os.environ.get("DEDALUS_API_KEY"), + browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"), ) ``` @@ -39,7 +39,7 @@ import os from dedalus_sdk import Dedalus client = Dedalus( - api_key=os.environ.get("DEDALUS_API_KEY"), + browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"), ) machine = client.machines.create( diff --git a/api.md b/api.md index d8d66bf..df5eeb5 100644 --- a/api.md +++ b/api.md @@ -45,7 +45,7 @@ import os from dedalus_sdk import Dedalus client = Dedalus( - api_key=os.environ.get("DEDALUS_API_KEY"), + browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"), ) ``` From 98dc480399dcfd33440715623408f98b14cb1297 Mon Sep 17 00:00:00 2001 From: "scalar-docs[bot]" <148485328+scalar-docs[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:38:51 +0000 Subject: [PATCH 3/3] release: 0.7.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 12 ++++++++++++ pyproject.toml | 2 +- src/dedalus_sdk/_version.py | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4208b5c..1b77f50 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.0" + ".": "0.7.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 83c73b3..590e76d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [0.7.0](https://github.com/dedalus-labs/dedalus-python/compare/v0.6.0...v0.7.0) (2026-10-28) + + +### Features + +* **api:** add auth scheme BrowserAuth (+1 more change) ([b016780](https://github.com/dedalus-labs/dedalus-python/commit/b016780a061a08941ca7d8e935e266cfecc4dabc)) + + +### Chores + +* **api:** update generated SDK content ([61afcb0](https://github.com/dedalus-labs/dedalus-python/commit/61afcb0d0783d257542428550145370ffd04fce9)) + ## [0.6.0](https://github.com/dedalus-labs/dedalus-python/compare/v0.5.0...v0.6.0) (2026-09-24) diff --git a/pyproject.toml b/pyproject.toml index a38b7c2..5ce5846 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dedalus-sdk" -version = "0.6.0" # x-release-please-version +version = "0.7.0" # x-release-please-version description = "Controlplane API for Dedalus Cloud Services (DCS)." readme = "README.md" license = "Apache-2.0" diff --git a/src/dedalus_sdk/_version.py b/src/dedalus_sdk/_version.py index d487cb2..7f9557a 100644 --- a/src/dedalus_sdk/_version.py +++ b/src/dedalus_sdk/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Scalar. See README.md for details. __title__ = "dedalus" -__version__ = "0.6.0" # x-release-please-version +__version__ = "0.7.0" # x-release-please-version