diff --git a/.claude/skills/dedalus-python-sdk/SKILL.md b/.claude/skills/dedalus-python-sdk/SKILL.md index fc31a48..b9e9204 100644 --- a/.claude/skills/dedalus-python-sdk/SKILL.md +++ b/.claude/skills/dedalus-python-sdk/SKILL.md @@ -21,14 +21,15 @@ import os from dedalus_sdk import Dedalus client = Dedalus( - x_api_key=os.environ.get("DEDALUS_X_API_KEY"), + browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"), ) ``` Provide credentials using the options below. Environment variables are read automatically when the target runtime supports them: - `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 . +- `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"), + browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"), ) 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/contract.yml b/.github/workflows/contract.yml new file mode 100644 index 0000000..dac304f --- /dev/null +++ b/.github/workflows/contract.yml @@ -0,0 +1,19 @@ +# @generated by Hollywood. Do not edit by hand. + +name: Generated API contract +on: + push: {} + pull_request: {} +permissions: + contents: read +jobs: + contract: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - name: Test contract guard + run: python3 scripts/test-contract.py + shell: bash + - name: Verify pinned public operations + run: python3 scripts/check-contract.py + shell: bash 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/.github/workflows/sdk-ci.yml b/.github/workflows/sdk-ci.yml index d9f0022..6086fbb 100644 --- a/.github/workflows/sdk-ci.yml +++ b/.github/workflows/sdk-ci.yml @@ -20,6 +20,8 @@ jobs: python-version: "3.11" - run: python -m pip install --upgrade build - run: python -m build + - run: python -m pip install dist/*.whl pytest pytest-asyncio + - run: python -m pytest tests -q # Verification only, never auto-fix: the SDK is generated formatted, so a diff here means # either hand-edited custom code or a regenerate that was not committed. ruff is pinned to # the release that produced these bytes, since a formatter minor can change them. 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..8567a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,61 @@ # Changelog +## [0.7.0](https://github.com/dedalus-labs/dedalus-python/compare/v0.6.0...v0.7.0) (2026-11-06) + + +### ⚠ BREAKING CHANGES + +* **api:** 30 breaking changes to the SDK surface. + - Removed body field `memory_mib` from `machines.update`. + - Removed body field `vcpu` from `machines.update`. + - Response of `machines.autoresizing.retrieve` changed from `settings` to `policy`. + - Operation `machines.autoresizing.update` route changed from `PUT /v1/machines/{machine_id}/autoresizing` to `PATCH /v1/machines/{machine_id}/autoresizing`. + - Removed body field `enabled` from `machines.autoresizing.update`. + - Response of `machines.autoresizing.update` changed from `settings` to `policy`. + - Operation `organization.autoresizing.update` route changed from `PUT /v1/organization/autoresizing` to `PATCH /v1/organization/autoresizing`. + - Removed body field `enabled` from `organization.autoresizing.update`. + - Removed operation `machines.executions.logs.reauthorize` (`POST /v1/machines/{machine_id}/executions/{execution_id}/logs/reauthorize`). + - Added required property `policy.cpu`. + - Added required property `policy.cpu_limit_source`. + - Added required property `policy.cpu_max_vcpu`. + - Added required property `policy.effective_cpu_max_vcpu`. + - Added required property `policy.effective_memory_max_mib`. + - Added required property `policy.memory`. + - Added required property `policy.memory_limit_source`. + - Added required property `policy.memory_max_mib`. + - Removed optional property `status.code`. + - Removed optional property `status.publication`. + - Added required property `lifecycle_status.autoresize`. + - Removed optional property `lifecycle_status.memory_assigned_mib`. + - Removed required property `lifecycle_status.memory_configured_mib`. + - Removed optional property `lifecycle_status.memory_last_autoresized_at`. + - Removed optional property `lifecycle_status.memory_resize_state`. + - Removed optional property `lifecycle_status.memory_target_mib`. + - Removed optional property `update_params.memory_mib`. + - Removed optional property `update_params.vcpu`. + - Removed schema `settings`. + - Removed schema `publication`. + - Removed schema `ReauthorizeResponseHeaders`. + +### Features + +* **api:** add auth scheme BrowserAuth (+1 more change) ([b016780](https://github.com/dedalus-labs/dedalus-python/commit/b016780a061a08941ca7d8e935e266cfecc4dabc)) +* **api:** integrate regenerated custom image contract ([0af3158](https://github.com/dedalus-labs/dedalus-python/commit/0af3158bfefb6b2f846ced27fd241a839920fc51)) +* **api:** update SDK surface (76 changes) ([bc3e12c](https://github.com/dedalus-labs/dedalus-python/commit/bc3e12c4dd8d4d0557b5e2847049a2ecc7df21df)) +* **images:** add python import workflows ([155dd8f](https://github.com/dedalus-labs/dedalus-python/commit/155dd8f56923c9ea5b6f3d737c333c27490678ea)) +* **images:** regenerate SDK with Python image workflows ([#18](https://github.com/dedalus-labs/dedalus-python/issues/18)) ([e088f07](https://github.com/dedalus-labs/dedalus-python/commit/e088f077c29cbd83bef42fa955eb72d5203a656b)) + + +### Bug Fixes + +* **images:** retain image identity on request timeout ([65814a1](https://github.com/dedalus-labs/dedalus-python/commit/65814a1074df2b18ba4b063a51bc09fa717937b8)) + + +### Chores + +* **api:** regenerate SDK ([7e6002f](https://github.com/dedalus-labs/dedalus-python/commit/7e6002f5a35a097dd9c4bec6ee9a97cf7ca2d7c7)) +* **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/README.md b/README.md index 0cd7cfb..b70adfa 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ The full API of this library can be found in [api.md](./api.md). - [Installation](#installation) - [Usage](#usage) +- [Image import](#image-import) - [API Reference](./api.md) - [Async](#async) - [Authentication](#authentication) @@ -31,6 +32,53 @@ pip install dedalus-sdk
+## Image import + +```python +from dedalus_sdk import Dedalus +from dedalus_sdk.images import import_image, poll_image + +with Dedalus() as client: + ready = import_image(client, "my-app.tar", name="my-app", platform="linux/amd64") + + # Return after upload and registration, then wait later. + accepted = import_image(client, "my-app.tar", wait=False) + image = poll_image(client, accepted["image_id"]) +``` + +Async clients use `async_import_image` and `async_poll_image` with the same options: + +```python +from dedalus_sdk import AsyncDedalus +from dedalus_sdk.images import async_import_image + +async with AsyncDedalus() as client: + ready = await async_import_image(client, "my-app.tar") +``` + +The helpers stream a `docker save` archive, register it, and wait for readiness by +default. Set `wait=False` to return after registration. `timeout` defaults to 3,600 +seconds for the import and `poll_interval` defaults to 2 seconds. `on_progress` +receives each observed image. `ImagePreparationError.image` preserves the image ID +on terminal failure or polling deadline expiry; timeout or task cancellation does +not cancel server preparation. + +The synchronous helper checks its deadline between I/O operations, uses the +remaining budget for request inactivity timeouts, and disables automatic API +retries. The async helper enforces its deadline with task cancellation and retains +the client's retry policy. Use the same `idempotency_key` when retrying an import +with unchanged input. + +Archives must be regular, nonempty files of at most 5 GiB. Storage uploads use a +separate HTTP client without API credentials. The helpers require the image API +to be deployed and enabled for your organization; they do not bypass access controls. + +Custom image imports currently require an Ubuntu 24.04 `linux/amd64` Docker-save +archive. On Apple silicon, build or pull with `--platform linux/amd64` before +saving the archive. The image provides the filesystem; container entrypoint, +CMD, environment, working directory, and user settings are not applied. Start +processes using Machine exec or SSH after the Machine reaches running. + ## Usage ```python @@ -39,7 +87,7 @@ import os from dedalus_sdk import Dedalus client = Dedalus( - x_api_key=os.environ.get("DEDALUS_X_API_KEY"), + browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"), ) machine = client.machines.create( @@ -91,13 +139,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
@@ -121,7 +171,7 @@ except APIStatusError as err: raise ``` -Documented error statuses: `401`, `403`, `409`, `429`, `503`, `default`. +Documented error statuses: `400`, `401`, `402`, `403`, `404`, `409`, `422`, `429`, `500`, `503`, `504`, `default`.
@@ -141,7 +191,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. | @@ -177,11 +228,12 @@ page = client.machines.list() - Set the `DEDALUS_LOG` environment variable to `info` or `debug` to enable HTTP logging. - Logs are emitted through the standard `logging` module under the `dedalus_sdk` logger. +- Importing the SDK wraps the process's log record factory: records from the SDK, `httpx`, `httpcore`, `hpack` and `websockets` loggers have credential headers, credential query parameters and URL userinfo logged as `***`. A record factory installed later that does not chain to the previous one turns this off.
## Requirements -- Python 3.8 or newer +- Python 3.9 or newer Powered by Scalar. diff --git a/SKILL.md b/SKILL.md index 35cd7f7..85fceb9 100644 --- a/SKILL.md +++ b/SKILL.md @@ -21,14 +21,15 @@ import os from dedalus_sdk import Dedalus client = Dedalus( - x_api_key=os.environ.get("DEDALUS_X_API_KEY"), + browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"), ) ``` Provide credentials using the options below. Environment variables are read automatically when the target runtime supports them: - `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 . +- `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"), + browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"), ) 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-contract.json b/api-contract.json new file mode 100644 index 0000000..c7418c4 --- /dev/null +++ b/api-contract.json @@ -0,0 +1,202 @@ +{ + "source_sha": "9f8d54f123e81b8bc7252f855c52ecbf7cc8ae42", + "spec_sha256": "c0b2db893e3017c5c83c14375e6085581584641f617ee7ea1305ab80e9b50421", + "operations": [ + { + "method": "GET", + "path": "/v1/images", + "bodyParams": [] + }, + { + "method": "POST", + "path": "/v1/images", + "bodyParams": [ + "credential_ref", + "name", + "platform", + "source" + ] + }, + { + "method": "POST", + "path": "/v1/images/uploads", + "bodyParams": [ + "platform", + "sha256", + "size_bytes" + ] + }, + { + "method": "DELETE", + "path": "/v1/images/{image_id}", + "bodyParams": [] + }, + { + "method": "GET", + "path": "/v1/images/{image_id}", + "bodyParams": [] + }, + { + "method": "PATCH", + "path": "/v1/images/{image_id}", + "bodyParams": [ + "name" + ] + }, + { + "method": "GET", + "path": "/v1/machines", + "bodyParams": [] + }, + { + "method": "POST", + "path": "/v1/machines", + "bodyParams": [ + "autosleep", + "image_id", + "memory_mib", + "storage_gib", + "vcpu" + ] + }, + { + "method": "DELETE", + "path": "/v1/machines/{machine_id}", + "bodyParams": [] + }, + { + "method": "GET", + "path": "/v1/machines/{machine_id}", + "bodyParams": [] + }, + { + "method": "PATCH", + "path": "/v1/machines/{machine_id}", + "bodyParams": [ + "autosleep", + "storage_gib" + ] + }, + { + "method": "GET", + "path": "/v1/machines/{machine_id}/autoresizing", + "bodyParams": [] + }, + { + "method": "PATCH", + "path": "/v1/machines/{machine_id}/autoresizing", + "bodyParams": [ + "cpu", + "cpu_max_vcpu", + "memory", + "memory_max_mib" + ] + }, + { + "method": "GET", + "path": "/v1/machines/{machine_id}/executions", + "bodyParams": [] + }, + { + "method": "POST", + "path": "/v1/machines/{machine_id}/executions", + "bodyParams": [ + "command", + "cwd", + "env", + "stdin", + "timeout_ms" + ] + }, + { + "method": "DELETE", + "path": "/v1/machines/{machine_id}/executions/{execution_id}", + "bodyParams": [] + }, + { + "method": "GET", + "path": "/v1/machines/{machine_id}/executions/{execution_id}", + "bodyParams": [] + }, + { + "method": "GET", + "path": "/v1/machines/{machine_id}/executions/{execution_id}/events", + "bodyParams": [] + }, + { + "method": "GET", + "path": "/v1/machines/{machine_id}/executions/{execution_id}/logs", + "bodyParams": [] + }, + { + "method": "POST", + "path": "/v1/machines/{machine_id}/executions/{execution_id}/logs/token", + "bodyParams": [] + }, + { + "method": "GET", + "path": "/v1/machines/{machine_id}/executions/{execution_id}/output", + "bodyParams": [] + }, + { + "method": "POST", + "path": "/v1/machines/{machine_id}/reboot", + "bodyParams": [] + }, + { + "method": "POST", + "path": "/v1/machines/{machine_id}/resize", + "bodyParams": [ + "memory_mib", + "vcpu" + ] + }, + { + "method": "POST", + "path": "/v1/machines/{machine_id}/sleep", + "bodyParams": [] + }, + { + "method": "GET", + "path": "/v1/machines/{machine_id}/ssh", + "bodyParams": [] + }, + { + "method": "POST", + "path": "/v1/machines/{machine_id}/ssh", + "bodyParams": [ + "public_key" + ] + }, + { + "method": "DELETE", + "path": "/v1/machines/{machine_id}/ssh/{session_id}", + "bodyParams": [] + }, + { + "method": "GET", + "path": "/v1/machines/{machine_id}/ssh/{session_id}", + "bodyParams": [] + }, + { + "method": "POST", + "path": "/v1/machines/{machine_id}/wake", + "bodyParams": [] + }, + { + "method": "GET", + "path": "/v1/organization/autoresizing", + "bodyParams": [] + }, + { + "method": "PATCH", + "path": "/v1/organization/autoresizing", + "bodyParams": [ + "cpu", + "cpu_max_vcpu", + "memory", + "memory_max_mib" + ] + } + ] +} diff --git a/api.md b/api.md index badbc20..6d0e06f 100644 --- a/api.md +++ b/api.md @@ -13,6 +13,7 @@ Complete reference of every operation, grouped by resource. See [the README](./R - [Sleep a running machine](#sleep-a-running-machine) - [Wake a sleeping machine](#wake-a-sleeping-machine) - [Reboot a machine with fresh memory](#reboot-a-machine-with-fresh-memory) + - [Resize machine](#resize-machine) - [`Machines Ssh`](#machines-ssh) - [List SSH sessions](#list-ssh-sessions) - [Create SSH session](#create-ssh-session) @@ -27,15 +28,22 @@ Complete reference of every operation, grouped by resource. See [the README](./R - [List execution events](#list-execution-events) - [`Machines Executions Logs`](#machines-executions-logs) - [Get execution log status](#get-execution-log-status) - - [Reauthorize execution log publication](#reauthorize-execution-log-publication) - [Create execution log read token](#create-execution-log-read-token) - [`Machines Autoresizing`](#machines-autoresizing) - - [Read this machine's RAM autoresizing settings](#read-this-machines-ram-autoresizing-settings) - - [Set this machine's RAM autoresizing settings](#set-this-machines-ram-autoresizing-settings) + - [Read this machine's autoresizing settings](#read-this-machines-autoresizing-settings) + - [Set this machine's autoresizing settings](#set-this-machines-autoresizing-settings) - [`Organization`](#organization) - [`Organization Autoresizing`](#organization-autoresizing) - - [Read organization RAM autoresizing policy](#read-organization-ram-autoresizing-policy) - - [Set organization RAM autoresizing policy](#set-organization-ram-autoresizing-policy) + - [Read organization autoresizing policy](#read-organization-autoresizing-policy) + - [Set organization autoresizing policy](#set-organization-autoresizing-policy) +- [`Images`](#images) + - [List image IDs owned by the authenticated organization](#list-image-ids-owned-by-the-authenticated-organization) + - [Register an immutable OCI image for preparation](#register-an-immutable-oci-image-for-preparation) + - [Read preparation status for an organization-owned image](#read-preparation-status-for-an-organization-owned-image) + - [Rename an organization-owned image](#rename-an-organization-owned-image) + - [Delete an organization-owned image](#delete-an-organization-owned-image) + - [`Images Uploads`](#images-uploads) + - [Reserve an immutable Docker image archive upload](#reserve-an-immutable-docker-image-archive-upload) ## Setup @@ -45,7 +53,7 @@ import os from dedalus_sdk import Dedalus client = Dedalus( - x_api_key=os.environ.get("DEDALUS_X_API_KEY"), + browser_token=os.environ.get("DEDALUS_BROWSER_TOKEN"), ) ``` @@ -92,6 +100,8 @@ machine = client.machines.retrieve( ### Update machine +Changes settings such as autosleep. + | Direction | Type | | --- | --- | | Request | [`MachineUpdateParams`](./src/dedalus_sdk/types/machine_update_params.py) | @@ -108,6 +118,7 @@ machine = client.machines.update( | Direction | Type | | --- | --- | +| Request | [`MachineDeleteParams`](./src/dedalus_sdk/types/machine_delete_params.py) | | Response | [`Machine`](./src/dedalus_sdk/types/machine.py) | ```python @@ -159,6 +170,22 @@ machine = client.machines.reboot( ) ``` +### Resize machine + +Wakes a sleeping Machine, then waits up to 30 seconds for the host to confirm CPU and RAM. Returns the Machine with its confirmed size. After RESIZE_TIMEOUT, repeat the same request and Idempotency-Key to wait again. + +| Direction | Type | +| --- | --- | +| Request | [`MachineResizeParams`](./src/dedalus_sdk/types/machine_resize_params.py) | +| Response | [`MachineResizeResponse`](./src/dedalus_sdk/types/machine_resize_response.py) | + +```python +machine = client.machines.resize( + machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", + idempotency_key="", +) +``` + ### `Machines Ssh` #### List SSH sessions @@ -312,20 +339,6 @@ log = client.machines.executions.logs.retrieve( ) ``` -##### Reauthorize execution log publication - -| Direction | Type | -| --- | --- | -| Response | [`Status`](./src/dedalus_sdk/types/machines/executions/status.py) | - -```python -log = client.machines.executions.logs.reauthorize( - machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", - execution_id="executionID", - idempotency_key="", -) -``` - ##### Create execution log read token | Direction | Type | @@ -342,11 +355,11 @@ log = client.machines.executions.logs.create_token( ### `Machines Autoresizing` -#### Read this machine's RAM autoresizing settings +#### Read this machine's autoresizing settings | Direction | Type | | --- | --- | -| Response | [`Settings`](./src/dedalus_sdk/types/machines/settings.py) | +| Response | [`Policy`](./src/dedalus_sdk/types/machines/policy.py) | ```python autoresizing = client.machines.autoresizing.retrieve( @@ -354,17 +367,18 @@ autoresizing = client.machines.autoresizing.retrieve( ) ``` -#### Set this machine's RAM autoresizing settings +#### Set this machine's autoresizing settings + +Changes RAM, CPU or both for this machine. An absent resource keeps its setting. | Direction | Type | | --- | --- | | Request | [`AutoresizingUpdateParams`](./src/dedalus_sdk/types/machines/autoresizing_update_params.py) | -| Response | [`Settings`](./src/dedalus_sdk/types/machines/settings.py) | +| Response | [`Policy`](./src/dedalus_sdk/types/machines/policy.py) | ```python autoresizing = client.machines.autoresizing.update( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", - enabled=False, idempotency_key="", ) ``` @@ -373,26 +387,125 @@ autoresizing = client.machines.autoresizing.update( ### `Organization Autoresizing` -#### Read organization RAM autoresizing policy +#### Read organization autoresizing policy | Direction | Type | | --- | --- | -| Response | [`Policy`](./src/dedalus_sdk/types/organization/policy.py) | +| Response | [`Policy`](./src/dedalus_sdk/types/machines/policy.py) | ```python autoresizing = client.organization.autoresizing.retrieve() ``` -#### Set organization RAM autoresizing policy +#### Set organization autoresizing policy + +Changes defaults copied to new Machines. Existing Machines retain their own policy. | Direction | Type | | --- | --- | | Request | [`AutoresizingUpdateParams`](./src/dedalus_sdk/types/organization/autoresizing_update_params.py) | -| Response | [`Policy`](./src/dedalus_sdk/types/organization/policy.py) | +| Response | [`Policy`](./src/dedalus_sdk/types/machines/policy.py) | ```python autoresizing = client.organization.autoresizing.update( - enabled=False, + idempotency_key="", +) +``` + +## `Images` + +### List image IDs owned by the authenticated organization + +Pages through the organization's images in ID order. A deleted image is left out; it still reads by ID. + +| Direction | Type | +| --- | --- | +| Request | [`ImageListParams`](./src/dedalus_sdk/types/image_list_params.py) | +| Response | [`ImageList`](./src/dedalus_sdk/types/image_list.py) | + +```python +image = client.images.list( + limit=100, +) +``` + +### Register an immutable OCI image for preparation + +Requires an entitled organization and linux/amd64 on the current fleet. Acceptance queues preparation; only a ready image can launch a machine. Private registry credentials are not yet supported. + +| Direction | Type | +| --- | --- | +| Request | [`ImageCreateParams`](./src/dedalus_sdk/types/image_create_params.py) | +| Response | [`Image`](./src/dedalus_sdk/types/image.py) | + +```python +image = client.images.create( + platform="linux/amd64", + source={"kind": ""}, + idempotency_key="", +) +``` + +### Read preparation status for an organization-owned image + +A preparing image reports its stage, the worker's last step and an estimated percent of the whole preparation. A failed image names the cause: IMAGE_OS_UNSUPPORTED, IMAGE_SOURCE_INVALID, IMAGE_SOURCE_NOT_FOUND and IMAGE_SOURCE_UNAUTHORIZED describe the image or its reference and are final; IMAGE_SOURCE_UNAVAILABLE, IMAGE_REGISTRY_UNREACHABLE, IMAGE_REGISTRY_RATE_LIMITED, IMAGE_ARCHIVE_UNAVAILABLE and IMAGE_EXECUTION_FAILED describe this attempt and are retryable with a new idempotency key. + +| Direction | Type | +| --- | --- | +| Response | [`Image`](./src/dedalus_sdk/types/image.py) | + +```python +image = client.images.retrieve( + image_id="imageID", +) +``` + +### Rename an organization-owned image + +Changes only the name. The image's source, platform and state are immutable. + +| Direction | Type | +| --- | --- | +| Request | [`ImageUpdateParams`](./src/dedalus_sdk/types/image_update_params.py) | +| Response | [`Image`](./src/dedalus_sdk/types/image.py) | + +```python +image = client.images.update( + image_id="imageID", + name="", + idempotency_key="", +) +``` + +### Delete an organization-owned image + +The image can create no new Machine and no longer counts against the organization's image limit. Machines already running from it keep working. The image stays readable, and deleting it again returns the same image. + +| Direction | Type | +| --- | --- | +| Response | [`Image`](./src/dedalus_sdk/types/image.py) | + +```python +image = client.images.delete( + image_id="imageID", + idempotency_key="", +) +``` + +### `Images Uploads` + +#### Reserve an immutable Docker image archive upload + +| Direction | Type | +| --- | --- | +| Request | [`UploadCreateParams`](./src/dedalus_sdk/types/images/upload_create_params.py) | +| Response | [`Reservation`](./src/dedalus_sdk/types/images/reservation.py) | + +```python +upload = client.images.uploads.create( + platform="", + sha256="", + size_bytes=0, idempotency_key="", ) ``` diff --git a/ci/contract.ts b/ci/contract.ts new file mode 100644 index 0000000..488e41c --- /dev/null +++ b/ci/contract.ts @@ -0,0 +1,24 @@ +import { command, workflow } from '@dedalus-labs/hollywood'; + +// Generate with Hollywood 0.0.5; this workflow never publishes packages. +export const contractCI = workflow({ + name: 'Generated API contract', + on: { push: {}, pull_request: {} }, + permissions: { contents: 'read' }, + jobs: { + contract: { + 'runs-on': 'ubuntu-24.04', + steps: [ + { uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' }, + { + name: 'Test contract guard', + run: command({ file: 'python3', args: ['scripts/test-contract.py'] }), + }, + { + name: 'Verify pinned public operations', + run: command({ file: 'python3', args: ['scripts/check-contract.py'] }), + }, + ], + }, + }, +}); diff --git a/generation-provenance.json b/generation-provenance.json new file mode 100644 index 0000000..0d44c1f --- /dev/null +++ b/generation-provenance.json @@ -0,0 +1,12 @@ +{ + "source_spec_sha": "9f8d54f123e81b8bc7252f855c52ecbf7cc8ae42", + "source_spec_sha256": "c0b2db893e3017c5c83c14375e6085581584641f617ee7ea1305ab80e9b50421", + "generator_version": "0.38.0", + "build_version": "0.1.16", + "configuration_sha256": "10d2f59b8a69dce627060e1104eac425cec104dd90d3177c08a29a902cb48875", + "operation_count": 31, + "generated_commit": "bc3e12c4dd8d4d0557b5e2847049a2ecc7df21df", + "download_sha256": null, + "integration_base": "scalar-next", + "published_package": false +} diff --git a/pyproject.toml b/pyproject.toml index 3ece763..f8c9e64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "dedalus-sdk" -version = "0.6.0" # x-release-please-version -description = "Controlplane API for Dedalus Cloud Services (DCS)." +version = "0.7.0" # x-release-please-version +description = "Create, run, and manage Dedalus Machines." readme = "README.md" license = "Apache-2.0" authors = [ @@ -13,10 +13,14 @@ dependencies = [ "pydantic>=1.9.0, <3", "typing-extensions>=4.14, <5", "anyio>=3.5.0, <5", - "distro>=1.7.0, <2", "sniffio", ] +[project.urls] +Source = "https://github.com/dedalus-labs/dedalus-python" +Issues = "https://github.com/dedalus-labs/dedalus-python/issues" +Changelog = "https://github.com/dedalus-labs/dedalus-python/blob/main/CHANGELOG.md" + [project.optional-dependencies] aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] diff --git a/scalar-sdk.manifest.json b/scalar-sdk.manifest.json index 0bcba7b..6896011 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.38.0", "servers": [ "https://dcs.dedaluslabs.ai" ], @@ -13,7 +13,8 @@ ], "auth": [ "apiKey", - "bearer" + "bearer", + "oauth2" ], "authDetails": [ { @@ -26,21 +27,37 @@ "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": [], "schemas": [ { - "name": "artifact_ref", - "source": "artifact_ref", + "name": "archive_request", + "source": "archive_request", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "artifact_id", - "publicName": "artifact_id", + "name": "platform", + "publicName": "platform", "required": true, "deprecated": false, "type": { @@ -49,89 +66,19 @@ } }, { - "name": "name", - "publicName": "name", - "required": true, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string" - } - } - ], - "additionalProperties": false - } - }, - { - "name": "execution_create_params", - "source": "execution_create_params", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ - { - "name": "command", - "publicName": "command", + "name": "sha256", + "publicName": "sha256", "required": true, "deprecated": false, - "type": { - "kind": "union", - "variants": [ - { - "kind": "array", - "items": { - "kind": "primitive", - "type": "string" - } - }, - { - "kind": "null" - } - ] - } - }, - { - "name": "cwd", - "publicName": "cwd", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string" - } - }, - { - "name": "env", - "publicName": "env", - "required": false, - "deprecated": false, - "type": { - "kind": "record", - "value": { - "kind": "primitive", - "type": "string" - }, - "propertyNames": { - "kind": "primitive", - "type": "string" - } - } - }, - { - "name": "stdin", - "publicName": "stdin", - "required": false, - "deprecated": false, "type": { "kind": "primitive", "type": "string" } }, { - "name": "timeout_ms", - "publicName": "timeout_ms", - "required": false, + "name": "size_bytes", + "publicName": "size_bytes", + "required": true, "deprecated": false, "type": { "kind": "primitive", @@ -144,17 +91,17 @@ } }, { - "name": "create_params", - "source": "create_params", + "name": "artifact_ref", + "source": "artifact_ref", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "autosleep", - "publicName": "autosleep", - "required": false, + "name": "artifact_id", + "publicName": "artifact_id", + "required": true, "deprecated": false, "type": { "kind": "primitive", @@ -162,62 +109,8 @@ } }, { - "name": "memory_mib", - "publicName": "memory_mib", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "integer", - "format": "int64", - "validation": { - "exclusiveMinimum": 0 - } - } - }, - { - "name": "storage_gib", - "publicName": "storage_gib", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "integer", - "format": "int64", - "validation": { - "exclusiveMinimum": 0 - } - } - }, - { - "name": "vcpu", - "publicName": "vcpu", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "number", - "format": "double", - "validation": { - "exclusiveMinimum": 0 - } - } - } - ], - "additionalProperties": false - } - }, - { - "name": "ssh_session_create_params", - "source": "ssh_session_create_params", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ - { - "name": "public_key", - "publicName": "public_key", + "name": "name", + "publicName": "name", "required": true, "deprecated": false, "type": { @@ -230,40 +123,61 @@ } }, { - "name": "error_detail", - "source": "error_detail", + "name": "autoresize_block", + "source": "autoresize_block", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "location", - "publicName": "location", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string" - } - }, - { - "name": "message", - "publicName": "message", + "name": "blocked_at", + "publicName": "blocked_at", "required": false, "deprecated": false, "type": { "kind": "primitive", - "type": "string" + "type": "string", + "format": "date-time" } }, { - "name": "value", - "publicName": "value", + "name": "blocked_reason", + "publicName": "blocked_reason", "required": false, "deprecated": false, "type": { - "kind": "unknown" + "kind": "enum", + "values": [ + "MACHINE_GROWTH_LIMIT_REACHED", + "MACHINE_TIER_SHAPE_EXCEEDED", + "MACHINE_QUOTA_EXCEEDED", + "MACHINE_MEMORY_ABOVE_LIMIT", + "ASSIGNED_HOST_CAPACITY_CONFLICT", + "MACHINE_PLACEMENT_EXHAUSTED", + "MACHINE_RESIZE_UNAVAILABLE", + "MACHINE_SHAPE_UNSUPPORTED" + ], + "names": [ + "MachineGrowthLimitReached", + "MachineTierShapeExceeded", + "MachineQuotaExceeded", + "MachineMemoryAboveLimit", + "AssignedHostCapacityConflict", + "MachinePlacementExhausted", + "MachineResizeUnavailable", + "MachineShapeUnsupported" + ], + "deprecations": [ + false, + false, + false, + false, + false, + false, + false, + false + ] } } ], @@ -271,43 +185,70 @@ } }, { - "name": "error_model", - "source": "error_model", + "name": "policy", + "source": "policy", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "detail", - "publicName": "detail", - "required": false, + "name": "cpu", + "publicName": "cpu", + "required": true, "deprecated": false, - "extensions": { - "source": [ - "examples" + "type": { + "kind": "enum", + "values": [ + "on", + "off" ], - "examples": "Property foo is required but is missing." - }, + "names": [ + "On", + "Off" + ], + "deprecations": [ + false, + false + ] + } + }, + { + "name": "cpu_limit_source", + "publicName": "cpu_limit_source", + "required": true, + "deprecated": false, "type": { - "kind": "primitive", - "type": "string" + "kind": "enum", + "values": [ + "plan", + "configured", + "platform" + ], + "names": [ + "Plan", + "Configured", + "Platform" + ], + "deprecations": [ + false, + false, + false + ] } }, { - "name": "errors", - "publicName": "errors", - "required": false, + "name": "cpu_max_vcpu", + "publicName": "cpu_max_vcpu", + "required": true, "deprecated": false, "type": { "kind": "union", "variants": [ { - "kind": "array", - "items": { - "kind": "ref", - "name": "error_detail" - } + "kind": "primitive", + "type": "number", + "format": "double" }, { "kind": "null" @@ -316,33 +257,21 @@ } }, { - "name": "instance", - "publicName": "instance", - "required": false, + "name": "effective_cpu_max_vcpu", + "publicName": "effective_cpu_max_vcpu", + "required": true, "deprecated": false, - "extensions": { - "source": [ - "examples" - ], - "examples": "https://example.com/error-log/abc123" - }, "type": { "kind": "primitive", - "type": "string", - "format": "uri" + "type": "number", + "format": "double" } }, { - "name": "status", - "publicName": "status", - "required": false, + "name": "effective_memory_max_mib", + "publicName": "effective_memory_max_mib", + "required": true, "deprecated": false, - "extensions": { - "source": [ - "examples" - ], - "examples": 400 - }, "type": { "kind": "primitive", "type": "integer", @@ -350,176 +279,52 @@ } }, { - "name": "title", - "publicName": "title", - "required": false, - "deprecated": false, - "extensions": { - "source": [ - "examples" - ], - "examples": "Bad Request" - }, - "type": { - "kind": "primitive", - "type": "string" - } - }, - { - "name": "type", - "publicName": "type", - "required": false, - "deprecated": false, - "extensions": { - "source": [ - "examples" - ], - "examples": "https://example.com/errors/example" - }, - "type": { - "kind": "primitive", - "type": "string", - "format": "uri" - } - } - ], - "additionalProperties": false - } - }, - { - "name": "execution_event", - "source": "execution_event", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ - { - "name": "at", - "publicName": "at", + "name": "enabled", + "publicName": "enabled", "required": true, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string", - "format": "date-time" - } - }, - { - "name": "chunk", - "publicName": "chunk", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string" - } - }, - { - "name": "error_code", - "publicName": "error_code", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string" - } - }, - { - "name": "error_message", - "publicName": "error_message", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string" - } - }, - { - "name": "exit_code", - "publicName": "exit_code", - "required": false, - "deprecated": false, + "deprecated": true, "type": { "kind": "primitive", - "type": "integer", - "format": "int64" + "type": "boolean" } }, { - "name": "sequence", - "publicName": "sequence", + "name": "memory", + "publicName": "memory", "required": true, "deprecated": false, - "type": { - "kind": "primitive", - "type": "integer", - "format": "int64" - } - }, - { - "name": "signal", - "publicName": "signal", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "integer", - "format": "int64" - } - }, - { - "name": "status", - "publicName": "status", - "required": false, - "deprecated": false, "type": { "kind": "enum", "values": [ - "wake_in_progress", - "queued", - "running", - "succeeded", - "failed", - "cancelled", - "expired" + "on", + "off" ], "names": [ - "WakeInProgress", - "Queued", - "Running", - "Succeeded", - "Failed", - "Cancelled", - "Expired" + "On", + "Off" ], "deprecations": [ - false, - false, - false, - false, - false, false, false ] } }, { - "name": "type", - "publicName": "type", + "name": "memory_limit_source", + "publicName": "memory_limit_source", "required": true, "deprecated": false, "type": { "kind": "enum", "values": [ - "lifecycle", - "stdout", - "stderr" + "plan", + "configured", + "platform" ], "names": [ - "Lifecycle", - "Stdout", - "Stderr" + "Plan", + "Configured", + "Platform" ], "deprecations": [ false, @@ -527,32 +332,74 @@ false ] } + }, + { + "name": "memory_max_mib", + "publicName": "memory_max_mib", + "required": true, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "primitive", + "type": "integer", + "format": "int64" + }, + { + "kind": "null" + } + ] + } } ], "additionalProperties": false } }, { - "name": "execution_events", - "source": "execution_events", + "name": "autoresizing_update", + "source": "autoresizing_update", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "items", - "publicName": "items", - "required": true, + "name": "cpu", + "publicName": "cpu", + "required": false, + "deprecated": false, + "type": { + "kind": "enum", + "values": [ + "on", + "off" + ], + "names": [ + "On", + "Off" + ], + "deprecations": [ + false, + false + ] + } + }, + { + "name": "cpu_max_vcpu", + "publicName": "cpu_max_vcpu", + "required": false, "deprecated": false, "type": { "kind": "union", "variants": [ { - "kind": "array", - "items": { - "kind": "ref", - "name": "execution_event" + "kind": "primitive", + "type": "number", + "format": "double", + "validation": { + "minimum": 0.25, + "multipleOf": 0.25 } }, { @@ -562,13 +409,46 @@ } }, { - "name": "next_cursor", - "publicName": "next_cursor", + "name": "memory", + "publicName": "memory", "required": false, "deprecated": false, "type": { - "kind": "primitive", - "type": "string" + "kind": "enum", + "values": [ + "on", + "off" + ], + "names": [ + "On", + "Off" + ], + "deprecations": [ + false, + false + ] + } + }, + { + "name": "memory_max_mib", + "publicName": "memory_max_mib", + "required": false, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "primitive", + "type": "integer", + "format": "int64", + "validation": { + "minimum": 256 + } + }, + { + "kind": "null" + } + ] } } ], @@ -576,16 +456,16 @@ } }, { - "name": "execution_list", - "source": "execution_list", + "name": "execution_create_params", + "source": "execution_create_params", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "items", - "publicName": "items", + "name": "command", + "publicName": "command", "required": true, "deprecated": false, "type": { @@ -594,8 +474,8 @@ { "kind": "array", "items": { - "kind": "ref", - "name": "execution" + "kind": "primitive", + "type": "string" } }, { @@ -605,40 +485,35 @@ } }, { - "name": "next_cursor", - "publicName": "next_cursor", + "name": "cwd", + "publicName": "cwd", "required": false, "deprecated": false, "type": { "kind": "primitive", "type": "string" } - } - ], - "additionalProperties": false - } - }, - { - "name": "status", - "source": "status", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ + }, { - "name": "capture", - "publicName": "capture", - "required": true, + "name": "env", + "publicName": "env", + "required": false, "deprecated": false, "type": { - "kind": "ref", - "name": "machine_execution_log_capture" + "kind": "record", + "value": { + "kind": "primitive", + "type": "string" + }, + "propertyNames": { + "kind": "primitive", + "type": "string" + } } }, { - "name": "code", - "publicName": "code", + "name": "stdin", + "publicName": "stdin", "required": false, "deprecated": false, "type": { @@ -647,9 +522,32 @@ } }, { - "name": "execution_id", - "publicName": "execution_id", - "required": true, + "name": "timeout_ms", + "publicName": "timeout_ms", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "integer", + "format": "int64" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "create_params", + "source": "create_params", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "autosleep", + "publicName": "autosleep", + "required": false, "deprecated": false, "type": { "kind": "primitive", @@ -657,23 +555,59 @@ } }, { - "name": "publication", - "publicName": "publication", + "name": "image_id", + "publicName": "image_id", "required": false, "deprecated": false, "type": { - "kind": "ref", - "name": "publication" + "kind": "primitive", + "type": "string", + "validation": { + "pattern": "^\\S+$", + "minLength": 1 + } } }, { - "name": "stream_id", - "publicName": "stream_id", + "name": "memory_mib", + "publicName": "memory_mib", "required": false, "deprecated": false, "type": { "kind": "primitive", - "type": "string" + "type": "integer", + "format": "int64", + "validation": { + "exclusiveMinimum": 0 + } + } + }, + { + "name": "storage_gib", + "publicName": "storage_gib", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "integer", + "format": "int64", + "validation": { + "exclusiveMinimum": 0 + } + } + }, + { + "name": "vcpu", + "publicName": "vcpu", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "number", + "format": "double", + "validation": { + "exclusiveMinimum": 0 + } } } ], @@ -681,26 +615,38 @@ } }, { - "name": "execution_output", - "source": "execution_output", + "name": "ssh_session_create_params", + "source": "ssh_session_create_params", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "execution_id", - "publicName": "execution_id", + "name": "public_key", + "publicName": "public_key", "required": true, "deprecated": false, "type": { "kind": "primitive", "type": "string" } - }, + } + ], + "additionalProperties": false + } + }, + { + "name": "error_detail", + "source": "error_detail", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ { - "name": "stderr", - "publicName": "stderr", + "name": "location", + "publicName": "location", "required": false, "deprecated": false, "type": { @@ -709,30 +655,67 @@ } }, { - "name": "stderr_bytes", - "publicName": "stderr_bytes", + "name": "message", + "publicName": "message", "required": false, "deprecated": false, "type": { "kind": "primitive", - "type": "integer", - "format": "int64" + "type": "string" } }, { - "name": "stderr_truncated", - "publicName": "stderr_truncated", + "name": "value", + "publicName": "value", + "required": false, + "deprecated": false, + "type": { + "kind": "unknown" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "error", + "source": "error", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "details", + "publicName": "details", "required": false, "deprecated": false, + "type": { + "kind": "record", + "value": { + "kind": "primitive", + "type": "string" + }, + "propertyNames": { + "kind": "primitive", + "type": "string" + } + } + }, + { + "name": "error_code", + "publicName": "error_code", + "required": true, + "deprecated": false, "type": { "kind": "primitive", - "type": "boolean" + "type": "string" } }, { - "name": "stdout", - "publicName": "stdout", - "required": false, + "name": "message", + "publicName": "message", + "required": true, "deprecated": false, "type": { "kind": "primitive", @@ -740,8 +723,8 @@ } }, { - "name": "stdout_bytes", - "publicName": "stdout_bytes", + "name": "retry_after_ms", + "publicName": "retry_after_ms", "required": false, "deprecated": false, "type": { @@ -751,9 +734,9 @@ } }, { - "name": "stdout_truncated", - "publicName": "stdout_truncated", - "required": false, + "name": "retryable", + "publicName": "retryable", + "required": true, "deprecated": false, "type": { "kind": "primitive", @@ -765,38 +748,33 @@ } }, { - "name": "execution", - "source": "execution", + "name": "error_model", + "source": "error_model", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "artifacts", - "publicName": "artifacts", + "name": "detail", + "publicName": "detail", "required": false, "deprecated": false, + "extensions": { + "source": [ + "examples" + ], + "examples": "Property foo is required but is missing." + }, "type": { - "kind": "union", - "variants": [ - { - "kind": "array", - "items": { - "kind": "ref", - "name": "artifact_ref" - } - }, - { - "kind": "null" - } - ] + "kind": "primitive", + "type": "string" } }, { - "name": "command", - "publicName": "command", - "required": true, + "name": "errors", + "publicName": "errors", + "required": false, "deprecated": false, "type": { "kind": "union", @@ -804,8 +782,8 @@ { "kind": "array", "items": { - "kind": "primitive", - "type": "string" + "kind": "ref", + "name": "error_detail" } }, { @@ -815,76 +793,103 @@ } }, { - "name": "completed_at", - "publicName": "completed_at", + "name": "instance", + "publicName": "instance", "required": false, "deprecated": false, + "extensions": { + "source": [ + "examples" + ], + "examples": "https://example.com/error-log/abc123" + }, "type": { "kind": "primitive", "type": "string", - "format": "date-time" + "format": "uri" } }, { - "name": "created_at", - "publicName": "created_at", - "required": true, + "name": "status", + "publicName": "status", + "required": false, "deprecated": false, + "extensions": { + "source": [ + "examples" + ], + "examples": 400 + }, "type": { "kind": "primitive", - "type": "string", - "format": "date-time" + "type": "integer", + "format": "int64" } }, { - "name": "creation_request_id", - "publicName": "creation_request_id", + "name": "title", + "publicName": "title", "required": false, "deprecated": false, + "extensions": { + "source": [ + "examples" + ], + "examples": "Bad Request" + }, "type": { "kind": "primitive", "type": "string" } }, { - "name": "creation_trace_id", - "publicName": "creation_trace_id", + "name": "type", + "publicName": "type", "required": false, "deprecated": false, + "extensions": { + "source": [ + "examples" + ], + "examples": "https://example.com/errors/example" + }, "type": { "kind": "primitive", - "type": "string" + "type": "string", + "format": "uri" } - }, + } + ], + "additionalProperties": false + } + }, + { + "name": "execution_event", + "source": "execution_event", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ { - "name": "cwd", - "publicName": "cwd", - "required": false, + "name": "at", + "publicName": "at", + "required": true, "deprecated": false, "type": { "kind": "primitive", - "type": "string" + "type": "string", + "format": "date-time" } }, { - "name": "env_keys", - "publicName": "env_keys", + "name": "chunk", + "publicName": "chunk", "required": false, "deprecated": false, "type": { - "kind": "union", - "variants": [ - { - "kind": "array", - "items": { - "kind": "primitive", - "type": "string" - } - }, - { - "kind": "null" - } - ] + "kind": "primitive", + "type": "string" } }, { @@ -907,16 +912,6 @@ "type": "string" } }, - { - "name": "execution_id", - "publicName": "execution_id", - "required": true, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string" - } - }, { "name": "exit_code", "publicName": "exit_code", @@ -929,42 +924,10 @@ } }, { - "name": "expires_at", - "publicName": "expires_at", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string", - "format": "date-time" - } - }, - { - "name": "log_capture", - "publicName": "log_capture", - "required": true, - "deprecated": false, - "type": { - "kind": "ref", - "name": "machine_execution_log_capture" - } - }, - { - "name": "machine_id", - "publicName": "machine_id", + "name": "sequence", + "publicName": "sequence", "required": true, "deprecated": false, - "type": { - "kind": "primitive", - "type": "string", - "format": "uuid" - } - }, - { - "name": "retry_after_ms", - "publicName": "retry_after_ms", - "required": false, - "deprecated": false, "type": { "kind": "primitive", "type": "integer", @@ -982,21 +945,10 @@ "format": "int64" } }, - { - "name": "started_at", - "publicName": "started_at", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string", - "format": "date-time" - } - }, { "name": "status", "publicName": "status", - "required": true, + "required": false, "deprecated": false, "type": { "kind": "enum", @@ -1030,45 +982,113 @@ } }, { - "name": "stderr_bytes", - "publicName": "stderr_bytes", - "required": false, + "name": "type", + "publicName": "type", + "required": true, "deprecated": false, "type": { - "kind": "primitive", - "type": "integer", - "format": "int64" + "kind": "enum", + "values": [ + "lifecycle", + "stdout", + "stderr" + ], + "names": [ + "Lifecycle", + "Stdout", + "Stderr" + ], + "deprecations": [ + false, + false, + false + ] } - }, + } + ], + "additionalProperties": false + } + }, + { + "name": "execution_events", + "source": "execution_events", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ { - "name": "stderr_truncated", - "publicName": "stderr_truncated", - "required": false, + "name": "items", + "publicName": "items", + "required": true, "deprecated": false, "type": { - "kind": "primitive", - "type": "boolean" + "kind": "union", + "variants": [ + { + "kind": "array", + "items": { + "kind": "ref", + "name": "execution_event" + } + }, + { + "kind": "null" + } + ] } }, { - "name": "stdout_bytes", - "publicName": "stdout_bytes", + "name": "next_cursor", + "publicName": "next_cursor", "required": false, "deprecated": false, "type": { "kind": "primitive", - "type": "integer", - "format": "int64" + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "execution_list", + "source": "execution_list", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "items", + "publicName": "items", + "required": true, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "array", + "items": { + "kind": "ref", + "name": "execution" + } + }, + { + "kind": "null" + } + ] } }, { - "name": "stdout_truncated", - "publicName": "stdout_truncated", + "name": "next_cursor", + "publicName": "next_cursor", "required": false, "deprecated": false, "type": { "kind": "primitive", - "type": "boolean" + "type": "string" } } ], @@ -1076,67 +1096,79 @@ } }, { - "name": "machine", - "source": "machine", + "name": "status", + "source": "status", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "autosleep_seconds", - "publicName": "autosleep_seconds", + "name": "capture", + "publicName": "capture", "required": true, "deprecated": false, "type": { - "kind": "primitive", - "type": "integer", - "format": "int64", - "validation": { - "minimum": 0, - "maximum": 9223372036 - } + "kind": "ref", + "name": "machine_execution_log_capture" } }, { - "name": "desired_state", - "publicName": "desired_state", + "name": "execution_id", + "publicName": "execution_id", "required": true, "deprecated": false, "type": { - "kind": "enum", - "values": [ - "running", - "sleeping", - "destroyed" - ], - "names": [ - "Running", - "Sleeping", - "Destroyed" - ], - "deprecations": [ - false, - false, - false - ] + "kind": "primitive", + "type": "string" } }, { - "name": "machine_id", - "publicName": "machine_id", + "name": "stream_id", + "publicName": "stream_id", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "execution_output", + "source": "execution_output", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "execution_id", + "publicName": "execution_id", "required": true, "deprecated": false, "type": { "kind": "primitive", - "type": "string", - "format": "uuid" + "type": "string" } }, { - "name": "memory_mib", - "publicName": "memory_mib", - "required": true, + "name": "stderr", + "publicName": "stderr", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "stderr_bytes", + "publicName": "stderr_bytes", + "required": false, "deprecated": false, "type": { "kind": "primitive", @@ -1145,50 +1177,61 @@ } }, { - "name": "phase", - "publicName": "phase", - "required": true, + "name": "stderr_truncated", + "publicName": "stderr_truncated", + "required": false, "deprecated": false, "type": { - "kind": "enum", - "values": [ - "accepted", - "placement_pending", - "starting", - "running", - "stopping", - "sleeping", - "destroying", - "destroyed", - "failed" - ], - "names": [ - "Accepted", - "PlacementPending", - "Starting", - "Running", - "Stopping", - "Sleeping", - "Destroying", - "Destroyed", - "Failed" - ], - "deprecations": [ - false, - false, - false, - false, - false, - false, - false, - false, - false - ] + "kind": "primitive", + "type": "boolean" } }, { - "name": "storage_gib", - "publicName": "storage_gib", + "name": "stdout", + "publicName": "stdout", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "stdout_bytes", + "publicName": "stdout_bytes", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "integer", + "format": "int64" + } + }, + { + "name": "stdout_truncated", + "publicName": "stdout_truncated", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "boolean" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "execution_progress", + "source": "execution_progress", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "completed", + "publicName": "completed", "required": true, "deprecated": false, "type": { @@ -1198,14 +1241,24 @@ } }, { - "name": "vcpu", - "publicName": "vcpu", + "name": "step", + "publicName": "step", "required": true, "deprecated": false, "type": { "kind": "primitive", - "type": "number", - "format": "double" + "type": "string" + } + }, + { + "name": "total", + "publicName": "total", + "required": true, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "integer", + "format": "int64" } } ], @@ -1213,28 +1266,60 @@ } }, { - "name": "lifecycle_status", - "source": "lifecycle_status", + "name": "execution", + "source": "execution", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "last_error", - "publicName": "last_error", + "name": "artifacts", + "publicName": "artifacts", "required": false, "deprecated": false, "type": { - "kind": "primitive", - "type": "string" + "kind": "union", + "variants": [ + { + "kind": "array", + "items": { + "kind": "ref", + "name": "artifact_ref" + } + }, + { + "kind": "null" + } + ] } }, { - "name": "last_progress_at", - "publicName": "last_progress_at", + "name": "command", + "publicName": "command", "required": true, "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "array", + "items": { + "kind": "primitive", + "type": "string" + } + }, + { + "kind": "null" + } + ] + } + }, + { + "name": "completed_at", + "publicName": "completed_at", + "required": false, + "deprecated": false, "type": { "kind": "primitive", "type": "string", @@ -1242,8 +1327,8 @@ } }, { - "name": "last_transition_at", - "publicName": "last_transition_at", + "name": "created_at", + "publicName": "created_at", "required": true, "deprecated": false, "type": { @@ -1253,127 +1338,79 @@ } }, { - "name": "memory_assigned_mib", - "publicName": "memory_assigned_mib", + "name": "creation_request_id", + "publicName": "creation_request_id", "required": false, "deprecated": false, "type": { "kind": "primitive", - "type": "integer", - "format": "int64", - "validation": { - "minimum": 1 - } + "type": "string" } }, { - "name": "memory_configured_mib", - "publicName": "memory_configured_mib", - "required": true, + "name": "creation_trace_id", + "publicName": "creation_trace_id", + "required": false, "deprecated": false, "type": { "kind": "primitive", - "type": "integer", - "format": "int64", - "validation": { - "minimum": 1 - } + "type": "string" } }, { - "name": "memory_last_autoresized_at", - "publicName": "memory_last_autoresized_at", + "name": "cwd", + "publicName": "cwd", "required": false, "deprecated": false, "type": { "kind": "primitive", - "type": "string", - "format": "date-time" + "type": "string" } }, { - "name": "memory_resize_state", - "publicName": "memory_resize_state", + "name": "env_keys", + "publicName": "env_keys", "required": false, "deprecated": false, "type": { - "kind": "enum", - "values": [ - "stable", - "error", - "pending_capacity" - ], - "names": [ - "Stable", - "Error", - "PendingCapacity" - ], - "deprecations": [ - false, - false, - false + "kind": "union", + "variants": [ + { + "kind": "array", + "items": { + "kind": "primitive", + "type": "string" + } + }, + { + "kind": "null" + } ] } }, { - "name": "memory_target_mib", - "publicName": "memory_target_mib", + "name": "error_code", + "publicName": "error_code", "required": false, "deprecated": false, "type": { "kind": "primitive", - "type": "integer", - "format": "int64", - "validation": { - "minimum": 1 - } + "type": "string" } }, { - "name": "phase", - "publicName": "phase", - "required": true, + "name": "error_message", + "publicName": "error_message", + "required": false, "deprecated": false, "type": { - "kind": "enum", - "values": [ - "accepted", - "placement_pending", - "starting", - "running", - "stopping", - "sleeping", - "destroying", - "destroyed", - "failed" - ], - "names": [ - "Accepted", - "PlacementPending", - "Starting", - "Running", - "Stopping", - "Sleeping", - "Destroying", - "Destroyed", - "Failed" - ], - "deprecations": [ - false, - false, - false, - false, - false, - false, - false, - false, - false - ] + "kind": "primitive", + "type": "string" } }, { - "name": "reason", - "publicName": "reason", + "name": "execution_id", + "publicName": "execution_id", "required": true, "deprecated": false, "type": { @@ -1382,70 +1419,111 @@ } }, { - "name": "retryable", - "publicName": "retryable", - "required": true, + "name": "exit_code", + "publicName": "exit_code", + "required": false, "deprecated": false, "type": { "kind": "primitive", - "type": "boolean" + "type": "integer", + "format": "int64" } }, { - "name": "revision", - "publicName": "revision", - "required": true, + "name": "expires_at", + "publicName": "expires_at", + "required": false, "deprecated": false, "type": { "kind": "primitive", - "type": "string" + "type": "string", + "format": "date-time" } - } - ], - "additionalProperties": false - } - }, - { - "name": "machine_detail_response", - "source": "machine_detail_response", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ + }, { - "name": "autosleep_seconds", - "publicName": "autosleep_seconds", + "name": "log_capture", + "publicName": "log_capture", + "required": true, + "deprecated": false, + "type": { + "kind": "ref", + "name": "machine_execution_log_capture" + } + }, + { + "name": "machine_id", + "publicName": "machine_id", "required": true, "deprecated": false, + "type": { + "kind": "primitive", + "type": "string", + "format": "uuid" + } + }, + { + "name": "retry_after_ms", + "publicName": "retry_after_ms", + "required": false, + "deprecated": false, "type": { "kind": "primitive", "type": "integer", - "format": "int64", - "validation": { - "minimum": 0, - "maximum": 9223372036 - } + "format": "int64" } }, { - "name": "desired_state", - "publicName": "desired_state", + "name": "signal", + "publicName": "signal", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "integer", + "format": "int64" + } + }, + { + "name": "started_at", + "publicName": "started_at", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string", + "format": "date-time" + } + }, + { + "name": "status", + "publicName": "status", "required": true, "deprecated": false, "type": { "kind": "enum", "values": [ + "wake_in_progress", + "queued", "running", - "sleeping", - "destroyed" + "succeeded", + "failed", + "cancelled", + "expired" ], "names": [ + "WakeInProgress", + "Queued", "Running", - "Sleeping", - "Destroyed" + "Succeeded", + "Failed", + "Cancelled", + "Expired" ], "deprecations": [ + false, + false, + false, + false, false, false, false @@ -1453,20 +1531,9 @@ } }, { - "name": "machine_id", - "publicName": "machine_id", - "required": true, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string", - "format": "uuid" - } - }, - { - "name": "memory_mib", - "publicName": "memory_mib", - "required": true, + "name": "stderr_bytes", + "publicName": "stderr_bytes", + "required": false, "deprecated": false, "type": { "kind": "primitive", @@ -1475,19 +1542,19 @@ } }, { - "name": "status", - "publicName": "status", - "required": true, + "name": "stderr_truncated", + "publicName": "stderr_truncated", + "required": false, "deprecated": false, "type": { - "kind": "ref", - "name": "lifecycle_status" + "kind": "primitive", + "type": "boolean" } }, { - "name": "storage_gib", - "publicName": "storage_gib", - "required": true, + "name": "stdout_bytes", + "publicName": "stdout_bytes", + "required": false, "deprecated": false, "type": { "kind": "primitive", @@ -1496,14 +1563,13 @@ } }, { - "name": "vcpu", - "publicName": "vcpu", - "required": true, + "name": "stdout_truncated", + "publicName": "stdout_truncated", + "required": false, "deprecated": false, "type": { "kind": "primitive", - "type": "number", - "format": "double" + "type": "boolean" } } ], @@ -1511,73 +1577,70 @@ } }, { - "name": "machine_execution_log_capture", - "source": "machine_execution_log_capture", + "name": "execution_upload", + "source": "execution_upload", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "code", - "publicName": "code", - "required": false, + "name": "expires_at", + "publicName": "expires_at", + "required": true, "deprecated": false, "type": { "kind": "primitive", - "type": "string" + "type": "string", + "format": "date-time" } }, { - "name": "lost_bytes", - "publicName": "lost_bytes", - "required": false, + "name": "headers", + "publicName": "headers", + "required": true, "deprecated": false, "type": { - "kind": "primitive", - "type": "string", - "validation": { - "pattern": "^(0|[1-9][0-9]*)$", - "maxLength": 20 + "kind": "record", + "value": { + "kind": "union", + "variants": [ + { + "kind": "array", + "items": { + "kind": "primitive", + "type": "string" + } + }, + { + "kind": "null" + } + ] + }, + "propertyNames": { + "kind": "primitive", + "type": "string" } } }, { - "name": "state", - "publicName": "state", - "required": false, + "name": "method", + "publicName": "method", + "required": true, "deprecated": false, "type": { - "kind": "enum", - "values": [ - "pending", - "incomplete", - "unavailable" - ], - "names": [ - "Pending", - "Incomplete", - "Unavailable" - ], - "deprecations": [ - false, - false, - false - ] + "kind": "primitive", + "type": "string" } }, { - "name": "unconfirmed_bytes", - "publicName": "unconfirmed_bytes", - "required": false, + "name": "url", + "publicName": "url", + "required": true, "deprecated": false, "type": { "kind": "primitive", - "type": "string", - "validation": { - "pattern": "^(0|[1-9][0-9]*)$", - "maxLength": 20 - } + "type": "string" } } ], @@ -1585,29 +1648,99 @@ } }, { - "name": "machine_id_path_segment", - "source": "machine_id_path_segment", + "name": "image_list", + "source": "image_list", "publicAliases": [], "deprecated": false, - "extensions": { - "source": [ - "examples" - ], - "examples": "017f22e2-79b0-7cc3-98c4-dc0c0c07398f" - }, "type": { - "kind": "primitive", - "type": "string", - "validation": { - "pattern": "^(dm-)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", - "minLength": 36, - "maxLength": 39 - } + "kind": "object", + "properties": [ + { + "name": "image_ids", + "publicName": "image_ids", + "required": true, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "array", + "items": { + "kind": "primitive", + "type": "string" + } + }, + { + "kind": "null" + } + ] + } + }, + { + "name": "next_cursor", + "publicName": "next_cursor", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false } }, { - "name": "machine_list_item", - "source": "machine_list_item", + "name": "lifecycle_autoresize", + "source": "lifecycle_autoresize", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "cpu", + "publicName": "cpu", + "required": true, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "ref", + "name": "autoresize_block" + }, + { + "kind": "null" + } + ] + } + }, + { + "name": "memory", + "publicName": "memory", + "required": true, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "ref", + "name": "autoresize_block" + }, + { + "kind": "null" + } + ] + } + } + ], + "additionalProperties": false + } + }, + { + "name": "machine", + "source": "machine", "publicAliases": [], "deprecated": false, "type": { @@ -1628,17 +1761,6 @@ } } }, - { - "name": "created_at", - "publicName": "created_at", - "required": true, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string", - "format": "date-time" - } - }, { "name": "desired_state", "publicName": "desired_state", @@ -1754,118 +1876,26 @@ } }, { - "name": "machine_list", - "source": "machine_list", + "name": "lifecycle_status", + "source": "lifecycle_status", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "items", - "publicName": "items", + "name": "autoresize", + "publicName": "autoresize", "required": true, "deprecated": false, "type": { - "kind": "union", - "variants": [ - { - "kind": "array", - "items": { - "kind": "ref", - "name": "machine_list_item" - } - }, - { - "kind": "null" - } - ] + "kind": "ref", + "name": "lifecycle_autoresize" } }, { - "name": "next_cursor", - "publicName": "next_cursor", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string" - } - } - ], - "additionalProperties": false - } - }, - { - "name": "policy", - "source": "policy", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ - { - "name": "enabled", - "publicName": "enabled", - "required": true, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "boolean" - } - } - ], - "additionalProperties": false - } - }, - { - "name": "settings", - "source": "settings", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ - { - "name": "enabled", - "publicName": "enabled", - "required": true, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "boolean" - } - } - ], - "additionalProperties": false - } - }, - { - "name": "public_path_segment", - "source": "public_path_segment", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string", - "validation": { - "pattern": "^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$", - "minLength": 1, - "maxLength": 253 - } - } - }, - { - "name": "publication", - "source": "publication", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ - { - "name": "code", - "publicName": "code", + "name": "last_error", + "publicName": "last_error", "required": false, "deprecated": false, "type": { @@ -1874,71 +1904,72 @@ } }, { - "name": "complete", - "publicName": "complete", + "name": "last_progress_at", + "publicName": "last_progress_at", "required": true, "deprecated": false, "type": { "kind": "primitive", - "type": "boolean" + "type": "string", + "format": "date-time" } }, { - "name": "queued_messages", - "publicName": "queued_messages", + "name": "last_transition_at", + "publicName": "last_transition_at", "required": true, "deprecated": false, "type": { "kind": "primitive", - "type": "integer", - "format": "int64", - "validation": { - "minimum": 0 - } + "type": "string", + "format": "date-time" } }, { - "name": "state", - "publicName": "state", + "name": "phase", + "publicName": "phase", "required": true, "deprecated": false, "type": { "kind": "enum", "values": [ - "active", - "finishing", - "failed", - "closed" + "accepted", + "placement_pending", + "starting", + "running", + "stopping", + "sleeping", + "destroying", + "destroyed", + "failed" ], "names": [ - "Active", - "Finishing", - "Failed", - "Closed" + "Accepted", + "PlacementPending", + "Starting", + "Running", + "Stopping", + "Sleeping", + "Destroying", + "Destroyed", + "Failed" ], "deprecations": [ + false, + false, + false, + false, + false, false, false, false, false ] } - } - ], - "additionalProperties": false - } - }, - { - "name": "read_token", - "source": "read_token", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ + }, { - "name": "access_token", - "publicName": "access_token", + "name": "reason", + "publicName": "reason", "required": true, "deprecated": false, "type": { @@ -1947,91 +1978,111 @@ } }, { - "name": "base_url", - "publicName": "base_url", + "name": "retryable", + "publicName": "retryable", "required": true, "deprecated": false, "type": { "kind": "primitive", - "type": "string" + "type": "boolean" } }, { - "name": "execution_id", - "publicName": "execution_id", + "name": "revision", + "publicName": "revision", "required": true, "deprecated": false, "type": { "kind": "primitive", "type": "string" } - }, + } + ], + "additionalProperties": false + } + }, + { + "name": "machine_detail_response", + "source": "machine_detail_response", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ { - "name": "expires_at", - "publicName": "expires_at", + "name": "autosleep_seconds", + "publicName": "autosleep_seconds", "required": true, "deprecated": false, "type": { "kind": "primitive", - "type": "string", - "format": "date-time" + "type": "integer", + "format": "int64", + "validation": { + "minimum": 0, + "maximum": 9223372036 + } } }, { - "name": "stream_id", - "publicName": "stream_id", + "name": "desired_state", + "publicName": "desired_state", "required": true, "deprecated": false, "type": { - "kind": "primitive", - "type": "string" + "kind": "enum", + "values": [ + "running", + "sleeping", + "destroyed" + ], + "names": [ + "Running", + "Sleeping", + "Destroyed" + ], + "deprecations": [ + false, + false, + false + ] } }, { - "name": "token_type", - "publicName": "token_type", + "name": "machine_id", + "publicName": "machine_id", "required": true, "deprecated": false, "type": { "kind": "primitive", - "type": "string" + "type": "string", + "format": "uuid" } - } - ], - "additionalProperties": false - } - }, - { - "name": "ssh_connection", - "source": "ssh_connection", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ + }, { - "name": "endpoint", - "publicName": "endpoint", + "name": "memory_mib", + "publicName": "memory_mib", "required": true, "deprecated": false, "type": { "kind": "primitive", - "type": "string" + "type": "integer", + "format": "int64" } }, { - "name": "host_trust", - "publicName": "host_trust", - "required": false, + "name": "status", + "publicName": "status", + "required": true, "deprecated": false, "type": { "kind": "ref", - "name": "ssh_host_trust" + "name": "lifecycle_status" } }, { - "name": "port", - "publicName": "port", + "name": "storage_gib", + "publicName": "storage_gib", "required": true, "deprecated": false, "type": { @@ -2041,23 +2092,14 @@ } }, { - "name": "ssh_username", - "publicName": "ssh_username", + "name": "vcpu", + "publicName": "vcpu", "required": true, "deprecated": false, "type": { "kind": "primitive", - "type": "string" - } - }, - { - "name": "user_certificate", - "publicName": "user_certificate", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string" + "type": "number", + "format": "double" } } ], @@ -2065,17 +2107,17 @@ } }, { - "name": "ssh_host_trust", - "source": "ssh_host_trust", + "name": "machine_execution_log_capture", + "source": "machine_execution_log_capture", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "host_pattern", - "publicName": "host_pattern", - "required": true, + "name": "code", + "publicName": "code", + "required": false, "deprecated": false, "type": { "kind": "primitive", @@ -2083,31 +2125,55 @@ } }, { - "name": "kind", - "publicName": "kind", - "required": true, + "name": "lost_bytes", + "publicName": "lost_bytes", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string", + "validation": { + "pattern": "^(0|[1-9][0-9]*)$", + "maxLength": 20 + } + } + }, + { + "name": "state", + "publicName": "state", + "required": false, "deprecated": false, "type": { "kind": "enum", "values": [ - "cert_authority" + "pending", + "incomplete", + "unavailable" ], "names": [ - "CertAuthority" + "Pending", + "Incomplete", + "Unavailable" ], "deprecations": [ + false, + false, false ] } }, { - "name": "public_key", - "publicName": "public_key", - "required": true, + "name": "unconfirmed_bytes", + "publicName": "unconfirmed_bytes", + "required": false, "deprecated": false, "type": { "kind": "primitive", - "type": "string" + "type": "string", + "validation": { + "pattern": "^(0|[1-9][0-9]*)$", + "maxLength": 20 + } } } ], @@ -2115,64 +2181,47 @@ } }, { - "name": "ssh_session_list", - "source": "ssh_session_list", + "name": "machine_id_path_segment", + "source": "machine_id_path_segment", "publicAliases": [], "deprecated": false, - "type": { - "kind": "object", - "properties": [ - { - "name": "items", - "publicName": "items", - "required": true, - "deprecated": false, - "type": { - "kind": "union", - "variants": [ - { - "kind": "array", - "items": { - "kind": "ref", - "name": "ssh_session" - } - }, - { - "kind": "null" - } - ] - } - }, - { - "name": "next_cursor", - "publicName": "next_cursor", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string" - } - } + "extensions": { + "source": [ + "examples" ], - "additionalProperties": false + "examples": "017f22e2-79b0-7cc3-98c4-dc0c0c07398f" + }, + "type": { + "kind": "primitive", + "type": "string", + "validation": { + "pattern": "^(dm-)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "minLength": 36, + "maxLength": 39 + } } }, { - "name": "ssh_session", - "source": "ssh_session", + "name": "machine_list_item", + "source": "machine_list_item", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "connection", - "publicName": "connection", - "required": false, + "name": "autosleep_seconds", + "publicName": "autosleep_seconds", + "required": true, "deprecated": false, "type": { - "kind": "ref", - "name": "ssh_connection" + "kind": "primitive", + "type": "integer", + "format": "int64", + "validation": { + "minimum": 0, + "maximum": 9223372036 + } } }, { @@ -2187,34 +2236,27 @@ } }, { - "name": "error_code", - "publicName": "error_code", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string" - } - }, - { - "name": "error_message", - "publicName": "error_message", - "required": false, + "name": "desired_state", + "publicName": "desired_state", + "required": true, "deprecated": false, "type": { - "kind": "primitive", - "type": "string" - } - }, - { - "name": "expires_at", - "publicName": "expires_at", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string", - "format": "date-time" + "kind": "enum", + "values": [ + "running", + "sleeping", + "destroyed" + ], + "names": [ + "Running", + "Sleeping", + "Destroyed" + ], + "deprecations": [ + false, + false, + false + ] } }, { @@ -2229,20 +2271,9 @@ } }, { - "name": "ready_at", - "publicName": "ready_at", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string", - "format": "date-time" - } - }, - { - "name": "retry_after_ms", - "publicName": "retry_after_ms", - "required": false, + "name": "memory_mib", + "publicName": "memory_mib", + "required": true, "deprecated": false, "type": { "kind": "primitive", @@ -2251,36 +2282,32 @@ } }, { - "name": "session_id", - "publicName": "session_id", - "required": true, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string" - } - }, - { - "name": "status", - "publicName": "status", + "name": "phase", + "publicName": "phase", "required": true, "deprecated": false, "type": { "kind": "enum", "values": [ - "wake_in_progress", - "ssh_in_progress", - "ready", - "closed", - "expired", + "accepted", + "placement_pending", + "starting", + "running", + "stopping", + "sleeping", + "destroying", + "destroyed", "failed" ], "names": [ - "WakeInProgress", - "SshInProgress", - "Ready", - "Closed", - "Expired", + "Accepted", + "PlacementPending", + "Starting", + "Running", + "Stopping", + "Sleeping", + "Destroying", + "Destroyed", "Failed" ], "deprecations": [ @@ -2289,47 +2316,17 @@ false, false, false, + false, + false, + false, false ] } - } - ], - "additionalProperties": false - } - }, - { - "name": "update_params", - "source": "update_params", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ - { - "name": "autosleep", - "publicName": "autosleep", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string" - } - }, - { - "name": "memory_mib", - "publicName": "memory_mib", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "integer", - "format": "int64" - } }, { "name": "storage_gib", "publicName": "storage_gib", - "required": false, + "required": true, "deprecated": false, "type": { "kind": "primitive", @@ -2340,7 +2337,7 @@ { "name": "vcpu", "publicName": "vcpu", - "required": false, + "required": true, "deprecated": false, "type": { "kind": "primitive", @@ -2353,36 +2350,59 @@ } }, { - "name": "CreateResponseHeaders", - "source": "CreateResponseHeaders", + "name": "machine_list", + "source": "machine_list", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "ETag", - "publicName": "ETag", - "required": false, + "name": "items", + "publicName": "items", + "required": true, "deprecated": false, "type": { - "kind": "primitive", - "type": "string" + "kind": "union", + "variants": [ + { + "kind": "array", + "items": { + "kind": "ref", + "name": "machine_list_item" + } + }, + { + "kind": "null" + } + ] } }, { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", + "name": "next_cursor", + "publicName": "next_cursor", "required": false, "deprecated": false, "type": { "kind": "primitive", "type": "string" } - }, + } + ], + "additionalProperties": false + } + }, + { + "name": "update_params", + "source": "update_params", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ { - "name": "Location", - "publicName": "Location", + "name": "autosleep", + "publicName": "autosleep", "required": false, "deprecated": false, "type": { @@ -2391,21 +2411,14 @@ } }, { - "name": "X-Dedalus-Storage-Operation-Id", - "publicName": "X-Dedalus-Storage-Operation-Id", + "name": "storage_gib", + "publicName": "storage_gib", "required": false, "deprecated": false, "type": { - "kind": "union", - "variants": [ - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "null" - } - ] + "kind": "primitive", + "type": "integer", + "format": "int64" } } ], @@ -2413,54 +2426,70 @@ } }, { - "name": "CreateResponseHeaders", - "source": "CreateResponseHeaders", + "name": "preparation_context", + "source": "preparation_context", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "ETag", - "publicName": "ETag", - "required": false, + "name": "percent", + "publicName": "percent", + "required": true, "deprecated": false, "type": { "kind": "primitive", - "type": "string" + "type": "integer", + "format": "int64" } }, { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", + "name": "progress", + "publicName": "progress", "required": false, "deprecated": false, "type": { - "kind": "primitive", - "type": "string" + "kind": "ref", + "name": "execution_progress" } }, { - "name": "Location", - "publicName": "Location", - "required": false, + "name": "stage", + "publicName": "stage", + "required": true, "deprecated": false, "type": { "kind": "primitive", "type": "string" } - }, + } + ], + "additionalProperties": false + } + }, + { + "name": "public_image_rename_input_body", + "source": "public_image_rename_input_body", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ { - "name": "X-Dedalus-Storage-Operation-Id", - "publicName": "X-Dedalus-Storage-Operation-Id", - "required": false, + "name": "name", + "publicName": "name", + "required": true, "deprecated": false, "type": { "kind": "union", "variants": [ { "kind": "primitive", - "type": "string" + "type": "string", + "validation": { + "maxLength": 128 + } }, { "kind": "null" @@ -2473,65 +2502,65 @@ } }, { - "name": "CreateStatus401ResponseHeaders", - "source": "CreateStatus401ResponseHeaders", + "name": "public_image_submit_input_body", + "source": "public_image_submit_input_body", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", + "name": "credential_ref", + "publicName": "credential_ref", "required": false, "deprecated": false, "type": { "kind": "primitive", "type": "string" } - } - ], - "additionalProperties": false - } - }, - { - "name": "CreateStatus403ResponseHeaders", - "source": "CreateStatus403ResponseHeaders", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ + }, { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", + "name": "name", + "publicName": "name", "required": false, "deprecated": false, "type": { "kind": "primitive", - "type": "string" + "type": "string", + "validation": { + "maxLength": 128 + } } - } - ], - "additionalProperties": false - } - }, - { - "name": "CreateStatus409ResponseHeaders", - "source": "CreateStatus409ResponseHeaders", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ + }, { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", - "required": false, + "name": "platform", + "publicName": "platform", + "required": true, "deprecated": false, "type": { - "kind": "primitive", - "type": "string" + "kind": "enum", + "values": [ + "linux/amd64", + "linux/arm64" + ], + "names": [ + "LinuxAmd64", + "LinuxArm64" + ], + "deprecations": [ + false, + false + ] + } + }, + { + "name": "source", + "publicName": "source", + "required": true, + "deprecated": false, + "type": { + "kind": "ref", + "name": "source" } } ], @@ -2539,83 +2568,83 @@ } }, { - "name": "CreateStatus429ResponseHeaders", - "source": "CreateStatus429ResponseHeaders", + "name": "public_path_segment", + "source": "public_path_segment", "publicAliases": [], "deprecated": false, "type": { - "kind": "object", - "properties": [ - { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string" - } - } - ], - "additionalProperties": false + "kind": "primitive", + "type": "string", + "validation": { + "pattern": "^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$", + "minLength": 1, + "maxLength": 253 + } } }, { - "name": "CreateStatus503ResponseHeaders", - "source": "CreateStatus503ResponseHeaders", + "name": "read_token", + "source": "read_token", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", - "required": false, + "name": "access_token", + "publicName": "access_token", + "required": true, "deprecated": false, "type": { "kind": "primitive", "type": "string" } - } - ], - "additionalProperties": false - } - }, - { - "name": "CreateStatusdefaultResponseHeaders", - "source": "CreateStatusdefaultResponseHeaders", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ + }, { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", - "required": false, + "name": "base_url", + "publicName": "base_url", + "required": true, "deprecated": false, "type": { "kind": "primitive", "type": "string" } - } - ], - "additionalProperties": false - } - }, - { - "name": "RetrieveResponseHeaders", - "source": "RetrieveResponseHeaders", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ + }, { - "name": "ETag", - "publicName": "ETag", - "required": false, + "name": "execution_id", + "publicName": "execution_id", + "required": true, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "expires_at", + "publicName": "expires_at", + "required": true, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string", + "format": "date-time" + } + }, + { + "name": "stream_id", + "publicName": "stream_id", + "required": true, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "token_type", + "publicName": "token_type", + "required": true, "deprecated": false, "type": { "kind": "primitive", @@ -2627,47 +2656,58 @@ } }, { - "name": "UpdateResponseHeaders", - "source": "UpdateResponseHeaders", + "name": "image", + "source": "image", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "ETag", - "publicName": "ETag", + "name": "context", + "publicName": "context", "required": false, "deprecated": false, "type": { - "kind": "primitive", - "type": "string" + "kind": "ref", + "name": "preparation_context" } }, { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", - "required": false, + "name": "created_at", + "publicName": "created_at", + "required": true, "deprecated": false, "type": { "kind": "primitive", - "type": "string" + "type": "string", + "format": "date-time" } }, { - "name": "Location", - "publicName": "Location", + "name": "error", + "publicName": "error", "required": false, "deprecated": false, + "type": { + "kind": "ref", + "name": "error" + } + }, + { + "name": "image_id", + "publicName": "image_id", + "required": true, + "deprecated": false, "type": { "kind": "primitive", "type": "string" } }, { - "name": "X-Dedalus-Storage-Operation-Id", - "publicName": "X-Dedalus-Storage-Operation-Id", - "required": false, + "name": "name", + "publicName": "name", + "required": true, "deprecated": false, "type": { "kind": "union", @@ -2681,23 +2721,11 @@ } ] } - } - ], - "additionalProperties": false - } - }, - { - "name": "UpdateResponseHeaders", - "source": "UpdateResponseHeaders", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ + }, { - "name": "ETag", - "publicName": "ETag", - "required": false, + "name": "platform", + "publicName": "platform", + "required": true, "deprecated": false, "type": { "kind": "primitive", @@ -2705,8 +2733,8 @@ } }, { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", + "name": "reason", + "publicName": "reason", "required": false, "deprecated": false, "type": { @@ -2715,31 +2743,23 @@ } }, { - "name": "Location", - "publicName": "Location", - "required": false, + "name": "source", + "publicName": "source", + "required": true, "deprecated": false, "type": { - "kind": "primitive", - "type": "string" + "kind": "ref", + "name": "source" } }, { - "name": "X-Dedalus-Storage-Operation-Id", - "publicName": "X-Dedalus-Storage-Operation-Id", - "required": false, + "name": "state", + "publicName": "state", + "required": true, "deprecated": false, "type": { - "kind": "union", - "variants": [ - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "null" - } - ] + "kind": "primitive", + "type": "string" } } ], @@ -2747,21 +2767,31 @@ } }, { - "name": "UpdateStatus401ResponseHeaders", - "source": "UpdateStatus401ResponseHeaders", + "name": "reservation", + "source": "reservation", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", - "required": false, + "name": "source", + "publicName": "source", + "required": true, "deprecated": false, "type": { - "kind": "primitive", - "type": "string" + "kind": "ref", + "name": "source" + } + }, + { + "name": "upload", + "publicName": "upload", + "required": true, + "deprecated": false, + "type": { + "kind": "ref", + "name": "execution_upload" } } ], @@ -2769,21 +2799,33 @@ } }, { - "name": "UpdateStatus403ResponseHeaders", - "source": "UpdateStatus403ResponseHeaders", + "name": "resize_machine_request", + "source": "resize_machine_request", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", + "name": "memory_mib", + "publicName": "memory_mib", "required": false, "deprecated": false, "type": { "kind": "primitive", - "type": "string" + "type": "integer", + "format": "int64" + } + }, + { + "name": "vcpu", + "publicName": "vcpu", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "number", + "format": "double" } } ], @@ -2791,38 +2833,57 @@ } }, { - "name": "UpdateStatus409ResponseHeaders", - "source": "UpdateStatus409ResponseHeaders", + "name": "ssh_connection", + "source": "ssh_connection", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", - "required": false, - "deprecated": false, + "name": "endpoint", + "publicName": "endpoint", + "required": true, + "deprecated": false, "type": { "kind": "primitive", "type": "string" } - } - ], - "additionalProperties": false - } - }, - { - "name": "UpdateStatus429ResponseHeaders", - "source": "UpdateStatus429ResponseHeaders", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ + }, { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", + "name": "host_trust", + "publicName": "host_trust", + "required": false, + "deprecated": false, + "type": { + "kind": "ref", + "name": "ssh_host_trust" + } + }, + { + "name": "port", + "publicName": "port", + "required": true, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "integer", + "format": "int64" + } + }, + { + "name": "ssh_username", + "publicName": "ssh_username", + "required": true, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "user_certificate", + "publicName": "user_certificate", "required": false, "deprecated": false, "type": { @@ -2835,17 +2896,45 @@ } }, { - "name": "UpdateStatus503ResponseHeaders", - "source": "UpdateStatus503ResponseHeaders", + "name": "ssh_host_trust", + "source": "ssh_host_trust", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", - "required": false, + "name": "host_pattern", + "publicName": "host_pattern", + "required": true, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "kind", + "publicName": "kind", + "required": true, + "deprecated": false, + "type": { + "kind": "enum", + "values": [ + "cert_authority" + ], + "names": [ + "CertAuthority" + ], + "deprecations": [ + false + ] + } + }, + { + "name": "public_key", + "publicName": "public_key", + "required": true, "deprecated": false, "type": { "kind": "primitive", @@ -2857,16 +2946,37 @@ } }, { - "name": "UpdateStatusdefaultResponseHeaders", - "source": "UpdateStatusdefaultResponseHeaders", + "name": "ssh_session_list", + "source": "ssh_session_list", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", + "name": "items", + "publicName": "items", + "required": true, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "array", + "items": { + "kind": "ref", + "name": "ssh_session" + } + }, + { + "kind": "null" + } + ] + } + }, + { + "name": "next_cursor", + "publicName": "next_cursor", "required": false, "deprecated": false, "type": { @@ -2879,26 +2989,37 @@ } }, { - "name": "DeleteResponseHeaders", - "source": "DeleteResponseHeaders", + "name": "ssh_session", + "source": "ssh_session", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "ETag", - "publicName": "ETag", + "name": "connection", + "publicName": "connection", "required": false, "deprecated": false, + "type": { + "kind": "ref", + "name": "ssh_connection" + } + }, + { + "name": "created_at", + "publicName": "created_at", + "required": true, + "deprecated": false, "type": { "kind": "primitive", - "type": "string" + "type": "string", + "format": "date-time" } }, { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", + "name": "error_code", + "publicName": "error_code", "required": false, "deprecated": false, "type": { @@ -2907,8 +3028,8 @@ } }, { - "name": "Location", - "publicName": "Location", + "name": "error_message", + "publicName": "error_message", "required": false, "deprecated": false, "type": { @@ -2917,20 +3038,89 @@ } }, { - "name": "X-Dedalus-Storage-Operation-Id", - "publicName": "X-Dedalus-Storage-Operation-Id", + "name": "expires_at", + "publicName": "expires_at", "required": false, "deprecated": false, "type": { - "kind": "union", - "variants": [ - { - "kind": "primitive", - "type": "string" - }, - { - "kind": "null" - } + "kind": "primitive", + "type": "string", + "format": "date-time" + } + }, + { + "name": "machine_id", + "publicName": "machine_id", + "required": true, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string", + "format": "uuid" + } + }, + { + "name": "ready_at", + "publicName": "ready_at", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string", + "format": "date-time" + } + }, + { + "name": "retry_after_ms", + "publicName": "retry_after_ms", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "integer", + "format": "int64" + } + }, + { + "name": "session_id", + "publicName": "session_id", + "required": true, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "status", + "publicName": "status", + "required": true, + "deprecated": false, + "type": { + "kind": "enum", + "values": [ + "wake_in_progress", + "ssh_in_progress", + "ready", + "closed", + "expired", + "failed" + ], + "names": [ + "WakeInProgress", + "SshInProgress", + "Ready", + "Closed", + "Expired", + "Failed" + ], + "deprecations": [ + false, + false, + false, + false, + false, + false ] } } @@ -2939,17 +3129,17 @@ } }, { - "name": "DeleteResponseHeaders", - "source": "DeleteResponseHeaders", + "name": "source", + "source": "source", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "ETag", - "publicName": "ETag", - "required": false, + "name": "kind", + "publicName": "kind", + "required": true, "deprecated": false, "type": { "kind": "primitive", @@ -2957,8 +3147,8 @@ } }, { - "name": "Idempotency-Key", - "publicName": "Idempotency-Key", + "name": "reference", + "publicName": "reference", "required": false, "deprecated": false, "type": { @@ -2967,8 +3157,8 @@ } }, { - "name": "Location", - "publicName": "Location", + "name": "sha256", + "publicName": "sha256", "required": false, "deprecated": false, "type": { @@ -2977,13 +3167,65 @@ } }, { - "name": "X-Dedalus-Storage-Operation-Id", - "publicName": "X-Dedalus-Storage-Operation-Id", + "name": "upload_id", + "publicName": "upload_id", "required": false, "deprecated": false, "type": { - "kind": "union", - "variants": [ + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateResponseHeaders", + "source": "CreateResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "ETag", + "publicName": "ETag", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "Location", + "publicName": "Location", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "X-Dedalus-Storage-Operation-Id", + "publicName": "X-Dedalus-Storage-Operation-Id", + "required": false, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ { "kind": "primitive", "type": "string" @@ -2999,13 +3241,23 @@ } }, { - "name": "DeleteStatus401ResponseHeaders", - "source": "DeleteStatus401ResponseHeaders", + "name": "CreateResponseHeaders", + "source": "CreateResponseHeaders", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ + { + "name": "ETag", + "publicName": "ETag", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, { "name": "Idempotency-Key", "publicName": "Idempotency-Key", @@ -3015,14 +3267,42 @@ "kind": "primitive", "type": "string" } + }, + { + "name": "Location", + "publicName": "Location", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "X-Dedalus-Storage-Operation-Id", + "publicName": "X-Dedalus-Storage-Operation-Id", + "required": false, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "null" + } + ] + } } ], "additionalProperties": false } }, { - "name": "DeleteStatus403ResponseHeaders", - "source": "DeleteStatus403ResponseHeaders", + "name": "CreateStatus401ResponseHeaders", + "source": "CreateStatus401ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3043,8 +3323,8 @@ } }, { - "name": "DeleteStatus409ResponseHeaders", - "source": "DeleteStatus409ResponseHeaders", + "name": "CreateStatus403ResponseHeaders", + "source": "CreateStatus403ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3065,8 +3345,8 @@ } }, { - "name": "DeleteStatus429ResponseHeaders", - "source": "DeleteStatus429ResponseHeaders", + "name": "CreateStatus409ResponseHeaders", + "source": "CreateStatus409ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3087,8 +3367,8 @@ } }, { - "name": "DeleteStatus503ResponseHeaders", - "source": "DeleteStatus503ResponseHeaders", + "name": "CreateStatus429ResponseHeaders", + "source": "CreateStatus429ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3109,8 +3389,8 @@ } }, { - "name": "DeleteStatusdefaultResponseHeaders", - "source": "DeleteStatusdefaultResponseHeaders", + "name": "CreateStatus503ResponseHeaders", + "source": "CreateStatus503ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3131,8 +3411,52 @@ } }, { - "name": "SleepResponseHeaders", - "source": "SleepResponseHeaders", + "name": "CreateStatusdefaultResponseHeaders", + "source": "CreateStatusdefaultResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "RetrieveResponseHeaders", + "source": "RetrieveResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "ETag", + "publicName": "ETag", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "UpdateResponseHeaders", + "source": "UpdateResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3191,8 +3515,8 @@ } }, { - "name": "SleepResponseHeaders", - "source": "SleepResponseHeaders", + "name": "UpdateResponseHeaders", + "source": "UpdateResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3251,8 +3575,8 @@ } }, { - "name": "SleepStatus401ResponseHeaders", - "source": "SleepStatus401ResponseHeaders", + "name": "UpdateStatus401ResponseHeaders", + "source": "UpdateStatus401ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3273,8 +3597,8 @@ } }, { - "name": "SleepStatus403ResponseHeaders", - "source": "SleepStatus403ResponseHeaders", + "name": "UpdateStatus403ResponseHeaders", + "source": "UpdateStatus403ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3295,8 +3619,8 @@ } }, { - "name": "SleepStatus409ResponseHeaders", - "source": "SleepStatus409ResponseHeaders", + "name": "UpdateStatus409ResponseHeaders", + "source": "UpdateStatus409ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3317,8 +3641,8 @@ } }, { - "name": "SleepStatus429ResponseHeaders", - "source": "SleepStatus429ResponseHeaders", + "name": "UpdateStatus429ResponseHeaders", + "source": "UpdateStatus429ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3339,8 +3663,8 @@ } }, { - "name": "SleepStatus503ResponseHeaders", - "source": "SleepStatus503ResponseHeaders", + "name": "UpdateStatus503ResponseHeaders", + "source": "UpdateStatus503ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3361,8 +3685,8 @@ } }, { - "name": "SleepStatusdefaultResponseHeaders", - "source": "SleepStatusdefaultResponseHeaders", + "name": "UpdateStatusdefaultResponseHeaders", + "source": "UpdateStatusdefaultResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3383,8 +3707,8 @@ } }, { - "name": "WakeResponseHeaders", - "source": "WakeResponseHeaders", + "name": "DeleteResponseHeaders", + "source": "DeleteResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3443,8 +3767,8 @@ } }, { - "name": "WakeResponseHeaders", - "source": "WakeResponseHeaders", + "name": "DeleteResponseHeaders", + "source": "DeleteResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3503,8 +3827,8 @@ } }, { - "name": "WakeStatus401ResponseHeaders", - "source": "WakeStatus401ResponseHeaders", + "name": "DeleteStatus401ResponseHeaders", + "source": "DeleteStatus401ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3525,8 +3849,8 @@ } }, { - "name": "WakeStatus403ResponseHeaders", - "source": "WakeStatus403ResponseHeaders", + "name": "DeleteStatus403ResponseHeaders", + "source": "DeleteStatus403ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3547,8 +3871,8 @@ } }, { - "name": "WakeStatus409ResponseHeaders", - "source": "WakeStatus409ResponseHeaders", + "name": "DeleteStatus409ResponseHeaders", + "source": "DeleteStatus409ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3569,8 +3893,8 @@ } }, { - "name": "WakeStatus429ResponseHeaders", - "source": "WakeStatus429ResponseHeaders", + "name": "DeleteStatus429ResponseHeaders", + "source": "DeleteStatus429ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3591,8 +3915,8 @@ } }, { - "name": "WakeStatus503ResponseHeaders", - "source": "WakeStatus503ResponseHeaders", + "name": "DeleteStatus503ResponseHeaders", + "source": "DeleteStatus503ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3613,8 +3937,8 @@ } }, { - "name": "WakeStatusdefaultResponseHeaders", - "source": "WakeStatusdefaultResponseHeaders", + "name": "DeleteStatusdefaultResponseHeaders", + "source": "DeleteStatusdefaultResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3635,8 +3959,8 @@ } }, { - "name": "RebootResponseHeaders", - "source": "RebootResponseHeaders", + "name": "SleepResponseHeaders", + "source": "SleepResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3695,8 +4019,8 @@ } }, { - "name": "RebootResponseHeaders", - "source": "RebootResponseHeaders", + "name": "SleepResponseHeaders", + "source": "SleepResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3755,8 +4079,8 @@ } }, { - "name": "RebootStatus401ResponseHeaders", - "source": "RebootStatus401ResponseHeaders", + "name": "SleepStatus401ResponseHeaders", + "source": "SleepStatus401ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3777,8 +4101,8 @@ } }, { - "name": "RebootStatus403ResponseHeaders", - "source": "RebootStatus403ResponseHeaders", + "name": "SleepStatus403ResponseHeaders", + "source": "SleepStatus403ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3799,8 +4123,8 @@ } }, { - "name": "RebootStatus409ResponseHeaders", - "source": "RebootStatus409ResponseHeaders", + "name": "SleepStatus409ResponseHeaders", + "source": "SleepStatus409ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3821,8 +4145,8 @@ } }, { - "name": "RebootStatus429ResponseHeaders", - "source": "RebootStatus429ResponseHeaders", + "name": "SleepStatus429ResponseHeaders", + "source": "SleepStatus429ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3843,8 +4167,8 @@ } }, { - "name": "RebootStatus503ResponseHeaders", - "source": "RebootStatus503ResponseHeaders", + "name": "SleepStatus503ResponseHeaders", + "source": "SleepStatus503ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3865,8 +4189,8 @@ } }, { - "name": "RebootStatusdefaultResponseHeaders", - "source": "RebootStatusdefaultResponseHeaders", + "name": "SleepStatusdefaultResponseHeaders", + "source": "SleepStatusdefaultResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3887,13 +4211,23 @@ } }, { - "name": "CreateResponseHeaders", - "source": "CreateResponseHeaders", + "name": "WakeResponseHeaders", + "source": "WakeResponseHeaders", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ + { + "name": "ETag", + "publicName": "ETag", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, { "name": "Idempotency-Key", "publicName": "Idempotency-Key", @@ -3903,19 +4237,57 @@ "kind": "primitive", "type": "string" } + }, + { + "name": "Location", + "publicName": "Location", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "X-Dedalus-Storage-Operation-Id", + "publicName": "X-Dedalus-Storage-Operation-Id", + "required": false, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "null" + } + ] + } } ], "additionalProperties": false } }, { - "name": "CreateStatus401ResponseHeaders", - "source": "CreateStatus401ResponseHeaders", + "name": "WakeResponseHeaders", + "source": "WakeResponseHeaders", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ + { + "name": "ETag", + "publicName": "ETag", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, { "name": "Idempotency-Key", "publicName": "Idempotency-Key", @@ -3925,14 +4297,42 @@ "kind": "primitive", "type": "string" } + }, + { + "name": "Location", + "publicName": "Location", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "X-Dedalus-Storage-Operation-Id", + "publicName": "X-Dedalus-Storage-Operation-Id", + "required": false, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "null" + } + ] + } } ], "additionalProperties": false } }, { - "name": "CreateStatus403ResponseHeaders", - "source": "CreateStatus403ResponseHeaders", + "name": "WakeStatus401ResponseHeaders", + "source": "WakeStatus401ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3953,8 +4353,8 @@ } }, { - "name": "CreateStatus409ResponseHeaders", - "source": "CreateStatus409ResponseHeaders", + "name": "WakeStatus403ResponseHeaders", + "source": "WakeStatus403ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3975,8 +4375,8 @@ } }, { - "name": "CreateStatus429ResponseHeaders", - "source": "CreateStatus429ResponseHeaders", + "name": "WakeStatus409ResponseHeaders", + "source": "WakeStatus409ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -3997,8 +4397,8 @@ } }, { - "name": "CreateStatus503ResponseHeaders", - "source": "CreateStatus503ResponseHeaders", + "name": "WakeStatus429ResponseHeaders", + "source": "WakeStatus429ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -4019,8 +4419,8 @@ } }, { - "name": "CreateStatusdefaultResponseHeaders", - "source": "CreateStatusdefaultResponseHeaders", + "name": "WakeStatus503ResponseHeaders", + "source": "WakeStatus503ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -4041,8 +4441,8 @@ } }, { - "name": "CreateResponseHeaders", - "source": "CreateResponseHeaders", + "name": "WakeStatusdefaultResponseHeaders", + "source": "WakeStatusdefaultResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -4063,13 +4463,83 @@ } }, { - "name": "CreateStatus401ResponseHeaders", - "source": "CreateStatus401ResponseHeaders", + "name": "RebootResponseHeaders", + "source": "RebootResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "ETag", + "publicName": "ETag", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "Location", + "publicName": "Location", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "X-Dedalus-Storage-Operation-Id", + "publicName": "X-Dedalus-Storage-Operation-Id", + "required": false, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "null" + } + ] + } + } + ], + "additionalProperties": false + } + }, + { + "name": "RebootResponseHeaders", + "source": "RebootResponseHeaders", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ + { + "name": "ETag", + "publicName": "ETag", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, { "name": "Idempotency-Key", "publicName": "Idempotency-Key", @@ -4079,14 +4549,42 @@ "kind": "primitive", "type": "string" } + }, + { + "name": "Location", + "publicName": "Location", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "X-Dedalus-Storage-Operation-Id", + "publicName": "X-Dedalus-Storage-Operation-Id", + "required": false, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "null" + } + ] + } } ], "additionalProperties": false } }, { - "name": "CreateStatus403ResponseHeaders", - "source": "CreateStatus403ResponseHeaders", + "name": "RebootStatus401ResponseHeaders", + "source": "RebootStatus401ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -4107,8 +4605,8 @@ } }, { - "name": "CreateStatus409ResponseHeaders", - "source": "CreateStatus409ResponseHeaders", + "name": "RebootStatus403ResponseHeaders", + "source": "RebootStatus403ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -4129,8 +4627,8 @@ } }, { - "name": "CreateStatus429ResponseHeaders", - "source": "CreateStatus429ResponseHeaders", + "name": "RebootStatus409ResponseHeaders", + "source": "RebootStatus409ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -4151,8 +4649,8 @@ } }, { - "name": "CreateStatus503ResponseHeaders", - "source": "CreateStatus503ResponseHeaders", + "name": "RebootStatus429ResponseHeaders", + "source": "RebootStatus429ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -4173,8 +4671,8 @@ } }, { - "name": "CreateStatusdefaultResponseHeaders", - "source": "CreateStatusdefaultResponseHeaders", + "name": "RebootStatus503ResponseHeaders", + "source": "RebootStatus503ResponseHeaders", "publicAliases": [], "deprecated": false, "type": { @@ -4195,16 +4693,16 @@ } }, { - "name": "RetrieveResponseHeaders", - "source": "RetrieveResponseHeaders", + "name": "RebootStatusdefaultResponseHeaders", + "source": "RebootStatusdefaultResponseHeaders", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "Cache-Control", - "publicName": "Cache-Control", + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", "required": false, "deprecated": false, "type": { @@ -4217,114 +4715,1797 @@ } }, { - "name": "ReauthorizeResponseHeaders", - "source": "ReauthorizeResponseHeaders", + "name": "ResizeResponseHeaders", + "source": "ResizeResponseHeaders", "publicAliases": [], "deprecated": false, "type": { "kind": "object", "properties": [ { - "name": "Cache-Control", - "publicName": "Cache-Control", + "name": "ETag", + "publicName": "ETag", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", "required": false, "deprecated": false, "type": { "kind": "primitive", "type": "string" } + }, + { + "name": "Location", + "publicName": "Location", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "X-Dedalus-Storage-Operation-Id", + "publicName": "X-Dedalus-Storage-Operation-Id", + "required": false, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "null" + } + ] + } + } + ], + "additionalProperties": false + } + }, + { + "name": "ResizeStatus400ResponseHeaders", + "source": "ResizeStatus400ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "ResizeStatus401ResponseHeaders", + "source": "ResizeStatus401ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "ResizeStatus402ResponseHeaders", + "source": "ResizeStatus402ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "ResizeStatus403ResponseHeaders", + "source": "ResizeStatus403ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "ResizeStatus404ResponseHeaders", + "source": "ResizeStatus404ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "ResizeStatus409ResponseHeaders", + "source": "ResizeStatus409ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "ResizeStatus422ResponseHeaders", + "source": "ResizeStatus422ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "ResizeStatus429ResponseHeaders", + "source": "ResizeStatus429ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "ResizeStatus500ResponseHeaders", + "source": "ResizeStatus500ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "ResizeStatus503ResponseHeaders", + "source": "ResizeStatus503ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "ResizeStatus504ResponseHeaders", + "source": "ResizeStatus504ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "ResizeStatusdefaultResponseHeaders", + "source": "ResizeStatusdefaultResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "Retry-After", + "publicName": "Retry-After", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "x-should-retry", + "publicName": "x-should-retry", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateResponseHeaders", + "source": "CreateResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatus401ResponseHeaders", + "source": "CreateStatus401ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatus403ResponseHeaders", + "source": "CreateStatus403ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatus409ResponseHeaders", + "source": "CreateStatus409ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatus429ResponseHeaders", + "source": "CreateStatus429ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatus503ResponseHeaders", + "source": "CreateStatus503ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatusdefaultResponseHeaders", + "source": "CreateStatusdefaultResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateResponseHeaders", + "source": "CreateResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatus401ResponseHeaders", + "source": "CreateStatus401ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatus403ResponseHeaders", + "source": "CreateStatus403ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatus409ResponseHeaders", + "source": "CreateStatus409ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatus429ResponseHeaders", + "source": "CreateStatus429ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatus503ResponseHeaders", + "source": "CreateStatus503ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatusdefaultResponseHeaders", + "source": "CreateStatusdefaultResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "RetrieveResponseHeaders", + "source": "RetrieveResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Cache-Control", + "publicName": "Cache-Control", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateTokenResponseHeaders", + "source": "CreateTokenResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Cache-Control", + "publicName": "Cache-Control", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateResponseHeaders", + "source": "CreateResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatus401ResponseHeaders", + "source": "CreateStatus401ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatus403ResponseHeaders", + "source": "CreateStatus403ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatusdefaultResponseHeaders", + "source": "CreateStatusdefaultResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateResponseHeaders", + "source": "CreateResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatus401ResponseHeaders", + "source": "CreateStatus401ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatus403ResponseHeaders", + "source": "CreateStatus403ResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CreateStatusdefaultResponseHeaders", + "source": "CreateStatusdefaultResponseHeaders", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "Idempotency-Key", + "publicName": "Idempotency-Key", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + } + ], + "resources": [ + "machines", + "machines.ssh", + "machines.executions", + "machines.executions.logs", + "machines.autoresizing", + "organization", + "organization.autoresizing", + "images", + "images.uploads" + ], + "publicResources": [ + "machines", + "machines.ssh", + "machines.executions", + "machines.executions.logs", + "machines.autoresizing", + "organization", + "organization.autoresizing", + "images", + "images.uploads" + ], + "operations": [ + { + "resource": "machines", + "publicResource": "machines", + "operation": "list", + "publicOperation": "list", + "deprecated": false, + "method": "GET", + "path": "/v1/machines", + "pathParams": [], + "publicPathParams": [], + "queryParams": [ + "limit", + "cursor" + ], + "publicQueryParams": [ + "limit", + "cursor" + ], + "headerParams": [], + "publicHeaderParams": [], + "bodyParams": [], + "publicBodyParams": [], + "bodyParamDetails": [], + "publicPositionalParams": [], + "pathParamDetails": [], + "queryParamDetails": [ + { + "name": "limit", + "required": false, + "style": "form", + "explode": false + }, + { + "name": "cursor", + "required": false, + "style": "form", + "explode": false + } + ], + "headerParamDetails": [], + "cookieParams": [], + "publicCookieParams": [], + "cookieParamDetails": [], + "paramsModel": { + "publicName": "MachineListParams" + }, + "response": { + "status": "200", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + "responseModel": { + "name": "machine_list", + "publicAliases": [] + }, + "result": { + "successStatus": "200", + "errorStatuses": [ + "401", + "403", + "429", + "503", + "default" + ] + }, + "errorResponses": [ + { + "status": "401", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + { + "status": "403", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + { + "status": "429", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + { + "status": "503", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + { + "status": "default", + "contentType": "application/problem+json", + "encoding": "json", + "contents": [ + { + "contentType": "application/problem+json", + "encoding": "json" + } + ], + "model": { + "name": "error_model", + "publicAliases": [] + } + } + ], + "responseLinks": [], + "pagination": { + "style": "cursor", + "itemsField": "items", + "cursorField": "next_cursor", + "cursorParam": "cursor", + "cursorFields": [ + "next_cursor" + ], + "cursorParams": [ + "cursor" + ], + "requestLocation": "query", + "responseLocation": "body", + "itemModel": { + "name": "machine_list_item", + "publicAliases": [] + } + }, + "transport": "http" + }, + { + "resource": "machines", + "publicResource": "machines", + "operation": "create", + "publicOperation": "create", + "deprecated": false, + "method": "POST", + "path": "/v1/machines", + "pathParams": [], + "publicPathParams": [], + "queryParams": [], + "publicQueryParams": [], + "headerParams": [], + "publicHeaderParams": [], + "bodyParams": [ + "autosleep", + "image_id", + "memory_mib", + "storage_gib", + "vcpu" + ], + "publicBodyParams": [ + "autosleep", + "image_id", + "memory_mib", + "storage_gib", + "vcpu" + ], + "bodyParamDetails": [ + { + "name": "autosleep", + "required": false + }, + { + "name": "image_id", + "required": false + }, + { + "name": "memory_mib", + "required": false + }, + { + "name": "storage_gib", + "required": false + }, + { + "name": "vcpu", + "required": false + } + ], + "publicPositionalParams": [], + "pathParamDetails": [], + "queryParamDetails": [], + "headerParamDetails": [], + "cookieParams": [], + "publicCookieParams": [], + "cookieParamDetails": [], + "paramsModel": { + "publicName": "MachineCreateParams" + }, + "requestBody": { + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "required": true, + "publicName": "body", + "publicIdentifier": "body" + }, + "response": { + "status": "200", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + "responseModel": { + "name": "machine", + "publicAliases": [] + }, + "responseHeadersModel": { + "name": "CreateResponseHeaders", + "publicAliases": [] + }, + "result": { + "successStatus": "200", + "errorStatuses": [ + "401", + "403", + "409", + "429", + "503", + "default" + ] + }, + "errorResponses": [ + { + "status": "401", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "CreateStatus401ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "403", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "CreateStatus403ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "409", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "CreateStatus409ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "429", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "CreateStatus429ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "503", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "CreateStatus503ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "default", + "contentType": "application/problem+json", + "encoding": "json", + "contents": [ + { + "contentType": "application/problem+json", + "encoding": "json" + } + ], + "model": { + "name": "error_model", + "publicAliases": [] + }, + "headersModel": { + "name": "CreateStatusdefaultResponseHeaders", + "publicAliases": [] + } + } + ], + "responseLinks": [], + "transport": "http" + }, + { + "resource": "machines", + "publicResource": "machines", + "operation": "retrieve", + "publicOperation": "retrieve", + "deprecated": false, + "method": "GET", + "path": "/v1/machines/{machine_id}", + "pathParams": [ + "machine_id" + ], + "publicPathParams": [ + "machine_id" + ], + "queryParams": [], + "publicQueryParams": [], + "headerParams": [], + "publicHeaderParams": [], + "bodyParams": [], + "publicBodyParams": [], + "bodyParamDetails": [], + "publicPositionalParams": [], + "pathParamDetails": [ + { + "name": "machine_id", + "required": true, + "style": "simple", + "explode": false + } + ], + "queryParamDetails": [], + "headerParamDetails": [], + "cookieParams": [], + "publicCookieParams": [], + "cookieParamDetails": [], + "paramsModel": { + "publicName": "MachineRetrieveParams" + }, + "response": { + "status": "200", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + "responseModel": { + "name": "machine_detail_response", + "publicAliases": [] + }, + "responseHeadersModel": { + "name": "RetrieveResponseHeaders", + "publicAliases": [] + }, + "result": { + "successStatus": "200", + "errorStatuses": [ + "401", + "403", + "429", + "503", + "default" + ] + }, + "errorResponses": [ + { + "status": "401", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + { + "status": "403", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + { + "status": "429", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + { + "status": "503", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + { + "status": "default", + "contentType": "application/problem+json", + "encoding": "json", + "contents": [ + { + "contentType": "application/problem+json", + "encoding": "json" + } + ], + "model": { + "name": "error_model", + "publicAliases": [] + } + } + ], + "responseLinks": [], + "transport": "http" + }, + { + "resource": "machines", + "publicResource": "machines", + "operation": "update", + "publicOperation": "update", + "deprecated": false, + "method": "PATCH", + "path": "/v1/machines/{machine_id}", + "pathParams": [ + "machine_id" + ], + "publicPathParams": [ + "machine_id" + ], + "queryParams": [], + "publicQueryParams": [], + "headerParams": [], + "publicHeaderParams": [], + "bodyParams": [ + "autosleep", + "storage_gib" + ], + "publicBodyParams": [ + "autosleep", + "storage_gib" + ], + "bodyParamDetails": [ + { + "name": "autosleep", + "required": false + }, + { + "name": "storage_gib", + "required": false + } + ], + "publicPositionalParams": [], + "pathParamDetails": [ + { + "name": "machine_id", + "required": true, + "style": "simple", + "explode": false + } + ], + "queryParamDetails": [], + "headerParamDetails": [], + "cookieParams": [], + "publicCookieParams": [], + "cookieParamDetails": [], + "paramsModel": { + "publicName": "MachineUpdateParams" + }, + "requestBody": { + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "required": true, + "publicName": "body", + "publicIdentifier": "body" + }, + "response": { + "status": "200", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + "responseModel": { + "name": "machine", + "publicAliases": [] + }, + "responseHeadersModel": { + "name": "UpdateResponseHeaders", + "publicAliases": [] + }, + "result": { + "successStatus": "200", + "errorStatuses": [ + "401", + "403", + "409", + "429", + "503", + "default" + ] + }, + "errorResponses": [ + { + "status": "401", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "UpdateStatus401ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "403", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "UpdateStatus403ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "409", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "UpdateStatus409ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "429", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "UpdateStatus429ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "503", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "UpdateStatus503ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "default", + "contentType": "application/problem+json", + "encoding": "json", + "contents": [ + { + "contentType": "application/problem+json", + "encoding": "json" + } + ], + "model": { + "name": "error_model", + "publicAliases": [] + }, + "headersModel": { + "name": "UpdateStatusdefaultResponseHeaders", + "publicAliases": [] + } + } + ], + "responseLinks": [], + "transport": "http" + }, + { + "resource": "machines", + "publicResource": "machines", + "operation": "delete", + "publicOperation": "delete", + "deprecated": false, + "method": "DELETE", + "path": "/v1/machines/{machine_id}", + "pathParams": [ + "machine_id" + ], + "publicPathParams": [ + "machine_id" + ], + "queryParams": [ + "force" + ], + "publicQueryParams": [ + "force" + ], + "headerParams": [], + "publicHeaderParams": [], + "bodyParams": [], + "publicBodyParams": [], + "bodyParamDetails": [], + "publicPositionalParams": [], + "pathParamDetails": [ + { + "name": "machine_id", + "required": true, + "style": "simple", + "explode": false + } + ], + "queryParamDetails": [ + { + "name": "force", + "required": false, + "style": "form", + "explode": false + } + ], + "headerParamDetails": [], + "cookieParams": [], + "publicCookieParams": [], + "cookieParamDetails": [], + "paramsModel": { + "publicName": "MachineDeleteParams" + }, + "response": { + "status": "200", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + "responseModel": { + "name": "machine", + "publicAliases": [] + }, + "responseHeadersModel": { + "name": "DeleteResponseHeaders", + "publicAliases": [] + }, + "result": { + "successStatus": "200", + "errorStatuses": [ + "401", + "403", + "409", + "429", + "503", + "default" + ] + }, + "errorResponses": [ + { + "status": "401", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "DeleteStatus401ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "403", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "DeleteStatus403ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "409", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "DeleteStatus409ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "429", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "DeleteStatus429ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "503", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "DeleteStatus503ResponseHeaders", + "publicAliases": [] } - ], - "additionalProperties": false - } - }, - { - "name": "CreateTokenResponseHeaders", - "source": "CreateTokenResponseHeaders", - "publicAliases": [], - "deprecated": false, - "type": { - "kind": "object", - "properties": [ - { - "name": "Cache-Control", - "publicName": "Cache-Control", - "required": false, - "deprecated": false, - "type": { - "kind": "primitive", - "type": "string" + }, + { + "status": "default", + "contentType": "application/problem+json", + "encoding": "json", + "contents": [ + { + "contentType": "application/problem+json", + "encoding": "json" } + ], + "model": { + "name": "error_model", + "publicAliases": [] + }, + "headersModel": { + "name": "DeleteStatusdefaultResponseHeaders", + "publicAliases": [] } - ], - "additionalProperties": false - } - } - ], - "resources": [ - "machines", - "machines.ssh", - "machines.executions", - "machines.executions.logs", - "machines.autoresizing", - "organization", - "organization.autoresizing" - ], - "publicResources": [ - "machines", - "machines.ssh", - "machines.executions", - "machines.executions.logs", - "machines.autoresizing", - "organization", - "organization.autoresizing" - ], - "operations": [ + } + ], + "responseLinks": [], + "transport": "http" + }, { "resource": "machines", "publicResource": "machines", - "operation": "list", - "publicOperation": "list", + "operation": "sleep", + "publicOperation": "sleep", "deprecated": false, - "method": "GET", - "path": "/v1/machines", - "pathParams": [], - "publicPathParams": [], - "queryParams": [ - "limit", - "cursor" + "method": "POST", + "path": "/v1/machines/{machine_id}/sleep", + "pathParams": [ + "machine_id" ], - "publicQueryParams": [ - "limit", - "cursor" + "publicPathParams": [ + "machine_id" ], + "queryParams": [], + "publicQueryParams": [], "headerParams": [], "publicHeaderParams": [], "bodyParams": [], "publicBodyParams": [], "bodyParamDetails": [], "publicPositionalParams": [], - "pathParamDetails": [], - "queryParamDetails": [ - { - "name": "limit", - "required": false, - "style": "form", - "explode": false - }, + "pathParamDetails": [ { - "name": "cursor", - "required": false, - "style": "form", + "name": "machine_id", + "required": true, + "style": "simple", "explode": false } ], + "queryParamDetails": [], "headerParamDetails": [], "cookieParams": [], "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "MachineListParams" + "publicName": "MachineSleepParams" }, "response": { "status": "200", @@ -4338,7 +6519,11 @@ ] }, "responseModel": { - "name": "machine_list", + "name": "machine", + "publicAliases": [] + }, + "responseHeadersModel": { + "name": "SleepResponseHeaders", "publicAliases": [] }, "result": { @@ -4346,6 +6531,7 @@ "errorStatuses": [ "401", "403", + "409", "429", "503", "default" @@ -4361,7 +6547,11 @@ "contentType": "application/json", "encoding": "json" } - ] + ], + "headersModel": { + "name": "SleepStatus401ResponseHeaders", + "publicAliases": [] + } }, { "status": "403", @@ -4372,7 +6562,26 @@ "contentType": "application/json", "encoding": "json" } - ] + ], + "headersModel": { + "name": "SleepStatus403ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "409", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "SleepStatus409ResponseHeaders", + "publicAliases": [] + } }, { "status": "429", @@ -4383,7 +6592,11 @@ "contentType": "application/json", "encoding": "json" } - ] + ], + "headersModel": { + "name": "SleepStatus429ResponseHeaders", + "publicAliases": [] + } }, { "status": "503", @@ -4394,7 +6607,11 @@ "contentType": "application/json", "encoding": "json" } - ] + ], + "headersModel": { + "name": "SleepStatus503ResponseHeaders", + "publicAliases": [] + } }, { "status": "default", @@ -4409,96 +6626,53 @@ "model": { "name": "error_model", "publicAliases": [] + }, + "headersModel": { + "name": "SleepStatusdefaultResponseHeaders", + "publicAliases": [] } } ], "responseLinks": [], - "pagination": { - "style": "cursor", - "itemsField": "items", - "cursorField": "next_cursor", - "cursorParam": "cursor", - "cursorFields": [ - "next_cursor" - ], - "cursorParams": [ - "cursor" - ], - "requestLocation": "query", - "responseLocation": "body", - "itemModel": { - "name": "machine_list_item", - "publicAliases": [] - } - }, "transport": "http" }, { "resource": "machines", "publicResource": "machines", - "operation": "create", - "publicOperation": "create", + "operation": "wake", + "publicOperation": "wake", "deprecated": false, "method": "POST", - "path": "/v1/machines", - "pathParams": [], - "publicPathParams": [], + "path": "/v1/machines/{machine_id}/wake", + "pathParams": [ + "machine_id" + ], + "publicPathParams": [ + "machine_id" + ], "queryParams": [], "publicQueryParams": [], "headerParams": [], "publicHeaderParams": [], - "bodyParams": [ - "autosleep", - "memory_mib", - "storage_gib", - "vcpu" - ], - "publicBodyParams": [ - "autosleep", - "memory_mib", - "storage_gib", - "vcpu" - ], - "bodyParamDetails": [ - { - "name": "autosleep", - "required": false - }, - { - "name": "memory_mib", - "required": false - }, - { - "name": "storage_gib", - "required": false - }, + "bodyParams": [], + "publicBodyParams": [], + "bodyParamDetails": [], + "publicPositionalParams": [], + "pathParamDetails": [ { - "name": "vcpu", - "required": false + "name": "machine_id", + "required": true, + "style": "simple", + "explode": false } ], - "publicPositionalParams": [], - "pathParamDetails": [], "queryParamDetails": [], "headerParamDetails": [], "cookieParams": [], "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "MachineCreateParams" - }, - "requestBody": { - "contentType": "application/json", - "encoding": "json", - "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ], - "required": true, - "publicName": "body", - "publicIdentifier": "body" + "publicName": "MachineWakeParams" }, "response": { "status": "200", @@ -4516,7 +6690,7 @@ "publicAliases": [] }, "responseHeadersModel": { - "name": "CreateResponseHeaders", + "name": "WakeResponseHeaders", "publicAliases": [] }, "result": { @@ -4542,7 +6716,7 @@ } ], "headersModel": { - "name": "CreateStatus401ResponseHeaders", + "name": "WakeStatus401ResponseHeaders", "publicAliases": [] } }, @@ -4557,7 +6731,7 @@ } ], "headersModel": { - "name": "CreateStatus403ResponseHeaders", + "name": "WakeStatus403ResponseHeaders", "publicAliases": [] } }, @@ -4572,7 +6746,7 @@ } ], "headersModel": { - "name": "CreateStatus409ResponseHeaders", + "name": "WakeStatus409ResponseHeaders", "publicAliases": [] } }, @@ -4587,7 +6761,7 @@ } ], "headersModel": { - "name": "CreateStatus429ResponseHeaders", + "name": "WakeStatus429ResponseHeaders", "publicAliases": [] } }, @@ -4602,7 +6776,7 @@ } ], "headersModel": { - "name": "CreateStatus503ResponseHeaders", + "name": "WakeStatus503ResponseHeaders", "publicAliases": [] } }, @@ -4621,7 +6795,7 @@ "publicAliases": [] }, "headersModel": { - "name": "CreateStatusdefaultResponseHeaders", + "name": "WakeStatusdefaultResponseHeaders", "publicAliases": [] } } @@ -4632,19 +6806,23 @@ { "resource": "machines", "publicResource": "machines", - "operation": "retrieve", - "publicOperation": "retrieve", + "operation": "reboot", + "publicOperation": "reboot", "deprecated": false, - "method": "GET", - "path": "/v1/machines/{machine_id}", + "method": "POST", + "path": "/v1/machines/{machine_id}/reboot", "pathParams": [ "machine_id" ], "publicPathParams": [ "machine_id" ], - "queryParams": [], - "publicQueryParams": [], + "queryParams": [ + "force" + ], + "publicQueryParams": [ + "force" + ], "headerParams": [], "publicHeaderParams": [], "bodyParams": [], @@ -4659,13 +6837,20 @@ "explode": false } ], - "queryParamDetails": [], + "queryParamDetails": [ + { + "name": "force", + "required": false, + "style": "form", + "explode": false + } + ], "headerParamDetails": [], "cookieParams": [], "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "MachineRetrieveParams" + "publicName": "MachineRebootParams" }, "response": { "status": "200", @@ -4679,11 +6864,11 @@ ] }, "responseModel": { - "name": "machine_detail_response", + "name": "machine", "publicAliases": [] }, "responseHeadersModel": { - "name": "RetrieveResponseHeaders", + "name": "RebootResponseHeaders", "publicAliases": [] }, "result": { @@ -4691,6 +6876,7 @@ "errorStatuses": [ "401", "403", + "409", "429", "503", "default" @@ -4706,7 +6892,11 @@ "contentType": "application/json", "encoding": "json" } - ] + ], + "headersModel": { + "name": "RebootStatus401ResponseHeaders", + "publicAliases": [] + } }, { "status": "403", @@ -4717,7 +6907,26 @@ "contentType": "application/json", "encoding": "json" } - ] + ], + "headersModel": { + "name": "RebootStatus403ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "409", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "RebootStatus409ResponseHeaders", + "publicAliases": [] + } }, { "status": "429", @@ -4728,7 +6937,11 @@ "contentType": "application/json", "encoding": "json" } - ] + ], + "headersModel": { + "name": "RebootStatus429ResponseHeaders", + "publicAliases": [] + } }, { "status": "503", @@ -4739,7 +6952,11 @@ "contentType": "application/json", "encoding": "json" } - ] + ], + "headersModel": { + "name": "RebootStatus503ResponseHeaders", + "publicAliases": [] + } }, { "status": "default", @@ -4754,6 +6971,10 @@ "model": { "name": "error_model", "publicAliases": [] + }, + "headersModel": { + "name": "RebootStatusdefaultResponseHeaders", + "publicAliases": [] } } ], @@ -4763,11 +6984,11 @@ { "resource": "machines", "publicResource": "machines", - "operation": "update", - "publicOperation": "update", + "operation": "resize", + "publicOperation": "resize", "deprecated": false, - "method": "PATCH", - "path": "/v1/machines/{machine_id}", + "method": "POST", + "path": "/v1/machines/{machine_id}/resize", "pathParams": [ "machine_id" ], @@ -4779,30 +7000,18 @@ "headerParams": [], "publicHeaderParams": [], "bodyParams": [ - "autosleep", "memory_mib", - "storage_gib", "vcpu" ], "publicBodyParams": [ - "autosleep", "memory_mib", - "storage_gib", "vcpu" ], "bodyParamDetails": [ - { - "name": "autosleep", - "required": false - }, { "name": "memory_mib", "required": false }, - { - "name": "storage_gib", - "required": false - }, { "name": "vcpu", "required": false @@ -4823,7 +7032,7 @@ "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "MachineUpdateParams" + "publicName": "MachineResizeParams" }, "requestBody": { "contentType": "application/json", @@ -4850,25 +7059,46 @@ ] }, "responseModel": { - "name": "machine", + "name": "machine_detail_response", "publicAliases": [] }, "responseHeadersModel": { - "name": "UpdateResponseHeaders", + "name": "ResizeResponseHeaders", "publicAliases": [] }, "result": { "successStatus": "200", "errorStatuses": [ + "400", "401", + "402", "403", + "404", "409", + "422", "429", + "500", "503", + "504", "default" ] }, "errorResponses": [ + { + "status": "400", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "ResizeStatus400ResponseHeaders", + "publicAliases": [] + } + }, { "status": "401", "contentType": "application/json", @@ -4880,7 +7110,22 @@ } ], "headersModel": { - "name": "UpdateStatus401ResponseHeaders", + "name": "ResizeStatus401ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "402", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "ResizeStatus402ResponseHeaders", "publicAliases": [] } }, @@ -4895,7 +7140,22 @@ } ], "headersModel": { - "name": "UpdateStatus403ResponseHeaders", + "name": "ResizeStatus403ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "404", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "ResizeStatus404ResponseHeaders", "publicAliases": [] } }, @@ -4910,7 +7170,22 @@ } ], "headersModel": { - "name": "UpdateStatus409ResponseHeaders", + "name": "ResizeStatus409ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "422", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "ResizeStatus422ResponseHeaders", "publicAliases": [] } }, @@ -4925,7 +7200,22 @@ } ], "headersModel": { - "name": "UpdateStatus429ResponseHeaders", + "name": "ResizeStatus429ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "500", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "ResizeStatus500ResponseHeaders", "publicAliases": [] } }, @@ -4940,26 +7230,41 @@ } ], "headersModel": { - "name": "UpdateStatus503ResponseHeaders", + "name": "ResizeStatus503ResponseHeaders", + "publicAliases": [] + } + }, + { + "status": "504", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "headersModel": { + "name": "ResizeStatus504ResponseHeaders", "publicAliases": [] } }, { "status": "default", - "contentType": "application/problem+json", + "contentType": "application/json", "encoding": "json", "contents": [ { - "contentType": "application/problem+json", + "contentType": "application/json", "encoding": "json" } ], "model": { - "name": "error_model", + "name": "error", "publicAliases": [] }, "headersModel": { - "name": "UpdateStatusdefaultResponseHeaders", + "name": "ResizeStatusdefaultResponseHeaders", "publicAliases": [] } } @@ -4968,21 +7273,27 @@ "transport": "http" }, { - "resource": "machines", - "publicResource": "machines", - "operation": "delete", - "publicOperation": "delete", + "resource": "machines.ssh", + "publicResource": "machines.ssh", + "operation": "list", + "publicOperation": "list", "deprecated": false, - "method": "DELETE", - "path": "/v1/machines/{machine_id}", + "method": "GET", + "path": "/v1/machines/{machine_id}/ssh", "pathParams": [ "machine_id" ], "publicPathParams": [ "machine_id" ], - "queryParams": [], - "publicQueryParams": [], + "queryParams": [ + "limit", + "cursor" + ], + "publicQueryParams": [ + "limit", + "cursor" + ], "headerParams": [], "publicHeaderParams": [], "bodyParams": [], @@ -4997,13 +7308,26 @@ "explode": false } ], - "queryParamDetails": [], + "queryParamDetails": [ + { + "name": "limit", + "required": false, + "style": "form", + "explode": false + }, + { + "name": "cursor", + "required": false, + "style": "form", + "explode": false + } + ], "headerParamDetails": [], "cookieParams": [], "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "MachineDeleteParams" + "publicName": "SshListParams" }, "response": { "status": "200", @@ -5017,11 +7341,7 @@ ] }, "responseModel": { - "name": "machine", - "publicAliases": [] - }, - "responseHeadersModel": { - "name": "DeleteResponseHeaders", + "name": "ssh_session_list", "publicAliases": [] }, "result": { @@ -5029,7 +7349,6 @@ "errorStatuses": [ "401", "403", - "409", "429", "503", "default" @@ -5045,29 +7364,10 @@ "contentType": "application/json", "encoding": "json" } - ], - "headersModel": { - "name": "DeleteStatus401ResponseHeaders", - "publicAliases": [] - } - }, - { - "status": "403", - "contentType": "application/json", - "encoding": "json", - "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ], - "headersModel": { - "name": "DeleteStatus403ResponseHeaders", - "publicAliases": [] - } + ] }, { - "status": "409", + "status": "403", "contentType": "application/json", "encoding": "json", "contents": [ @@ -5075,11 +7375,7 @@ "contentType": "application/json", "encoding": "json" } - ], - "headersModel": { - "name": "DeleteStatus409ResponseHeaders", - "publicAliases": [] - } + ] }, { "status": "429", @@ -5090,11 +7386,7 @@ "contentType": "application/json", "encoding": "json" } - ], - "headersModel": { - "name": "DeleteStatus429ResponseHeaders", - "publicAliases": [] - } + ] }, { "status": "503", @@ -5105,11 +7397,7 @@ "contentType": "application/json", "encoding": "json" } - ], - "headersModel": { - "name": "DeleteStatus503ResponseHeaders", - "publicAliases": [] - } + ] }, { "status": "default", @@ -5124,24 +7412,38 @@ "model": { "name": "error_model", "publicAliases": [] - }, - "headersModel": { - "name": "DeleteStatusdefaultResponseHeaders", - "publicAliases": [] } } ], "responseLinks": [], + "pagination": { + "style": "cursor", + "itemsField": "items", + "cursorField": "next_cursor", + "cursorParam": "cursor", + "cursorFields": [ + "next_cursor" + ], + "cursorParams": [ + "cursor" + ], + "requestLocation": "query", + "responseLocation": "body", + "itemModel": { + "name": "ssh_session", + "publicAliases": [] + } + }, "transport": "http" }, { - "resource": "machines", - "publicResource": "machines", - "operation": "sleep", - "publicOperation": "sleep", + "resource": "machines.ssh", + "publicResource": "machines.ssh", + "operation": "create", + "publicOperation": "create", "deprecated": false, "method": "POST", - "path": "/v1/machines/{machine_id}/sleep", + "path": "/v1/machines/{machine_id}/ssh", "pathParams": [ "machine_id" ], @@ -5152,9 +7454,18 @@ "publicQueryParams": [], "headerParams": [], "publicHeaderParams": [], - "bodyParams": [], - "publicBodyParams": [], - "bodyParamDetails": [], + "bodyParams": [ + "public_key" + ], + "publicBodyParams": [ + "public_key" + ], + "bodyParamDetails": [ + { + "name": "public_key", + "required": true + } + ], "publicPositionalParams": [], "pathParamDetails": [ { @@ -5170,7 +7481,20 @@ "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "MachineSleepParams" + "publicName": "SshCreateParams" + }, + "requestBody": { + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "required": true, + "publicName": "body", + "publicIdentifier": "body" }, "response": { "status": "200", @@ -5184,11 +7508,11 @@ ] }, "responseModel": { - "name": "machine", + "name": "ssh_session", "publicAliases": [] }, "responseHeadersModel": { - "name": "SleepResponseHeaders", + "name": "CreateResponseHeaders", "publicAliases": [] }, "result": { @@ -5214,7 +7538,7 @@ } ], "headersModel": { - "name": "SleepStatus401ResponseHeaders", + "name": "CreateStatus401ResponseHeaders", "publicAliases": [] } }, @@ -5229,7 +7553,7 @@ } ], "headersModel": { - "name": "SleepStatus403ResponseHeaders", + "name": "CreateStatus403ResponseHeaders", "publicAliases": [] } }, @@ -5244,7 +7568,7 @@ } ], "headersModel": { - "name": "SleepStatus409ResponseHeaders", + "name": "CreateStatus409ResponseHeaders", "publicAliases": [] } }, @@ -5259,7 +7583,7 @@ } ], "headersModel": { - "name": "SleepStatus429ResponseHeaders", + "name": "CreateStatus429ResponseHeaders", "publicAliases": [] } }, @@ -5274,7 +7598,7 @@ } ], "headersModel": { - "name": "SleepStatus503ResponseHeaders", + "name": "CreateStatus503ResponseHeaders", "publicAliases": [] } }, @@ -5293,7 +7617,7 @@ "publicAliases": [] }, "headersModel": { - "name": "SleepStatusdefaultResponseHeaders", + "name": "CreateStatusdefaultResponseHeaders", "publicAliases": [] } } @@ -5302,18 +7626,20 @@ "transport": "http" }, { - "resource": "machines", - "publicResource": "machines", - "operation": "wake", - "publicOperation": "wake", + "resource": "machines.ssh", + "publicResource": "machines.ssh", + "operation": "retrieve", + "publicOperation": "retrieve", "deprecated": false, - "method": "POST", - "path": "/v1/machines/{machine_id}/wake", + "method": "GET", + "path": "/v1/machines/{machine_id}/ssh/{session_id}", "pathParams": [ - "machine_id" + "machine_id", + "session_id" ], "publicPathParams": [ - "machine_id" + "machine_id", + "session_id" ], "queryParams": [], "publicQueryParams": [], @@ -5329,6 +7655,12 @@ "required": true, "style": "simple", "explode": false + }, + { + "name": "session_id", + "required": true, + "style": "simple", + "explode": false } ], "queryParamDetails": [], @@ -5337,7 +7669,7 @@ "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "MachineWakeParams" + "publicName": "SshRetrieveParams" }, "response": { "status": "200", @@ -5351,11 +7683,7 @@ ] }, "responseModel": { - "name": "machine", - "publicAliases": [] - }, - "responseHeadersModel": { - "name": "WakeResponseHeaders", + "name": "ssh_session", "publicAliases": [] }, "result": { @@ -5363,7 +7691,6 @@ "errorStatuses": [ "401", "403", - "409", "429", "503", "default" @@ -5379,11 +7706,7 @@ "contentType": "application/json", "encoding": "json" } - ], - "headersModel": { - "name": "WakeStatus401ResponseHeaders", - "publicAliases": [] - } + ] }, { "status": "403", @@ -5394,26 +7717,7 @@ "contentType": "application/json", "encoding": "json" } - ], - "headersModel": { - "name": "WakeStatus403ResponseHeaders", - "publicAliases": [] - } - }, - { - "status": "409", - "contentType": "application/json", - "encoding": "json", - "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ], - "headersModel": { - "name": "WakeStatus409ResponseHeaders", - "publicAliases": [] - } + ] }, { "status": "429", @@ -5424,11 +7728,7 @@ "contentType": "application/json", "encoding": "json" } - ], - "headersModel": { - "name": "WakeStatus429ResponseHeaders", - "publicAliases": [] - } + ] }, { "status": "503", @@ -5439,11 +7739,7 @@ "contentType": "application/json", "encoding": "json" } - ], - "headersModel": { - "name": "WakeStatus503ResponseHeaders", - "publicAliases": [] - } + ] }, { "status": "default", @@ -5458,10 +7754,6 @@ "model": { "name": "error_model", "publicAliases": [] - }, - "headersModel": { - "name": "WakeStatusdefaultResponseHeaders", - "publicAliases": [] } } ], @@ -5469,25 +7761,23 @@ "transport": "http" }, { - "resource": "machines", - "publicResource": "machines", - "operation": "reboot", - "publicOperation": "reboot", + "resource": "machines.ssh", + "publicResource": "machines.ssh", + "operation": "delete", + "publicOperation": "delete", "deprecated": false, - "method": "POST", - "path": "/v1/machines/{machine_id}/reboot", + "method": "DELETE", + "path": "/v1/machines/{machine_id}/ssh/{session_id}", "pathParams": [ - "machine_id" + "machine_id", + "session_id" ], "publicPathParams": [ - "machine_id" - ], - "queryParams": [ - "force" - ], - "publicQueryParams": [ - "force" + "machine_id", + "session_id" ], + "queryParams": [], + "publicQueryParams": [], "headerParams": [], "publicHeaderParams": [], "bodyParams": [], @@ -5500,22 +7790,21 @@ "required": true, "style": "simple", "explode": false - } - ], - "queryParamDetails": [ + }, { - "name": "force", - "required": false, - "style": "form", + "name": "session_id", + "required": true, + "style": "simple", "explode": false } ], + "queryParamDetails": [], "headerParamDetails": [], "cookieParams": [], "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "MachineRebootParams" + "publicName": "SshDeleteParams" }, "response": { "status": "200", @@ -5529,11 +7818,7 @@ ] }, "responseModel": { - "name": "machine", - "publicAliases": [] - }, - "responseHeadersModel": { - "name": "RebootResponseHeaders", + "name": "ssh_session", "publicAliases": [] }, "result": { @@ -5541,7 +7826,6 @@ "errorStatuses": [ "401", "403", - "409", "429", "503", "default" @@ -5557,11 +7841,7 @@ "contentType": "application/json", "encoding": "json" } - ], - "headersModel": { - "name": "RebootStatus401ResponseHeaders", - "publicAliases": [] - } + ] }, { "status": "403", @@ -5572,41 +7852,18 @@ "contentType": "application/json", "encoding": "json" } - ], - "headersModel": { - "name": "RebootStatus403ResponseHeaders", - "publicAliases": [] - } - }, - { - "status": "409", - "contentType": "application/json", - "encoding": "json", - "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ], - "headersModel": { - "name": "RebootStatus409ResponseHeaders", - "publicAliases": [] - } + ] }, { "status": "429", "contentType": "application/json", "encoding": "json", "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ], - "headersModel": { - "name": "RebootStatus429ResponseHeaders", - "publicAliases": [] - } + { + "contentType": "application/json", + "encoding": "json" + } + ] }, { "status": "503", @@ -5617,11 +7874,7 @@ "contentType": "application/json", "encoding": "json" } - ], - "headersModel": { - "name": "RebootStatus503ResponseHeaders", - "publicAliases": [] - } + ] }, { "status": "default", @@ -5636,10 +7889,6 @@ "model": { "name": "error_model", "publicAliases": [] - }, - "headersModel": { - "name": "RebootStatusdefaultResponseHeaders", - "publicAliases": [] } } ], @@ -5647,13 +7896,13 @@ "transport": "http" }, { - "resource": "machines.ssh", - "publicResource": "machines.ssh", + "resource": "machines.executions", + "publicResource": "machines.executions", "operation": "list", "publicOperation": "list", "deprecated": false, "method": "GET", - "path": "/v1/machines/{machine_id}/ssh", + "path": "/v1/machines/{machine_id}/executions", "pathParams": [ "machine_id" ], @@ -5701,7 +7950,7 @@ "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "SshListParams" + "publicName": "ExecutionListParams" }, "response": { "status": "200", @@ -5715,7 +7964,7 @@ ] }, "responseModel": { - "name": "ssh_session_list", + "name": "execution_list", "publicAliases": [] }, "result": { @@ -5804,20 +8053,20 @@ "requestLocation": "query", "responseLocation": "body", "itemModel": { - "name": "ssh_session", + "name": "execution", "publicAliases": [] } }, "transport": "http" }, { - "resource": "machines.ssh", - "publicResource": "machines.ssh", + "resource": "machines.executions", + "publicResource": "machines.executions", "operation": "create", "publicOperation": "create", "deprecated": false, "method": "POST", - "path": "/v1/machines/{machine_id}/ssh", + "path": "/v1/machines/{machine_id}/executions", "pathParams": [ "machine_id" ], @@ -5829,15 +8078,39 @@ "headerParams": [], "publicHeaderParams": [], "bodyParams": [ - "public_key" + "command", + "cwd", + "env", + "stdin", + "timeout_ms" ], "publicBodyParams": [ - "public_key" + "command", + "cwd", + "env", + "stdin", + "timeout_ms" ], "bodyParamDetails": [ { - "name": "public_key", + "name": "command", "required": true + }, + { + "name": "cwd", + "required": false + }, + { + "name": "env", + "required": false + }, + { + "name": "stdin", + "required": false + }, + { + "name": "timeout_ms", + "required": false } ], "publicPositionalParams": [], @@ -5855,7 +8128,7 @@ "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "SshCreateParams" + "publicName": "ExecutionCreateParams" }, "requestBody": { "contentType": "application/json", @@ -5882,7 +8155,7 @@ ] }, "responseModel": { - "name": "ssh_session", + "name": "execution", "publicAliases": [] }, "responseHeadersModel": { @@ -6000,20 +8273,20 @@ "transport": "http" }, { - "resource": "machines.ssh", - "publicResource": "machines.ssh", + "resource": "machines.executions", + "publicResource": "machines.executions", "operation": "retrieve", "publicOperation": "retrieve", "deprecated": false, "method": "GET", - "path": "/v1/machines/{machine_id}/ssh/{session_id}", + "path": "/v1/machines/{machine_id}/executions/{execution_id}", "pathParams": [ "machine_id", - "session_id" + "execution_id" ], "publicPathParams": [ "machine_id", - "session_id" + "execution_id" ], "queryParams": [], "publicQueryParams": [], @@ -6031,7 +8304,7 @@ "explode": false }, { - "name": "session_id", + "name": "execution_id", "required": true, "style": "simple", "explode": false @@ -6043,7 +8316,7 @@ "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "SshRetrieveParams" + "publicName": "ExecutionRetrieveParams" }, "response": { "status": "200", @@ -6057,7 +8330,7 @@ ] }, "responseModel": { - "name": "ssh_session", + "name": "execution", "publicAliases": [] }, "result": { @@ -6135,20 +8408,20 @@ "transport": "http" }, { - "resource": "machines.ssh", - "publicResource": "machines.ssh", + "resource": "machines.executions", + "publicResource": "machines.executions", "operation": "delete", "publicOperation": "delete", "deprecated": false, "method": "DELETE", - "path": "/v1/machines/{machine_id}/ssh/{session_id}", + "path": "/v1/machines/{machine_id}/executions/{execution_id}", "pathParams": [ "machine_id", - "session_id" + "execution_id" ], "publicPathParams": [ "machine_id", - "session_id" + "execution_id" ], "queryParams": [], "publicQueryParams": [], @@ -6166,7 +8439,7 @@ "explode": false }, { - "name": "session_id", + "name": "execution_id", "required": true, "style": "simple", "explode": false @@ -6178,7 +8451,7 @@ "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "SshDeleteParams" + "publicName": "ExecutionDeleteParams" }, "response": { "status": "200", @@ -6192,7 +8465,7 @@ ] }, "responseModel": { - "name": "ssh_session", + "name": "execution", "publicAliases": [] }, "result": { @@ -6272,16 +8545,153 @@ { "resource": "machines.executions", "publicResource": "machines.executions", - "operation": "list", - "publicOperation": "list", + "operation": "output", + "publicOperation": "output", "deprecated": false, "method": "GET", - "path": "/v1/machines/{machine_id}/executions", + "path": "/v1/machines/{machine_id}/executions/{execution_id}/output", "pathParams": [ - "machine_id" + "machine_id", + "execution_id" ], "publicPathParams": [ - "machine_id" + "machine_id", + "execution_id" + ], + "queryParams": [], + "publicQueryParams": [], + "headerParams": [], + "publicHeaderParams": [], + "bodyParams": [], + "publicBodyParams": [], + "bodyParamDetails": [], + "publicPositionalParams": [], + "pathParamDetails": [ + { + "name": "machine_id", + "required": true, + "style": "simple", + "explode": false + }, + { + "name": "execution_id", + "required": true, + "style": "simple", + "explode": false + } + ], + "queryParamDetails": [], + "headerParamDetails": [], + "cookieParams": [], + "publicCookieParams": [], + "cookieParamDetails": [], + "paramsModel": { + "publicName": "ExecutionOutputParams" + }, + "response": { + "status": "200", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + "responseModel": { + "name": "execution_output", + "publicAliases": [] + }, + "result": { + "successStatus": "200", + "errorStatuses": [ + "401", + "403", + "429", + "503", + "default" + ] + }, + "errorResponses": [ + { + "status": "401", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + { + "status": "403", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + { + "status": "429", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + { + "status": "503", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + { + "status": "default", + "contentType": "application/problem+json", + "encoding": "json", + "contents": [ + { + "contentType": "application/problem+json", + "encoding": "json" + } + ], + "model": { + "name": "error_model", + "publicAliases": [] + } + } + ], + "responseLinks": [], + "transport": "http" + }, + { + "resource": "machines.executions", + "publicResource": "machines.executions", + "operation": "events", + "publicOperation": "events", + "deprecated": false, + "method": "GET", + "path": "/v1/machines/{machine_id}/executions/{execution_id}/events", + "pathParams": [ + "machine_id", + "execution_id" + ], + "publicPathParams": [ + "machine_id", + "execution_id" ], "queryParams": [ "limit", @@ -6303,6 +8713,12 @@ "required": true, "style": "simple", "explode": false + }, + { + "name": "execution_id", + "required": true, + "style": "simple", + "explode": false } ], "queryParamDetails": [ @@ -6324,7 +8740,7 @@ "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "ExecutionListParams" + "publicName": "ExecutionEventsParams" }, "response": { "status": "200", @@ -6338,7 +8754,7 @@ ] }, "responseModel": { - "name": "execution_list", + "name": "execution_events", "publicAliases": [] }, "result": { @@ -6427,66 +8843,35 @@ "requestLocation": "query", "responseLocation": "body", "itemModel": { - "name": "execution", + "name": "execution_event", "publicAliases": [] } }, "transport": "http" }, { - "resource": "machines.executions", - "publicResource": "machines.executions", - "operation": "create", - "publicOperation": "create", + "resource": "machines.executions.logs", + "publicResource": "machines.executions.logs", + "operation": "retrieve", + "publicOperation": "retrieve", "deprecated": false, - "method": "POST", - "path": "/v1/machines/{machine_id}/executions", + "method": "GET", + "path": "/v1/machines/{machine_id}/executions/{execution_id}/logs", "pathParams": [ - "machine_id" + "machine_id", + "execution_id" ], "publicPathParams": [ - "machine_id" + "machine_id", + "execution_id" ], "queryParams": [], "publicQueryParams": [], "headerParams": [], "publicHeaderParams": [], - "bodyParams": [ - "command", - "cwd", - "env", - "stdin", - "timeout_ms" - ], - "publicBodyParams": [ - "command", - "cwd", - "env", - "stdin", - "timeout_ms" - ], - "bodyParamDetails": [ - { - "name": "command", - "required": true - }, - { - "name": "cwd", - "required": false - }, - { - "name": "env", - "required": false - }, - { - "name": "stdin", - "required": false - }, - { - "name": "timeout_ms", - "required": false - } - ], + "bodyParams": [], + "publicBodyParams": [], + "bodyParamDetails": [], "publicPositionalParams": [], "pathParamDetails": [ { @@ -6494,6 +8879,12 @@ "required": true, "style": "simple", "explode": false + }, + { + "name": "execution_id", + "required": true, + "style": "simple", + "explode": false } ], "queryParamDetails": [], @@ -6502,20 +8893,7 @@ "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "ExecutionCreateParams" - }, - "requestBody": { - "contentType": "application/json", - "encoding": "json", - "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ], - "required": true, - "publicName": "body", - "publicIdentifier": "body" + "publicName": "LogRetrieveParams" }, "response": { "status": "200", @@ -6529,11 +8907,11 @@ ] }, "responseModel": { - "name": "execution", + "name": "status", "publicAliases": [] }, "responseHeadersModel": { - "name": "CreateResponseHeaders", + "name": "RetrieveResponseHeaders", "publicAliases": [] }, "result": { @@ -6541,9 +8919,6 @@ "errorStatuses": [ "401", "403", - "409", - "429", - "503", "default" ] }, @@ -6557,11 +8932,7 @@ "contentType": "application/json", "encoding": "json" } - ], - "headersModel": { - "name": "CreateStatus401ResponseHeaders", - "publicAliases": [] - } + ] }, { "status": "403", @@ -6572,56 +8943,7 @@ "contentType": "application/json", "encoding": "json" } - ], - "headersModel": { - "name": "CreateStatus403ResponseHeaders", - "publicAliases": [] - } - }, - { - "status": "409", - "contentType": "application/json", - "encoding": "json", - "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ], - "headersModel": { - "name": "CreateStatus409ResponseHeaders", - "publicAliases": [] - } - }, - { - "status": "429", - "contentType": "application/json", - "encoding": "json", - "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ], - "headersModel": { - "name": "CreateStatus429ResponseHeaders", - "publicAliases": [] - } - }, - { - "status": "503", - "contentType": "application/json", - "encoding": "json", - "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ], - "headersModel": { - "name": "CreateStatus503ResponseHeaders", - "publicAliases": [] - } + ] }, { "status": "default", @@ -6636,10 +8958,6 @@ "model": { "name": "error_model", "publicAliases": [] - }, - "headersModel": { - "name": "CreateStatusdefaultResponseHeaders", - "publicAliases": [] } } ], @@ -6647,13 +8965,13 @@ "transport": "http" }, { - "resource": "machines.executions", - "publicResource": "machines.executions", - "operation": "retrieve", - "publicOperation": "retrieve", + "resource": "machines.executions.logs", + "publicResource": "machines.executions.logs", + "operation": "createToken", + "publicOperation": "createToken", "deprecated": false, - "method": "GET", - "path": "/v1/machines/{machine_id}/executions/{execution_id}", + "method": "POST", + "path": "/v1/machines/{machine_id}/executions/{execution_id}/logs/token", "pathParams": [ "machine_id", "execution_id" @@ -6690,7 +9008,7 @@ "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "ExecutionRetrieveParams" + "publicName": "LogCreateTokenParams" }, "response": { "status": "200", @@ -6704,7 +9022,11 @@ ] }, "responseModel": { - "name": "execution", + "name": "read_token", + "publicAliases": [] + }, + "responseHeadersModel": { + "name": "CreateTokenResponseHeaders", "publicAliases": [] }, "result": { @@ -6712,8 +9034,6 @@ "errorStatuses": [ "401", "403", - "429", - "503", "default" ] }, @@ -6740,28 +9060,6 @@ } ] }, - { - "status": "429", - "contentType": "application/json", - "encoding": "json", - "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ] - }, - { - "status": "503", - "contentType": "application/json", - "encoding": "json", - "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ] - }, { "status": "default", "contentType": "application/problem+json", @@ -6782,20 +9080,18 @@ "transport": "http" }, { - "resource": "machines.executions", - "publicResource": "machines.executions", - "operation": "delete", - "publicOperation": "delete", + "resource": "machines.autoresizing", + "publicResource": "machines.autoresizing", + "operation": "retrieve", + "publicOperation": "retrieve", "deprecated": false, - "method": "DELETE", - "path": "/v1/machines/{machine_id}/executions/{execution_id}", + "method": "GET", + "path": "/v1/machines/{machine_id}/autoresizing", "pathParams": [ - "machine_id", - "execution_id" + "machine_id" ], "publicPathParams": [ - "machine_id", - "execution_id" + "machine_id" ], "queryParams": [], "publicQueryParams": [], @@ -6811,12 +9107,6 @@ "required": true, "style": "simple", "explode": false - }, - { - "name": "execution_id", - "required": true, - "style": "simple", - "explode": false } ], "queryParamDetails": [], @@ -6825,7 +9115,7 @@ "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "ExecutionDeleteParams" + "publicName": "AutoresizingRetrieveParams" }, "response": { "status": "200", @@ -6837,46 +9127,22 @@ "encoding": "json" } ] - }, - "responseModel": { - "name": "execution", - "publicAliases": [] - }, - "result": { - "successStatus": "200", - "errorStatuses": [ - "401", - "403", - "429", - "503", - "default" - ] - }, - "errorResponses": [ - { - "status": "401", - "contentType": "application/json", - "encoding": "json", - "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ] - }, - { - "status": "403", - "contentType": "application/json", - "encoding": "json", - "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ] - }, + }, + "responseModel": { + "name": "policy", + "publicAliases": [] + }, + "result": { + "successStatus": "200", + "errorStatuses": [ + "401", + "403", + "default" + ] + }, + "errorResponses": [ { - "status": "429", + "status": "401", "contentType": "application/json", "encoding": "json", "contents": [ @@ -6887,7 +9153,7 @@ ] }, { - "status": "503", + "status": "403", "contentType": "application/json", "encoding": "json", "contents": [ @@ -6917,28 +9183,53 @@ "transport": "http" }, { - "resource": "machines.executions", - "publicResource": "machines.executions", - "operation": "output", - "publicOperation": "output", + "resource": "machines.autoresizing", + "publicResource": "machines.autoresizing", + "operation": "update", + "publicOperation": "update", "deprecated": false, - "method": "GET", - "path": "/v1/machines/{machine_id}/executions/{execution_id}/output", + "method": "PATCH", + "path": "/v1/machines/{machine_id}/autoresizing", "pathParams": [ - "machine_id", - "execution_id" + "machine_id" ], "publicPathParams": [ - "machine_id", - "execution_id" + "machine_id" ], "queryParams": [], "publicQueryParams": [], "headerParams": [], "publicHeaderParams": [], - "bodyParams": [], - "publicBodyParams": [], - "bodyParamDetails": [], + "bodyParams": [ + "cpu", + "cpu_max_vcpu", + "memory", + "memory_max_mib" + ], + "publicBodyParams": [ + "cpu", + "cpu_max_vcpu", + "memory", + "memory_max_mib" + ], + "bodyParamDetails": [ + { + "name": "cpu", + "required": false + }, + { + "name": "cpu_max_vcpu", + "required": false + }, + { + "name": "memory", + "required": false + }, + { + "name": "memory_max_mib", + "required": false + } + ], "publicPositionalParams": [], "pathParamDetails": [ { @@ -6946,12 +9237,6 @@ "required": true, "style": "simple", "explode": false - }, - { - "name": "execution_id", - "required": true, - "style": "simple", - "explode": false } ], "queryParamDetails": [], @@ -6960,7 +9245,20 @@ "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "ExecutionOutputParams" + "publicName": "AutoresizingUpdateParams" + }, + "requestBody": { + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "required": true, + "publicName": "body", + "publicIdentifier": "body" }, "response": { "status": "200", @@ -6974,7 +9272,7 @@ ] }, "responseModel": { - "name": "execution_output", + "name": "policy", "publicAliases": [] }, "result": { @@ -6982,8 +9280,6 @@ "errorStatuses": [ "401", "403", - "429", - "503", "default" ] }, @@ -7010,28 +9306,6 @@ } ] }, - { - "status": "429", - "contentType": "application/json", - "encoding": "json", - "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ] - }, - { - "status": "503", - "contentType": "application/json", - "encoding": "json", - "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ] - }, { "status": "default", "contentType": "application/problem+json", @@ -7052,70 +9326,29 @@ "transport": "http" }, { - "resource": "machines.executions", - "publicResource": "machines.executions", - "operation": "events", - "publicOperation": "events", + "resource": "organization.autoresizing", + "publicResource": "organization.autoresizing", + "operation": "retrieve", + "publicOperation": "retrieve", "deprecated": false, "method": "GET", - "path": "/v1/machines/{machine_id}/executions/{execution_id}/events", - "pathParams": [ - "machine_id", - "execution_id" - ], - "publicPathParams": [ - "machine_id", - "execution_id" - ], - "queryParams": [ - "limit", - "cursor" - ], - "publicQueryParams": [ - "limit", - "cursor" - ], + "path": "/v1/organization/autoresizing", + "pathParams": [], + "publicPathParams": [], + "queryParams": [], + "publicQueryParams": [], "headerParams": [], "publicHeaderParams": [], "bodyParams": [], "publicBodyParams": [], "bodyParamDetails": [], "publicPositionalParams": [], - "pathParamDetails": [ - { - "name": "machine_id", - "required": true, - "style": "simple", - "explode": false - }, - { - "name": "execution_id", - "required": true, - "style": "simple", - "explode": false - } - ], - "queryParamDetails": [ - { - "name": "limit", - "required": false, - "style": "form", - "explode": false - }, - { - "name": "cursor", - "required": false, - "style": "form", - "explode": false - } - ], + "pathParamDetails": [], + "queryParamDetails": [], "headerParamDetails": [], "cookieParams": [], "publicCookieParams": [], "cookieParamDetails": [], - "paramsModel": { - "publicName": "ExecutionEventsParams" - }, "response": { "status": "200", "contentType": "application/json", @@ -7128,7 +9361,7 @@ ] }, "responseModel": { - "name": "execution_events", + "name": "policy", "publicAliases": [] }, "result": { @@ -7136,8 +9369,6 @@ "errorStatuses": [ "401", "403", - "429", - "503", "default" ] }, @@ -7164,28 +9395,6 @@ } ] }, - { - "status": "429", - "contentType": "application/json", - "encoding": "json", - "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ] - }, - { - "status": "503", - "contentType": "application/json", - "encoding": "json", - "contents": [ - { - "contentType": "application/json", - "encoding": "json" - } - ] - }, { "status": "default", "contentType": "application/problem+json", @@ -7203,71 +9412,74 @@ } ], "responseLinks": [], - "pagination": { - "style": "cursor", - "itemsField": "items", - "cursorField": "next_cursor", - "cursorParam": "cursor", - "cursorFields": [ - "next_cursor" - ], - "cursorParams": [ - "cursor" - ], - "requestLocation": "query", - "responseLocation": "body", - "itemModel": { - "name": "execution_event", - "publicAliases": [] - } - }, "transport": "http" }, { - "resource": "machines.executions.logs", - "publicResource": "machines.executions.logs", - "operation": "retrieve", - "publicOperation": "retrieve", + "resource": "organization.autoresizing", + "publicResource": "organization.autoresizing", + "operation": "update", + "publicOperation": "update", "deprecated": false, - "method": "GET", - "path": "/v1/machines/{machine_id}/executions/{execution_id}/logs", - "pathParams": [ - "machine_id", - "execution_id" - ], - "publicPathParams": [ - "machine_id", - "execution_id" - ], + "method": "PATCH", + "path": "/v1/organization/autoresizing", + "pathParams": [], + "publicPathParams": [], "queryParams": [], "publicQueryParams": [], "headerParams": [], "publicHeaderParams": [], - "bodyParams": [], - "publicBodyParams": [], - "bodyParamDetails": [], - "publicPositionalParams": [], - "pathParamDetails": [ + "bodyParams": [ + "cpu", + "cpu_max_vcpu", + "memory", + "memory_max_mib" + ], + "publicBodyParams": [ + "cpu", + "cpu_max_vcpu", + "memory", + "memory_max_mib" + ], + "bodyParamDetails": [ { - "name": "machine_id", - "required": true, - "style": "simple", - "explode": false + "name": "cpu", + "required": false }, { - "name": "execution_id", - "required": true, - "style": "simple", - "explode": false + "name": "cpu_max_vcpu", + "required": false + }, + { + "name": "memory", + "required": false + }, + { + "name": "memory_max_mib", + "required": false } ], + "publicPositionalParams": [], + "pathParamDetails": [], "queryParamDetails": [], "headerParamDetails": [], "cookieParams": [], "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "LogRetrieveParams" + "publicName": "AutoresizingUpdateParams" + }, + "requestBody": { + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "required": true, + "publicName": "body", + "publicIdentifier": "body" }, "response": { "status": "200", @@ -7281,11 +9493,7 @@ ] }, "responseModel": { - "name": "status", - "publicAliases": [] - }, - "responseHeadersModel": { - "name": "RetrieveResponseHeaders", + "name": "policy", "publicAliases": [] }, "result": { @@ -7339,50 +9547,51 @@ "transport": "http" }, { - "resource": "machines.executions.logs", - "publicResource": "machines.executions.logs", - "operation": "reauthorize", - "publicOperation": "reauthorize", + "resource": "images", + "publicResource": "images", + "operation": "list", + "publicOperation": "list", "deprecated": false, - "method": "POST", - "path": "/v1/machines/{machine_id}/executions/{execution_id}/logs/reauthorize", - "pathParams": [ - "machine_id", - "execution_id" + "method": "GET", + "path": "/v1/images", + "pathParams": [], + "publicPathParams": [], + "queryParams": [ + "limit", + "cursor" ], - "publicPathParams": [ - "machine_id", - "execution_id" + "publicQueryParams": [ + "limit", + "cursor" ], - "queryParams": [], - "publicQueryParams": [], "headerParams": [], "publicHeaderParams": [], "bodyParams": [], "publicBodyParams": [], "bodyParamDetails": [], "publicPositionalParams": [], - "pathParamDetails": [ + "pathParamDetails": [], + "queryParamDetails": [ { - "name": "machine_id", - "required": true, - "style": "simple", - "explode": false + "name": "limit", + "required": false, + "style": "form", + "explode": false, + "defaultValue": 100 }, { - "name": "execution_id", - "required": true, - "style": "simple", + "name": "cursor", + "required": false, + "style": "form", "explode": false } ], - "queryParamDetails": [], "headerParamDetails": [], "cookieParams": [], "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "LogReauthorizeParams" + "publicName": "ImageListParams" }, "response": { "status": "200", @@ -7396,11 +9605,7 @@ ] }, "responseModel": { - "name": "status", - "publicAliases": [] - }, - "responseHeadersModel": { - "name": "ReauthorizeResponseHeaders", + "name": "image_list", "publicAliases": [] }, "result": { @@ -7436,16 +9641,16 @@ }, { "status": "default", - "contentType": "application/problem+json", + "contentType": "application/json", "encoding": "json", "contents": [ { - "contentType": "application/problem+json", + "contentType": "application/json", "encoding": "json" } ], "model": { - "name": "error_model", + "name": "error", "publicAliases": [] } } @@ -7454,53 +9659,74 @@ "transport": "http" }, { - "resource": "machines.executions.logs", - "publicResource": "machines.executions.logs", - "operation": "createToken", - "publicOperation": "createToken", + "resource": "images", + "publicResource": "images", + "operation": "create", + "publicOperation": "create", "deprecated": false, "method": "POST", - "path": "/v1/machines/{machine_id}/executions/{execution_id}/logs/token", - "pathParams": [ - "machine_id", - "execution_id" - ], - "publicPathParams": [ - "machine_id", - "execution_id" - ], + "path": "/v1/images", + "pathParams": [], + "publicPathParams": [], "queryParams": [], "publicQueryParams": [], "headerParams": [], "publicHeaderParams": [], - "bodyParams": [], - "publicBodyParams": [], - "bodyParamDetails": [], - "publicPositionalParams": [], - "pathParamDetails": [ + "bodyParams": [ + "credential_ref", + "name", + "platform", + "source" + ], + "publicBodyParams": [ + "credential_ref", + "name", + "platform", + "source" + ], + "bodyParamDetails": [ { - "name": "machine_id", - "required": true, - "style": "simple", - "explode": false + "name": "credential_ref", + "required": false }, { - "name": "execution_id", - "required": true, - "style": "simple", - "explode": false + "name": "name", + "required": false + }, + { + "name": "platform", + "required": true + }, + { + "name": "source", + "required": true } ], + "publicPositionalParams": [], + "pathParamDetails": [], "queryParamDetails": [], "headerParamDetails": [], "cookieParams": [], "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "LogCreateTokenParams" + "publicName": "ImageCreateParams" + }, + "requestBody": { + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "required": true, + "publicName": "body", + "publicIdentifier": "body" }, "response": { - "status": "200", + "status": "202", "contentType": "application/json", "encoding": "json", "contents": [ @@ -7511,15 +9737,15 @@ ] }, "responseModel": { - "name": "read_token", + "name": "image", "publicAliases": [] }, "responseHeadersModel": { - "name": "CreateTokenResponseHeaders", + "name": "CreateResponseHeaders", "publicAliases": [] }, "result": { - "successStatus": "200", + "successStatus": "202", "errorStatuses": [ "401", "403", @@ -7536,7 +9762,11 @@ "contentType": "application/json", "encoding": "json" } - ] + ], + "headersModel": { + "name": "CreateStatus401ResponseHeaders", + "publicAliases": [] + } }, { "status": "403", @@ -7547,20 +9777,28 @@ "contentType": "application/json", "encoding": "json" } - ] + ], + "headersModel": { + "name": "CreateStatus403ResponseHeaders", + "publicAliases": [] + } }, { "status": "default", - "contentType": "application/problem+json", + "contentType": "application/json", "encoding": "json", "contents": [ { - "contentType": "application/problem+json", + "contentType": "application/json", "encoding": "json" } ], "model": { - "name": "error_model", + "name": "error", + "publicAliases": [] + }, + "headersModel": { + "name": "CreateStatusdefaultResponseHeaders", "publicAliases": [] } } @@ -7569,18 +9807,18 @@ "transport": "http" }, { - "resource": "machines.autoresizing", - "publicResource": "machines.autoresizing", + "resource": "images", + "publicResource": "images", "operation": "retrieve", "publicOperation": "retrieve", "deprecated": false, "method": "GET", - "path": "/v1/machines/{machine_id}/autoresizing", + "path": "/v1/images/{image_id}", "pathParams": [ - "machine_id" + "image_id" ], "publicPathParams": [ - "machine_id" + "image_id" ], "queryParams": [], "publicQueryParams": [], @@ -7592,7 +9830,7 @@ "publicPositionalParams": [], "pathParamDetails": [ { - "name": "machine_id", + "name": "image_id", "required": true, "style": "simple", "explode": false @@ -7604,7 +9842,7 @@ "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "AutoresizingRetrieveParams" + "publicName": "ImageRetrieveParams" }, "response": { "status": "200", @@ -7618,7 +9856,7 @@ ] }, "responseModel": { - "name": "settings", + "name": "image", "publicAliases": [] }, "result": { @@ -7654,16 +9892,16 @@ }, { "status": "default", - "contentType": "application/problem+json", + "contentType": "application/json", "encoding": "json", "contents": [ { - "contentType": "application/problem+json", + "contentType": "application/json", "encoding": "json" } ], "model": { - "name": "error_model", + "name": "error", "publicAliases": [] } } @@ -7672,39 +9910,39 @@ "transport": "http" }, { - "resource": "machines.autoresizing", - "publicResource": "machines.autoresizing", + "resource": "images", + "publicResource": "images", "operation": "update", "publicOperation": "update", "deprecated": false, - "method": "PUT", - "path": "/v1/machines/{machine_id}/autoresizing", + "method": "PATCH", + "path": "/v1/images/{image_id}", "pathParams": [ - "machine_id" + "image_id" ], "publicPathParams": [ - "machine_id" + "image_id" ], "queryParams": [], "publicQueryParams": [], "headerParams": [], "publicHeaderParams": [], "bodyParams": [ - "enabled" + "name" ], "publicBodyParams": [ - "enabled" + "name" ], "bodyParamDetails": [ { - "name": "enabled", + "name": "name", "required": true } ], "publicPositionalParams": [], "pathParamDetails": [ { - "name": "machine_id", + "name": "image_id", "required": true, "style": "simple", "explode": false @@ -7716,7 +9954,7 @@ "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "AutoresizingUpdateParams" + "publicName": "ImageUpdateParams" }, "requestBody": { "contentType": "application/json", @@ -7743,7 +9981,7 @@ ] }, "responseModel": { - "name": "settings", + "name": "image", "publicAliases": [] }, "result": { @@ -7779,16 +10017,16 @@ }, { "status": "default", - "contentType": "application/problem+json", + "contentType": "application/json", "encoding": "json", "contents": [ { - "contentType": "application/problem+json", + "contentType": "application/json", "encoding": "json" } ], "model": { - "name": "error_model", + "name": "error", "publicAliases": [] } } @@ -7797,15 +10035,19 @@ "transport": "http" }, { - "resource": "organization.autoresizing", - "publicResource": "organization.autoresizing", - "operation": "retrieve", - "publicOperation": "retrieve", + "resource": "images", + "publicResource": "images", + "operation": "delete", + "publicOperation": "delete", "deprecated": false, - "method": "GET", - "path": "/v1/organization/autoresizing", - "pathParams": [], - "publicPathParams": [], + "method": "DELETE", + "path": "/v1/images/{image_id}", + "pathParams": [ + "image_id" + ], + "publicPathParams": [ + "image_id" + ], "queryParams": [], "publicQueryParams": [], "headerParams": [], @@ -7814,12 +10056,22 @@ "publicBodyParams": [], "bodyParamDetails": [], "publicPositionalParams": [], - "pathParamDetails": [], + "pathParamDetails": [ + { + "name": "image_id", + "required": true, + "style": "simple", + "explode": false + } + ], "queryParamDetails": [], "headerParamDetails": [], "cookieParams": [], "publicCookieParams": [], "cookieParamDetails": [], + "paramsModel": { + "publicName": "ImageDeleteParams" + }, "response": { "status": "200", "contentType": "application/json", @@ -7832,7 +10084,7 @@ ] }, "responseModel": { - "name": "policy", + "name": "image", "publicAliases": [] }, "result": { @@ -7868,16 +10120,16 @@ }, { "status": "default", - "contentType": "application/problem+json", + "contentType": "application/json", "encoding": "json", "contents": [ { - "contentType": "application/problem+json", + "contentType": "application/json", "encoding": "json" } ], "model": { - "name": "error_model", + "name": "error", "publicAliases": [] } } @@ -7886,13 +10138,13 @@ "transport": "http" }, { - "resource": "organization.autoresizing", - "publicResource": "organization.autoresizing", - "operation": "update", - "publicOperation": "update", + "resource": "images.uploads", + "publicResource": "images.uploads", + "operation": "create", + "publicOperation": "create", "deprecated": false, - "method": "PUT", - "path": "/v1/organization/autoresizing", + "method": "POST", + "path": "/v1/images/uploads", "pathParams": [], "publicPathParams": [], "queryParams": [], @@ -7900,14 +10152,26 @@ "headerParams": [], "publicHeaderParams": [], "bodyParams": [ - "enabled" + "platform", + "sha256", + "size_bytes" ], "publicBodyParams": [ - "enabled" + "platform", + "sha256", + "size_bytes" ], "bodyParamDetails": [ { - "name": "enabled", + "name": "platform", + "required": true + }, + { + "name": "sha256", + "required": true + }, + { + "name": "size_bytes", "required": true } ], @@ -7919,7 +10183,7 @@ "publicCookieParams": [], "cookieParamDetails": [], "paramsModel": { - "publicName": "AutoresizingUpdateParams" + "publicName": "UploadCreateParams" }, "requestBody": { "contentType": "application/json", @@ -7946,7 +10210,11 @@ ] }, "responseModel": { - "name": "policy", + "name": "reservation", + "publicAliases": [] + }, + "responseHeadersModel": { + "name": "CreateResponseHeaders", "publicAliases": [] }, "result": { @@ -7967,7 +10235,11 @@ "contentType": "application/json", "encoding": "json" } - ] + ], + "headersModel": { + "name": "CreateStatus401ResponseHeaders", + "publicAliases": [] + } }, { "status": "403", @@ -7978,20 +10250,28 @@ "contentType": "application/json", "encoding": "json" } - ] + ], + "headersModel": { + "name": "CreateStatus403ResponseHeaders", + "publicAliases": [] + } }, { "status": "default", - "contentType": "application/problem+json", + "contentType": "application/json", "encoding": "json", "contents": [ { - "contentType": "application/problem+json", + "contentType": "application/json", "encoding": "json" } ], "model": { - "name": "error_model", + "name": "error", + "publicAliases": [] + }, + "headersModel": { + "name": "CreateStatusdefaultResponseHeaders", "publicAliases": [] } } diff --git a/scripts/check-contract.py b/scripts/check-contract.py new file mode 100644 index 0000000..0e1b4b5 --- /dev/null +++ b/scripts/check-contract.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Compare generated operations with the independently pinned public API contract.""" + +import json +from collections import Counter +from pathlib import Path + + +def check(contract, manifest): + expected = {(op["method"], op["path"]): op for op in contract["operations"]} + counts = Counter((op["method"].upper(), op["path"]) for op in manifest["operations"]) + errors = [] + for key in sorted(expected.keys() - counts.keys()): + errors.append(f"missing operation: {key}") + for key in sorted(counts.keys() - expected.keys()): + errors.append(f"obsolete operation: {key}") + for key, count in counts.items(): + if count != 1: + errors.append(f"duplicate operation: {key}") + for op in manifest["operations"]: + key = (op["method"].upper(), op["path"]) + if key in expected and sorted(op.get("bodyParams", [])) != expected[key]["bodyParams"]: + errors.append(f"request fields differ: {key}") + return errors + + +if __name__ == "__main__": + root = Path(__file__).resolve().parents[1] + contract = json.loads((root / "api-contract.json").read_text()) + manifest = json.loads((root / "scalar-sdk.manifest.json").read_text()) + errors = check(contract, manifest) + if errors: + raise SystemExit("\n".join(errors)) + print(f"Contract verified: {len(contract['operations'])} operations; source {contract['source_sha']}") diff --git a/scripts/test-contract.py b/scripts/test-contract.py new file mode 100644 index 0000000..4e50569 --- /dev/null +++ b/scripts/test-contract.py @@ -0,0 +1,36 @@ +"""Mutation tests: stale output must never satisfy the pinned contract.""" + +import copy +import json +import runpy +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +check = runpy.run_path(str(ROOT / "scripts/check-contract.py"))["check"] + + +class ContractGuardTests(unittest.TestCase): + def test_rejects_missing_obsolete_duplicate_and_changed_fields(self): + contract = json.loads((ROOT / "api-contract.json").read_text()) + manifest = json.loads((ROOT / "scalar-sdk.manifest.json").read_text()) + self.assertEqual(check(contract, manifest), []) + for mode in ("missing", "obsolete", "duplicate", "field"): + with self.subTest(mode=mode): + changed = copy.deepcopy(manifest) + operations = changed["operations"] + if mode == "missing": + operations.pop() + elif mode == "obsolete": + operations[0]["method"] = "TRACE" + elif mode == "duplicate": + operations.append(operations[0]) + else: + next(op for op in operations if op["method"] == "POST" and op["path"] == "/v1/machines")[ + "bodyParams" + ].remove("image_id") + self.assertTrue(check(contract, changed)) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/dedalus_sdk/_base_client.py b/src/dedalus_sdk/_base_client.py index 76c3b9d..15f5274 100644 --- a/src/dedalus_sdk/_base_client.py +++ b/src/dedalus_sdk/_base_client.py @@ -4,6 +4,7 @@ import json import time import uuid +import zlib import email import asyncio import inspect @@ -36,13 +37,13 @@ import anyio import httpx -import distro import pydantic from httpx import URL from pydantic import PrivateAttr from . import _exceptions from ._qs import Querystring +from ._decoding import CodingRefused, BoundedDecoder, decoding_error from ._files import to_httpx_files, async_to_httpx_files from ._types import ( Body, @@ -70,6 +71,10 @@ APIResponse, BaseAPIResponse, AsyncAPIResponse, + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, extract_response_type, ) from ._constants import ( @@ -83,6 +88,7 @@ ) from ._streaming import Stream, SSEDecoder, AsyncStream, SSEBytesDecoder from ._exceptions import ( + APIError, APIStatusError, APITimeoutError, APIConnectionError, @@ -113,10 +119,422 @@ try: from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT except ImportError: - # taken from https://github.com/encode/httpx/blob/3ba5fe0d7ac70222590e759c31442b1cab263791/httpx/_config.py#L366 + # httpx's own default of five seconds, for a version that no longer exposes the constant. HTTPX_DEFAULT_TIMEOUT = Timeout(5.0) +# The redirect statuses the SDK follows, the ones the fetch spec follows. A 300 only offers choices +# and a 304 tells a conditional request its copy is current, so neither is a hop: like a redirect +# without a `Location`, each reaches the caller as a status error. +_REDIRECT_STATUSES: frozenset[int] = frozenset({301, 302, 303, 307, 308}) + +# How many redirects one request follows before giving up with a connection error. +MAX_REDIRECTS = 20 + +# Every header a credential travels in, lower-cased: `Authorization`, `Proxy-Authorization`, `Cookie`, +# the conventional key headers, and each header this SDK's auth schemes place (an `apiKey` scheme +# names its own). A redirect that leaves the origin drops all of them; the log masks the same set. +_CREDENTIAL_HEADERS: frozenset[str] = frozenset( + ("authorization", "proxy-authorization", "api-key", "x-api-key", "cookie", "set-cookie") +) + +# The query parameters this SDK's `apiKey` schemes place a credential in. +_CREDENTIAL_QUERY: tuple[str, ...] = () + +# The headers that describe a request body, dropped with the body when a redirect turns the request +# into a bodiless GET (the fetch spec's list, plus the framing headers httpx sets). +_BODY_HEADERS: tuple[str, ...] = ( + "content-type", + "content-length", + "content-encoding", + "content-language", + "content-location", + "transfer-encoding", +) + +_DEFAULT_PORTS: Dict[str, int] = {"http": 80, "https": 443} + +# The most bytes a buffered response body may decode to (after transparent decompression), so a +# hostile or broken server cannot make the client hold an unbounded body in memory. A binary +# download or a streamed response is handed to the caller unread and is not capped. +MAX_RESPONSE_BODY_BYTES = 64 * 1024 * 1024 + +# The most bytes of an error response's body read for its message and `body`; the rest is dropped. +MAX_ERROR_BODY_BYTES = 64 * 1024 + + +class _ResponseRefused(Exception): + """A response the SDK will not act on, reported to the caller as an SDK error with `message`.""" + + def __init__(self, message: str, *, request: httpx.Request) -> None: + super().__init__(message) + self.message = message + self.request = request + + +class _RedirectRefused(_ResponseRefused): + """A redirect the SDK will not follow; it reaches the caller as an `APIConnectionError`.""" + + +class _BodyRefused(_ResponseRefused): + """A buffered body the SDK will not read; it reaches the caller as an `APIError` and is not retried.""" + + +class _BodyTooLarge(_BodyRefused): + """A buffered body larger than `MAX_RESPONSE_BODY_BYTES`.""" + + +def _origin(url: URL) -> tuple[str, str, int | None]: + """Scheme, host and port of `url`, the default port spelled out, so `https://h:443` is `https://h`.""" + scheme = url.scheme.lower() + return (scheme, url.host.lower(), url.port if url.port is not None else _DEFAULT_PORTS.get(scheme)) + + +def _is_followed_redirect(response: httpx.Response) -> bool: + """Whether `response` is a redirect the SDK follows: a 301, 302, 303, 307 or 308 naming a `Location`.""" + return response.status_code in _REDIRECT_STATUSES and bool(response.headers.get("location", "").strip()) + + +def _is_replayable(stream: object) -> bool: + """Whether a request body can be sent again on the next hop. + + Bytes can, and so can a multipart body whose files are bytes or seekable (httpx rewinds each one + as it renders the part). An iterator or an unseekable file was consumed by the first request, so + resending it would deliver an empty or truncated body the server would act on. + """ + if isinstance(stream, httpx.ByteStream): + return True + fields = getattr(stream, "fields", None) + if fields is None: + return False + for field in cast("list[Any]", fields): + file = getattr(field, "file", None) + if file is None or isinstance(file, (bytes, str)): + continue + seekable = getattr(file, "seekable", None) + try: + if not (callable(seekable) and seekable()): + return False + except Exception: + return False + return True + + +def _redirect_request( + request: httpx.Request, response: httpx.Response, *, sent_headers: httpx.Headers, keep_credentials: bool +) -> tuple[httpx.Request, bool]: + """The request a followed redirect sends next, and whether it still carries the credentials. + + `Location` is resolved against the URL that was redirected (RFC 3986 section 5.2). Only http and + https are followed, and never from https down to http, which would send the request and its + credentials in clear. + + A 303 answering anything but HEAD, and a 301 or 302 answering a POST, is fetched again as a + bodiless GET, as the fetch spec does. Every other redirect keeps its method and resends its body + (a PUT answered with a 301 must not become a GET that succeeds without doing what was asked), and + a body that cannot be replayed is refused rather than resent empty. + + Leaving the origin (scheme, host and port) drops every credential header, whether the client or a + per-call `extra_headers` set it, and the drop holds for the rest of the chain, so a chain that + comes back to the first origin arrives without them. A query-placed key cannot leave at all: the + target's query is the `Location`'s own. On the same origin a relative `Location` cannot carry the + query key, so it is carried over exactly as a header credential is. + + The next hop is built from `sent_headers`, the headers as they stood before httpx ran the auth flow: + an `httpx.Auth` signs the request object in place, so its headers (whatever their names) must not + be copied onto a hop it did not sign. A hop that stays on the origin is signed again on send. + """ + location = response.headers.get("location", "").strip() + try: + target = request.url.join(location) + except Exception as exc: + raise _RedirectRefused("Redirect with an unusable Location header", request=request) from exc + + scheme = target.scheme.lower() + if scheme not in ("http", "https") or not target.host: + raise _RedirectRefused("Redirect to an unsupported URL scheme", request=request) + if request.url.scheme.lower() == "https" and scheme == "http": + raise _RedirectRefused("Redirect from https to insecure http", request=request) + target = target.copy_with(fragment=None) + + headers = httpx.Headers(sent_headers) + method = request.method.upper() + status = response.status_code + stream: Any = request.stream + if (status == 303 and method != "HEAD") or (status in (301, 302) and method == "POST"): + method = "GET" + stream = None + for name in _BODY_HEADERS: + headers.pop(name, None) + elif not _is_replayable(stream): + raise _RedirectRefused("Redirect would resend a request body that cannot be replayed", request=request) + + if keep_credentials and _origin(request.url) == _origin(target): + present = set(target.params.keys()) + carried = [ + (key, value) + for key, value in request.url.params.multi_items() + if key in _CREDENTIAL_QUERY and key not in present + ] + if carried: + target = target.copy_merge_params(tuple(carried)) + _merge_set_cookies(headers, response, target) + else: + keep_credentials = False + for name in [name for name in headers if name.lower() in _CREDENTIAL_HEADERS]: + del headers[name] + + # httpx only fills `Host` in for a request built without a stream, so it is set here for both. + headers["Host"] = target.netloc.decode("ascii") + extensions = dict(request.extensions) + if "_" in target.host: + # work around https://github.com/encode/httpx/discussions/2880, as `_build_request` does + extensions["sni_hostname"] = target.host.replace("_", "-") + else: + extensions.pop("sni_hostname", None) + + return httpx.Request(method, target, headers=headers, stream=stream, extensions=extensions), keep_credentials + + +def _merge_set_cookies(headers: httpx.Headers, response: httpx.Response, target: httpx.URL) -> None: + """Folds the cookies a same-origin redirect sets into the next hop's `Cookie` header. + + httpx writes a request's cookies into a header when the request is built, so without this a cookie + the 3xx itself sets (a session, say) would be missing from the next hop, as it is not when httpx + follows. A cookie the redirect sets replaces the one of the same name the header carried. + """ + if not response.headers.get_list("set-cookie"): + return + fresh = httpx.Cookies() + fresh.extract_cookies(response) + probe = httpx.Request("GET", target) + fresh.set_cookie_header(probe) + added = probe.headers.get("cookie") + if not added: + return + pairs: Dict[str, str] = {} + for part in [*headers.get("cookie", "").split(";"), *added.split(";")]: + part = part.strip() + if part: + pairs[part.partition("=")[0]] = part + headers["Cookie"] = "; ".join(pairs.values()) + + +def _describe_body(value: object) -> str: + """A request body as the debug log shows it: its shape and size, never its values. + + A mapping lists its keys, which are the API's field names; every value, nested ones included, + stays out of the log. + """ + if isinstance(value, (bytes, bytearray)): + return f"<{len(value)} bytes>" + if isinstance(value, str): + return f"<{len(value)} characters>" + if is_mapping(value): + keys = ", ".join(sorted(str(key) for key in value)) + return f"" + if is_list(value) or isinstance(value, tuple): + return f"<{len(cast('list[object]', value))} items>" + return f"<{type(value).__name__}>" + + +def _read_deadline(request: httpx.Request, started: float) -> float | None: + """When reading a buffered body must have finished: the request's read timeout after `started`, when its headers arrived. + + httpx applies its read timeout to each socket read, so a server trickling one byte per read keeps + a body arriving forever; this turns the same number into a deadline for reading the whole body. + """ + timeout = cast("Mapping[str, Any]", request.extensions.get("timeout") or {}) + read = timeout.get("read") + return None if read is None else started + float(read) + + +def _is_unbuffered_cast(cast_to: object) -> bool: + """Whether `cast_to` hands the body to the caller as it arrived: a binary download or a raw `httpx.Response`. + + Those bodies are the caller's to bound, so neither the size cap nor the read deadline applies. + The legacy binary wrapper is matched by name, since its module ships only with the legacy surface. + """ + origin = get_origin(cast_to) or cast_to + if not inspect.isclass(origin): + return False + if issubclass( + origin, + ( + httpx.Response, + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, + ), + ): + return True + return any(base.__name__ == "HttpxBinaryResponseContent" for base in origin.__mro__) + + +def _body_limit(response: httpx.Response, *, capped: bool) -> tuple[int | None, bool]: + """The byte limit for reading `response`, and whether reaching it truncates rather than refuses. + + An error body is read for its message only, so it is cut at `MAX_ERROR_BODY_BYTES`; a success body + is refused past `MAX_RESPONSE_BODY_BYTES`, unless `capped` is off for a body the caller takes raw. + """ + if not response.is_success: + return MAX_ERROR_BODY_BYTES, True + return (MAX_RESPONSE_BODY_BYTES if capped else None), False + + +def _too_large(response: httpx.Response, limit: int) -> _BodyTooLarge: + """The error for a success body that decoded past `limit` bytes.""" + return _BodyTooLarge( + f"Response body exceeds the {limit} byte limit ({limit // (1024 * 1024)} MiB) for a buffered response", + request=response.request, + ) + + +class _BodyBuffer: + """The decoded bytes of a buffered body read so far, held to the limit `_body_limit` sets.""" + + def __init__(self, response: httpx.Response, *, capped: bool) -> None: + self._response = response + self._limit, self._truncate = _body_limit(response, capped=capped) + self._chunks: List[bytes] = [] + self._size = 0 + + def add(self, pieces: Iterable[bytes]) -> bool: + """Appends `pieces`; True once a truncated body is full and reading should stop. + + A body refused past the limit raises `_BodyTooLarge` as soon as a piece would cross it, so a + decompression bomb stops being inflated there. + """ + for piece in pieces: + if self._limit is not None and self._size + len(piece) > self._limit: + if not self._truncate: + raise _too_large(self._response, self._limit) + self._chunks.append(piece[: self._limit - self._size]) + return True + self._size += len(piece) + self._chunks.append(piece) + return False + + @property + def truncates(self) -> bool: + """Whether this is an error body, read for its message: past the limit or the deadline it is cut, not refused.""" + return self._truncate + + def content(self) -> bytes: + """The body read, decoded and joined.""" + return b"".join(self._chunks) + + +def _read_body(response: httpx.Response, *, capped: bool, deadline: float | None) -> None: + """Reads `response`'s body into memory, bounded in size and, when `deadline` is set, in time. + + The bytes are counted after transparent decompression, and gzip and deflate (stacked or not) are + inflated by `BoundedDecoder` in bounded steps, so a small compressed body cannot expand past the + limit. A read still running at `deadline` raises `httpx.ReadTimeout`, which the request loop + retries or reports like any other timeout, except on an error body, which keeps what arrived by + then so its status error still stands; httpx cannot interrupt one blocked socket read, so the + deadline can be overrun by at most one read timeout. + """ + body = _BodyBuffer(response, capped=capped) + try: + decoder = BoundedDecoder.for_response(response, capped=capped) + if decoder is None: + for chunk in response.iter_bytes(): + if body.add((chunk,)): + break + if deadline is not None and time.monotonic() > deadline: + if body.truncates: + break + raise httpx.ReadTimeout("Timed out reading the response body", request=response.request) + else: + for raw in response.iter_raw(): + if body.add(decoder.decode(raw)): + break + if deadline is not None and time.monotonic() > deadline: + if body.truncates: + break + raise httpx.ReadTimeout("Timed out reading the response body", request=response.request) + else: + body.add(decoder.flush()) + except CodingRefused as exc: + raise _BodyRefused(exc.message, request=response.request) from None + except zlib.error as exc: + raise decoding_error(response, exc) from exc + finally: + response.close() + response._content = body.content() # pyright: ignore[reportPrivateUsage] + + +async def _aread_body(response: httpx.Response, *, capped: bool, deadline: float | None) -> None: + """The async twin of `_read_body`, where the deadline cancels a read that is still blocked.""" + body = _BodyBuffer(response, capped=capped) + + async def read() -> None: + """Reads the body into `body` until it ends or `body` is full.""" + decoder = BoundedDecoder.for_response(response, capped=capped) + if decoder is None: + async for chunk in response.aiter_bytes(): + if body.add((chunk,)): + return + return + async for raw in response.aiter_raw(): + if body.add(decoder.decode(raw)): + return + body.add(decoder.flush()) + + try: + if deadline is None: + await read() + else: + try: + with anyio.fail_after(max(0.0, deadline - time.monotonic())): + await read() + except TimeoutError as exc: + if not body.truncates: + raise httpx.ReadTimeout("Timed out reading the response body", request=response.request) from exc + except CodingRefused as exc: + raise _BodyRefused(exc.message, request=response.request) from None + except zlib.error as exc: + raise decoding_error(response, exc) from exc + finally: + await response.aclose() + response._content = body.content() # pyright: ignore[reportPrivateUsage] + + +@dataclass +class SchemeCredentials: + """The credential one security scheme puts on a request, split by where it is sent.""" + + headers: Dict[str, str] = field(default_factory=dict) + query: Dict[str, str] = field(default_factory=dict) + cookies: Dict[str, str] = field(default_factory=dict) + + @property + def configured(self) -> bool: + """Whether the scheme's client option(s) are set, so it has a credential to send.""" + return bool(self.headers or self.query or self.cookies) + + +def _merge_scheme_credentials(parts: List[SchemeCredentials]) -> SchemeCredentials: + """The credentials of several schemes sent together, one request's worth.""" + merged = SchemeCredentials() + for part in parts: + merged.headers.update(part.headers) + merged.query.update(part.query) + merged.cookies.update(part.cookies) + return merged + + +@dataclass(frozen=True) +class AuthLocations: + """The lower-cased header names, and the query and cookie names, the client's credentials use.""" + + headers: frozenset[str] = frozenset() + query: frozenset[str] = frozenset() + cookies: frozenset[str] = frozenset() + + @dataclass class CursorPageConfig: """Per-operation cursor pagination descriptor baked into a page by the generated list method. @@ -239,6 +657,17 @@ def _info_to_options(self, info: PageInfo) -> FinalRequestOptions: return options if not isinstance(info.url, NotGiven): + # The next request carries this client's credentials, so a page URL a response names on + # another scheme, host or port is refused rather than dialed: a server must not be able + # to send the caller's key elsewhere by naming it as the next page. The message names + # neither URL, since either may carry a token. The origin compared is the one that served + # this page: an operation with its own `servers` entry is sent to an absolute URL, else + # to the client's base URL. `_client` is declared by the sync and async page classes, + # each with its own client type. + current = URL(self._options.url) + served_by: URL = current if current.is_absolute_url else cast(Any, self)._client.base_url + if info.url.is_absolute_url and _origin(info.url) != _origin(served_by): + raise RuntimeError("Refusing to follow a next page URL that points away from the API's origin") params = self._params_from_url(info.url) url = info.url.copy_with(params=params) options.params = dict(url.params) @@ -260,6 +689,25 @@ def _info_to_options(self, info: PageInfo) -> FinalRequestOptions: raise ValueError("Unexpected PageInfo state") + def _next_page_repeats(self) -> bool: + """Whether the next page's request would be exactly the request that fetched this page. + + A server that answers with the cursor (or link, or body token) it was just given would make + auto-pagination fetch the same page forever, so the walk stops there instead. + """ + info = self.next_page_info() + if info is None: + return False + try: + options = self._info_to_options(info) + except (TypeError, ValueError): + return False + return ( + options.url == self._options.url + and options.params == self._options.params + and options.json_data == self._options.json_data + ) + class BaseSyncPage(BasePage[_T], Generic[_T]): _client: SyncAPIClient = pydantic.PrivateAttr() @@ -296,7 +744,7 @@ def iter_pages(self: SyncPageT) -> Iterator[SyncPageT]: page = self while True: yield page - if page.has_next_page(): + if page.has_next_page() and not page._next_page_repeats(): page = page.get_next_page() else: return @@ -384,7 +832,7 @@ async def iter_pages(self: AsyncPageT) -> AsyncIterator[AsyncPageT]: page = self while True: yield page - if page.has_next_page(): + if page.has_next_page() and not page._next_page_repeats(): page = await page.get_next_page() else: return @@ -479,21 +927,106 @@ def _make_status_error( ) -> _exceptions.APIStatusError: raise NotImplementedError() + @property + def _auth_by_scheme(self) -> dict[str, SchemeCredentials]: + """The credentials each security scheme is sent with, by the scheme's name in the document.""" + return {} + + def _select_auth(self, security: SecurityOptions) -> SchemeCredentials: + """ + The credentials an operation with its own `security` requirements is sent with. + + The first alternative whose schemes all have a configured credential wins, and only its + credentials are sent: two schemes can share a wire slot (`Authorization` for Basic and + Bearer), so merging every configured credential would let one overwrite the one the + operation asked for. An empty alternative is the anonymous one and selects nothing. + + With no alternative met, the configured part of the first alternative that has one is sent: + the request only goes out at all when the caller supplies the rest some other way (see + `_credential_supplied`), and withholding the half the client holds would leave it incomplete. + An empty (anonymous) alternative counts as met, so an operation that also accepts `{}` is sent + nothing rather than a partial credential. Only schemes the operation names are ever sent. + """ + available = self._auth_by_scheme + partial: SchemeCredentials | None = None + for alternative in security: + parts = [available.get(scheme) for scheme in alternative] + configured = [part for part in parts if part is not None and part.configured] + if len(configured) == len(parts): + return _merge_scheme_credentials(configured) + if partial is None and configured: + partial = _merge_scheme_credentials(configured) + return partial if partial is not None else SchemeCredentials() + + @property + def _auth_locations(self) -> AuthLocations: + """Where the client's credentials travel, configured or not.""" + return AuthLocations() + + def _credential_supplied( + self, + security: SecurityOptions, + headers: Mapping[str, str], + custom_headers: Headers, + params: Query, + cookies: Mapping[str, str], + ) -> bool: + """ + Whether the request already carries a credential the caller set some other way than a client + option — the client's `default_headers` or `default_query`, or the call's `extra_headers` or + `extra_query` — or omits a credential header on purpose, as the client-wide check accepts. + + A value `_select_auth` put on the request is the client's own partial credential, not the + caller's, so it never counts: a half-configured alternative is refused unless the caller + supplies the rest. + """ + locations = self._auth_locations + selected = self._select_auth(security) + selected_headers = {name.lower(): value for name, value in selected.headers.items()} + return ( + any( + name.lower() in locations.headers and value and selected_headers.get(name.lower()) != value + for name, value in headers.items() + ) + or any( + name.lower() in locations.headers and isinstance(value, Omit) for name, value in custom_headers.items() + ) + or any( + params.get(name) is not None and params.get(name) != selected.query.get(name) + for name in locations.query + ) + or any( + cookies.get(name) is not None and cookies.get(name) != selected.cookies.get(name) + for name in locations.cookies + ) + ) + + def _security_met(self, security: SecurityOptions) -> bool: + """ + Whether an operation's own requirements can be met: it is public (`[]`), lists the anonymous + alternative (an empty one), or the credentials of some alternative are all configured. + """ + available = self._auth_by_scheme + return not security or any( + all((part := available.get(scheme)) is not None and part.configured for scheme in alternative) + for alternative in security + ) + def _auth_query( self, - security: SecurityOptions, # noqa: ARG002 + security: SecurityOptions | None, # noqa: ARG002 ) -> dict[str, str]: return {} def _auth_cookies( self, - security: SecurityOptions, # noqa: ARG002 + security: SecurityOptions | None, # noqa: ARG002 ) -> dict[str, str]: return {} def _custom_auth( self, - security: SecurityOptions, # noqa: ARG002 + security: SecurityOptions | None, # noqa: ARG002 ) -> httpx.Auth | None: # Single hook for custom httpx auth on both the sync and async request paths. It defaults to # the legacy public `custom_auth` property so subclasses that already override that property @@ -512,8 +1045,38 @@ def _build_headers( custom_headers = options.headers or {} # Auth headers are folded into `default_headers` (matching the reference SDKs), so # merging `default_headers` here already applies the configured credentials. - headers_dict = _merge_mappings(self.default_headers, custom_headers) - self._validate_headers(headers_dict, custom_headers, params, cookies) + default_headers = self.default_headers + if options.security is not None: + # The operation names its own requirements: swap the client-wide credential headers for + # the selected alternative's, keeping any header the caller configured on the client. + auth_names = {name.lower() for name in self.auth_headers} + custom_names = {name.lower() for name in self._custom_headers} + default_headers = { + **{ + name: value + for name, value in default_headers.items() + if name.lower() not in auth_names or name.lower() in custom_names + }, + **self._select_auth(options.security).headers, + **self._custom_headers, + } + headers_dict = _merge_mappings(default_headers, custom_headers) + if options.security is None: + # The client-wide check that some credential is configured. + self._validate_headers(headers_dict, custom_headers, params, cookies) + elif not self._security_met(options.security) and not self._credential_supplied( + options.security, headers_dict, custom_headers, params, cookies + ): + # An operation naming its own requirements is checked against those alone: a public one + # or one met by a query key is never refused for want of a header, and the client-wide + # check would both pass a document whose root allows anonymous calls and name credentials + # that cannot meet this operation. A credential the caller supplies outside the client's + # options takes the place of a configured one; the selected credentials are already + # swapped in above, so an unselected configured one does not count. + alternatives = " or ".join(" + ".join(alternative) for alternative in options.security) + raise TypeError( + f"Could not resolve authentication method for this operation. Expected the credentials of {alternatives} to be set." + ) # headers are case-insensitive while dictionaries are not. headers = httpx.Headers(headers_dict) @@ -541,13 +1104,15 @@ def _prepare_url(self, url: str) -> URL: Merge a URL argument together with any 'base_url' on the client, to create the URL used for the outgoing request. """ - # Copied from httpx's `_merge_url` method. - merge_url = URL(url) - if merge_url.is_relative_url: - merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/") - return self.base_url.copy_with(raw_path=merge_raw_path) + target = URL(url) + if target.is_absolute_url: + return target - return merge_url + # A relative URL always extends the base path, even when it starts with "/": operation paths + # are written from the API root, which is wherever `base_url` points. Exactly one "/" joins + # the two halves. `raw_path` carries the query string, so a query on `url` is kept. + base_path = self.base_url.raw_path.rstrip(b"/") + return self.base_url.copy_with(raw_path=b"/".join((base_path, target.raw_path.lstrip(b"/")))) def _make_sse_decoder(self) -> SSEDecoder | SSEBytesDecoder: return SSEDecoder() @@ -559,19 +1124,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug( - "Request options: %s", - model_dump( - options, - exclude_unset=True, - # Pydantic v1 can't dump every type we support in content, so we exclude it for now. - exclude={ - "content", - } - if PYDANTIC_V1 - else {}, - ), + # The request bodies are described, never dumped: a body carries whatever the caller + # sends (passwords, tokens, personal data), and the log redaction only knows credential + # header and query names. The headers and params are dumped, masked by that redaction. + dumped = model_dump( + options, + exclude_unset=True, + exclude={"content", "json_data", "extra_json", "files"}, ) + for name in ("content", "json_data", "extra_json", "files"): + value: object = getattr(options, name, None) + if value is not None and is_given(value): + dumped[name] = _describe_body(value) + log.debug("Request options: %s", dumped) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -752,6 +1317,9 @@ def auth_headers(self) -> dict[str, str]: def default_headers(self) -> dict[str, str | Omit]: return { "Accept": "application/json", + # Only the codings `BoundedDecoder` inflates in bounded steps; httpx would otherwise add br + # and zstd whenever their optional packages are installed, and inflates those unbounded. + "Accept-Encoding": "gzip, deflate", "Content-Type": "application/json", "User-Agent": self.user_agent, **self.platform_headers(), @@ -900,7 +1468,8 @@ class _DefaultHttpxClient(httpx.Client): def __init__(self, **kwargs: Any) -> None: kwargs.setdefault("timeout", DEFAULT_TIMEOUT) kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) - kwargs.setdefault("follow_redirects", True) + # The SDK follows redirects itself (see `_send_following_redirects`), so httpx never does. + kwargs.setdefault("follow_redirects", False) super().__init__(**kwargs) @@ -1022,6 +1591,48 @@ def _prepare_request( """ return None + def _send_following_redirects( + self, + request: httpx.Request, + *, + options: FinalRequestOptions, + auth: httpx.Auth | None, + ) -> httpx.Response: + """Sends `request`, following redirects in the SDK rather than in httpx, and returns the final response unread. + + httpx is told not to follow on every send, so a caller-supplied `http_client` built with + `follow_redirects=True` cannot follow either: httpx strips only `Authorization` on a + cross-origin hop and would resend an API key header, cookies and a 307/308 body to whatever + host the server names. See `_redirect_request` for what each hop keeps. A hop that has left + the origin is sent with a no-op `auth`, so neither `custom_auth`, the caller's client-level + `auth=` nor a `.netrc` entry (httpx < 0.24 reads one by default) signs it. + + `options.follow_redirects=False` turns following off altogether: the 3xx itself reaches the + caller as a status error. Unset or `True`, the SDK follows at most `MAX_REDIRECTS` hops. + """ + keep_credentials = True + hops = 0 + while True: + kwargs: HttpxSendArgs = {"follow_redirects": False} + if not keep_credentials: + kwargs["auth"] = httpx.Auth() + elif auth is not None: + kwargs["auth"] = auth + sent_headers = httpx.Headers(request.headers) + response = self._client.send(request, stream=True, **kwargs) + if options.follow_redirects is False or not _is_followed_redirect(response): + return response + try: + if hops >= MAX_REDIRECTS: + raise _RedirectRefused(f"Maximum redirects ({MAX_REDIRECTS}) exceeded", request=request) + request, keep_credentials = _redirect_request( + request, response, sent_headers=sent_headers, keep_credentials=keep_credentials + ) + finally: + response.close() + hops += 1 + log.debug("Following redirect %i to %s", response.status_code, request.url) + @overload def request( self, @@ -1081,23 +1692,32 @@ def request( request = self._build_request(options, retries_taken=retries_taken) self._prepare_request(request) - kwargs: HttpxSendArgs = {} custom_auth = self._custom_auth(options.security) - if custom_auth is not None: - kwargs["auth"] = custom_auth - - if options.follow_redirects is not None: - kwargs["follow_redirects"] = options.follow_redirects + streamed = stream or self._should_stream_response_body(request=request) log.debug("Sending HTTP Request: %s %s", request.method, request.url) response = None try: - response = self._client.send( - request, - stream=stream or self._should_stream_response_body(request=request), - **kwargs, - ) + response = self._send_following_redirects(request, options=options, auth=custom_auth) + # The body's deadline starts once the headers are in: sending the request and waiting + # for the answer are bounded by httpx's own write and read timeouts. + started = time.monotonic() + if not streamed: + # Only a successful download is the caller's to bound; an error body is read for + # its message, capped and on the deadline like any other. + unbounded = _is_unbuffered_cast(cast_to) and response.is_success + _read_body( + response, + capped=not unbounded, + deadline=None if unbounded else _read_deadline(request, started), + ) + except _RedirectRefused as err: + log.debug("Refusing redirect: %s", err.message) + raise APIConnectionError(message=err.message, request=err.request) from None + except _BodyRefused as err: + log.debug("Refusing response body: %s", err.message) + raise APIError(err.message, err.request, body=None) from None except httpx.TimeoutException as err: log.debug("Encountered httpx.TimeoutException", exc_info=True) @@ -1129,8 +1749,8 @@ def request( log.debug( 'HTTP Response: %s %s "%i %s" %s', - request.method, - request.url, + response.request.method, + response.request.url, response.status_code, response.reason_phrase, response.headers, @@ -1138,7 +1758,7 @@ def request( try: response.raise_for_status() - except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + except httpx.HTTPStatusError as err: # thrown on 3xx (one not followed), 4xx and 5xx log.debug("Encountered httpx.HTTPStatusError", exc_info=True) if remaining_retries > 0 and self._should_retry(err.response): @@ -1151,10 +1771,13 @@ def request( ) continue - # If the response is streamed then we need to explicitly read the response - # to completion before attempting to access the response text. + # A streamed response is still unread: read the first `MAX_ERROR_BODY_BYTES` of it + # for the message, within the read deadline, never the whole body a hostile server + # could make endless or trickle forever. if not err.response.is_closed: - err.response.read() + _read_body( + err.response, capped=True, deadline=_read_deadline(err.response.request, time.monotonic()) + ) log.debug("Re-raising status error") raise self._make_status_error_from_response(err.response) from None @@ -1469,7 +2092,8 @@ class _DefaultAsyncHttpxClient(httpx.AsyncClient): def __init__(self, **kwargs: Any) -> None: kwargs.setdefault("timeout", DEFAULT_TIMEOUT) kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) - kwargs.setdefault("follow_redirects", True) + # The SDK follows redirects itself (see `_send_following_redirects`), so httpx never does. + kwargs.setdefault("follow_redirects", False) super().__init__(**kwargs) @@ -1486,7 +2110,8 @@ class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore def __init__(self, **kwargs: Any) -> None: kwargs.setdefault("timeout", DEFAULT_TIMEOUT) kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) - kwargs.setdefault("follow_redirects", True) + # The SDK follows redirects itself (see `_send_following_redirects`), so httpx never does. + kwargs.setdefault("follow_redirects", False) super().__init__(**kwargs) @@ -1611,6 +2236,37 @@ async def _prepare_request( """ return None + async def _send_following_redirects( + self, + request: httpx.Request, + *, + options: FinalRequestOptions, + auth: httpx.Auth | None, + ) -> httpx.Response: + """The async twin of `SyncAPIClient._send_following_redirects`, which documents the rules.""" + keep_credentials = True + hops = 0 + while True: + kwargs: HttpxSendArgs = {"follow_redirects": False} + if not keep_credentials: + kwargs["auth"] = httpx.Auth() + elif auth is not None: + kwargs["auth"] = auth + sent_headers = httpx.Headers(request.headers) + response = await self._client.send(request, stream=True, **kwargs) + if options.follow_redirects is False or not _is_followed_redirect(response): + return response + try: + if hops >= MAX_REDIRECTS: + raise _RedirectRefused(f"Maximum redirects ({MAX_REDIRECTS}) exceeded", request=request) + request, keep_credentials = _redirect_request( + request, response, sent_headers=sent_headers, keep_credentials=keep_credentials + ) + finally: + await response.aclose() + hops += 1 + log.debug("Following redirect %i to %s", response.status_code, request.url) + @overload async def request( self, @@ -1675,23 +2331,32 @@ async def request( request = self._build_request(options, retries_taken=retries_taken) await self._prepare_request(request) - kwargs: HttpxSendArgs = {} custom_auth = self._custom_auth(options.security) - if custom_auth is not None: - kwargs["auth"] = custom_auth - - if options.follow_redirects is not None: - kwargs["follow_redirects"] = options.follow_redirects + streamed = stream or self._should_stream_response_body(request=request) log.debug("Sending HTTP Request: %s %s", request.method, request.url) response = None try: - response = await self._client.send( - request, - stream=stream or self._should_stream_response_body(request=request), - **kwargs, - ) + response = await self._send_following_redirects(request, options=options, auth=custom_auth) + # The body's deadline starts once the headers are in: sending the request and waiting + # for the answer are bounded by httpx's own write and read timeouts. + started = time.monotonic() + if not streamed: + # Only a successful download is the caller's to bound; an error body is read for + # its message, capped and on the deadline like any other. + unbounded = _is_unbuffered_cast(cast_to) and response.is_success + await _aread_body( + response, + capped=not unbounded, + deadline=None if unbounded else _read_deadline(request, started), + ) + except _RedirectRefused as err: + log.debug("Refusing redirect: %s", err.message) + raise APIConnectionError(message=err.message, request=err.request) from None + except _BodyRefused as err: + log.debug("Refusing response body: %s", err.message) + raise APIError(err.message, err.request, body=None) from None except httpx.TimeoutException as err: log.debug("Encountered httpx.TimeoutException", exc_info=True) @@ -1723,8 +2388,8 @@ async def request( log.debug( 'HTTP Response: %s %s "%i %s" %s', - request.method, - request.url, + response.request.method, + response.request.url, response.status_code, response.reason_phrase, response.headers, @@ -1732,7 +2397,7 @@ async def request( try: response.raise_for_status() - except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + except httpx.HTTPStatusError as err: # thrown on 3xx (one not followed), 4xx and 5xx log.debug("Encountered httpx.HTTPStatusError", exc_info=True) if remaining_retries > 0 and self._should_retry(err.response): @@ -1745,10 +2410,13 @@ async def request( ) continue - # If the response is streamed then we need to explicitly read the response - # to completion before attempting to access the response text. + # A streamed response is still unread: read the first `MAX_ERROR_BODY_BYTES` of it + # for the message, within the read deadline, never the whole body a hostile server + # could make endless or trickle forever. if not err.response.is_closed: - await err.response.aread() + await _aread_body( + err.response, capped=True, deadline=_read_deadline(err.response.request, time.monotonic()) + ) log.debug("Re-raising status error") raise self._make_status_error_from_response(err.response) from None @@ -2146,15 +2814,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..b8f224b 100644 --- a/src/dedalus_sdk/_client.py +++ b/src/dedalus_sdk/_client.py @@ -24,8 +24,11 @@ from ._utils import is_given, is_mapping_t, get_async_library from ._compat import cached_property from ._exceptions import APIStatusError +from ._models import SecurityOptions from ._base_client import ( DEFAULT_MAX_RETRIES, + AuthLocations, + SchemeCredentials, SyncAPIClient, AsyncAPIClient, ) @@ -33,9 +36,10 @@ from ._version import __version__ if TYPE_CHECKING: - from .resources import machines, organization + from .resources import machines, organization, images from .resources.machines import MachinesResource, AsyncMachinesResource from .resources.organization import OrganizationResource, AsyncOrganizationResource + from .resources.images import ImagesResource, AsyncImagesResource # Serializes lazy resource imports so concurrent cold access from multiple # threads cannot deadlock on CPython import locks (see CPython 3.14). @@ -50,6 +54,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,13 +63,15 @@ 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, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, # Configure a custom httpx client. - # We provide a `DefaultHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. + # We provide a `DefaultHttpxClient` class that you can pass to retain the default values we use for `limits` & `timeout`. + # The SDK follows redirects itself, whatever the client's `follow_redirects` says. # See the [httpx documentation](https://www.python-httpx.org/api/#client) for more details. http_client: httpx.Client | None = None, # Enable or disable schema validation for data returned by the API. @@ -99,6 +106,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: @@ -136,6 +146,12 @@ def organization(self) -> "OrganizationResource": from .resources.organization import OrganizationResource return OrganizationResource(self) + @cached_property + def images(self) -> "ImagesResource": + with _RESOURCE_IMPORT_LOCK: + from .resources.images import ImagesResource + return ImagesResource(self) + @cached_property def with_raw_response(self) -> DedalusWithRawResponse: return DedalusWithRawResponse(self) @@ -155,18 +171,35 @@ 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 - def _auth_query(self, security: dict[str, bool]) -> dict[str, str]: - _ = security + def _auth_query(self, security: SecurityOptions | None) -> dict[str, str]: + if security is not None: + return self._select_auth(security).query return {} @override - def _auth_cookies(self, security: dict[str, bool]) -> dict[str, str]: - _ = security + def _auth_cookies(self, security: SecurityOptions | None) -> dict[str, str]: + if security is not None: + return self._select_auth(security).cookies return {} + @property + @override + def _auth_by_scheme(self) -> dict[str, SchemeCredentials]: + return { + "BearerAuth": SchemeCredentials(headers=self._api_key_header_auth), + "ApiKeyAuth": SchemeCredentials(headers=self._x_api_key_header_auth), + "BrowserAuth": SchemeCredentials(headers=self._browser_token_header_auth), + } + + @property + @override + def _auth_locations(self) -> AuthLocations: + return AuthLocations(headers=frozenset({"authorization", "x-api-key"})) + @property def _api_key_header_auth(self) -> dict[str, str]: value = self.api_key @@ -181,6 +214,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 +250,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 +260,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 +292,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 +332,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,13 +341,15 @@ 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, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, # Configure a custom httpx client. - # We provide a `DefaultAsyncHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. + # We provide a `DefaultAsyncHttpxClient` class that you can pass to retain the default values we use for `limits` & `timeout`. + # The SDK follows redirects itself, whatever the client's `follow_redirects` says. # See the [httpx documentation](https://www.python-httpx.org/api/#asyncclient) for more details. http_client: httpx.AsyncClient | None = None, # Enable or disable schema validation for data returned by the API. @@ -339,6 +384,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: @@ -376,6 +424,12 @@ def organization(self) -> "AsyncOrganizationResource": from .resources.organization import AsyncOrganizationResource return AsyncOrganizationResource(self) + @cached_property + def images(self) -> "AsyncImagesResource": + with _RESOURCE_IMPORT_LOCK: + from .resources.images import AsyncImagesResource + return AsyncImagesResource(self) + @cached_property def with_raw_response(self) -> AsyncDedalusWithRawResponse: return AsyncDedalusWithRawResponse(self) @@ -395,18 +449,35 @@ 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 - def _auth_query(self, security: dict[str, bool]) -> dict[str, str]: - _ = security + def _auth_query(self, security: SecurityOptions | None) -> dict[str, str]: + if security is not None: + return self._select_auth(security).query return {} @override - def _auth_cookies(self, security: dict[str, bool]) -> dict[str, str]: - _ = security + def _auth_cookies(self, security: SecurityOptions | None) -> dict[str, str]: + if security is not None: + return self._select_auth(security).cookies return {} + @property + @override + def _auth_by_scheme(self) -> dict[str, SchemeCredentials]: + return { + "BearerAuth": SchemeCredentials(headers=self._api_key_header_auth), + "ApiKeyAuth": SchemeCredentials(headers=self._x_api_key_header_auth), + "BrowserAuth": SchemeCredentials(headers=self._browser_token_header_auth), + } + + @property + @override + def _auth_locations(self) -> AuthLocations: + return AuthLocations(headers=frozenset({"authorization", "x-api-key"})) + @property def _api_key_header_auth(self) -> dict[str, str]: value = self.api_key @@ -421,6 +492,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 +528,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 +538,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 +570,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, @@ -542,6 +622,12 @@ def organization(self) -> organization.OrganizationResourceWithRawResponse: from .resources.organization import OrganizationResourceWithRawResponse return OrganizationResourceWithRawResponse(self._client.organization) + @cached_property + def images(self) -> images.ImagesResourceWithRawResponse: + with _RESOURCE_IMPORT_LOCK: + from .resources.images import ImagesResourceWithRawResponse + return ImagesResourceWithRawResponse(self._client.images) + class AsyncDedalusWithRawResponse: _client: AsyncDedalus @@ -561,6 +647,12 @@ def organization(self) -> organization.AsyncOrganizationResourceWithRawResponse: from .resources.organization import AsyncOrganizationResourceWithRawResponse return AsyncOrganizationResourceWithRawResponse(self._client.organization) + @cached_property + def images(self) -> images.AsyncImagesResourceWithRawResponse: + with _RESOURCE_IMPORT_LOCK: + from .resources.images import AsyncImagesResourceWithRawResponse + return AsyncImagesResourceWithRawResponse(self._client.images) + class DedalusWithStreamedResponse: _client: Dedalus @@ -580,6 +672,12 @@ def organization(self) -> organization.OrganizationResourceWithStreamingResponse from .resources.organization import OrganizationResourceWithStreamingResponse return OrganizationResourceWithStreamingResponse(self._client.organization) + @cached_property + def images(self) -> images.ImagesResourceWithStreamingResponse: + with _RESOURCE_IMPORT_LOCK: + from .resources.images import ImagesResourceWithStreamingResponse + return ImagesResourceWithStreamingResponse(self._client.images) + class AsyncDedalusWithStreamedResponse: _client: AsyncDedalus @@ -599,6 +697,12 @@ def organization(self) -> organization.AsyncOrganizationResourceWithStreamingRes from .resources.organization import AsyncOrganizationResourceWithStreamingResponse return AsyncOrganizationResourceWithStreamingResponse(self._client.organization) + @cached_property + def images(self) -> images.AsyncImagesResourceWithStreamingResponse: + with _RESOURCE_IMPORT_LOCK: + from .resources.images import AsyncImagesResourceWithStreamingResponse + return AsyncImagesResourceWithStreamingResponse(self._client.images) + # Alias names for the documented `Client` / `AsyncClient` symbols. Client = Dedalus diff --git a/src/dedalus_sdk/_compat.py b/src/dedalus_sdk/_compat.py index e6690a4..fb3d763 100644 --- a/src/dedalus_sdk/_compat.py +++ b/src/dedalus_sdk/_compat.py @@ -1,15 +1,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload +from typing import TYPE_CHECKING, Any, Union, TypeVar, cast from datetime import date, datetime -from typing_extensions import Self, Literal, TypedDict +from typing_extensions import Literal, TypedDict import pydantic from pydantic.fields import FieldInfo from ._types import IncEx, StrBytesIntFloat -_T = TypeVar("_T") _ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) # --------------- Pydantic v2, v3 compatibility --------------- @@ -192,35 +191,5 @@ class GenericModel(pydantic.BaseModel): ... # cached properties if TYPE_CHECKING: cached_property = property - - # we define a separate type (copied from typeshed) - # that represents that `cached_property` is `set`able - # at runtime, which differs from `@property`. - # - # this is a separate type as editors likely special case - # `@property` and we don't want to cause issues just to have - # more helpful internal types. - - class typed_cached_property(Generic[_T]): - func: Callable[[Any], _T] - attrname: str | None - - def __init__(self, func: Callable[[Any], _T]) -> None: ... - - @overload - def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... - - @overload - def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: ... - - def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self: - raise NotImplementedError() - - def __set_name__(self, owner: type[Any], name: str) -> None: ... - - # __set__ is not defined at runtime, but @cached_property is designed to be settable - def __set__(self, instance: object, value: _T) -> None: ... else: from functools import cached_property as cached_property - - typed_cached_property = cached_property diff --git a/src/dedalus_sdk/_decoding.py b/src/dedalus_sdk/_decoding.py new file mode 100644 index 0000000..2b38a5c --- /dev/null +++ b/src/dedalus_sdk/_decoding.py @@ -0,0 +1,187 @@ +"""Bounded decoding of a response's content codings. + +httpx inflates a whole network read in one call, so with stacked codings (`gzip, gzip`) a few KiB read +can expand to gigabytes before any size limit sees a byte. Buffered bodies and streamed ones (SSE, +JSON Lines) read their bytes through `BoundedDecoder` instead, which inflates gzip and deflate in +steps of at most `INFLATE_STEP` bytes, so every reader can stop at its own limit after each step. +""" + +from __future__ import annotations + +import zlib +from typing import Dict, List, Mapping, Iterator, AsyncIterator, cast + +import httpx + +from ._exceptions import APIError + +# The most bytes one inflate step produces. +INFLATE_STEP = 64 * 1024 + +# The content codings `BoundedDecoder` undoes itself, with the zlib window bits that read each one. +ZLIB_CODINGS: Dict[str, int] = { + "gzip": zlib.MAX_WBITS | 16, + "x-gzip": zlib.MAX_WBITS | 16, + "deflate": zlib.MAX_WBITS, +} + +# Codings only httpx can undo, each through an optional package and without bounding the output, so a +# body that uses one is refused. httpx skips one whose package is missing, and so does `BoundedDecoder`. +HTTPX_ONLY_CODINGS = frozenset({"br", "zstd"}) + + +class CodingRefused(Exception): + """A success body coded br or zstd, which no limit could bound; the caller reports it as an SDK error.""" + + def __init__(self, message: str) -> None: + super().__init__(message) + self.message = message + + +class Inflater: + """One gzip or deflate layer, inflated at most `INFLATE_STEP` bytes per step. + + A deflate body should be zlib-wrapped, but some servers send it raw; as httpx does, a stream whose + first bytes are not a zlib header is read again as raw deflate. + """ + + def __init__(self, wbits: int) -> None: + self._wbits = wbits + self._decompressor = zlib.decompressobj(wbits) + self._started = False + + def feed(self, data: bytes) -> Iterator[bytes]: + """Inflates `data`, yielding the output in pieces of at most `INFLATE_STEP` bytes.""" + while not self._decompressor.eof: + try: + out = self._decompressor.decompress(data, INFLATE_STEP) + except zlib.error: + if self._started or self._wbits != zlib.MAX_WBITS: + raise + self._wbits = -zlib.MAX_WBITS + self._decompressor = zlib.decompressobj(self._wbits) + continue + self._started = True + # Input past the step stays in `unconsumed_tail`; an empty step with nothing left means + # zlib needs more input. + data = self._decompressor.unconsumed_tail + if out: + yield out + elif not data: + break + + def flush(self) -> Iterator[bytes]: + """Whatever the layer still holds once the body has ended; `feed` has drained it to a few bytes.""" + out = self._decompressor.flush() + if out: + yield out + + +def httpx_decoders() -> Mapping[str, object]: + """The content codings the installed httpx decodes; br and zstd appear only with their optional packages.""" + try: + from httpx._decoders import SUPPORTED_DECODERS # pyright: ignore[reportPrivateUsage] + except ImportError: + return {} + return cast("Mapping[str, object]", SUPPORTED_DECODERS) + + +class BoundedDecoder: + """Undoes a response's gzip and deflate content codings, stacked ones included, in bounded steps. + + Each layer hands the next a piece of at most `INFLATE_STEP` bytes, so a reader can stop at its + limit after every step instead of after httpx has inflated a whole network read at once. + """ + + def __init__(self, layers: List[Inflater]) -> None: + self._layers = layers + + @classmethod + def for_response(cls, response: httpx.Response, *, capped: bool) -> BoundedDecoder | None: + """The decoder for `response`'s `Content-Encoding`, or None when httpx must decode it. + + httpx decodes a body read uncapped and a body it already holds (a transport may hand back a + response read in full): that one has no raw stream left and is served from memory. Codings + are undone last-applied first. `identity` and codings httpx does not know pass through + unchanged, as they do in httpx. A br or zstd coding httpx would decode raises `CodingRefused` + on a success; an error body with one is read raw. + """ + if hasattr(response, "_content") or not capped: + # An uncapped body (a successful binary download) is the caller's to bound, so httpx + # decodes it as it would. + return None + codings = [coding.strip().lower() for coding in response.headers.get("content-encoding", "").split(",")] + if any(coding in HTTPX_ONLY_CODINGS and coding in httpx_decoders() for coding in codings): + # httpx inflates br and zstd a whole network read per call, which no limit can stop, and the + # SDK never asks for them (`Accept-Encoding: gzip, deflate`). + if not response.is_success: + # An error body is read for its message only: keep its raw bytes, so the status error + # (and its retry) still stands. + return cls([]) + raise CodingRefused( + "A br or zstd content coding is not decoded for a buffered or streamed response; " + "the SDK accepts gzip and deflate" + ) + return cls([Inflater(ZLIB_CODINGS[coding]) for coding in reversed(codings) if coding in ZLIB_CODINGS]) + + def decode(self, data: bytes) -> Iterator[bytes]: + """Decodes one network read, yielding the body in bounded pieces.""" + return self._push(0, data) + + def flush(self) -> Iterator[bytes]: + """Decodes what the layers still hold once the body has ended.""" + for index, layer in enumerate(self._layers): + for piece in layer.flush(): + yield from self._push(index + 1, piece) + + def _push(self, index: int, data: bytes) -> Iterator[bytes]: + """Runs `data` through the layers from `index` on.""" + if index == len(self._layers): + yield data + return + for piece in self._layers[index].feed(data): + yield from self._push(index + 1, piece) + + +def decoding_error(response: httpx.Response, exc: zlib.error) -> httpx.DecodingError: + """The error for a body that is not valid for its `Content-Encoding`, as httpx raises it.""" + return httpx.DecodingError(f"Error decoding the response body: {exc}", request=response.request) + + +def _stream_decoder(response: httpx.Response) -> BoundedDecoder | None: + """The bounded decoder for a streamed body, reporting a br or zstd one as an `APIError`.""" + try: + return BoundedDecoder.for_response(response, capped=True) + except CodingRefused as exc: + raise APIError(exc.message, response.request, body=None) from None + + +def iter_decoded(response: httpx.Response) -> Iterator[bytes]: + """A streamed body's bytes, decoded in bounded pieces as they arrive (SSE and JSON Lines read this).""" + decoder = _stream_decoder(response) + if decoder is None: + yield from response.iter_bytes() + return + try: + for raw in response.iter_raw(): + yield from decoder.decode(raw) + yield from decoder.flush() + except zlib.error as exc: + raise decoding_error(response, exc) from exc + + +async def aiter_decoded(response: httpx.Response) -> AsyncIterator[bytes]: + """The async twin of `iter_decoded`.""" + decoder = _stream_decoder(response) + if decoder is None: + async for chunk in response.aiter_bytes(): + yield chunk + return + try: + async for raw in response.aiter_raw(): + for piece in decoder.decode(raw): + yield piece + for piece in decoder.flush(): + yield piece + except zlib.error as exc: + raise decoding_error(response, exc) from exc diff --git a/src/dedalus_sdk/_models.py b/src/dedalus_sdk/_models.py index ce728eb..d385cd4 100644 --- a/src/dedalus_sdk/_models.py +++ b/src/dedalus_sdk/_models.py @@ -237,6 +237,12 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] _extra = {} for key, value in values.items(): if key not in model_fields: + if PYDANTIC_V1 and hasattr(__cls, key): + # Pydantic v1 keeps an extra key in the instance `__dict__`, which outranks the + # class: a response key named `json`, `dict` or `to_dict` would replace that method + # on this object with the server's value. Such a key is dropped instead; v2 keeps + # extras in `__pydantic_extra__`, which only answers names the class lacks. + continue parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value if PYDANTIC_V1: @@ -251,10 +257,11 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] # init_private_attributes() does not exist in v2 m._init_private_attributes() # type: ignore - # copied from Pydantic v1's `construct()` method + # Record which fields were set, under the attribute Pydantic v1 reads them from. object.__setattr__(m, "__fields_set__", _fields_set) else: - # these properties are copied from Pydantic's `model_construct()` method + # Pydantic v2 reads private, extra and set fields from these attributes; a model built + # without validation has to set all three itself. object.__setattr__(m, "__pydantic_private__", None) object.__setattr__(m, "__pydantic_extra__", _extra) object.__setattr__(m, "__pydantic_fields_set__", _fields_set) @@ -291,33 +298,26 @@ def model_dump( fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False, ) -> dict[str, Any]: - """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump + """Returns the model's fields as a dictionary, the Pydantic v2 `model_dump` call on Pydantic v1. - Generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + See https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_dump for the + full semantics. Args: - mode: The mode in which `to_python` should run. - If mode is 'json', the output will only contain JSON serializable types. - If mode is 'python', the output may contain non-JSON-serializable Python objects. - include: A set of fields to include in the output. - exclude: A set of fields to exclude from the output. - context: Additional context to pass to the serializer. - by_alias: Whether to use the field's alias in the dictionary key if defined. - exclude_unset: Whether to exclude fields that have not been explicitly set. - exclude_defaults: Whether to exclude fields that are set to their default value. - exclude_none: Whether to exclude fields that have a value of `None`. - exclude_computed_fields: Whether to exclude computed fields. - While this can be useful for round-tripping, it is usually recommended to use the dedicated - `round_trip` parameter instead. - round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. - warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, - "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. - fallback: A function to call when an unknown value is encountered. If not provided, - a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. - serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. - - Returns: - A dictionary representation of the model. + mode: `"python"` keeps Python objects such as `datetime`; `"json"` converts every value + to a JSON-compatible type. + include: The fields to keep, as a set or a nested mapping. + exclude: The fields to drop, as a set or a nested mapping. + by_alias: Key the output by each field's alias rather than its attribute name. + exclude_unset: Drop fields that were never set explicitly. + exclude_defaults: Drop fields still equal to their default. + exclude_none: Drop fields whose value is `None`. + context: Pydantic v2 only; must keep its default, or this raises `ValueError`. + exclude_computed_fields: Pydantic v2 only; must keep its default, or this raises `ValueError`. + round_trip: Pydantic v2 only; must keep its default, or this raises `ValueError`. + warnings: Pydantic v2 only; must keep its default, or this raises `ValueError`. + fallback: Pydantic v2 only; must keep its default, or this raises `ValueError`. + serialize_as_any: Pydantic v2 only; must keep its default, or this raises `ValueError`. """ if mode not in {"json", "python"}: raise ValueError("mode must be either 'json' or 'python'") @@ -363,23 +363,26 @@ def model_dump_json( fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False, ) -> str: - """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json + """Returns the model serialized as a JSON string, the Pydantic v2 `model_dump_json` call on Pydantic v1. - Generates a JSON representation of the model using Pydantic's `to_json` method. + See https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_dump_json + for the full semantics. Args: - indent: Indentation to use in the JSON output. If None is passed, the output will be compact. - include: Field(s) to include in the JSON output. Can take either a string or set of strings. - exclude: Field(s) to exclude from the JSON output. Can take either a string or set of strings. - by_alias: Whether to serialize using field aliases. - exclude_unset: Whether to exclude fields that have not been explicitly set. - exclude_defaults: Whether to exclude fields that have the default value. - exclude_none: Whether to exclude fields that have a value of `None`. - round_trip: Whether to use serialization/deserialization between JSON and class instance. - warnings: Whether to show any warnings that occurred during serialization. - - Returns: - A JSON string representation of the model. + indent: Spaces to indent nested values by; `None` writes compact JSON. + include: The fields to keep, as a set or a nested mapping. + exclude: The fields to drop, as a set or a nested mapping. + by_alias: Key the output by each field's alias rather than its attribute name. + exclude_unset: Drop fields that were never set explicitly. + exclude_defaults: Drop fields still equal to their default. + exclude_none: Drop fields whose value is `None`. + ensure_ascii: Pydantic v2 only; must keep its default, or this raises `ValueError`. + context: Pydantic v2 only; must keep its default, or this raises `ValueError`. + exclude_computed_fields: Pydantic v2 only; must keep its default, or this raises `ValueError`. + round_trip: Pydantic v2 only; must keep its default, or this raises `ValueError`. + warnings: Pydantic v2 only; must keep its default, or this raises `ValueError`. + fallback: Pydantic v2 only; must keep its default, or this raises `ValueError`. + serialize_as_any: Pydantic v2 only; must keep its default, or this raises `ValueError`. """ if round_trip != False: raise ValueError("round_trip is only supported in Pydantic v2") @@ -871,9 +874,12 @@ def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]: return RootModel[type_] # type: ignore -class SecurityOptions(TypedDict, total=False): - api_key_auth: bool - bearer_auth: bool +# An operation's own security requirements, by security scheme name: alternatives (any one +# suffices), each the schemes sent together. The client sends only the schemes these name: the +# first alternative whose credentials are all configured, else the configured part of the first +# alternative that has one. An empty list sends no credential. `None` is an operation that inherits +# the client's default and is sent every configured credential. +SecurityOptions: TypeAlias = List[List[str]] class FinalRequestOptionsInput(TypedDict, total=False): @@ -904,10 +910,7 @@ class FinalRequestOptions(pydantic.BaseModel): idempotency_key: Union[str, None] = None post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None - security: SecurityOptions = { - "api_key_auth": True, - "bearer_auth": True, - } + security: Union[SecurityOptions, None] = None content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override diff --git a/src/dedalus_sdk/_qs.py b/src/dedalus_sdk/_qs.py index 4127c19..7ab0802 100644 --- a/src/dedalus_sdk/_qs.py +++ b/src/dedalus_sdk/_qs.py @@ -118,13 +118,11 @@ def _stringify_item( return [(key, serialised)] def _primitive_value_to_str(self, value: PrimitiveData) -> str: - # copied from httpx - if value is True: - return "true" - elif value is False: - return "false" - elif value is None: + """Renders one query value: booleans as lowercase `true` / `false`, and `None` as empty.""" + if value is None: return "" + if isinstance(value, bool): + return "true" if value else "false" return str(value) diff --git a/src/dedalus_sdk/_streaming.py b/src/dedalus_sdk/_streaming.py index 172e796..e797d00 100644 --- a/src/dedalus_sdk/_streaming.py +++ b/src/dedalus_sdk/_streaming.py @@ -1,6 +1,7 @@ -# Note: initially copied from https://github.com/florimondmanca/httpx-sse/blob/master/src/httpx_sse/_decoders.py from __future__ import annotations +import re +import sys import json import inspect from types import TracebackType @@ -10,6 +11,8 @@ import httpx from ._utils import extract_type_var_from_base +from ._decoding import iter_decoded, aiter_decoded +from ._exceptions import DedalusError if TYPE_CHECKING: from ._client import Dedalus, AsyncDedalus @@ -49,7 +52,7 @@ def __iter__(self) -> Iterator[_T]: yield item def _iter_events(self) -> Iterator[ServerSentEvent]: - yield from self._decoder.iter_bytes(self.response.iter_bytes()) + yield from self._decoder.iter_bytes(iter_decoded(self.response)) def __stream__(self) -> Iterator[_T]: cast_to = cast(Any, self._cast_to) @@ -120,7 +123,7 @@ async def __aiter__(self) -> AsyncIterator[_T]: yield item async def _iter_events(self) -> AsyncIterator[ServerSentEvent]: - async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()): + async for sse in self._decoder.aiter_bytes(aiter_decoded(self.response)): yield sse async def __stream__(self) -> AsyncIterator[_T]: @@ -203,110 +206,169 @@ def __repr__(self) -> str: return f"ServerSentEvent(event={self.event}, data={self.data}, id={self.id}, retry={self.retry})" +_LINE_TERMINATOR = re.compile(rb"\r\n|\r|\n") +_UTF8_BOM = b"\xef\xbb\xbf" + +# The most bytes one line still waiting for its terminator, or one event still waiting for the blank +# line that dispatches it, may hold. A server that never ends a line or an event would otherwise +# make the decoder buffer without bound. +MAX_PENDING_BYTES = 64 * 1024 * 1024 + +# What one pending `data` line costs besides the string itself: its slot in the list of lines. +_LIST_SLOT_BYTES = 8 + + +def _pending_too_large(what: str) -> DedalusError: + """The error for a stream whose pending `what` grew past `MAX_PENDING_BYTES`.""" + return DedalusError( + f"Server-sent event stream {what} exceeds the {MAX_PENDING_BYTES} byte limit " + f"({MAX_PENDING_BYTES // (1024 * 1024)} MiB)" + ) + + class SSEDecoder: - _data: list[str] - _event: str | None - _retry: int | None - _last_event_id: str | None + """Incremental decoder for a `text/event-stream` body. + + Follows "Interpreting an event stream" in the WHATWG HTML standard: + https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation + + Bytes arrive in arbitrary network chunks, so an unfinished line is buffered until its line + terminator arrives. As the standard requires, a byte order mark opening the stream is skipped, + bytes that are not valid UTF-8 decode to U+FFFD, and an event still pending when the stream ends + (it never saw the blank line that dispatches it) is dropped. + """ def __init__(self) -> None: - self._event = None - self._data = [] - self._last_event_id = None - self._retry = None + self._event: str | None = None + self._data: list[str] = [] + # Characters held in `_data`, kept beside it so the event cap costs nothing per line. + self._data_size = 0 + self._last_event_id: str | None = None + self._retry: int | None = None + # The pieces of a line whose terminator has not arrived yet, joined once it does so a long + # line arriving in many chunks is copied once rather than once per chunk. + self._pending: list[bytes] = [] + # Bytes held in `_pending`, kept beside it so the line cap costs nothing per chunk. + self._pending_size = 0 + # Set when a chunk ended on `\r`: a `\n` opening the next chunk completes that `\r\n`, so it + # must not be read as a second, empty line (which would dispatch an event). + self._skip_leading_lf = False + # The opening bytes of the stream, held until there are enough to tell whether they are a + # byte order mark (which can itself arrive split across chunks); `None` once that is settled. + self._head: bytes | None = b"" def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: - """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" - for chunk in self._iter_chunks(iterator): - # Split before decoding so splitlines() only uses \r and \n - for raw_line in chunk.splitlines(): - line = raw_line.decode("utf-8") - sse = self.decode(line) - if sse: - yield sse - - def _iter_chunks(self, iterator: Iterator[bytes]) -> Iterator[bytes]: - """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" - data = b"" + """Decodes raw body chunks and yields each event as soon as its blank line arrives.""" for chunk in iterator: - for line in chunk.splitlines(keepends=True): - data += line - if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): - yield data - data = b"" - if data: - yield data + yield from self._feed(chunk) async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: - """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" - async for chunk in self._aiter_chunks(iterator): - # Split before decoding so splitlines() only uses \r and \n - for raw_line in chunk.splitlines(): - line = raw_line.decode("utf-8") - sse = self.decode(line) - if sse: - yield sse - - async def _aiter_chunks(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[bytes]: - """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" - data = b"" + """Decodes raw async body chunks and yields each event as soon as its blank line arrives.""" async for chunk in iterator: - for line in chunk.splitlines(keepends=True): - data += line - if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): - yield data - data = b"" - if data: - yield data + for sse in self._feed(chunk): + yield sse + + def _feed(self, chunk: bytes) -> Iterator[ServerSentEvent]: + """Consumes one chunk and yields the events completed by the lines it finishes.""" + if not chunk: + return + if self._head is not None: + chunk = self._head + chunk + if len(chunk) < len(_UTF8_BOM) and _UTF8_BOM.startswith(chunk): + self._head = chunk + return + self._head = None + if chunk.startswith(_UTF8_BOM): + chunk = chunk[len(_UTF8_BOM) :] + if not chunk: + return + if self._skip_leading_lf and chunk.startswith(b"\n"): + chunk = chunk[1:] + self._skip_leading_lf = chunk.endswith(b"\r") + + # Lines are split on the raw bytes, which no UTF-8 continuation byte can be mistaken for, + # and each complete line is decoded on its own so a character split across chunks waits + # in `_pending` with the rest of its line. Only the new chunk is scanned: `_pending` holds + # no terminator, and a `\r\n` split across chunks was handled above. + *finished, unfinished = _LINE_TERMINATOR.split(chunk) + if not finished: + self._hold(unfinished) + return + if self._pending_size + len(finished[0]) > MAX_PENDING_BYTES: + raise _pending_too_large("line") + lines = [b"".join([*self._pending, finished[0]]), *finished[1:]] + self._pending = [] + self._pending_size = 0 + self._hold(unfinished) + for line in lines: + sse = self.decode(line.decode("utf-8", errors="replace")) + if sse is not None: + yield sse + + def _hold(self, piece: bytes) -> None: + """Keeps `piece`, the start of a line whose terminator has not arrived, within the line cap.""" + if not piece: + return + self._pending_size += len(piece) + if self._pending_size > MAX_PENDING_BYTES: + raise _pending_too_large("line") + self._pending.append(piece) def decode(self, line: str) -> ServerSentEvent | None: - # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 - + """Processes one line, without its terminator, and returns the event a blank line dispatches.""" if not line: - if not self._event and not self._data and not self._last_event_id and self._retry is None: - return None - - sse = ServerSentEvent( - event=self._event, - data="\n".join(self._data), - id=self._last_event_id, - retry=self._retry, - ) - - # NOTE: as per the SSE spec, do not reset last_event_id. - self._event = None - self._data = [] - self._retry = None - - return sse + return self._dispatch() if line.startswith(":"): + # A comment, such as a keep-alive. return None - fieldname, _, value = line.partition(":") - + # A line with no colon names a field whose value is empty. + name, _, value = line.partition(":") if value.startswith(" "): value = value[1:] - if fieldname == "event": - self._event = value - elif fieldname == "data": + if name == "data": + # Each `data` line is held as its own string in a list until the event ends, so it is + # charged what that costs in memory: the string's own size (up to 4 bytes a character + # for astral text, plus its header) and its list slot. An empty line is not free. + self._data_size += sys.getsizeof(value) + _LIST_SLOT_BYTES + if self._data_size > MAX_PENDING_BYTES: + raise _pending_too_large("event") self._data.append(value) - elif fieldname == "id": - if "\0" in value: - pass - else: + elif name == "event": + self._event = value + elif name == "id": + # The standard ignores an id containing NUL rather than truncating it. + if "\0" not in value: self._last_event_id = value - elif fieldname == "retry": - try: + elif name == "retry": + # Only ASCII digits count; `int()` alone would also take a sign, spaces or other scripts' digits. + if value.isascii() and value.isdigit(): self._retry = int(value) - except (TypeError, ValueError): - pass - else: - pass # Field is ignored. - + # Any other field name is ignored. return None + def _dispatch(self) -> ServerSentEvent | None: + """Builds the event a blank line completes, and resets the per-event fields.""" + # An event with no data still dispatches when it set a name, an id or a retry, so a consumer + # sees named events that carry no payload. The last event id persists across events, so + # once one is set every blank line dispatches. + if not (self._data or self._event or self._last_event_id or self._retry is not None): + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + self._event = None + self._data = [] + self._data_size = 0 + self._retry = None + return sse + @runtime_checkable class SSEBytesDecoder(Protocol): diff --git a/src/dedalus_sdk/_types.py b/src/dedalus_sdk/_types.py index 33f4432..accae37 100644 --- a/src/dedalus_sdk/_types.py +++ b/src/dedalus_sdk/_types.py @@ -223,9 +223,14 @@ def get(self, __key: str) -> str | None: ... StrBytesIntFloat = Union[str, bytes, int, float] -# Note: copied from Pydantic -# https://github.com/pydantic/pydantic/blob/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79 -IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]] +# The `include` / `exclude` selection `model_dump` takes: a set of field names (or sequence indexes), +# or a mapping from each one to `True` or a nested selection for that field's own value. +IncEx: TypeAlias = Union[ + Set[str], + Set[int], + Mapping[str, Union["IncEx", bool]], + Mapping[int, Union["IncEx", bool]], +] PostParser = Callable[[Any], Any] diff --git a/src/dedalus_sdk/_utils/_datetime_parse.py b/src/dedalus_sdk/_utils/_datetime_parse.py index 7cb9d9e..3c6b525 100644 --- a/src/dedalus_sdk/_utils/_datetime_parse.py +++ b/src/dedalus_sdk/_utils/_datetime_parse.py @@ -1,136 +1,156 @@ """ -This file contains code from https://github.com/pydantic/pydantic/blob/main/pydantic/v1/datetime_parse.py -without the Pydantic v1 specific errors. +Parsing for the `date` and `date-time` values a response carries. + +Models built without validation (`construct_type`) turn these values into `date` and `datetime` +objects here. A string is read as a date or date-time in the RFC 3339 / ISO 8601 shape, with the +departures `_DATE` and `_DATE_TIME` list; a number, or a string or bytes holding one, is read as a +Unix timestamp. + +`datetime.fromisoformat` is not used: before Python 3.11 it rejects a `Z` offset and fractions that +are not exactly three or six digits, and the supported floor is lower than that. """ from __future__ import annotations import re -from typing import Dict, Union, Optional +from typing import Union, Optional from datetime import date, datetime, timezone, timedelta from .._types import StrBytesIntFloat -date_expr = r"(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})" -time_expr = ( - r"(?P\d{1,2}):(?P\d{1,2})" - r"(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?" - r"(?PZ|[+-]\d{2}(?::?\d{2})?)?$" +# RFC 3339 `full-date`, loosened to accept a one-digit month or day. +_DATE = re.compile(r"(\d{4})-(\d\d?)-(\d\d?)$") + +# RFC 3339 `date-time`, loosened the same way: a space may stand in for `T`, the seconds may be left +# out, and the offset may be `Z`, `+HH`, `+HHMM` or `+HH:MM`, or missing for a local time. It is also +# stricter in two places: `T` and `Z` must be uppercase, and a leap second (`:60`) is rejected, +# because `datetime` cannot hold one. +_DATE_TIME = re.compile( + r""" + (\d{4}) - (\d\d?) - (\d\d?) + [T ] + (\d\d?) : (\d\d?) + (?: : (\d\d?) (?: \. (\d{1,12}) )? )? + (Z | [+-] \d\d (?: :? \d\d )? )? + $ + """, + re.VERBOSE, ) -date_re = re.compile(f"{date_expr}$") -datetime_re = re.compile(f"{date_expr}[T ]{time_expr}") +_UNIX_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) +# A timestamp whose magnitude is above this is taken to be in milliseconds (or a finer unit): as +# seconds it would land after the year 2603. +_SECONDS_LIMIT = 2e10 +# Past this magnitude no unit brings the value inside the range `datetime` can represent. +_CLAMP_LIMIT = 3e20 -EPOCH = datetime(1970, 1, 1) -# if greater than this, the number is in ms, if less than or equal it's in seconds -# (in seconds this is 11th October 2603, in ms it's 20th August 1970) -MS_WATERSHED = int(2e10) -# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9 -MAX_NUMBER = int(3e20) +def _number_or_text(value: object, kind: str) -> Union[int, float, str]: + """Sorts an input into a timestamp (returned as a number) or text to parse as a `kind`. - -def _get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]: + Text that reads as a number, such as `"1700000000"` or even `"2024"`, is a timestamp. Any other + type `float()` accepts, such as `Decimal` or a NumPy scalar, is a timestamp too; everything else + raises `TypeError`. + """ if isinstance(value, (int, float)): return value + if isinstance(value, bytes): + value = value.decode() + if isinstance(value, str): + try: + return float(value) + except ValueError: + return value try: - return float(value) - except ValueError: - return None - except TypeError: - raise TypeError(f"invalid type; expected {native_expected_type}, string, bytes, int or float") from None + return float(value) # type: ignore[arg-type] + except (TypeError, ValueError): + raise TypeError(f"cannot parse a {type(value).__name__} as a {kind}") from None -def _from_unix_seconds(seconds: Union[int, float]) -> datetime: - if seconds > MAX_NUMBER: - return datetime.max - elif seconds < -MAX_NUMBER: - return datetime.min +def _from_timestamp(number: Union[int, float]) -> datetime: + """Converts a Unix timestamp to an aware UTC `datetime`. - while abs(seconds) > MS_WATERSHED: - seconds /= 1000 - dt = EPOCH + timedelta(seconds=seconds) - return dt.replace(tzinfo=timezone.utc) - - -def _parse_timezone(value: Optional[str]) -> Union[None, int, timezone]: - if value == "Z": - return timezone.utc - elif value is not None: - offset_mins = int(value[-2:]) if len(value) > 3 else 0 - offset = 60 * int(value[1:3]) + offset_mins - if value[0] == "-": - offset = -offset - return timezone(timedelta(minutes=offset)) - else: + The unit is inferred from the size: a value too large to be seconds is read as milliseconds, + one too large for that as microseconds, and so on. A value too large for any unit is clamped to + the naive `datetime.max` or `datetime.min`. + """ + magnitude = abs(number) + if magnitude > _CLAMP_LIMIT: + return datetime.max if number > 0 else datetime.min + divisor = 1 + while magnitude > _SECONDS_LIMIT * divisor: + divisor *= 1000 + return _UNIX_EPOCH + timedelta(seconds=number / divisor) + + +def _zone(offset: Optional[str]) -> Optional[timezone]: + """Returns the fixed-offset zone a `Z` or `±HH[[:]MM]` suffix names, or `None` for no suffix.""" + if offset is None: return None + if offset == "Z": + return timezone.utc + digits = offset[1:].replace(":", "") + delta = timedelta(hours=int(digits[:2]), minutes=int(digits[2:] or 0)) + # `timezone` itself rejects an offset of a day or more with a `ValueError`. + return timezone(-delta if offset.startswith("-") else delta) def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: """ - Parse a datetime/int/float/string and return a datetime.datetime. + Parses a date-time string, or a Unix timestamp in seconds or milliseconds, into a `datetime`. - This function supports time zone offsets. When the input contains one, - the output uses a timezone with a fixed offset from UTC. + A string with an offset gives an aware `datetime` with that fixed offset, and one without gives + a naive one. A timestamp gives an aware UTC `datetime`, except that one beyond the representable + range is clamped to the naive `datetime.max` or `datetime.min`. A `datetime` is returned unchanged. - Raise ValueError if the input is well formatted but not a valid datetime. - Raise ValueError if the input isn't well formatted. + Raises `ValueError` for a malformed string or out-of-range fields, and `TypeError` for any other + input type. """ if isinstance(value, datetime): return value - number = _get_numeric(value, "datetime") - if number is not None: - return _from_unix_seconds(number) - - if isinstance(value, bytes): - value = value.decode() - - assert not isinstance(value, (float, int)) + parsed = _number_or_text(value, "datetime") + if not isinstance(parsed, str): + return _from_timestamp(parsed) - match = datetime_re.match(value) + match = _DATE_TIME.match(parsed) if match is None: - raise ValueError("invalid datetime format") - - kw = match.groupdict() - if kw["microsecond"]: - kw["microsecond"] = kw["microsecond"].ljust(6, "0") - - tzinfo = _parse_timezone(kw.pop("tzinfo")) - kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None} - kw_["tzinfo"] = tzinfo - - return datetime(**kw_) # type: ignore + raise ValueError("invalid date-time format") + year, month, day, hour, minute, second, fraction, offset = match.groups() + # Pad the fraction out to microseconds, or cut it down to them. + microsecond = int(f"{fraction or ''}000000"[:6]) + return datetime( + int(year), + int(month), + int(day), + int(hour), + int(minute), + int(second or 0), + microsecond, + tzinfo=_zone(offset), + ) def parse_date(value: Union[date, StrBytesIntFloat]) -> date: """ - Parse a date/int/float/string and return a datetime.date. + Parses a date string, or a Unix timestamp in seconds or milliseconds, into a `date`. + + A `datetime` gives its date part, and a `date` is returned unchanged. - Raise ValueError if the input is well formatted but not a valid date. - Raise ValueError if the input isn't well formatted. + Raises `ValueError` for a malformed string or an impossible date, and `TypeError` for any other + input type. """ + if isinstance(value, datetime): + return value.date() if isinstance(value, date): - if isinstance(value, datetime): - return value.date() - else: - return value - - number = _get_numeric(value, "date") - if number is not None: - return _from_unix_seconds(number).date() + return value - if isinstance(value, bytes): - value = value.decode() + parsed = _number_or_text(value, "date") + if not isinstance(parsed, str): + return _from_timestamp(parsed).date() - assert not isinstance(value, (float, int)) - match = date_re.match(value) + match = _DATE.match(parsed) if match is None: raise ValueError("invalid date format") - - kw = {k: int(v) for k, v in match.groupdict().items()} - - try: - return date(**kw) - except ValueError: - raise ValueError("invalid date format") from None + year, month, day = (int(part) for part in match.groups()) + return date(year, month, day) diff --git a/src/dedalus_sdk/_utils/_logs.py b/src/dedalus_sdk/_utils/_logs.py index 9436776..159ac27 100644 --- a/src/dedalus_sdk/_utils/_logs.py +++ b/src/dedalus_sdk/_utils/_logs.py @@ -1,5 +1,336 @@ +from __future__ import annotations + import os +import re import logging +from typing import Any, cast +from collections.abc import Mapping + +REDACTED = "***" + +# Credential headers, lower-cased: the conventional ones plus every header this SDK's auth schemes +# write (an `apiKey` scheme names its own, which no fixed list could know). +_REDACTED_HEADERS: frozenset[str] = frozenset( + ("authorization", "proxy-authorization", "api-key", "x-api-key", "cookie", "set-cookie") +) + +# Query parameters this SDK sends a credential in, from its `apiKey` schemes placed in the query. +# A credential there is worse off than one in a header: httpx logs every request URL at `info`. +_REDACTED_QUERY: tuple[str, ...] = () + +# The user part of a URL's authority, where a token often rides (`https://user:token@host`). Stops +# at `/`, `?`, `#` and whitespace, so an `@` in a path or query is left alone. +_USERINFO = re.compile(r"(//)[^/?#@\s]+@") + +# How deep `_redact_value` follows nested mappings and sequences before it falls back to masking +# the value's text, so a self-referencing structure cannot recurse without end. +_MAX_DEPTH = 8 + + +def _name_pattern(name: str) -> str: + """A pattern matching one credential query name in every spelling a URL can carry it in. + + Each character as written or percent-encoded, at any depth and in either hex case (`k`, `%6B`, + `%6b`, `%256B`), and a space also as `+`. A non-ASCII character is matched as written or as its + UTF-8 bytes encoded. A lone surrogate has no UTF-8 bytes, so it is matched only as written: + this runs at import time, and a spec can name a scheme that way. Only the hex ignores case, not + the name. + """ + parts: list[str] = [] + for char in name: + try: + utf8 = char.encode("utf-8") + except UnicodeEncodeError: + utf8 = b"" + encoded = "".join(f"%(?:25)*(?i:{byte:02x})" for byte in utf8) + alternatives = f"|{encoded}" if encoded else "" + plus = "|\\+" if char == " " else "" + parts.append(f"(?:{re.escape(char)}{alternatives}{plus})") + return "".join(parts) + + +def _query_pattern() -> re.Pattern[str] | None: + """The pattern behind `redact()`'s query masking, or `None` when no credential is placed in the query.""" + names = [_name_pattern(name) for name in _REDACTED_QUERY if name] + if not names: + return None + # A name counts after a separator, `?`, `&` or `;`, either literal or percent-encoded any number + # of times over (`%3F`, `%253F`), since a server can echo a whole URL encoded inside another + # parameter's value, as a redirect's `return_to` does. `;` also counts as a separator; masking an + # unrelated `;name=` value as well is harmless. The `=` after it can be encoded the same way. The + # value then runs to the next literal `&`, `#` or whitespace: an encoded `%26` may end a nested + # value, but stopping there would leak the rest of a value a sloppy encoder left half-encoded, + # and masking too much is harmless. `<`, `>` and `"` end it too, since no valid URL carries them + # unencoded, so a `Link` header's `` keeps its closing delimiter. `'` does not: a query value + # can hold it raw. A JSON-escaped `\u0026` (hex digits in either case) separates too, since + # `encoding/json` and others write `&` that way in a URL quoted inside a JSON body. No quantifier + # nests inside another, so matching stays linear on hostile input. + return re.compile( + r"((?:[?&;]|%(?:25)*(?i:3f|26|3b)|\\(?i:u0026))(?:" + "|".join(names) + r")(?:=|%(?:25)*(?i:3d)))[^&#\s<>\"]*" + ) + + +_QUERY = _query_pattern() + +# A header as httpcore's trace logging prints it, `(b'set-cookie', b'session=...')`: the response +# headers of every attempt go out at DEBUG as a list of these byte-string pairs, in whichever quote +# style each bytes repr picks. Matched pair by pair so a value is masked inside its own quotes and the +# trace keeps its `'),` structure. Each quoted part is a run of non-quote, non-backslash characters +# or escapes, so matching stays linear. +_BYTE_PAIR = re.compile(r"""(\(b?['"])([^'"\\]*)(['"],\s*)(b?)(?:'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)")(\))""") + + +def _redact_plain(text: str) -> str: + """Masks URL userinfo and every credential query value in `text`.""" + text = _USERINFO.sub(r"\1" + REDACTED + "@", text) + if _QUERY is None: + return text + return _QUERY.sub(r"\1" + REDACTED, text) + + +def _redact_pair(match: re.Match[str]) -> str: + """One httpcore header pair with its value masked: whole for a credential header, else inside.""" + open_, name, between, prefix, single, double, close = match.groups() + if name.lower() in _REDACTED_HEADERS: + return f"{open_}{name}{between}{prefix}'{REDACTED}'{close}" + if single is not None: + return f"{open_}{name}{between}{prefix}'{_redact_plain(single)}'{close}" + return f'{open_}{name}{between}{prefix}"{_redact_plain(double)}"{close}' + + +def redact(text: str) -> str: + """Masks URL userinfo, credential header pairs and every credential query value in `text`. + + Works on free text rather than a parsed URL, so it covers a URL quoted in an error message, a + `Location` or `Link` header, and a credential nested, percent-encoded, in another parameter's + value (`/login?return_to=%2Fv1%3Fapi_key%3D...`) alike. httpcore's header pairs are masked one + by one and the text between them on its own, so a masked value cannot run past its pair. + """ + parts: list[str] = [] + end = 0 + for match in _BYTE_PAIR.finditer(text): + parts.append(_redact_plain(text[end : match.start()])) + parts.append(_redact_pair(match)) + end = match.end() + parts.append(_redact_plain(text[end:])) + return "".join(parts) + + +def _is_secret_key(key: object) -> bool: + """Whether a mapping key names a credential: a redacted header (any case) or query parameter.""" + return isinstance(key, str) and (key.lower() in _REDACTED_HEADERS or key in _REDACTED_QUERY) + + +def _is_secret_header(name: object) -> bool: + """Whether `name`, as text or bytes (hpack and httpcore hold headers as bytes), is a credential header.""" + if isinstance(name, (bytes, bytearray)): + name = bytes(name).decode("latin-1") + return isinstance(name, str) and name.lower() in _REDACTED_HEADERS + + +def _masked_like(value: object) -> object: + """`REDACTED` in the type `value` had, bytes for bytes, so a `b'...'` repr stays one.""" + return REDACTED.encode("ascii") if isinstance(value, (bytes, bytearray)) else REDACTED + + +def _redact_value(value: object, depth: int = 0) -> object: + """A log argument with every credential in it masked. + + A mapping (response headers, a request's `headers` or `params`) has the values of credential + keys replaced whole, and so does a `(name, value)` pair whose name is a credential header, the + shape hpack logs a header in (`HeaderTuple`, text or bytes). Every string, and every byte string + read as Latin-1, is passed through `redact()`. Numbers keep their type, so a `%i` or `%f` + placeholder still formats. Any other object is replaced by its masked text only when that text + held a credential (an `httpx.URL`, a request's repr), so its own formatting survives otherwise. + """ + if value is None or isinstance(value, (bool, int, float)): + return value + if isinstance(value, str): + return redact(value) + if isinstance(value, (bytes, bytearray)): + text = bytes(value).decode("latin-1") + masked = redact(text) + return value if masked == text else masked.encode("latin-1") + if depth < _MAX_DEPTH: + if isinstance(value, Mapping): + return { + key: REDACTED if _is_secret_key(key) else _redact_value(item, depth + 1) + for key, item in cast("Mapping[Any, Any]", value).items() + } + if isinstance(value, list): + return [_redact_value(item, depth + 1) for item in cast("list[Any]", value)] + if isinstance(value, tuple): + items = cast("tuple[Any, ...]", value) + if len(items) == 2 and _is_secret_header(items[0]): + return (items[0], _masked_like(items[1])) + return tuple(_redact_value(item, depth + 1) for item in items) + text = str(value) + masked = redact(text) + return value if masked == text else masked + + +# The loggers whose records are masked, each with its children: this package's own, and the HTTP +# libraries under it, which log on loggers of their own: the request URL (httpx, at `info`), every +# response header (httpcore, at `debug`), every header an HTTP/2 connection encodes or decodes +# (hpack, at `debug`), and a websocket handshake's request line and headers (websockets, at `debug`). +_REDACTED_LOGGERS: tuple[str, ...] = ("dedalus_sdk", "httpx", "httpcore", "hpack", "websockets") + + +def _redacts(name: object) -> bool: + """Whether a record logged under `name` is one this SDK masks. + + `logging.makeLogRecord` builds its record with no name and fills it in afterwards, so a name + that is not a string is simply not this SDK's. + """ + return isinstance(name, str) and any(name == root or name.startswith(root + ".") for root in _REDACTED_LOGGERS) + + +def _redact_arg(value: object) -> object: + """`_redact_value`, failing closed: an argument that cannot be masked is logged as `REDACTED`.""" + try: + return _redact_value(value) + except Exception: + return REDACTED + + +def _redact_text(text: str) -> str: + """`redact`, failing closed: text that cannot be masked is logged as `REDACTED`.""" + try: + return redact(text) + except Exception: + return REDACTED + + +# The placeholders a line logging one header as two arguments, its name then its value, joins them +# with: websockets' handshake trace (`> %s: %s`), hpack's `Adding %s=%s to the header table` and +# `Evicting %s: %s from the header table`. +_HEADER_LINE_JOINS: tuple[str, ...] = ("%s: %s", "%s=%s") + + +def _redact_header_lines(msg: str, args: tuple[object, ...]) -> tuple[object, ...]: + """Masks the value of a credential header a line logs as a name argument and a value argument. + + The name alone says which value is a secret, so the value is masked whole, in its own type. + """ + if not any(join in msg for join in _HEADER_LINE_JOINS): + return args + masked = list(args) + for index, arg in enumerate(args[:-1]): + if _is_secret_header(arg): + masked[index + 1] = _masked_like(args[index + 1]) + return tuple(masked) + + +def _is_hpack(name: str) -> bool: + """Whether a record comes from hpack's own loggers.""" + return name == "hpack" or name.startswith("hpack.") + + +def _is_byte_string(value: object) -> bool: + """Whether `value` is raw bytes, the way hpack hands it a header block.""" + return isinstance(value, (bytes, bytearray, memoryview)) + + +def _redact_header_blocks(msg: str, args: tuple[object, ...]) -> tuple[object, ...]: + """Masks every byte string an hpack record logs other than one header's name and value. + + `Encoded header block to %s` and `Decoding %s` log a whole encoded header block, each header's + name and value in it as written or Huffman-coded, which is no protection: no pattern can find a + credential in it, so the block is masked whole. A line that logs one header as a name and a + value (`_HEADER_LINE_JOINS`) keeps both, its credential values already masked. + """ + if any(join in msg for join in _HEADER_LINE_JOINS): + return args + return tuple(_masked_like(arg) if _is_byte_string(arg) else arg for arg in args) + + +def _redact_record(record: logging.LogRecord) -> None: + """Masks every credential a log record carries, in place. + + The arguments are masked one by one rather than the formatted message as a whole, so a header + mapping is masked by its keys and a URL argument's masked value ends where the URL does, not at + the next space of the surrounding message. A record with no arguments has its message masked + instead, whatever it is: a logged `httpx.URL` carries the query too. Each part fails closed: one + that cannot be masked is replaced by `REDACTED` rather than logged as it was. + + An attached traceback is formatted and masked into `exc_text`, which `logging.Formatter` uses as + is (httpx quotes the request URL in a status error's message). `exc_info` itself is left alone, + since a handler may need the exception object, so a formatter that reads `exc_info` rather than + `exc_text` still sees the raw traceback. + """ + if isinstance(record.args, Mapping): + mapping = cast("Mapping[Any, Any]", record.args) + record.args = {key: REDACTED if _is_secret_key(key) else _redact_arg(item) for key, item in mapping.items()} + elif isinstance(record.args, tuple) and record.args: + args = tuple(_redact_arg(arg) for arg in record.args) + if isinstance(record.msg, str): + args = _redact_header_lines(record.msg, args) + if _is_hpack(record.name): + args = _redact_header_blocks(record.msg, args) + record.args = args + elif isinstance(record.msg, str): + record.msg = _redact_text(record.msg) + else: + record.msg = _redact_arg(record.msg) + if record.exc_info and not record.exc_text: + try: + record.exc_text = redact(logging.Formatter().formatException(record.exc_info)) + except Exception: + record.exc_text = REDACTED + if isinstance(record.stack_info, str): + record.stack_info = _redact_text(record.stack_info) + + +def _blank_record(record: logging.LogRecord) -> None: + """Replaces everything a record could print with `REDACTED`, for one that could not be masked. + + The traceback goes too, `exc_info` as well as `exc_text`, since a handler that formats the + exception itself would otherwise print it raw. + """ + try: + had_traceback = bool(record.exc_info or record.exc_text) + record.msg = REDACTED + record.args = () + record.exc_info = None + record.exc_text = REDACTED if had_traceback else None + record.stack_info = None + except Exception: + return + + +def _install_record_factory() -> None: + """Wraps the process's log record factory so this SDK's records are masked as they are made. + + A record factory rather than a logger filter, because a logger's filters see only the records + logged on that logger itself: every runtime module logs on a child (`._base_client`), + and httpx, httpcore and websockets on children of their own. Masking at creation also comes + before every filter and handler, whoever configured them. The factory already installed is + wrapped, not replaced, so another library's factory keeps working, and records of every other + logger pass through untouched. A factory installed later that does not chain to this one turns + the masking off. + """ + previous = logging.getLogRecordFactory() + + def factory(*args: Any, **kwargs: Any) -> logging.LogRecord: + """The previous factory's record, masked when it belongs to one of `_REDACTED_LOGGERS`. + + Nothing here may raise: the factory runs inside every `log.debug(...)` in the process, so + a failure would break the caller's code rather than its logging. + """ + record = previous(*args, **kwargs) + try: + if _redacts(record.name): + _redact_record(record) + except Exception: + # Every part of `_redact_record` already fails closed; this is the last line, for a + # record a part of which cannot even be read (a mapping whose `.items()` raises). + _blank_record(record) + return record + + logging.setLogRecordFactory(factory) + + +_install_record_factory() logger: logging.Logger = logging.getLogger("dedalus_sdk") httpx_logger: logging.Logger = logging.getLogger("httpx") 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 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 diff --git a/src/dedalus_sdk/images.py b/src/dedalus_sdk/images.py new file mode 100644 index 0000000..0872a06 --- /dev/null +++ b/src/dedalus_sdk/images.py @@ -0,0 +1,326 @@ +"""Image import workflows using the SDK transport and a separate signed-upload client.""" + +from __future__ import annotations + +import base64 +import hashlib +import math +import os +import stat +import time +from datetime import datetime, timezone +from typing import BinaryIO, Callable, Literal, Optional, Union, cast +from urllib.parse import parse_qs, quote, urlsplit +from uuid import uuid4 + +import anyio +import httpx +from typing_extensions import TypedDict + +from . import APITimeoutError, AsyncDedalus, Dedalus + + +class Image(TypedDict): + image_id: str + state: str + + +class ImagePreparationError(RuntimeError): + def __init__(self, image: Image, message: str) -> None: + self.image = image + super().__init__(f"Image {image['image_id']}: {message}") + + +def _record(value: object) -> dict[str, object]: + if not isinstance(value, dict): + raise ValueError("Invalid image API response") + return cast("dict[str, object]", value) + + +def _image(value: object, expected: Optional[str] = None) -> Image: + result = _record(value) + if ( + not isinstance(result.get("image_id"), str) + or not result["image_id"] + or not isinstance(result.get("state"), str) + or not result["state"] + or (expected is not None and result["image_id"] != expected) + ): + raise ValueError("Invalid or mismatched image API response") + return cast(Image, result) + + +def _ready(image: Image, progress: Optional[Callable[[Image], None]]) -> bool: + if progress: + progress(image) + if image["state"] in ("failed", "rejected", "revoked"): + details = _record(image) + error = _record(details.get("error") or {}) + message = error.get("message") or details.get("reason") or image["state"] + code = f"{error['error_code']}: " if error.get("error_code") else "" + raise ImagePreparationError(image, f"{code}{message}") + return image["state"] == "ready" + + +def _duration(value: float, name: str) -> float: + if not math.isfinite(value) or not 0 < value <= 86400: + raise ValueError(f"{name} must be between 0 and 86400 seconds") + return value + + +def _remaining(end: float, image: Optional[Image] = None) -> float: + remaining = end - time.monotonic() + if remaining <= 0: + if image is not None: + raise ImagePreparationError( + image, "Timed out waiting. Preparation continues; use this image ID to resume polling." + ) + raise TimeoutError("Image import timed out before receiving an image ID") + return remaining + + +def _identity(image: Union[str, Image]) -> str: + value = image if isinstance(image, str) else image["image_id"] + if not value or any(char.isspace() for char in value): + raise ValueError("image ID must be a nonempty string without whitespace") + return value + + +def poll_image( + client: Dedalus, + image: Union[str, Image], + *, + timeout: float = 3600, + poll_interval: float = 2, + on_progress: Optional[Callable[[Image], None]] = None, +) -> Image: + """Wait for readiness. Timeout stops polling, never server preparation.""" + end = time.monotonic() + _duration(timeout, "timeout") + interval = _duration(poll_interval, "poll_interval") + image_id = _identity(image) + current = None if isinstance(image, str) else _image(image, image_id) + while True: + remaining = _remaining(end, current or Image(image_id=image_id, state="unknown")) + if current is None: + try: + response = client.get( + f"/v1/images/{quote(image_id, safe='')}", + cast_to=httpx.Response, + options={"timeout": remaining, "max_retries": 0}, + ) + except APITimeoutError as error: + raise ImagePreparationError( + Image(image_id=image_id, state="unknown"), + "Status request timed out. Preparation continues; use this image ID to resume polling.", + ) from error + current = _image(response.json(), image_id) + _remaining(end, current) + if _ready(current, on_progress): + return current + time.sleep(min(interval, _remaining(end, current))) + _remaining(end, current) + current = None + + +async def async_poll_image( + client: AsyncDedalus, + image: Union[str, Image], + *, + timeout: float = 3600, + poll_interval: float = 2, + on_progress: Optional[Callable[[Image], None]] = None, +) -> Image: + """Async readiness polling; task cancellation leaves preparation running.""" + image_id = _identity(image) + interval = _duration(poll_interval, "poll_interval") + current = None if isinstance(image, str) else _image(image, image_id) + try: + with anyio.fail_after(_duration(timeout, "timeout")): + while True: + if current is None: + response = await client.get(f"/v1/images/{quote(image_id, safe='')}", cast_to=httpx.Response) + current = _image(response.json(), image_id) + if _ready(current, on_progress): + return current + await anyio.sleep(interval) + current = None + except (TimeoutError, APITimeoutError) as error: + raise ImagePreparationError( + Image(image_id=image_id, state="unknown"), + "Timed out waiting. Preparation continues; use this image ID to resume polling.", + ) from error + + +def _hash(file: BinaryIO, end: float) -> tuple[str, int]: + before = os.fstat(file.fileno()) + if not stat.S_ISREG(before.st_mode) or not 0 < before.st_size <= 5 * 1024**3: + raise ValueError("Image archive must be a regular file between 1 byte and 5 GiB") + digest = hashlib.sha256() + count = 0 + while True: + _remaining(end) + chunk = file.read(1024 * 1024) + if not chunk: + break + digest.update(chunk) + count += len(chunk) + after = os.fstat(file.fileno()) + if count != before.st_size or after.st_size != before.st_size or after.st_mtime_ns != before.st_mtime_ns: + raise ValueError("Image archive changed while hashing") + file.seek(0) + return digest.hexdigest(), count + + +def _capability(value: object, sha256: str, size: int) -> tuple[dict[str, object], str, httpx.Headers]: + reservation = _record(value) + source, upload = _record(reservation.get("source")), _record(reservation.get("upload")) + try: + url = upload["url"] + if not isinstance(url, str) or not isinstance(upload["expires_at"], str): + raise ValueError() + endpoint = urlsplit(url) + expires = datetime.fromisoformat(upload["expires_at"].replace("Z", "+00:00")) + headers = httpx.Headers() + for key, values in _record(upload.get("headers")).items(): + if key.lower() in ("authorization", "proxy-authorization", "cookie") or not isinstance(values, list): + raise ValueError() + if not all(isinstance(value, str) for value in values): + raise ValueError() + headers[key] = ", ".join(values) + checksum = ( + headers.get("x-amz-checksum-sha256") or parse_qs(endpoint.query).get("X-Amz-Checksum-Sha256", [""])[0] + ) + if ( + source.get("kind") != "docker_archive" + or source.get("sha256") != sha256 + or not isinstance(source.get("upload_id"), str) + or not source["upload_id"] + or upload.get("method") != "PUT" + or endpoint.scheme != "https" + or not endpoint.hostname + or endpoint.username + or endpoint.password + or endpoint.fragment + or expires <= datetime.now(timezone.utc) + or headers.get("content-length") != str(size) + or headers.get("if-none-match") != "*" + or checksum != base64.b64encode(bytes.fromhex(sha256)).decode() + ): + raise ValueError() + return source, url, headers + except (KeyError, ValueError, TypeError): + raise ValueError("Invalid image upload capability") from None + + +def _uploaded(response: httpx.Response) -> None: + # Registration verifies an existing immutable object after a repeated PUT. + if not response.is_success and response.status_code != 412: + raise RuntimeError(f"Image archive upload failed: HTTP {response.status_code}") + + +def import_image( + client: Dedalus, + path: Union[str, os.PathLike[str]], + *, + wait: bool = True, + name: Optional[str] = None, + platform: Literal["linux/amd64"] = "linux/amd64", + timeout: float = 3600, + poll_interval: float = 2, + idempotency_key: Optional[str] = None, + on_progress: Optional[Callable[[Image], None]] = None, + upload_transport: Optional[httpx.BaseTransport] = None, +) -> Image: + """Stream a docker-save archive, register it, and wait by default. + + The sync deadline is checked between I/O operations; each request has a + remaining-budget inactivity timeout and automatic API retries are disabled. + """ + if platform != "linux/amd64": + raise ValueError("Custom images currently require linux/amd64") + end = time.monotonic() + _duration(timeout, "timeout") + _duration(poll_interval, "poll_interval") + key = idempotency_key or str(uuid4()) + with open(path, "rb") as file: + sha256, size = _hash(file, end) + response = client.post( + "/v1/images/uploads", + cast_to=httpx.Response, + body={"sha256": sha256, "size_bytes": size, "platform": platform}, + options={"idempotency_key": key, "timeout": _remaining(end), "max_retries": 0}, + ) + source, url, headers = _capability(response.json(), sha256, size) + with httpx.Client(transport=upload_transport, follow_redirects=False) as storage: + _uploaded(storage.put(url, content=file, headers=headers, timeout=_remaining(end))) + response = client.post( + "/v1/images", + cast_to=httpx.Response, + body={"source": source, "platform": platform, **({"name": name} if name is not None else {})}, + options={"idempotency_key": key, "timeout": _remaining(end), "max_retries": 0}, + ) + image = _image(response.json()) + if not wait: + _ready(image, on_progress) + return image + return poll_image( + client, image, timeout=_remaining(end, image), poll_interval=poll_interval, on_progress=on_progress + ) + + +async def async_import_image( + client: AsyncDedalus, + path: Union[str, os.PathLike[str]], + *, + wait: bool = True, + name: Optional[str] = None, + platform: Literal["linux/amd64"] = "linux/amd64", + timeout: float = 3600, + poll_interval: float = 2, + idempotency_key: Optional[str] = None, + on_progress: Optional[Callable[[Image], None]] = None, + upload_transport: Optional[httpx.AsyncBaseTransport] = None, +) -> Image: + """Async import with automatic waiting; cancellation never deletes an image.""" + if platform != "linux/amd64": + raise ValueError("Custom images currently require linux/amd64") + end = time.monotonic() + _duration(timeout, "timeout") + _duration(poll_interval, "poll_interval") + key = idempotency_key or str(uuid4()) + image = None + try: + with anyio.fail_after(timeout): + with open(path, "rb") as file: + sha256, size = await anyio.to_thread.run_sync(_hash, file, end) + response = await client.post( + "/v1/images/uploads", + cast_to=httpx.Response, + body={"sha256": sha256, "size_bytes": size, "platform": platform}, + options={"idempotency_key": key}, + ) + source, url, headers = _capability(response.json(), sha256, size) + + async def chunks(): + while True: + chunk = await anyio.to_thread.run_sync(file.read, 1024 * 1024) + if not chunk: + break + yield chunk + + async with httpx.AsyncClient(transport=upload_transport, follow_redirects=False) as storage: + _uploaded(await storage.put(url, content=chunks(), headers=headers, timeout=_remaining(end))) + response = await client.post( + "/v1/images", + cast_to=httpx.Response, + body={"source": source, "platform": platform, **({"name": name} if name is not None else {})}, + options={"idempotency_key": key}, + ) + image = _image(response.json()) + if not wait: + _ready(image, on_progress) + return image + return await async_poll_image( + client, image, timeout=_remaining(end, image), poll_interval=poll_interval, on_progress=on_progress + ) + except TimeoutError: + _remaining(end, image) + raise diff --git a/src/dedalus_sdk/resources/__init__.py b/src/dedalus_sdk/resources/__init__.py index 164b6bd..aa924a5 100644 --- a/src/dedalus_sdk/resources/__init__.py +++ b/src/dedalus_sdk/resources/__init__.py @@ -16,6 +16,14 @@ OrganizationResourceWithStreamingResponse, AsyncOrganizationResourceWithStreamingResponse, ) +from .images import ( + ImagesResource, + AsyncImagesResource, + ImagesResourceWithRawResponse, + AsyncImagesResourceWithRawResponse, + ImagesResourceWithStreamingResponse, + AsyncImagesResourceWithStreamingResponse, +) __all__ = [ "MachinesResource", @@ -30,4 +38,10 @@ "AsyncOrganizationResourceWithRawResponse", "OrganizationResourceWithStreamingResponse", "AsyncOrganizationResourceWithStreamingResponse", + "ImagesResource", + "AsyncImagesResource", + "ImagesResourceWithRawResponse", + "AsyncImagesResourceWithRawResponse", + "ImagesResourceWithStreamingResponse", + "AsyncImagesResourceWithStreamingResponse", ] diff --git a/src/dedalus_sdk/resources/images/__init__.py b/src/dedalus_sdk/resources/images/__init__.py new file mode 100644 index 0000000..da9a7ba --- /dev/null +++ b/src/dedalus_sdk/resources/images/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from .images import ( + ImagesResource, + AsyncImagesResource, + ImagesResourceWithRawResponse, + AsyncImagesResourceWithRawResponse, + ImagesResourceWithStreamingResponse, + AsyncImagesResourceWithStreamingResponse, +) +from .uploads import ( + UploadsResource, + AsyncUploadsResource, + UploadsResourceWithRawResponse, + AsyncUploadsResourceWithRawResponse, + UploadsResourceWithStreamingResponse, + AsyncUploadsResourceWithStreamingResponse, +) + +__all__ = [ + "ImagesResource", + "AsyncImagesResource", + "ImagesResourceWithRawResponse", + "AsyncImagesResourceWithRawResponse", + "ImagesResourceWithStreamingResponse", + "AsyncImagesResourceWithStreamingResponse", + "UploadsResource", + "AsyncUploadsResource", + "UploadsResourceWithRawResponse", + "AsyncUploadsResourceWithRawResponse", + "UploadsResourceWithStreamingResponse", + "AsyncUploadsResourceWithStreamingResponse", +] diff --git a/src/dedalus_sdk/resources/images/images.py b/src/dedalus_sdk/resources/images/images.py new file mode 100644 index 0000000..0bc5072 --- /dev/null +++ b/src/dedalus_sdk/resources/images/images.py @@ -0,0 +1,666 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from __future__ import annotations + +import httpx + +from typing import Optional +from typing_extensions import Literal + +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import path_template, maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from .uploads import ( + UploadsResource, + AsyncUploadsResource, + UploadsResourceWithRawResponse, + AsyncUploadsResourceWithRawResponse, + UploadsResourceWithStreamingResponse, + AsyncUploadsResourceWithStreamingResponse, +) +from ...types.image_list import ImageList +from ...types import image_list_params, image_create_params, image_update_params +from ...types.image import Image +from ...types.source_param import SourceParam + +__all__ = ["ImagesResource", "AsyncImagesResource"] + + +class ImagesResource(SyncAPIResource): + @cached_property + def uploads(self) -> UploadsResource: + return UploadsResource(self._client) + + @cached_property + def with_raw_response(self) -> ImagesResourceWithRawResponse: + return ImagesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ImagesResourceWithStreamingResponse: + return ImagesResourceWithStreamingResponse(self) + + def list( + self, + *, + limit: int | Omit = omit, + cursor: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ImageList: + """ + Pages through the organization's images in ID order. A deleted image is left out; it still reads by ID. + + Args: + limit: Query parameter. + cursor: Query parameter. + extra_headers: Send extra headers with the request. + extra_query: Send extra query parameters with the request. + extra_body: Send extra JSON properties with the request. + timeout: Override the client-level default timeout for this request, in seconds. + + Returns: + ImageList: OK + + Example: + ```python + image = client.images.list( + limit=100, + ) + ``` + """ + return self._get( + "/v1/images", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"limit": limit, "cursor": cursor}, image_list_params.ImageListParams), + ), + cast_to=ImageList, + ) + + def create( + self, + *, + credential_ref: str | Omit = omit, + name: str | Omit = omit, + platform: Literal["linux/amd64", "linux/arm64"], + source: SourceParam, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> Image: + """ + Requires an entitled organization and linux/amd64 on the current fleet. Acceptance queues preparation; only a ready image can launch a machine. Private registry credentials are not yet supported. + + Args: + credential_ref: Body parameter. + name: A label people use to tell images apart. Programs use the image ID. + platform: Body parameter. + source: Body parameter. + extra_headers: Send extra headers with the request. + extra_query: Send extra query parameters with the request. + extra_body: Send extra JSON properties with the request. + timeout: Override the client-level default timeout for this request, in seconds. + idempotency_key: Override or provide the idempotency key for this request. + + Returns: + Image: Accepted + + Example: + ```python + image = client.images.create( + platform="linux/amd64", + source={"kind": ""}, + idempotency_key="", + ) + ``` + """ + return self._post( + "/v1/images", + body=maybe_transform( + { + "credential_ref": credential_ref, + "name": name, + "platform": platform, + "source": source, + }, + image_create_params.ImageCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=Image, + ) + + def retrieve( + self, + *, + image_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Image: + """ + A preparing image reports its stage, the worker's last step and an estimated percent of the whole preparation. A failed image names the cause: IMAGE_OS_UNSUPPORTED, IMAGE_SOURCE_INVALID, IMAGE_SOURCE_NOT_FOUND and IMAGE_SOURCE_UNAUTHORIZED describe the image or its reference and are final; IMAGE_SOURCE_UNAVAILABLE, IMAGE_REGISTRY_UNREACHABLE, IMAGE_REGISTRY_RATE_LIMITED, IMAGE_ARCHIVE_UNAVAILABLE and IMAGE_EXECUTION_FAILED describe this attempt and are retryable with a new idempotency key. + + Args: + image_id: Path parameter. + extra_headers: Send extra headers with the request. + extra_query: Send extra query parameters with the request. + extra_body: Send extra JSON properties with the request. + timeout: Override the client-level default timeout for this request, in seconds. + + Returns: + Image: OK + + Example: + ```python + image = client.images.retrieve( + image_id="imageID", + ) + ``` + """ + if image_id is None or (isinstance(image_id, str) and not image_id): + raise ValueError(f"Expected a non-empty value for `image_id` but received {image_id!r}") + return self._get( + path_template("/v1/images/{image_id}", **{"image_id": image_id}), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Image, + ) + + def update( + self, + *, + image_id: str, + name: Optional[str], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> Image: + """ + Changes only the name. The image's source, platform and state are immutable. + + Args: + image_id: Path parameter. + name: The new label, or null to clear it. No other field can change. + extra_headers: Send extra headers with the request. + extra_query: Send extra query parameters with the request. + extra_body: Send extra JSON properties with the request. + timeout: Override the client-level default timeout for this request, in seconds. + idempotency_key: Override or provide the idempotency key for this request. + + Returns: + Image: OK + + Example: + ```python + image = client.images.update( + image_id="imageID", + name="", + idempotency_key="", + ) + ``` + """ + if image_id is None or (isinstance(image_id, str) and not image_id): + raise ValueError(f"Expected a non-empty value for `image_id` but received {image_id!r}") + return self._patch( + path_template("/v1/images/{image_id}", **{"image_id": image_id}), + body=maybe_transform( + {"name": name}, + image_update_params.ImageUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=Image, + ) + + def delete( + self, + *, + image_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> Image: + """ + The image can create no new Machine and no longer counts against the organization's image limit. Machines already running from it keep working. The image stays readable, and deleting it again returns the same image. + + Args: + image_id: Path parameter. + extra_headers: Send extra headers with the request. + extra_query: Send extra query parameters with the request. + extra_body: Send extra JSON properties with the request. + timeout: Override the client-level default timeout for this request, in seconds. + idempotency_key: Override or provide the idempotency key for this request. + + Returns: + Image: OK + + Example: + ```python + image = client.images.delete( + image_id="imageID", + idempotency_key="", + ) + ``` + """ + if image_id is None or (isinstance(image_id, str) and not image_id): + raise ValueError(f"Expected a non-empty value for `image_id` but received {image_id!r}") + return self._delete( + path_template("/v1/images/{image_id}", **{"image_id": image_id}), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=Image, + ) + + +class AsyncImagesResource(AsyncAPIResource): + @cached_property + def uploads(self) -> AsyncUploadsResource: + return AsyncUploadsResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncImagesResourceWithRawResponse: + return AsyncImagesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncImagesResourceWithStreamingResponse: + return AsyncImagesResourceWithStreamingResponse(self) + + async def list( + self, + *, + limit: int | Omit = omit, + cursor: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ImageList: + """ + Pages through the organization's images in ID order. A deleted image is left out; it still reads by ID. + + Args: + limit: Query parameter. + cursor: Query parameter. + extra_headers: Send extra headers with the request. + extra_query: Send extra query parameters with the request. + extra_body: Send extra JSON properties with the request. + timeout: Override the client-level default timeout for this request, in seconds. + + Returns: + ImageList: OK + + Example: + ```python + image = await client.images.list( + limit=100, + ) + ``` + """ + return await self._get( + "/v1/images", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + {"limit": limit, "cursor": cursor}, image_list_params.ImageListParams + ), + ), + cast_to=ImageList, + ) + + async def create( + self, + *, + credential_ref: str | Omit = omit, + name: str | Omit = omit, + platform: Literal["linux/amd64", "linux/arm64"], + source: SourceParam, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> Image: + """ + Requires an entitled organization and linux/amd64 on the current fleet. Acceptance queues preparation; only a ready image can launch a machine. Private registry credentials are not yet supported. + + Args: + credential_ref: Body parameter. + name: A label people use to tell images apart. Programs use the image ID. + platform: Body parameter. + source: Body parameter. + extra_headers: Send extra headers with the request. + extra_query: Send extra query parameters with the request. + extra_body: Send extra JSON properties with the request. + timeout: Override the client-level default timeout for this request, in seconds. + idempotency_key: Override or provide the idempotency key for this request. + + Returns: + Image: Accepted + + Example: + ```python + image = await client.images.create( + platform="linux/amd64", + source={"kind": ""}, + idempotency_key="", + ) + ``` + """ + return await self._post( + "/v1/images", + body=await async_maybe_transform( + { + "credential_ref": credential_ref, + "name": name, + "platform": platform, + "source": source, + }, + image_create_params.ImageCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=Image, + ) + + async def retrieve( + self, + *, + image_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Image: + """ + A preparing image reports its stage, the worker's last step and an estimated percent of the whole preparation. A failed image names the cause: IMAGE_OS_UNSUPPORTED, IMAGE_SOURCE_INVALID, IMAGE_SOURCE_NOT_FOUND and IMAGE_SOURCE_UNAUTHORIZED describe the image or its reference and are final; IMAGE_SOURCE_UNAVAILABLE, IMAGE_REGISTRY_UNREACHABLE, IMAGE_REGISTRY_RATE_LIMITED, IMAGE_ARCHIVE_UNAVAILABLE and IMAGE_EXECUTION_FAILED describe this attempt and are retryable with a new idempotency key. + + Args: + image_id: Path parameter. + extra_headers: Send extra headers with the request. + extra_query: Send extra query parameters with the request. + extra_body: Send extra JSON properties with the request. + timeout: Override the client-level default timeout for this request, in seconds. + + Returns: + Image: OK + + Example: + ```python + image = await client.images.retrieve( + image_id="imageID", + ) + ``` + """ + if image_id is None or (isinstance(image_id, str) and not image_id): + raise ValueError(f"Expected a non-empty value for `image_id` but received {image_id!r}") + return await self._get( + path_template("/v1/images/{image_id}", **{"image_id": image_id}), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Image, + ) + + async def update( + self, + *, + image_id: str, + name: Optional[str], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> Image: + """ + Changes only the name. The image's source, platform and state are immutable. + + Args: + image_id: Path parameter. + name: The new label, or null to clear it. No other field can change. + extra_headers: Send extra headers with the request. + extra_query: Send extra query parameters with the request. + extra_body: Send extra JSON properties with the request. + timeout: Override the client-level default timeout for this request, in seconds. + idempotency_key: Override or provide the idempotency key for this request. + + Returns: + Image: OK + + Example: + ```python + image = await client.images.update( + image_id="imageID", + name="", + idempotency_key="", + ) + ``` + """ + if image_id is None or (isinstance(image_id, str) and not image_id): + raise ValueError(f"Expected a non-empty value for `image_id` but received {image_id!r}") + return await self._patch( + path_template("/v1/images/{image_id}", **{"image_id": image_id}), + body=await async_maybe_transform( + {"name": name}, + image_update_params.ImageUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=Image, + ) + + async def delete( + self, + *, + image_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> Image: + """ + The image can create no new Machine and no longer counts against the organization's image limit. Machines already running from it keep working. The image stays readable, and deleting it again returns the same image. + + Args: + image_id: Path parameter. + extra_headers: Send extra headers with the request. + extra_query: Send extra query parameters with the request. + extra_body: Send extra JSON properties with the request. + timeout: Override the client-level default timeout for this request, in seconds. + idempotency_key: Override or provide the idempotency key for this request. + + Returns: + Image: OK + + Example: + ```python + image = await client.images.delete( + image_id="imageID", + idempotency_key="", + ) + ``` + """ + if image_id is None or (isinstance(image_id, str) and not image_id): + raise ValueError(f"Expected a non-empty value for `image_id` but received {image_id!r}") + return await self._delete( + path_template("/v1/images/{image_id}", **{"image_id": image_id}), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=Image, + ) + + +class ImagesResourceWithRawResponse: + def __init__(self, images: ImagesResource) -> None: + self._images = images + + self.list = to_raw_response_wrapper( + images.list, + ) + self.create = to_raw_response_wrapper( + images.create, + ) + self.retrieve = to_raw_response_wrapper( + images.retrieve, + ) + self.update = to_raw_response_wrapper( + images.update, + ) + self.delete = to_raw_response_wrapper( + images.delete, + ) + + @cached_property + def uploads(self) -> UploadsResourceWithRawResponse: + return UploadsResourceWithRawResponse(self._images.uploads) + + +class AsyncImagesResourceWithRawResponse: + def __init__(self, images: AsyncImagesResource) -> None: + self._images = images + + self.list = async_to_raw_response_wrapper( + images.list, + ) + self.create = async_to_raw_response_wrapper( + images.create, + ) + self.retrieve = async_to_raw_response_wrapper( + images.retrieve, + ) + self.update = async_to_raw_response_wrapper( + images.update, + ) + self.delete = async_to_raw_response_wrapper( + images.delete, + ) + + @cached_property + def uploads(self) -> AsyncUploadsResourceWithRawResponse: + return AsyncUploadsResourceWithRawResponse(self._images.uploads) + + +class ImagesResourceWithStreamingResponse: + def __init__(self, images: ImagesResource) -> None: + self._images = images + + self.list = to_streamed_response_wrapper( + images.list, + ) + self.create = to_streamed_response_wrapper( + images.create, + ) + self.retrieve = to_streamed_response_wrapper( + images.retrieve, + ) + self.update = to_streamed_response_wrapper( + images.update, + ) + self.delete = to_streamed_response_wrapper( + images.delete, + ) + + @cached_property + def uploads(self) -> UploadsResourceWithStreamingResponse: + return UploadsResourceWithStreamingResponse(self._images.uploads) + + +class AsyncImagesResourceWithStreamingResponse: + def __init__(self, images: AsyncImagesResource) -> None: + self._images = images + + self.list = async_to_streamed_response_wrapper( + images.list, + ) + self.create = async_to_streamed_response_wrapper( + images.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + images.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + images.update, + ) + self.delete = async_to_streamed_response_wrapper( + images.delete, + ) + + @cached_property + def uploads(self) -> AsyncUploadsResourceWithStreamingResponse: + return AsyncUploadsResourceWithStreamingResponse(self._images.uploads) diff --git a/src/dedalus_sdk/resources/images/uploads.py b/src/dedalus_sdk/resources/images/uploads.py new file mode 100644 index 0000000..1705815 --- /dev/null +++ b/src/dedalus_sdk/resources/images/uploads.py @@ -0,0 +1,197 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import Body, Query, Headers, NotGiven, not_given +from ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.images.reservation import Reservation +from ...types.images import upload_create_params + +__all__ = ["UploadsResource", "AsyncUploadsResource"] + + +class UploadsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> UploadsResourceWithRawResponse: + return UploadsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> UploadsResourceWithStreamingResponse: + return UploadsResourceWithStreamingResponse(self) + + def create( + self, + *, + platform: str, + sha256: str, + size_bytes: int, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> Reservation: + """ + Reserve an immutable Docker image archive upload + + Args: + platform: Body parameter. + sha256: Body parameter. + size_bytes: Body parameter. + extra_headers: Send extra headers with the request. + extra_query: Send extra query parameters with the request. + extra_body: Send extra JSON properties with the request. + timeout: Override the client-level default timeout for this request, in seconds. + idempotency_key: Override or provide the idempotency key for this request. + + Returns: + Reservation: OK + + Example: + ```python + upload = client.images.uploads.create( + platform="", + sha256="", + size_bytes=0, + idempotency_key="", + ) + ``` + """ + return self._post( + "/v1/images/uploads", + body=maybe_transform( + { + "platform": platform, + "sha256": sha256, + "size_bytes": size_bytes, + }, + upload_create_params.UploadCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=Reservation, + ) + + +class AsyncUploadsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncUploadsResourceWithRawResponse: + return AsyncUploadsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncUploadsResourceWithStreamingResponse: + return AsyncUploadsResourceWithStreamingResponse(self) + + async def create( + self, + *, + platform: str, + sha256: str, + size_bytes: int, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> Reservation: + """ + Reserve an immutable Docker image archive upload + + Args: + platform: Body parameter. + sha256: Body parameter. + size_bytes: Body parameter. + extra_headers: Send extra headers with the request. + extra_query: Send extra query parameters with the request. + extra_body: Send extra JSON properties with the request. + timeout: Override the client-level default timeout for this request, in seconds. + idempotency_key: Override or provide the idempotency key for this request. + + Returns: + Reservation: OK + + Example: + ```python + upload = await client.images.uploads.create( + platform="", + sha256="", + size_bytes=0, + idempotency_key="", + ) + ``` + """ + return await self._post( + "/v1/images/uploads", + body=await async_maybe_transform( + { + "platform": platform, + "sha256": sha256, + "size_bytes": size_bytes, + }, + upload_create_params.UploadCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=Reservation, + ) + + +class UploadsResourceWithRawResponse: + def __init__(self, uploads: UploadsResource) -> None: + self._uploads = uploads + + self.create = to_raw_response_wrapper( + uploads.create, + ) + + +class AsyncUploadsResourceWithRawResponse: + def __init__(self, uploads: AsyncUploadsResource) -> None: + self._uploads = uploads + + self.create = async_to_raw_response_wrapper( + uploads.create, + ) + + +class UploadsResourceWithStreamingResponse: + def __init__(self, uploads: UploadsResource) -> None: + self._uploads = uploads + + self.create = to_streamed_response_wrapper( + uploads.create, + ) + + +class AsyncUploadsResourceWithStreamingResponse: + def __init__(self, uploads: AsyncUploadsResource) -> None: + self._uploads = uploads + + self.create = async_to_streamed_response_wrapper( + uploads.create, + ) diff --git a/src/dedalus_sdk/resources/machines/autoresizing.py b/src/dedalus_sdk/resources/machines/autoresizing.py index 7d2899f..519ce04 100644 --- a/src/dedalus_sdk/resources/machines/autoresizing.py +++ b/src/dedalus_sdk/resources/machines/autoresizing.py @@ -4,7 +4,10 @@ import httpx -from ..._types import Body, Query, Headers, NotGiven, not_given +from typing import Optional +from typing_extensions import Literal + +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -15,7 +18,7 @@ async_to_streamed_response_wrapper, ) from ..._base_client import make_request_options -from ...types.machines.settings import Settings +from ...types.machines.policy import Policy from ...types.machines import autoresizing_update_params __all__ = ["AutoresizingResource", "AsyncAutoresizingResource"] @@ -40,9 +43,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Settings: + ) -> Policy: """ - Read this machine's RAM autoresizing settings + Read this machine's autoresizing settings Args: machine_id: Bare, lowercase, hyphenated Machine UUID. Pass the returned machine_id unchanged. @@ -52,7 +55,7 @@ def retrieve( timeout: Override the client-level default timeout for this request, in seconds. Returns: - Settings: OK + Policy: OK Example: ```python @@ -68,14 +71,17 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Settings, + cast_to=Policy, ) def update( self, *, machine_id: str, - enabled: bool, + cpu: Literal["on", "off"] | Omit = omit, + cpu_max_vcpu: Optional[float] | Omit = omit, + memory: Literal["on", "off"] | Omit = omit, + memory_max_mib: Optional[int] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -83,13 +89,16 @@ def update( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, idempotency_key: str | None = None, - ) -> Settings: + ) -> Policy: """ - Set this machine's RAM autoresizing settings + Changes RAM, CPU or both for this machine. An absent resource keeps its setting. Args: machine_id: Bare, lowercase, hyphenated Machine UUID. Pass the returned machine_id unchanged. - enabled: Body parameter. + cpu: Body parameter. + cpu_max_vcpu: Body parameter. + memory: Body parameter. + memory_max_mib: Body parameter. extra_headers: Send extra headers with the request. extra_query: Send extra query parameters with the request. extra_body: Send extra JSON properties with the request. @@ -97,23 +106,27 @@ def update( idempotency_key: Override or provide the idempotency key for this request. Returns: - Settings: OK + Policy: OK Example: ```python autoresizing = client.machines.autoresizing.update( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", - enabled=False, idempotency_key="", ) ``` """ if machine_id is None or (isinstance(machine_id, str) and not machine_id): raise ValueError(f"Expected a non-empty value for `machine_id` but received {machine_id!r}") - return self._put( + return self._patch( path_template("/v1/machines/{machine_id}/autoresizing", **{"machine_id": machine_id}), body=maybe_transform( - {"enabled": enabled}, + { + "cpu": cpu, + "cpu_max_vcpu": cpu_max_vcpu, + "memory": memory, + "memory_max_mib": memory_max_mib, + }, autoresizing_update_params.AutoresizingUpdateParams, ), options=make_request_options( @@ -123,7 +136,7 @@ def update( timeout=timeout, idempotency_key=idempotency_key, ), - cast_to=Settings, + cast_to=Policy, ) @@ -146,9 +159,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Settings: + ) -> Policy: """ - Read this machine's RAM autoresizing settings + Read this machine's autoresizing settings Args: machine_id: Bare, lowercase, hyphenated Machine UUID. Pass the returned machine_id unchanged. @@ -158,7 +171,7 @@ async def retrieve( timeout: Override the client-level default timeout for this request, in seconds. Returns: - Settings: OK + Policy: OK Example: ```python @@ -174,14 +187,17 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Settings, + cast_to=Policy, ) async def update( self, *, machine_id: str, - enabled: bool, + cpu: Literal["on", "off"] | Omit = omit, + cpu_max_vcpu: Optional[float] | Omit = omit, + memory: Literal["on", "off"] | Omit = omit, + memory_max_mib: Optional[int] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -189,13 +205,16 @@ async def update( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, idempotency_key: str | None = None, - ) -> Settings: + ) -> Policy: """ - Set this machine's RAM autoresizing settings + Changes RAM, CPU or both for this machine. An absent resource keeps its setting. Args: machine_id: Bare, lowercase, hyphenated Machine UUID. Pass the returned machine_id unchanged. - enabled: Body parameter. + cpu: Body parameter. + cpu_max_vcpu: Body parameter. + memory: Body parameter. + memory_max_mib: Body parameter. extra_headers: Send extra headers with the request. extra_query: Send extra query parameters with the request. extra_body: Send extra JSON properties with the request. @@ -203,23 +222,27 @@ async def update( idempotency_key: Override or provide the idempotency key for this request. Returns: - Settings: OK + Policy: OK Example: ```python autoresizing = await client.machines.autoresizing.update( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", - enabled=False, idempotency_key="", ) ``` """ if machine_id is None or (isinstance(machine_id, str) and not machine_id): raise ValueError(f"Expected a non-empty value for `machine_id` but received {machine_id!r}") - return await self._put( + return await self._patch( path_template("/v1/machines/{machine_id}/autoresizing", **{"machine_id": machine_id}), body=await async_maybe_transform( - {"enabled": enabled}, + { + "cpu": cpu, + "cpu_max_vcpu": cpu_max_vcpu, + "memory": memory, + "memory_max_mib": memory_max_mib, + }, autoresizing_update_params.AutoresizingUpdateParams, ), options=make_request_options( @@ -229,7 +252,7 @@ async def update( timeout=timeout, idempotency_key=idempotency_key, ), - cast_to=Settings, + cast_to=Policy, ) diff --git a/src/dedalus_sdk/resources/machines/executions/logs.py b/src/dedalus_sdk/resources/machines/executions/logs.py index 652324f..d31db31 100644 --- a/src/dedalus_sdk/resources/machines/executions/logs.py +++ b/src/dedalus_sdk/resources/machines/executions/logs.py @@ -79,62 +79,6 @@ def retrieve( cast_to=Status, ) - def reauthorize( - self, - *, - machine_id: str, - execution_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - idempotency_key: str | None = None, - ) -> Status: - """ - Reauthorize execution log publication - - Args: - machine_id: Bare, lowercase, hyphenated Machine UUID. Pass the returned machine_id unchanged. - execution_id: Path parameter. - extra_headers: Send extra headers with the request. - extra_query: Send extra query parameters with the request. - extra_body: Send extra JSON properties with the request. - timeout: Override the client-level default timeout for this request, in seconds. - idempotency_key: Override or provide the idempotency key for this request. - - Returns: - Status: OK - - Example: - ```python - log = client.machines.executions.logs.reauthorize( - machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", - execution_id="executionID", - idempotency_key="", - ) - ``` - """ - if machine_id is None or (isinstance(machine_id, str) and not machine_id): - raise ValueError(f"Expected a non-empty value for `machine_id` but received {machine_id!r}") - if execution_id is None or (isinstance(execution_id, str) and not execution_id): - raise ValueError(f"Expected a non-empty value for `execution_id` but received {execution_id!r}") - return self._post( - path_template( - "/v1/machines/{machine_id}/executions/{execution_id}/logs/reauthorize", - **{"machine_id": machine_id, "execution_id": execution_id}, - ), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - idempotency_key=idempotency_key, - ), - cast_to=Status, - ) - def create_token( self, *, @@ -250,62 +194,6 @@ async def retrieve( cast_to=Status, ) - async def reauthorize( - self, - *, - machine_id: str, - execution_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - idempotency_key: str | None = None, - ) -> Status: - """ - Reauthorize execution log publication - - Args: - machine_id: Bare, lowercase, hyphenated Machine UUID. Pass the returned machine_id unchanged. - execution_id: Path parameter. - extra_headers: Send extra headers with the request. - extra_query: Send extra query parameters with the request. - extra_body: Send extra JSON properties with the request. - timeout: Override the client-level default timeout for this request, in seconds. - idempotency_key: Override or provide the idempotency key for this request. - - Returns: - Status: OK - - Example: - ```python - log = await client.machines.executions.logs.reauthorize( - machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", - execution_id="executionID", - idempotency_key="", - ) - ``` - """ - if machine_id is None or (isinstance(machine_id, str) and not machine_id): - raise ValueError(f"Expected a non-empty value for `machine_id` but received {machine_id!r}") - if execution_id is None or (isinstance(execution_id, str) and not execution_id): - raise ValueError(f"Expected a non-empty value for `execution_id` but received {execution_id!r}") - return await self._post( - path_template( - "/v1/machines/{machine_id}/executions/{execution_id}/logs/reauthorize", - **{"machine_id": machine_id, "execution_id": execution_id}, - ), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - idempotency_key=idempotency_key, - ), - cast_to=Status, - ) - async def create_token( self, *, @@ -370,9 +258,6 @@ def __init__(self, logs: LogsResource) -> None: self.retrieve = to_raw_response_wrapper( logs.retrieve, ) - self.reauthorize = to_raw_response_wrapper( - logs.reauthorize, - ) self.create_token = to_raw_response_wrapper( logs.create_token, ) @@ -385,9 +270,6 @@ def __init__(self, logs: AsyncLogsResource) -> None: self.retrieve = async_to_raw_response_wrapper( logs.retrieve, ) - self.reauthorize = async_to_raw_response_wrapper( - logs.reauthorize, - ) self.create_token = async_to_raw_response_wrapper( logs.create_token, ) @@ -400,9 +282,6 @@ def __init__(self, logs: LogsResource) -> None: self.retrieve = to_streamed_response_wrapper( logs.retrieve, ) - self.reauthorize = to_streamed_response_wrapper( - logs.reauthorize, - ) self.create_token = to_streamed_response_wrapper( logs.create_token, ) @@ -415,9 +294,6 @@ def __init__(self, logs: AsyncLogsResource) -> None: self.retrieve = async_to_streamed_response_wrapper( logs.retrieve, ) - self.reauthorize = async_to_streamed_response_wrapper( - logs.reauthorize, - ) self.create_token = async_to_streamed_response_wrapper( logs.create_token, ) diff --git a/src/dedalus_sdk/resources/machines/machines.py b/src/dedalus_sdk/resources/machines/machines.py index a41e7a3..98b7e4c 100644 --- a/src/dedalus_sdk/resources/machines/machines.py +++ b/src/dedalus_sdk/resources/machines/machines.py @@ -40,10 +40,18 @@ AutoresizingResourceWithStreamingResponse, AsyncAutoresizingResourceWithStreamingResponse, ) -from ...types import machine_list_params, machine_create_params, machine_update_params, machine_reboot_params +from ...types import ( + machine_list_params, + machine_create_params, + machine_update_params, + machine_delete_params, + machine_reboot_params, + machine_resize_params, +) from ...types.machine_list_item import MachineListItem from ...types.machine import Machine from ...types.machine_retrieve_response import MachineRetrieveResponse +from ...types.machine_resize_response import MachineResizeResponse __all__ = ["MachinesResource", "AsyncMachinesResource"] @@ -118,6 +126,7 @@ def create( self, *, autosleep: str | Omit = omit, + image_id: str | Omit = omit, memory_mib: int | Omit = omit, storage_gib: int | Omit = omit, vcpu: float | Omit = omit, @@ -134,6 +143,7 @@ def create( Args: autosleep: Idle window before autosleep. Accepts fixed duration units like 30s, 30m, 2h, 7d3h4s, or 1w3d, raw seconds ("1800"), or never to disable. + image_id: Organization-owned ready custom image ID. Requires Pro. Omit for the platform default. memory_mib: Memory in MiB. storage_gib: Storage in GiB. vcpu: CPU in vCPUs. @@ -144,7 +154,7 @@ def create( idempotency_key: Override or provide the idempotency key for this request. Returns: - Machine: Create converged inline + Machine: Create has converged Example: ```python @@ -162,6 +172,7 @@ def create( body=maybe_transform( { "autosleep": autosleep, + "image_id": image_id, "memory_mib": memory_mib, "storage_gib": storage_gib, "vcpu": vcpu, @@ -224,9 +235,7 @@ def update( *, machine_id: str, autosleep: str | Omit = omit, - memory_mib: int | Omit = omit, storage_gib: int | Omit = omit, - vcpu: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -236,14 +245,12 @@ def update( idempotency_key: str | None = None, ) -> Machine: """ - Update machine + Changes settings such as autosleep. Args: machine_id: Bare, lowercase, hyphenated Machine UUID. Pass the returned machine_id unchanged. - autosleep: Idle window before autosleep. Accepts fixed duration units like 30s, 30m, 2h, 7d3h4s, or 1w3d, raw seconds ("1800"), or never to disable. - memory_mib: Memory in MiB. - storage_gib: Storage in GiB. - vcpu: CPU in vCPUs. + autosleep: Body parameter. + storage_gib: Body parameter. extra_headers: Send extra headers with the request. extra_query: Send extra query parameters with the request. extra_body: Send extra JSON properties with the request. @@ -268,9 +275,7 @@ def update( body=maybe_transform( { "autosleep": autosleep, - "memory_mib": memory_mib, "storage_gib": storage_gib, - "vcpu": vcpu, }, machine_update_params.MachineUpdateParams, ), @@ -288,6 +293,7 @@ def delete( self, *, machine_id: str, + force: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -301,6 +307,7 @@ def delete( Args: machine_id: Bare, lowercase, hyphenated Machine UUID. Pass the returned machine_id unchanged. + force: Destroy from the last published filesystem checkpoint without publishing the Machine's remaining writes, whether the Machine is running or failed. Unpublished file writes are lost. The default publishes them first, and waits while it cannot. extra_headers: Send extra headers with the request. extra_query: Send extra query parameters with the request. extra_body: Send extra JSON properties with the request. @@ -328,6 +335,7 @@ def delete( extra_body=extra_body, timeout=timeout, idempotency_key=idempotency_key, + query=maybe_transform({"force": force}, machine_delete_params.MachineDeleteParams), ), cast_to=Machine, ) @@ -479,6 +487,65 @@ def reboot( cast_to=Machine, ) + def resize( + self, + *, + machine_id: str, + memory_mib: int | Omit = omit, + vcpu: float | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> MachineResizeResponse: + """ + Wakes a sleeping Machine, then waits up to 30 seconds for the host to confirm CPU and RAM. Returns the Machine with its confirmed size. After RESIZE_TIMEOUT, repeat the same request and Idempotency-Key to wait again. + + Args: + machine_id: Bare, lowercase, hyphenated Machine UUID. Pass the returned machine_id unchanged. + memory_mib: RAM in MiB. + vcpu: CPU in 0.25-vCPU steps. + extra_headers: Send extra headers with the request. + extra_query: Send extra query parameters with the request. + extra_body: Send extra JSON properties with the request. + timeout: Override the client-level default timeout for this request, in seconds. + idempotency_key: Override or provide the idempotency key for this request. + + Returns: + MachineResizeResponse: Machine with the confirmed size. + + Example: + ```python + machine = client.machines.resize( + machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", + idempotency_key="", + ) + ``` + """ + if machine_id is None or (isinstance(machine_id, str) and not machine_id): + raise ValueError(f"Expected a non-empty value for `machine_id` but received {machine_id!r}") + return self._post( + path_template("/v1/machines/{machine_id}/resize", **{"machine_id": machine_id}), + body=maybe_transform( + { + "memory_mib": memory_mib, + "vcpu": vcpu, + }, + machine_resize_params.MachineResizeParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=MachineResizeResponse, + ) + class AsyncMachinesResource(AsyncAPIResource): @cached_property @@ -550,6 +617,7 @@ async def create( self, *, autosleep: str | Omit = omit, + image_id: str | Omit = omit, memory_mib: int | Omit = omit, storage_gib: int | Omit = omit, vcpu: float | Omit = omit, @@ -566,6 +634,7 @@ async def create( Args: autosleep: Idle window before autosleep. Accepts fixed duration units like 30s, 30m, 2h, 7d3h4s, or 1w3d, raw seconds ("1800"), or never to disable. + image_id: Organization-owned ready custom image ID. Requires Pro. Omit for the platform default. memory_mib: Memory in MiB. storage_gib: Storage in GiB. vcpu: CPU in vCPUs. @@ -576,7 +645,7 @@ async def create( idempotency_key: Override or provide the idempotency key for this request. Returns: - Machine: Create converged inline + Machine: Create has converged Example: ```python @@ -594,6 +663,7 @@ async def create( body=await async_maybe_transform( { "autosleep": autosleep, + "image_id": image_id, "memory_mib": memory_mib, "storage_gib": storage_gib, "vcpu": vcpu, @@ -656,9 +726,7 @@ async def update( *, machine_id: str, autosleep: str | Omit = omit, - memory_mib: int | Omit = omit, storage_gib: int | Omit = omit, - vcpu: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -668,14 +736,12 @@ async def update( idempotency_key: str | None = None, ) -> Machine: """ - Update machine + Changes settings such as autosleep. Args: machine_id: Bare, lowercase, hyphenated Machine UUID. Pass the returned machine_id unchanged. - autosleep: Idle window before autosleep. Accepts fixed duration units like 30s, 30m, 2h, 7d3h4s, or 1w3d, raw seconds ("1800"), or never to disable. - memory_mib: Memory in MiB. - storage_gib: Storage in GiB. - vcpu: CPU in vCPUs. + autosleep: Body parameter. + storage_gib: Body parameter. extra_headers: Send extra headers with the request. extra_query: Send extra query parameters with the request. extra_body: Send extra JSON properties with the request. @@ -700,9 +766,7 @@ async def update( body=await async_maybe_transform( { "autosleep": autosleep, - "memory_mib": memory_mib, "storage_gib": storage_gib, - "vcpu": vcpu, }, machine_update_params.MachineUpdateParams, ), @@ -720,6 +784,7 @@ async def delete( self, *, machine_id: str, + force: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -733,6 +798,7 @@ async def delete( Args: machine_id: Bare, lowercase, hyphenated Machine UUID. Pass the returned machine_id unchanged. + force: Destroy from the last published filesystem checkpoint without publishing the Machine's remaining writes, whether the Machine is running or failed. Unpublished file writes are lost. The default publishes them first, and waits while it cannot. extra_headers: Send extra headers with the request. extra_query: Send extra query parameters with the request. extra_body: Send extra JSON properties with the request. @@ -760,6 +826,7 @@ async def delete( extra_body=extra_body, timeout=timeout, idempotency_key=idempotency_key, + query=await async_maybe_transform({"force": force}, machine_delete_params.MachineDeleteParams), ), cast_to=Machine, ) @@ -911,6 +978,65 @@ async def reboot( cast_to=Machine, ) + async def resize( + self, + *, + machine_id: str, + memory_mib: int | Omit = omit, + vcpu: float | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> MachineResizeResponse: + """ + Wakes a sleeping Machine, then waits up to 30 seconds for the host to confirm CPU and RAM. Returns the Machine with its confirmed size. After RESIZE_TIMEOUT, repeat the same request and Idempotency-Key to wait again. + + Args: + machine_id: Bare, lowercase, hyphenated Machine UUID. Pass the returned machine_id unchanged. + memory_mib: RAM in MiB. + vcpu: CPU in 0.25-vCPU steps. + extra_headers: Send extra headers with the request. + extra_query: Send extra query parameters with the request. + extra_body: Send extra JSON properties with the request. + timeout: Override the client-level default timeout for this request, in seconds. + idempotency_key: Override or provide the idempotency key for this request. + + Returns: + MachineResizeResponse: Machine with the confirmed size. + + Example: + ```python + machine = await client.machines.resize( + machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", + idempotency_key="", + ) + ``` + """ + if machine_id is None or (isinstance(machine_id, str) and not machine_id): + raise ValueError(f"Expected a non-empty value for `machine_id` but received {machine_id!r}") + return await self._post( + path_template("/v1/machines/{machine_id}/resize", **{"machine_id": machine_id}), + body=await async_maybe_transform( + { + "memory_mib": memory_mib, + "vcpu": vcpu, + }, + machine_resize_params.MachineResizeParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=MachineResizeResponse, + ) + class MachinesResourceWithRawResponse: def __init__(self, machines: MachinesResource) -> None: @@ -940,6 +1066,9 @@ def __init__(self, machines: MachinesResource) -> None: self.reboot = to_raw_response_wrapper( machines.reboot, ) + self.resize = to_raw_response_wrapper( + machines.resize, + ) @cached_property def ssh(self) -> SSHResourceWithRawResponse: @@ -982,6 +1111,9 @@ def __init__(self, machines: AsyncMachinesResource) -> None: self.reboot = async_to_raw_response_wrapper( machines.reboot, ) + self.resize = async_to_raw_response_wrapper( + machines.resize, + ) @cached_property def ssh(self) -> AsyncSSHResourceWithRawResponse: @@ -1024,6 +1156,9 @@ def __init__(self, machines: MachinesResource) -> None: self.reboot = to_streamed_response_wrapper( machines.reboot, ) + self.resize = to_streamed_response_wrapper( + machines.resize, + ) @cached_property def ssh(self) -> SSHResourceWithStreamingResponse: @@ -1066,6 +1201,9 @@ def __init__(self, machines: AsyncMachinesResource) -> None: self.reboot = async_to_streamed_response_wrapper( machines.reboot, ) + self.resize = async_to_streamed_response_wrapper( + machines.resize, + ) @cached_property def ssh(self) -> AsyncSSHResourceWithStreamingResponse: diff --git a/src/dedalus_sdk/resources/organization/autoresizing.py b/src/dedalus_sdk/resources/organization/autoresizing.py index e1b1f85..1fed981 100644 --- a/src/dedalus_sdk/resources/organization/autoresizing.py +++ b/src/dedalus_sdk/resources/organization/autoresizing.py @@ -4,7 +4,10 @@ import httpx -from ..._types import Body, Query, Headers, NotGiven, not_given +from typing import Optional +from typing_extensions import Literal + +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -15,7 +18,7 @@ async_to_streamed_response_wrapper, ) from ..._base_client import make_request_options -from ...types.organization.policy import Policy +from ...types.machines.policy import Policy from ...types.organization import autoresizing_update_params __all__ = ["AutoresizingResource", "AsyncAutoresizingResource"] @@ -41,7 +44,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Policy: """ - Read organization RAM autoresizing policy + Read organization autoresizing policy Args: extra_headers: Send extra headers with the request. @@ -68,7 +71,10 @@ def retrieve( def update( self, *, - enabled: bool, + cpu: Literal["on", "off"] | Omit = omit, + cpu_max_vcpu: Optional[float] | Omit = omit, + memory: Literal["on", "off"] | Omit = omit, + memory_max_mib: Optional[int] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -78,10 +84,13 @@ def update( idempotency_key: str | None = None, ) -> Policy: """ - Set organization RAM autoresizing policy + Changes defaults copied to new Machines. Existing Machines retain their own policy. Args: - enabled: Allow automatic RAM increases for all organization machines. Disabling preserves applied RAM and already admitted resizes. + cpu: Body parameter. + cpu_max_vcpu: Body parameter. + memory: Body parameter. + memory_max_mib: Body parameter. extra_headers: Send extra headers with the request. extra_query: Send extra query parameters with the request. extra_body: Send extra JSON properties with the request. @@ -94,15 +103,19 @@ def update( Example: ```python autoresizing = client.organization.autoresizing.update( - enabled=False, idempotency_key="", ) ``` """ - return self._put( + return self._patch( "/v1/organization/autoresizing", body=maybe_transform( - {"enabled": enabled}, + { + "cpu": cpu, + "cpu_max_vcpu": cpu_max_vcpu, + "memory": memory, + "memory_max_mib": memory_max_mib, + }, autoresizing_update_params.AutoresizingUpdateParams, ), options=make_request_options( @@ -136,7 +149,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Policy: """ - Read organization RAM autoresizing policy + Read organization autoresizing policy Args: extra_headers: Send extra headers with the request. @@ -163,7 +176,10 @@ async def retrieve( async def update( self, *, - enabled: bool, + cpu: Literal["on", "off"] | Omit = omit, + cpu_max_vcpu: Optional[float] | Omit = omit, + memory: Literal["on", "off"] | Omit = omit, + memory_max_mib: Optional[int] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -173,10 +189,13 @@ async def update( idempotency_key: str | None = None, ) -> Policy: """ - Set organization RAM autoresizing policy + Changes defaults copied to new Machines. Existing Machines retain their own policy. Args: - enabled: Allow automatic RAM increases for all organization machines. Disabling preserves applied RAM and already admitted resizes. + cpu: Body parameter. + cpu_max_vcpu: Body parameter. + memory: Body parameter. + memory_max_mib: Body parameter. extra_headers: Send extra headers with the request. extra_query: Send extra query parameters with the request. extra_body: Send extra JSON properties with the request. @@ -189,15 +208,19 @@ async def update( Example: ```python autoresizing = await client.organization.autoresizing.update( - enabled=False, idempotency_key="", ) ``` """ - return await self._put( + return await self._patch( "/v1/organization/autoresizing", body=await async_maybe_transform( - {"enabled": enabled}, + { + "cpu": cpu, + "cpu_max_vcpu": cpu_max_vcpu, + "memory": memory, + "memory_max_mib": memory_max_mib, + }, autoresizing_update_params.AutoresizingUpdateParams, ), options=make_request_options( diff --git a/src/dedalus_sdk/types/__init__.py b/src/dedalus_sdk/types/__init__.py index ba5db4d..54b6050 100644 --- a/src/dedalus_sdk/types/__init__.py +++ b/src/dedalus_sdk/types/__init__.py @@ -2,12 +2,23 @@ from __future__ import annotations +from .error import Error as Error +from .image_list import ImageList as ImageList from .machine import Machine as Machine from .lifecycle_status import LifecycleStatus as LifecycleStatus from .machine_list_item import MachineListItem as MachineListItem from .machine_list import MachineList as MachineList +from .image import Image as Image +from .source import Source as Source +from .source_param import SourceParam as SourceParam from .machine_list_params import MachineListParams as MachineListParams from .machine_create_params import MachineCreateParams as MachineCreateParams from .machine_retrieve_response import MachineRetrieveResponse as MachineRetrieveResponse from .machine_update_params import MachineUpdateParams as MachineUpdateParams +from .machine_delete_params import MachineDeleteParams as MachineDeleteParams from .machine_reboot_params import MachineRebootParams as MachineRebootParams +from .machine_resize_response import MachineResizeResponse as MachineResizeResponse +from .machine_resize_params import MachineResizeParams as MachineResizeParams +from .image_list_params import ImageListParams as ImageListParams +from .image_create_params import ImageCreateParams as ImageCreateParams +from .image_update_params import ImageUpdateParams as ImageUpdateParams diff --git a/src/dedalus_sdk/types/error.py b/src/dedalus_sdk/types/error.py new file mode 100644 index 0000000..78bee3c --- /dev/null +++ b/src/dedalus_sdk/types/error.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from typing import Dict, Optional + +from .._models import BaseModel + +__all__ = ["Error"] + + +class Error(BaseModel): + details: Optional[Dict[str, str]] = None + + error_code: str + + message: str + + retry_after_ms: Optional[int] = None + + retryable: bool diff --git a/src/dedalus_sdk/types/image.py b/src/dedalus_sdk/types/image.py new file mode 100644 index 0000000..c09e0d7 --- /dev/null +++ b/src/dedalus_sdk/types/image.py @@ -0,0 +1,49 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from __future__ import annotations + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel + +from .error import Error +from .source import Source + +__all__ = ["Image", "Context", "ContextProgress"] + + +class ContextProgress(BaseModel): + completed: int + + step: str + + total: int + + +class Context(BaseModel): + percent: int + + progress: Optional[ContextProgress] = None + + stage: str + + +class Image(BaseModel): + context: Optional[Context] = None + + created_at: datetime + + error: Optional[Error] = None + + image_id: str + + name: Optional[str] = None + + platform: str + + reason: Optional[str] = None + + source: Source + + state: str diff --git a/src/dedalus_sdk/types/image_create_params.py b/src/dedalus_sdk/types/image_create_params.py new file mode 100644 index 0000000..c81daca --- /dev/null +++ b/src/dedalus_sdk/types/image_create_params.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +from .source_param import SourceParam + +__all__ = ["ImageCreateParams"] + + +class ImageCreateParams(TypedDict, total=False): + credential_ref: str + + name: str + """A label people use to tell images apart. Programs use the image ID.""" + + platform: Required[Literal["linux/amd64", "linux/arm64"]] + + source: Required[SourceParam] diff --git a/src/dedalus_sdk/types/image_list.py b/src/dedalus_sdk/types/image_list.py new file mode 100644 index 0000000..eb7ce7a --- /dev/null +++ b/src/dedalus_sdk/types/image_list.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from typing import List, Optional + +from .._models import BaseModel + +__all__ = ["ImageList"] + + +class ImageList(BaseModel): + image_ids: Optional[List[str]] = None + + next_cursor: Optional[str] = None diff --git a/src/dedalus_sdk/types/image_list_params.py b/src/dedalus_sdk/types/image_list_params.py new file mode 100644 index 0000000..7d30710 --- /dev/null +++ b/src/dedalus_sdk/types/image_list_params.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["ImageListParams"] + + +class ImageListParams(TypedDict, total=False): + limit: int + + cursor: str diff --git a/src/dedalus_sdk/types/image_update_params.py b/src/dedalus_sdk/types/image_update_params.py new file mode 100644 index 0000000..04daa46 --- /dev/null +++ b/src/dedalus_sdk/types/image_update_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Required, TypedDict + +__all__ = ["ImageUpdateParams"] + + +class ImageUpdateParams(TypedDict, total=False): + image_id: Required[str] + + name: Required[Optional[str]] + """The new label, or null to clear it. No other field can change.""" diff --git a/src/dedalus_sdk/types/images/__init__.py b/src/dedalus_sdk/types/images/__init__.py new file mode 100644 index 0000000..d3a06f9 --- /dev/null +++ b/src/dedalus_sdk/types/images/__init__.py @@ -0,0 +1,6 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from __future__ import annotations + +from .reservation import Reservation as Reservation +from .upload_create_params import UploadCreateParams as UploadCreateParams diff --git a/src/dedalus_sdk/types/images/reservation.py b/src/dedalus_sdk/types/images/reservation.py new file mode 100644 index 0000000..f9e432e --- /dev/null +++ b/src/dedalus_sdk/types/images/reservation.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from typing import Dict, List, Optional +from datetime import datetime + +from ..._models import BaseModel + +from ..source import Source + +__all__ = ["Reservation", "Upload"] + + +class Upload(BaseModel): + expires_at: datetime + + headers: Dict[str, Optional[List[str]]] + + method: str + + url: str + + +class Reservation(BaseModel): + source: Source + + upload: Upload diff --git a/src/dedalus_sdk/types/images/upload_create_params.py b/src/dedalus_sdk/types/images/upload_create_params.py new file mode 100644 index 0000000..57c9dfa --- /dev/null +++ b/src/dedalus_sdk/types/images/upload_create_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["UploadCreateParams"] + + +class UploadCreateParams(TypedDict, total=False): + platform: Required[str] + + sha256: Required[str] + + size_bytes: Required[int] diff --git a/src/dedalus_sdk/types/lifecycle_status.py b/src/dedalus_sdk/types/lifecycle_status.py index fe2ae8c..a1912b1 100644 --- a/src/dedalus_sdk/types/lifecycle_status.py +++ b/src/dedalus_sdk/types/lifecycle_status.py @@ -1,35 +1,68 @@ # File generated from our OpenAPI spec by Scalar. See README.md for details. +from __future__ import annotations + from typing import Optional from datetime import datetime from typing_extensions import Literal from .._models import BaseModel -__all__ = ["LifecycleStatus"] +__all__ = ["LifecycleStatus", "Autoresize", "AutoresizeCPU", "AutoresizeMemory"] -class LifecycleStatus(BaseModel): - last_error: Optional[str] = None +class AutoresizeMemory(BaseModel): + blocked_at: Optional[datetime] = None + """Time the current refusal was first recorded.""" - last_progress_at: datetime + blocked_reason: Optional[ + Literal[ + "MACHINE_GROWTH_LIMIT_REACHED", + "MACHINE_TIER_SHAPE_EXCEEDED", + "MACHINE_QUOTA_EXCEEDED", + "MACHINE_MEMORY_ABOVE_LIMIT", + "ASSIGNED_HOST_CAPACITY_CONFLICT", + "MACHINE_PLACEMENT_EXHAUSTED", + "MACHINE_RESIZE_UNAVAILABLE", + "MACHINE_SHAPE_UNSUPPORTED", + ] + ] = None + """Why the latest automatic increase could not proceed, as the error code an explicit resize would return. Absent once an automatic increase is accepted.""" - last_transition_at: datetime - memory_assigned_mib: Optional[int] = None - """Last confirmed RAM allocation for the current running generation. Absent when the allocation is unknown or no longer current.""" +class AutoresizeCPU(BaseModel): + blocked_at: Optional[datetime] = None + """Time the current refusal was first recorded.""" + + blocked_reason: Optional[ + Literal[ + "MACHINE_GROWTH_LIMIT_REACHED", + "MACHINE_TIER_SHAPE_EXCEEDED", + "MACHINE_QUOTA_EXCEEDED", + "MACHINE_MEMORY_ABOVE_LIMIT", + "ASSIGNED_HOST_CAPACITY_CONFLICT", + "MACHINE_PLACEMENT_EXHAUSTED", + "MACHINE_RESIZE_UNAVAILABLE", + "MACHINE_SHAPE_UNSUPPORTED", + ] + ] = None + """Why the latest automatic increase could not proceed, as the error code an explicit resize would return. Absent once an automatic increase is accepted.""" + - memory_configured_mib: int - """Accepted RAM maximum, including completed automatic increases.""" +class Autoresize(BaseModel): + cpu: Optional[AutoresizeCPU] = None - memory_last_autoresized_at: Optional[datetime] = None - """Time of the latest confirmed automatic RAM increase. Does not include explicit resizing or a complete change history.""" + memory: Optional[AutoresizeMemory] = None - memory_resize_state: Optional[Literal["stable", "error", "pending_capacity"]] = None - """Resize progress reported by the current runtime. A pending automatic target may not yet be applied by that runtime.""" - memory_target_mib: Optional[int] = None - """Pending automatic RAM target, or the current runtime target when no automatic target is pending.""" +class LifecycleStatus(BaseModel): + autoresize: Autoresize + + last_error: Optional[str] = None + + last_progress_at: datetime + + last_transition_at: datetime phase: Literal[ "accepted", diff --git a/src/dedalus_sdk/types/machine_create_params.py b/src/dedalus_sdk/types/machine_create_params.py index 9b80971..9dd74a7 100644 --- a/src/dedalus_sdk/types/machine_create_params.py +++ b/src/dedalus_sdk/types/machine_create_params.py @@ -11,6 +11,9 @@ class MachineCreateParams(TypedDict, total=False): autosleep: str """Idle window before autosleep. Accepts fixed duration units like 30s, 30m, 2h, 7d3h4s, or 1w3d, raw seconds ("1800"), or never to disable.""" + image_id: str + """Organization-owned ready custom image ID. Requires Pro. Omit for the platform default.""" + memory_mib: int """Memory in MiB.""" diff --git a/src/dedalus_sdk/types/machine_delete_params.py b/src/dedalus_sdk/types/machine_delete_params.py new file mode 100644 index 0000000..cc39d63 --- /dev/null +++ b/src/dedalus_sdk/types/machine_delete_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["MachineDeleteParams"] + + +class MachineDeleteParams(TypedDict, total=False): + machine_id: Required[str] + """Bare, lowercase, hyphenated Machine UUID. Pass the returned machine_id unchanged.""" + + force: bool + """Destroy from the last published filesystem checkpoint without publishing the Machine's remaining writes, whether the Machine is running or failed. Unpublished file writes are lost. The default publishes them first, and waits while it cannot.""" diff --git a/src/dedalus_sdk/types/machine_resize_params.py b/src/dedalus_sdk/types/machine_resize_params.py new file mode 100644 index 0000000..026e94e --- /dev/null +++ b/src/dedalus_sdk/types/machine_resize_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["MachineResizeParams"] + + +class MachineResizeParams(TypedDict, total=False): + machine_id: Required[str] + """Bare, lowercase, hyphenated Machine UUID. Pass the returned machine_id unchanged.""" + + memory_mib: int + """RAM in MiB.""" + + vcpu: float + """CPU in 0.25-vCPU steps.""" diff --git a/src/dedalus_sdk/types/machine_resize_response.py b/src/dedalus_sdk/types/machine_resize_response.py new file mode 100644 index 0000000..1315671 --- /dev/null +++ b/src/dedalus_sdk/types/machine_resize_response.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from typing_extensions import Literal + +from .._models import BaseModel + +from .lifecycle_status import LifecycleStatus + +__all__ = ["MachineResizeResponse"] + + +class MachineResizeResponse(BaseModel): + autosleep_seconds: int + """Seconds of inactivity before autosleep. 0 disables autosleep.""" + + desired_state: Literal["running", "sleeping", "destroyed"] + + machine_id: str + + memory_mib: int + """Memory in MiB.""" + + status: LifecycleStatus + + storage_gib: int + + vcpu: float + """CPU in vCPUs.""" diff --git a/src/dedalus_sdk/types/machine_update_params.py b/src/dedalus_sdk/types/machine_update_params.py index c655af9..7163067 100644 --- a/src/dedalus_sdk/types/machine_update_params.py +++ b/src/dedalus_sdk/types/machine_update_params.py @@ -12,13 +12,5 @@ class MachineUpdateParams(TypedDict, total=False): """Bare, lowercase, hyphenated Machine UUID. Pass the returned machine_id unchanged.""" autosleep: str - """Idle window before autosleep. Accepts fixed duration units like 30s, 30m, 2h, 7d3h4s, or 1w3d, raw seconds ("1800"), or never to disable.""" - - memory_mib: int - """Memory in MiB.""" storage_gib: int - """Storage in GiB.""" - - vcpu: float - """CPU in vCPUs.""" diff --git a/src/dedalus_sdk/types/machines/__init__.py b/src/dedalus_sdk/types/machines/__init__.py index 6e62e9d..34e1e9e 100644 --- a/src/dedalus_sdk/types/machines/__init__.py +++ b/src/dedalus_sdk/types/machines/__init__.py @@ -3,12 +3,12 @@ from __future__ import annotations from .artifact_ref import ArtifactRef as ArtifactRef +from .policy import Policy as Policy from .execution_event import ExecutionEvent as ExecutionEvent from .execution_events import ExecutionEvents as ExecutionEvents from .execution_list import ExecutionList as ExecutionList from .execution_output import ExecutionOutput as ExecutionOutput from .execution import Execution as Execution -from .settings import Settings as Settings from .ssh_connection import SSHConnection as SSHConnection from .ssh_host_trust import SSHHostTrust as SSHHostTrust from .ssh_session_list import SSHSessionList as SSHSessionList diff --git a/src/dedalus_sdk/types/machines/autoresizing_update_params.py b/src/dedalus_sdk/types/machines/autoresizing_update_params.py index a70e87b..c00cb0b 100644 --- a/src/dedalus_sdk/types/machines/autoresizing_update_params.py +++ b/src/dedalus_sdk/types/machines/autoresizing_update_params.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing_extensions import Required, TypedDict +from typing import Optional +from typing_extensions import Literal, Required, TypedDict __all__ = ["AutoresizingUpdateParams"] @@ -11,4 +12,10 @@ class AutoresizingUpdateParams(TypedDict, total=False): machine_id: Required[str] """Bare, lowercase, hyphenated Machine UUID. Pass the returned machine_id unchanged.""" - enabled: Required[bool] + cpu: Literal["on", "off"] + + cpu_max_vcpu: Optional[float] + + memory: Literal["on", "off"] + + memory_max_mib: Optional[int] diff --git a/src/dedalus_sdk/types/machines/executions/status.py b/src/dedalus_sdk/types/machines/executions/status.py index 36c3dfb..94db1d5 100644 --- a/src/dedalus_sdk/types/machines/executions/status.py +++ b/src/dedalus_sdk/types/machines/executions/status.py @@ -5,17 +5,7 @@ from ...._models import BaseModel -__all__ = ["Status", "Capture", "Publication"] - - -class Publication(BaseModel): - code: Optional[str] = None - - complete: bool - - queued_messages: int - - state: Literal["active", "finishing", "failed", "closed"] +__all__ = ["Status", "Capture"] class Capture(BaseModel): @@ -31,10 +21,6 @@ class Capture(BaseModel): class Status(BaseModel): capture: Capture - code: Optional[str] = None - execution_id: str - publication: Optional[Publication] = None - stream_id: Optional[str] = None diff --git a/src/dedalus_sdk/types/machines/policy.py b/src/dedalus_sdk/types/machines/policy.py new file mode 100644 index 0000000..03fa595 --- /dev/null +++ b/src/dedalus_sdk/types/machines/policy.py @@ -0,0 +1,35 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["Policy"] + + +class Policy(BaseModel): + cpu: Literal["on", "off"] + """Permit automatic CPU growth independently of RAM.""" + + cpu_limit_source: Literal["plan", "configured", "platform"] + + cpu_max_vcpu: Optional[float] = None + """Configured CPU growth limit in 0.25-vCPU steps. Null uses the plan limit.""" + + effective_cpu_max_vcpu: float + """Largest automatic CPU size allowed by this policy, plan and platform.""" + + effective_memory_max_mib: int + """Largest automatic RAM size allowed by this policy and plan, in MiB.""" + + enabled: bool + """Deprecated RAM permission alias. Use memory.""" + + memory: Literal["on", "off"] + """Permit automatic RAM growth. Automatic shrink remains internal.""" + + memory_limit_source: Literal["plan", "configured", "platform"] + + memory_max_mib: Optional[int] = None + """Configured RAM growth limit in MiB. Null uses the plan limit.""" diff --git a/src/dedalus_sdk/types/machines/settings.py b/src/dedalus_sdk/types/machines/settings.py deleted file mode 100644 index ced1e34..0000000 --- a/src/dedalus_sdk/types/machines/settings.py +++ /dev/null @@ -1,9 +0,0 @@ -# File generated from our OpenAPI spec by Scalar. See README.md for details. - -from ..._models import BaseModel - -__all__ = ["Settings"] - - -class Settings(BaseModel): - enabled: bool diff --git a/src/dedalus_sdk/types/organization/__init__.py b/src/dedalus_sdk/types/organization/__init__.py index 843c352..4968f26 100644 --- a/src/dedalus_sdk/types/organization/__init__.py +++ b/src/dedalus_sdk/types/organization/__init__.py @@ -2,5 +2,4 @@ from __future__ import annotations -from .policy import Policy as Policy from .autoresizing_update_params import AutoresizingUpdateParams as AutoresizingUpdateParams diff --git a/src/dedalus_sdk/types/organization/autoresizing_update_params.py b/src/dedalus_sdk/types/organization/autoresizing_update_params.py index 998d8cd..425325b 100644 --- a/src/dedalus_sdk/types/organization/autoresizing_update_params.py +++ b/src/dedalus_sdk/types/organization/autoresizing_update_params.py @@ -2,11 +2,17 @@ from __future__ import annotations -from typing_extensions import Required, TypedDict +from typing import Optional +from typing_extensions import Literal, TypedDict __all__ = ["AutoresizingUpdateParams"] class AutoresizingUpdateParams(TypedDict, total=False): - enabled: Required[bool] - """Allow automatic RAM increases for all organization machines. Disabling preserves applied RAM and already admitted resizes.""" + cpu: Literal["on", "off"] + + cpu_max_vcpu: Optional[float] + + memory: Literal["on", "off"] + + memory_max_mib: Optional[int] diff --git a/src/dedalus_sdk/types/organization/policy.py b/src/dedalus_sdk/types/organization/policy.py deleted file mode 100644 index abd22ac..0000000 --- a/src/dedalus_sdk/types/organization/policy.py +++ /dev/null @@ -1,10 +0,0 @@ -# File generated from our OpenAPI spec by Scalar. See README.md for details. - -from ..._models import BaseModel - -__all__ = ["Policy"] - - -class Policy(BaseModel): - enabled: bool - """Allow automatic RAM increases for all organization machines. Disabling preserves applied RAM and already admitted resizes.""" diff --git a/src/dedalus_sdk/types/source.py b/src/dedalus_sdk/types/source.py new file mode 100644 index 0000000..e567a83 --- /dev/null +++ b/src/dedalus_sdk/types/source.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["Source"] + + +class Source(BaseModel): + kind: str + + reference: Optional[str] = None + + sha256: Optional[str] = None + + upload_id: Optional[str] = None diff --git a/src/dedalus_sdk/types/source_param.py b/src/dedalus_sdk/types/source_param.py new file mode 100644 index 0000000..c2f8ba8 --- /dev/null +++ b/src/dedalus_sdk/types/source_param.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Scalar. See README.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["SourceParam"] + + +class SourceParam(TypedDict, total=False): + kind: Required[str] + + reference: str + + sha256: str + + upload_id: str diff --git a/tests/smoke-test.py b/tests/smoke-test.py index 23daa51..448d470 100644 --- a/tests/smoke-test.py +++ b/tests/smoke-test.py @@ -60,58 +60,75 @@ def _smoke_case_1() -> None: def _smoke_case_2() -> None: + machine = client.machines.create( + autosleep="300s", + image_id="x", + memory_mib=4096, + storage_gib=10, + vcpu=1, + idempotency_key="", + ) + + +def _smoke_case_3() -> None: machine = client.machines.retrieve( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", ) -def _smoke_case_3() -> None: +def _smoke_case_4() -> None: machine = client.machines.update( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", idempotency_key="", ) -def _smoke_case_4() -> None: +def _smoke_case_5() -> None: machine = client.machines.update( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", autosleep="", - memory_mib=0, storage_gib=0, - vcpu=0, idempotency_key="", ) -def _smoke_case_5() -> None: +def _smoke_case_6() -> None: machine = client.machines.delete( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", idempotency_key="", ) -def _smoke_case_6() -> None: +def _smoke_case_7() -> None: + machine = client.machines.delete( + machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", + force=True, + idempotency_key="", + ) + + +def _smoke_case_8() -> None: machine = client.machines.sleep( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", idempotency_key="", ) -def _smoke_case_7() -> None: +def _smoke_case_9() -> None: machine = client.machines.wake( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", idempotency_key="", ) -def _smoke_case_8() -> None: +def _smoke_case_10() -> None: machine = client.machines.reboot( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", idempotency_key="", ) -def _smoke_case_9() -> None: +def _smoke_case_11() -> None: machine = client.machines.reboot( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", force=True, @@ -119,13 +136,29 @@ def _smoke_case_9() -> None: ) -def _smoke_case_10() -> None: +def _smoke_case_12() -> None: + machine = client.machines.resize( + machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", + idempotency_key="", + ) + + +def _smoke_case_13() -> None: + machine = client.machines.resize( + machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", + memory_mib=0, + vcpu=0, + idempotency_key="", + ) + + +def _smoke_case_14() -> None: page = client.machines.ssh.list( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", ) -def _smoke_case_11() -> None: +def _smoke_case_15() -> None: ssh = client.machines.ssh.create( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", public_key="", @@ -133,14 +166,14 @@ def _smoke_case_11() -> None: ) -def _smoke_case_12() -> None: +def _smoke_case_16() -> None: ssh = client.machines.ssh.retrieve( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", session_id="sessionID", ) -def _smoke_case_13() -> None: +def _smoke_case_17() -> None: ssh = client.machines.ssh.delete( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", session_id="sessionID", @@ -148,13 +181,13 @@ def _smoke_case_13() -> None: ) -def _smoke_case_14() -> None: +def _smoke_case_18() -> None: page = client.machines.executions.list( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", ) -def _smoke_case_15() -> None: +def _smoke_case_19() -> None: execution = client.machines.executions.create( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", command=[""], @@ -162,7 +195,7 @@ def _smoke_case_15() -> None: ) -def _smoke_case_16() -> None: +def _smoke_case_20() -> None: execution = client.machines.executions.create( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", command=[""], @@ -174,14 +207,14 @@ def _smoke_case_16() -> None: ) -def _smoke_case_17() -> None: +def _smoke_case_21() -> None: execution = client.machines.executions.retrieve( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", execution_id="executionID", ) -def _smoke_case_18() -> None: +def _smoke_case_22() -> None: execution = client.machines.executions.delete( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", execution_id="executionID", @@ -189,64 +222,136 @@ def _smoke_case_18() -> None: ) -def _smoke_case_19() -> None: +def _smoke_case_23() -> None: execution = client.machines.executions.output( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", execution_id="executionID", ) -def _smoke_case_20() -> None: +def _smoke_case_24() -> None: page = client.machines.executions.events( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", execution_id="executionID", ) -def _smoke_case_21() -> None: +def _smoke_case_25() -> None: log = client.machines.executions.logs.retrieve( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", execution_id="executionID", ) -def _smoke_case_22() -> None: - log = client.machines.executions.logs.reauthorize( +def _smoke_case_26() -> None: + log = client.machines.executions.logs.create_token( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", execution_id="executionID", idempotency_key="", ) -def _smoke_case_23() -> None: - log = client.machines.executions.logs.create_token( +def _smoke_case_27() -> None: + autoresizing = client.machines.autoresizing.retrieve( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", - execution_id="executionID", - idempotency_key="", ) -def _smoke_case_24() -> None: - autoresizing = client.machines.autoresizing.retrieve( +def _smoke_case_28() -> None: + autoresizing = client.machines.autoresizing.update( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", + idempotency_key="", ) -def _smoke_case_25() -> None: +def _smoke_case_29() -> None: autoresizing = client.machines.autoresizing.update( machine_id="017f22e2-79b0-7cc3-98c4-dc0c0c07398f", - enabled=False, + cpu="on", + cpu_max_vcpu=0.25, + memory="on", + memory_max_mib=256, idempotency_key="", ) -def _smoke_case_26() -> None: +def _smoke_case_30() -> None: autoresizing = client.organization.autoresizing.retrieve() -def _smoke_case_27() -> None: +def _smoke_case_31() -> None: + autoresizing = client.organization.autoresizing.update( + idempotency_key="", + ) + + +def _smoke_case_32() -> None: autoresizing = client.organization.autoresizing.update( - enabled=False, + cpu="on", + cpu_max_vcpu=0.25, + memory="on", + memory_max_mib=256, + idempotency_key="", + ) + + +def _smoke_case_33() -> None: + image = client.images.list( + limit=100, + ) + + +def _smoke_case_34() -> None: + image = client.images.list( + limit=100, + cursor="cursor", + ) + + +def _smoke_case_35() -> None: + image = client.images.create( + platform="linux/amd64", + source={"kind": ""}, + idempotency_key="", + ) + + +def _smoke_case_36() -> None: + image = client.images.create( + credential_ref="", + name="", + platform="linux/amd64", + source={"kind": "", "reference": "", "sha256": "", "upload_id": ""}, + idempotency_key="", + ) + + +def _smoke_case_37() -> None: + image = client.images.retrieve( + image_id="imageID", + ) + + +def _smoke_case_38() -> None: + image = client.images.update( + image_id="imageID", + name="", + idempotency_key="", + ) + + +def _smoke_case_39() -> None: + image = client.images.delete( + image_id="imageID", + idempotency_key="", + ) + + +def _smoke_case_40() -> None: + upload = client.images.uploads.create( + platform="", + sha256="", + size_bytes=0, idempotency_key="", ) @@ -262,169 +367,261 @@ def _smoke_case_27() -> None: "operation": "create", "method": "POST", "path": "/v1/machines", + "label": "required params", "run": _smoke_case_1, }, + { + "operation": "create", + "method": "POST", + "path": "/v1/machines", + "label": "all params", + "run": _smoke_case_2, + }, { "operation": "retrieve", "method": "GET", "path": "/v1/machines/{machine_id}", - "run": _smoke_case_2, + "run": _smoke_case_3, }, { "operation": "update", "method": "PATCH", "path": "/v1/machines/{machine_id}", "label": "required params", - "run": _smoke_case_3, + "run": _smoke_case_4, }, { "operation": "update", "method": "PATCH", "path": "/v1/machines/{machine_id}", "label": "all params", - "run": _smoke_case_4, + "run": _smoke_case_5, }, { "operation": "delete", "method": "DELETE", "path": "/v1/machines/{machine_id}", - "run": _smoke_case_5, + "label": "required params", + "run": _smoke_case_6, + }, + { + "operation": "delete", + "method": "DELETE", + "path": "/v1/machines/{machine_id}", + "label": "all params", + "run": _smoke_case_7, }, { "operation": "sleep", "method": "POST", "path": "/v1/machines/{machine_id}/sleep", - "run": _smoke_case_6, + "run": _smoke_case_8, }, { "operation": "wake", "method": "POST", "path": "/v1/machines/{machine_id}/wake", - "run": _smoke_case_7, + "run": _smoke_case_9, }, { "operation": "reboot", "method": "POST", "path": "/v1/machines/{machine_id}/reboot", "label": "required params", - "run": _smoke_case_8, + "run": _smoke_case_10, }, { "operation": "reboot", "method": "POST", "path": "/v1/machines/{machine_id}/reboot", "label": "all params", - "run": _smoke_case_9, + "run": _smoke_case_11, + }, + { + "operation": "resize", + "method": "POST", + "path": "/v1/machines/{machine_id}/resize", + "label": "required params", + "run": _smoke_case_12, + }, + { + "operation": "resize", + "method": "POST", + "path": "/v1/machines/{machine_id}/resize", + "label": "all params", + "run": _smoke_case_13, }, { "operation": "list", "method": "GET", "path": "/v1/machines/{machine_id}/ssh", - "run": _smoke_case_10, + "run": _smoke_case_14, }, { "operation": "create", "method": "POST", "path": "/v1/machines/{machine_id}/ssh", - "run": _smoke_case_11, + "run": _smoke_case_15, }, { "operation": "retrieve", "method": "GET", "path": "/v1/machines/{machine_id}/ssh/{session_id}", - "run": _smoke_case_12, + "run": _smoke_case_16, }, { "operation": "delete", "method": "DELETE", "path": "/v1/machines/{machine_id}/ssh/{session_id}", - "run": _smoke_case_13, + "run": _smoke_case_17, }, { "operation": "list", "method": "GET", "path": "/v1/machines/{machine_id}/executions", - "run": _smoke_case_14, + "run": _smoke_case_18, }, { "operation": "create", "method": "POST", "path": "/v1/machines/{machine_id}/executions", "label": "required params", - "run": _smoke_case_15, + "run": _smoke_case_19, }, { "operation": "create", "method": "POST", "path": "/v1/machines/{machine_id}/executions", "label": "all params", - "run": _smoke_case_16, + "run": _smoke_case_20, }, { "operation": "retrieve", "method": "GET", "path": "/v1/machines/{machine_id}/executions/{execution_id}", - "run": _smoke_case_17, + "run": _smoke_case_21, }, { "operation": "delete", "method": "DELETE", "path": "/v1/machines/{machine_id}/executions/{execution_id}", - "run": _smoke_case_18, + "run": _smoke_case_22, }, { "operation": "output", "method": "GET", "path": "/v1/machines/{machine_id}/executions/{execution_id}/output", - "run": _smoke_case_19, + "run": _smoke_case_23, }, { "operation": "events", "method": "GET", "path": "/v1/machines/{machine_id}/executions/{execution_id}/events", - "run": _smoke_case_20, + "run": _smoke_case_24, }, { "operation": "retrieve", "method": "GET", "path": "/v1/machines/{machine_id}/executions/{execution_id}/logs", - "run": _smoke_case_21, - }, - { - "operation": "reauthorize", - "method": "POST", - "path": "/v1/machines/{machine_id}/executions/{execution_id}/logs/reauthorize", - "run": _smoke_case_22, + "run": _smoke_case_25, }, { "operation": "createToken", "method": "POST", "path": "/v1/machines/{machine_id}/executions/{execution_id}/logs/token", - "run": _smoke_case_23, + "run": _smoke_case_26, }, { "operation": "retrieve", "method": "GET", "path": "/v1/machines/{machine_id}/autoresizing", - "run": _smoke_case_24, + "run": _smoke_case_27, + }, + { + "operation": "update", + "method": "PATCH", + "path": "/v1/machines/{machine_id}/autoresizing", + "label": "required params", + "run": _smoke_case_28, }, { "operation": "update", - "method": "PUT", + "method": "PATCH", "path": "/v1/machines/{machine_id}/autoresizing", - "run": _smoke_case_25, + "label": "all params", + "run": _smoke_case_29, }, { "operation": "retrieve", "method": "GET", "path": "/v1/organization/autoresizing", - "run": _smoke_case_26, + "run": _smoke_case_30, }, { "operation": "update", - "method": "PUT", + "method": "PATCH", "path": "/v1/organization/autoresizing", - "run": _smoke_case_27, + "label": "required params", + "run": _smoke_case_31, + }, + { + "operation": "update", + "method": "PATCH", + "path": "/v1/organization/autoresizing", + "label": "all params", + "run": _smoke_case_32, + }, + { + "operation": "list", + "method": "GET", + "path": "/v1/images", + "label": "required params", + "run": _smoke_case_33, + }, + { + "operation": "list", + "method": "GET", + "path": "/v1/images", + "label": "all params", + "run": _smoke_case_34, + }, + { + "operation": "create", + "method": "POST", + "path": "/v1/images", + "label": "required params", + "run": _smoke_case_35, + }, + { + "operation": "create", + "method": "POST", + "path": "/v1/images", + "label": "all params", + "run": _smoke_case_36, + }, + { + "operation": "retrieve", + "method": "GET", + "path": "/v1/images/{image_id}", + "run": _smoke_case_37, + }, + { + "operation": "update", + "method": "PATCH", + "path": "/v1/images/{image_id}", + "run": _smoke_case_38, + }, + { + "operation": "delete", + "method": "DELETE", + "path": "/v1/images/{image_id}", + "run": _smoke_case_39, + }, + { + "operation": "create", + "method": "POST", + "path": "/v1/images/uploads", + "run": _smoke_case_40, }, ] diff --git a/tests/test_images.py b/tests/test_images.py new file mode 100644 index 0000000..b4aa2d0 --- /dev/null +++ b/tests/test_images.py @@ -0,0 +1,310 @@ +from __future__ import annotations + +import base64 +import hashlib +import json + +import httpx +import pytest + +from dedalus_sdk import APITimeoutError, AsyncDedalus, Dedalus +from dedalus_sdk.images import ImagePreparationError, async_import_image, async_poll_image, import_image, poll_image + +IMAGE_ID = "01994a10-1234-7000-8000-000000000001" +ARCHIVE = b"fixture docker-save archive" +DIGEST = hashlib.sha256(ARCHIVE).hexdigest() + + +def fixture(states=None, mutate=lambda grant: None, upload_status=200): + requests, uploads, keys = [], [], [] + states = iter(states or ["ready"]) + grant = { + "source": {"kind": "docker_archive", "upload_id": IMAGE_ID, "sha256": DIGEST}, + "upload": { + "url": "https://storage.example/image?signature=secret", + "method": "PUT", + "expires_at": "2099-01-01T00:00:00Z", + "headers": { + "content-length": [str(len(ARCHIVE))], + "if-none-match": ["*"], + "x-amz-checksum-sha256": [base64.b64encode(bytes.fromhex(DIGEST)).decode()], + }, + }, + } + mutate(grant) + + def api(request): + requests.append(request.url.path) + assert "fixture-secret" in request.headers["authorization"] + if request.method == "POST": + keys.append(request.headers["idempotency-key"]) + if request.url.path.endswith("/uploads"): + assert json.loads(request.content)["sha256"] == DIGEST + return httpx.Response(200, json=grant) + if request.url.path == "/v1/machines": + assert json.loads(request.content)["image_id"] == IMAGE_ID + return httpx.Response( + 200, + json={ + "machine_id": "fixture", + "lifecycle_status": { + "autoresize": { + "cpu": {"blocked_reason": "MACHINE_QUOTA_EXCEEDED", "blocked_at": "2026-01-01T00:00:00Z"}, + "memory": None, + } + }, + }, + ) + state = "submitted" if request.method == "POST" else next(states, "preparing") + return httpx.Response(202 if request.method == "POST" else 200, json={"image_id": IMAGE_ID, "state": state}) + + def storage(request): + uploads.append(request) + assert "authorization" not in request.headers + assert request.content == ARCHIVE + return httpx.Response(upload_status) + + return httpx.MockTransport(api), httpx.MockTransport(storage), requests, uploads, keys + + +@pytest.fixture +def archive(tmp_path): + path = tmp_path / "image.tar" + path.write_bytes(ARCHIVE) + return path + + +@pytest.mark.parametrize("wait", [True, False]) +def test_import_sync(archive, wait): + api, storage, requests, uploads, keys = fixture(["preparing", "ready"]) + progress = [] + with Dedalus(api_key="fixture-secret", http_client=httpx.Client(transport=api)) as client: + result = import_image( + client, + archive, + wait=wait, + poll_interval=0.001, + upload_transport=storage, + on_progress=lambda image: progress.append(image["state"]), + ) + assert result["state"] == ("ready" if wait else "submitted") + assert progress == (["submitted", "preparing", "ready"] if wait else ["submitted"]) + assert len(uploads) == 1 and keys[0] == keys[1] + assert len(requests) == (4 if wait else 2) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("wait", [True, False]) +async def test_import_async(archive, wait): + api, storage, requests, uploads, keys = fixture(["preparing", "ready"]) + async with AsyncDedalus(api_key="fixture-secret", http_client=httpx.AsyncClient(transport=api)) as client: + result = await async_import_image(client, archive, wait=wait, poll_interval=0.001, upload_transport=storage) + assert result["state"] == ("ready" if wait else "submitted") + assert len(uploads) == 1 and keys[0] == keys[1] + assert len(requests) == (4 if wait else 2) + + +@pytest.mark.parametrize( + "change", + [ + lambda g: g["upload"].update(url="http://storage.example/x"), + lambda g: g["upload"].update(method="POST"), + lambda g: g["upload"].update(expires_at="invalid"), + lambda g: g["source"].update(sha256="wrong"), + lambda g: g["upload"]["headers"].update(authorization=["secret"]), + lambda g: g["upload"]["headers"].update({"content-length": ["1"]}), + ], +) +def test_invalid_capability_never_uploads_or_registers(archive, change): + api, storage, requests, uploads, _ = fixture(mutate=change) + with Dedalus(api_key="fixture-secret", http_client=httpx.Client(transport=api)) as client: + with pytest.raises(ValueError, match="Invalid image upload"): + import_image(client, archive, upload_transport=storage) + assert len(requests) == 1 and not uploads + + +@pytest.mark.parametrize("state", ["failed", "rejected", "revoked"]) +def test_failure_preserves_image_id(state): + api, _, requests, _, _ = fixture([state]) + with Dedalus(api_key="fixture-secret", http_client=httpx.Client(transport=api)) as client: + with pytest.raises(ImagePreparationError, match=state) as error: + poll_image(client, IMAGE_ID) + assert error.value.image["image_id"] == IMAGE_ID + assert requests == [f"/v1/images/{IMAGE_ID}"] + + +def test_sync_status_request_timeout_preserves_accepted_image(archive): + api, storage, requests, _, _ = fixture() + timeout = httpx.ReadTimeout("status response timed out") + + def handle(request): + if request.method == "GET": + timeout.request = request + raise timeout + return api.handle_request(request) + + with ( + Dedalus(api_key="fixture-secret", http_client=httpx.Client(transport=httpx.MockTransport(handle))) as client, + pytest.raises(ImagePreparationError, match="Preparation continues.*resume polling") as error, + ): + import_image(client, archive, poll_interval=0.001, upload_transport=storage) + + assert requests == ["/v1/images/uploads", "/v1/images"] + assert error.value.image == {"image_id": IMAGE_ID, "state": "unknown"} + assert IMAGE_ID in str(error.value) + assert isinstance(error.value.__cause__, APITimeoutError) + assert error.value.__cause__.__cause__ is timeout + assert timeout.request.url.path == f"/v1/images/{IMAGE_ID}" + + +@pytest.mark.asyncio +async def test_async_status_request_timeout_preserves_accepted_image(archive): + api, storage, requests, _, _ = fixture() + timeout = httpx.ReadTimeout("status response timed out") + + async def handle(request): + if request.method == "GET": + timeout.request = request + raise timeout + return await api.handle_async_request(request) + + async with AsyncDedalus( + api_key="fixture-secret", max_retries=0, http_client=httpx.AsyncClient(transport=httpx.MockTransport(handle)) + ) as client: + with pytest.raises(ImagePreparationError, match="Preparation continues.*resume polling") as error: + await async_import_image(client, archive, poll_interval=0.001, upload_transport=storage) + + assert requests == ["/v1/images/uploads", "/v1/images"] + assert error.value.image == {"image_id": IMAGE_ID, "state": "unknown"} + assert IMAGE_ID in str(error.value) + assert isinstance(error.value.__cause__, APITimeoutError) + assert error.value.__cause__.__cause__ is timeout + assert timeout.request.url.path == f"/v1/images/{IMAGE_ID}" + + +@pytest.mark.asyncio +async def test_async_failure_timeout_and_cancellation(): + import asyncio + + api, _, requests, _, _ = fixture(["failed"]) + async with AsyncDedalus(api_key="fixture-secret", http_client=httpx.AsyncClient(transport=api)) as client: + with pytest.raises(ImagePreparationError, match="failed"): + await async_poll_image(client, IMAGE_ID) + with pytest.raises(ImagePreparationError, match="Preparation continues") as error: + await async_poll_image(client, IMAGE_ID, timeout=0.02, poll_interval=0.005) + assert error.value.image["image_id"] == IMAGE_ID + assert isinstance(error.value.__cause__, TimeoutError) + task = asyncio.create_task(async_poll_image(client, IMAGE_ID, poll_interval=0.005)) + await asyncio.sleep(0.01) + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + assert all(path == f"/v1/images/{IMAGE_ID}" for path in requests) + + +def test_sync_unknown_states_and_timeout(): + api, _, _, _, _ = fixture(["future-state", "ready"]) + with Dedalus(api_key="fixture-secret", http_client=httpx.Client(transport=api)) as client: + assert poll_image(client, IMAGE_ID, poll_interval=0.001)["state"] == "ready" + with pytest.raises(ImagePreparationError, match="Preparation continues"): + poll_image(client, IMAGE_ID, timeout=0.02, poll_interval=0.005) + + +@pytest.mark.parametrize("status", [302, 500, 412]) +def test_upload_failure_never_registers_but_repeated_put_can(archive, status): + api, storage, requests, _, _ = fixture(upload_status=status) + with Dedalus(api_key="fixture-secret", http_client=httpx.Client(transport=api)) as client: + if status == 412: + assert import_image(client, archive, wait=False, upload_transport=storage)["state"] == "submitted" + else: + with pytest.raises(RuntimeError, match=f"HTTP {status}"): + import_image(client, archive, upload_transport=storage) + assert len(requests) == 1 + + +def test_ready_image_to_generated_machine(archive): + api, storage, requests, _, _ = fixture(["ready"]) + with Dedalus(api_key="fixture-secret", http_client=httpx.Client(transport=api)) as client: + ready = import_image(client, archive, upload_transport=storage, poll_interval=0.001) + machine = client.machines.create(image_id=ready["image_id"]) + assert machine.machine_id == "fixture" + assert requests == ["/v1/images/uploads", "/v1/images", f"/v1/images/{IMAGE_ID}", "/v1/machines"] + + +@pytest.mark.asyncio +async def test_async_ready_image_to_generated_machine(archive): + api, storage, requests, _, _ = fixture(["ready"]) + async with AsyncDedalus(api_key="fixture-secret", http_client=httpx.AsyncClient(transport=api)) as client: + ready = await async_import_image(client, archive, upload_transport=storage, poll_interval=0.001) + machine = await client.machines.create(image_id=ready["image_id"]) + assert machine.machine_id == "fixture" + assert requests[-1] == "/v1/machines" + + +def test_generated_patch_resize_and_flat_errors(): + from dedalus_sdk import APIStatusError + + requests = [] + envelope = { + "error_code": "IMAGE_SOURCE_INVALID", + "message": "archive rejected", + "retryable": False, + "details": {"stage": "validation"}, + "retry_after_ms": 500, + } + + def api(request): + requests.append((request.method, request.url.path)) + return ( + httpx.Response(422, json=envelope) + if "/images/" in request.url.path + else httpx.Response( + 200, + json={ + "status": { + "phase": "running", + "reason": "", + "retryable": False, + "revision": "1", + "last_transition_at": "2026-01-01T00:00:00Z", + "last_progress_at": "2026-01-01T00:00:00Z", + "autoresize": {"cpu": {"blocked_reason": "MACHINE_QUOTA_EXCEEDED"}, "memory": None}, + } + }, + ) + ) + + with Dedalus( + api_key="fixture", max_retries=0, http_client=httpx.Client(transport=httpx.MockTransport(api)) + ) as client: + client.machines.autoresizing.update(machine_id=IMAGE_ID) + client.organization.autoresizing.update() + resized = client.machines.resize(machine_id=IMAGE_ID, vcpu=2) + assert resized.status.autoresize.cpu.blocked_reason == "MACHINE_QUOTA_EXCEEDED" + assert resized.status.autoresize.memory is None + with pytest.raises(APIStatusError) as error: + client.images.retrieve(image_id=IMAGE_ID) + assert error.value.body == envelope + assert "archive rejected" in str(error.value) + assert requests[:3] == [ + ("PATCH", f"/v1/machines/{IMAGE_ID}/autoresizing"), + ("PATCH", "/v1/organization/autoresizing"), + ("POST", f"/v1/machines/{IMAGE_ID}/resize"), + ] + + +def test_unsupported_platform_never_reserves(archive): + api, storage, requests, uploads, _ = fixture() + with Dedalus(api_key="fixture-secret", http_client=httpx.Client(transport=api)) as client: + with pytest.raises(ValueError, match="linux/amd64"): + import_image(client, archive, platform="linux/arm64", upload_transport=storage) + assert not requests and not uploads + + +@pytest.mark.asyncio +async def test_async_unsupported_platform_never_reserves(archive): + api, storage, requests, uploads, _ = fixture() + async with AsyncDedalus(api_key="fixture-secret", http_client=httpx.AsyncClient(transport=api)) as client: + with pytest.raises(ValueError, match="linux/amd64"): + await async_import_image(client, archive, platform="linux/arm64", upload_transport=storage) + assert not requests and not uploads