diff --git a/.github/cla-allowlist.txt b/.github/cla-allowlist.txt new file mode 100644 index 00000000..c3c08db3 --- /dev/null +++ b/.github/cla-allowlist.txt @@ -0,0 +1,25 @@ +# Accounts exempt from the CLA check (.github/workflows/cla.yml). +# One GitHub username per line; lines starting with # and blank lines are +# ignored. '*' wildcards are supported (e.g. bot accounts). +# +# The workflow reads this file from the default branch, so changes take +# effect once they land there — a PR cannot allowlist its own author. +# +# Bots: +dependabot[bot] +renovate[bot] + +# Tower employees: exempt because they have already signed equivalent +# agreements as part of employment; the CLA record for them lives in their +# employment paperwork, not in signatures/. Keep in sync with org +# membership; remove people when they leave (their later contributions need +# a signature like anyone else's). +bradhe +datancoffee +giray123 +jo-sm +konstantinoscs +MemoAlfa +mesmith027 +sammuti +socksy diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 00000000..184fa342 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,102 @@ +# CLA enforcement via CLA Assistant Lite (contributor-assistant/github-action). +# +# NOTE: the upstream repository (contributor-assistant/github-action) was +# archived in March 2026 and is read-only. Existing releases keep working. +# This is deliberate: unlike the hosted cla-assistant.io app, this action +# stores signatures inside this repository, which is a hard requirement. +# Consequences: +# - The action is pinned to a commit SHA (of the last release, v2.6.1). +# - Treat it as frozen: do not add Dependabot/Renovate config for it, and +# do not expect upstream updates or fixes. +# +# Signature storage: the action writes cla.json to `develop` directly (it +# cannot open PRs), which the develop ruleset's pull-request requirement +# would normally reject — and GitHub does not allow the built-in Actions +# token to bypass rulesets. So the write goes through a dedicated org-owned +# GitHub App (Contents: read/write only): its token is minted below and +# passed as PERSONAL_ACCESS_TOKEN, which the action uses for signature +# persistence only (comments/status/lock still use GITHUB_TOKEN). The app +# must be installed on this repo and listed as a bypass actor on the develop +# ruleset, or signing fails at the moment someone signs. +# Secrets required: CLA_APP_ID, CLA_APP_PRIVATE_KEY. +# cla.json reaches main through the normal develop -> main release merges. +# +# Signature versioning: signatures are stored under signatures/version1/. +# If CLA.md is ever materially changed, bump the path to signatures/version2/ +# (and so on) so that prior signatures do not silently appear to cover text +# nobody agreed to. +name: CLA Assistant +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened, closed, synchronize] + +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + +jobs: + cla: + runs-on: ubuntu-latest + # The `github.event.issue.pull_request` guard is required: without it, + # comments on ordinary issues (not PRs) trigger the workflow. The upstream + # README example omits it; that is a known upstream bug. + if: > + github.event_name == 'pull_request_target' || + (github.event.issue.pull_request && + contains(fromJSON('["recheck","I have read the CLA Document and I hereby sign the CLA"]'), + github.event.comment.body)) + # One run at a time per PR: `opened` + `synchronize` arriving together + # would otherwise race on the cla.json write. + concurrency: + group: cla-${{ github.event.pull_request.number || github.event.issue.number }} + cancel-in-progress: false + steps: + # The action only accepts the allowlist as a string input, so it is + # tracked in .github/cla-allowlist.txt and loaded here. It is pinned to + # `develop` (maintainer-controlled, matches the signature branch) so a + # PR cannot allowlist its own author by editing the file, and so + # issue_comment runs — which execute from the default branch — read the + # same copy as pull_request_target runs. pipefail makes a failed fetch + # fail the job instead of silently producing an empty allowlist. + - name: Load allowlist + id: allowlist + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + list=$(gh api "repos/${{ github.repository }}/contents/.github/cla-allowlist.txt?ref=develop" --jq '.content' \ + | base64 --decode | grep -vE '^[[:space:]]*(#|$)' | paste -sd, -) + echo "list=$list" >> "$GITHUB_OUTPUT" + - name: Mint signature-write token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.CLA_APP_ID }} + private-key: ${{ secrets.CLA_APP_PRIVATE_KEY }} + - uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 — upstream archived 2026-03 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PERSONAL_ACCESS_TOKEN: ${{ steps.app-token.outputs.token }} + with: + path-to-signatures: 'signatures/version1/cla.json' + path-to-document: 'https://github.com/tower/tower-cli/blob/develop/CLA.md' + branch: 'develop' + # Pointing "remote" at this same repo is what routes the signature + # write through PERSONAL_ACCESS_TOKEN (the app token) instead of + # GITHUB_TOKEN — the action only uses the PAT client when a remote + # repo/org is configured. + remote-organization-name: 'tower' + remote-repository-name: 'tower-cli' + allowlist: ${{ steps.allowlist.outputs.list }} + # Without this, the action matches the sign phrase as a substring + # (regex .*phrase.*), so "... I hereby sign the CLA, but I do not + # agree" would count as a signature. Setting custom-pr-sign-comment + # switches the action to exact (trimmed, case-insensitive) matching. + custom-pr-sign-comment: 'I have read the CLA Document and I hereby sign the CLA' + # lock-on-merge is left at its default (enabled) on purpose: locking + # the PR conversation after merge is what makes the signature + # comments immutable, which is the evidentiary value of the record. diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index f917c397..18ad47b8 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -7,6 +7,9 @@ name: "[tower] Test python" on: pull_request: + schedule: + # Detect new stable PyArrow/PyIceberg releases even when no PR is open. + - cron: "17 6 * * *" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -14,6 +17,7 @@ concurrency: jobs: test: + if: github.event_name == 'pull_request' runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -43,3 +47,55 @@ jobs: - name: Run tests run: uv run pytest tests + + iceberg-compatibility: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - dependency_set: minimum + pyarrow: "pyarrow==23.0.1" + pyiceberg: "pyiceberg[sql-sqlite]==0.11.1" + - dependency_set: latest + pyarrow: "pyarrow" + pyiceberg: "pyiceberg[sql-sqlite]" + + steps: + - uses: actions/checkout@v6 + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v6 + + - name: "Set up Python" + uses: actions/setup-python@v6 + with: + python-version-file: ".python-version" + + - name: Install the project + run: uv sync --locked --all-extras --dev + + - name: Resolve ${{ matrix.dependency_set }} Iceberg dependencies + run: >- + uv pip install + --python .venv/bin/python + --strict + --resolution highest + --upgrade-package pyarrow + --upgrade-package pyiceberg + '${{ matrix.pyarrow }}' + '${{ matrix.pyiceberg }}' + + - name: Show resolved dependency versions + run: | + uv run --no-sync python - <<'PY' + from importlib.metadata import version + + print(f"PyArrow {version('pyarrow')}; PyIceberg {version('pyiceberg')}") + PY + + - name: Run Iceberg compatibility tests + run: >- + uv run --no-sync pytest + tests/tower/test_table*.py + tests/tower/test_storage.py diff --git a/.gitignore b/.gitignore index 01c5d225..66143969 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ __pycache__ # may contain sensitive data pytest.ini -# +# # Artifacts from the Rust client generation process # /crates/tower-api/.openapi-generator-ignore @@ -31,3 +31,8 @@ pytest.ini # local MCP overrides (e.g. internal tooling with secrets) .mcp.json.local + +# llm/AI harness folders plus a .plans folder for storing LLM plans +.claude +.codex +.plans diff --git a/CLA.md b/CLA.md new file mode 100644 index 00000000..75a28f49 --- /dev/null +++ b/CLA.md @@ -0,0 +1,121 @@ +# Tower Individual Contributor License Agreement + +Before your pull request can be merged, Tower asks you to sign this +contributor license agreement. Signing does not transfer ownership of your +work: you keep the copyright in your contributions and grant Tower and users +of Tower's software a license to use, modify, and distribute them. You sign +by replying to the CLA bot's comment on your pull request, and the signature +is recorded in this repository. + +--- + +Thank you for your interest in Tower Computing GmbH ("Tower"). To clarify the +intellectual property license granted with Contributions from any person or +entity, Tower must have on file a signed Contributor License Agreement +("CLA") from each Contributor, indicating agreement with the license terms +below. This agreement is for your protection as a Contributor as well as the +protection of Tower and its users. It does not change your rights to use your +own Contributions for any other purpose. + +You sign this Agreement electronically by posting the signature comment +requested by the CLA workflow on your GitHub pull request. Your GitHub +username and the metadata recorded by the workflow constitute the record of +your signature and are stored in this repository. + +You accept and agree to the following terms and conditions for Your +Contributions (present and future) that you submit to Tower. Except for the +license granted herein to Tower and recipients of software distributed by +Tower, You reserve all right, title, and interest in and to Your +Contributions. + +1. Definitions. + + "You" (or "Your") shall mean the copyright owner or legal entity + authorized by the copyright owner that is making this Agreement with + Tower. For legal entities, the entity making a Contribution and all other + entities that control, are controlled by, or are under common control with + that entity are considered to be a single Contributor. For the purposes of + this definition, "control" means (i) the power, direct or indirect, to + cause the direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "Contribution" shall mean any original work of authorship, including any + modifications or additions to an existing work, that is intentionally + submitted by You to Tower for inclusion in, or documentation of, any of + the products owned or managed by Tower (the "Work"). For the purposes of + this definition, "submitted" means any form of electronic, verbal, or + written communication sent to Tower or its representatives, including but + not limited to communication on electronic mailing lists, source code + control systems, and issue tracking systems that are managed by, or on + behalf of, Tower for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by You as "Not a Contribution." + +2. Grant of Copyright License. Subject to the terms and conditions of this + Agreement, You hereby grant to Tower and to recipients of software + distributed by Tower a perpetual, worldwide, non-exclusive, no-charge, + royalty-free, irrevocable copyright license to reproduce, prepare + derivative works of, publicly display, publicly perform, sublicense, and + distribute Your Contributions and such derivative works. + +3. Grant of Patent License. Subject to the terms and conditions of this + Agreement, You hereby grant to Tower and to recipients of software + distributed by Tower a perpetual, worldwide, non-exclusive, no-charge, + royalty-free, irrevocable (except as stated in this section) patent + license to make, have made, use, offer to sell, sell, import, and + otherwise transfer the Work, where such license applies only to those + patent claims licensable by You that are necessarily infringed by Your + Contribution(s) alone or by combination of Your Contribution(s) with the + Work to which such Contribution(s) was submitted. If any entity institutes + patent litigation against You or any other entity (including a cross-claim + or counterclaim in a lawsuit) alleging that your Contribution, or the Work + to which you have contributed, constitutes direct or contributory patent + infringement, then any patent licenses granted to that entity under this + Agreement for that Contribution or Work shall terminate as of the date + such litigation is filed. + +4. You represent that you are legally entitled to grant the above license. + If your employer(s) has rights to intellectual property that you create + that includes your Contributions, you represent that you have received + permission to make Contributions on behalf of that employer, that your + employer has waived such rights for your Contributions to Tower, or that + your employer has executed a separate Corporate CLA with Tower. + +5. You represent that each of Your Contributions is Your original creation + (see section 7 for submissions on behalf of others). You represent that + Your Contribution submissions include complete details of any third-party + license or other restriction (including, but not limited to, related + patents and trademarks) of which you are personally aware and which are + associated with any part of Your Contributions. + +6. You are not expected to provide support for Your Contributions, except to + the extent You desire to provide support. You may provide support for + free, for a fee, or not at all. Unless required by applicable law or + agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + including, without limitation, any warranties or conditions of TITLE, + NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + +7. Should You wish to submit work that is not Your original creation, You + may submit it to Tower separately from any Contribution, identifying the + complete details of its source and of any license or other restriction + (including, but not limited to, related patents, trademarks, and license + agreements) of which you are personally aware, and conspicuously marking + the work as "Submitted on behalf of a third-party: [named here]". + +8. You agree to notify Tower of any facts or circumstances of which you + become aware that would make these representations inaccurate in any + respect. + +9. Prior Contributions. This Agreement applies to all Contributions You + submitted to Tower before the date You sign this Agreement, as well as to + all Contributions You submit on or after that date. The licenses granted + in sections 2 and 3, and the representations made in sections 4, 5, 7, + and 8, apply equally to such prior Contributions. + +10. Governing Law. This Agreement shall be governed by and construed in + accordance with the laws of the Federal Republic of Germany, without + regard to its conflict of laws principles. Any dispute arising out of or + relating to this Agreement shall be subject to the exclusive jurisdiction + of the courts at the registered seat of Tower Computing GmbH in Germany. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12749ce9..51c5a1d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,6 +27,14 @@ If you still need help: > **Legal Notice:** When contributing, you must agree that you have authored 100% of the content, have the necessary rights, and that it may be provided under the project license. +### Contributor License Agreement + +A signed [Contributor License Agreement](CLA.md) is required before any pull request can be merged. + +Signing happens on the pull request itself: when you open a PR, a bot comments with a link to the CLA and a required status check fails. Reply to the bot's comment with the exact phrase it asks for (`I have read the CLA Document and I hereby sign the CLA`) and the check clears. You only need to do this once; it covers your future pull requests too. + +If you can't sign on your own behalf — for example, because your employer holds rights in your work — please say so before submitting a pull request. + ### Reporting Bugs #### Before Submitting diff --git a/Cargo.lock b/Cargo.lock index 1d8f7edb..f3eb3398 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -841,7 +841,7 @@ dependencies = [ [[package]] name = "config" -version = "0.3.70" +version = "0.3.71" dependencies = [ "base64", "chrono", @@ -1033,7 +1033,7 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto" -version = "0.3.70" +version = "0.3.71" dependencies = [ "aes-gcm", "base64", @@ -4456,7 +4456,7 @@ dependencies = [ [[package]] name = "testutils" -version = "0.3.70" +version = "0.3.71" dependencies = [ "pem", "rsa", @@ -4765,7 +4765,7 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "tower" -version = "0.3.70" +version = "0.3.71" dependencies = [ "config", "pyo3", @@ -4793,7 +4793,7 @@ dependencies = [ [[package]] name = "tower-api" -version = "0.3.70" +version = "0.3.71" dependencies = [ "reqwest", "serde", @@ -4805,7 +4805,7 @@ dependencies = [ [[package]] name = "tower-cmd" -version = "0.3.70" +version = "0.3.71" dependencies = [ "axum", "bytes", @@ -4854,7 +4854,7 @@ dependencies = [ [[package]] name = "tower-duckdb" -version = "0.3.70" +version = "0.3.71" dependencies = [ "chrono", "duckdb", @@ -4889,7 +4889,7 @@ checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-package" -version = "0.3.70" +version = "0.3.71" dependencies = [ "async-compression", "flate2", @@ -4914,7 +4914,7 @@ dependencies = [ [[package]] name = "tower-runtime" -version = "0.3.70" +version = "0.3.71" dependencies = [ "async-trait", "chrono", @@ -4938,7 +4938,7 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tower-telemetry" -version = "0.3.70" +version = "0.3.71" dependencies = [ "tracing", "tracing-appender", @@ -4947,7 +4947,7 @@ dependencies = [ [[package]] name = "tower-uv" -version = "0.3.70" +version = "0.3.71" dependencies = [ "async-compression", "async_zip", @@ -4967,7 +4967,7 @@ dependencies = [ [[package]] name = "tower-version" -version = "0.3.70" +version = "0.3.71" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 2866d01d..8a369227 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.3.70" +version = "0.3.71" description = "Tower is the best way to host Python data apps in production" # Matches rust-toolchain.toml. The two had drifted: the toolchain has been 1.88 # for a while, and the dependency tree (testcontainers and its transitive deps, diff --git a/INSTALL-AND-REFERENCE.md b/INSTALL-AND-REFERENCE.md index 3dd698de..0cccf67e 100644 --- a/INSTALL-AND-REFERENCE.md +++ b/INSTALL-AND-REFERENCE.md @@ -128,8 +128,27 @@ pip install "tower[ai]" pip install "tower[iceberg]" ``` -- `tower.create_table`: create Iceberg tables -- `tower.load_table`: load data from Iceberg tables +- `tower.tables(...)`: load, create, update, and delete Iceberg table data + +Delete filters are SQL-like strings or native PyIceberg boolean expressions. The +`Table.column()` builder creates composable PyIceberg predicates: + +```python +table = tower.tables("events").load() +table.delete( + (table.column("age") >= 18) + & ~(table.column("status") == "inactive") +) +``` + +Arrow schemas passed to `create()` or `create_if_not_exists()` are validated directly by +PyIceberg, which assigns field IDs and preserves nested nullability and `b"doc"` field +metadata. Timestamp units from seconds through microseconds, UTC-zoned microsecond +timestamps, `time64[us]`, `date32`, and Decimal128 values up to precision 38 are supported. +Nanosecond timestamps are rejected by default instead of being silently downcast, as are +`time32`, `time64[ns]`, `date64`, Float16, Decimal256, and non-UTC zoned timestamps. Convert +those fields explicitly before creating the table when the loss is acceptable. PyIceberg's +native validation exceptions propagate unchanged. ### dbt Core support diff --git a/LICENSE b/LICENSE index 34da8d48..486f8251 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Tower Computing Inc. +Copyright (c) 2024 Tower Computing GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/crates/tower-api/README.md b/crates/tower-api/README.md index 6c22dca1..70d5aeb4 100644 --- a/crates/tower-api/README.md +++ b/crates/tower-api/README.md @@ -8,7 +8,7 @@ For more information, please visit [https://tower.dev](https://tower.dev) This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. -- API version: v0.11.17 +- API version: v0.11.24 - Package version: 1.0.0 - Generator version: 7.19.0 - Build package: `org.openapitools.codegen.languages.RustClientCodegen` @@ -30,6 +30,8 @@ Class | Method | HTTP request | Description *DefaultApi* | [**acknowledge_alert**](docs/DefaultApi.md#acknowledge_alert) | **POST** /alerts/{alert_seq}/acknowledge | Acknowledge alert *DefaultApi* | [**acknowledge_all_alerts**](docs/DefaultApi.md#acknowledge_all_alerts) | **POST** /alerts/acknowledge-all | Acknowledge all alerts *DefaultApi* | [**activate_schedules**](docs/DefaultApi.md#activate_schedules) | **PATCH** /schedules/activate | Activate multiple schedules +*DefaultApi* | [**batch_describe_runs**](docs/DefaultApi.md#batch_describe_runs) | **POST** /batch/describe-runs | Batch describe runs +*DefaultApi* | [**batch_describe_runs_logs**](docs/DefaultApi.md#batch_describe_runs_logs) | **POST** /batch/describe-runs-logs | Batch describe runs logs *DefaultApi* | [**cancel_run**](docs/DefaultApi.md#cancel_run) | **POST** /apps/{name}/runs/{seq} | Cancel run *DefaultApi* | [**check_webhook**](docs/DefaultApi.md#check_webhook) | **POST** /webhooks/{name}/test | Check webhook *DefaultApi* | [**claim_device_login_ticket**](docs/DefaultApi.md#claim_device_login_ticket) | **POST** /login/device/claim | Claim a device login ticket @@ -71,6 +73,7 @@ Class | Method | HTTP request | Description *DefaultApi* | [**describe_authentication_context**](docs/DefaultApi.md#describe_authentication_context) | **GET** /user/auth-context | Describe authentication context *DefaultApi* | [**describe_catalog**](docs/DefaultApi.md#describe_catalog) | **GET** /catalogs/{name} | Describe catalog *DefaultApi* | [**describe_catalog_fact**](docs/DefaultApi.md#describe_catalog_fact) | **GET** /catalogs/{catalog}/facts/{name} | Describe a catalog fact +*DefaultApi* | [**describe_catalog_usage**](docs/DefaultApi.md#describe_catalog_usage) | **GET** /catalogs/{name}/usage | Describe catalog usage *DefaultApi* | [**describe_default_catalog**](docs/DefaultApi.md#describe_default_catalog) | **GET** /storage/catalogs/default | Describe default catalog *DefaultApi* | [**describe_device_login_session**](docs/DefaultApi.md#describe_device_login_session) | **GET** /login/device/{device_code} | Describe device login session *DefaultApi* | [**describe_email_preferences**](docs/DefaultApi.md#describe_email_preferences) | **GET** /user/email-preferences | Describe email preferences @@ -159,13 +162,20 @@ Class | Method | HTTP request | Description - [AppTag](docs/AppTag.md) - [AppVersion](docs/AppVersion.md) - [AuthenticationContext](docs/AuthenticationContext.md) + - [BatchDescribeRunsLogsParams](docs/BatchDescribeRunsLogsParams.md) + - [BatchDescribeRunsParams](docs/BatchDescribeRunsParams.md) + - [BatchDescribeRunsResponse](docs/BatchDescribeRunsResponse.md) + - [BatchError](docs/BatchError.md) + - [BatchRunAndLinks](docs/BatchRunAndLinks.md) - [BatchScheduleParams](docs/BatchScheduleParams.md) - [BatchScheduleResponse](docs/BatchScheduleResponse.md) + - [BatchedRunLogLines](docs/BatchedRunLogLines.md) - [CancelRunResponse](docs/CancelRunResponse.md) - [Catalog](docs/Catalog.md) - [CatalogCredentials](docs/CatalogCredentials.md) - [CatalogFact](docs/CatalogFact.md) - [CatalogProperty](docs/CatalogProperty.md) + - [CatalogUsage](docs/CatalogUsage.md) - [ClaimDeviceLoginTicketParams](docs/ClaimDeviceLoginTicketParams.md) - [ClaimDeviceLoginTicketResponse](docs/ClaimDeviceLoginTicketResponse.md) - [CreateAccountParams](docs/CreateAccountParams.md) @@ -223,6 +233,7 @@ Class | Method | HTTP request | Description - [DescribeAuthenticationContextBody](docs/DescribeAuthenticationContextBody.md) - [DescribeCatalogFactResponse](docs/DescribeCatalogFactResponse.md) - [DescribeCatalogResponse](docs/DescribeCatalogResponse.md) + - [DescribeCatalogUsageResponse](docs/DescribeCatalogUsageResponse.md) - [DescribeDeviceLoginSessionResponse](docs/DescribeDeviceLoginSessionResponse.md) - [DescribeEmailPreferencesBody](docs/DescribeEmailPreferencesBody.md) - [DescribeEnvironmentResponse](docs/DescribeEnvironmentResponse.md) @@ -287,6 +298,7 @@ Class | Method | HTTP request | Description - [ListTeamsResponse](docs/ListTeamsResponse.md) - [ListWebhooksResponse](docs/ListWebhooksResponse.md) - [Organization](docs/Organization.md) + - [OrganizationStorageUsage](docs/OrganizationStorageUsage.md) - [OrganizationUsage](docs/OrganizationUsage.md) - [Pagination](docs/Pagination.md) - [Parameter](docs/Parameter.md) @@ -300,6 +312,7 @@ Class | Method | HTTP request | Description - [ResendTeamInvitationParams](docs/ResendTeamInvitationParams.md) - [ResendTeamInvitationResponse](docs/ResendTeamInvitationResponse.md) - [Run](docs/Run.md) + - [RunAndLinks](docs/RunAndLinks.md) - [RunAppInitiatorData](docs/RunAppInitiatorData.md) - [RunAppParams](docs/RunAppParams.md) - [RunAppResponse](docs/RunAppResponse.md) @@ -310,6 +323,7 @@ Class | Method | HTTP request | Description - [RunGraphRunId](docs/RunGraphRunId.md) - [RunInitiator](docs/RunInitiator.md) - [RunInitiatorDetails](docs/RunInitiatorDetails.md) + - [RunLinks](docs/RunLinks.md) - [RunLogLine](docs/RunLogLine.md) - [RunParameter](docs/RunParameter.md) - [RunResults](docs/RunResults.md) diff --git a/crates/tower-api/src/apis/configuration.rs b/crates/tower-api/src/apis/configuration.rs index eed91008..bf2ef5e5 100644 --- a/crates/tower-api/src/apis/configuration.rs +++ b/crates/tower-api/src/apis/configuration.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/apis/default_api.rs b/crates/tower-api/src/apis/default_api.rs index 80aa8413..f20dd504 100644 --- a/crates/tower-api/src/apis/default_api.rs +++ b/crates/tower-api/src/apis/default_api.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ @@ -27,6 +27,18 @@ pub struct ActivateSchedulesParams { pub batch_schedule_params: models::BatchScheduleParams, } +/// struct for passing parameters to the method [`batch_describe_runs`] +#[derive(Clone, Debug)] +pub struct BatchDescribeRunsParams { + pub batch_describe_runs_params: Vec, +} + +/// struct for passing parameters to the method [`batch_describe_runs_logs`] +#[derive(Clone, Debug)] +pub struct BatchDescribeRunsLogsParams { + pub batch_describe_runs_logs_params: Vec, +} + /// struct for passing parameters to the method [`cancel_run`] #[derive(Clone, Debug)] pub struct CancelRunParams { @@ -177,7 +189,7 @@ pub struct DeleteCatalogFactParams { pub catalog: String, /// The name of the fact. pub name: String, - /// The environment of the catalog. + /// Environment containing the catalog definition to delete from. This operation does not fall back to default. pub environment: Option, } @@ -263,6 +275,8 @@ pub struct DeployAppParams { pub x_tower_checksum_sha256: Option, /// Optional opaque key (typically a git commit SHA or CI build ID). If a prior deploy for this app supplied the same value, the server reuses that AppVersion instead of creating a new one — letting consecutive deploys to different environments share a single version when the source hasn't changed. pub x_tower_idempotency_key: Option, + /// Optional account-scoped monotonic sequence token used to reject out-of-order writes. See the fence documentation for the acceptance window and retry behavior. + pub x_tower_request_number: Option, /// Size of the uploaded bundle in bytes. pub content_length: Option, /// The environment to deploy to. @@ -309,7 +323,7 @@ pub struct DescribeAppVersionParams { pub struct DescribeCatalogParams { /// The name of the catalog. pub name: String, - /// The environment of the catalog. + /// Environment whose catalog to return. When it has no same-named catalog, the catalog from default is returned instead. pub environment: Option, } @@ -320,7 +334,16 @@ pub struct DescribeCatalogFactParams { pub catalog: String, /// The name of the fact. pub name: String, - /// The environment of the catalog. + /// The environment of the catalog. Note that if a catalog with the requested name doesn't exist in the requested environment, the fact from the catalog with the same name in the default environment will be returned. + pub environment: Option, +} + +/// struct for passing parameters to the method [`describe_catalog_usage`] +#[derive(Clone, Debug)] +pub struct DescribeCatalogUsageParams { + /// The name of the catalog. + pub name: String, + /// Environment whose catalog usage to return. When it has no same-named catalog, usage for the catalog from default is returned instead. pub environment: Option, } @@ -525,7 +548,7 @@ pub struct ListAppsParams { pub struct ListCatalogFactsParams { /// The name of the catalog. pub catalog: String, - /// The environment of the catalog. + /// The environment of the catalog. Note that if a catalog with the requested name doesn't exist in the requested environment, facts for the catalog with the same name from the default environment will be returned. pub environment: Option, /// Filter facts by scope. When omitted, facts of every scope are returned. pub scope: Option, @@ -540,9 +563,9 @@ pub struct ListCatalogsParams { pub page: Option, /// The number of records to fetch on each page. pub page_size: Option, - /// The environment to filter by. When omitted, catalogs across all environments are returned. + /// The environment of the catalogs. Catalogs from the default environment will be returned for names that don't exist in the requested environment. When omitted, catalogs across all environments are returned. pub environment: Option, - /// Whether to fetch all catalogs across all environments or only for the current environment. + /// Whether to return catalogs across all environments, without applying default-environment inheritance or deduplication. pub all: Option, /// Filter catalogs by type, e.g. \"tower-catalog\". When omitted, all catalog types are returned. pub r#type: Option, @@ -748,6 +771,8 @@ pub struct UpdateAppParams { /// The name of the App to update. pub name: String, pub update_app_params: models::UpdateAppParams, + /// Optional account-scoped monotonic sequence token used to reject out-of-order writes. See the fence documentation for the acceptance window and retry behavior. + pub x_tower_request_number: Option, } /// struct for passing parameters to the method [`update_app_environment`] @@ -776,7 +801,7 @@ pub struct UpdateCatalogFactParams { /// The name of the fact. pub name: String, pub update_catalog_fact_body: models::UpdateCatalogFactBody, - /// The environment of the catalog. + /// Environment containing the catalog definition to update. This operation does not fall back to default. pub environment: Option, } @@ -820,6 +845,8 @@ pub struct UpdateScheduleParams { /// The ID or name of the schedule to update. pub id_or_name: String, pub update_schedule_params: models::UpdateScheduleParams, + /// Optional account-scoped monotonic sequence token used to reject out-of-order writes. See the fence documentation for the acceptance window and retry behavior. + pub x_tower_request_number: Option, } /// struct for passing parameters to the method [`update_secret`] @@ -873,7 +900,7 @@ pub struct VendCatalogCredentialsParams { /// The name of the catalog. pub name: String, pub vend_catalog_credentials_body: models::VendCatalogCredentialsBody, - /// The environment of the catalog. + /// Environment whose catalog credentials to vend. When it has no same-named catalog, credentials for the catalog from default are vended instead. pub environment: Option, } @@ -901,6 +928,22 @@ pub enum ActivateSchedulesSuccess { UnknownValue(serde_json::Value), } +/// struct for typed successes of method [`batch_describe_runs`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum BatchDescribeRunsSuccess { + Status200(models::BatchDescribeRunsResponse), + UnknownValue(serde_json::Value), +} + +/// struct for typed successes of method [`batch_describe_runs_logs`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum BatchDescribeRunsLogsSuccess { + Status200(Vec), + UnknownValue(serde_json::Value), +} + /// struct for typed successes of method [`cancel_run`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -1229,6 +1272,14 @@ pub enum DescribeCatalogFactSuccess { UnknownValue(serde_json::Value), } +/// struct for typed successes of method [`describe_catalog_usage`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum DescribeCatalogUsageSuccess { + Status200(models::DescribeCatalogUsageResponse), + UnknownValue(serde_json::Value), +} + /// struct for typed successes of method [`describe_default_catalog`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -1822,6 +1873,22 @@ pub enum ActivateSchedulesError { UnknownValue(serde_json::Value), } +/// struct for typed errors of method [`batch_describe_runs`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum BatchDescribeRunsError { + DefaultResponse(models::ErrorModel), + UnknownValue(serde_json::Value), +} + +/// struct for typed errors of method [`batch_describe_runs_logs`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum BatchDescribeRunsLogsError { + DefaultResponse(models::ErrorModel), + UnknownValue(serde_json::Value), +} + /// struct for typed errors of method [`cancel_run`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -1874,6 +1941,12 @@ pub enum CreateAppError { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum CreateCatalogError { + Status400(models::ErrorModel), + Status401(models::ErrorModel), + Status403(models::ErrorModel), + Status409(models::ErrorModel), + Status422(models::ErrorModel), + Status500(models::ErrorModel), DefaultResponse(models::ErrorModel), UnknownValue(serde_json::Value), } @@ -2005,6 +2078,12 @@ pub enum DeleteAppError { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum DeleteCatalogError { + Status401(models::ErrorModel), + Status403(models::ErrorModel), + Status404(models::ErrorModel), + Status409(models::ErrorModel), + Status422(models::ErrorModel), + Status500(models::ErrorModel), DefaultResponse(models::ErrorModel), UnknownValue(serde_json::Value), } @@ -2021,6 +2100,12 @@ pub enum DeleteCatalogFactError { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum DeleteEnvironmentError { + Status401(models::ErrorModel), + Status403(models::ErrorModel), + Status404(models::ErrorModel), + Status409(models::ErrorModel), + Status422(models::ErrorModel), + Status500(models::ErrorModel), DefaultResponse(models::ErrorModel), UnknownValue(serde_json::Value), } @@ -2102,6 +2187,7 @@ pub enum DeleteWebhookError { #[serde(untagged)] pub enum DeployAppError { Status400(models::ErrorModel), + Status412(models::ErrorModel), Status422(models::ErrorModel), Status500(models::ErrorModel), UnknownValue(serde_json::Value), @@ -2155,6 +2241,17 @@ pub enum DescribeCatalogFactError { UnknownValue(serde_json::Value), } +/// struct for typed errors of method [`describe_catalog_usage`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum DescribeCatalogUsageError { + Status401(models::ErrorModel), + Status404(models::ErrorModel), + Status422(models::ErrorModel), + Status500(models::ErrorModel), + UnknownValue(serde_json::Value), +} + /// struct for typed errors of method [`describe_default_catalog`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -2280,7 +2377,12 @@ pub enum DescribeWhoamiError { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum ExportCatalogsError { - DefaultResponse(models::ErrorModel), + Status400(models::ErrorModel), + Status401(models::ErrorModel), + Status403(models::ErrorModel), + Status409(models::ErrorModel), + Status422(models::ErrorModel), + Status500(models::ErrorModel), UnknownValue(serde_json::Value), } @@ -2592,7 +2694,9 @@ pub enum UpdateAccountError { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum UpdateAppError { - DefaultResponse(models::ErrorModel), + Status412(models::ErrorModel), + Status422(models::ErrorModel), + Status500(models::ErrorModel), UnknownValue(serde_json::Value), } @@ -2608,6 +2712,11 @@ pub enum UpdateAppEnvironmentError { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum UpdateCatalogError { + Status401(models::ErrorModel), + Status403(models::ErrorModel), + Status404(models::ErrorModel), + Status422(models::ErrorModel), + Status500(models::ErrorModel), DefaultResponse(models::ErrorModel), UnknownValue(serde_json::Value), } @@ -2632,6 +2741,12 @@ pub enum UpdateEmailPreferencesError { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum UpdateEnvironmentError { + Status401(models::ErrorModel), + Status403(models::ErrorModel), + Status404(models::ErrorModel), + Status409(models::ErrorModel), + Status422(models::ErrorModel), + Status500(models::ErrorModel), DefaultResponse(models::ErrorModel), UnknownValue(serde_json::Value), } @@ -2670,7 +2785,9 @@ pub enum UpdatePlanError { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum UpdateScheduleError { - DefaultResponse(models::ErrorModel), + Status412(models::ErrorModel), + Status422(models::ErrorModel), + Status500(models::ErrorModel), UnknownValue(serde_json::Value), } @@ -2909,6 +3026,122 @@ pub async fn activate_schedules( } } +/// Describe multiple runs in a single request. +pub async fn batch_describe_runs( + configuration: &configuration::Configuration, + params: BatchDescribeRunsParams, +) -> Result, Error> { + let uri_str = format!("{}/batch/describe-runs", configuration.base_path); + let mut req_builder = configuration + .client + .request(reqwest::Method::POST, &uri_str); + + if let Some(ref user_agent) = configuration.user_agent { + req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + if let Some(ref token) = configuration.bearer_access_token { + req_builder = req_builder.bearer_auth(token.to_owned()); + }; + if let Some(ref apikey) = configuration.api_key { + let key = apikey.key.clone(); + let value = match apikey.prefix { + Some(ref prefix) => format!("{} {}", prefix, key), + None => key, + }; + req_builder = req_builder.header("X-API-Key", value); + }; + req_builder = req_builder.json(¶ms.batch_describe_runs_params); + + let req = req_builder.build()?; + let resp = configuration.client.execute(req).await?; + + let status = resp.status(); + + let tower_trace_id = resp + .headers() + .get("x-tower-trace-id") + .and_then(|v| v.to_str().ok()) + .map_or(String::from(DEFAULT_TOWER_TRACE_ID), String::from); + + if !status.is_client_error() && !status.is_server_error() { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Ok(ResponseContent { + tower_trace_id, + status, + content, + entity, + }) + } else { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { + tower_trace_id, + status, + content, + entity, + })) + } +} + +/// Describe multiple run logs in a single request. +pub async fn batch_describe_runs_logs( + configuration: &configuration::Configuration, + params: BatchDescribeRunsLogsParams, +) -> Result, Error> { + let uri_str = format!("{}/batch/describe-runs-logs", configuration.base_path); + let mut req_builder = configuration + .client + .request(reqwest::Method::POST, &uri_str); + + if let Some(ref user_agent) = configuration.user_agent { + req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + if let Some(ref token) = configuration.bearer_access_token { + req_builder = req_builder.bearer_auth(token.to_owned()); + }; + if let Some(ref apikey) = configuration.api_key { + let key = apikey.key.clone(); + let value = match apikey.prefix { + Some(ref prefix) => format!("{} {}", prefix, key), + None => key, + }; + req_builder = req_builder.header("X-API-Key", value); + }; + req_builder = req_builder.json(¶ms.batch_describe_runs_logs_params); + + let req = req_builder.build()?; + let resp = configuration.client.execute(req).await?; + + let status = resp.status(); + + let tower_trace_id = resp + .headers() + .get("x-tower-trace-id") + .and_then(|v| v.to_str().ok()) + .map_or(String::from(DEFAULT_TOWER_TRACE_ID), String::from); + + if !status.is_client_error() && !status.is_server_error() { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Ok(ResponseContent { + tower_trace_id, + status, + content, + entity, + }) + } else { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { + tower_trace_id, + status, + content, + entity, + })) + } +} + /// Cancel a run pub async fn cancel_run( configuration: &configuration::Configuration, @@ -4930,6 +5163,9 @@ pub async fn deploy_app( if let Some(param_value) = params.x_tower_idempotency_key { req_builder = req_builder.header("X-Tower-Idempotency-Key", param_value.to_string()); } + if let Some(param_value) = params.x_tower_request_number { + req_builder = req_builder.header("X-Tower-Request-Number", param_value.to_string()); + } if let Some(param_value) = params.content_length { req_builder = req_builder.header("Content-Length", param_value.to_string()); } @@ -5230,7 +5466,7 @@ pub async fn describe_authentication_context( } } -/// Returns details for a specific catalog, including its property names and previews. +/// Returns non-secret details for a catalog in the selected environment. When that environment has no same-named catalog, the catalog from default is returned instead. The response's catalog environment identifies where its definition is stored. pub async fn describe_catalog( configuration: &configuration::Configuration, params: DescribeCatalogParams, @@ -5355,6 +5591,68 @@ pub async fn describe_catalog_fact( } } +/// Returns physical bytes stored by one Tower-managed catalog, including Iceberg metadata and not-yet-compacted snapshot history. Measurements are cached and may be temporarily unavailable; measured_at is null when no measurement exists. BYO and S3 Tables catalogs are not metered. +pub async fn describe_catalog_usage( + configuration: &configuration::Configuration, + params: DescribeCatalogUsageParams, +) -> Result, Error> { + let uri_str = format!( + "{}/catalogs/{name}/usage", + configuration.base_path, + name = crate::apis::urlencode(params.name) + ); + let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str); + + if let Some(ref param_value) = params.environment { + req_builder = req_builder.query(&[("environment", ¶m_value.to_string())]); + } + if let Some(ref user_agent) = configuration.user_agent { + req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + if let Some(ref token) = configuration.bearer_access_token { + req_builder = req_builder.bearer_auth(token.to_owned()); + }; + if let Some(ref apikey) = configuration.api_key { + let key = apikey.key.clone(); + let value = match apikey.prefix { + Some(ref prefix) => format!("{} {}", prefix, key), + None => key, + }; + req_builder = req_builder.header("X-API-Key", value); + }; + + let req = req_builder.build()?; + let resp = configuration.client.execute(req).await?; + + let status = resp.status(); + + let tower_trace_id = resp + .headers() + .get("x-tower-trace-id") + .and_then(|v| v.to_str().ok()) + .map_or(String::from(DEFAULT_TOWER_TRACE_ID), String::from); + + if !status.is_client_error() && !status.is_server_error() { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Ok(ResponseContent { + tower_trace_id, + status, + content, + entity, + }) + } else { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { + tower_trace_id, + status, + content, + entity, + })) + } +} + /// Returns the team's default catalog, provisioning it lazily if it does not yet exist. When two concurrent first calls race to provision the catalog, the loser receives 202 Accepted; retry after a few seconds and the catalog will be ready. pub async fn describe_default_catalog( configuration: &configuration::Configuration, @@ -8706,6 +9004,9 @@ pub async fn update_app( if let Some(ref user_agent) = configuration.user_agent { req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); } + if let Some(param_value) = params.x_tower_request_number { + req_builder = req_builder.header("X-Tower-Request-Number", param_value.to_string()); + } if let Some(ref token) = configuration.bearer_access_token { req_builder = req_builder.bearer_auth(token.to_owned()); }; @@ -9238,6 +9539,9 @@ pub async fn update_schedule( if let Some(ref user_agent) = configuration.user_agent { req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); } + if let Some(param_value) = params.x_tower_request_number { + req_builder = req_builder.header("X-Tower-Request-Number", param_value.to_string()); + } if let Some(ref token) = configuration.bearer_access_token { req_builder = req_builder.bearer_auth(token.to_owned()); }; diff --git a/crates/tower-api/src/apis/feature_flags_api.rs b/crates/tower-api/src/apis/feature_flags_api.rs index bb90cc43..0baa1861 100644 --- a/crates/tower-api/src/apis/feature_flags_api.rs +++ b/crates/tower-api/src/apis/feature_flags_api.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/account.rs b/crates/tower-api/src/models/account.rs index 13f84cda..53361f8b 100644 --- a/crates/tower-api/src/models/account.rs +++ b/crates/tower-api/src/models/account.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/acknowledge_alert_response.rs b/crates/tower-api/src/models/acknowledge_alert_response.rs index d55f0d56..a4d07846 100644 --- a/crates/tower-api/src/models/acknowledge_alert_response.rs +++ b/crates/tower-api/src/models/acknowledge_alert_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/acknowledge_all_alerts_response.rs b/crates/tower-api/src/models/acknowledge_all_alerts_response.rs index 3562b641..7beb979e 100644 --- a/crates/tower-api/src/models/acknowledge_all_alerts_response.rs +++ b/crates/tower-api/src/models/acknowledge_all_alerts_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/alert.rs b/crates/tower-api/src/models/alert.rs index 917366f8..14f5ce31 100644 --- a/crates/tower-api/src/models/alert.rs +++ b/crates/tower-api/src/models/alert.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/api_key.rs b/crates/tower-api/src/models/api_key.rs index c92f2dff..3cb4a482 100644 --- a/crates/tower-api/src/models/api_key.rs +++ b/crates/tower-api/src/models/api_key.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/api_key_owner.rs b/crates/tower-api/src/models/api_key_owner.rs index 840d5b93..285a9a4a 100644 --- a/crates/tower-api/src/models/api_key_owner.rs +++ b/crates/tower-api/src/models/api_key_owner.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/app.rs b/crates/tower-api/src/models/app.rs index 6afd89c2..84607114 100644 --- a/crates/tower-api/src/models/app.rs +++ b/crates/tower-api/src/models/app.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ @@ -22,6 +22,10 @@ pub struct App { #[serde_as(as = "DefaultOnNull")] #[serde(rename = "health_status")] pub health_status: HealthStatus, + /// Whether this app was deployed from the Tower examples catalog. + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "is_example")] + pub is_example: bool, #[serde_as(as = "DefaultOnNull")] #[serde(rename = "is_externally_accessible")] pub is_externally_accessible: bool, @@ -81,6 +85,7 @@ impl App { pub fn new( created_at: String, health_status: HealthStatus, + is_example: bool, is_externally_accessible: bool, name: String, next_run_at: Option, @@ -94,6 +99,7 @@ impl App { App { created_at, health_status, + is_example, is_externally_accessible, last_run: None, name, diff --git a/crates/tower-api/src/models/app_statistics.rs b/crates/tower-api/src/models/app_statistics.rs index e691a736..18d89de6 100644 --- a/crates/tower-api/src/models/app_statistics.rs +++ b/crates/tower-api/src/models/app_statistics.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/app_summary.rs b/crates/tower-api/src/models/app_summary.rs index fd6eaacf..1590e114 100644 --- a/crates/tower-api/src/models/app_summary.rs +++ b/crates/tower-api/src/models/app_summary.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/app_tag.rs b/crates/tower-api/src/models/app_tag.rs index 59fadff6..7c3bd0f5 100644 --- a/crates/tower-api/src/models/app_tag.rs +++ b/crates/tower-api/src/models/app_tag.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/app_version.rs b/crates/tower-api/src/models/app_version.rs index b1193b3c..e3f307ca 100644 --- a/crates/tower-api/src/models/app_version.rs +++ b/crates/tower-api/src/models/app_version.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/authentication_context.rs b/crates/tower-api/src/models/authentication_context.rs index 16e7d68b..cb5d9c57 100644 --- a/crates/tower-api/src/models/authentication_context.rs +++ b/crates/tower-api/src/models/authentication_context.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/batch_describe_runs_logs_params.rs b/crates/tower-api/src/models/batch_describe_runs_logs_params.rs new file mode 100644 index 00000000..a2630e72 --- /dev/null +++ b/crates/tower-api/src/models/batch_describe_runs_logs_params.rs @@ -0,0 +1,61 @@ +/* + * Tower API + * + * REST API to interact with Tower Services. + * + * The version of the OpenAPI document: v0.11.24 + * Contact: hello@tower.dev + * Generated by: https://openapi-generator.tech + */ +use crate::models; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::{serde_as, DefaultOnNull}; + +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct BatchDescribeRunsLogsParams { + /// Return only the first N log lines. Cannot be combined with tail. + #[serde( + rename = "head", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] + pub head: Option>, + /// The name of the app to describe the run for. + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "name")] + pub name: String, + /// The number of the run to describe. + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "seq")] + pub seq: i64, + /// Fetch logs from this timestamp onwards (inclusive). + #[serde( + rename = "start_at", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] + pub start_at: Option>, + /// Return only the last N log lines. Cannot be combined with head. + #[serde( + rename = "tail", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] + pub tail: Option>, +} + +impl BatchDescribeRunsLogsParams { + pub fn new(name: String, seq: i64) -> BatchDescribeRunsLogsParams { + BatchDescribeRunsLogsParams { + head: None, + name, + seq, + start_at: None, + tail: None, + } + } +} diff --git a/crates/tower-api/src/models/batch_describe_runs_params.rs b/crates/tower-api/src/models/batch_describe_runs_params.rs new file mode 100644 index 00000000..831d411b --- /dev/null +++ b/crates/tower-api/src/models/batch_describe_runs_params.rs @@ -0,0 +1,31 @@ +/* + * Tower API + * + * REST API to interact with Tower Services. + * + * The version of the OpenAPI document: v0.11.24 + * Contact: hello@tower.dev + * Generated by: https://openapi-generator.tech + */ +use crate::models; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::{serde_as, DefaultOnNull}; + +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct BatchDescribeRunsParams { + /// The name of the app to describe the run for. + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "name")] + pub name: String, + /// The number of the run to describe. + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "seq")] + pub seq: i64, +} + +impl BatchDescribeRunsParams { + pub fn new(name: String, seq: i64) -> BatchDescribeRunsParams { + BatchDescribeRunsParams { name, seq } + } +} diff --git a/crates/tower-api/src/models/batch_describe_runs_response.rs b/crates/tower-api/src/models/batch_describe_runs_response.rs new file mode 100644 index 00000000..076fe5b6 --- /dev/null +++ b/crates/tower-api/src/models/batch_describe_runs_response.rs @@ -0,0 +1,29 @@ +/* + * Tower API + * + * REST API to interact with Tower Services. + * + * The version of the OpenAPI document: v0.11.24 + * Contact: hello@tower.dev + * Generated by: https://openapi-generator.tech + */ +use crate::models; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::{serde_as, DefaultOnNull}; + +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct BatchDescribeRunsResponse { + /// A URL to the JSON Schema for this object. + #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")] + pub schema: Option, + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "runs")] + pub runs: Vec, +} + +impl BatchDescribeRunsResponse { + pub fn new(runs: Vec) -> BatchDescribeRunsResponse { + BatchDescribeRunsResponse { schema: None, runs } + } +} diff --git a/crates/tower-api/src/models/batch_error.rs b/crates/tower-api/src/models/batch_error.rs new file mode 100644 index 00000000..68534bd9 --- /dev/null +++ b/crates/tower-api/src/models/batch_error.rs @@ -0,0 +1,26 @@ +/* + * Tower API + * + * REST API to interact with Tower Services. + * + * The version of the OpenAPI document: v0.11.24 + * Contact: hello@tower.dev + * Generated by: https://openapi-generator.tech + */ +use crate::models; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::{serde_as, DefaultOnNull}; + +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct BatchError { + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "message")] + pub message: String, +} + +impl BatchError { + pub fn new(message: String) -> BatchError { + BatchError { message } + } +} diff --git a/crates/tower-api/src/models/batch_run_and_links.rs b/crates/tower-api/src/models/batch_run_and_links.rs new file mode 100644 index 00000000..da357f04 --- /dev/null +++ b/crates/tower-api/src/models/batch_run_and_links.rs @@ -0,0 +1,30 @@ +/* + * Tower API + * + * REST API to interact with Tower Services. + * + * The version of the OpenAPI document: v0.11.24 + * Contact: hello@tower.dev + * Generated by: https://openapi-generator.tech + */ +use crate::models; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::{serde_as, DefaultOnNull}; + +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct BatchRunAndLinks { + #[serde(rename = "data", skip_serializing_if = "Option::is_none")] + pub data: Option, + #[serde(rename = "error", skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +impl BatchRunAndLinks { + pub fn new() -> BatchRunAndLinks { + BatchRunAndLinks { + data: None, + error: None, + } + } +} diff --git a/crates/tower-api/src/models/batch_schedule_params.rs b/crates/tower-api/src/models/batch_schedule_params.rs index 81e52aa1..2a4f6b7d 100644 --- a/crates/tower-api/src/models/batch_schedule_params.rs +++ b/crates/tower-api/src/models/batch_schedule_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/batch_schedule_response.rs b/crates/tower-api/src/models/batch_schedule_response.rs index 09532e94..547f2330 100644 --- a/crates/tower-api/src/models/batch_schedule_response.rs +++ b/crates/tower-api/src/models/batch_schedule_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/batched_run_log_lines.rs b/crates/tower-api/src/models/batched_run_log_lines.rs new file mode 100644 index 00000000..3cfd1523 --- /dev/null +++ b/crates/tower-api/src/models/batched_run_log_lines.rs @@ -0,0 +1,35 @@ +/* + * Tower API + * + * REST API to interact with Tower Services. + * + * The version of the OpenAPI document: v0.11.24 + * Contact: hello@tower.dev + * Generated by: https://openapi-generator.tech + */ +use crate::models; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::{serde_as, DefaultOnNull}; + +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct BatchedRunLogLines { + #[serde( + rename = "error", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] + pub error: Option>, + #[serde(rename = "log_lines", skip_serializing_if = "Option::is_none")] + pub log_lines: Option>, +} + +impl BatchedRunLogLines { + pub fn new() -> BatchedRunLogLines { + BatchedRunLogLines { + error: None, + log_lines: None, + } + } +} diff --git a/crates/tower-api/src/models/cancel_run_response.rs b/crates/tower-api/src/models/cancel_run_response.rs index d7c0e02a..ca1c73e0 100644 --- a/crates/tower-api/src/models/cancel_run_response.rs +++ b/crates/tower-api/src/models/cancel_run_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/catalog.rs b/crates/tower-api/src/models/catalog.rs index 352181b2..a2e07c9f 100644 --- a/crates/tower-api/src/models/catalog.rs +++ b/crates/tower-api/src/models/catalog.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ @@ -17,6 +17,7 @@ pub struct Catalog { #[serde_as(as = "DefaultOnNull")] #[serde(rename = "CreatedAt")] pub created_at: String, + /// Environment containing the catalog definition. #[serde_as(as = "DefaultOnNull")] #[serde(rename = "environment")] pub environment: String, diff --git a/crates/tower-api/src/models/catalog_credentials.rs b/crates/tower-api/src/models/catalog_credentials.rs index 284b8aee..5de1ad47 100644 --- a/crates/tower-api/src/models/catalog_credentials.rs +++ b/crates/tower-api/src/models/catalog_credentials.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/catalog_fact.rs b/crates/tower-api/src/models/catalog_fact.rs index 7689d4d6..5fae99ee 100644 --- a/crates/tower-api/src/models/catalog_fact.rs +++ b/crates/tower-api/src/models/catalog_fact.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/catalog_property.rs b/crates/tower-api/src/models/catalog_property.rs index 4b15bd7e..0f3ff9da 100644 --- a/crates/tower-api/src/models/catalog_property.rs +++ b/crates/tower-api/src/models/catalog_property.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/catalog_usage.rs b/crates/tower-api/src/models/catalog_usage.rs new file mode 100644 index 00000000..9b62e91b --- /dev/null +++ b/crates/tower-api/src/models/catalog_usage.rs @@ -0,0 +1,33 @@ +/* + * Tower API + * + * REST API to interact with Tower Services. + * + * The version of the OpenAPI document: v0.11.24 + * Contact: hello@tower.dev + * Generated by: https://openapi-generator.tech + */ +use crate::models; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::{serde_as, DefaultOnNull}; + +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct CatalogUsage { + /// When this value was measured. Null when metering is unconfigured, disabled, or failed without a cached value. + #[serde(rename = "measured_at", deserialize_with = "Option::deserialize")] + pub measured_at: Option, + /// Physical bytes stored by this Tower-managed catalog, including Iceberg metadata and not-yet-compacted snapshot history. + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "total_bytes")] + pub total_bytes: i64, +} + +impl CatalogUsage { + pub fn new(measured_at: Option, total_bytes: i64) -> CatalogUsage { + CatalogUsage { + measured_at, + total_bytes, + } + } +} diff --git a/crates/tower-api/src/models/claim_device_login_ticket_params.rs b/crates/tower-api/src/models/claim_device_login_ticket_params.rs index 0c5bf058..22debab4 100644 --- a/crates/tower-api/src/models/claim_device_login_ticket_params.rs +++ b/crates/tower-api/src/models/claim_device_login_ticket_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/claim_device_login_ticket_response.rs b/crates/tower-api/src/models/claim_device_login_ticket_response.rs index 327fa284..8aec468d 100644 --- a/crates/tower-api/src/models/claim_device_login_ticket_response.rs +++ b/crates/tower-api/src/models/claim_device_login_ticket_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_account_params.rs b/crates/tower-api/src/models/create_account_params.rs index 7aa1327d..d3bd03b5 100644 --- a/crates/tower-api/src/models/create_account_params.rs +++ b/crates/tower-api/src/models/create_account_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_account_params_flags_struct.rs b/crates/tower-api/src/models/create_account_params_flags_struct.rs index 66d4c825..d8b4f758 100644 --- a/crates/tower-api/src/models/create_account_params_flags_struct.rs +++ b/crates/tower-api/src/models/create_account_params_flags_struct.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_account_response.rs b/crates/tower-api/src/models/create_account_response.rs index bbed8fba..90d42733 100644 --- a/crates/tower-api/src/models/create_account_response.rs +++ b/crates/tower-api/src/models/create_account_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_api_key_params.rs b/crates/tower-api/src/models/create_api_key_params.rs index 4f026c03..05d56991 100644 --- a/crates/tower-api/src/models/create_api_key_params.rs +++ b/crates/tower-api/src/models/create_api_key_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_api_key_response.rs b/crates/tower-api/src/models/create_api_key_response.rs index c90b282b..d623904a 100644 --- a/crates/tower-api/src/models/create_api_key_response.rs +++ b/crates/tower-api/src/models/create_api_key_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_app_params.rs b/crates/tower-api/src/models/create_app_params.rs index 77b91935..fb9b08ad 100644 --- a/crates/tower-api/src/models/create_app_params.rs +++ b/crates/tower-api/src/models/create_app_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_app_response.rs b/crates/tower-api/src/models/create_app_response.rs index e1cac520..b2c59985 100644 --- a/crates/tower-api/src/models/create_app_response.rs +++ b/crates/tower-api/src/models/create_app_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_catalog_params.rs b/crates/tower-api/src/models/create_catalog_params.rs index 33196ef6..6134e916 100644 --- a/crates/tower-api/src/models/create_catalog_params.rs +++ b/crates/tower-api/src/models/create_catalog_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_catalog_response.rs b/crates/tower-api/src/models/create_catalog_response.rs index f4bb0866..dec14184 100644 --- a/crates/tower-api/src/models/create_catalog_response.rs +++ b/crates/tower-api/src/models/create_catalog_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_device_login_ticket_response.rs b/crates/tower-api/src/models/create_device_login_ticket_response.rs index 1da72fec..f2418751 100644 --- a/crates/tower-api/src/models/create_device_login_ticket_response.rs +++ b/crates/tower-api/src/models/create_device_login_ticket_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_environment_params.rs b/crates/tower-api/src/models/create_environment_params.rs index b1e70989..8cf45213 100644 --- a/crates/tower-api/src/models/create_environment_params.rs +++ b/crates/tower-api/src/models/create_environment_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_environment_response.rs b/crates/tower-api/src/models/create_environment_response.rs index 0aed4bf2..8412a1f7 100644 --- a/crates/tower-api/src/models/create_environment_response.rs +++ b/crates/tower-api/src/models/create_environment_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_guest_params.rs b/crates/tower-api/src/models/create_guest_params.rs index 7afc6838..df6d34fe 100644 --- a/crates/tower-api/src/models/create_guest_params.rs +++ b/crates/tower-api/src/models/create_guest_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_guest_response.rs b/crates/tower-api/src/models/create_guest_response.rs index d5df6972..c96afc6a 100644 --- a/crates/tower-api/src/models/create_guest_response.rs +++ b/crates/tower-api/src/models/create_guest_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_sandbox_secrets_params.rs b/crates/tower-api/src/models/create_sandbox_secrets_params.rs index 3918e1de..4b4f921a 100644 --- a/crates/tower-api/src/models/create_sandbox_secrets_params.rs +++ b/crates/tower-api/src/models/create_sandbox_secrets_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_sandbox_secrets_response.rs b/crates/tower-api/src/models/create_sandbox_secrets_response.rs index 46ac8d02..138c925d 100644 --- a/crates/tower-api/src/models/create_sandbox_secrets_response.rs +++ b/crates/tower-api/src/models/create_sandbox_secrets_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_schedule_params.rs b/crates/tower-api/src/models/create_schedule_params.rs index 791809b0..2f339b9d 100644 --- a/crates/tower-api/src/models/create_schedule_params.rs +++ b/crates/tower-api/src/models/create_schedule_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_schedule_response.rs b/crates/tower-api/src/models/create_schedule_response.rs index a95ab056..9823ed77 100644 --- a/crates/tower-api/src/models/create_schedule_response.rs +++ b/crates/tower-api/src/models/create_schedule_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_secret_params.rs b/crates/tower-api/src/models/create_secret_params.rs index 6e27c3ea..6b88eff4 100644 --- a/crates/tower-api/src/models/create_secret_params.rs +++ b/crates/tower-api/src/models/create_secret_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_secret_response.rs b/crates/tower-api/src/models/create_secret_response.rs index 3a8f27ed..1a836dc5 100644 --- a/crates/tower-api/src/models/create_secret_response.rs +++ b/crates/tower-api/src/models/create_secret_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_service_account_api_key_params.rs b/crates/tower-api/src/models/create_service_account_api_key_params.rs index a45d8088..e3753079 100644 --- a/crates/tower-api/src/models/create_service_account_api_key_params.rs +++ b/crates/tower-api/src/models/create_service_account_api_key_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_service_account_api_key_response.rs b/crates/tower-api/src/models/create_service_account_api_key_response.rs index 47758632..d717e222 100644 --- a/crates/tower-api/src/models/create_service_account_api_key_response.rs +++ b/crates/tower-api/src/models/create_service_account_api_key_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_service_account_params.rs b/crates/tower-api/src/models/create_service_account_params.rs index 799df04a..00308875 100644 --- a/crates/tower-api/src/models/create_service_account_params.rs +++ b/crates/tower-api/src/models/create_service_account_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_service_account_response.rs b/crates/tower-api/src/models/create_service_account_response.rs index 2c84d6f4..19e8e31a 100644 --- a/crates/tower-api/src/models/create_service_account_response.rs +++ b/crates/tower-api/src/models/create_service_account_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_session_params.rs b/crates/tower-api/src/models/create_session_params.rs index c6259b64..23721d7a 100644 --- a/crates/tower-api/src/models/create_session_params.rs +++ b/crates/tower-api/src/models/create_session_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_session_response.rs b/crates/tower-api/src/models/create_session_response.rs index a5239d45..300f1765 100644 --- a/crates/tower-api/src/models/create_session_response.rs +++ b/crates/tower-api/src/models/create_session_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_team_params.rs b/crates/tower-api/src/models/create_team_params.rs index 58119629..2e418ed3 100644 --- a/crates/tower-api/src/models/create_team_params.rs +++ b/crates/tower-api/src/models/create_team_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_team_response.rs b/crates/tower-api/src/models/create_team_response.rs index 9bdd10c4..0ccafb6a 100644 --- a/crates/tower-api/src/models/create_team_response.rs +++ b/crates/tower-api/src/models/create_team_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_webhook_params.rs b/crates/tower-api/src/models/create_webhook_params.rs index 490bfabb..65ba1bf0 100644 --- a/crates/tower-api/src/models/create_webhook_params.rs +++ b/crates/tower-api/src/models/create_webhook_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/create_webhook_response.rs b/crates/tower-api/src/models/create_webhook_response.rs index 045ef73e..13075174 100644 --- a/crates/tower-api/src/models/create_webhook_response.rs +++ b/crates/tower-api/src/models/create_webhook_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_api_key_params.rs b/crates/tower-api/src/models/delete_api_key_params.rs index 499f72a9..fab7b67e 100644 --- a/crates/tower-api/src/models/delete_api_key_params.rs +++ b/crates/tower-api/src/models/delete_api_key_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_api_key_response.rs b/crates/tower-api/src/models/delete_api_key_response.rs index 39ac6a3e..086fa66c 100644 --- a/crates/tower-api/src/models/delete_api_key_response.rs +++ b/crates/tower-api/src/models/delete_api_key_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_app_response.rs b/crates/tower-api/src/models/delete_app_response.rs index 2946b48a..5bd23d31 100644 --- a/crates/tower-api/src/models/delete_app_response.rs +++ b/crates/tower-api/src/models/delete_app_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_catalog_response.rs b/crates/tower-api/src/models/delete_catalog_response.rs index 1affd044..0e5041ee 100644 --- a/crates/tower-api/src/models/delete_catalog_response.rs +++ b/crates/tower-api/src/models/delete_catalog_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_environment_response.rs b/crates/tower-api/src/models/delete_environment_response.rs index 24bbd310..c8d2c1b6 100644 --- a/crates/tower-api/src/models/delete_environment_response.rs +++ b/crates/tower-api/src/models/delete_environment_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_guest_output_body.rs b/crates/tower-api/src/models/delete_guest_output_body.rs index fc40e6b7..f1f02ac5 100644 --- a/crates/tower-api/src/models/delete_guest_output_body.rs +++ b/crates/tower-api/src/models/delete_guest_output_body.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_schedule_params.rs b/crates/tower-api/src/models/delete_schedule_params.rs index cc5546ac..36c92ac9 100644 --- a/crates/tower-api/src/models/delete_schedule_params.rs +++ b/crates/tower-api/src/models/delete_schedule_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_schedule_response.rs b/crates/tower-api/src/models/delete_schedule_response.rs index 0a830666..606b82d6 100644 --- a/crates/tower-api/src/models/delete_schedule_response.rs +++ b/crates/tower-api/src/models/delete_schedule_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_secret_response.rs b/crates/tower-api/src/models/delete_secret_response.rs index 4ca7b11f..75f509e5 100644 --- a/crates/tower-api/src/models/delete_secret_response.rs +++ b/crates/tower-api/src/models/delete_secret_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_service_account_api_key_params.rs b/crates/tower-api/src/models/delete_service_account_api_key_params.rs index 16ebdbaa..c9a8d5a0 100644 --- a/crates/tower-api/src/models/delete_service_account_api_key_params.rs +++ b/crates/tower-api/src/models/delete_service_account_api_key_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_session_params.rs b/crates/tower-api/src/models/delete_session_params.rs index de8f855f..8db79b12 100644 --- a/crates/tower-api/src/models/delete_session_params.rs +++ b/crates/tower-api/src/models/delete_session_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_session_response.rs b/crates/tower-api/src/models/delete_session_response.rs index d7fc8575..5ce1b9fc 100644 --- a/crates/tower-api/src/models/delete_session_response.rs +++ b/crates/tower-api/src/models/delete_session_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_team_invitation_params.rs b/crates/tower-api/src/models/delete_team_invitation_params.rs index 96a895b4..d8c0ca74 100644 --- a/crates/tower-api/src/models/delete_team_invitation_params.rs +++ b/crates/tower-api/src/models/delete_team_invitation_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_team_invitation_response.rs b/crates/tower-api/src/models/delete_team_invitation_response.rs index ca73daa0..268bda31 100644 --- a/crates/tower-api/src/models/delete_team_invitation_response.rs +++ b/crates/tower-api/src/models/delete_team_invitation_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_team_params.rs b/crates/tower-api/src/models/delete_team_params.rs index e8175fa4..23a193f1 100644 --- a/crates/tower-api/src/models/delete_team_params.rs +++ b/crates/tower-api/src/models/delete_team_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_team_response.rs b/crates/tower-api/src/models/delete_team_response.rs index fcfc256f..3c6ab26a 100644 --- a/crates/tower-api/src/models/delete_team_response.rs +++ b/crates/tower-api/src/models/delete_team_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/delete_webhook_response.rs b/crates/tower-api/src/models/delete_webhook_response.rs index a0b3d6a6..59fd7c4e 100644 --- a/crates/tower-api/src/models/delete_webhook_response.rs +++ b/crates/tower-api/src/models/delete_webhook_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/deploy_app_request.rs b/crates/tower-api/src/models/deploy_app_request.rs index 2f2de5cc..4c12f7b2 100644 --- a/crates/tower-api/src/models/deploy_app_request.rs +++ b/crates/tower-api/src/models/deploy_app_request.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/deploy_app_response.rs b/crates/tower-api/src/models/deploy_app_response.rs index ec69374d..dece73dd 100644 --- a/crates/tower-api/src/models/deploy_app_response.rs +++ b/crates/tower-api/src/models/deploy_app_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_account_body.rs b/crates/tower-api/src/models/describe_account_body.rs index 3cdef8a6..12d30baf 100644 --- a/crates/tower-api/src/models/describe_account_body.rs +++ b/crates/tower-api/src/models/describe_account_body.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_app_response.rs b/crates/tower-api/src/models/describe_app_response.rs index 38666c9e..6f9e5013 100644 --- a/crates/tower-api/src/models/describe_app_response.rs +++ b/crates/tower-api/src/models/describe_app_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_app_version_response.rs b/crates/tower-api/src/models/describe_app_version_response.rs index 3258f6fd..e9f618df 100644 --- a/crates/tower-api/src/models/describe_app_version_response.rs +++ b/crates/tower-api/src/models/describe_app_version_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_authentication_context_body.rs b/crates/tower-api/src/models/describe_authentication_context_body.rs index c55cdd00..89eacdf9 100644 --- a/crates/tower-api/src/models/describe_authentication_context_body.rs +++ b/crates/tower-api/src/models/describe_authentication_context_body.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_catalog_fact_response.rs b/crates/tower-api/src/models/describe_catalog_fact_response.rs index 04cde236..dbda8e0e 100644 --- a/crates/tower-api/src/models/describe_catalog_fact_response.rs +++ b/crates/tower-api/src/models/describe_catalog_fact_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ @@ -17,13 +17,21 @@ pub struct DescribeCatalogFactResponse { /// A URL to the JSON Schema for this object. #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")] pub schema: Option, + /// Environment containing the catalog definition. + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "environment")] + pub environment: String, #[serde_as(as = "DefaultOnNull")] #[serde(rename = "fact")] pub fact: models::CatalogFact, } impl DescribeCatalogFactResponse { - pub fn new(fact: models::CatalogFact) -> DescribeCatalogFactResponse { - DescribeCatalogFactResponse { schema: None, fact } + pub fn new(environment: String, fact: models::CatalogFact) -> DescribeCatalogFactResponse { + DescribeCatalogFactResponse { + schema: None, + environment, + fact, + } } } diff --git a/crates/tower-api/src/models/describe_catalog_response.rs b/crates/tower-api/src/models/describe_catalog_response.rs index bf1f98cf..99eda2e1 100644 --- a/crates/tower-api/src/models/describe_catalog_response.rs +++ b/crates/tower-api/src/models/describe_catalog_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_catalog_usage_response.rs b/crates/tower-api/src/models/describe_catalog_usage_response.rs new file mode 100644 index 00000000..fb937aa1 --- /dev/null +++ b/crates/tower-api/src/models/describe_catalog_usage_response.rs @@ -0,0 +1,37 @@ +/* + * Tower API + * + * REST API to interact with Tower Services. + * + * The version of the OpenAPI document: v0.11.24 + * Contact: hello@tower.dev + * Generated by: https://openapi-generator.tech + */ +use crate::models; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::{serde_as, DefaultOnNull}; + +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct DescribeCatalogUsageResponse { + /// A URL to the JSON Schema for this object. + #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")] + pub schema: Option, + /// Environment containing the catalog definition. + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "environment")] + pub environment: String, + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "usage")] + pub usage: models::CatalogUsage, +} + +impl DescribeCatalogUsageResponse { + pub fn new(environment: String, usage: models::CatalogUsage) -> DescribeCatalogUsageResponse { + DescribeCatalogUsageResponse { + schema: None, + environment, + usage, + } + } +} diff --git a/crates/tower-api/src/models/describe_device_login_session_response.rs b/crates/tower-api/src/models/describe_device_login_session_response.rs index 5a6bbf0b..1b324515 100644 --- a/crates/tower-api/src/models/describe_device_login_session_response.rs +++ b/crates/tower-api/src/models/describe_device_login_session_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_email_preferences_body.rs b/crates/tower-api/src/models/describe_email_preferences_body.rs index befb4ee8..b9efcc55 100644 --- a/crates/tower-api/src/models/describe_email_preferences_body.rs +++ b/crates/tower-api/src/models/describe_email_preferences_body.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_environment_response.rs b/crates/tower-api/src/models/describe_environment_response.rs index 1657cafb..7f58cd00 100644 --- a/crates/tower-api/src/models/describe_environment_response.rs +++ b/crates/tower-api/src/models/describe_environment_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_plan_response.rs b/crates/tower-api/src/models/describe_plan_response.rs index 5bf3b864..99931268 100644 --- a/crates/tower-api/src/models/describe_plan_response.rs +++ b/crates/tower-api/src/models/describe_plan_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_run_graph_response.rs b/crates/tower-api/src/models/describe_run_graph_response.rs index 1d21caf4..1d45dfdd 100644 --- a/crates/tower-api/src/models/describe_run_graph_response.rs +++ b/crates/tower-api/src/models/describe_run_graph_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_run_links.rs b/crates/tower-api/src/models/describe_run_links.rs index 2cf98a00..bd81509b 100644 --- a/crates/tower-api/src/models/describe_run_links.rs +++ b/crates/tower-api/src/models/describe_run_links.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_run_logs_response.rs b/crates/tower-api/src/models/describe_run_logs_response.rs index da5664b9..1ea5ed40 100644 --- a/crates/tower-api/src/models/describe_run_logs_response.rs +++ b/crates/tower-api/src/models/describe_run_logs_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_run_response.rs b/crates/tower-api/src/models/describe_run_response.rs index 23d86c2d..071f348e 100644 --- a/crates/tower-api/src/models/describe_run_response.rs +++ b/crates/tower-api/src/models/describe_run_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_secrets_key_response.rs b/crates/tower-api/src/models/describe_secrets_key_response.rs index 9090dba3..a09cf55e 100644 --- a/crates/tower-api/src/models/describe_secrets_key_response.rs +++ b/crates/tower-api/src/models/describe_secrets_key_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_service_account_response.rs b/crates/tower-api/src/models/describe_service_account_response.rs index 668a4127..fbe87349 100644 --- a/crates/tower-api/src/models/describe_service_account_response.rs +++ b/crates/tower-api/src/models/describe_service_account_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_session_response.rs b/crates/tower-api/src/models/describe_session_response.rs index 33bc35b7..fb8f344e 100644 --- a/crates/tower-api/src/models/describe_session_response.rs +++ b/crates/tower-api/src/models/describe_session_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_team_response.rs b/crates/tower-api/src/models/describe_team_response.rs index b4a5d58c..57b30937 100644 --- a/crates/tower-api/src/models/describe_team_response.rs +++ b/crates/tower-api/src/models/describe_team_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_webhook_response.rs b/crates/tower-api/src/models/describe_webhook_response.rs index 99277e52..f5bcac84 100644 --- a/crates/tower-api/src/models/describe_webhook_response.rs +++ b/crates/tower-api/src/models/describe_webhook_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/describe_whoami_response.rs b/crates/tower-api/src/models/describe_whoami_response.rs index 9dae2606..6e765a28 100644 --- a/crates/tower-api/src/models/describe_whoami_response.rs +++ b/crates/tower-api/src/models/describe_whoami_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/email_subscriptions.rs b/crates/tower-api/src/models/email_subscriptions.rs index de02f82c..768eaa24 100644 --- a/crates/tower-api/src/models/email_subscriptions.rs +++ b/crates/tower-api/src/models/email_subscriptions.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/encrypted_catalog_property.rs b/crates/tower-api/src/models/encrypted_catalog_property.rs index 3c953566..34fcc182 100644 --- a/crates/tower-api/src/models/encrypted_catalog_property.rs +++ b/crates/tower-api/src/models/encrypted_catalog_property.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/environment.rs b/crates/tower-api/src/models/environment.rs index a84b27ea..4de84ece 100644 --- a/crates/tower-api/src/models/environment.rs +++ b/crates/tower-api/src/models/environment.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/error_detail.rs b/crates/tower-api/src/models/error_detail.rs index d67f1289..9073cef0 100644 --- a/crates/tower-api/src/models/error_detail.rs +++ b/crates/tower-api/src/models/error_detail.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/error_model.rs b/crates/tower-api/src/models/error_model.rs index 32c65920..8491659a 100644 --- a/crates/tower-api/src/models/error_model.rs +++ b/crates/tower-api/src/models/error_model.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/event_alert.rs b/crates/tower-api/src/models/event_alert.rs index 423dde06..94b1f922 100644 --- a/crates/tower-api/src/models/event_alert.rs +++ b/crates/tower-api/src/models/event_alert.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/event_error.rs b/crates/tower-api/src/models/event_error.rs index ad65d975..d1a8d385 100644 --- a/crates/tower-api/src/models/event_error.rs +++ b/crates/tower-api/src/models/event_error.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/event_log.rs b/crates/tower-api/src/models/event_log.rs index 7a0ca024..1a332653 100644 --- a/crates/tower-api/src/models/event_log.rs +++ b/crates/tower-api/src/models/event_log.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/event_shouldertap.rs b/crates/tower-api/src/models/event_shouldertap.rs index 170626c5..241818c3 100644 --- a/crates/tower-api/src/models/event_shouldertap.rs +++ b/crates/tower-api/src/models/event_shouldertap.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/event_warning.rs b/crates/tower-api/src/models/event_warning.rs index fc49ee89..34e2c310 100644 --- a/crates/tower-api/src/models/event_warning.rs +++ b/crates/tower-api/src/models/event_warning.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/export_catalogs_params.rs b/crates/tower-api/src/models/export_catalogs_params.rs index 413ccc54..c0d8f2da 100644 --- a/crates/tower-api/src/models/export_catalogs_params.rs +++ b/crates/tower-api/src/models/export_catalogs_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ @@ -17,11 +17,11 @@ pub struct ExportCatalogsParams { /// A URL to the JSON Schema for this object. #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")] pub schema: Option, - /// Whether to fetch all catalogs or only the ones in the supplied environment. + /// Whether to export catalogs across all environments, without applying default-environment inheritance or deduplication. #[serde_as(as = "DefaultOnNull")] #[serde(rename = "all")] pub all: bool, - /// The environment to filter by. + /// Environment whose catalogs to export when all is false. When it has no same-named catalog, a catalog from default is selected instead. #[serde_as(as = "DefaultOnNull")] #[serde(rename = "environment")] pub environment: String, diff --git a/crates/tower-api/src/models/export_catalogs_response.rs b/crates/tower-api/src/models/export_catalogs_response.rs index 16f13f08..071d0766 100644 --- a/crates/tower-api/src/models/export_catalogs_response.rs +++ b/crates/tower-api/src/models/export_catalogs_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/export_secrets_params.rs b/crates/tower-api/src/models/export_secrets_params.rs index 3b61f881..b055c52f 100644 --- a/crates/tower-api/src/models/export_secrets_params.rs +++ b/crates/tower-api/src/models/export_secrets_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/export_secrets_response.rs b/crates/tower-api/src/models/export_secrets_response.rs index 95a898a1..959474c6 100644 --- a/crates/tower-api/src/models/export_secrets_response.rs +++ b/crates/tower-api/src/models/export_secrets_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/exported_catalog.rs b/crates/tower-api/src/models/exported_catalog.rs index ec48600c..e031bb2e 100644 --- a/crates/tower-api/src/models/exported_catalog.rs +++ b/crates/tower-api/src/models/exported_catalog.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ @@ -17,6 +17,7 @@ pub struct ExportedCatalog { #[serde_as(as = "DefaultOnNull")] #[serde(rename = "CreatedAt")] pub created_at: String, + /// Environment containing the catalog definition. #[serde_as(as = "DefaultOnNull")] #[serde(rename = "environment")] pub environment: String, diff --git a/crates/tower-api/src/models/exported_catalog_property.rs b/crates/tower-api/src/models/exported_catalog_property.rs index ee03db06..f3f7cdc3 100644 --- a/crates/tower-api/src/models/exported_catalog_property.rs +++ b/crates/tower-api/src/models/exported_catalog_property.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/exported_secret.rs b/crates/tower-api/src/models/exported_secret.rs index d81128da..24f666aa 100644 --- a/crates/tower-api/src/models/exported_secret.rs +++ b/crates/tower-api/src/models/exported_secret.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/feature.rs b/crates/tower-api/src/models/feature.rs index a4b8ac57..b8a0a440 100644 --- a/crates/tower-api/src/models/feature.rs +++ b/crates/tower-api/src/models/feature.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/featurebase_identity.rs b/crates/tower-api/src/models/featurebase_identity.rs index e5fbe8ad..fc0bcd9a 100644 --- a/crates/tower-api/src/models/featurebase_identity.rs +++ b/crates/tower-api/src/models/featurebase_identity.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/generate_app_statistics_response.rs b/crates/tower-api/src/models/generate_app_statistics_response.rs index 5d8b8db5..0fb48822 100644 --- a/crates/tower-api/src/models/generate_app_statistics_response.rs +++ b/crates/tower-api/src/models/generate_app_statistics_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/generate_organization_usage_time_series_response.rs b/crates/tower-api/src/models/generate_organization_usage_time_series_response.rs index d6c64432..56688af8 100644 --- a/crates/tower-api/src/models/generate_organization_usage_time_series_response.rs +++ b/crates/tower-api/src/models/generate_organization_usage_time_series_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/generate_run_statistics_response.rs b/crates/tower-api/src/models/generate_run_statistics_response.rs index 1d1c46de..094abbfd 100644 --- a/crates/tower-api/src/models/generate_run_statistics_response.rs +++ b/crates/tower-api/src/models/generate_run_statistics_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/generate_runner_credentials_response.rs b/crates/tower-api/src/models/generate_runner_credentials_response.rs index a39529f9..736837c2 100644 --- a/crates/tower-api/src/models/generate_runner_credentials_response.rs +++ b/crates/tower-api/src/models/generate_runner_credentials_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/get_feature_flag_response_body.rs b/crates/tower-api/src/models/get_feature_flag_response_body.rs index 6e19cb36..3d337bce 100644 --- a/crates/tower-api/src/models/get_feature_flag_response_body.rs +++ b/crates/tower-api/src/models/get_feature_flag_response_body.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/guest.rs b/crates/tower-api/src/models/guest.rs index 8ca0a8a7..340ca753 100644 --- a/crates/tower-api/src/models/guest.rs +++ b/crates/tower-api/src/models/guest.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/invite_team_member_params.rs b/crates/tower-api/src/models/invite_team_member_params.rs index db6c84f9..e8dc80df 100644 --- a/crates/tower-api/src/models/invite_team_member_params.rs +++ b/crates/tower-api/src/models/invite_team_member_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/invite_team_member_response.rs b/crates/tower-api/src/models/invite_team_member_response.rs index 4d0d6d17..9b7f892d 100644 --- a/crates/tower-api/src/models/invite_team_member_response.rs +++ b/crates/tower-api/src/models/invite_team_member_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/leave_team_response.rs b/crates/tower-api/src/models/leave_team_response.rs index 8b8856d4..e7b9a877 100644 --- a/crates/tower-api/src/models/leave_team_response.rs +++ b/crates/tower-api/src/models/leave_team_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_alerts_response.rs b/crates/tower-api/src/models/list_alerts_response.rs index 6af2496d..885f4b5c 100644 --- a/crates/tower-api/src/models/list_alerts_response.rs +++ b/crates/tower-api/src/models/list_alerts_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_api_keys_response.rs b/crates/tower-api/src/models/list_api_keys_response.rs index af4f1ba6..4194ad72 100644 --- a/crates/tower-api/src/models/list_api_keys_response.rs +++ b/crates/tower-api/src/models/list_api_keys_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_app_environments_response.rs b/crates/tower-api/src/models/list_app_environments_response.rs index 9101caf8..6380166a 100644 --- a/crates/tower-api/src/models/list_app_environments_response.rs +++ b/crates/tower-api/src/models/list_app_environments_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_app_versions_response.rs b/crates/tower-api/src/models/list_app_versions_response.rs index 0ce2f3d4..b02e7f41 100644 --- a/crates/tower-api/src/models/list_app_versions_response.rs +++ b/crates/tower-api/src/models/list_app_versions_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_apps_response.rs b/crates/tower-api/src/models/list_apps_response.rs index 622c25b4..37322051 100644 --- a/crates/tower-api/src/models/list_apps_response.rs +++ b/crates/tower-api/src/models/list_apps_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_catalog_facts_response.rs b/crates/tower-api/src/models/list_catalog_facts_response.rs index 41c21216..0f33e88d 100644 --- a/crates/tower-api/src/models/list_catalog_facts_response.rs +++ b/crates/tower-api/src/models/list_catalog_facts_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ @@ -17,15 +17,20 @@ pub struct ListCatalogFactsResponse { /// A URL to the JSON Schema for this object. #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")] pub schema: Option, + /// Environment containing the catalog definition. + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "environment")] + pub environment: String, #[serde_as(as = "DefaultOnNull")] #[serde(rename = "facts")] pub facts: Vec, } impl ListCatalogFactsResponse { - pub fn new(facts: Vec) -> ListCatalogFactsResponse { + pub fn new(environment: String, facts: Vec) -> ListCatalogFactsResponse { ListCatalogFactsResponse { schema: None, + environment, facts, } } diff --git a/crates/tower-api/src/models/list_catalogs_response.rs b/crates/tower-api/src/models/list_catalogs_response.rs index c21d39c4..afbb9288 100644 --- a/crates/tower-api/src/models/list_catalogs_response.rs +++ b/crates/tower-api/src/models/list_catalogs_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_environments_response.rs b/crates/tower-api/src/models/list_environments_response.rs index e9f268fc..9cb3349c 100644 --- a/crates/tower-api/src/models/list_environments_response.rs +++ b/crates/tower-api/src/models/list_environments_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_guests_response.rs b/crates/tower-api/src/models/list_guests_response.rs index a0870acc..0ddeb350 100644 --- a/crates/tower-api/src/models/list_guests_response.rs +++ b/crates/tower-api/src/models/list_guests_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_my_team_invitations_response.rs b/crates/tower-api/src/models/list_my_team_invitations_response.rs index e4a43c25..5eb579e8 100644 --- a/crates/tower-api/src/models/list_my_team_invitations_response.rs +++ b/crates/tower-api/src/models/list_my_team_invitations_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_runners_response.rs b/crates/tower-api/src/models/list_runners_response.rs index 7aac8cf1..ece92754 100644 --- a/crates/tower-api/src/models/list_runners_response.rs +++ b/crates/tower-api/src/models/list_runners_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_runs_response.rs b/crates/tower-api/src/models/list_runs_response.rs index f86ede42..b4bd5417 100644 --- a/crates/tower-api/src/models/list_runs_response.rs +++ b/crates/tower-api/src/models/list_runs_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_schedules_response.rs b/crates/tower-api/src/models/list_schedules_response.rs index ded64720..403787d2 100644 --- a/crates/tower-api/src/models/list_schedules_response.rs +++ b/crates/tower-api/src/models/list_schedules_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_secret_environments_response.rs b/crates/tower-api/src/models/list_secret_environments_response.rs index a2337e8d..98394a6a 100644 --- a/crates/tower-api/src/models/list_secret_environments_response.rs +++ b/crates/tower-api/src/models/list_secret_environments_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_secrets_response.rs b/crates/tower-api/src/models/list_secrets_response.rs index a3eb54e3..7600e6ec 100644 --- a/crates/tower-api/src/models/list_secrets_response.rs +++ b/crates/tower-api/src/models/list_secrets_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_service_account_api_keys_response.rs b/crates/tower-api/src/models/list_service_account_api_keys_response.rs index 2df25684..d870ce97 100644 --- a/crates/tower-api/src/models/list_service_account_api_keys_response.rs +++ b/crates/tower-api/src/models/list_service_account_api_keys_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_service_accounts_response.rs b/crates/tower-api/src/models/list_service_accounts_response.rs index 87fa954d..3d7f1fcf 100644 --- a/crates/tower-api/src/models/list_service_accounts_response.rs +++ b/crates/tower-api/src/models/list_service_accounts_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_team_invitations_response.rs b/crates/tower-api/src/models/list_team_invitations_response.rs index cae3cd39..54c9b750 100644 --- a/crates/tower-api/src/models/list_team_invitations_response.rs +++ b/crates/tower-api/src/models/list_team_invitations_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_team_members_response.rs b/crates/tower-api/src/models/list_team_members_response.rs index 231c2daa..e12cfb52 100644 --- a/crates/tower-api/src/models/list_team_members_response.rs +++ b/crates/tower-api/src/models/list_team_members_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_teams_response.rs b/crates/tower-api/src/models/list_teams_response.rs index f4cd0cd6..2be38ff2 100644 --- a/crates/tower-api/src/models/list_teams_response.rs +++ b/crates/tower-api/src/models/list_teams_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/list_webhooks_response.rs b/crates/tower-api/src/models/list_webhooks_response.rs index 23bb637e..d08f3f36 100644 --- a/crates/tower-api/src/models/list_webhooks_response.rs +++ b/crates/tower-api/src/models/list_webhooks_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/mod.rs b/crates/tower-api/src/models/mod.rs index bdcad19e..f08dced9 100644 --- a/crates/tower-api/src/models/mod.rs +++ b/crates/tower-api/src/models/mod.rs @@ -22,10 +22,22 @@ pub mod app_version; pub use self::app_version::AppVersion; pub mod authentication_context; pub use self::authentication_context::AuthenticationContext; +pub mod batch_describe_runs_logs_params; +pub use self::batch_describe_runs_logs_params::BatchDescribeRunsLogsParams; +pub mod batch_describe_runs_params; +pub use self::batch_describe_runs_params::BatchDescribeRunsParams; +pub mod batch_describe_runs_response; +pub use self::batch_describe_runs_response::BatchDescribeRunsResponse; +pub mod batch_error; +pub use self::batch_error::BatchError; +pub mod batch_run_and_links; +pub use self::batch_run_and_links::BatchRunAndLinks; pub mod batch_schedule_params; pub use self::batch_schedule_params::BatchScheduleParams; pub mod batch_schedule_response; pub use self::batch_schedule_response::BatchScheduleResponse; +pub mod batched_run_log_lines; +pub use self::batched_run_log_lines::BatchedRunLogLines; pub mod cancel_run_response; pub use self::cancel_run_response::CancelRunResponse; pub mod catalog; @@ -36,6 +48,8 @@ pub mod catalog_fact; pub use self::catalog_fact::CatalogFact; pub mod catalog_property; pub use self::catalog_property::CatalogProperty; +pub mod catalog_usage; +pub use self::catalog_usage::CatalogUsage; pub mod claim_device_login_ticket_params; pub use self::claim_device_login_ticket_params::ClaimDeviceLoginTicketParams; pub mod claim_device_login_ticket_response; @@ -150,6 +164,8 @@ pub mod describe_catalog_fact_response; pub use self::describe_catalog_fact_response::DescribeCatalogFactResponse; pub mod describe_catalog_response; pub use self::describe_catalog_response::DescribeCatalogResponse; +pub mod describe_catalog_usage_response; +pub use self::describe_catalog_usage_response::DescribeCatalogUsageResponse; pub mod describe_device_login_session_response; pub use self::describe_device_login_session_response::DescribeDeviceLoginSessionResponse; pub mod describe_email_preferences_body; @@ -278,6 +294,8 @@ pub mod list_webhooks_response; pub use self::list_webhooks_response::ListWebhooksResponse; pub mod organization; pub use self::organization::Organization; +pub mod organization_storage_usage; +pub use self::organization_storage_usage::OrganizationStorageUsage; pub mod organization_usage; pub use self::organization_usage::OrganizationUsage; pub mod pagination; @@ -304,6 +322,8 @@ pub mod resend_team_invitation_response; pub use self::resend_team_invitation_response::ResendTeamInvitationResponse; pub mod run; pub use self::run::Run; +pub mod run_and_links; +pub use self::run_and_links::RunAndLinks; pub mod run_app_initiator_data; pub use self::run_app_initiator_data::RunAppInitiatorData; pub mod run_app_params; @@ -324,6 +344,8 @@ pub mod run_initiator; pub use self::run_initiator::RunInitiator; pub mod run_initiator_details; pub use self::run_initiator_details::RunInitiatorDetails; +pub mod run_links; +pub use self::run_links::RunLinks; pub mod run_log_line; pub use self::run_log_line::RunLogLine; pub mod run_parameter; diff --git a/crates/tower-api/src/models/organization.rs b/crates/tower-api/src/models/organization.rs index 61247d6b..e09184df 100644 --- a/crates/tower-api/src/models/organization.rs +++ b/crates/tower-api/src/models/organization.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/organization_storage_usage.rs b/crates/tower-api/src/models/organization_storage_usage.rs new file mode 100644 index 00000000..aa3a707c --- /dev/null +++ b/crates/tower-api/src/models/organization_storage_usage.rs @@ -0,0 +1,33 @@ +/* + * Tower API + * + * REST API to interact with Tower Services. + * + * The version of the OpenAPI document: v0.11.24 + * Contact: hello@tower.dev + * Generated by: https://openapi-generator.tech + */ +use crate::models; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::{serde_as, DefaultOnNull}; + +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct OrganizationStorageUsage { + /// When the total was measured. Null when no measurement is available. + #[serde(rename = "measured_at", deserialize_with = "Option::deserialize")] + pub measured_at: Option, + /// Physical bytes across the organization's Tower-managed catalogs, including Iceberg metadata and not-yet-compacted snapshot history. + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "total_bytes")] + pub total_bytes: i64, +} + +impl OrganizationStorageUsage { + pub fn new(measured_at: Option, total_bytes: i64) -> OrganizationStorageUsage { + OrganizationStorageUsage { + measured_at, + total_bytes, + } + } +} diff --git a/crates/tower-api/src/models/organization_usage.rs b/crates/tower-api/src/models/organization_usage.rs index 18026159..e4f3f797 100644 --- a/crates/tower-api/src/models/organization_usage.rs +++ b/crates/tower-api/src/models/organization_usage.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ @@ -45,6 +45,10 @@ pub struct OrganizationUsage { #[serde_as(as = "DefaultOnNull")] #[serde(rename = "self_hosted_runners")] pub self_hosted_runners: models::UsageLimit, + /// Current pay-as-you-go Tower-managed storage usage. This is not a plan limit. + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "storage")] + pub storage: models::OrganizationStorageUsage, } impl OrganizationUsage { @@ -56,6 +60,7 @@ impl OrganizationUsage { members: models::UsageLimit, organization_name: String, self_hosted_runners: models::UsageLimit, + storage: models::OrganizationStorageUsage, ) -> OrganizationUsage { OrganizationUsage { schema: None, @@ -66,6 +71,7 @@ impl OrganizationUsage { members, organization_name, self_hosted_runners, + storage, } } } diff --git a/crates/tower-api/src/models/pagination.rs b/crates/tower-api/src/models/pagination.rs index 9cd2012f..0c4978aa 100644 --- a/crates/tower-api/src/models/pagination.rs +++ b/crates/tower-api/src/models/pagination.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/parameter.rs b/crates/tower-api/src/models/parameter.rs index 0226331c..c51c4bde 100644 --- a/crates/tower-api/src/models/parameter.rs +++ b/crates/tower-api/src/models/parameter.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/plan.rs b/crates/tower-api/src/models/plan.rs index 9daedc53..50370855 100644 --- a/crates/tower-api/src/models/plan.rs +++ b/crates/tower-api/src/models/plan.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/refresh_session_params.rs b/crates/tower-api/src/models/refresh_session_params.rs index 21101fff..33f4d805 100644 --- a/crates/tower-api/src/models/refresh_session_params.rs +++ b/crates/tower-api/src/models/refresh_session_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/refresh_session_response.rs b/crates/tower-api/src/models/refresh_session_response.rs index cc00dc96..c0621a10 100644 --- a/crates/tower-api/src/models/refresh_session_response.rs +++ b/crates/tower-api/src/models/refresh_session_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/regenerate_guest_login_url_params.rs b/crates/tower-api/src/models/regenerate_guest_login_url_params.rs index a512b9c8..3eff969a 100644 --- a/crates/tower-api/src/models/regenerate_guest_login_url_params.rs +++ b/crates/tower-api/src/models/regenerate_guest_login_url_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/regenerate_guest_login_url_response.rs b/crates/tower-api/src/models/regenerate_guest_login_url_response.rs index f0a97ee9..e2bb57fc 100644 --- a/crates/tower-api/src/models/regenerate_guest_login_url_response.rs +++ b/crates/tower-api/src/models/regenerate_guest_login_url_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/remove_team_member_params.rs b/crates/tower-api/src/models/remove_team_member_params.rs index 96d83918..08f47af2 100644 --- a/crates/tower-api/src/models/remove_team_member_params.rs +++ b/crates/tower-api/src/models/remove_team_member_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/remove_team_member_response.rs b/crates/tower-api/src/models/remove_team_member_response.rs index a11492fe..11cdad0d 100644 --- a/crates/tower-api/src/models/remove_team_member_response.rs +++ b/crates/tower-api/src/models/remove_team_member_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/resend_team_invitation_params.rs b/crates/tower-api/src/models/resend_team_invitation_params.rs index 4691e655..54be4733 100644 --- a/crates/tower-api/src/models/resend_team_invitation_params.rs +++ b/crates/tower-api/src/models/resend_team_invitation_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/resend_team_invitation_response.rs b/crates/tower-api/src/models/resend_team_invitation_response.rs index 17313fc5..51d90376 100644 --- a/crates/tower-api/src/models/resend_team_invitation_response.rs +++ b/crates/tower-api/src/models/resend_team_invitation_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run.rs b/crates/tower-api/src/models/run.rs index 2e38e158..52dfbf57 100644 --- a/crates/tower-api/src/models/run.rs +++ b/crates/tower-api/src/models/run.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_and_links.rs b/crates/tower-api/src/models/run_and_links.rs new file mode 100644 index 00000000..7d4c6060 --- /dev/null +++ b/crates/tower-api/src/models/run_and_links.rs @@ -0,0 +1,29 @@ +/* + * Tower API + * + * REST API to interact with Tower Services. + * + * The version of the OpenAPI document: v0.11.24 + * Contact: hello@tower.dev + * Generated by: https://openapi-generator.tech + */ +use crate::models; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::{serde_as, DefaultOnNull}; + +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct RunAndLinks { + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "$links")] + pub dollar_links: models::RunLinks, + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "run")] + pub run: models::Run, +} + +impl RunAndLinks { + pub fn new(dollar_links: models::RunLinks, run: models::Run) -> RunAndLinks { + RunAndLinks { dollar_links, run } + } +} diff --git a/crates/tower-api/src/models/run_app_initiator_data.rs b/crates/tower-api/src/models/run_app_initiator_data.rs index 37ee9516..c7333f05 100644 --- a/crates/tower-api/src/models/run_app_initiator_data.rs +++ b/crates/tower-api/src/models/run_app_initiator_data.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_app_params.rs b/crates/tower-api/src/models/run_app_params.rs index 9917741f..9dcda5c0 100644 --- a/crates/tower-api/src/models/run_app_params.rs +++ b/crates/tower-api/src/models/run_app_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_app_response.rs b/crates/tower-api/src/models/run_app_response.rs index 4deee56f..d5b483d5 100644 --- a/crates/tower-api/src/models/run_app_response.rs +++ b/crates/tower-api/src/models/run_app_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_attempt.rs b/crates/tower-api/src/models/run_attempt.rs index b1e88712..fcb84aa5 100644 --- a/crates/tower-api/src/models/run_attempt.rs +++ b/crates/tower-api/src/models/run_attempt.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_creator.rs b/crates/tower-api/src/models/run_creator.rs index 6139b8cd..f7d04389 100644 --- a/crates/tower-api/src/models/run_creator.rs +++ b/crates/tower-api/src/models/run_creator.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_failure_alert.rs b/crates/tower-api/src/models/run_failure_alert.rs index 340b228e..12dd319b 100644 --- a/crates/tower-api/src/models/run_failure_alert.rs +++ b/crates/tower-api/src/models/run_failure_alert.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_graph_node.rs b/crates/tower-api/src/models/run_graph_node.rs index 2ccaafc9..ed1f4104 100644 --- a/crates/tower-api/src/models/run_graph_node.rs +++ b/crates/tower-api/src/models/run_graph_node.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_graph_run_id.rs b/crates/tower-api/src/models/run_graph_run_id.rs index 60a9dc3a..1ea0c118 100644 --- a/crates/tower-api/src/models/run_graph_run_id.rs +++ b/crates/tower-api/src/models/run_graph_run_id.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_initiator.rs b/crates/tower-api/src/models/run_initiator.rs index c859adec..ed7198b3 100644 --- a/crates/tower-api/src/models/run_initiator.rs +++ b/crates/tower-api/src/models/run_initiator.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_initiator_details.rs b/crates/tower-api/src/models/run_initiator_details.rs index 5787e17f..5c0ca4e4 100644 --- a/crates/tower-api/src/models/run_initiator_details.rs +++ b/crates/tower-api/src/models/run_initiator_details.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_links.rs b/crates/tower-api/src/models/run_links.rs new file mode 100644 index 00000000..d9770407 --- /dev/null +++ b/crates/tower-api/src/models/run_links.rs @@ -0,0 +1,32 @@ +/* + * Tower API + * + * REST API to interact with Tower Services. + * + * The version of the OpenAPI document: v0.11.24 + * Contact: hello@tower.dev + * Generated by: https://openapi-generator.tech + */ +use crate::models; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::{serde_as, DefaultOnNull}; + +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct RunLinks { + /// The number of the next run, if any. + #[serde(rename = "next_number", deserialize_with = "Option::deserialize")] + pub next_number: Option, + /// The number of the previous run, if any. + #[serde(rename = "prev_number", deserialize_with = "Option::deserialize")] + pub prev_number: Option, +} + +impl RunLinks { + pub fn new(next_number: Option, prev_number: Option) -> RunLinks { + RunLinks { + next_number, + prev_number, + } + } +} diff --git a/crates/tower-api/src/models/run_log_line.rs b/crates/tower-api/src/models/run_log_line.rs index 5f85622f..81da108b 100644 --- a/crates/tower-api/src/models/run_log_line.rs +++ b/crates/tower-api/src/models/run_log_line.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_parameter.rs b/crates/tower-api/src/models/run_parameter.rs index 92c5e901..faf59f2b 100644 --- a/crates/tower-api/src/models/run_parameter.rs +++ b/crates/tower-api/src/models/run_parameter.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_results.rs b/crates/tower-api/src/models/run_results.rs index bb2d2185..807d4064 100644 --- a/crates/tower-api/src/models/run_results.rs +++ b/crates/tower-api/src/models/run_results.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_retry_policy.rs b/crates/tower-api/src/models/run_retry_policy.rs index fb9beaa9..7e3a59ee 100644 --- a/crates/tower-api/src/models/run_retry_policy.rs +++ b/crates/tower-api/src/models/run_retry_policy.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_run_initiator_details.rs b/crates/tower-api/src/models/run_run_initiator_details.rs index 8cc3e977..423b821a 100644 --- a/crates/tower-api/src/models/run_run_initiator_details.rs +++ b/crates/tower-api/src/models/run_run_initiator_details.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_statistics.rs b/crates/tower-api/src/models/run_statistics.rs index 9e3daa52..71232767 100644 --- a/crates/tower-api/src/models/run_statistics.rs +++ b/crates/tower-api/src/models/run_statistics.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/run_timeseries_point.rs b/crates/tower-api/src/models/run_timeseries_point.rs index 38cb0a6f..b4423f86 100644 --- a/crates/tower-api/src/models/run_timeseries_point.rs +++ b/crates/tower-api/src/models/run_timeseries_point.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/runner.rs b/crates/tower-api/src/models/runner.rs index d7d50001..921a8ac7 100644 --- a/crates/tower-api/src/models/runner.rs +++ b/crates/tower-api/src/models/runner.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/runner_credentials.rs b/crates/tower-api/src/models/runner_credentials.rs index d6a356b5..1b8c3933 100644 --- a/crates/tower-api/src/models/runner_credentials.rs +++ b/crates/tower-api/src/models/runner_credentials.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/schedule.rs b/crates/tower-api/src/models/schedule.rs index bfe69267..c4ad3d8a 100644 --- a/crates/tower-api/src/models/schedule.rs +++ b/crates/tower-api/src/models/schedule.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/schedule_owner.rs b/crates/tower-api/src/models/schedule_owner.rs index fa51d221..f803f2aa 100644 --- a/crates/tower-api/src/models/schedule_owner.rs +++ b/crates/tower-api/src/models/schedule_owner.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/schedule_run_initiator_details.rs b/crates/tower-api/src/models/schedule_run_initiator_details.rs index 31177899..90192fe6 100644 --- a/crates/tower-api/src/models/schedule_run_initiator_details.rs +++ b/crates/tower-api/src/models/schedule_run_initiator_details.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/search_runs_response.rs b/crates/tower-api/src/models/search_runs_response.rs index 46525224..2594dc7b 100644 --- a/crates/tower-api/src/models/search_runs_response.rs +++ b/crates/tower-api/src/models/search_runs_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/secret.rs b/crates/tower-api/src/models/secret.rs index 875456c3..3c7df533 100644 --- a/crates/tower-api/src/models/secret.rs +++ b/crates/tower-api/src/models/secret.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/server_sent_events_inner.rs b/crates/tower-api/src/models/server_sent_events_inner.rs index 500613a8..0d6cd701 100644 --- a/crates/tower-api/src/models/server_sent_events_inner.rs +++ b/crates/tower-api/src/models/server_sent_events_inner.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/server_sent_events_inner_1.rs b/crates/tower-api/src/models/server_sent_events_inner_1.rs index 3a685e7c..b1e8bf94 100644 --- a/crates/tower-api/src/models/server_sent_events_inner_1.rs +++ b/crates/tower-api/src/models/server_sent_events_inner_1.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/server_sent_events_inner_2.rs b/crates/tower-api/src/models/server_sent_events_inner_2.rs index acd3ee24..ee1d1970 100644 --- a/crates/tower-api/src/models/server_sent_events_inner_2.rs +++ b/crates/tower-api/src/models/server_sent_events_inner_2.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/service_account.rs b/crates/tower-api/src/models/service_account.rs index af725e1e..acf89177 100644 --- a/crates/tower-api/src/models/service_account.rs +++ b/crates/tower-api/src/models/service_account.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/service_account_creator.rs b/crates/tower-api/src/models/service_account_creator.rs index 6b932206..810dc15e 100644 --- a/crates/tower-api/src/models/service_account_creator.rs +++ b/crates/tower-api/src/models/service_account_creator.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/session.rs b/crates/tower-api/src/models/session.rs index e50b4821..5e9f4936 100644 --- a/crates/tower-api/src/models/session.rs +++ b/crates/tower-api/src/models/session.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/shoulder_tap.rs b/crates/tower-api/src/models/shoulder_tap.rs index a1402155..3046f672 100644 --- a/crates/tower-api/src/models/shoulder_tap.rs +++ b/crates/tower-api/src/models/shoulder_tap.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/sse_warning.rs b/crates/tower-api/src/models/sse_warning.rs index 93724481..cc7621fc 100644 --- a/crates/tower-api/src/models/sse_warning.rs +++ b/crates/tower-api/src/models/sse_warning.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/statistics_settings.rs b/crates/tower-api/src/models/statistics_settings.rs index 1b928471..6fcb104b 100644 --- a/crates/tower-api/src/models/statistics_settings.rs +++ b/crates/tower-api/src/models/statistics_settings.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/tag_filter.rs b/crates/tower-api/src/models/tag_filter.rs index f398dd19..39d01d1f 100644 --- a/crates/tower-api/src/models/tag_filter.rs +++ b/crates/tower-api/src/models/tag_filter.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/team.rs b/crates/tower-api/src/models/team.rs index 1a3378ae..f4e63e6c 100644 --- a/crates/tower-api/src/models/team.rs +++ b/crates/tower-api/src/models/team.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/team_invitation.rs b/crates/tower-api/src/models/team_invitation.rs index cb2d6174..5168ea52 100644 --- a/crates/tower-api/src/models/team_invitation.rs +++ b/crates/tower-api/src/models/team_invitation.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/team_membership.rs b/crates/tower-api/src/models/team_membership.rs index 54703232..bac06171 100644 --- a/crates/tower-api/src/models/team_membership.rs +++ b/crates/tower-api/src/models/team_membership.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/test_webhook_response.rs b/crates/tower-api/src/models/test_webhook_response.rs index fd43fd86..dc66ad30 100644 --- a/crates/tower-api/src/models/test_webhook_response.rs +++ b/crates/tower-api/src/models/test_webhook_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/token.rs b/crates/tower-api/src/models/token.rs index e6619048..d1532840 100644 --- a/crates/tower-api/src/models/token.rs +++ b/crates/tower-api/src/models/token.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ @@ -18,6 +18,7 @@ pub struct Token { #[serde_as(as = "DefaultOnNull")] #[serde(rename = "access_token")] pub access_token: String, + /// This property is deprecated. Use access_token instead. #[serde_as(as = "DefaultOnNull")] #[serde(rename = "jwt")] pub jwt: String, diff --git a/crates/tower-api/src/models/update_account_params.rs b/crates/tower-api/src/models/update_account_params.rs index 531f2c4d..d56bb90b 100644 --- a/crates/tower-api/src/models/update_account_params.rs +++ b/crates/tower-api/src/models/update_account_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ @@ -52,6 +52,10 @@ pub enum ExecutionRegion { UsWest2, #[serde(rename = "eu-west-1")] EuWest1, + #[serde(rename = "ap-northeast-1")] + ApNortheast1, + #[serde(rename = "ap-southeast-2")] + ApSoutheast2, } impl Default for ExecutionRegion { @@ -71,9 +75,18 @@ impl<'de> Deserialize<'de> for ExecutionRegion { "us-east-1" => Ok(Self::UsEast1), "us-west-2" => Ok(Self::UsWest2), "eu-west-1" => Ok(Self::EuWest1), + "ap-northeast-1" => Ok(Self::ApNortheast1), + "ap-southeast-2" => Ok(Self::ApSoutheast2), _ => Err(serde::de::Error::unknown_variant( &s, - &["eu-central-1", "us-east-1", "us-west-2", "eu-west-1"], + &[ + "eu-central-1", + "us-east-1", + "us-west-2", + "eu-west-1", + "ap-northeast-1", + "ap-southeast-2", + ], )), } } diff --git a/crates/tower-api/src/models/update_account_response.rs b/crates/tower-api/src/models/update_account_response.rs index d1f93fa3..e5bed001 100644 --- a/crates/tower-api/src/models/update_account_response.rs +++ b/crates/tower-api/src/models/update_account_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_app_environment_params.rs b/crates/tower-api/src/models/update_app_environment_params.rs index 15576bad..71faf506 100644 --- a/crates/tower-api/src/models/update_app_environment_params.rs +++ b/crates/tower-api/src/models/update_app_environment_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_app_environment_response.rs b/crates/tower-api/src/models/update_app_environment_response.rs index 56f4df95..6f1df29d 100644 --- a/crates/tower-api/src/models/update_app_environment_response.rs +++ b/crates/tower-api/src/models/update_app_environment_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_app_params.rs b/crates/tower-api/src/models/update_app_params.rs index 0097d767..ea65ef5a 100644 --- a/crates/tower-api/src/models/update_app_params.rs +++ b/crates/tower-api/src/models/update_app_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_app_response.rs b/crates/tower-api/src/models/update_app_response.rs index b7cdab5f..f2af1038 100644 --- a/crates/tower-api/src/models/update_app_response.rs +++ b/crates/tower-api/src/models/update_app_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_catalog_fact_body.rs b/crates/tower-api/src/models/update_catalog_fact_body.rs index 70942a3b..9c8fe818 100644 --- a/crates/tower-api/src/models/update_catalog_fact_body.rs +++ b/crates/tower-api/src/models/update_catalog_fact_body.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_catalog_fact_response.rs b/crates/tower-api/src/models/update_catalog_fact_response.rs index a880eafd..b496893c 100644 --- a/crates/tower-api/src/models/update_catalog_fact_response.rs +++ b/crates/tower-api/src/models/update_catalog_fact_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_catalog_params.rs b/crates/tower-api/src/models/update_catalog_params.rs index 98c75a2c..09bbfc2f 100644 --- a/crates/tower-api/src/models/update_catalog_params.rs +++ b/crates/tower-api/src/models/update_catalog_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ @@ -17,7 +17,7 @@ pub struct UpdateCatalogParams { /// A URL to the JSON Schema for this object. #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")] pub schema: Option, - /// New environment for the catalog + /// The environment containing the catalog to update. Catalogs cannot be moved between environments. #[serde_as(as = "DefaultOnNull")] #[serde(rename = "environment")] pub environment: String, diff --git a/crates/tower-api/src/models/update_catalog_response.rs b/crates/tower-api/src/models/update_catalog_response.rs index 012a5405..6e08c504 100644 --- a/crates/tower-api/src/models/update_catalog_response.rs +++ b/crates/tower-api/src/models/update_catalog_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_email_preferences_body.rs b/crates/tower-api/src/models/update_email_preferences_body.rs index 0110b72d..5eaea2e7 100644 --- a/crates/tower-api/src/models/update_email_preferences_body.rs +++ b/crates/tower-api/src/models/update_email_preferences_body.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_environment_params.rs b/crates/tower-api/src/models/update_environment_params.rs index 3dbfaab2..2026fa92 100644 --- a/crates/tower-api/src/models/update_environment_params.rs +++ b/crates/tower-api/src/models/update_environment_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_environment_response.rs b/crates/tower-api/src/models/update_environment_response.rs index 77acbdee..69d0d1d9 100644 --- a/crates/tower-api/src/models/update_environment_response.rs +++ b/crates/tower-api/src/models/update_environment_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_my_team_invitation_params.rs b/crates/tower-api/src/models/update_my_team_invitation_params.rs index 4d410c0e..f1360788 100644 --- a/crates/tower-api/src/models/update_my_team_invitation_params.rs +++ b/crates/tower-api/src/models/update_my_team_invitation_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_my_team_invitation_response.rs b/crates/tower-api/src/models/update_my_team_invitation_response.rs index 7b89b844..31673486 100644 --- a/crates/tower-api/src/models/update_my_team_invitation_response.rs +++ b/crates/tower-api/src/models/update_my_team_invitation_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_organization_params.rs b/crates/tower-api/src/models/update_organization_params.rs index a7928d5b..0f868428 100644 --- a/crates/tower-api/src/models/update_organization_params.rs +++ b/crates/tower-api/src/models/update_organization_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_organization_response.rs b/crates/tower-api/src/models/update_organization_response.rs index 8d3b9342..2f281f99 100644 --- a/crates/tower-api/src/models/update_organization_response.rs +++ b/crates/tower-api/src/models/update_organization_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_plan_params.rs b/crates/tower-api/src/models/update_plan_params.rs index 8d5453c6..dc4ce1c1 100644 --- a/crates/tower-api/src/models/update_plan_params.rs +++ b/crates/tower-api/src/models/update_plan_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_plan_response.rs b/crates/tower-api/src/models/update_plan_response.rs index ac46aa70..78b087b0 100644 --- a/crates/tower-api/src/models/update_plan_response.rs +++ b/crates/tower-api/src/models/update_plan_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_schedule_params.rs b/crates/tower-api/src/models/update_schedule_params.rs index 353fee57..7a610ec8 100644 --- a/crates/tower-api/src/models/update_schedule_params.rs +++ b/crates/tower-api/src/models/update_schedule_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_schedule_response.rs b/crates/tower-api/src/models/update_schedule_response.rs index 25c07220..f47681fc 100644 --- a/crates/tower-api/src/models/update_schedule_response.rs +++ b/crates/tower-api/src/models/update_schedule_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_secret_params.rs b/crates/tower-api/src/models/update_secret_params.rs index 24022a3a..f26d7067 100644 --- a/crates/tower-api/src/models/update_secret_params.rs +++ b/crates/tower-api/src/models/update_secret_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_secret_response.rs b/crates/tower-api/src/models/update_secret_response.rs index 6fe7b48c..7aed911b 100644 --- a/crates/tower-api/src/models/update_secret_response.rs +++ b/crates/tower-api/src/models/update_secret_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_service_account_params.rs b/crates/tower-api/src/models/update_service_account_params.rs index e6a748ee..2b0c7a49 100644 --- a/crates/tower-api/src/models/update_service_account_params.rs +++ b/crates/tower-api/src/models/update_service_account_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_service_account_response.rs b/crates/tower-api/src/models/update_service_account_response.rs index 2ae9c535..80937d5c 100644 --- a/crates/tower-api/src/models/update_service_account_response.rs +++ b/crates/tower-api/src/models/update_service_account_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_team_member_params.rs b/crates/tower-api/src/models/update_team_member_params.rs index bec1cf7e..50279bc0 100644 --- a/crates/tower-api/src/models/update_team_member_params.rs +++ b/crates/tower-api/src/models/update_team_member_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_team_member_response.rs b/crates/tower-api/src/models/update_team_member_response.rs index c8afc1b3..89bb3f49 100644 --- a/crates/tower-api/src/models/update_team_member_response.rs +++ b/crates/tower-api/src/models/update_team_member_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_team_params.rs b/crates/tower-api/src/models/update_team_params.rs index 9a91486e..d859a994 100644 --- a/crates/tower-api/src/models/update_team_params.rs +++ b/crates/tower-api/src/models/update_team_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ @@ -45,6 +45,10 @@ pub enum ExecutionRegion { UsWest2, #[serde(rename = "eu-west-1")] EuWest1, + #[serde(rename = "ap-northeast-1")] + ApNortheast1, + #[serde(rename = "ap-southeast-2")] + ApSoutheast2, } impl Default for ExecutionRegion { @@ -64,9 +68,18 @@ impl<'de> Deserialize<'de> for ExecutionRegion { "us-east-1" => Ok(Self::UsEast1), "us-west-2" => Ok(Self::UsWest2), "eu-west-1" => Ok(Self::EuWest1), + "ap-northeast-1" => Ok(Self::ApNortheast1), + "ap-southeast-2" => Ok(Self::ApSoutheast2), _ => Err(serde::de::Error::unknown_variant( &s, - &["eu-central-1", "us-east-1", "us-west-2", "eu-west-1"], + &[ + "eu-central-1", + "us-east-1", + "us-west-2", + "eu-west-1", + "ap-northeast-1", + "ap-southeast-2", + ], )), } } diff --git a/crates/tower-api/src/models/update_team_response.rs b/crates/tower-api/src/models/update_team_response.rs index 1d3606a8..322a9419 100644 --- a/crates/tower-api/src/models/update_team_response.rs +++ b/crates/tower-api/src/models/update_team_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_user_params.rs b/crates/tower-api/src/models/update_user_params.rs index 2f2c68e1..1afecc79 100644 --- a/crates/tower-api/src/models/update_user_params.rs +++ b/crates/tower-api/src/models/update_user_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_user_response.rs b/crates/tower-api/src/models/update_user_response.rs index 051691c7..f9292943 100644 --- a/crates/tower-api/src/models/update_user_response.rs +++ b/crates/tower-api/src/models/update_user_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_webhook_params.rs b/crates/tower-api/src/models/update_webhook_params.rs index 1bb54639..c0bd91d8 100644 --- a/crates/tower-api/src/models/update_webhook_params.rs +++ b/crates/tower-api/src/models/update_webhook_params.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/update_webhook_response.rs b/crates/tower-api/src/models/update_webhook_response.rs index 1a75fd49..6a951c65 100644 --- a/crates/tower-api/src/models/update_webhook_response.rs +++ b/crates/tower-api/src/models/update_webhook_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/usage_limit.rs b/crates/tower-api/src/models/usage_limit.rs index 9726cf29..5ad7921c 100644 --- a/crates/tower-api/src/models/usage_limit.rs +++ b/crates/tower-api/src/models/usage_limit.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/usage_metric_time_series_point.rs b/crates/tower-api/src/models/usage_metric_time_series_point.rs index 6c4bdef2..c56ed810 100644 --- a/crates/tower-api/src/models/usage_metric_time_series_point.rs +++ b/crates/tower-api/src/models/usage_metric_time_series_point.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/user.rs b/crates/tower-api/src/models/user.rs index e96c7257..ae2407e8 100644 --- a/crates/tower-api/src/models/user.rs +++ b/crates/tower-api/src/models/user.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/vend_catalog_credentials_body.rs b/crates/tower-api/src/models/vend_catalog_credentials_body.rs index 6c54274a..254b5dff 100644 --- a/crates/tower-api/src/models/vend_catalog_credentials_body.rs +++ b/crates/tower-api/src/models/vend_catalog_credentials_body.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-api/src/models/vend_catalog_credentials_response.rs b/crates/tower-api/src/models/vend_catalog_credentials_response.rs index 461dd693..bf5860b7 100644 --- a/crates/tower-api/src/models/vend_catalog_credentials_response.rs +++ b/crates/tower-api/src/models/vend_catalog_credentials_response.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ @@ -20,13 +20,21 @@ pub struct VendCatalogCredentialsResponse { #[serde_as(as = "DefaultOnNull")] #[serde(rename = "credentials")] pub credentials: models::CatalogCredentials, + /// Environment containing the catalog definition. + #[serde_as(as = "DefaultOnNull")] + #[serde(rename = "environment")] + pub environment: String, } impl VendCatalogCredentialsResponse { - pub fn new(credentials: models::CatalogCredentials) -> VendCatalogCredentialsResponse { + pub fn new( + credentials: models::CatalogCredentials, + environment: String, + ) -> VendCatalogCredentialsResponse { VendCatalogCredentialsResponse { schema: None, credentials, + environment, } } } diff --git a/crates/tower-api/src/models/webhook.rs b/crates/tower-api/src/models/webhook.rs index 3f6031bd..b3915d38 100644 --- a/crates/tower-api/src/models/webhook.rs +++ b/crates/tower-api/src/models/webhook.rs @@ -3,7 +3,7 @@ * * REST API to interact with Tower Services. * - * The version of the OpenAPI document: v0.11.17 + * The version of the OpenAPI document: v0.11.24 * Contact: hello@tower.dev * Generated by: https://openapi-generator.tech */ diff --git a/crates/tower-cmd/src/api.rs b/crates/tower-cmd/src/api.rs index a76fc97f..b88c656c 100644 --- a/crates/tower-cmd/src/api.rs +++ b/crates/tower-cmd/src/api.rs @@ -200,7 +200,6 @@ pub async fn create_app( create_app_params: tower_api::models::CreateAppParams { schema: None, name: name.to_string(), - // API create expects short_description; CLI/Towerfile expose "description". short_description: Some(description.to_string()), slug: None, is_externally_accessible: None, @@ -695,25 +694,79 @@ pub async fn list_teams( pub enum LogStreamEvent { EventLog(tower_api::models::RunLogLine), - EventWarning(tower_api::models::EventWarning), + EventWarning(tower_api::models::SseWarning), } #[derive(Debug)] pub enum LogStreamError { Reqwest(reqwest::Error), + /// The server rejected the stream request with a non-success status code. + InvalidStatus(StatusCode), Unknown, } +impl LogStreamError { + /// The HTTP status carried by this error, when one is known. + pub fn status(&self) -> Option { + match self { + LogStreamError::InvalidStatus(status) => Some(*status), + LogStreamError::Reqwest(err) => err.status().map(|s| { + StatusCode::from_u16(s.as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR) + }), + LogStreamError::Unknown => None, + } + } + + /// A stream-open failure is fatal (not worth retrying) when the server + /// answered with a client error other than 429 Too Many Requests. + pub fn is_fatal(&self) -> bool { + match self.status() { + Some(status) => status.is_client_error() && status != StatusCode::TOO_MANY_REQUESTS, + None => false, + } + } +} + impl std::fmt::Display for LogStreamError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - LogStreamError::Reqwest(err) => write!(f, "{err}"), - LogStreamError::Unknown => write!(f, "unknown log stream error"), + LogStreamError::Reqwest(err) => { + write!(f, "transport error while streaming run logs: {}", err) + } + LogStreamError::InvalidStatus(status) => { + write!( + f, + "the server rejected the log stream request with status {}", + status + ) + } + LogStreamError::Unknown => write!(f, "unknown error while streaming run logs"), } } } -impl std::error::Error for LogStreamError {} +impl std::error::Error for LogStreamError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + LogStreamError::Reqwest(err) => Some(err), + _ => None, + } + } +} + +/// Parses the `data` field of a `warning` SSE event. On the wire it carries +/// the bare warning payload (the `SseWarning` fields), but the enveloped +/// `EventWarning` shape (`{event, data, ...}`) is accepted too for +/// robustness. +fn parse_warning_payload(data: &str) -> Option { + if let Ok(warning) = serde_json::from_str::(data) { + return Some(warning); + } + + serde_json::from_str::(data) + .map(|event| event.data) + .ok() +} impl From for LogStreamError { fn from(err: reqwest_eventsource::CannotCloneRequestError) -> Self { @@ -746,31 +799,10 @@ async fn drain_run_logs_stream(mut source: EventSource, tx: mpsc::Sender { - let event_warning = serde_json::from_str(&message.data); - match event_warning { - Ok(event) => { - tx.send(LogStreamEvent::EventWarning(event)).await.ok(); - } - Err(err) => { - let warning_data = serde_json::from_str(&message.data); - match warning_data { - Ok(data) => { - let event = tower_api::models::EventWarning { - data, - event: tower_api::models::event_warning::Event::Warning, - id: None, - retry: None, - }; - tx.send(LogStreamEvent::EventWarning(event)).await.ok(); - } - Err(_) => { - debug!( - "Failed to parse warning message: {:?}. Error: {}", - message.data, err - ); - } - } - } + if let Some(warning) = parse_warning_payload(&message.data) { + tx.send(LogStreamEvent::EventWarning(warning)).await.ok(); + } else { + debug!("Failed to parse warning message: {:?}", message.data); } } _ => { @@ -841,6 +873,11 @@ pub async fn stream_run_logs( } Err(err) => match err { reqwest_eventsource::Error::Transport(e) => Err(LogStreamError::Reqwest(e)), + reqwest_eventsource::Error::InvalidStatusCode(status, _) => { + let status = StatusCode::from_u16(status.as_u16()) + .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); + Err(LogStreamError::InvalidStatus(status)) + } reqwest_eventsource::Error::StreamEnded => { drop(tx); Ok(rx) @@ -1435,13 +1472,11 @@ pub async fn update_schedule( let params = tower_api::apis::default_api::UpdateScheduleParams { id_or_name: schedule_id.to_string(), update_schedule_params: tower_api::models::UpdateScheduleParams { - schema: None, - cron: cron.cloned(), - environment: None, - app_version: None, + cron: cron.map(|s| s.clone()), parameters: run_parameters, ..Default::default() }, + x_tower_request_number: None, }; unwrap_api_response(tower_api::apis::default_api::update_schedule( @@ -1556,10 +1591,50 @@ impl ResponseEntity for tower_api::apis::default_api::CancelRunSuccess { #[cfg(test)] mod tests { - use super::{unwrap_api_response_redacted, ResponseEntity}; + use super::{ + parse_warning_payload, unwrap_api_response_redacted, LogStreamError, ResponseEntity, + }; use http::StatusCode; use tower_api::apis::{Error, ResponseContent}; + #[test] + fn parses_bare_warning_payload() { + let data = r#"{"content":"Something looks off","reported_at":"2025-08-22T12:00:00Z"}"#; + let warning = parse_warning_payload(data).expect("expected warning to parse"); + + assert_eq!(warning.content, "Something looks off"); + assert_eq!(warning.reported_at, "2025-08-22T12:00:00Z"); + } + + #[test] + fn parses_enveloped_warning_payload() { + let data = r#"{"event":"warning","data":{"content":"Wrapped warning","reported_at":"2025-08-22T12:00:00Z"}}"#; + let warning = parse_warning_payload(data).expect("expected warning to parse"); + + assert_eq!(warning.content, "Wrapped warning"); + } + + #[test] + fn rejects_unparseable_warning_payload() { + assert!(parse_warning_payload("not json").is_none()); + assert!(parse_warning_payload(r#"{"unrelated":true}"#).is_none()); + } + + #[test] + fn log_stream_error_display_mentions_status_code() { + let err = LogStreamError::InvalidStatus(StatusCode::NOT_FOUND); + assert!(err.to_string().contains("404")); + + let err = LogStreamError::Unknown; + assert!(!err.to_string().is_empty()); + } + + #[test] + fn log_stream_error_boxes_as_std_error() { + let err: Box = Box::new(LogStreamError::Unknown); + assert!(!err.to_string().is_empty()); + } + enum SensitiveSuccess { UnknownValue, } diff --git a/crates/tower-cmd/src/apps.rs b/crates/tower-cmd/src/apps.rs index ad7256b2..bccc33cd 100644 --- a/crates/tower-cmd/src/apps.rs +++ b/crates/tower-cmd/src/apps.rs @@ -1,10 +1,13 @@ use clap::{value_parser, Arg, ArgMatches, Command}; use colored::Colorize; use config::Config; +use std::time::Duration; use tokio::sync::oneshot; -use tokio::time::{sleep, Duration, Instant}; +use tokio::time::{sleep, timeout, Instant}; -use tower_api::models::{Run, RunLogLine}; +use tower_api::models::run::Status as RunStatus; +use tower_api::models::Run; +use tower_telemetry::debug; use crate::{api, output, util::cmd}; @@ -42,6 +45,8 @@ pub fn apps_cmd() -> Command { .help("The environment to resolve the app against") .action(clap::ArgAction::Set), ) + .override_usage("tower apps show [OPTIONS] ") + .after_help("Example:\n tower apps show hello-world") .about("Show details for a Tower app and its recent runs"), ) .subcommand( @@ -62,9 +67,16 @@ pub fn apps_cmd() -> Command { Arg::new("follow") .short('f') .long("follow") - .help("Follow logs in real time") + .help("Follow the logs of the run in real time") .action(clap::ArgAction::SetTrue), ) + .override_usage("tower apps logs [OPTIONS] #") + .after_help( + "Examples:\n \ + tower apps logs hello-world#11 Show the stored logs of run 11\n \ + tower apps logs hello-world 11 Same, with a separate run number\n \ + tower apps logs hello-world --follow Follow the latest run in real time", + ) .about("Get the logs from a previous Tower app run"), ) .subcommand( @@ -95,6 +107,8 @@ pub fn apps_cmd() -> Command { .required(true) .help("Name of the app"), ) + .override_usage("tower apps delete [OPTIONS] ") + .after_help("Example:\n tower apps delete hello-world") .about("Delete an app in Tower"), ) .subcommand( @@ -133,10 +147,9 @@ pub async fn do_logs(out: &output::Out, config: Config, cmd: &ArgMatches) { }; (app_name_raw.clone(), num) }; - let follow = cmd.get_one::("follow").copied().unwrap_or(false); - if follow { - follow_logs(out, config, name, seq).await; + if cmd.get_flag("follow") { + follow_run_logs(out, &config, &name, seq).await; return; } @@ -147,6 +160,347 @@ pub async fn do_logs(out: &output::Out, config: Config, cmd: &ArgMatches) { } } +/// How often the run's status is polled, both while waiting for it to start +/// and while monitoring for completion during streaming. +const STATUS_POLL_INTERVAL: Duration = Duration::from_millis(500); + +/// How long to wait quietly before printing the "Waiting for run to start..." +/// notice, so fast starts stay quiet but a slow start isn't a silent hang. +const WAIT_NOTICE_AFTER: Duration = Duration::from_secs(3); + +/// How long to wait for a run to start before giving up. +const WAIT_FOR_START_TIMEOUT: Duration = Duration::from_secs(30); + +/// Grace window after the run completes for the stream to deliver any +/// remaining buffered lines. +const STREAM_DRAIN_GRACE: Duration = Duration::from_secs(5); + +/// Consecutive status-check failures tolerated before completion monitoring +/// gives up (without killing an otherwise healthy stream). +const MAX_STATUS_CHECK_FAILURES: u32 = 5; + +/// The three groups a run status can fall into for follow purposes. The +/// grouping is deliberately conservative: only known-final statuses are +/// terminal, only known pre-start statuses count as not started, and anything +/// else — including statuses introduced after this code was written — counts +/// as in progress so a follow doesn't silently end early. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RunPhase { + Terminal, + NotStarted, + InProgress, +} + +fn run_phase(status: &RunStatus) -> RunPhase { + match status { + RunStatus::Crashed | RunStatus::Errored | RunStatus::Exited | RunStatus::Cancelled => { + RunPhase::Terminal + } + RunStatus::Scheduled | RunStatus::Pending | RunStatus::Starting => RunPhase::NotStarted, + _ => RunPhase::InProgress, + } +} + +/// Tracks the highest log line number printed so far, so a line is never +/// printed twice across reconnects and the final catch-up fetch. Log line +/// numbers increase monotonically, so anything at or below the highest number +/// already printed is a repeat (or out of order) and is dropped. +struct LineTracker { + highest: Option, +} + +impl LineTracker { + fn new() -> Self { + Self { highest: None } + } + + /// Returns true when the line should be printed, updating the high-water + /// mark; false when it's a duplicate or out-of-order line. + fn accept(&mut self, line_num: i64) -> bool { + match self.highest { + Some(highest) if line_num <= highest => false, + _ => { + self.highest = Some(line_num); + true + } + } + } +} + +/// Exponential reconnect backoff: starts at 500ms, doubles per attempt, caps +/// at 5s, and resets to the initial delay after any successful connection. +struct Backoff { + current: Duration, +} + +impl Backoff { + const INITIAL: Duration = Duration::from_millis(500); + const MAX: Duration = Duration::from_secs(5); + + fn new() -> Self { + Self { + current: Self::INITIAL, + } + } + + fn next_delay(&mut self) -> Duration { + let delay = self.current; + self.current = std::cmp::min(self.current * 2, Self::MAX); + delay + } + + fn reset(&mut self) { + self.current = Self::INITIAL; + } +} + +async fn describe_run_or_die(out: &output::Out, config: &Config, name: &str, seq: i64) -> Run { + match api::describe_run(config, name, seq).await { + Ok(resp) => resp.run, + Err(err) => out.tower_error_and_die(err, "Fetching run details failed"), + } +} + +/// Prints the stored logs of a run, skipping anything already printed. +async fn print_stored_logs( + out: &output::Out, + config: &Config, + name: &str, + seq: i64, + tracker: &mut LineTracker, +) { + match api::describe_run_logs(config, name, seq).await { + Ok(resp) => { + for line in resp.log_lines { + if tracker.accept(line.line_num) { + out.remote_log_event(&line); + } + } + } + Err(err) => out.tower_error_and_die(err, "Fetching run logs failed"), + } +} + +enum WaitOutcome { + Started, + Finished, + TimedOut, +} + +/// Polls the run until it starts, finishes, or the wait times out. Prints a +/// single informational notice if the wait takes longer than a few seconds. +async fn wait_for_run_start( + out: &output::Out, + config: &Config, + name: &str, + seq: i64, +) -> WaitOutcome { + let started_waiting = Instant::now(); + let mut printed_notice = false; + + loop { + if started_waiting.elapsed() >= WAIT_FOR_START_TIMEOUT { + return WaitOutcome::TimedOut; + } + + if !printed_notice && started_waiting.elapsed() >= WAIT_NOTICE_AFTER { + out.write("Waiting for run to start...\n"); + printed_notice = true; + } + + sleep(STATUS_POLL_INTERVAL).await; + + let run = describe_run_or_die(out, config, name, seq).await; + match run_phase(&run.status) { + RunPhase::Terminal => return WaitOutcome::Finished, + RunPhase::InProgress => return WaitOutcome::Started, + RunPhase::NotStarted => {} + } + } +} + +/// Watches the run's status in the background and resolves the returned +/// channel when it reaches a terminal state. If the status check fails several +/// times in a row, monitoring is abandoned (with a diagnostic) and the channel +/// closes without resolving, so the stream itself keeps running. +fn spawn_completion_monitor( + out: output::Out, + config: Config, + name: String, + seq: i64, +) -> oneshot::Receiver<()> { + let (tx, rx) = oneshot::channel(); + + tokio::spawn(async move { + let mut failures: u32 = 0; + + loop { + match api::describe_run(&config, &name, seq).await { + Ok(resp) => { + failures = 0; + if run_phase(&resp.run.status) == RunPhase::Terminal { + let _ = tx.send(()); + return; + } + } + Err(err) => { + debug!("Failed to check run status: {:?}", err); + failures += 1; + if failures >= MAX_STATUS_CHECK_FAILURES { + out.error( + "Monitoring the run status failed repeatedly; continuing to stream logs.", + ); + return; + } + } + } + + sleep(STATUS_POLL_INTERVAL).await; + } + }); + + rx +} + +/// Waits on the completion monitor. Returns true when the run completed and +/// false when monitoring was abandoned; either way the receiver is consumed so +/// the caller stops selecting on it. +async fn wait_completion(rx: &mut Option>) -> bool { + let receiver = rx + .as_mut() + .expect("wait_completion called without a receiver"); + let completed = receiver.await.is_ok(); + *rx = None; + completed +} + +/// Prints a single stream event: log lines are deduped through the tracker, +/// warnings are rendered as `Warning: `. Shared between the live +/// streaming loop and the post-completion drain. +fn print_stream_event(out: &output::Out, event: api::LogStreamEvent, tracker: &mut LineTracker) { + match event { + api::LogStreamEvent::EventLog(log) => { + if tracker.accept(log.line_num) { + out.remote_log_event(&log); + } + } + api::LogStreamEvent::EventWarning(warning) => { + out.write(&format!("Warning: {}\n", warning.content)); + } + } +} + +/// Drains any remaining buffered lines and warnings from the stream for a +/// short grace window after the run completes. +async fn drain_stream_with_grace( + out: &output::Out, + mut events: tokio::sync::mpsc::Receiver, + tracker: &mut LineTracker, +) { + let _ = timeout(STREAM_DRAIN_GRACE, async { + while let Some(event) = events.recv().await { + print_stream_event(out, event, tracker); + } + }) + .await; +} + +/// Follows the logs of a run: prints stored logs for a finished run, waits for +/// a not-yet-started run, and otherwise attaches to the live log stream with +/// reconnects, dedup, and independent completion detection. +async fn follow_run_logs(out: &output::Out, config: &Config, name: &str, seq: i64) { + let mut tracker = LineTracker::new(); + + let run = describe_run_or_die(out, config, name, seq).await; + + match run_phase(&run.status) { + RunPhase::Terminal => { + print_stored_logs(out, config, name, seq, &mut tracker).await; + return; + } + RunPhase::NotStarted => match wait_for_run_start(out, config, name, seq).await { + WaitOutcome::Started => {} + WaitOutcome::Finished => { + print_stored_logs(out, config, name, seq, &mut tracker).await; + return; + } + WaitOutcome::TimedOut => { + out.die("Timed out waiting for the run to start. The runner may be unavailable."); + } + }, + RunPhase::InProgress => {} + } + + stream_logs_with_reconnect(out, config, name, seq, &run.dollar_link, &mut tracker).await; +} + +async fn stream_logs_with_reconnect( + out: &output::Out, + config: &Config, + name: &str, + seq: i64, + run_link: &str, + tracker: &mut LineTracker, +) { + let enable_ctrl_c = out.foreground(); + let mut backoff = Backoff::new(); + let mut run_complete: Option> = Some(spawn_completion_monitor( + out.clone(), + config.clone(), + name.to_string(), + seq, + )); + + loop { + match api::stream_run_logs(config, name, seq).await { + Ok(mut events) => { + backoff.reset(); + + loop { + tokio::select! { + event = events.recv() => match event { + Some(event) => print_stream_event(out, event, tracker), + // Stream closed; fall through to the disconnect path. + None => break, + }, + completed = wait_completion(&mut run_complete), if run_complete.is_some() => { + if completed { + drain_stream_with_grace(out, events, tracker).await; + print_stored_logs(out, config, name, seq, tracker).await; + return; + } + // Monitoring was abandoned; keep streaming and rely + // on the disconnect path to notice completion. + } + _ = tokio::signal::ctrl_c(), if enable_ctrl_c => { + out.write("Received Ctrl+C, stopping log streaming...\n"); + out.write("Note: The run will continue in Tower cloud\n"); + out.write(&format!(" See more: {}\n", run_link)); + return; + } + } + } + } + Err(err) => { + out.error(&format!("Failed to stream run logs: {}", err)); + if err.is_fatal() { + std::process::exit(1); + } + } + } + + // Disconnected (or a transient open failure): re-check the run status, + // stop if the run is done, otherwise retry with backoff. + let run = describe_run_or_die(out, config, name, seq).await; + if run_phase(&run.status) == RunPhase::Terminal { + print_stored_logs(out, config, name, seq, tracker).await; + return; + } + + sleep(backoff.next_delay()).await; + } +} + pub async fn do_show(out: &output::Out, config: Config, cmd: &ArgMatches) { let name = cmd .get_one::("app_name") @@ -301,333 +655,43 @@ async fn latest_run_number(out: &output::Out, config: &Config, name: &str) -> i6 } } -const FOLLOW_BACKOFF_INITIAL: Duration = Duration::from_millis(500); -const FOLLOW_BACKOFF_MAX: Duration = Duration::from_secs(5); -const LOG_DRAIN_DURATION: Duration = Duration::from_secs(5); -const RUN_START_POLL_INTERVAL: Duration = Duration::from_millis(500); -const RUN_START_MESSAGE_DELAY: Duration = Duration::from_secs(3); -const RUN_START_TIMEOUT: Duration = Duration::from_secs(30); - -async fn follow_logs(out: &output::Out, config: Config, name: String, seq: i64) { - let mut backoff = FOLLOW_BACKOFF_INITIAL; - let mut cancel_monitor: Option> = None; - let mut last_line_num: Option = None; - - loop { - let mut run = match api::describe_run(&config, &name, seq).await { - Ok(res) => res.run, - Err(err) => out.tower_error_and_die(err, "Fetching run details failed"), - }; - - if is_run_finished(&run) { - if let Ok(resp) = api::describe_run_logs(&config, &name, seq).await { - for line in resp.log_lines { - emit_log_if_new(out, &line, &mut last_line_num); - } - } - return; - } - - if !is_run_started(&run) { - let wait_started = Instant::now(); - let mut notified = false; - loop { - sleep(RUN_START_POLL_INTERVAL).await; - - if wait_started.elapsed() > RUN_START_TIMEOUT { - out.error("Timed out waiting for run to start. The runner may be unavailable."); - return; - } - - // Avoid blank output on slow starts while keeping fast starts quiet. - if should_notify_run_wait(notified, wait_started.elapsed()) { - out.write("Waiting for run to start...\n"); - notified = true; - } - run = match api::describe_run(&config, &name, seq).await { - Ok(res) => res.run, - Err(err) => out.tower_error_and_die(err, "Fetching run details failed"), - }; - if is_run_finished(&run) { - if let Ok(resp) = api::describe_run_logs(&config, &name, seq).await { - for line in resp.log_lines { - emit_log_if_new(out, &line, &mut last_line_num); - } - } - return; - } - if is_run_started(&run) { - break; - } - } - } - - // Cancel any prior watcher so we don't accumulate pollers after reconnects. - if let Some(cancel) = cancel_monitor.take() { - let _ = cancel.send(()); - } - let (cancel_tx, cancel_rx) = oneshot::channel(); - cancel_monitor = Some(cancel_tx); - let run_complete = monitor_run_completion(&config, &name, seq, cancel_rx); - match api::stream_run_logs(&config, &name, seq).await { - Ok(log_stream) => { - // Reset after a successful connection so transient drops recover quickly. - backoff = FOLLOW_BACKOFF_INITIAL; - match stream_logs_until_complete( - out, - log_stream, - run_complete, - out.foreground(), - &run.dollar_link, - &mut last_line_num, - ) - .await - { - Ok(LogFollowOutcome::Completed) => { - if let Some(cancel) = cancel_monitor.take() { - let _ = cancel.send(()); - } - return; - } - Ok(LogFollowOutcome::Interrupted) => { - if let Some(cancel) = cancel_monitor.take() { - let _ = cancel.send(()); - } - return; - } - Ok(LogFollowOutcome::Disconnected) => {} - Err(_) => { - if let Some(cancel) = cancel_monitor.take() { - let _ = cancel.send(()); - } - return; - } - } - } - Err(err) => { - if is_fatal_stream_error(&err) { - out.error(&format!("Failed to stream run logs: {}", err)); - return; - } - out.error(&format!("Failed to stream run logs: {}", err)); - sleep(backoff).await; - backoff = next_backoff(backoff); - continue; - } - } - - let latest = match api::describe_run(&config, &name, seq).await { - Ok(res) => res.run, - Err(err) => out.tower_error_and_die(err, "Fetching run details failed"), - }; - if is_run_finished(&latest) { - return; - } - - sleep(backoff).await; - backoff = next_backoff(backoff); - } -} - -fn next_backoff(current: Duration) -> Duration { - let next = current.checked_mul(2).unwrap_or(FOLLOW_BACKOFF_MAX); - if next > FOLLOW_BACKOFF_MAX { - FOLLOW_BACKOFF_MAX - } else { - next - } -} - -enum LogFollowOutcome { - Completed, - Disconnected, - Interrupted, -} - -async fn stream_logs_until_complete( - out: &output::Out, - mut log_stream: tokio::sync::mpsc::Receiver, - mut run_complete: oneshot::Receiver, - enable_ctrl_c: bool, - run_link: &str, - last_line_num: &mut Option, -) -> Result { - loop { - tokio::select! { - event = log_stream.recv() => match event { - Some(api::LogStreamEvent::EventLog(log)) => { - emit_log_if_new(out, &log, last_line_num); - }, - Some(api::LogStreamEvent::EventWarning(warning)) => { - out.write(&format!("Warning: {}\n", warning.data.content)); - } - None => return Ok(LogFollowOutcome::Disconnected), - }, - res = &mut run_complete => { - match res { - Ok(_) => { - drain_remaining_logs(out, log_stream, last_line_num).await; - return Ok(LogFollowOutcome::Completed); - } - // If monitoring failed, keep following and let the caller retry. - Err(_) => return Ok(LogFollowOutcome::Disconnected), - } - }, - _ = tokio::signal::ctrl_c(), if enable_ctrl_c => { - out.write("Received Ctrl+C, stopping log streaming...\n"); - out.write("Note: The run will continue in Tower cloud\n"); - out.write(&format!(" See more: {}\n", run_link)); - return Ok(LogFollowOutcome::Interrupted); - }, - } - } -} - -async fn drain_remaining_logs( - out: &output::Out, - mut log_stream: tokio::sync::mpsc::Receiver, - last_line_num: &mut Option, -) { - let _ = tokio::time::timeout(LOG_DRAIN_DURATION, async { - while let Some(event) = log_stream.recv().await { - match event { - api::LogStreamEvent::EventLog(log) => { - emit_log_if_new(out, &log, last_line_num); - } - api::LogStreamEvent::EventWarning(warning) => { - out.write(&format!("Warning: {}\n", warning.data.content)); - } - } - } - }) - .await; -} - -fn emit_log_if_new(out: &output::Out, log: &RunLogLine, last_line_num: &mut Option) { - if should_emit_line(last_line_num, log.line_num) { - out.remote_log_event(log); - } -} - -fn should_emit_line(last_line_num: &mut Option, line_num: i64) -> bool { - if last_line_num.map_or(true, |last| line_num > last) { - *last_line_num = Some(line_num); - true - } else { - false - } -} - -fn is_fatal_stream_error(err: &api::LogStreamError) -> bool { - match err { - api::LogStreamError::Reqwest(reqwest_err) => reqwest_err - .status() - .map(|status| status.is_client_error() && status.as_u16() != 429) - .unwrap_or(false), - api::LogStreamError::Unknown => false, - } -} - -fn monitor_run_completion( - config: &Config, - app_name: &str, - seq: i64, - mut cancel: oneshot::Receiver<()>, -) -> oneshot::Receiver { - let (tx, rx) = oneshot::channel(); - let config_clone = config.clone(); - let app_name = app_name.to_string(); - - tokio::spawn(async move { - let mut failures = 0; - loop { - tokio::select! { - _ = &mut cancel => return, - result = api::describe_run(&config_clone, &app_name, seq) => match result { - Ok(res) => { - failures = 0; - if is_run_finished(&res.run) { - let _ = tx.send(res.run); - return; - } - } - Err(_) => { - failures += 1; - if failures >= 5 { - output::background_error( - "Failed to monitor run completion after repeated errors", - ); - return; - } - } - }, - } - sleep(Duration::from_millis(500)).await; - } - }); - - rx -} - -fn is_run_finished(run: &Run) -> bool { - match run.status { - // Be explicit about terminal states so new non-terminal statuses - // don't cause us to stop following logs too early. - tower_api::models::run::Status::Crashed - | tower_api::models::run::Status::Errored - | tower_api::models::run::Status::Exited - | tower_api::models::run::Status::Cancelled => true, - _ => false, - } -} - -fn is_run_started(run: &Run) -> bool { - match run.status { - tower_api::models::run::Status::Scheduled - | tower_api::models::run::Status::Pending - | tower_api::models::run::Status::Starting => false, - _ => true, - } -} - -fn should_notify_run_wait(already_notified: bool, elapsed: Duration) -> bool { - !already_notified && elapsed >= RUN_START_MESSAGE_DELAY -} - #[cfg(test)] mod tests { - use super::is_run_finished; - use super::{ - apps_cmd, is_run_started, next_backoff, should_emit_line, should_notify_run_wait, - stream_logs_until_complete, LogFollowOutcome, FOLLOW_BACKOFF_INITIAL, FOLLOW_BACKOFF_MAX, - }; - use tokio::sync::{mpsc, oneshot}; - use tokio::time::Duration; - use tower_api::models::run::Status; - use tower_api::models::Run; + use super::{apps_cmd, run_phase, Backoff, LineTracker, RunPhase}; + use std::time::Duration; + use tower_api::models::run::Status as RunStatus; #[test] - fn test_follow_flag_parsing() { + fn follow_flag_with_hash_form() { let matches = apps_cmd() - .try_get_matches_from(["apps", "logs", "--follow", "hello-world#11"]) + .try_get_matches_from(["apps", "logs", "hello-world#11", "--follow"]) .unwrap(); - let (cmd, sub_matches) = matches.subcommand().unwrap(); + let (_, sub_matches) = matches.subcommand().unwrap(); - assert_eq!(cmd, "logs"); - assert_eq!(sub_matches.get_one::("follow"), Some(&true)); assert_eq!( sub_matches .get_one::("app_name") .map(|s| s.as_str()), Some("hello-world#11") ); - assert_eq!(sub_matches.get_one::("run_number"), None); + assert!(sub_matches.get_flag("follow")); } #[test] - fn test_separate_run_number_parsing() { + fn follow_flag_with_separate_run_number() { let matches = apps_cmd() - .try_get_matches_from(["apps", "logs", "hello-world", "11"]) + .try_get_matches_from(["apps", "logs", "hello-world", "11", "--follow"]) + .unwrap(); + let (_, sub_matches) = matches.subcommand().unwrap(); + + assert_eq!(sub_matches.get_one::("run_number"), Some(&11)); + assert!(sub_matches.get_flag("follow")); + } + + #[test] + fn follow_flag_short_form_with_app_only() { + let matches = apps_cmd() + .try_get_matches_from(["apps", "logs", "hello-world", "-f"]) .unwrap(); let (_, sub_matches) = matches.subcommand().unwrap(); @@ -637,175 +701,109 @@ mod tests { .map(|s| s.as_str()), Some("hello-world") ); - assert_eq!(sub_matches.get_one::("run_number"), Some(&11)); + assert_eq!(sub_matches.get_one::("run_number"), None); + assert!(sub_matches.get_flag("follow")); } #[test] - fn test_terminal_statuses_explicit() { - let non_terminal = [ - Status::Scheduled, - Status::Pending, - Status::Running, - Status::Retrying, - ]; - for status in non_terminal { - let run = Run { - status, - ..Default::default() - }; - assert!(!is_run_finished(&run)); - } + fn follow_flag_defaults_to_false() { + let matches = apps_cmd() + .try_get_matches_from(["apps", "logs", "hello-world"]) + .unwrap(); + let (_, sub_matches) = matches.subcommand().unwrap(); - let terminal = [ - Status::Crashed, - Status::Errored, - Status::Exited, - Status::Cancelled, - ]; - for status in terminal { - let run = Run { - status, - ..Default::default() - }; - assert!(is_run_finished(&run)); - } + assert!(!sub_matches.get_flag("follow")); } #[test] - fn test_status_variants_exhaustive() { - let status = Status::Scheduled; - match status { - Status::Scheduled => {} - Status::Starting => {} - Status::Pending => {} - Status::Running => {} - Status::Retrying => {} - Status::Crashed => {} - Status::Errored => {} - Status::Exited => {} - Status::Cancelled => {} + fn terminal_statuses_group_as_terminal() { + for status in [ + RunStatus::Crashed, + RunStatus::Errored, + RunStatus::Exited, + RunStatus::Cancelled, + ] { + assert_eq!(run_phase(&status), RunPhase::Terminal); } } #[test] - fn test_run_started_statuses() { - let not_started = [Status::Scheduled, Status::Pending, Status::Starting]; - for status in not_started { - let run = Run { - status, - ..Default::default() - }; - assert!(!is_run_started(&run)); + fn pre_start_statuses_group_as_not_started() { + for status in [ + RunStatus::Scheduled, + RunStatus::Pending, + RunStatus::Starting, + ] { + assert_eq!(run_phase(&status), RunPhase::NotStarted); } + } - let started = [ - Status::Running, - Status::Retrying, - Status::Crashed, - Status::Errored, - Status::Exited, - Status::Cancelled, - ]; - for status in started { - let run = Run { - status, - ..Default::default() - }; - assert!(is_run_started(&run)); - } + #[test] + fn other_statuses_group_as_in_progress() { + // Running and Retrying aren't in either explicit list, so they (like + // any future status) count as in progress. + assert_eq!(run_phase(&RunStatus::Running), RunPhase::InProgress); + assert_eq!(run_phase(&RunStatus::Retrying), RunPhase::InProgress); } #[test] - fn test_run_wait_notification_logic() { - assert!(!should_notify_run_wait( - true, - super::RUN_START_MESSAGE_DELAY - )); - assert!(!should_notify_run_wait( - false, - super::RUN_START_MESSAGE_DELAY - Duration::from_millis(1) - )); - assert!(should_notify_run_wait( - false, - super::RUN_START_MESSAGE_DELAY - )); - } - - #[tokio::test] - async fn test_stream_completion_on_run_finish() { - let (tx, rx) = mpsc::channel(1); - let (done_tx, done_rx) = oneshot::channel(); - let mut last_line_num = None; - - let done_task = tokio::spawn(async move { - let _ = done_tx.send(Run::default()); - tokio::time::sleep(Duration::from_millis(10)).await; - drop(tx); - }); - - let out = crate::output::Out::sink(); - let res = - stream_logs_until_complete(&out, rx, done_rx, false, "link", &mut last_line_num).await; - done_task.await.unwrap(); - - assert!(matches!(res, Ok(LogFollowOutcome::Completed))); - } - - #[tokio::test] - async fn test_stream_disconnection_on_close() { - let (tx, rx) = mpsc::channel(1); - drop(tx); - let (_done_tx, done_rx) = oneshot::channel::(); - let mut last_line_num = None; - - let out = crate::output::Out::sink(); - let res = - stream_logs_until_complete(&out, rx, done_rx, false, "link", &mut last_line_num).await; - - assert!(matches!(res, Ok(LogFollowOutcome::Disconnected))); + fn backoff_starts_small_doubles_and_caps() { + let mut backoff = Backoff::new(); + + assert_eq!(backoff.next_delay(), Duration::from_millis(500)); + assert_eq!(backoff.next_delay(), Duration::from_secs(1)); + assert_eq!(backoff.next_delay(), Duration::from_secs(2)); + assert_eq!(backoff.next_delay(), Duration::from_secs(4)); + assert_eq!(backoff.next_delay(), Duration::from_secs(5)); + assert_eq!(backoff.next_delay(), Duration::from_secs(5)); } #[test] - fn test_backoff_growth_and_cap() { - let mut backoff = FOLLOW_BACKOFF_INITIAL; - backoff = next_backoff(backoff); - assert_eq!(backoff, Duration::from_secs(1)); - backoff = next_backoff(backoff); - assert_eq!(backoff, Duration::from_secs(2)); - backoff = next_backoff(backoff); - assert_eq!(backoff, Duration::from_secs(4)); - backoff = next_backoff(backoff); - assert_eq!(backoff, FOLLOW_BACKOFF_MAX); - backoff = next_backoff(backoff); - assert_eq!(backoff, FOLLOW_BACKOFF_MAX); + fn backoff_resets_to_initial_delay() { + let mut backoff = Backoff::new(); + backoff.next_delay(); + backoff.next_delay(); + backoff.next_delay(); + + backoff.reset(); + assert_eq!(backoff.next_delay(), Duration::from_millis(500)); } #[test] - fn test_duplicate_line_filtering() { - let mut last_line_num = None; - assert!(should_emit_line(&mut last_line_num, 1)); - assert_eq!(last_line_num, Some(1)); - assert!(!should_emit_line(&mut last_line_num, 1)); - assert_eq!(last_line_num, Some(1)); - assert!(!should_emit_line(&mut last_line_num, 0)); - assert_eq!(last_line_num, Some(1)); - assert!(should_emit_line(&mut last_line_num, 2)); - assert_eq!(last_line_num, Some(2)); - assert!(should_emit_line(&mut last_line_num, 10)); - assert_eq!(last_line_num, Some(10)); + fn line_tracker_accepts_monotonically_increasing_lines() { + let mut tracker = LineTracker::new(); + + assert!(tracker.accept(1)); + assert!(tracker.accept(2)); + assert!(tracker.accept(5)); } #[test] - fn test_out_of_order_log_handling() { - let mut last_line_num = None; - assert!(should_emit_line(&mut last_line_num, 1)); - assert_eq!(last_line_num, Some(1)); - assert!(should_emit_line(&mut last_line_num, 3)); - assert_eq!(last_line_num, Some(3)); - assert!(!should_emit_line(&mut last_line_num, 2)); - assert_eq!(last_line_num, Some(3)); - assert!(should_emit_line(&mut last_line_num, 4)); - assert_eq!(last_line_num, Some(4)); + fn line_tracker_drops_repeats_and_out_of_order_lines() { + let mut tracker = LineTracker::new(); + + assert!(tracker.accept(3)); + assert!(!tracker.accept(3)); + assert!(!tracker.accept(2)); + assert!(!tracker.accept(1)); + assert!(tracker.accept(4)); + assert!(!tracker.accept(4)); + } + + #[test] + fn test_separate_run_number_parsing() { + let matches = apps_cmd() + .try_get_matches_from(["apps", "logs", "hello-world", "11"]) + .unwrap(); + let (_, sub_matches) = matches.subcommand().unwrap(); + + assert_eq!( + sub_matches + .get_one::("app_name") + .map(|s| s.as_str()), + Some("hello-world") + ); + assert_eq!(sub_matches.get_one::("run_number"), Some(&11)); } #[test] diff --git a/crates/tower-cmd/src/deploy.rs b/crates/tower-cmd/src/deploy.rs index 74c864d9..a0117341 100644 --- a/crates/tower-cmd/src/deploy.rs +++ b/crates/tower-cmd/src/deploy.rs @@ -119,12 +119,12 @@ pub async fn do_deploy(out: &output::Out, config: Config, args: &ArgMatches) { crate::Error::ApiDeployError { source } => { out.tower_error_and_die(source, "Deploying app failed") } - crate::Error::ApiCreateAppError { source } => { - out.tower_error_and_die(source, "Creating app failed") - } crate::Error::ApiDescribeAppError { source } => { out.tower_error_and_die(source, "Fetching app details failed") } + crate::Error::ApiCreateAppError { source } => { + out.tower_error_and_die(source, "Creating app failed") + } crate::Error::PackageError { source } => { out.package_error(source); std::process::exit(1); @@ -158,15 +158,27 @@ pub async fn deploy_from_dir( })?; let api_config = config.into(); - // Add app existence check before proceeding - util::apps::ensure_app_exists( + // Add app existence check before proceeding. When the app is created here, + // the Towerfile description (if present) is applied at creation time only; + // an already-existing app's description is never touched by deploy. + if let Err(err) = util::apps::ensure_app_exists( out, &api_config, &towerfile.app.name, towerfile.app.description.as_deref(), create_app, ) - .await?; + .await + { + return Err(match err { + util::apps::EnsureAppError::Describe(source) => { + crate::Error::ApiDescribeAppError { source } + } + util::apps::EnsureAppError::Create(source) => { + crate::Error::ApiCreateAppError { source } + } + }); + } let spec = PackageSpec::from_towerfile(&towerfile); let mut spinner = out.spinner("Building package..."); diff --git a/crates/tower-cmd/src/error.rs b/crates/tower-cmd/src/error.rs index 94dfaff7..55461df5 100644 --- a/crates/tower-cmd/src/error.rs +++ b/crates/tower-cmd/src/error.rs @@ -97,18 +97,18 @@ pub enum Error { source: tower_api::apis::Error, }, - // API create app error - #[snafu(display("API create app error: {}", source))] - ApiCreateAppError { - source: tower_api::apis::Error, - }, - // API describe app error #[snafu(display("API describe app error: {}", source))] ApiDescribeAppError { source: tower_api::apis::Error, }, + // API create app error + #[snafu(display("API create app error: {}", source))] + ApiCreateAppError { + source: tower_api::apis::Error, + }, + // Channel error #[snafu(display("Channel receive error"))] ChannelReceiveError, @@ -176,12 +176,6 @@ impl From> for Error { } } -impl From> for Error { - fn from(source: tower_api::apis::Error) -> Self { - Self::ApiCreateAppError { source } - } -} - impl From> for Error { fn from(source: tower_api::apis::Error) -> Self { Self::ApiDescribeAppError { source } diff --git a/crates/tower-cmd/src/schedules.rs b/crates/tower-cmd/src/schedules.rs index 493d39e8..d795ac84 100644 --- a/crates/tower-cmd/src/schedules.rs +++ b/crates/tower-cmd/src/schedules.rs @@ -76,19 +76,22 @@ pub fn schedules_cmd() -> Command { .value_parser(value_parser!(String)) .index(1) .required(true) - .help("The schedule ID to delete"), + .help("The ID of the schedule to delete"), + ) + .override_usage("tower schedules delete [OPTIONS] ") + .after_help( + "Example:\n tower schedules delete 01890a5d-ac96-774b-bcce-b302099a8057", ) - .after_help("Example: tower schedules delete 123") .about("Delete a schedule"), ) .subcommand( Command::new("update") .arg( - Arg::new("id_or_name") + Arg::new("schedule_id") .value_parser(value_parser!(String)) .index(1) .required(true) - .help("ID or name of the schedule to update"), + .help("The ID of the schedule to update"), ) .arg( Arg::new("cron") @@ -105,7 +108,10 @@ pub fn schedules_cmd() -> Command { .help("Parameters (key=value) to pass to the app") .action(clap::ArgAction::Append), ) - .after_help("Example: tower schedules update 123 --cron \"*/15 * * * *\"") + .override_usage("tower schedules update [OPTIONS] ") + .after_help( + "Example:\n tower schedules update 01890a5d-ac96-774b-bcce-b302099a8057 --cron '0 9 * * *'", + ) .about("Update an existing schedule"), ) } @@ -172,19 +178,19 @@ pub async fn do_create(out: &crate::output::Out, config: Config, args: &ArgMatch } pub async fn do_update(out: &crate::output::Out, config: Config, args: &ArgMatches) { - let id_or_name = args - .get_one::("id_or_name") - .expect("id_or_name is required"); + let schedule_id = args + .get_one::("schedule_id") + .expect("schedule_id is required"); let cron = args.get_one::("cron"); let parameters = parse_parameters(out, args); out.with_spinner( "Updating schedule", - api::update_schedule(&config, id_or_name, cron, parameters), + api::update_schedule(&config, schedule_id, cron, parameters), ) .await; - out.success(&format!("Schedule {} updated", id_or_name)); + out.success(&format!("Schedule {} updated", schedule_id)); } pub async fn do_delete(out: &crate::output::Out, config: Config, args: &ArgMatches) { @@ -202,209 +208,174 @@ pub async fn do_delete(out: &crate::output::Out, config: Config, args: &ArgMatch } /// Parses `--parameter` arguments into a HashMap of key-value pairs. -/// Handles format like "--parameter key=value" +/// Handles format like "--parameter key=value". Malformed entries (no `=`, or +/// an empty key) are reported and dropped. When no valid entries remain, this +/// returns `None` so the request is sent as if `--parameter` was never given. fn parse_parameters( out: &crate::output::Out, args: &ArgMatches, ) -> Option> { + let parameters = args.get_many::("parameters")?; let mut param_map = HashMap::new(); - if let Some(parameters) = args.get_many::("parameters") { - for param in parameters { - match param.split_once('=') { - Some((key, value)) => { - if key.is_empty() { - out.error(&format!( - "Invalid parameter format: '{}'. Key cannot be empty.", - param - )); - continue; - } - param_map.insert(key.to_string(), value.to_string()); - } - None => { + for param in parameters { + match param.split_once('=') { + Some((key, value)) => { + if key.is_empty() { out.error(&format!( - "Invalid parameter format: '{}'. Expected 'key=value'.", + "Invalid parameter format: '{}'. Key cannot be empty.", param )); + continue; } + param_map.insert(key.to_string(), value.to_string()); + } + None => { + out.error(&format!( + "Invalid parameter format: '{}'. Expected 'key=value'.", + param + )); } } + } - if param_map.is_empty() { - None - } else { - Some(param_map) - } - } else { + if param_map.is_empty() { None + } else { + Some(param_map) } } #[cfg(test)] mod tests { use super::{parse_parameters, schedules_cmd}; - use crate::output::Out; + use crate::output; + + fn parse(args: &[&str]) -> Result { + let mut full = vec!["schedules"]; + full.extend_from_slice(args); + schedules_cmd().try_get_matches_from(full) + } + + fn sub<'a>(matches: &'a clap::ArgMatches) -> (&'a str, &'a clap::ArgMatches) { + matches.subcommand().unwrap() + } #[test] - fn update_accepts_positional_schedule_id_and_flags() { - let matches = schedules_cmd() - .try_get_matches_from([ - "schedules", - "update", - "sch_123", - "--cron", - "*/10 * * * *", - "--parameter", - "env=prod", - "-p", - "team=platform", - ]) - .expect("update args should parse"); - - let ("update", update_args) = matches.subcommand().expect("expected update subcommand") - else { - panic!("expected update subcommand"); - }; + fn delete_parses_positional_schedule_id() { + let matches = parse(&["delete", "sched-123"]).unwrap(); + let (name, args) = sub(&matches); + assert_eq!(name, "delete"); assert_eq!( - update_args - .get_one::("id_or_name") - .map(String::as_str), - Some("sch_123") - ); - assert_eq!( - update_args.get_one::("cron").map(String::as_str), - Some("*/10 * * * *") + args.get_one::("schedule_id").map(|s| s.as_str()), + Some("sched-123") ); + } - let params: Vec<&str> = update_args - .get_many::("parameters") - .expect("expected parameters") - .map(String::as_str) - .collect(); - assert_eq!(params, vec!["env=prod", "team=platform"]); + #[test] + fn delete_without_schedule_id_is_a_parse_error() { + assert!(parse(&["delete"]).is_err()); } #[test] - fn update_accepts_equals_sign_flag_forms() { - let matches = schedules_cmd() - .try_get_matches_from([ - "schedules", - "update", - "sch_456", - "--cron=*/5 * * * *", - "--parameter=region=us-east-1", - ]) - .expect("equals-form args should parse"); - - let ("update", update_args) = matches.subcommand().expect("expected update subcommand") - else { - panic!("expected update subcommand"); - }; + fn update_parses_positional_id_with_space_separated_flags() { + let matches = parse(&["update", "sched-123", "--cron", "0 9 * * *"]).unwrap(); + let (name, args) = sub(&matches); + assert_eq!(name, "update"); assert_eq!( - update_args - .get_one::("id_or_name") - .map(String::as_str), - Some("sch_456") + args.get_one::("schedule_id").map(|s| s.as_str()), + Some("sched-123") ); assert_eq!( - update_args.get_one::("cron").map(String::as_str), - Some("*/5 * * * *") + args.get_one::("cron").map(|s| s.as_str()), + Some("0 9 * * *") ); + } + + #[test] + fn update_parses_equals_flag_forms() { + let matches = parse(&[ + "update", + "sched-123", + "--cron=0 9 * * *", + "--parameter=key=value", + ]) + .unwrap(); + let (_, args) = sub(&matches); + assert_eq!( - update_args - .get_many::("parameters") - .expect("expected parameter") - .next() - .map(String::as_str), - Some("region=us-east-1") + args.get_one::("cron").map(|s| s.as_str()), + Some("0 9 * * *") ); + let params: Vec<&String> = args.get_many::("parameters").unwrap().collect(); + assert_eq!(params, vec!["key=value"]); } #[test] - fn update_requires_schedule_id() { - let result = - schedules_cmd().try_get_matches_from(["schedules", "update", "--cron", "*/15 * * * *"]); - assert!(result.is_err()); + fn update_accepts_repeated_parameters() { + let matches = parse(&["update", "sched-123", "-p", "a=1", "-p", "b=2"]).unwrap(); + let (_, args) = sub(&matches); + + let params: Vec<&String> = args.get_many::("parameters").unwrap().collect(); + assert_eq!(params, vec!["a=1", "b=2"]); } #[test] - fn delete_requires_schedule_id() { - let result = schedules_cmd().try_get_matches_from(["schedules", "delete"]); - assert!(result.is_err()); + fn update_without_schedule_id_is_a_parse_error() { + assert!(parse(&["update"]).is_err()); + assert!(parse(&["update", "--cron", "0 9 * * *"]).is_err()); } #[test] - fn parse_parameters_valid_pairs() { - let matches = schedules_cmd() - .try_get_matches_from([ - "schedules", - "update", - "sch_789", - "--parameter", - "env=prod", - "-p", - "team=platform", - ]) - .expect("update args should parse"); - - let ("update", update_args) = matches.subcommand().expect("expected update subcommand") - else { - panic!("expected update subcommand"); - }; - - let params = - parse_parameters(&Out::sink(), update_args).expect("expected parsed parameters"); - assert_eq!(params.get("env"), Some(&"prod".to_string())); - assert_eq!(params.get("team"), Some(&"platform".to_string())); + fn parse_parameters_keeps_valid_pairs() { + let out = output::Out::sink(); + let matches = parse(&["update", "sched-123", "-p", "a=1", "-p", "b=two"]).unwrap(); + let (_, args) = sub(&matches); + + let params = parse_parameters(&out, args).expect("expected parameters"); + assert_eq!(params.get("a").map(|s| s.as_str()), Some("1")); + assert_eq!(params.get("b").map(|s| s.as_str()), Some("two")); } #[test] - fn parse_parameters_invalid_entries_return_none() { - let matches = schedules_cmd() - .try_get_matches_from([ - "schedules", - "update", - "sch_789", - "--parameter", - "invalid", - "-p", - "=missing-key", - ]) - .expect("update args should parse"); - - let ("update", update_args) = matches.subcommand().expect("expected update subcommand") - else { - panic!("expected update subcommand"); - }; - - assert_eq!(parse_parameters(&Out::sink(), update_args), None); + fn parse_parameters_drops_malformed_entries() { + let out = output::Out::sink(); + let matches = parse(&[ + "update", + "sched-123", + "-p", + "good=1", + "-p", + "no-equals", + "-p", + "=empty-key", + ]) + .unwrap(); + let (_, args) = sub(&matches); + + let params = parse_parameters(&out, args).expect("expected parameters"); + assert_eq!(params.len(), 1); + assert_eq!(params.get("good").map(|s| s.as_str()), Some("1")); } #[test] - fn parse_parameters_mixed_valid_and_invalid_keeps_valid() { - let matches = schedules_cmd() - .try_get_matches_from([ - "schedules", - "update", - "sch_789", - "--parameter", - "env=prod", - "-p", - "invalid", - ]) - .expect("update args should parse"); - - let ("update", update_args) = matches.subcommand().expect("expected update subcommand") - else { - panic!("expected update subcommand"); - }; - - let params = - parse_parameters(&Out::sink(), update_args).expect("expected parsed parameters"); - assert_eq!(params.get("env"), Some(&"prod".to_string())); - assert_eq!(params.len(), 1); + fn parse_parameters_yields_none_when_nothing_valid_remains() { + let out = output::Out::sink(); + let matches = + parse(&["update", "sched-123", "-p", "no-equals", "-p", "=empty-key"]).unwrap(); + let (_, args) = sub(&matches); + + assert!(parse_parameters(&out, args).is_none()); + } + + #[test] + fn parse_parameters_yields_none_when_flag_absent() { + let out = output::Out::sink(); + let matches = parse(&["update", "sched-123"]).unwrap(); + let (_, args) = sub(&matches); + + assert!(parse_parameters(&out, args).is_none()); } } diff --git a/crates/tower-cmd/src/secrets.rs b/crates/tower-cmd/src/secrets.rs index 25acafbb..446268a2 100644 --- a/crates/tower-cmd/src/secrets.rs +++ b/crates/tower-cmd/src/secrets.rs @@ -19,7 +19,7 @@ pub fn secrets_cmd() -> Command { Arg::new("show") .short('s') .long("show") - .help("Show secrets in plain text") + .help("Show the secret values in plain text") .action(clap::ArgAction::SetTrue), ) .arg( @@ -28,7 +28,7 @@ pub fn secrets_cmd() -> Command { .long("environment") .default_value("default") .value_parser(value_parser!(String)) - .help("List secrets in this environment") + .help("The environment to list secrets from") .action(clap::ArgAction::Set), ) .arg( @@ -48,7 +48,7 @@ pub fn secrets_cmd() -> Command { .long("name") .value_parser(value_parser!(String)) .required(true) - .help("Secret name to create") + .help("The name of the secret to create") .action(clap::ArgAction::Set), ) .arg( @@ -57,7 +57,7 @@ pub fn secrets_cmd() -> Command { .long("environment") .default_value("default") .value_parser(value_parser!(String)) - .help("Environment to store the secret in") + .help("The environment to create the secret in") .action(clap::ArgAction::Set), ) .arg( @@ -66,7 +66,7 @@ pub fn secrets_cmd() -> Command { .long("value") .value_parser(value_parser!(String)) .required(true) - .help("Secret value to store") + .help("The value of the secret") .action(clap::ArgAction::Set), ) .about("Create a new secret in your Tower account"), @@ -89,6 +89,8 @@ pub fn secrets_cmd() -> Command { .help("environment to delete the secret from") .action(clap::ArgAction::Set), ) + .override_usage("tower secrets delete [OPTIONS] ") + .after_help("Example:\n tower secrets delete MY_API_KEY") .about("Delete a secret in Tower"), ) } diff --git a/crates/tower-cmd/src/util/apps.rs b/crates/tower-cmd/src/util/apps.rs index bedcec17..7c5c7393 100644 --- a/crates/tower-cmd/src/util/apps.rs +++ b/crates/tower-cmd/src/util/apps.rs @@ -6,13 +6,20 @@ use tower_api::apis::{ }; use tower_api::models::CreateAppParams as CreateAppParamsModel; +/// Distinguishes the two ways `ensure_app_exists` can fail: checking whether +/// the app exists (describe) versus creating it when it doesn't. +pub enum EnsureAppError { + Describe(tower_api::apis::Error), + Create(tower_api::apis::Error), +} + pub async fn ensure_app_exists( out: &output::Out, api_config: &Configuration, app_name: &str, description: Option<&str>, create_app: bool, -) -> Result<(), crate::Error> { +) -> Result<(), EnsureAppError> { // Try to describe the app first (with spinner) let mut spinner = out.spinner("Checking app..."); let describe_result = default_api::describe_app( @@ -28,7 +35,7 @@ pub async fn ensure_app_exists( ) .await; - // If the app exists, return Ok (description is create-only). + // If the app exists, return Ok if describe_result.is_ok() { spinner.success(out); return Ok(()); @@ -50,7 +57,7 @@ pub async fn ensure_app_exists( // If it's not a 404 error, fail the spinner and return the error if !is_not_found { spinner.failure(out); - return Err(crate::Error::ApiDescribeAppError { source: err }); + return Err(EnsureAppError::Describe(err)); } // App not found - stop spinner before prompting user @@ -69,10 +76,12 @@ pub async fn ensure_app_exists( // If the user doesn't want to create the app, return the original error if !create_app { - return Err(crate::Error::ApiDescribeAppError { source: err }); + return Err(EnsureAppError::Describe(err)); } - // Try to create the app (with a new spinner) + // Try to create the app (with a new spinner). The Towerfile description + // (when present) becomes the new app's short description; description is + // only ever set at creation time. let mut spinner = out.spinner("Creating app..."); let create_result = default_api::create_app( api_config, @@ -80,8 +89,7 @@ pub async fn ensure_app_exists( create_app_params: CreateAppParamsModel { schema: None, name: app_name.to_string(), - // API create expects short_description; CLI/Towerfile expose "description". - short_description: description.map(|desc| desc.to_string()), + short_description: description.map(|s| s.to_string()), slug: None, is_externally_accessible: None, subdomain: None, @@ -102,7 +110,7 @@ pub async fn ensure_app_exists( } Err(create_err) => { spinner.failure(out); - Err(crate::Error::ApiCreateAppError { source: create_err }) + Err(EnsureAppError::Create(create_err)) } } } diff --git a/crates/tower-package/src/towerfile.rs b/crates/tower-package/src/towerfile.rs index 715e6f67..c97250b4 100644 --- a/crates/tower-package/src/towerfile.rs +++ b/crates/tower-package/src/towerfile.rs @@ -31,6 +31,9 @@ pub struct App { #[serde(default)] pub schedule: String, + /// Optional short description of the app. `None` means the Towerfile + /// didn't set one (and the key is omitted when serializing), which is + /// distinct from an explicitly empty description. #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, @@ -189,7 +192,6 @@ mod test { assert_eq!(towerfile.app.script, "./script.py"); assert_eq!(towerfile.app.source, vec!["*.py"]); assert_eq!(towerfile.app.schedule, "0 0 * * *"); - assert_eq!(towerfile.app.description, None); } #[test] @@ -206,7 +208,6 @@ mod test { assert_eq!(towerfile.app.script, "./script.py"); assert_eq!(towerfile.app.source, vec!["*.py"]); assert_eq!(towerfile.app.schedule, ""); - assert_eq!(towerfile.app.description, None); } #[test] @@ -390,6 +391,51 @@ mod test { assert!(!towerfile.remove_parameter("param1")); } + #[test] + fn test_description_absent_when_not_set() { + let toml = r#" + [app] + name = "test" + script = "./script.py" + "#; + + let towerfile = crate::Towerfile::from_toml(toml).unwrap(); + assert_eq!(towerfile.app.description, None); + + // Serializing a Towerfile without a description omits the key. + let serialized = toml::to_string_pretty(&towerfile).unwrap(); + assert!(!serialized.contains("description")); + } + + #[test] + fn test_description_distinguishes_empty_from_absent() { + let toml = r#" + [app] + name = "test" + script = "./script.py" + description = "" + "#; + + let towerfile = crate::Towerfile::from_toml(toml).unwrap(); + assert_eq!(towerfile.app.description, Some(String::new())); + } + + #[test] + fn test_description_roundtrips_when_present() { + let toml = r#" + [app] + name = "test" + script = "./script.py" + description = "My app" + "#; + + let towerfile = crate::Towerfile::from_toml(toml).unwrap(); + assert_eq!(towerfile.app.description.as_deref(), Some("My app")); + + let serialized = toml::to_string_pretty(&towerfile).unwrap(); + assert!(serialized.contains(r#"description = "My app""#)); + } + #[test] fn test_roundtrip_serialization() { let original_toml = r#"[app] @@ -417,7 +463,6 @@ default = "value2" assert_eq!(towerfile.app.name, reparsed.app.name); assert_eq!(towerfile.app.script, reparsed.app.script); assert_eq!(towerfile.app.source, reparsed.app.source); - assert_eq!(towerfile.app.description, reparsed.app.description); assert_eq!(towerfile.parameters.len(), reparsed.parameters.len()); assert_eq!(towerfile.parameters[0].name, reparsed.parameters[0].name); } diff --git a/flake.nix b/flake.nix index 29ab86e4..14937be2 100644 --- a/flake.nix +++ b/flake.nix @@ -22,7 +22,7 @@ overlays = [ rust-overlay.overlays.default ]; }; - maintainer = "Tower Computing Inc. "; + maintainer = "Tower Computing GmbH "; homepage = "https://github.com/tower/tower-cli"; description = "Tower CLI and runtime environment"; longDescription = " | @@ -47,7 +47,7 @@ ${if packager == "rpm" then "release: 1" else "section: utils\npriority: optional"} maintainer: ${maintainer} description: ${longDescription} - vendor: Tower Computing Inc. + vendor: Tower Computing GmbH homepage: "${homepage}" license: MIT diff --git a/plugin/skills/tower/SKILL.md b/plugin/skills/tower/SKILL.md index e34070c1..a6fe0ad7 100644 --- a/plugin/skills/tower/SKILL.md +++ b/plugin/skills/tower/SKILL.md @@ -173,7 +173,7 @@ Get the logs from a previous Tower app run - `` *(required)* — app_name#run_number - `` -- `-f`, `--follow` — Follow logs in real time +- `-f`, `--follow` — Follow the logs of the run in real time #### `tower apps create` @@ -331,7 +331,7 @@ Delete a schedule **Arguments:** -- `` *(required)* — The schedule ID to delete +- `` *(required)* — The ID of the schedule to delete #### `tower schedules update` @@ -339,7 +339,7 @@ Update an existing schedule **Arguments:** -- `` *(required)* — ID or name of the schedule to update +- `` *(required)* — The ID of the schedule to update - `-c`, `--cron` — The cron expression defining when the app should run - `-p`, `--parameter` — Parameters (key=value) to pass to the app @@ -353,8 +353,8 @@ List secrets in your Tower account **Arguments:** -- `-s`, `--show` — Show secrets in plain text -- `-e`, `--environment` — List secrets in this environment +- `-s`, `--show` — Show the secret values in plain text +- `-e`, `--environment` — The environment to list secrets from - `-a`, `--all` — List secrets across all environments #### `tower secrets create` @@ -363,9 +363,9 @@ Create a new secret in your Tower account **Arguments:** -- `-n`, `--name` *(required)* — Secret name to create -- `-e`, `--environment` — Environment to store the secret in -- `-v`, `--value` *(required)* — Secret value to store +- `-n`, `--name` *(required)* — The name of the secret to create +- `-e`, `--environment` — The environment to create the secret in +- `-v`, `--value` *(required)* — The value of the secret #### `tower secrets delete` diff --git a/pyproject.toml b/pyproject.toml index 8d84c009..fdf331b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,9 +4,9 @@ build-backend = "maturin" [project] name = "tower" -version = "0.3.70" +version = "0.3.71" description = "Tower CLI and runtime environment for Tower." -authors = [{ name = "Tower Computing Inc.", email = "brad@tower.dev" }] +authors = [{ name = "Tower Computing GmbH", email = "brad@tower.dev" }] readme = "README.md" requires-python = ">=3.12" license = { file = "LICENSE" } diff --git a/src/tower/_dbt.py b/src/tower/_dbt.py index 518fe5b3..83370f6f 100644 --- a/src/tower/_dbt.py +++ b/src/tower/_dbt.py @@ -5,7 +5,6 @@ import os import shlex import tempfile -from collections.abc import Iterable as IterableABC from contextlib import contextmanager from dataclasses import dataclass, field from pathlib import Path @@ -39,9 +38,11 @@ def to_arg_list(self) -> list[str]: DbtCommand("build"), ) -# Commands that support --select flag -# See: https://docs.getdbt.com/reference/node-selection/syntax -COMMANDS_WITH_SELECT: frozenset[str] = frozenset( +# dbt commands that accept node selection (--select) per dbt's documented +# node-selection syntax. Commands outside this set (e.g. deps, debug, parse, +# clean) reject --select, so a configured selector must never be injected +# into them. +SELECT_SUPPORTED_COMMANDS: frozenset[str] = frozenset( { "run", "test", @@ -203,7 +204,7 @@ def run_dbt_workflow(config: DbtRunnerConfig) -> list[object]: if ( config.selector - and command.name in COMMANDS_WITH_SELECT + and command.name in SELECT_SUPPORTED_COMMANDS and not (_has_flag(args, "--select") or _has_flag(args, "-s")) ): args.extend(["--select", config.selector]) @@ -241,35 +242,30 @@ def run_dbt_workflow(config: DbtRunnerConfig) -> list[object]: def _log_run_results(log: logging.Logger, entries: Iterable[object] | None) -> None: - """Log individual model/test results from dbt commands that produce them. + """Log per-node results when a dbt command produced them. - Based on dbt-core's return types (see dbt.cli.main.dbtRunnerResult): - - Commands returning RunExecutionResult (iterable, has node-level results): - - build, compile, run, seed, snapshot, test, run-operation - - Commands returning non-iterable results: - - docs generate → CatalogArtifact - - parse → Manifest - - list/ls → List[str] (iterable but no node results) - - debug → bool - - clean, deps, init, docs serve → None - - This function logs node-level results when available (RunExecutionResult). - For other return types, dbt's own logging is sufficient. + dbt commands return heterogeneous payloads: some yield an iterable of + node-level results, others a single non-iterable object (a bool, a + manifest, a catalog artifact, or nothing). Non-iterable payloads (with + strings/bytes treated as non-iterable) are skipped without raising. """ if not entries: return - - if not isinstance(entries, IterableABC) or isinstance(entries, (str, bytes)): - result_type = type(entries).__name__ + if isinstance(entries, (str, bytes)): log.debug( - "Command returned %s (not iterable node results), skipping detailed logging", - result_type, + "Skipping per-node result logging for non-iterable payload of type %s", + type(entries).__name__, ) return - - for entry in entries: + try: + iterator = iter(entries) + except TypeError: + log.debug( + "Skipping per-node result logging for non-iterable payload of type %s", + type(entries).__name__, + ) + return + for entry in iterator: node = getattr(entry, "node", None) status = getattr(entry, "status", None) if node and hasattr(node, "name"): diff --git a/src/tower/_tables.py b/src/tower/_tables.py index 2b0d8401..40d5b53b 100644 --- a/src/tower/_tables.py +++ b/src/tower/_tables.py @@ -1,15 +1,25 @@ from __future__ import annotations +import math import os +import random +import time from dataclasses import dataclass -from typing import List, Optional, TypeVar, Union - +from typing import Any, Callable, Optional, TypeVar, Union + +from pyiceberg.expressions import ( + BooleanExpression, + EqualTo, + GreaterThan, + GreaterThanOrEqual, + LessThan, + LessThanOrEqual, + NotEqualTo, +) from pyiceberg.exceptions import CommitFailedException, NoSuchTableError TTable = TypeVar("TTable", bound="Table") - -import random -import time +TRetryResult = TypeVar("TRetryResult") import polars as pl import pyarrow as pa @@ -27,16 +37,15 @@ get_tower_catalog_credentials, load_vended_catalog, ) +from .exceptions import PyArrowFilterMigrationError from .tower_api_client.models import CatalogCredentials -from .utils.pyarrow import ( - convert_pyarrow_expressions, - convert_pyarrow_schema, -) from .utils.tables import ( make_table_name, namespace_or_default, ) +_MAX_COMMIT_RETRY_DELAY_SECONDS = 30.0 + @dataclass class RowsAffectedInformation: @@ -44,6 +53,29 @@ class RowsAffectedInformation: updates: int +@dataclass(frozen=True, eq=False) +class _TableColumn: + name: str + + def __eq__(self, value: Any) -> BooleanExpression: + return EqualTo(self.name, value) + + def __ne__(self, value: Any) -> BooleanExpression: + return NotEqualTo(self.name, value) + + def __gt__(self, value: Any) -> BooleanExpression: + return GreaterThan(self.name, value) + + def __ge__(self, value: Any) -> BooleanExpression: + return GreaterThanOrEqual(self.name, value) + + def __lt__(self, value: Any) -> BooleanExpression: + return LessThan(self.name, value) + + def __le__(self, value: Any) -> BooleanExpression: + return LessThanOrEqual(self.name, value) + + _VendedCatalogIdentity = tuple[str, str, str] @@ -249,8 +281,34 @@ def rows_affected(self) -> RowsAffectedInformation: def _validate_retry_args(max_retries: int, retry_delay_seconds: float) -> None: if max_retries < 0: raise ValueError("max_retries must be >= 0") - if retry_delay_seconds < 0: - raise ValueError("retry_delay_seconds must be >= 0") + if not math.isfinite(retry_delay_seconds) or retry_delay_seconds < 0: + raise ValueError("retry_delay_seconds must be finite and >= 0") + + def _commit_with_retry( + self, + operation: Callable[[], TRetryResult], + max_retries: int, + initial_retry_ceiling_seconds: float, + ) -> TRetryResult: + retry_ceiling_seconds = min( + initial_retry_ceiling_seconds, _MAX_COMMIT_RETRY_DELAY_SECONDS + ) + + for attempt in range(max_retries + 1): + try: + return operation() + except CommitFailedException: + if attempt == max_retries: + raise + + delay_seconds = random.uniform(0.0, retry_ceiling_seconds) + time.sleep(delay_seconds) + self._table.refresh() + retry_ceiling_seconds = min( + retry_ceiling_seconds * 2, _MAX_COMMIT_RETRY_DELAY_SECONDS + ) + + raise AssertionError("unreachable") def insert( self, @@ -270,8 +328,9 @@ def insert( must match the schema of the target table. max_retries (int): Maximum number of retry attempts on commit conflicts. Defaults to 5. - retry_delay_seconds (float): Wait time in seconds between retries. - Defaults to 0.5 seconds. + retry_delay_seconds (float): Maximum randomized wait before the first retry, + in seconds. The maximum doubles after each conflict but never exceeds + 30 seconds; values above 30 are treated as 30. Defaults to 0.5 seconds. Returns: TTable: The table instance with the newly inserted rows, allowing for method chaining. @@ -296,23 +355,11 @@ def insert( self._validate_retry_args(max_retries, retry_delay_seconds) self._ensure_read_write_table() - last_exception = None - - for attempt in range(max_retries + 1): - try: - if attempt > 0: - self._table.refresh() - - self._table.append(data) - self._stats.inserts += data.num_rows - return self - - except CommitFailedException as e: - last_exception = e - if attempt < max_retries: - time.sleep(retry_delay_seconds) - - raise last_exception + self._commit_with_retry( + lambda: self._table.append(data), max_retries, retry_delay_seconds + ) + self._stats.inserts += data.num_rows + return self def upsert( self, @@ -337,8 +384,9 @@ def upsert( If not provided, all columns will be used for matching. max_retries (int): Maximum number of retry attempts on commit conflicts. Defaults to 5. - retry_delay_seconds (float): Wait time in seconds between retries. - Defaults to 0.5 seconds. + retry_delay_seconds (float): Maximum randomized wait before the first retry, + in seconds. The maximum doubles after each conflict but never exceeds + 30 seconds; values above 30 are treated as 30. Defaults to 0.5 seconds. Returns: TTable: The table instance with the upserted rows, allowing for method chaining. @@ -370,38 +418,28 @@ def upsert( self._validate_retry_args(max_retries, retry_delay_seconds) self._ensure_read_write_table() - last_exception = None - - for attempt in range(max_retries + 1): - try: - if attempt > 0: - self._table.refresh() - - res = self._table.upsert( - data, - join_cols=join_cols, - # All upserts will always be case sensitive. Perhaps we'll add this - # as a parameter in the future? - case_sensitive=True, - # These are the defaults, but we're including them to be complete. - when_matched_update_all=True, - when_not_matched_insert_all=True, - ) - - self._stats.updates += res.rows_updated - self._stats.inserts += res.rows_inserted - return self - - except CommitFailedException as e: - last_exception = e - if attempt < max_retries: - time.sleep(retry_delay_seconds) + res = self._commit_with_retry( + lambda: self._table.upsert( + data, + join_cols=join_cols, + # All upserts will always be case sensitive. Perhaps we'll add this + # as a parameter in the future? + case_sensitive=True, + # These are the defaults, but we're including them to be complete. + when_matched_update_all=True, + when_not_matched_insert_all=True, + ), + max_retries, + retry_delay_seconds, + ) - raise last_exception + self._stats.updates += res.rows_updated + self._stats.inserts += res.rows_inserted + return self def delete( self, - filters: Union[str, List[pc.Expression]], + filters: str | BooleanExpression, max_retries: int = 5, retry_delay_seconds: float = 0.5, ) -> TTable: @@ -414,15 +452,13 @@ def delete( cannot be tracked due to limitations in the underlying Iceberg implementation. Args: - filters (Union[str, List[pc.Expression]]): The filter conditions to apply. - Can be either: - - A single PyArrow compute expression - - A list of PyArrow compute expressions (combined with AND) - - A string expression + filters (str | BooleanExpression): A SQL-like string or a PyIceberg + boolean expression. Use ``Table.column()`` to construct expressions. max_retries (int): Maximum number of retry attempts on commit conflicts. Defaults to 5. - retry_delay_seconds (float): Wait time in seconds between retries. - Defaults to 0.5 seconds. + retry_delay_seconds (float): Maximum randomized wait before the first retry, + in seconds. The maximum doubles after each conflict but never exceeds + 30 seconds; values above 30 are treated as 30. Defaults to 0.5 seconds. Returns: TTable: The table instance with the deleted rows, allowing for method chaining. @@ -440,45 +476,41 @@ def delete( >>> # Delete rows where age is greater than 30 >>> table.delete(table.column("age") > 30) >>> # Delete rows matching multiple conditions - >>> table.delete([ - ... table.column("age") > 30, - ... table.column("department") == "IT" - ... ]) + >>> table.delete( + ... (table.column("age") > 30) + ... & (table.column("department") == "IT") + ... ) >>> # Delete rows using a string expression >>> table.delete("age > 30 AND department = 'IT'") """ self._validate_retry_args(max_retries, retry_delay_seconds) + filters = self._normalize_delete_filter(filters) self._ensure_read_write_table() - if isinstance(filters, list): - # We need to convert the pc.Expression into PyIceberg - next_filters = convert_pyarrow_expressions(filters) - filters = next_filters - - last_exception = None - - for attempt in range(max_retries + 1): - try: - if attempt > 0: - self._table.refresh() - - self._table.delete( - delete_filter=filters, - # We want this to always be the case. Not sure why you wouldn't? - case_sensitive=True, - ) - - # NOTE: There is, unfortunately, no way to get the number of rows - # deleted besides comparing the two snapshots that were created. + self._commit_with_retry( + lambda: self._table.delete( + delete_filter=filters, + # We want this to always be the case. Not sure why you wouldn't? + case_sensitive=True, + ), + max_retries, + retry_delay_seconds, + ) - return self + # NOTE: There is, unfortunately, no way to get the number of rows + # deleted besides comparing the two snapshots that were created. - except CommitFailedException as e: - last_exception = e - if attempt < max_retries: - time.sleep(retry_delay_seconds) + return self - raise last_exception + @staticmethod + def _normalize_delete_filter(filters: object) -> str | BooleanExpression: + if isinstance(filters, (pc.Expression, list)): + raise PyArrowFilterMigrationError() + if isinstance(filters, (str, BooleanExpression)): + return filters + raise TypeError( + "filters must be a SQL-like string or a PyIceberg BooleanExpression" + ) def schema(self) -> pa.Schema: """ @@ -496,19 +528,19 @@ def schema(self) -> pa.Schema: iceberg_schema = self._table.schema() return iceberg_schema.as_arrow() - def column(self, name: str) -> pa.compute.Expression: + def column(self, name: str) -> _TableColumn: """ - Returns a column from the table as a PyArrow compute expression. + Returns a structural builder for PyIceberg filter expressions. This method is useful for creating column-based expressions that can be used in - operations like filtering, sorting, or aggregating data. The returned expression - can be used with PyArrow's compute functions. + comparison operators build PyIceberg boolean expressions that can be passed to + ``delete()`` and composed with ``&``, ``|``, and ``~``. Args: name (str): The name of the column to retrieve from the table schema. Returns: - pa.compute.Expression: A PyArrow compute expression representing the column. + _TableColumn: A builder for PyIceberg comparison expressions. Raises: ValueError: If the specified column name is not found in the table schema. @@ -520,13 +552,12 @@ def column(self, name: str) -> pa.compute.Expression: >>> # Use the expression in a delete operation >>> table.delete(age_expr) """ - field = self.schema().field(name) - - if field is None: - raise ValueError(f"Column {name} not found in table schema") + try: + self._table.schema().find_field(name, case_sensitive=True) + except ValueError: + raise ValueError(f"Column {name} not found in table schema") from None - # We need to convert the PyArrow field into pa.compute.Expression - return pa.compute.field(name) + return _TableColumn(name) class TableReference: @@ -618,7 +649,9 @@ def create(self, schema: pa.Schema) -> Table: Args: schema (pa.Schema): The PyArrow schema defining the structure of the table. - This will be converted to an Iceberg schema internally. + PyIceberg validates it and assigns Iceberg field IDs. Lossy or + unsupported types, including nanosecond timestamps by default, are + rejected. Returns: Table: A new Table instance wrapping the created Iceberg table. @@ -653,7 +686,7 @@ def create(self, schema: pa.Schema) -> Table: # along the way. table = catalog.create_table( identifier=table_name, - schema=convert_pyarrow_schema(schema), + schema=schema, ) return Table( @@ -680,8 +713,10 @@ def create_if_not_exists(self, schema: pa.Schema) -> Table: Args: schema (pa.Schema): The PyArrow schema defining the structure of the table. - This will be converted to an Iceberg schema internally. Note that this - schema is only used if the table needs to be created. + PyIceberg validates it and assigns Iceberg field IDs. Lossy or + unsupported types, including nanosecond timestamps by default, are + rejected. This schema is only used if the + table needs to be created. Returns: Table: A Table instance wrapping either the newly created or existing Iceberg table. @@ -715,7 +750,7 @@ def create_if_not_exists(self, schema: pa.Schema) -> Table: # exists. table = catalog.create_table_if_not_exists( identifier=table_name, - schema=convert_pyarrow_schema(schema), + schema=schema, ) return Table( diff --git a/src/tower/exceptions.py b/src/tower/exceptions.py index b37c2a80..0035f952 100644 --- a/src/tower/exceptions.py +++ b/src/tower/exceptions.py @@ -32,3 +32,14 @@ def __init__(self, app_name: str, number: int, state: str): class AppNotFoundError(RuntimeError): def __init__(self, app_name: str): super().__init__(f"App '{app_name}' not found in the Tower.") + + +class PyArrowFilterMigrationError(TypeError): + def __init__(self): + super().__init__( + "PyArrow compute expressions are no longer accepted as table delete " + 'filters. Replace pc.field("age") >= 18 with ' + 'table.column("age") >= 18. Combine predicates with &, |, and ~; ' + "replace [a, b] with a & b. You can also pass a PyIceberg " + "BooleanExpression or a SQL-like filter string." + ) diff --git a/src/tower/tower_api_client/api/default/batch_describe_runs.py b/src/tower/tower_api_client/api/default/batch_describe_runs.py new file mode 100644 index 00000000..4d966901 --- /dev/null +++ b/src/tower/tower_api_client/api/default/batch_describe_runs.py @@ -0,0 +1,170 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ...client import AuthenticatedClient, Client +from ...models.batch_describe_runs_params import BatchDescribeRunsParams +from ...models.batch_describe_runs_response import BatchDescribeRunsResponse +from ...models.error_model import ErrorModel +from ...types import Response + + +def _get_kwargs( + *, + body: list[BatchDescribeRunsParams], +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/batch/describe-runs", + } + + _kwargs["json"] = [] + for body_item_data in body: + body_item = body_item_data.to_dict() + _kwargs["json"].append(body_item) + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> BatchDescribeRunsResponse | ErrorModel: + if response.status_code == 200: + response_200 = BatchDescribeRunsResponse.from_dict(response.json()) + + return response_200 + + response_default = ErrorModel.from_dict(response.json()) + + return response_default + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[BatchDescribeRunsResponse | ErrorModel]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: list[BatchDescribeRunsParams], +) -> Response[BatchDescribeRunsResponse | ErrorModel]: + """Batch describe runs + + Describe multiple runs in a single request. + + Args: + body (list[BatchDescribeRunsParams]): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[BatchDescribeRunsResponse | ErrorModel] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: list[BatchDescribeRunsParams], +) -> BatchDescribeRunsResponse | ErrorModel | None: + """Batch describe runs + + Describe multiple runs in a single request. + + Args: + body (list[BatchDescribeRunsParams]): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + BatchDescribeRunsResponse | ErrorModel + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: list[BatchDescribeRunsParams], +) -> Response[BatchDescribeRunsResponse | ErrorModel]: + """Batch describe runs + + Describe multiple runs in a single request. + + Args: + body (list[BatchDescribeRunsParams]): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[BatchDescribeRunsResponse | ErrorModel] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: list[BatchDescribeRunsParams], +) -> BatchDescribeRunsResponse | ErrorModel | None: + """Batch describe runs + + Describe multiple runs in a single request. + + Args: + body (list[BatchDescribeRunsParams]): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + BatchDescribeRunsResponse | ErrorModel + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/src/tower/tower_api_client/api/default/batch_describe_runs_logs.py b/src/tower/tower_api_client/api/default/batch_describe_runs_logs.py new file mode 100644 index 00000000..3096693e --- /dev/null +++ b/src/tower/tower_api_client/api/default/batch_describe_runs_logs.py @@ -0,0 +1,175 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ...client import AuthenticatedClient, Client +from ...models.batch_describe_runs_logs_params import BatchDescribeRunsLogsParams +from ...models.batched_run_log_lines import BatchedRunLogLines +from ...models.error_model import ErrorModel +from ...types import Response + + +def _get_kwargs( + *, + body: list[BatchDescribeRunsLogsParams], +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/batch/describe-runs-logs", + } + + _kwargs["json"] = [] + for body_item_data in body: + body_item = body_item_data.to_dict() + _kwargs["json"].append(body_item) + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorModel | list[BatchedRunLogLines]: + if response.status_code == 200: + response_200 = [] + _response_200 = response.json() + for response_200_item_data in _response_200: + response_200_item = BatchedRunLogLines.from_dict(response_200_item_data) + + response_200.append(response_200_item) + + return response_200 + + response_default = ErrorModel.from_dict(response.json()) + + return response_default + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorModel | list[BatchedRunLogLines]]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: list[BatchDescribeRunsLogsParams], +) -> Response[ErrorModel | list[BatchedRunLogLines]]: + """Batch describe runs logs + + Describe multiple run logs in a single request. + + Args: + body (list[BatchDescribeRunsLogsParams]): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorModel | list[BatchedRunLogLines]] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: list[BatchDescribeRunsLogsParams], +) -> ErrorModel | list[BatchedRunLogLines] | None: + """Batch describe runs logs + + Describe multiple run logs in a single request. + + Args: + body (list[BatchDescribeRunsLogsParams]): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorModel | list[BatchedRunLogLines] + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: list[BatchDescribeRunsLogsParams], +) -> Response[ErrorModel | list[BatchedRunLogLines]]: + """Batch describe runs logs + + Describe multiple run logs in a single request. + + Args: + body (list[BatchDescribeRunsLogsParams]): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorModel | list[BatchedRunLogLines]] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: list[BatchDescribeRunsLogsParams], +) -> ErrorModel | list[BatchedRunLogLines] | None: + """Batch describe runs logs + + Describe multiple run logs in a single request. + + Args: + body (list[BatchDescribeRunsLogsParams]): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorModel | list[BatchedRunLogLines] + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/src/tower/tower_api_client/api/default/create_catalog.py b/src/tower/tower_api_client/api/default/create_catalog.py index 37914243..1d7f212b 100644 --- a/src/tower/tower_api_client/api/default/create_catalog.py +++ b/src/tower/tower_api_client/api/default/create_catalog.py @@ -37,6 +37,36 @@ def _parse_response( return response_200 + if response.status_code == 400: + response_400 = ErrorModel.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorModel.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = ErrorModel.from_dict(response.json()) + + return response_403 + + if response.status_code == 409: + response_409 = ErrorModel.from_dict(response.json()) + + return response_409 + + if response.status_code == 422: + response_422 = ErrorModel.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ErrorModel.from_dict(response.json()) + + return response_500 + response_default = ErrorModel.from_dict(response.json()) return response_default diff --git a/src/tower/tower_api_client/api/default/delete_catalog.py b/src/tower/tower_api_client/api/default/delete_catalog.py index 3a967033..21b4d1eb 100644 --- a/src/tower/tower_api_client/api/default/delete_catalog.py +++ b/src/tower/tower_api_client/api/default/delete_catalog.py @@ -40,6 +40,36 @@ def _parse_response( return response_204 + if response.status_code == 401: + response_401 = ErrorModel.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = ErrorModel.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = ErrorModel.from_dict(response.json()) + + return response_404 + + if response.status_code == 409: + response_409 = ErrorModel.from_dict(response.json()) + + return response_409 + + if response.status_code == 422: + response_422 = ErrorModel.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ErrorModel.from_dict(response.json()) + + return response_500 + response_default = ErrorModel.from_dict(response.json()) return response_default diff --git a/src/tower/tower_api_client/api/default/delete_catalog_fact.py b/src/tower/tower_api_client/api/default/delete_catalog_fact.py new file mode 100644 index 00000000..0a571f93 --- /dev/null +++ b/src/tower/tower_api_client/api/default/delete_catalog_fact.py @@ -0,0 +1,198 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ...client import AuthenticatedClient, Client +from ...models.error_model import ErrorModel +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + catalog: str, + name: str, + *, + environment: str | Unset = "default", +) -> dict[str, Any]: + params: dict[str, Any] = {} + + params["environment"] = environment + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "delete", + "url": "/catalogs/{catalog}/facts/{name}".format( + catalog=quote(str(catalog), safe=""), + name=quote(str(name), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | ErrorModel: + if response.status_code == 204: + response_204 = cast(Any, None) + return response_204 + + response_default = ErrorModel.from_dict(response.json()) + + return response_default + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | ErrorModel]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + catalog: str, + name: str, + *, + client: AuthenticatedClient, + environment: str | Unset = "default", +) -> Response[Any | ErrorModel]: + """Delete a catalog fact + + Deletes a semantic metadata fact addressed by its name within a catalog. + + Args: + catalog (str): The name of the catalog. + name (str): The name of the fact. + environment (str | Unset): Environment containing the catalog definition to delete from. + This operation does not fall back to default. Default: 'default'. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | ErrorModel] + """ + + kwargs = _get_kwargs( + catalog=catalog, + name=name, + environment=environment, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + catalog: str, + name: str, + *, + client: AuthenticatedClient, + environment: str | Unset = "default", +) -> Any | ErrorModel | None: + """Delete a catalog fact + + Deletes a semantic metadata fact addressed by its name within a catalog. + + Args: + catalog (str): The name of the catalog. + name (str): The name of the fact. + environment (str | Unset): Environment containing the catalog definition to delete from. + This operation does not fall back to default. Default: 'default'. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | ErrorModel + """ + + return sync_detailed( + catalog=catalog, + name=name, + client=client, + environment=environment, + ).parsed + + +async def asyncio_detailed( + catalog: str, + name: str, + *, + client: AuthenticatedClient, + environment: str | Unset = "default", +) -> Response[Any | ErrorModel]: + """Delete a catalog fact + + Deletes a semantic metadata fact addressed by its name within a catalog. + + Args: + catalog (str): The name of the catalog. + name (str): The name of the fact. + environment (str | Unset): Environment containing the catalog definition to delete from. + This operation does not fall back to default. Default: 'default'. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | ErrorModel] + """ + + kwargs = _get_kwargs( + catalog=catalog, + name=name, + environment=environment, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + catalog: str, + name: str, + *, + client: AuthenticatedClient, + environment: str | Unset = "default", +) -> Any | ErrorModel | None: + """Delete a catalog fact + + Deletes a semantic metadata fact addressed by its name within a catalog. + + Args: + catalog (str): The name of the catalog. + name (str): The name of the fact. + environment (str | Unset): Environment containing the catalog definition to delete from. + This operation does not fall back to default. Default: 'default'. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | ErrorModel + """ + + return ( + await asyncio_detailed( + catalog=catalog, + name=name, + client=client, + environment=environment, + ) + ).parsed diff --git a/src/tower/tower_api_client/api/default/delete_environment.py b/src/tower/tower_api_client/api/default/delete_environment.py index b3263192..a6f4326c 100644 --- a/src/tower/tower_api_client/api/default/delete_environment.py +++ b/src/tower/tower_api_client/api/default/delete_environment.py @@ -31,6 +31,36 @@ def _parse_response( return response_200 + if response.status_code == 401: + response_401 = ErrorModel.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = ErrorModel.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = ErrorModel.from_dict(response.json()) + + return response_404 + + if response.status_code == 409: + response_409 = ErrorModel.from_dict(response.json()) + + return response_409 + + if response.status_code == 422: + response_422 = ErrorModel.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ErrorModel.from_dict(response.json()) + + return response_500 + response_default = ErrorModel.from_dict(response.json()) return response_default diff --git a/src/tower/tower_api_client/api/default/deploy_app.py b/src/tower/tower_api_client/api/default/deploy_app.py index 47824719..4aa38cf0 100644 --- a/src/tower/tower_api_client/api/default/deploy_app.py +++ b/src/tower/tower_api_client/api/default/deploy_app.py @@ -20,6 +20,7 @@ def _get_kwargs( all_environments: bool | Unset = False, x_tower_checksum_sha256: str | Unset = UNSET, x_tower_idempotency_key: str | Unset = UNSET, + x_tower_request_number: int | Unset = UNSET, content_length: int | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -29,6 +30,9 @@ def _get_kwargs( if not isinstance(x_tower_idempotency_key, Unset): headers["X-Tower-Idempotency-Key"] = x_tower_idempotency_key + if not isinstance(x_tower_request_number, Unset): + headers["X-Tower-Request-Number"] = str(x_tower_request_number) + if not isinstance(content_length, Unset): headers["Content-Length"] = str(content_length) @@ -74,6 +78,11 @@ def _parse_response( return response_400 + if response.status_code == 412: + response_412 = ErrorModel.from_dict(response.json()) + + return response_412 + if response.status_code == 422: response_422 = ErrorModel.from_dict(response.json()) @@ -110,6 +119,7 @@ def sync_detailed( all_environments: bool | Unset = False, x_tower_checksum_sha256: str | Unset = UNSET, x_tower_idempotency_key: str | Unset = UNSET, + x_tower_request_number: int | Unset = UNSET, content_length: int | Unset = UNSET, ) -> Response[DeployAppResponse | ErrorModel]: """Deploy app @@ -128,6 +138,9 @@ def sync_detailed( CI build ID). If a prior deploy for this app supplied the same value, the server reuses that AppVersion instead of creating a new one — letting consecutive deploys to different environments share a single version when the source hasn't changed. + x_tower_request_number (int | Unset): Optional account-scoped monotonic sequence token + used to reject out-of-order writes. See the fence documentation for the acceptance window + and retry behavior. content_length (int | Unset): Size of the uploaded bundle in bytes. body (DeployAppJsonBody): Example: {'source_uri': 'https://github.com/tower/tower- examples/tree/main/01-hello-world'}. @@ -149,6 +162,7 @@ def sync_detailed( all_environments=all_environments, x_tower_checksum_sha256=x_tower_checksum_sha256, x_tower_idempotency_key=x_tower_idempotency_key, + x_tower_request_number=x_tower_request_number, content_length=content_length, ) @@ -168,6 +182,7 @@ def sync( all_environments: bool | Unset = False, x_tower_checksum_sha256: str | Unset = UNSET, x_tower_idempotency_key: str | Unset = UNSET, + x_tower_request_number: int | Unset = UNSET, content_length: int | Unset = UNSET, ) -> DeployAppResponse | ErrorModel | None: """Deploy app @@ -186,6 +201,9 @@ def sync( CI build ID). If a prior deploy for this app supplied the same value, the server reuses that AppVersion instead of creating a new one — letting consecutive deploys to different environments share a single version when the source hasn't changed. + x_tower_request_number (int | Unset): Optional account-scoped monotonic sequence token + used to reject out-of-order writes. See the fence documentation for the acceptance window + and retry behavior. content_length (int | Unset): Size of the uploaded bundle in bytes. body (DeployAppJsonBody): Example: {'source_uri': 'https://github.com/tower/tower- examples/tree/main/01-hello-world'}. @@ -208,6 +226,7 @@ def sync( all_environments=all_environments, x_tower_checksum_sha256=x_tower_checksum_sha256, x_tower_idempotency_key=x_tower_idempotency_key, + x_tower_request_number=x_tower_request_number, content_length=content_length, ).parsed @@ -221,6 +240,7 @@ async def asyncio_detailed( all_environments: bool | Unset = False, x_tower_checksum_sha256: str | Unset = UNSET, x_tower_idempotency_key: str | Unset = UNSET, + x_tower_request_number: int | Unset = UNSET, content_length: int | Unset = UNSET, ) -> Response[DeployAppResponse | ErrorModel]: """Deploy app @@ -239,6 +259,9 @@ async def asyncio_detailed( CI build ID). If a prior deploy for this app supplied the same value, the server reuses that AppVersion instead of creating a new one — letting consecutive deploys to different environments share a single version when the source hasn't changed. + x_tower_request_number (int | Unset): Optional account-scoped monotonic sequence token + used to reject out-of-order writes. See the fence documentation for the acceptance window + and retry behavior. content_length (int | Unset): Size of the uploaded bundle in bytes. body (DeployAppJsonBody): Example: {'source_uri': 'https://github.com/tower/tower- examples/tree/main/01-hello-world'}. @@ -260,6 +283,7 @@ async def asyncio_detailed( all_environments=all_environments, x_tower_checksum_sha256=x_tower_checksum_sha256, x_tower_idempotency_key=x_tower_idempotency_key, + x_tower_request_number=x_tower_request_number, content_length=content_length, ) @@ -277,6 +301,7 @@ async def asyncio( all_environments: bool | Unset = False, x_tower_checksum_sha256: str | Unset = UNSET, x_tower_idempotency_key: str | Unset = UNSET, + x_tower_request_number: int | Unset = UNSET, content_length: int | Unset = UNSET, ) -> DeployAppResponse | ErrorModel | None: """Deploy app @@ -295,6 +320,9 @@ async def asyncio( CI build ID). If a prior deploy for this app supplied the same value, the server reuses that AppVersion instead of creating a new one — letting consecutive deploys to different environments share a single version when the source hasn't changed. + x_tower_request_number (int | Unset): Optional account-scoped monotonic sequence token + used to reject out-of-order writes. See the fence documentation for the acceptance window + and retry behavior. content_length (int | Unset): Size of the uploaded bundle in bytes. body (DeployAppJsonBody): Example: {'source_uri': 'https://github.com/tower/tower- examples/tree/main/01-hello-world'}. @@ -318,6 +346,7 @@ async def asyncio( all_environments=all_environments, x_tower_checksum_sha256=x_tower_checksum_sha256, x_tower_idempotency_key=x_tower_idempotency_key, + x_tower_request_number=x_tower_request_number, content_length=content_length, ) ).parsed diff --git a/src/tower/tower_api_client/api/default/describe_catalog.py b/src/tower/tower_api_client/api/default/describe_catalog.py index 9f990bb3..38505d7a 100644 --- a/src/tower/tower_api_client/api/default/describe_catalog.py +++ b/src/tower/tower_api_client/api/default/describe_catalog.py @@ -64,11 +64,14 @@ def sync_detailed( ) -> Response[DescribeCatalogResponse | ErrorModel]: """Describe catalog - Returns details for a specific catalog, including its property names and previews. + Returns non-secret details for a catalog in the selected environment. When that environment has no + same-named catalog, the catalog from default is returned instead. The response's catalog environment + identifies where its definition is stored. Args: name (str): The name of the catalog. - environment (str | Unset): The environment of the catalog. Default: 'default'. + environment (str | Unset): Environment whose catalog to return. When it has no same-named + catalog, the catalog from default is returned instead. Default: 'default'. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -98,11 +101,14 @@ def sync( ) -> DescribeCatalogResponse | ErrorModel | None: """Describe catalog - Returns details for a specific catalog, including its property names and previews. + Returns non-secret details for a catalog in the selected environment. When that environment has no + same-named catalog, the catalog from default is returned instead. The response's catalog environment + identifies where its definition is stored. Args: name (str): The name of the catalog. - environment (str | Unset): The environment of the catalog. Default: 'default'. + environment (str | Unset): Environment whose catalog to return. When it has no same-named + catalog, the catalog from default is returned instead. Default: 'default'. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -127,11 +133,14 @@ async def asyncio_detailed( ) -> Response[DescribeCatalogResponse | ErrorModel]: """Describe catalog - Returns details for a specific catalog, including its property names and previews. + Returns non-secret details for a catalog in the selected environment. When that environment has no + same-named catalog, the catalog from default is returned instead. The response's catalog environment + identifies where its definition is stored. Args: name (str): The name of the catalog. - environment (str | Unset): The environment of the catalog. Default: 'default'. + environment (str | Unset): Environment whose catalog to return. When it has no same-named + catalog, the catalog from default is returned instead. Default: 'default'. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -159,11 +168,14 @@ async def asyncio( ) -> DescribeCatalogResponse | ErrorModel | None: """Describe catalog - Returns details for a specific catalog, including its property names and previews. + Returns non-secret details for a catalog in the selected environment. When that environment has no + same-named catalog, the catalog from default is returned instead. The response's catalog environment + identifies where its definition is stored. Args: name (str): The name of the catalog. - environment (str | Unset): The environment of the catalog. Default: 'default'. + environment (str | Unset): Environment whose catalog to return. When it has no same-named + catalog, the catalog from default is returned instead. Default: 'default'. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. diff --git a/src/tower/tower_api_client/api/default/describe_catalog_fact.py b/src/tower/tower_api_client/api/default/describe_catalog_fact.py new file mode 100644 index 00000000..f7e5326b --- /dev/null +++ b/src/tower/tower_api_client/api/default/describe_catalog_fact.py @@ -0,0 +1,204 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ...client import AuthenticatedClient, Client +from ...models.describe_catalog_fact_response import DescribeCatalogFactResponse +from ...models.error_model import ErrorModel +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + catalog: str, + name: str, + *, + environment: str | Unset = "default", +) -> dict[str, Any]: + params: dict[str, Any] = {} + + params["environment"] = environment + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/catalogs/{catalog}/facts/{name}".format( + catalog=quote(str(catalog), safe=""), + name=quote(str(name), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> DescribeCatalogFactResponse | ErrorModel: + if response.status_code == 200: + response_200 = DescribeCatalogFactResponse.from_dict(response.json()) + + return response_200 + + response_default = ErrorModel.from_dict(response.json()) + + return response_default + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[DescribeCatalogFactResponse | ErrorModel]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + catalog: str, + name: str, + *, + client: AuthenticatedClient, + environment: str | Unset = "default", +) -> Response[DescribeCatalogFactResponse | ErrorModel]: + """Describe a catalog fact + + Returns a single semantic metadata fact addressed by its name within a catalog. + + Args: + catalog (str): The name of the catalog. + name (str): The name of the fact. + environment (str | Unset): The environment of the catalog. Note that if a catalog with the + requested name doesn't exist in the requested environment, the fact from the catalog with + the same name in the default environment will be returned. Default: 'default'. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DescribeCatalogFactResponse | ErrorModel] + """ + + kwargs = _get_kwargs( + catalog=catalog, + name=name, + environment=environment, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + catalog: str, + name: str, + *, + client: AuthenticatedClient, + environment: str | Unset = "default", +) -> DescribeCatalogFactResponse | ErrorModel | None: + """Describe a catalog fact + + Returns a single semantic metadata fact addressed by its name within a catalog. + + Args: + catalog (str): The name of the catalog. + name (str): The name of the fact. + environment (str | Unset): The environment of the catalog. Note that if a catalog with the + requested name doesn't exist in the requested environment, the fact from the catalog with + the same name in the default environment will be returned. Default: 'default'. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DescribeCatalogFactResponse | ErrorModel + """ + + return sync_detailed( + catalog=catalog, + name=name, + client=client, + environment=environment, + ).parsed + + +async def asyncio_detailed( + catalog: str, + name: str, + *, + client: AuthenticatedClient, + environment: str | Unset = "default", +) -> Response[DescribeCatalogFactResponse | ErrorModel]: + """Describe a catalog fact + + Returns a single semantic metadata fact addressed by its name within a catalog. + + Args: + catalog (str): The name of the catalog. + name (str): The name of the fact. + environment (str | Unset): The environment of the catalog. Note that if a catalog with the + requested name doesn't exist in the requested environment, the fact from the catalog with + the same name in the default environment will be returned. Default: 'default'. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DescribeCatalogFactResponse | ErrorModel] + """ + + kwargs = _get_kwargs( + catalog=catalog, + name=name, + environment=environment, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + catalog: str, + name: str, + *, + client: AuthenticatedClient, + environment: str | Unset = "default", +) -> DescribeCatalogFactResponse | ErrorModel | None: + """Describe a catalog fact + + Returns a single semantic metadata fact addressed by its name within a catalog. + + Args: + catalog (str): The name of the catalog. + name (str): The name of the fact. + environment (str | Unset): The environment of the catalog. Note that if a catalog with the + requested name doesn't exist in the requested environment, the fact from the catalog with + the same name in the default environment will be returned. Default: 'default'. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DescribeCatalogFactResponse | ErrorModel + """ + + return ( + await asyncio_detailed( + catalog=catalog, + name=name, + client=client, + environment=environment, + ) + ).parsed diff --git a/src/tower/tower_api_client/api/default/describe_catalog_usage.py b/src/tower/tower_api_client/api/default/describe_catalog_usage.py new file mode 100644 index 00000000..b2a216c5 --- /dev/null +++ b/src/tower/tower_api_client/api/default/describe_catalog_usage.py @@ -0,0 +1,216 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.describe_catalog_usage_response import DescribeCatalogUsageResponse +from ...models.error_model import ErrorModel +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + name: str, + *, + environment: str | Unset = "default", +) -> dict[str, Any]: + params: dict[str, Any] = {} + + params["environment"] = environment + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/catalogs/{name}/usage".format( + name=quote(str(name), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> DescribeCatalogUsageResponse | ErrorModel | None: + if response.status_code == 200: + response_200 = DescribeCatalogUsageResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 401: + response_401 = ErrorModel.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorModel.from_dict(response.json()) + + return response_404 + + if response.status_code == 422: + response_422 = ErrorModel.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ErrorModel.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[DescribeCatalogUsageResponse | ErrorModel]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + name: str, + *, + client: AuthenticatedClient, + environment: str | Unset = "default", +) -> Response[DescribeCatalogUsageResponse | ErrorModel]: + """Describe catalog usage + + Returns physical bytes stored by one Tower-managed catalog, including Iceberg metadata and not-yet- + compacted snapshot history. Measurements are cached and may be temporarily unavailable; measured_at + is null when no measurement exists. BYO and S3 Tables catalogs are not metered. + + Args: + name (str): The name of the catalog. + environment (str | Unset): Environment whose catalog usage to return. When it has no same- + named catalog, usage for the catalog from default is returned instead. Default: 'default'. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DescribeCatalogUsageResponse | ErrorModel] + """ + + kwargs = _get_kwargs( + name=name, + environment=environment, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + name: str, + *, + client: AuthenticatedClient, + environment: str | Unset = "default", +) -> DescribeCatalogUsageResponse | ErrorModel | None: + """Describe catalog usage + + Returns physical bytes stored by one Tower-managed catalog, including Iceberg metadata and not-yet- + compacted snapshot history. Measurements are cached and may be temporarily unavailable; measured_at + is null when no measurement exists. BYO and S3 Tables catalogs are not metered. + + Args: + name (str): The name of the catalog. + environment (str | Unset): Environment whose catalog usage to return. When it has no same- + named catalog, usage for the catalog from default is returned instead. Default: 'default'. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DescribeCatalogUsageResponse | ErrorModel + """ + + return sync_detailed( + name=name, + client=client, + environment=environment, + ).parsed + + +async def asyncio_detailed( + name: str, + *, + client: AuthenticatedClient, + environment: str | Unset = "default", +) -> Response[DescribeCatalogUsageResponse | ErrorModel]: + """Describe catalog usage + + Returns physical bytes stored by one Tower-managed catalog, including Iceberg metadata and not-yet- + compacted snapshot history. Measurements are cached and may be temporarily unavailable; measured_at + is null when no measurement exists. BYO and S3 Tables catalogs are not metered. + + Args: + name (str): The name of the catalog. + environment (str | Unset): Environment whose catalog usage to return. When it has no same- + named catalog, usage for the catalog from default is returned instead. Default: 'default'. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DescribeCatalogUsageResponse | ErrorModel] + """ + + kwargs = _get_kwargs( + name=name, + environment=environment, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + name: str, + *, + client: AuthenticatedClient, + environment: str | Unset = "default", +) -> DescribeCatalogUsageResponse | ErrorModel | None: + """Describe catalog usage + + Returns physical bytes stored by one Tower-managed catalog, including Iceberg metadata and not-yet- + compacted snapshot history. Measurements are cached and may be temporarily unavailable; measured_at + is null when no measurement exists. BYO and S3 Tables catalogs are not metered. + + Args: + name (str): The name of the catalog. + environment (str | Unset): Environment whose catalog usage to return. When it has no same- + named catalog, usage for the catalog from default is returned instead. Default: 'default'. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DescribeCatalogUsageResponse | ErrorModel + """ + + return ( + await asyncio_detailed( + name=name, + client=client, + environment=environment, + ) + ).parsed diff --git a/src/tower/tower_api_client/api/default/describe_run_logs.py b/src/tower/tower_api_client/api/default/describe_run_logs.py index 0d205fdf..c373d299 100644 --- a/src/tower/tower_api_client/api/default/describe_run_logs.py +++ b/src/tower/tower_api_client/api/default/describe_run_logs.py @@ -16,6 +16,8 @@ def _get_kwargs( seq: int, *, start_at: datetime.datetime | Unset = UNSET, + head: int | Unset = UNSET, + tail: int | Unset = UNSET, ) -> dict[str, Any]: params: dict[str, Any] = {} @@ -24,6 +26,10 @@ def _get_kwargs( json_start_at = start_at.isoformat() params["start_at"] = json_start_at + params["head"] = head + + params["tail"] = tail + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} _kwargs: dict[str, Any] = { @@ -68,6 +74,8 @@ def sync_detailed( *, client: AuthenticatedClient, start_at: datetime.datetime | Unset = UNSET, + head: int | Unset = UNSET, + tail: int | Unset = UNSET, ) -> Response[DescribeRunLogsResponse | ErrorModel]: """Describe run logs @@ -77,6 +85,8 @@ def sync_detailed( name (str): The name of the app to get logs for. seq (int): The sequence number of the run to get logs for. start_at (datetime.datetime | Unset): Fetch logs from this timestamp onwards (inclusive). + head (int | Unset): Return only the first N log lines. Cannot be combined with tail. + tail (int | Unset): Return only the last N log lines. Cannot be combined with head. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -90,6 +100,8 @@ def sync_detailed( name=name, seq=seq, start_at=start_at, + head=head, + tail=tail, ) response = client.get_httpx_client().request( @@ -105,6 +117,8 @@ def sync( *, client: AuthenticatedClient, start_at: datetime.datetime | Unset = UNSET, + head: int | Unset = UNSET, + tail: int | Unset = UNSET, ) -> DescribeRunLogsResponse | ErrorModel | None: """Describe run logs @@ -114,6 +128,8 @@ def sync( name (str): The name of the app to get logs for. seq (int): The sequence number of the run to get logs for. start_at (datetime.datetime | Unset): Fetch logs from this timestamp onwards (inclusive). + head (int | Unset): Return only the first N log lines. Cannot be combined with tail. + tail (int | Unset): Return only the last N log lines. Cannot be combined with head. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -128,6 +144,8 @@ def sync( seq=seq, client=client, start_at=start_at, + head=head, + tail=tail, ).parsed @@ -137,6 +155,8 @@ async def asyncio_detailed( *, client: AuthenticatedClient, start_at: datetime.datetime | Unset = UNSET, + head: int | Unset = UNSET, + tail: int | Unset = UNSET, ) -> Response[DescribeRunLogsResponse | ErrorModel]: """Describe run logs @@ -146,6 +166,8 @@ async def asyncio_detailed( name (str): The name of the app to get logs for. seq (int): The sequence number of the run to get logs for. start_at (datetime.datetime | Unset): Fetch logs from this timestamp onwards (inclusive). + head (int | Unset): Return only the first N log lines. Cannot be combined with tail. + tail (int | Unset): Return only the last N log lines. Cannot be combined with head. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -159,6 +181,8 @@ async def asyncio_detailed( name=name, seq=seq, start_at=start_at, + head=head, + tail=tail, ) response = await client.get_async_httpx_client().request(**kwargs) @@ -172,6 +196,8 @@ async def asyncio( *, client: AuthenticatedClient, start_at: datetime.datetime | Unset = UNSET, + head: int | Unset = UNSET, + tail: int | Unset = UNSET, ) -> DescribeRunLogsResponse | ErrorModel | None: """Describe run logs @@ -181,6 +207,8 @@ async def asyncio( name (str): The name of the app to get logs for. seq (int): The sequence number of the run to get logs for. start_at (datetime.datetime | Unset): Fetch logs from this timestamp onwards (inclusive). + head (int | Unset): Return only the first N log lines. Cannot be combined with tail. + tail (int | Unset): Return only the last N log lines. Cannot be combined with head. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -196,5 +224,7 @@ async def asyncio( seq=seq, client=client, start_at=start_at, + head=head, + tail=tail, ) ).parsed diff --git a/src/tower/tower_api_client/api/default/export_catalogs.py b/src/tower/tower_api_client/api/default/export_catalogs.py index 093e5f30..e040ac1c 100644 --- a/src/tower/tower_api_client/api/default/export_catalogs.py +++ b/src/tower/tower_api_client/api/default/export_catalogs.py @@ -3,6 +3,7 @@ import httpx +from ... import errors from ...client import AuthenticatedClient, Client from ...models.error_model import ErrorModel from ...models.export_catalogs_params import ExportCatalogsParams @@ -31,15 +32,46 @@ def _get_kwargs( def _parse_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> ErrorModel | ExportCatalogsResponse: +) -> ErrorModel | ExportCatalogsResponse | None: if response.status_code == 200: response_200 = ExportCatalogsResponse.from_dict(response.json()) return response_200 - response_default = ErrorModel.from_dict(response.json()) + if response.status_code == 400: + response_400 = ErrorModel.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorModel.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = ErrorModel.from_dict(response.json()) + + return response_403 + + if response.status_code == 409: + response_409 = ErrorModel.from_dict(response.json()) + + return response_409 + + if response.status_code == 422: + response_422 = ErrorModel.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ErrorModel.from_dict(response.json()) + + return response_500 - return response_default + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None def _build_response( diff --git a/src/tower/tower_api_client/api/default/list_apps.py b/src/tower/tower_api_client/api/default/list_apps.py index c71e0f10..ffe82f04 100644 --- a/src/tower/tower_api_client/api/default/list_apps.py +++ b/src/tower/tower_api_client/api/default/list_apps.py @@ -15,6 +15,7 @@ def _get_kwargs( *, page: int | Unset = 1, page_size: int | Unset = 20, + tag_filter: str | Unset = UNSET, query: str | Unset = UNSET, num_runs: int | Unset = 20, sort: ListAppsSort | Unset = ListAppsSort.CREATED_AT, @@ -27,6 +28,8 @@ def _get_kwargs( params["page_size"] = page_size + params["tag_filter"] = tag_filter + params["query"] = query params["num_runs"] = num_runs @@ -85,6 +88,7 @@ def sync_detailed( client: AuthenticatedClient, page: int | Unset = 1, page_size: int | Unset = 20, + tag_filter: str | Unset = UNSET, query: str | Unset = UNSET, num_runs: int | Unset = 20, sort: ListAppsSort | Unset = ListAppsSort.CREATED_AT, @@ -98,6 +102,9 @@ def sync_detailed( Args: page (int | Unset): The page number to fetch. Default: 1. page_size (int | Unset): The number of records to fetch on each page. Default: 20. + tag_filter (str | Unset): The tag query to filter apps by. Provide a serialized TagFilter + as the value for the parameter. Example: + tag_filter={"op":"eq","name":"foo","value":"bar"}. query (str | Unset): The search query to filter apps by. num_runs (int | Unset): Number of recent runs to fetch (-1 for all runs, defaults to 20) Default: 20. @@ -117,6 +124,7 @@ def sync_detailed( kwargs = _get_kwargs( page=page, page_size=page_size, + tag_filter=tag_filter, query=query, num_runs=num_runs, sort=sort, @@ -136,6 +144,7 @@ def sync( client: AuthenticatedClient, page: int | Unset = 1, page_size: int | Unset = 20, + tag_filter: str | Unset = UNSET, query: str | Unset = UNSET, num_runs: int | Unset = 20, sort: ListAppsSort | Unset = ListAppsSort.CREATED_AT, @@ -149,6 +158,9 @@ def sync( Args: page (int | Unset): The page number to fetch. Default: 1. page_size (int | Unset): The number of records to fetch on each page. Default: 20. + tag_filter (str | Unset): The tag query to filter apps by. Provide a serialized TagFilter + as the value for the parameter. Example: + tag_filter={"op":"eq","name":"foo","value":"bar"}. query (str | Unset): The search query to filter apps by. num_runs (int | Unset): Number of recent runs to fetch (-1 for all runs, defaults to 20) Default: 20. @@ -169,6 +181,7 @@ def sync( client=client, page=page, page_size=page_size, + tag_filter=tag_filter, query=query, num_runs=num_runs, sort=sort, @@ -182,6 +195,7 @@ async def asyncio_detailed( client: AuthenticatedClient, page: int | Unset = 1, page_size: int | Unset = 20, + tag_filter: str | Unset = UNSET, query: str | Unset = UNSET, num_runs: int | Unset = 20, sort: ListAppsSort | Unset = ListAppsSort.CREATED_AT, @@ -195,6 +209,9 @@ async def asyncio_detailed( Args: page (int | Unset): The page number to fetch. Default: 1. page_size (int | Unset): The number of records to fetch on each page. Default: 20. + tag_filter (str | Unset): The tag query to filter apps by. Provide a serialized TagFilter + as the value for the parameter. Example: + tag_filter={"op":"eq","name":"foo","value":"bar"}. query (str | Unset): The search query to filter apps by. num_runs (int | Unset): Number of recent runs to fetch (-1 for all runs, defaults to 20) Default: 20. @@ -214,6 +231,7 @@ async def asyncio_detailed( kwargs = _get_kwargs( page=page, page_size=page_size, + tag_filter=tag_filter, query=query, num_runs=num_runs, sort=sort, @@ -231,6 +249,7 @@ async def asyncio( client: AuthenticatedClient, page: int | Unset = 1, page_size: int | Unset = 20, + tag_filter: str | Unset = UNSET, query: str | Unset = UNSET, num_runs: int | Unset = 20, sort: ListAppsSort | Unset = ListAppsSort.CREATED_AT, @@ -244,6 +263,9 @@ async def asyncio( Args: page (int | Unset): The page number to fetch. Default: 1. page_size (int | Unset): The number of records to fetch on each page. Default: 20. + tag_filter (str | Unset): The tag query to filter apps by. Provide a serialized TagFilter + as the value for the parameter. Example: + tag_filter={"op":"eq","name":"foo","value":"bar"}. query (str | Unset): The search query to filter apps by. num_runs (int | Unset): Number of recent runs to fetch (-1 for all runs, defaults to 20) Default: 20. @@ -265,6 +287,7 @@ async def asyncio( client=client, page=page, page_size=page_size, + tag_filter=tag_filter, query=query, num_runs=num_runs, sort=sort, diff --git a/src/tower/tower_api_client/api/default/list_catalog_facts.py b/src/tower/tower_api_client/api/default/list_catalog_facts.py new file mode 100644 index 00000000..d3d452ca --- /dev/null +++ b/src/tower/tower_api_client/api/default/list_catalog_facts.py @@ -0,0 +1,232 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ...client import AuthenticatedClient, Client +from ...models.error_model import ErrorModel +from ...models.list_catalog_facts_response import ListCatalogFactsResponse +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + catalog: str, + *, + environment: str | Unset = "default", + scope: str | Unset = UNSET, + object_: str | Unset = UNSET, +) -> dict[str, Any]: + params: dict[str, Any] = {} + + params["environment"] = environment + + params["scope"] = scope + + params["object"] = object_ + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/catalogs/{catalog}/facts".format( + catalog=quote(str(catalog), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorModel | ListCatalogFactsResponse: + if response.status_code == 200: + response_200 = ListCatalogFactsResponse.from_dict(response.json()) + + return response_200 + + response_default = ErrorModel.from_dict(response.json()) + + return response_default + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorModel | ListCatalogFactsResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + catalog: str, + *, + client: AuthenticatedClient, + environment: str | Unset = "default", + scope: str | Unset = UNSET, + object_: str | Unset = UNSET, +) -> Response[ErrorModel | ListCatalogFactsResponse]: + """List catalog facts + + Lists the semantic metadata facts attached to a catalog, optionally filtered by scope and/or object + path. + + Args: + catalog (str): The name of the catalog. + environment (str | Unset): The environment of the catalog. Note that if a catalog with the + requested name doesn't exist in the requested environment, facts for the catalog with the + same name from the default environment will be returned. Default: 'default'. + scope (str | Unset): Filter facts by scope. When omitted, facts of every scope are + returned. + object_ (str | Unset): Filter facts by object path (exact match). When omitted, facts + about any object are returned. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorModel | ListCatalogFactsResponse] + """ + + kwargs = _get_kwargs( + catalog=catalog, + environment=environment, + scope=scope, + object_=object_, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + catalog: str, + *, + client: AuthenticatedClient, + environment: str | Unset = "default", + scope: str | Unset = UNSET, + object_: str | Unset = UNSET, +) -> ErrorModel | ListCatalogFactsResponse | None: + """List catalog facts + + Lists the semantic metadata facts attached to a catalog, optionally filtered by scope and/or object + path. + + Args: + catalog (str): The name of the catalog. + environment (str | Unset): The environment of the catalog. Note that if a catalog with the + requested name doesn't exist in the requested environment, facts for the catalog with the + same name from the default environment will be returned. Default: 'default'. + scope (str | Unset): Filter facts by scope. When omitted, facts of every scope are + returned. + object_ (str | Unset): Filter facts by object path (exact match). When omitted, facts + about any object are returned. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorModel | ListCatalogFactsResponse + """ + + return sync_detailed( + catalog=catalog, + client=client, + environment=environment, + scope=scope, + object_=object_, + ).parsed + + +async def asyncio_detailed( + catalog: str, + *, + client: AuthenticatedClient, + environment: str | Unset = "default", + scope: str | Unset = UNSET, + object_: str | Unset = UNSET, +) -> Response[ErrorModel | ListCatalogFactsResponse]: + """List catalog facts + + Lists the semantic metadata facts attached to a catalog, optionally filtered by scope and/or object + path. + + Args: + catalog (str): The name of the catalog. + environment (str | Unset): The environment of the catalog. Note that if a catalog with the + requested name doesn't exist in the requested environment, facts for the catalog with the + same name from the default environment will be returned. Default: 'default'. + scope (str | Unset): Filter facts by scope. When omitted, facts of every scope are + returned. + object_ (str | Unset): Filter facts by object path (exact match). When omitted, facts + about any object are returned. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorModel | ListCatalogFactsResponse] + """ + + kwargs = _get_kwargs( + catalog=catalog, + environment=environment, + scope=scope, + object_=object_, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + catalog: str, + *, + client: AuthenticatedClient, + environment: str | Unset = "default", + scope: str | Unset = UNSET, + object_: str | Unset = UNSET, +) -> ErrorModel | ListCatalogFactsResponse | None: + """List catalog facts + + Lists the semantic metadata facts attached to a catalog, optionally filtered by scope and/or object + path. + + Args: + catalog (str): The name of the catalog. + environment (str | Unset): The environment of the catalog. Note that if a catalog with the + requested name doesn't exist in the requested environment, facts for the catalog with the + same name from the default environment will be returned. Default: 'default'. + scope (str | Unset): Filter facts by scope. When omitted, facts of every scope are + returned. + object_ (str | Unset): Filter facts by object path (exact match). When omitted, facts + about any object are returned. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorModel | ListCatalogFactsResponse + """ + + return ( + await asyncio_detailed( + catalog=catalog, + client=client, + environment=environment, + scope=scope, + object_=object_, + ) + ).parsed diff --git a/src/tower/tower_api_client/api/default/list_catalogs.py b/src/tower/tower_api_client/api/default/list_catalogs.py index 84b69bdb..f881eeef 100644 --- a/src/tower/tower_api_client/api/default/list_catalogs.py +++ b/src/tower/tower_api_client/api/default/list_catalogs.py @@ -80,10 +80,11 @@ def sync_detailed( Args: page (int | Unset): The page number to fetch. Default: 1. page_size (int | Unset): The number of records to fetch on each page. Default: 20. - environment (str | Unset): The environment to filter by. When omitted, catalogs across all - environments are returned. - all_ (bool | Unset): Whether to fetch all catalogs across all environments or only for the - current environment. + environment (str | Unset): The environment of the catalogs. Catalogs from the default + environment will be returned for names that don't exist in the requested environment. When + omitted, catalogs across all environments are returned. + all_ (bool | Unset): Whether to return catalogs across all environments, without applying + default-environment inheritance or deduplication. type_ (str | Unset): Filter catalogs by type, e.g. "tower-catalog". When omitted, all catalog types are returned. @@ -126,10 +127,11 @@ def sync( Args: page (int | Unset): The page number to fetch. Default: 1. page_size (int | Unset): The number of records to fetch on each page. Default: 20. - environment (str | Unset): The environment to filter by. When omitted, catalogs across all - environments are returned. - all_ (bool | Unset): Whether to fetch all catalogs across all environments or only for the - current environment. + environment (str | Unset): The environment of the catalogs. Catalogs from the default + environment will be returned for names that don't exist in the requested environment. When + omitted, catalogs across all environments are returned. + all_ (bool | Unset): Whether to return catalogs across all environments, without applying + default-environment inheritance or deduplication. type_ (str | Unset): Filter catalogs by type, e.g. "tower-catalog". When omitted, all catalog types are returned. @@ -167,10 +169,11 @@ async def asyncio_detailed( Args: page (int | Unset): The page number to fetch. Default: 1. page_size (int | Unset): The number of records to fetch on each page. Default: 20. - environment (str | Unset): The environment to filter by. When omitted, catalogs across all - environments are returned. - all_ (bool | Unset): Whether to fetch all catalogs across all environments or only for the - current environment. + environment (str | Unset): The environment of the catalogs. Catalogs from the default + environment will be returned for names that don't exist in the requested environment. When + omitted, catalogs across all environments are returned. + all_ (bool | Unset): Whether to return catalogs across all environments, without applying + default-environment inheritance or deduplication. type_ (str | Unset): Filter catalogs by type, e.g. "tower-catalog". When omitted, all catalog types are returned. @@ -211,10 +214,11 @@ async def asyncio( Args: page (int | Unset): The page number to fetch. Default: 1. page_size (int | Unset): The number of records to fetch on each page. Default: 20. - environment (str | Unset): The environment to filter by. When omitted, catalogs across all - environments are returned. - all_ (bool | Unset): Whether to fetch all catalogs across all environments or only for the - current environment. + environment (str | Unset): The environment of the catalogs. Catalogs from the default + environment will be returned for names that don't exist in the requested environment. When + omitted, catalogs across all environments are returned. + all_ (bool | Unset): Whether to return catalogs across all environments, without applying + default-environment inheritance or deduplication. type_ (str | Unset): Filter catalogs by type, e.g. "tower-catalog". When omitted, all catalog types are returned. diff --git a/src/tower/tower_api_client/api/default/update_app.py b/src/tower/tower_api_client/api/default/update_app.py index 6e68bd0c..ed8917ac 100644 --- a/src/tower/tower_api_client/api/default/update_app.py +++ b/src/tower/tower_api_client/api/default/update_app.py @@ -4,19 +4,23 @@ import httpx +from ... import errors from ...client import AuthenticatedClient, Client from ...models.error_model import ErrorModel from ...models.update_app_params import UpdateAppParams from ...models.update_app_response import UpdateAppResponse -from ...types import Response +from ...types import UNSET, Response, Unset def _get_kwargs( name: str, *, body: UpdateAppParams, + x_tower_request_number: int | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} + if not isinstance(x_tower_request_number, Unset): + headers["X-Tower-Request-Number"] = str(x_tower_request_number) _kwargs: dict[str, Any] = { "method": "put", @@ -35,15 +39,31 @@ def _get_kwargs( def _parse_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> ErrorModel | UpdateAppResponse: +) -> ErrorModel | UpdateAppResponse | None: if response.status_code == 200: response_200 = UpdateAppResponse.from_dict(response.json()) return response_200 - response_default = ErrorModel.from_dict(response.json()) + if response.status_code == 412: + response_412 = ErrorModel.from_dict(response.json()) + + return response_412 + + if response.status_code == 422: + response_422 = ErrorModel.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ErrorModel.from_dict(response.json()) + + return response_500 - return response_default + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None def _build_response( @@ -62,6 +82,7 @@ def sync_detailed( *, client: AuthenticatedClient, body: UpdateAppParams, + x_tower_request_number: int | Unset = UNSET, ) -> Response[ErrorModel | UpdateAppResponse]: """Update app @@ -69,6 +90,9 @@ def sync_detailed( Args: name (str): The name of the App to update. + x_tower_request_number (int | Unset): Optional account-scoped monotonic sequence token + used to reject out-of-order writes. See the fence documentation for the acceptance window + and retry behavior. body (UpdateAppParams): Raises: @@ -82,6 +106,7 @@ def sync_detailed( kwargs = _get_kwargs( name=name, body=body, + x_tower_request_number=x_tower_request_number, ) response = client.get_httpx_client().request( @@ -96,6 +121,7 @@ def sync( *, client: AuthenticatedClient, body: UpdateAppParams, + x_tower_request_number: int | Unset = UNSET, ) -> ErrorModel | UpdateAppResponse | None: """Update app @@ -103,6 +129,9 @@ def sync( Args: name (str): The name of the App to update. + x_tower_request_number (int | Unset): Optional account-scoped monotonic sequence token + used to reject out-of-order writes. See the fence documentation for the acceptance window + and retry behavior. body (UpdateAppParams): Raises: @@ -117,6 +146,7 @@ def sync( name=name, client=client, body=body, + x_tower_request_number=x_tower_request_number, ).parsed @@ -125,6 +155,7 @@ async def asyncio_detailed( *, client: AuthenticatedClient, body: UpdateAppParams, + x_tower_request_number: int | Unset = UNSET, ) -> Response[ErrorModel | UpdateAppResponse]: """Update app @@ -132,6 +163,9 @@ async def asyncio_detailed( Args: name (str): The name of the App to update. + x_tower_request_number (int | Unset): Optional account-scoped monotonic sequence token + used to reject out-of-order writes. See the fence documentation for the acceptance window + and retry behavior. body (UpdateAppParams): Raises: @@ -145,6 +179,7 @@ async def asyncio_detailed( kwargs = _get_kwargs( name=name, body=body, + x_tower_request_number=x_tower_request_number, ) response = await client.get_async_httpx_client().request(**kwargs) @@ -157,6 +192,7 @@ async def asyncio( *, client: AuthenticatedClient, body: UpdateAppParams, + x_tower_request_number: int | Unset = UNSET, ) -> ErrorModel | UpdateAppResponse | None: """Update app @@ -164,6 +200,9 @@ async def asyncio( Args: name (str): The name of the App to update. + x_tower_request_number (int | Unset): Optional account-scoped monotonic sequence token + used to reject out-of-order writes. See the fence documentation for the acceptance window + and retry behavior. body (UpdateAppParams): Raises: @@ -179,5 +218,6 @@ async def asyncio( name=name, client=client, body=body, + x_tower_request_number=x_tower_request_number, ) ).parsed diff --git a/src/tower/tower_api_client/api/default/update_catalog.py b/src/tower/tower_api_client/api/default/update_catalog.py index 446a800c..136bdb82 100644 --- a/src/tower/tower_api_client/api/default/update_catalog.py +++ b/src/tower/tower_api_client/api/default/update_catalog.py @@ -41,6 +41,31 @@ def _parse_response( return response_200 + if response.status_code == 401: + response_401 = ErrorModel.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = ErrorModel.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = ErrorModel.from_dict(response.json()) + + return response_404 + + if response.status_code == 422: + response_422 = ErrorModel.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ErrorModel.from_dict(response.json()) + + return response_500 + response_default = ErrorModel.from_dict(response.json()) return response_default diff --git a/src/tower/tower_api_client/api/default/update_catalog_fact.py b/src/tower/tower_api_client/api/default/update_catalog_fact.py new file mode 100644 index 00000000..190d2c06 --- /dev/null +++ b/src/tower/tower_api_client/api/default/update_catalog_fact.py @@ -0,0 +1,225 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ...client import AuthenticatedClient, Client +from ...models.error_model import ErrorModel +from ...models.update_catalog_fact_body import UpdateCatalogFactBody +from ...models.update_catalog_fact_response import UpdateCatalogFactResponse +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + catalog: str, + name: str, + *, + body: UpdateCatalogFactBody, + environment: str | Unset = "default", +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + params: dict[str, Any] = {} + + params["environment"] = environment + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "put", + "url": "/catalogs/{catalog}/facts/{name}".format( + catalog=quote(str(catalog), safe=""), + name=quote(str(name), safe=""), + ), + "params": params, + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorModel | UpdateCatalogFactResponse: + if response.status_code == 200: + response_200 = UpdateCatalogFactResponse.from_dict(response.json()) + + return response_200 + + response_default = ErrorModel.from_dict(response.json()) + + return response_default + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorModel | UpdateCatalogFactResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + catalog: str, + name: str, + *, + client: AuthenticatedClient, + body: UpdateCatalogFactBody, + environment: str | Unset = "default", +) -> Response[ErrorModel | UpdateCatalogFactResponse]: + """Update a catalog fact + + Idempotently sets a semantic metadata fact by name: creates it when the name is new, updates it when + it already exists. An inferred write cannot overwrite an existing confirmed fact. + + Args: + catalog (str): The name of the catalog. + name (str): The name of the fact. + environment (str | Unset): Environment containing the catalog definition to update. This + operation does not fall back to default. Default: 'default'. + body (UpdateCatalogFactBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorModel | UpdateCatalogFactResponse] + """ + + kwargs = _get_kwargs( + catalog=catalog, + name=name, + body=body, + environment=environment, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + catalog: str, + name: str, + *, + client: AuthenticatedClient, + body: UpdateCatalogFactBody, + environment: str | Unset = "default", +) -> ErrorModel | UpdateCatalogFactResponse | None: + """Update a catalog fact + + Idempotently sets a semantic metadata fact by name: creates it when the name is new, updates it when + it already exists. An inferred write cannot overwrite an existing confirmed fact. + + Args: + catalog (str): The name of the catalog. + name (str): The name of the fact. + environment (str | Unset): Environment containing the catalog definition to update. This + operation does not fall back to default. Default: 'default'. + body (UpdateCatalogFactBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorModel | UpdateCatalogFactResponse + """ + + return sync_detailed( + catalog=catalog, + name=name, + client=client, + body=body, + environment=environment, + ).parsed + + +async def asyncio_detailed( + catalog: str, + name: str, + *, + client: AuthenticatedClient, + body: UpdateCatalogFactBody, + environment: str | Unset = "default", +) -> Response[ErrorModel | UpdateCatalogFactResponse]: + """Update a catalog fact + + Idempotently sets a semantic metadata fact by name: creates it when the name is new, updates it when + it already exists. An inferred write cannot overwrite an existing confirmed fact. + + Args: + catalog (str): The name of the catalog. + name (str): The name of the fact. + environment (str | Unset): Environment containing the catalog definition to update. This + operation does not fall back to default. Default: 'default'. + body (UpdateCatalogFactBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorModel | UpdateCatalogFactResponse] + """ + + kwargs = _get_kwargs( + catalog=catalog, + name=name, + body=body, + environment=environment, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + catalog: str, + name: str, + *, + client: AuthenticatedClient, + body: UpdateCatalogFactBody, + environment: str | Unset = "default", +) -> ErrorModel | UpdateCatalogFactResponse | None: + """Update a catalog fact + + Idempotently sets a semantic metadata fact by name: creates it when the name is new, updates it when + it already exists. An inferred write cannot overwrite an existing confirmed fact. + + Args: + catalog (str): The name of the catalog. + name (str): The name of the fact. + environment (str | Unset): Environment containing the catalog definition to update. This + operation does not fall back to default. Default: 'default'. + body (UpdateCatalogFactBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorModel | UpdateCatalogFactResponse + """ + + return ( + await asyncio_detailed( + catalog=catalog, + name=name, + client=client, + body=body, + environment=environment, + ) + ).parsed diff --git a/src/tower/tower_api_client/api/default/update_environment.py b/src/tower/tower_api_client/api/default/update_environment.py index c0a2f94a..993956a6 100644 --- a/src/tower/tower_api_client/api/default/update_environment.py +++ b/src/tower/tower_api_client/api/default/update_environment.py @@ -41,6 +41,36 @@ def _parse_response( return response_200 + if response.status_code == 401: + response_401 = ErrorModel.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = ErrorModel.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = ErrorModel.from_dict(response.json()) + + return response_404 + + if response.status_code == 409: + response_409 = ErrorModel.from_dict(response.json()) + + return response_409 + + if response.status_code == 422: + response_422 = ErrorModel.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ErrorModel.from_dict(response.json()) + + return response_500 + response_default = ErrorModel.from_dict(response.json()) return response_default diff --git a/src/tower/tower_api_client/api/default/update_schedule.py b/src/tower/tower_api_client/api/default/update_schedule.py index 33a68f47..8169965c 100644 --- a/src/tower/tower_api_client/api/default/update_schedule.py +++ b/src/tower/tower_api_client/api/default/update_schedule.py @@ -4,19 +4,23 @@ import httpx +from ... import errors from ...client import AuthenticatedClient, Client from ...models.error_model import ErrorModel from ...models.update_schedule_params import UpdateScheduleParams from ...models.update_schedule_response import UpdateScheduleResponse -from ...types import Response +from ...types import UNSET, Response, Unset def _get_kwargs( id_or_name: str, *, body: UpdateScheduleParams, + x_tower_request_number: int | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} + if not isinstance(x_tower_request_number, Unset): + headers["X-Tower-Request-Number"] = str(x_tower_request_number) _kwargs: dict[str, Any] = { "method": "put", @@ -35,15 +39,31 @@ def _get_kwargs( def _parse_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> ErrorModel | UpdateScheduleResponse: +) -> ErrorModel | UpdateScheduleResponse | None: if response.status_code == 200: response_200 = UpdateScheduleResponse.from_dict(response.json()) return response_200 - response_default = ErrorModel.from_dict(response.json()) + if response.status_code == 412: + response_412 = ErrorModel.from_dict(response.json()) + + return response_412 + + if response.status_code == 422: + response_422 = ErrorModel.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ErrorModel.from_dict(response.json()) + + return response_500 - return response_default + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None def _build_response( @@ -62,6 +82,7 @@ def sync_detailed( *, client: AuthenticatedClient, body: UpdateScheduleParams, + x_tower_request_number: int | Unset = UNSET, ) -> Response[ErrorModel | UpdateScheduleResponse]: """Update schedule @@ -69,6 +90,9 @@ def sync_detailed( Args: id_or_name (str): The ID or name of the schedule to update. + x_tower_request_number (int | Unset): Optional account-scoped monotonic sequence token + used to reject out-of-order writes. See the fence documentation for the acceptance window + and retry behavior. body (UpdateScheduleParams): Raises: @@ -82,6 +106,7 @@ def sync_detailed( kwargs = _get_kwargs( id_or_name=id_or_name, body=body, + x_tower_request_number=x_tower_request_number, ) response = client.get_httpx_client().request( @@ -96,6 +121,7 @@ def sync( *, client: AuthenticatedClient, body: UpdateScheduleParams, + x_tower_request_number: int | Unset = UNSET, ) -> ErrorModel | UpdateScheduleResponse | None: """Update schedule @@ -103,6 +129,9 @@ def sync( Args: id_or_name (str): The ID or name of the schedule to update. + x_tower_request_number (int | Unset): Optional account-scoped monotonic sequence token + used to reject out-of-order writes. See the fence documentation for the acceptance window + and retry behavior. body (UpdateScheduleParams): Raises: @@ -117,6 +146,7 @@ def sync( id_or_name=id_or_name, client=client, body=body, + x_tower_request_number=x_tower_request_number, ).parsed @@ -125,6 +155,7 @@ async def asyncio_detailed( *, client: AuthenticatedClient, body: UpdateScheduleParams, + x_tower_request_number: int | Unset = UNSET, ) -> Response[ErrorModel | UpdateScheduleResponse]: """Update schedule @@ -132,6 +163,9 @@ async def asyncio_detailed( Args: id_or_name (str): The ID or name of the schedule to update. + x_tower_request_number (int | Unset): Optional account-scoped monotonic sequence token + used to reject out-of-order writes. See the fence documentation for the acceptance window + and retry behavior. body (UpdateScheduleParams): Raises: @@ -145,6 +179,7 @@ async def asyncio_detailed( kwargs = _get_kwargs( id_or_name=id_or_name, body=body, + x_tower_request_number=x_tower_request_number, ) response = await client.get_async_httpx_client().request(**kwargs) @@ -157,6 +192,7 @@ async def asyncio( *, client: AuthenticatedClient, body: UpdateScheduleParams, + x_tower_request_number: int | Unset = UNSET, ) -> ErrorModel | UpdateScheduleResponse | None: """Update schedule @@ -164,6 +200,9 @@ async def asyncio( Args: id_or_name (str): The ID or name of the schedule to update. + x_tower_request_number (int | Unset): Optional account-scoped monotonic sequence token + used to reject out-of-order writes. See the fence documentation for the acceptance window + and retry behavior. body (UpdateScheduleParams): Raises: @@ -179,5 +218,6 @@ async def asyncio( id_or_name=id_or_name, client=client, body=body, + x_tower_request_number=x_tower_request_number, ) ).parsed diff --git a/src/tower/tower_api_client/api/default/vend_catalog_credentials.py b/src/tower/tower_api_client/api/default/vend_catalog_credentials.py index 90d71648..0870416c 100644 --- a/src/tower/tower_api_client/api/default/vend_catalog_credentials.py +++ b/src/tower/tower_api_client/api/default/vend_catalog_credentials.py @@ -108,7 +108,9 @@ def sync_detailed( Args: name (str): The name of the catalog. - environment (str | Unset): The environment of the catalog. Default: 'default'. + environment (str | Unset): Environment whose catalog credentials to vend. When it has no + same-named catalog, credentials for the catalog from default are vended instead. Default: + 'default'. body (VendCatalogCredentialsBody): Raises: @@ -148,7 +150,9 @@ def sync( Args: name (str): The name of the catalog. - environment (str | Unset): The environment of the catalog. Default: 'default'. + environment (str | Unset): Environment whose catalog credentials to vend. When it has no + same-named catalog, credentials for the catalog from default are vended instead. Default: + 'default'. body (VendCatalogCredentialsBody): Raises: @@ -183,7 +187,9 @@ async def asyncio_detailed( Args: name (str): The name of the catalog. - environment (str | Unset): The environment of the catalog. Default: 'default'. + environment (str | Unset): Environment whose catalog credentials to vend. When it has no + same-named catalog, credentials for the catalog from default are vended instead. Default: + 'default'. body (VendCatalogCredentialsBody): Raises: @@ -221,7 +227,9 @@ async def asyncio( Args: name (str): The name of the catalog. - environment (str | Unset): The environment of the catalog. Default: 'default'. + environment (str | Unset): Environment whose catalog credentials to vend. When it has no + same-named catalog, credentials for the catalog from default are vended instead. Default: + 'default'. body (VendCatalogCredentialsBody): Raises: diff --git a/src/tower/tower_api_client/models/__init__.py b/src/tower/tower_api_client/models/__init__.py index 956c7b07..15753bba 100644 --- a/src/tower/tower_api_client/models/__init__.py +++ b/src/tower/tower_api_client/models/__init__.py @@ -12,14 +12,25 @@ from .app_statistics import AppStatistics from .app_status import AppStatus from .app_summary import AppSummary +from .app_tag import AppTag from .app_version import AppVersion from .authentication_context import AuthenticationContext +from .batch_describe_runs_logs_params import BatchDescribeRunsLogsParams +from .batch_describe_runs_params import BatchDescribeRunsParams +from .batch_describe_runs_response import BatchDescribeRunsResponse +from .batch_error import BatchError +from .batch_run_and_links import BatchRunAndLinks from .batch_schedule_params import BatchScheduleParams from .batch_schedule_response import BatchScheduleResponse +from .batched_run_log_lines import BatchedRunLogLines from .cancel_run_response import CancelRunResponse from .catalog import Catalog from .catalog_credentials import CatalogCredentials +from .catalog_fact import CatalogFact +from .catalog_fact_confidence import CatalogFactConfidence +from .catalog_fact_scope import CatalogFactScope from .catalog_property import CatalogProperty +from .catalog_usage import CatalogUsage from .claim_device_login_ticket_params import ClaimDeviceLoginTicketParams from .claim_device_login_ticket_response import ClaimDeviceLoginTicketResponse from .create_account_params import CreateAccountParams @@ -79,7 +90,9 @@ from .describe_app_response import DescribeAppResponse from .describe_app_version_response import DescribeAppVersionResponse from .describe_authentication_context_body import DescribeAuthenticationContextBody +from .describe_catalog_fact_response import DescribeCatalogFactResponse from .describe_catalog_response import DescribeCatalogResponse +from .describe_catalog_usage_response import DescribeCatalogUsageResponse from .describe_device_login_session_response import DescribeDeviceLoginSessionResponse from .describe_email_preferences_body import DescribeEmailPreferencesBody from .describe_environment_response import DescribeEnvironmentResponse @@ -131,6 +144,7 @@ from .list_apps_filter import ListAppsFilter from .list_apps_response import ListAppsResponse from .list_apps_sort import ListAppsSort +from .list_catalog_facts_response import ListCatalogFactsResponse from .list_catalogs_response import ListCatalogsResponse from .list_environments_response import ListEnvironmentsResponse from .list_guests_response import ListGuestsResponse @@ -148,6 +162,7 @@ from .list_teams_response import ListTeamsResponse from .list_webhooks_response import ListWebhooksResponse from .organization import Organization +from .organization_storage_usage import OrganizationStorageUsage from .organization_usage import OrganizationUsage from .pagination import Pagination from .parameter import Parameter @@ -161,6 +176,7 @@ from .resend_team_invitation_params import ResendTeamInvitationParams from .resend_team_invitation_response import ResendTeamInvitationResponse from .run import Run +from .run_and_links import RunAndLinks from .run_app_initiator_data import RunAppInitiatorData from .run_app_initiator_data_type import RunAppInitiatorDataType from .run_app_params import RunAppParams @@ -173,6 +189,7 @@ from .run_graph_node import RunGraphNode from .run_graph_run_id import RunGraphRunID from .run_initiator import RunInitiator +from .run_links import RunLinks from .run_log_line import RunLogLine from .run_log_line_channel import RunLogLineChannel from .run_parameter import RunParameter @@ -210,6 +227,8 @@ from .stream_run_logs_event_warning import StreamRunLogsEventWarning from .stream_shouldertaps_event_shouldertap import StreamShouldertapsEventShouldertap from .stream_shouldertaps_event_warning import StreamShouldertapsEventWarning +from .tag_filter import TagFilter +from .tag_filter_op import TagFilterOp from .team import Team from .team_invitation import TeamInvitation from .team_membership import TeamMembership @@ -223,6 +242,10 @@ from .update_app_environment_response import UpdateAppEnvironmentResponse from .update_app_params import UpdateAppParams from .update_app_response import UpdateAppResponse +from .update_catalog_fact_body import UpdateCatalogFactBody +from .update_catalog_fact_body_confidence import UpdateCatalogFactBodyConfidence +from .update_catalog_fact_body_scope import UpdateCatalogFactBodyScope +from .update_catalog_fact_response import UpdateCatalogFactResponse from .update_catalog_params import UpdateCatalogParams from .update_catalog_response import UpdateCatalogResponse from .update_email_preferences_body import UpdateEmailPreferencesBody @@ -276,14 +299,25 @@ "AppStatistics", "AppStatus", "AppSummary", + "AppTag", "AppVersion", "AuthenticationContext", + "BatchDescribeRunsLogsParams", + "BatchDescribeRunsParams", + "BatchDescribeRunsResponse", + "BatchedRunLogLines", + "BatchError", + "BatchRunAndLinks", "BatchScheduleParams", "BatchScheduleResponse", "CancelRunResponse", "Catalog", "CatalogCredentials", + "CatalogFact", + "CatalogFactConfidence", + "CatalogFactScope", "CatalogProperty", + "CatalogUsage", "ClaimDeviceLoginTicketParams", "ClaimDeviceLoginTicketResponse", "CreateAccountParams", @@ -343,7 +377,9 @@ "DescribeAppResponse", "DescribeAppVersionResponse", "DescribeAuthenticationContextBody", + "DescribeCatalogFactResponse", "DescribeCatalogResponse", + "DescribeCatalogUsageResponse", "DescribeDeviceLoginSessionResponse", "DescribeEmailPreferencesBody", "DescribeEnvironmentResponse", @@ -391,6 +427,7 @@ "ListAppsResponse", "ListAppsSort", "ListAppVersionsResponse", + "ListCatalogFactsResponse", "ListCatalogsResponse", "ListEnvironmentsResponse", "ListGuestsResponse", @@ -408,6 +445,7 @@ "ListTeamsResponse", "ListWebhooksResponse", "Organization", + "OrganizationStorageUsage", "OrganizationUsage", "Pagination", "Parameter", @@ -421,6 +459,7 @@ "ResendTeamInvitationParams", "ResendTeamInvitationResponse", "Run", + "RunAndLinks", "RunAppInitiatorData", "RunAppInitiatorDataType", "RunAppParams", @@ -433,6 +472,7 @@ "RunGraphNode", "RunGraphRunID", "RunInitiator", + "RunLinks", "RunLogLine", "RunLogLineChannel", "Runner", @@ -470,6 +510,8 @@ "StreamRunLogsEventWarning", "StreamShouldertapsEventShouldertap", "StreamShouldertapsEventWarning", + "TagFilter", + "TagFilterOp", "Team", "TeamInvitation", "TeamMembership", @@ -483,6 +525,10 @@ "UpdateAppEnvironmentResponse", "UpdateAppParams", "UpdateAppResponse", + "UpdateCatalogFactBody", + "UpdateCatalogFactBodyConfidence", + "UpdateCatalogFactBodyScope", + "UpdateCatalogFactResponse", "UpdateCatalogParams", "UpdateCatalogResponse", "UpdateEmailPreferencesBody", diff --git a/src/tower/tower_api_client/models/app.py b/src/tower/tower_api_client/models/app.py index 73d7656b..6dc69873 100644 --- a/src/tower/tower_api_client/models/app.py +++ b/src/tower/tower_api_client/models/app.py @@ -12,6 +12,7 @@ from ..types import UNSET, Unset if TYPE_CHECKING: + from ..models.app_tag import AppTag from ..models.run import Run from ..models.run_results import RunResults from ..models.run_retry_policy import RunRetryPolicy @@ -26,6 +27,7 @@ class App: Attributes: created_at (datetime.datetime): The date and time this app was created. health_status (AppHealthStatus): This property is deprecated. It will always be 'healthy'. + is_example (bool): Whether this app was deployed from the Tower examples catalog. is_externally_accessible (bool): name (str): The name of the app. next_run_at (datetime.datetime | None): The next time this app will run as part of it's schedule, null if none. @@ -43,10 +45,12 @@ class App: slug (str | Unset): This property is deprecated. Use name instead. status (AppStatus | Unset): The status of the app. subdomain (str | Unset): The subdomain that this app is accessible via. Must be externally accessible first. + tags (list[AppTag] | Unset): The tags applied to the app. """ created_at: datetime.datetime health_status: AppHealthStatus + is_example: bool is_externally_accessible: bool name: str next_run_at: datetime.datetime | None @@ -62,12 +66,15 @@ class App: slug: str | Unset = UNSET status: AppStatus | Unset = UNSET subdomain: str | Unset = UNSET + tags: list[AppTag] | Unset = UNSET def to_dict(self) -> dict[str, Any]: created_at = self.created_at.isoformat() health_status = self.health_status.value + is_example = self.is_example + is_externally_accessible = self.is_externally_accessible name = self.name @@ -112,12 +119,20 @@ def to_dict(self) -> dict[str, Any]: subdomain = self.subdomain + tags: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = [] + for tags_item_data in self.tags: + tags_item = tags_item_data.to_dict() + tags.append(tags_item) + field_dict: dict[str, Any] = {} field_dict.update( { "created_at": created_at, "health_status": health_status, + "is_example": is_example, "is_externally_accessible": is_externally_accessible, "name": name, "next_run_at": next_run_at, @@ -141,11 +156,14 @@ def to_dict(self) -> dict[str, Any]: field_dict["status"] = status if subdomain is not UNSET: field_dict["subdomain"] = subdomain + if tags is not UNSET: + field_dict["tags"] = tags return field_dict @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.app_tag import AppTag from ..models.run import Run from ..models.run_results import RunResults from ..models.run_retry_policy import RunRetryPolicy @@ -155,6 +173,8 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: health_status = AppHealthStatus(d.pop("health_status")) + is_example = d.pop("is_example") + is_externally_accessible = d.pop("is_externally_accessible") name = d.pop("name") @@ -228,9 +248,19 @@ def _parse_version(data: object) -> None | str: subdomain = d.pop("subdomain", UNSET) + _tags = d.pop("tags", UNSET) + tags: list[AppTag] | Unset = UNSET + if _tags is not UNSET: + tags = [] + for tags_item_data in _tags: + tags_item = AppTag.from_dict(tags_item_data) + + tags.append(tags_item) + app = cls( created_at=created_at, health_status=health_status, + is_example=is_example, is_externally_accessible=is_externally_accessible, name=name, next_run_at=next_run_at, @@ -246,6 +276,7 @@ def _parse_version(data: object) -> None | str: slug=slug, status=status, subdomain=subdomain, + tags=tags, ) return app diff --git a/src/tower/tower_api_client/models/app_tag.py b/src/tower/tower_api_client/models/app_tag.py new file mode 100644 index 00000000..79fdb7ad --- /dev/null +++ b/src/tower/tower_api_client/models/app_tag.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define + +T = TypeVar("T", bound="AppTag") + + +@_attrs_define +class AppTag: + """ + Attributes: + name (str): + value (str): + """ + + name: str + value: str + + def to_dict(self) -> dict[str, Any]: + name = self.name + + value = self.value + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "name": name, + "value": value, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + name = d.pop("name") + + value = d.pop("value") + + app_tag = cls( + name=name, + value=value, + ) + + return app_tag diff --git a/src/tower/tower_api_client/models/batch_describe_runs_logs_params.py b/src/tower/tower_api_client/models/batch_describe_runs_logs_params.py new file mode 100644 index 00000000..9d558855 --- /dev/null +++ b/src/tower/tower_api_client/models/batch_describe_runs_logs_params.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from dateutil.parser import isoparse + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="BatchDescribeRunsLogsParams") + + +@_attrs_define +class BatchDescribeRunsLogsParams: + """ + Attributes: + name (str): The name of the app to describe the run for. + seq (int): The number of the run to describe. + head (int | None | Unset): Return only the first N log lines. Cannot be combined with tail. + start_at (datetime.datetime | None | Unset): Fetch logs from this timestamp onwards (inclusive). + tail (int | None | Unset): Return only the last N log lines. Cannot be combined with head. + """ + + name: str + seq: int + head: int | None | Unset = UNSET + start_at: datetime.datetime | None | Unset = UNSET + tail: int | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + name = self.name + + seq = self.seq + + head: int | None | Unset + if isinstance(self.head, Unset): + head = UNSET + else: + head = self.head + + start_at: None | str | Unset + if isinstance(self.start_at, Unset): + start_at = UNSET + elif isinstance(self.start_at, datetime.datetime): + start_at = self.start_at.isoformat() + else: + start_at = self.start_at + + tail: int | None | Unset + if isinstance(self.tail, Unset): + tail = UNSET + else: + tail = self.tail + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "name": name, + "seq": seq, + } + ) + if head is not UNSET: + field_dict["head"] = head + if start_at is not UNSET: + field_dict["start_at"] = start_at + if tail is not UNSET: + field_dict["tail"] = tail + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + name = d.pop("name") + + seq = d.pop("seq") + + def _parse_head(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + head = _parse_head(d.pop("head", UNSET)) + + def _parse_start_at(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + start_at_type_0 = isoparse(data) + + return start_at_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + start_at = _parse_start_at(d.pop("start_at", UNSET)) + + def _parse_tail(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + tail = _parse_tail(d.pop("tail", UNSET)) + + batch_describe_runs_logs_params = cls( + name=name, + seq=seq, + head=head, + start_at=start_at, + tail=tail, + ) + + return batch_describe_runs_logs_params diff --git a/src/tower/tower_api_client/models/batch_describe_runs_params.py b/src/tower/tower_api_client/models/batch_describe_runs_params.py new file mode 100644 index 00000000..66fe7b53 --- /dev/null +++ b/src/tower/tower_api_client/models/batch_describe_runs_params.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define + +T = TypeVar("T", bound="BatchDescribeRunsParams") + + +@_attrs_define +class BatchDescribeRunsParams: + """ + Attributes: + name (str): The name of the app to describe the run for. + seq (int): The number of the run to describe. + """ + + name: str + seq: int + + def to_dict(self) -> dict[str, Any]: + name = self.name + + seq = self.seq + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "name": name, + "seq": seq, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + name = d.pop("name") + + seq = d.pop("seq") + + batch_describe_runs_params = cls( + name=name, + seq=seq, + ) + + return batch_describe_runs_params diff --git a/src/tower/tower_api_client/models/batch_describe_runs_response.py b/src/tower/tower_api_client/models/batch_describe_runs_response.py new file mode 100644 index 00000000..5b23963c --- /dev/null +++ b/src/tower/tower_api_client/models/batch_describe_runs_response.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.batch_run_and_links import BatchRunAndLinks + + +T = TypeVar("T", bound="BatchDescribeRunsResponse") + + +@_attrs_define +class BatchDescribeRunsResponse: + """ + Attributes: + runs (list[BatchRunAndLinks]): + schema (str | Unset): A URL to the JSON Schema for this object. Example: + https://api.tower.dev/v1/schemas/BatchDescribeRunsResponse.json. + """ + + runs: list[BatchRunAndLinks] + schema: str | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + runs = [] + for runs_item_data in self.runs: + runs_item = runs_item_data.to_dict() + runs.append(runs_item) + + schema = self.schema + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "runs": runs, + } + ) + if schema is not UNSET: + field_dict["$schema"] = schema + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.batch_run_and_links import BatchRunAndLinks + + d = dict(src_dict) + runs = [] + _runs = d.pop("runs") + for runs_item_data in _runs: + runs_item = BatchRunAndLinks.from_dict(runs_item_data) + + runs.append(runs_item) + + schema = d.pop("$schema", UNSET) + + batch_describe_runs_response = cls( + runs=runs, + schema=schema, + ) + + return batch_describe_runs_response diff --git a/src/tower/tower_api_client/models/batch_error.py b/src/tower/tower_api_client/models/batch_error.py new file mode 100644 index 00000000..2b211ba3 --- /dev/null +++ b/src/tower/tower_api_client/models/batch_error.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define + +T = TypeVar("T", bound="BatchError") + + +@_attrs_define +class BatchError: + """ + Attributes: + message (str): + """ + + message: str + + def to_dict(self) -> dict[str, Any]: + message = self.message + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "message": message, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + message = d.pop("message") + + batch_error = cls( + message=message, + ) + + return batch_error diff --git a/src/tower/tower_api_client/models/batch_run_and_links.py b/src/tower/tower_api_client/models/batch_run_and_links.py new file mode 100644 index 00000000..25cc598d --- /dev/null +++ b/src/tower/tower_api_client/models/batch_run_and_links.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.batch_error import BatchError + from ..models.run_and_links import RunAndLinks + + +T = TypeVar("T", bound="BatchRunAndLinks") + + +@_attrs_define +class BatchRunAndLinks: + """ + Attributes: + data (RunAndLinks | Unset): + error (BatchError | Unset): + """ + + data: RunAndLinks | Unset = UNSET + error: BatchError | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + data: dict[str, Any] | Unset = UNSET + if not isinstance(self.data, Unset): + data = self.data.to_dict() + + error: dict[str, Any] | Unset = UNSET + if not isinstance(self.error, Unset): + error = self.error.to_dict() + + field_dict: dict[str, Any] = {} + + field_dict.update({}) + if data is not UNSET: + field_dict["data"] = data + if error is not UNSET: + field_dict["error"] = error + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.batch_error import BatchError + from ..models.run_and_links import RunAndLinks + + d = dict(src_dict) + _data = d.pop("data", UNSET) + data: RunAndLinks | Unset + if isinstance(_data, Unset): + data = UNSET + else: + data = RunAndLinks.from_dict(_data) + + _error = d.pop("error", UNSET) + error: BatchError | Unset + if isinstance(_error, Unset): + error = UNSET + else: + error = BatchError.from_dict(_error) + + batch_run_and_links = cls( + data=data, + error=error, + ) + + return batch_run_and_links diff --git a/src/tower/tower_api_client/models/batched_run_log_lines.py b/src/tower/tower_api_client/models/batched_run_log_lines.py new file mode 100644 index 00000000..8e2fa515 --- /dev/null +++ b/src/tower/tower_api_client/models/batched_run_log_lines.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.run_log_line import RunLogLine + + +T = TypeVar("T", bound="BatchedRunLogLines") + + +@_attrs_define +class BatchedRunLogLines: + """ + Attributes: + error (None | str | Unset): + log_lines (list[RunLogLine] | Unset): + """ + + error: None | str | Unset = UNSET + log_lines: list[RunLogLine] | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + error: None | str | Unset + if isinstance(self.error, Unset): + error = UNSET + else: + error = self.error + + log_lines: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.log_lines, Unset): + log_lines = [] + for log_lines_item_data in self.log_lines: + log_lines_item = log_lines_item_data.to_dict() + log_lines.append(log_lines_item) + + field_dict: dict[str, Any] = {} + + field_dict.update({}) + if error is not UNSET: + field_dict["error"] = error + if log_lines is not UNSET: + field_dict["log_lines"] = log_lines + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.run_log_line import RunLogLine + + d = dict(src_dict) + + def _parse_error(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + error = _parse_error(d.pop("error", UNSET)) + + _log_lines = d.pop("log_lines", UNSET) + log_lines: list[RunLogLine] | Unset = UNSET + if _log_lines is not UNSET: + log_lines = [] + for log_lines_item_data in _log_lines: + log_lines_item = RunLogLine.from_dict(log_lines_item_data) + + log_lines.append(log_lines_item) + + batched_run_log_lines = cls( + error=error, + log_lines=log_lines, + ) + + return batched_run_log_lines diff --git a/src/tower/tower_api_client/models/catalog.py b/src/tower/tower_api_client/models/catalog.py index ab6e6e2d..fdaaab5f 100644 --- a/src/tower/tower_api_client/models/catalog.py +++ b/src/tower/tower_api_client/models/catalog.py @@ -21,7 +21,7 @@ class Catalog: """ Attributes: created_at (datetime.datetime): - environment (str): + environment (str): Environment containing the catalog definition. name (str): properties (list[CatalogProperty]): type_ (str): diff --git a/src/tower/tower_api_client/models/catalog_fact.py b/src/tower/tower_api_client/models/catalog_fact.py new file mode 100644 index 00000000..6db9bb1c --- /dev/null +++ b/src/tower/tower_api_client/models/catalog_fact.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from dateutil.parser import isoparse + +from ..models.catalog_fact_confidence import CatalogFactConfidence +from ..models.catalog_fact_scope import CatalogFactScope +from ..types import UNSET, Unset + +T = TypeVar("T", bound="CatalogFact") + + +@_attrs_define +class CatalogFact: + """ + Attributes: + confidence (CatalogFactConfidence): How trustworthy the fact is. + created_at (datetime.datetime): + name (str): The natural key of the fact within the catalog. + object_ (str): Descriptive path to what the fact is about, e.g. "bronze.runs.deleted_at". Empty for catalog- + scoped facts. + scope (CatalogFactScope): What kind of object the fact is about. + statement (str): The human-readable meaning of the fact. + updated_at (datetime.datetime): + body (Any | Unset): Optional structured payload (SQL, unit, enum values). + source (str | Unset): Where the fact came from (agent id, user, ...). + """ + + confidence: CatalogFactConfidence + created_at: datetime.datetime + name: str + object_: str + scope: CatalogFactScope + statement: str + updated_at: datetime.datetime + body: Any | Unset = UNSET + source: str | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + confidence = self.confidence.value + + created_at = self.created_at.isoformat() + + name = self.name + + object_ = self.object_ + + scope = self.scope.value + + statement = self.statement + + updated_at = self.updated_at.isoformat() + + body = self.body + + source = self.source + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "confidence": confidence, + "created_at": created_at, + "name": name, + "object": object_, + "scope": scope, + "statement": statement, + "updated_at": updated_at, + } + ) + if body is not UNSET: + field_dict["body"] = body + if source is not UNSET: + field_dict["source"] = source + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + confidence = CatalogFactConfidence(d.pop("confidence")) + + created_at = isoparse(d.pop("created_at")) + + name = d.pop("name") + + object_ = d.pop("object") + + scope = CatalogFactScope(d.pop("scope")) + + statement = d.pop("statement") + + updated_at = isoparse(d.pop("updated_at")) + + body = d.pop("body", UNSET) + + source = d.pop("source", UNSET) + + catalog_fact = cls( + confidence=confidence, + created_at=created_at, + name=name, + object_=object_, + scope=scope, + statement=statement, + updated_at=updated_at, + body=body, + source=source, + ) + + return catalog_fact diff --git a/src/tower/tower_api_client/models/catalog_fact_confidence.py b/src/tower/tower_api_client/models/catalog_fact_confidence.py new file mode 100644 index 00000000..051d428f --- /dev/null +++ b/src/tower/tower_api_client/models/catalog_fact_confidence.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class CatalogFactConfidence(str, Enum): + CONFIRMED = "confirmed" + HEURISTIC = "heuristic" + INFERRED = "inferred" + + def __str__(self) -> str: + return str(self.value) diff --git a/src/tower/tower_api_client/models/catalog_fact_scope.py b/src/tower/tower_api_client/models/catalog_fact_scope.py new file mode 100644 index 00000000..4793be33 --- /dev/null +++ b/src/tower/tower_api_client/models/catalog_fact_scope.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class CatalogFactScope(str, Enum): + CATALOG = "catalog" + COLUMN = "column" + METRIC = "metric" + NAMESPACE = "namespace" + TABLE = "table" + + def __str__(self) -> str: + return str(self.value) diff --git a/src/tower/tower_api_client/models/catalog_usage.py b/src/tower/tower_api_client/models/catalog_usage.py new file mode 100644 index 00000000..ca554186 --- /dev/null +++ b/src/tower/tower_api_client/models/catalog_usage.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from dateutil.parser import isoparse + +T = TypeVar("T", bound="CatalogUsage") + + +@_attrs_define +class CatalogUsage: + """ + Attributes: + measured_at (datetime.datetime | None): When this value was measured. Null when metering is unconfigured, + disabled, or failed without a cached value. + total_bytes (int): Physical bytes stored by this Tower-managed catalog, including Iceberg metadata and not-yet- + compacted snapshot history. + """ + + measured_at: datetime.datetime | None + total_bytes: int + + def to_dict(self) -> dict[str, Any]: + measured_at: None | str + if isinstance(self.measured_at, datetime.datetime): + measured_at = self.measured_at.isoformat() + else: + measured_at = self.measured_at + + total_bytes = self.total_bytes + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "measured_at": measured_at, + "total_bytes": total_bytes, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + + def _parse_measured_at(data: object) -> datetime.datetime | None: + if data is None: + return data + try: + if not isinstance(data, str): + raise TypeError() + measured_at_type_0 = isoparse(data) + + return measured_at_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None, data) + + measured_at = _parse_measured_at(d.pop("measured_at")) + + total_bytes = d.pop("total_bytes") + + catalog_usage = cls( + measured_at=measured_at, + total_bytes=total_bytes, + ) + + return catalog_usage diff --git a/src/tower/tower_api_client/models/create_app_params.py b/src/tower/tower_api_client/models/create_app_params.py index fdf0e085..c219bd33 100644 --- a/src/tower/tower_api_client/models/create_app_params.py +++ b/src/tower/tower_api_client/models/create_app_params.py @@ -1,12 +1,17 @@ from __future__ import annotations from collections.abc import Mapping -from typing import Any, TypeVar, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast from attrs import define as _attrs_define from ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.app_tag import AppTag + from ..models.run_retry_policy import RunRetryPolicy + + T = TypeVar("T", bound="CreateAppParams") @@ -19,18 +24,28 @@ class CreateAppParams: https://api.tower.dev/v1/schemas/CreateAppParams.json. is_externally_accessible (bool | Unset): Indicates that web traffic should be routed to this app and that its runs should get a hostname assigned to it. Default: False. + pending_timeout (int | None | Unset): The amount of time in seconds that runs of this app can stay in pending + state before being marked as failed. + retry_policy (RunRetryPolicy | Unset): + running_timeout (int | None | Unset): The amount of time in seconds that runs of this app can stay in running + state before being marked as failed. short_description (str | Unset): A description of the app. slug (str | Unset): The slug of the app. Legacy CLI will send it but we don't need it. subdomain (None | str | Unset): The subdomain this app is accessible under. Requires is_externally_accessible to be true. + tags (list[AppTag] | Unset): The tags for this app. """ name: str schema: str | Unset = UNSET is_externally_accessible: bool | Unset = False + pending_timeout: int | None | Unset = UNSET + retry_policy: RunRetryPolicy | Unset = UNSET + running_timeout: int | None | Unset = UNSET short_description: str | Unset = UNSET slug: str | Unset = UNSET subdomain: None | str | Unset = UNSET + tags: list[AppTag] | Unset = UNSET def to_dict(self) -> dict[str, Any]: name = self.name @@ -39,6 +54,22 @@ def to_dict(self) -> dict[str, Any]: is_externally_accessible = self.is_externally_accessible + pending_timeout: int | None | Unset + if isinstance(self.pending_timeout, Unset): + pending_timeout = UNSET + else: + pending_timeout = self.pending_timeout + + retry_policy: dict[str, Any] | Unset = UNSET + if not isinstance(self.retry_policy, Unset): + retry_policy = self.retry_policy.to_dict() + + running_timeout: int | None | Unset + if isinstance(self.running_timeout, Unset): + running_timeout = UNSET + else: + running_timeout = self.running_timeout + short_description = self.short_description slug = self.slug @@ -49,6 +80,13 @@ def to_dict(self) -> dict[str, Any]: else: subdomain = self.subdomain + tags: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = [] + for tags_item_data in self.tags: + tags_item = tags_item_data.to_dict() + tags.append(tags_item) + field_dict: dict[str, Any] = {} field_dict.update( @@ -60,17 +98,28 @@ def to_dict(self) -> dict[str, Any]: field_dict["$schema"] = schema if is_externally_accessible is not UNSET: field_dict["is_externally_accessible"] = is_externally_accessible + if pending_timeout is not UNSET: + field_dict["pending_timeout"] = pending_timeout + if retry_policy is not UNSET: + field_dict["retry_policy"] = retry_policy + if running_timeout is not UNSET: + field_dict["running_timeout"] = running_timeout if short_description is not UNSET: field_dict["short_description"] = short_description if slug is not UNSET: field_dict["slug"] = slug if subdomain is not UNSET: field_dict["subdomain"] = subdomain + if tags is not UNSET: + field_dict["tags"] = tags return field_dict @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.app_tag import AppTag + from ..models.run_retry_policy import RunRetryPolicy + d = dict(src_dict) name = d.pop("name") @@ -78,6 +127,31 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: is_externally_accessible = d.pop("is_externally_accessible", UNSET) + def _parse_pending_timeout(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + pending_timeout = _parse_pending_timeout(d.pop("pending_timeout", UNSET)) + + _retry_policy = d.pop("retry_policy", UNSET) + retry_policy: RunRetryPolicy | Unset + if isinstance(_retry_policy, Unset): + retry_policy = UNSET + else: + retry_policy = RunRetryPolicy.from_dict(_retry_policy) + + def _parse_running_timeout(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + running_timeout = _parse_running_timeout(d.pop("running_timeout", UNSET)) + short_description = d.pop("short_description", UNSET) slug = d.pop("slug", UNSET) @@ -91,13 +165,26 @@ def _parse_subdomain(data: object) -> None | str | Unset: subdomain = _parse_subdomain(d.pop("subdomain", UNSET)) + _tags = d.pop("tags", UNSET) + tags: list[AppTag] | Unset = UNSET + if _tags is not UNSET: + tags = [] + for tags_item_data in _tags: + tags_item = AppTag.from_dict(tags_item_data) + + tags.append(tags_item) + create_app_params = cls( name=name, schema=schema, is_externally_accessible=is_externally_accessible, + pending_timeout=pending_timeout, + retry_policy=retry_policy, + running_timeout=running_timeout, short_description=short_description, slug=slug, subdomain=subdomain, + tags=tags, ) return create_app_params diff --git a/src/tower/tower_api_client/models/describe_catalog_fact_response.py b/src/tower/tower_api_client/models/describe_catalog_fact_response.py new file mode 100644 index 00000000..1c060caa --- /dev/null +++ b/src/tower/tower_api_client/models/describe_catalog_fact_response.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.catalog_fact import CatalogFact + + +T = TypeVar("T", bound="DescribeCatalogFactResponse") + + +@_attrs_define +class DescribeCatalogFactResponse: + """ + Attributes: + environment (str): Environment containing the catalog definition. + fact (CatalogFact): + schema (str | Unset): A URL to the JSON Schema for this object. Example: + https://api.tower.dev/v1/schemas/DescribeCatalogFactResponse.json. + """ + + environment: str + fact: CatalogFact + schema: str | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + environment = self.environment + + fact = self.fact.to_dict() + + schema = self.schema + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "environment": environment, + "fact": fact, + } + ) + if schema is not UNSET: + field_dict["$schema"] = schema + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.catalog_fact import CatalogFact + + d = dict(src_dict) + environment = d.pop("environment") + + fact = CatalogFact.from_dict(d.pop("fact")) + + schema = d.pop("$schema", UNSET) + + describe_catalog_fact_response = cls( + environment=environment, + fact=fact, + schema=schema, + ) + + return describe_catalog_fact_response diff --git a/src/tower/tower_api_client/models/describe_catalog_usage_response.py b/src/tower/tower_api_client/models/describe_catalog_usage_response.py new file mode 100644 index 00000000..7c2548b8 --- /dev/null +++ b/src/tower/tower_api_client/models/describe_catalog_usage_response.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.catalog_usage import CatalogUsage + + +T = TypeVar("T", bound="DescribeCatalogUsageResponse") + + +@_attrs_define +class DescribeCatalogUsageResponse: + """ + Attributes: + environment (str): Environment containing the catalog definition. + usage (CatalogUsage): + schema (str | Unset): A URL to the JSON Schema for this object. Example: + https://api.tower.dev/v1/schemas/DescribeCatalogUsageResponse.json. + """ + + environment: str + usage: CatalogUsage + schema: str | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + environment = self.environment + + usage = self.usage.to_dict() + + schema = self.schema + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "environment": environment, + "usage": usage, + } + ) + if schema is not UNSET: + field_dict["$schema"] = schema + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.catalog_usage import CatalogUsage + + d = dict(src_dict) + environment = d.pop("environment") + + usage = CatalogUsage.from_dict(d.pop("usage")) + + schema = d.pop("$schema", UNSET) + + describe_catalog_usage_response = cls( + environment=environment, + usage=usage, + schema=schema, + ) + + return describe_catalog_usage_response diff --git a/src/tower/tower_api_client/models/export_catalogs_params.py b/src/tower/tower_api_client/models/export_catalogs_params.py index 8183ce47..6d5438ec 100644 --- a/src/tower/tower_api_client/models/export_catalogs_params.py +++ b/src/tower/tower_api_client/models/export_catalogs_params.py @@ -14,8 +14,10 @@ class ExportCatalogsParams: """ Attributes: - all_ (bool): Whether to fetch all catalogs or only the ones in the supplied environment. Default: False. - environment (str): The environment to filter by. Default: 'default'. + all_ (bool): Whether to export catalogs across all environments, without applying default-environment + inheritance or deduplication. Default: False. + environment (str): Environment whose catalogs to export when all is false. When it has no same-named catalog, a + catalog from default is selected instead. Default: 'default'. page (int): The page number to fetch. Default: 1. page_size (int): The number of records to fetch on each page. Default: 20. public_key (str): The PEM-encoded public key you want to use to encrypt sensitive catalog properties. diff --git a/src/tower/tower_api_client/models/exported_catalog.py b/src/tower/tower_api_client/models/exported_catalog.py index 0a18c5db..9b1c7101 100644 --- a/src/tower/tower_api_client/models/exported_catalog.py +++ b/src/tower/tower_api_client/models/exported_catalog.py @@ -21,7 +21,7 @@ class ExportedCatalog: """ Attributes: created_at (datetime.datetime): - environment (str): + environment (str): Environment containing the catalog definition. name (str): properties (list[ExportedCatalogProperty]): type_ (str): diff --git a/src/tower/tower_api_client/models/list_catalog_facts_response.py b/src/tower/tower_api_client/models/list_catalog_facts_response.py new file mode 100644 index 00000000..7ca62354 --- /dev/null +++ b/src/tower/tower_api_client/models/list_catalog_facts_response.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.catalog_fact import CatalogFact + + +T = TypeVar("T", bound="ListCatalogFactsResponse") + + +@_attrs_define +class ListCatalogFactsResponse: + """ + Attributes: + environment (str): Environment containing the catalog definition. + facts (list[CatalogFact]): + schema (str | Unset): A URL to the JSON Schema for this object. Example: + https://api.tower.dev/v1/schemas/ListCatalogFactsResponse.json. + """ + + environment: str + facts: list[CatalogFact] + schema: str | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + environment = self.environment + + facts = [] + for facts_item_data in self.facts: + facts_item = facts_item_data.to_dict() + facts.append(facts_item) + + schema = self.schema + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "environment": environment, + "facts": facts, + } + ) + if schema is not UNSET: + field_dict["$schema"] = schema + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.catalog_fact import CatalogFact + + d = dict(src_dict) + environment = d.pop("environment") + + facts = [] + _facts = d.pop("facts") + for facts_item_data in _facts: + facts_item = CatalogFact.from_dict(facts_item_data) + + facts.append(facts_item) + + schema = d.pop("$schema", UNSET) + + list_catalog_facts_response = cls( + environment=environment, + facts=facts, + schema=schema, + ) + + return list_catalog_facts_response diff --git a/src/tower/tower_api_client/models/organization_storage_usage.py b/src/tower/tower_api_client/models/organization_storage_usage.py new file mode 100644 index 00000000..beb8473b --- /dev/null +++ b/src/tower/tower_api_client/models/organization_storage_usage.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from dateutil.parser import isoparse + +T = TypeVar("T", bound="OrganizationStorageUsage") + + +@_attrs_define +class OrganizationStorageUsage: + """ + Attributes: + measured_at (datetime.datetime | None): When the total was measured. Null when no measurement is available. + total_bytes (int): Physical bytes across the organization's Tower-managed catalogs, including Iceberg metadata + and not-yet-compacted snapshot history. + """ + + measured_at: datetime.datetime | None + total_bytes: int + + def to_dict(self) -> dict[str, Any]: + measured_at: None | str + if isinstance(self.measured_at, datetime.datetime): + measured_at = self.measured_at.isoformat() + else: + measured_at = self.measured_at + + total_bytes = self.total_bytes + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "measured_at": measured_at, + "total_bytes": total_bytes, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + + def _parse_measured_at(data: object) -> datetime.datetime | None: + if data is None: + return data + try: + if not isinstance(data, str): + raise TypeError() + measured_at_type_0 = isoparse(data) + + return measured_at_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None, data) + + measured_at = _parse_measured_at(d.pop("measured_at")) + + total_bytes = d.pop("total_bytes") + + organization_storage_usage = cls( + measured_at=measured_at, + total_bytes=total_bytes, + ) + + return organization_storage_usage diff --git a/src/tower/tower_api_client/models/organization_usage.py b/src/tower/tower_api_client/models/organization_usage.py index 74bda4e3..9591c2f7 100644 --- a/src/tower/tower_api_client/models/organization_usage.py +++ b/src/tower/tower_api_client/models/organization_usage.py @@ -8,6 +8,7 @@ from ..types import UNSET, Unset if TYPE_CHECKING: + from ..models.organization_storage_usage import OrganizationStorageUsage from ..models.usage_limit import UsageLimit @@ -25,6 +26,7 @@ class OrganizationUsage: members (UsageLimit): organization_name (str): The name of the organization. self_hosted_runners (UsageLimit): + storage (OrganizationStorageUsage): schema (str | Unset): A URL to the JSON Schema for this object. Example: https://api.tower.dev/v1/schemas/OrganizationUsage.json. """ @@ -36,6 +38,7 @@ class OrganizationUsage: members: UsageLimit organization_name: str self_hosted_runners: UsageLimit + storage: OrganizationStorageUsage schema: str | Unset = UNSET def to_dict(self) -> dict[str, Any]: @@ -53,6 +56,8 @@ def to_dict(self) -> dict[str, Any]: self_hosted_runners = self.self_hosted_runners.to_dict() + storage = self.storage.to_dict() + schema = self.schema field_dict: dict[str, Any] = {} @@ -66,6 +71,7 @@ def to_dict(self) -> dict[str, Any]: "members": members, "organization_name": organization_name, "self_hosted_runners": self_hosted_runners, + "storage": storage, } ) if schema is not UNSET: @@ -75,6 +81,7 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.organization_storage_usage import OrganizationStorageUsage from ..models.usage_limit import UsageLimit d = dict(src_dict) @@ -92,6 +99,8 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: self_hosted_runners = UsageLimit.from_dict(d.pop("self_hosted_runners")) + storage = OrganizationStorageUsage.from_dict(d.pop("storage")) + schema = d.pop("$schema", UNSET) organization_usage = cls( @@ -102,6 +111,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: members=members, organization_name=organization_name, self_hosted_runners=self_hosted_runners, + storage=storage, schema=schema, ) diff --git a/src/tower/tower_api_client/models/run_and_links.py b/src/tower/tower_api_client/models/run_and_links.py new file mode 100644 index 00000000..8267fe53 --- /dev/null +++ b/src/tower/tower_api_client/models/run_and_links.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define + +if TYPE_CHECKING: + from ..models.run import Run + from ..models.run_links import RunLinks + + +T = TypeVar("T", bound="RunAndLinks") + + +@_attrs_define +class RunAndLinks: + """ + Attributes: + links (RunLinks): + run (Run): + """ + + links: RunLinks + run: Run + + def to_dict(self) -> dict[str, Any]: + links = self.links.to_dict() + + run = self.run.to_dict() + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "$links": links, + "run": run, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.run import Run + from ..models.run_links import RunLinks + + d = dict(src_dict) + links = RunLinks.from_dict(d.pop("$links")) + + run = Run.from_dict(d.pop("run")) + + run_and_links = cls( + links=links, + run=run, + ) + + return run_and_links diff --git a/src/tower/tower_api_client/models/run_links.py b/src/tower/tower_api_client/models/run_links.py new file mode 100644 index 00000000..cccc1afb --- /dev/null +++ b/src/tower/tower_api_client/models/run_links.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define + +T = TypeVar("T", bound="RunLinks") + + +@_attrs_define +class RunLinks: + """ + Attributes: + next_number (int | None): The number of the next run, if any. + prev_number (int | None): The number of the previous run, if any. + """ + + next_number: int | None + prev_number: int | None + + def to_dict(self) -> dict[str, Any]: + next_number: int | None + next_number = self.next_number + + prev_number: int | None + prev_number = self.prev_number + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "next_number": next_number, + "prev_number": prev_number, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + + def _parse_next_number(data: object) -> int | None: + if data is None: + return data + return cast(int | None, data) + + next_number = _parse_next_number(d.pop("next_number")) + + def _parse_prev_number(data: object) -> int | None: + if data is None: + return data + return cast(int | None, data) + + prev_number = _parse_prev_number(d.pop("prev_number")) + + run_links = cls( + next_number=next_number, + prev_number=prev_number, + ) + + return run_links diff --git a/src/tower/tower_api_client/models/tag_filter.py b/src/tower/tower_api_client/models/tag_filter.py new file mode 100644 index 00000000..9dc8d879 --- /dev/null +++ b/src/tower/tower_api_client/models/tag_filter.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..models.tag_filter_op import TagFilterOp +from ..types import UNSET, Unset + +T = TypeVar("T", bound="TagFilter") + + +@_attrs_define +class TagFilter: + """ + Attributes: + name (str): The tag name to search. + op (TagFilterOp): + value (str | Unset): Required if operator is eq + values (list[str] | Unset): Required if operator is in or notIn + """ + + name: str + op: TagFilterOp + value: str | Unset = UNSET + values: list[str] | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + name = self.name + + op = self.op.value + + value = self.value + + values: list[str] | Unset = UNSET + if not isinstance(self.values, Unset): + values = self.values + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "name": name, + "op": op, + } + ) + if value is not UNSET: + field_dict["value"] = value + if values is not UNSET: + field_dict["values"] = values + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + name = d.pop("name") + + op = TagFilterOp(d.pop("op")) + + value = d.pop("value", UNSET) + + values = cast(list[str], d.pop("values", UNSET)) + + tag_filter = cls( + name=name, + op=op, + value=value, + values=values, + ) + + return tag_filter diff --git a/src/tower/tower_api_client/models/tag_filter_op.py b/src/tower/tower_api_client/models/tag_filter_op.py new file mode 100644 index 00000000..4ebd5995 --- /dev/null +++ b/src/tower/tower_api_client/models/tag_filter_op.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class TagFilterOp(str, Enum): + EQ = "eq" + IN = "in" + NOTIN = "notIn" + + def __str__(self) -> str: + return str(self.value) diff --git a/src/tower/tower_api_client/models/token.py b/src/tower/tower_api_client/models/token.py index 598af35c..b61bf039 100644 --- a/src/tower/tower_api_client/models/token.py +++ b/src/tower/tower_api_client/models/token.py @@ -15,7 +15,7 @@ class Token: """ Attributes: access_token (str): The access token to use when authenticating API requests with Tower. - jwt (str): + jwt (str): This property is deprecated. Use access_token instead. refresh_token (str | Unset): The refresh token to use when refreshing an expired access token. For security reasons, refresh tokens should only be transmitted over secure channels and never logged or stored in plaintext. It will only be returned upon initial authentication or when explicitly refreshing the access token. diff --git a/src/tower/tower_api_client/models/update_account_params_execution_region.py b/src/tower/tower_api_client/models/update_account_params_execution_region.py index 132b9ce6..54752542 100644 --- a/src/tower/tower_api_client/models/update_account_params_execution_region.py +++ b/src/tower/tower_api_client/models/update_account_params_execution_region.py @@ -2,7 +2,10 @@ class UpdateAccountParamsExecutionRegion(str, Enum): + AP_NORTHEAST_1 = "ap-northeast-1" + AP_SOUTHEAST_2 = "ap-southeast-2" EU_CENTRAL_1 = "eu-central-1" + EU_WEST_1 = "eu-west-1" US_EAST_1 = "us-east-1" US_WEST_2 = "us-west-2" diff --git a/src/tower/tower_api_client/models/update_app_params.py b/src/tower/tower_api_client/models/update_app_params.py index c970dfd0..97b4846f 100644 --- a/src/tower/tower_api_client/models/update_app_params.py +++ b/src/tower/tower_api_client/models/update_app_params.py @@ -8,6 +8,7 @@ from ..types import UNSET, Unset if TYPE_CHECKING: + from ..models.app_tag import AppTag from ..models.run_retry_policy import RunRetryPolicy @@ -20,7 +21,7 @@ class UpdateAppParams: Attributes: schema (str | Unset): A URL to the JSON Schema for this object. Example: https://api.tower.dev/v1/schemas/UpdateAppParams.json. - description (None | str | Unset): New description for the App + description (None | str | Unset): Deprecated: use short_description instead. is_externally_accessible (bool | None | Unset): Indicates that web traffic should be routed to this app and that its runs should get a hostname assigned to it. pending_timeout (int | None | Unset): The amount of time in seconds that runs of this app can stay in pending @@ -28,9 +29,11 @@ class UpdateAppParams: retry_policy (RunRetryPolicy | Unset): running_timeout (int | None | Unset): The amount of time in seconds that runs of this app can stay in running state before being marked as failed. + short_description (None | str | Unset): New description for the app. status (None | str | Unset): New status for the App subdomain (None | str | Unset): The subdomain this app is accessible under. Requires is_externally_accessible to be true. + tags (list[AppTag] | Unset): The tags for this app. """ schema: str | Unset = UNSET @@ -39,8 +42,10 @@ class UpdateAppParams: pending_timeout: int | None | Unset = UNSET retry_policy: RunRetryPolicy | Unset = UNSET running_timeout: int | None | Unset = UNSET + short_description: None | str | Unset = UNSET status: None | str | Unset = UNSET subdomain: None | str | Unset = UNSET + tags: list[AppTag] | Unset = UNSET def to_dict(self) -> dict[str, Any]: schema = self.schema @@ -73,6 +78,12 @@ def to_dict(self) -> dict[str, Any]: else: running_timeout = self.running_timeout + short_description: None | str | Unset + if isinstance(self.short_description, Unset): + short_description = UNSET + else: + short_description = self.short_description + status: None | str | Unset if isinstance(self.status, Unset): status = UNSET @@ -85,6 +96,13 @@ def to_dict(self) -> dict[str, Any]: else: subdomain = self.subdomain + tags: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.tags, Unset): + tags = [] + for tags_item_data in self.tags: + tags_item = tags_item_data.to_dict() + tags.append(tags_item) + field_dict: dict[str, Any] = {} field_dict.update({}) @@ -100,15 +118,20 @@ def to_dict(self) -> dict[str, Any]: field_dict["retry_policy"] = retry_policy if running_timeout is not UNSET: field_dict["running_timeout"] = running_timeout + if short_description is not UNSET: + field_dict["short_description"] = short_description if status is not UNSET: field_dict["status"] = status if subdomain is not UNSET: field_dict["subdomain"] = subdomain + if tags is not UNSET: + field_dict["tags"] = tags return field_dict @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.app_tag import AppTag from ..models.run_retry_policy import RunRetryPolicy d = dict(src_dict) @@ -159,6 +182,15 @@ def _parse_running_timeout(data: object) -> int | None | Unset: running_timeout = _parse_running_timeout(d.pop("running_timeout", UNSET)) + def _parse_short_description(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + short_description = _parse_short_description(d.pop("short_description", UNSET)) + def _parse_status(data: object) -> None | str | Unset: if data is None: return data @@ -177,6 +209,15 @@ def _parse_subdomain(data: object) -> None | str | Unset: subdomain = _parse_subdomain(d.pop("subdomain", UNSET)) + _tags = d.pop("tags", UNSET) + tags: list[AppTag] | Unset = UNSET + if _tags is not UNSET: + tags = [] + for tags_item_data in _tags: + tags_item = AppTag.from_dict(tags_item_data) + + tags.append(tags_item) + update_app_params = cls( schema=schema, description=description, @@ -184,8 +225,10 @@ def _parse_subdomain(data: object) -> None | str | Unset: pending_timeout=pending_timeout, retry_policy=retry_policy, running_timeout=running_timeout, + short_description=short_description, status=status, subdomain=subdomain, + tags=tags, ) return update_app_params diff --git a/src/tower/tower_api_client/models/update_catalog_fact_body.py b/src/tower/tower_api_client/models/update_catalog_fact_body.py new file mode 100644 index 00000000..02d57f5e --- /dev/null +++ b/src/tower/tower_api_client/models/update_catalog_fact_body.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define + +from ..models.update_catalog_fact_body_confidence import UpdateCatalogFactBodyConfidence +from ..models.update_catalog_fact_body_scope import UpdateCatalogFactBodyScope +from ..types import UNSET, Unset + +T = TypeVar("T", bound="UpdateCatalogFactBody") + + +@_attrs_define +class UpdateCatalogFactBody: + """ + Attributes: + confidence (UpdateCatalogFactBodyConfidence): How trustworthy the fact is. + scope (UpdateCatalogFactBodyScope): What kind of object the fact is about. + statement (str): The human-readable meaning of the fact. + schema (str | Unset): A URL to the JSON Schema for this object. Example: + https://api.tower.dev/v1/schemas/UpdateCatalogFactBody.json. + body (str | Unset): Optional structured payload (SQL, unit, enum values) as a JSON string. + object_ (str | Unset): Descriptive path to what the fact is about, e.g. "bronze.runs.deleted_at". Empty for + catalog-scoped facts. + source (str | Unset): Where the fact came from (agent id, user, ...). + """ + + confidence: UpdateCatalogFactBodyConfidence + scope: UpdateCatalogFactBodyScope + statement: str + schema: str | Unset = UNSET + body: str | Unset = UNSET + object_: str | Unset = UNSET + source: str | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + confidence = self.confidence.value + + scope = self.scope.value + + statement = self.statement + + schema = self.schema + + body = self.body + + object_ = self.object_ + + source = self.source + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "confidence": confidence, + "scope": scope, + "statement": statement, + } + ) + if schema is not UNSET: + field_dict["$schema"] = schema + if body is not UNSET: + field_dict["body"] = body + if object_ is not UNSET: + field_dict["object"] = object_ + if source is not UNSET: + field_dict["source"] = source + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + confidence = UpdateCatalogFactBodyConfidence(d.pop("confidence")) + + scope = UpdateCatalogFactBodyScope(d.pop("scope")) + + statement = d.pop("statement") + + schema = d.pop("$schema", UNSET) + + body = d.pop("body", UNSET) + + object_ = d.pop("object", UNSET) + + source = d.pop("source", UNSET) + + update_catalog_fact_body = cls( + confidence=confidence, + scope=scope, + statement=statement, + schema=schema, + body=body, + object_=object_, + source=source, + ) + + return update_catalog_fact_body diff --git a/src/tower/tower_api_client/models/update_catalog_fact_body_confidence.py b/src/tower/tower_api_client/models/update_catalog_fact_body_confidence.py new file mode 100644 index 00000000..864de3b8 --- /dev/null +++ b/src/tower/tower_api_client/models/update_catalog_fact_body_confidence.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class UpdateCatalogFactBodyConfidence(str, Enum): + CONFIRMED = "confirmed" + HEURISTIC = "heuristic" + INFERRED = "inferred" + + def __str__(self) -> str: + return str(self.value) diff --git a/src/tower/tower_api_client/models/update_catalog_fact_body_scope.py b/src/tower/tower_api_client/models/update_catalog_fact_body_scope.py new file mode 100644 index 00000000..1e6ef283 --- /dev/null +++ b/src/tower/tower_api_client/models/update_catalog_fact_body_scope.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class UpdateCatalogFactBodyScope(str, Enum): + CATALOG = "catalog" + COLUMN = "column" + METRIC = "metric" + NAMESPACE = "namespace" + TABLE = "table" + + def __str__(self) -> str: + return str(self.value) diff --git a/src/tower/tower_api_client/models/update_catalog_fact_response.py b/src/tower/tower_api_client/models/update_catalog_fact_response.py new file mode 100644 index 00000000..d2a77508 --- /dev/null +++ b/src/tower/tower_api_client/models/update_catalog_fact_response.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.catalog_fact import CatalogFact + + +T = TypeVar("T", bound="UpdateCatalogFactResponse") + + +@_attrs_define +class UpdateCatalogFactResponse: + """ + Attributes: + fact (CatalogFact): + schema (str | Unset): A URL to the JSON Schema for this object. Example: + https://api.tower.dev/v1/schemas/UpdateCatalogFactResponse.json. + """ + + fact: CatalogFact + schema: str | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + fact = self.fact.to_dict() + + schema = self.schema + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "fact": fact, + } + ) + if schema is not UNSET: + field_dict["$schema"] = schema + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.catalog_fact import CatalogFact + + d = dict(src_dict) + fact = CatalogFact.from_dict(d.pop("fact")) + + schema = d.pop("$schema", UNSET) + + update_catalog_fact_response = cls( + fact=fact, + schema=schema, + ) + + return update_catalog_fact_response diff --git a/src/tower/tower_api_client/models/update_catalog_params.py b/src/tower/tower_api_client/models/update_catalog_params.py index 673099c9..555e31b8 100644 --- a/src/tower/tower_api_client/models/update_catalog_params.py +++ b/src/tower/tower_api_client/models/update_catalog_params.py @@ -18,7 +18,8 @@ class UpdateCatalogParams: """ Attributes: - environment (str): New environment for the catalog + environment (str): The environment containing the catalog to update. Catalogs cannot be moved between + environments. properties (list[EncryptedCatalogProperty]): schema (str | Unset): A URL to the JSON Schema for this object. Example: https://api.tower.dev/v1/schemas/UpdateCatalogParams.json. diff --git a/src/tower/tower_api_client/models/update_team_params_execution_region.py b/src/tower/tower_api_client/models/update_team_params_execution_region.py index de99bdc5..e209436e 100644 --- a/src/tower/tower_api_client/models/update_team_params_execution_region.py +++ b/src/tower/tower_api_client/models/update_team_params_execution_region.py @@ -2,7 +2,10 @@ class UpdateTeamParamsExecutionRegion(str, Enum): + AP_NORTHEAST_1 = "ap-northeast-1" + AP_SOUTHEAST_2 = "ap-southeast-2" EU_CENTRAL_1 = "eu-central-1" + EU_WEST_1 = "eu-west-1" US_EAST_1 = "us-east-1" US_WEST_2 = "us-west-2" diff --git a/src/tower/tower_api_client/models/vend_catalog_credentials_response.py b/src/tower/tower_api_client/models/vend_catalog_credentials_response.py index 484088bd..3d59c98e 100644 --- a/src/tower/tower_api_client/models/vend_catalog_credentials_response.py +++ b/src/tower/tower_api_client/models/vend_catalog_credentials_response.py @@ -19,16 +19,20 @@ class VendCatalogCredentialsResponse: """ Attributes: credentials (CatalogCredentials): + environment (str): Environment containing the catalog definition. schema (str | Unset): A URL to the JSON Schema for this object. Example: https://api.tower.dev/v1/schemas/VendCatalogCredentialsResponse.json. """ credentials: CatalogCredentials + environment: str schema: str | Unset = UNSET def to_dict(self) -> dict[str, Any]: credentials = self.credentials.to_dict() + environment = self.environment + schema = self.schema field_dict: dict[str, Any] = {} @@ -36,6 +40,7 @@ def to_dict(self) -> dict[str, Any]: field_dict.update( { "credentials": credentials, + "environment": environment, } ) if schema is not UNSET: @@ -50,10 +55,13 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: d = dict(src_dict) credentials = CatalogCredentials.from_dict(d.pop("credentials")) + environment = d.pop("environment") + schema = d.pop("$schema", UNSET) vend_catalog_credentials_response = cls( credentials=credentials, + environment=environment, schema=schema, ) diff --git a/src/tower/utils/pyarrow.py b/src/tower/utils/pyarrow.py deleted file mode 100644 index 9b8c1111..00000000 --- a/src/tower/utils/pyarrow.py +++ /dev/null @@ -1,328 +0,0 @@ -from typing import Any, Optional, List - -import pyarrow as pa -import pyarrow.compute as pc - -from pyiceberg import types as iceberg_types -from pyiceberg.schema import Schema as IcebergSchema -from pyiceberg.expressions import ( - BooleanExpression, - And, - Or, - Not, - EqualTo, - NotEqualTo, - GreaterThan, - GreaterThanOrEqual, - LessThan, - LessThanOrEqual, - Reference, -) - - -class FieldIdManager: - """ - Manages the assignment of unique field IDs. - Field IDs in Iceberg start from 1. - """ - - def __init__(self, start_id=1): - # Initialize current_id to start_id - 1 so the first call to get_next_id() returns start_id - self.current_id = start_id - 1 - - def get_next_id(self) -> int: - """Returns the next available unique field ID.""" - self.current_id += 1 - return self.current_id - - -def arrow_to_iceberg_type_recursive( - arrow_type: pa.DataType, field_id_manager: FieldIdManager -) -> iceberg_types.IcebergType: - """ - Recursively convert a PyArrow DataType to a PyIceberg type, - managing field IDs for nested structures. - """ - # Primitive type mappings (most remain the same) - if pa.types.is_string(arrow_type) or pa.types.is_large_string(arrow_type): - return iceberg_types.StringType() - elif pa.types.is_integer(arrow_type): - if arrow_type.bit_width <= 32: # type: ignore - return iceberg_types.IntegerType() - else: - return iceberg_types.LongType() - elif pa.types.is_floating(arrow_type): - if arrow_type.bit_width <= 32: # type: ignore - return iceberg_types.FloatType() - else: - return iceberg_types.DoubleType() - elif pa.types.is_boolean(arrow_type): - return iceberg_types.BooleanType() - elif pa.types.is_date(arrow_type): - return iceberg_types.DateType() - elif pa.types.is_time(arrow_type): - return iceberg_types.TimeType() - elif pa.types.is_timestamp(arrow_type): - if arrow_type.tz is not None: # type: ignore - return iceberg_types.TimestamptzType() - else: - return iceberg_types.TimestampType() - elif pa.types.is_binary(arrow_type) or pa.types.is_large_binary(arrow_type): - return iceberg_types.BinaryType() - elif pa.types.is_fixed_size_binary(arrow_type): - return iceberg_types.FixedType(length=arrow_type.byte_width) # type: ignore - elif pa.types.is_decimal(arrow_type): - return iceberg_types.DecimalType(arrow_type.precision, arrow_type.scale) # type: ignore - - # Nested type mappings - elif ( - pa.types.is_list(arrow_type) - or pa.types.is_large_list(arrow_type) - or pa.types.is_fixed_size_list(arrow_type) - ): - # The element field itself in Iceberg needs an ID. - element_id = field_id_manager.get_next_id() - - # Recursively convert the list's element type. - # arrow_type.value_type is the DataType of the elements. - # arrow_type.value_field is the Field of the elements (contains name, type, nullability). - element_pyarrow_type = arrow_type.value_type # type: ignore - element_iceberg_type = arrow_to_iceberg_type_recursive( - element_pyarrow_type, field_id_manager - ) - - # Determine if the elements themselves are required (not nullable). - element_is_required = not arrow_type.value_field.nullable # type: ignore - - return iceberg_types.ListType( - element_id=element_id, - element_type=element_iceberg_type, - element_required=element_is_required, - ) - elif pa.types.is_struct(arrow_type): - struct_iceberg_fields = [] - # arrow_type is a StructType. Iterate through its fields. - for i in range(arrow_type.num_fields): # type: ignore - pyarrow_child_field = arrow_type.field(i) # This is a pyarrow.Field - - # Each field within the struct needs its own unique ID. - nested_field_id = field_id_manager.get_next_id() - nested_iceberg_type = arrow_to_iceberg_type_recursive( - pyarrow_child_field.type, field_id_manager - ) - - doc = None - if pyarrow_child_field.metadata and b"doc" in pyarrow_child_field.metadata: - doc = pyarrow_child_field.metadata[b"doc"].decode("utf-8") - - struct_iceberg_fields.append( - iceberg_types.NestedField( - field_id=nested_field_id, - name=pyarrow_child_field.name, - field_type=nested_iceberg_type, - required=not pyarrow_child_field.nullable, - doc=doc, - ) - ) - return iceberg_types.StructType(*struct_iceberg_fields) - elif pa.types.is_map(arrow_type): - # Iceberg MapType requires IDs for key and value fields. - key_id = field_id_manager.get_next_id() - value_id = field_id_manager.get_next_id() - - key_iceberg_type = arrow_to_iceberg_type_recursive( - arrow_type.key_type, field_id_manager - ) # type: ignore - value_iceberg_type = arrow_to_iceberg_type_recursive( - arrow_type.item_type, field_id_manager - ) # type: ignore - - # PyArrow map keys are always non-nullable by Arrow specification. - # Nullability of map values comes from the item_field. - value_is_required = not arrow_type.item_field.nullable # type: ignore - - return iceberg_types.MapType( - key_id=key_id, - key_type=key_iceberg_type, - value_id=value_id, - value_type=value_iceberg_type, - value_required=value_is_required, - ) - else: - raise ValueError(f"Unsupported Arrow type: {arrow_type}") - - -def convert_pyarrow_schema( - arrow_schema: pa.Schema, schema_id: int = 1, start_field_id: int = 1 -) -> IcebergSchema: - """ - Convert a PyArrow schema to a PyIceberg schema. - - Args: - arrow_schema: The input PyArrow.Schema. - schema_id: The schema ID for the Iceberg schema. - start_field_id: The starting ID for field ID assignment. - Returns: - An IcebergSchema object. - """ - field_id_manager = FieldIdManager(start_id=start_field_id) - iceberg_fields = [] - - for pyarrow_field in arrow_schema: # pyarrow_field is a pa.Field object - # Assign a unique ID for this top-level field. - top_level_field_id = field_id_manager.get_next_id() - - # Recursively convert the field's type. This will handle ID assignment - # for any nested structures using the same field_id_manager. - iceberg_field_type = arrow_to_iceberg_type_recursive( - pyarrow_field.type, field_id_manager - ) - - doc = None - if pyarrow_field.metadata and b"doc" in pyarrow_field.metadata: - doc = pyarrow_field.metadata[b"doc"].decode("utf-8") - - iceberg_fields.append( - iceberg_types.NestedField( - field_id=top_level_field_id, - name=pyarrow_field.name, - field_type=iceberg_field_type, - required=not pyarrow_field.nullable, # Top-level field nullability - doc=doc, - ) - ) - return IcebergSchema(*iceberg_fields, schema_id=schema_id) - - -def extract_field_and_literal(expr: pc.Expression) -> tuple[str, Any]: - """Extract field name and literal value from a comparison expression.""" - # First, convert the expression to a string and parse it - expr_str = str(expr) - - # PyArrow expression strings look like: "(field_name == literal)" or similar - # Need to determine the operator and then split accordingly - operators = ["==", "!=", ">", ">=", "<", "<="] - op_used = None - for op in operators: - if op in expr_str: - op_used = op - break - - if not op_used: - raise ValueError( - f"Could not find comparison operator in expression: {expr_str}" - ) - - # Remove parentheses and split by operator - expr_clean = expr_str.strip("()") - parts = expr_clean.split(op_used) - if len(parts) != 2: - raise ValueError(f"Expected binary comparison in expression: {expr_str}") - - # Determine which part is the field and which is the literal - field_name = None - literal_value = None - - # Clean up the parts - left = parts[0].strip() - right = parts[1].strip() - - # Typically field name doesn't have quotes, literals (strings) do - if left.startswith('"') or left.startswith("'"): - # Right side is the field - field_name = right - # Extract the literal value - this is a simplification - literal_value = left.strip("\"'") - else: - # Left side is the field - field_name = left - # Extract the literal value - this is a simplification - literal_value = right.strip("\"'") - - # Try to convert numeric literals - try: - if "." in literal_value: - literal_value = float(literal_value) - else: - literal_value = int(literal_value) - except ValueError: - # Keep as string if not numeric - pass - - return field_name, literal_value - - -def convert_pyarrow_expression(expr: pc.Expression) -> Optional[BooleanExpression]: - """Convert a PyArrow compute expression to a PyIceberg boolean expression.""" - if expr is None: - return None - - # Handle the expression based on its string representation - expr_str = str(expr) - - # Handle logical operations - if "and" in expr_str.lower() and isinstance(expr, pc.Expression): - # This is a simplification - in real code, you'd need to parse the expression - # to extract the sub-expressions properly - left_expr = None # You'd need to extract this - right_expr = None # You'd need to extract this - return And( - convert_pyarrow_expression(left_expr), - convert_pyarrow_expression(right_expr), - ) - elif "or" in expr_str.lower() and isinstance(expr, pc.Expression): - # Similar simplification - left_expr = None # You'd need to extract this - right_expr = None # You'd need to extract this - return Or( - convert_pyarrow_expression(left_expr), - convert_pyarrow_expression(right_expr), - ) - elif "not" in expr_str.lower() and isinstance(expr, pc.Expression): - # Similar simplification - inner_expr = None # You'd need to extract this - return Not(convert_pyarrow_expression(inner_expr)) - - # Handle comparison operations - try: - if "==" in expr_str: - field_name, value = extract_field_and_literal(expr) - return EqualTo(Reference(field_name), value) - elif "!=" in expr_str: - field_name, value = extract_field_and_literal(expr) - return NotEqualTo(Reference(field_name), value) - elif ">=" in expr_str: - field_name, value = extract_field_and_literal(expr) - return GreaterThanOrEqual(Reference(field_name), value) - elif ">" in expr_str: - field_name, value = extract_field_and_literal(expr) - return GreaterThan(Reference(field_name), value) - elif "<=" in expr_str: - field_name, value = extract_field_and_literal(expr) - return LessThanOrEqual(Reference(field_name), value) - elif "<" in expr_str: - field_name, value = extract_field_and_literal(expr) - return LessThan(Reference(field_name), value) - else: - raise ValueError(f"Unsupported expression: {expr_str}") - except Exception as e: - raise ValueError(f"Failed to convert expression '{expr_str}': {str(e)}") - - -def convert_pyarrow_expressions(exprs: List[pc.Expression]) -> BooleanExpression: - """ - Convert a list of PyArrow expressions to a single PyIceberg expression. - Multiple expressions are combined with AND. - """ - if not exprs: - raise ValueError("No expressions provided") - - if len(exprs) == 1: - return convert_pyarrow_expression(exprs[0]) - - # Combine multiple expressions with AND - result = convert_pyarrow_expression(exprs[0]) - for expr in exprs[1:]: - result = And(result, convert_pyarrow_expression(expr)) - - return result diff --git a/tests/integration/features/cli_app_management.feature b/tests/integration/features/cli_app_management.feature index fbadbe34..56d25c10 100644 --- a/tests/integration/features/cli_app_management.feature +++ b/tests/integration/features/cli_app_management.feature @@ -27,10 +27,15 @@ Feature: CLI App Management Then the output should be valid JSON And the JSON should contain the created app information And the app name should be "test-cli-app-123" - And the app description should be "Test app" - Scenario: CLI deploy --create creates app with description from Towerfile + Scenario: CLI apps create returns the description in JSON output + When I run "tower apps create --json --name test-cli-app-desc-456 --description 'Test app'" via CLI + Then the output should be valid JSON + And the JSON app short description should be "Test app" + + Scenario: Deploy with create applies the Towerfile description to the new app Given I have a valid Towerfile in the current directory When I run "tower deploy --create" via CLI - And I run "tower apps show --json {app_name}" via CLI using created app name - Then the app description should be "A test app" + And I run "tower apps show --json {app_name}" via CLI with the created app name + Then the output should be valid JSON + And the JSON app short description should be "A test app" \ No newline at end of file diff --git a/tests/integration/features/cli_runs.feature b/tests/integration/features/cli_runs.feature index b2e4c384..630ef2a6 100644 --- a/tests/integration/features/cli_runs.feature +++ b/tests/integration/features/cli_runs.feature @@ -36,25 +36,30 @@ Feature: CLI Run Commands Given I have a simple hello world application named "app-logs-after-completion" When I run "tower deploy --create" via CLI And I run "tower run" via CLI - Then the output should show "Hello, World!" + Then the output should show "First log before run completes" + And the output should show "Second log after run completes" - Scenario: CLI apps logs follow should stream logs and drain after completion - Given I have a simple hello world application named "app-logs-after-completion" + Scenario: CLI apps cancel stops a running run + Given I have a valid Towerfile in the current directory When I run "tower deploy --create" via CLI - And I run "tower run --detached" via CLI and capture run number - And I run "tower apps logs --follow {app_name}#{run_number}" via CLI using created app name and run number - Then the output should show "Hello, World!" + And I run "tower run --detached" via CLI and capture the run number + And I run "tower apps cancel {app_name} {run_number}" via CLI with the created app name and run number + Then the output should show "cancelled" - Scenario: CLI apps cancel should cancel a running run + Scenario: CLI apps logs --follow streams logs for a running run without duplicates Given I have a valid Towerfile in the current directory When I run "tower deploy --create" via CLI - And I run "tower run --detached" via CLI and capture run number - And I run "tower apps cancel {app_name} {run_number}" via CLI using created app name and run number - Then the output should show "cancelled" + And I run "tower run --detached" via CLI and capture the run number + And I run "tower apps logs {app_name}#{run_number} --follow" via CLI with the created app name and run number + Then the output should show "Hello, World!" + And the output should contain "Hello, World!" exactly once + And the output should show "Warning: This run is using a deprecated runtime" - Scenario: CLI apps logs follow should display warnings - Given I have a simple hello world application named "app-logs-warning" + Scenario: CLI apps logs --follow on a finished run prints stored logs exactly once + Given I have a simple hello world application named "app-logs-after-completion" When I run "tower deploy --create" via CLI - And I run "tower run --detached" via CLI and capture run number - And I run "tower apps logs --follow {app_name}#{run_number}" via CLI using created app name and run number - Then the output should show "Warning: No new logs available" + And I run "tower run --detached" via CLI and capture the run number + And I wait for 2 seconds + And I run "tower apps logs {app_name} {run_number} --follow" via CLI with the created app name and run number + Then the output should show "Hello, World!" + And the output should contain "Hello, World!" exactly once diff --git a/tests/integration/features/steps/cli_steps.py b/tests/integration/features/steps/cli_steps.py index a6f2d471..7332e98b 100644 --- a/tests/integration/features/steps/cli_steps.py +++ b/tests/integration/features/steps/cli_steps.py @@ -2,11 +2,12 @@ import subprocess import os +import re import tempfile import shutil import json import shlex -import re +import time from datetime import datetime from pathlib import Path import requests @@ -116,44 +117,77 @@ def step_run_cli_command_with_temp_session(context, command): return run_command_with_env(context, command, test_env) -@step("no session.json should exist in the temp home") -def step_no_session_json(context): - """Verify that API key auth did not create a session.json file""" - session_path = Path(context.temp_dir) / ".config" / "tower" / "session.json" +def _substitute_captured_values(context, command): + """Substitute the created app name and captured run number into a command.""" + if "{app_name}" in command: + command = command.replace("{app_name}", context.app_name) + if "{run_number}" in command: + command = command.replace("{run_number}", str(context.run_number)) + return command + + +@step('I run "{command}" via CLI with the created app name') +def step_run_cli_with_app_name(context, command): + """Run a CLI command with {app_name} replaced by the created app's name.""" + step_run_cli_command(context, _substitute_captured_values(context, command)) + + +@step('I run "{command}" via CLI and capture the run number') +def step_run_cli_capture_run_number(context, command): + """Run a CLI command and capture the run number from 'Run #' output.""" + step_run_cli_command(context, _substitute_captured_values(context, command)) + output = _strip_ansi(context.cli_output) + match = re.search(r"Run #(\d+)", output) + assert match, f"Expected 'Run #' in output, got: {output}" + context.run_number = int(match.group(1)) + + +@step('I run "{command}" via CLI with the created app name and run number') +def step_run_cli_with_app_name_and_run_number(context, command): + """Run a CLI command with {app_name} and {run_number} substituted.""" + step_run_cli_command(context, _substitute_captured_values(context, command)) + + +@step('the output should contain "{text}" exactly once') +def step_output_contains_text_exactly_once(context, text): + """Verify the output contains the text exactly once (no duplicates).""" + output = _strip_ansi(context.cli_output) + count = output.count(text) assert ( - not session_path.exists() - ), f"session.json should not exist but found at {session_path}" + count == 1 + ), f"Expected '{text}' exactly once, found {count} times in: {output}" -@step('I run "{command}" via CLI using created app name') -def step_run_cli_command_with_app_name(context, command): - """Run a Tower CLI command with the generated app name injected.""" - if not hasattr(context, "app_name"): - raise AssertionError("Expected context.app_name to be set by app setup step") - formatted = command.format(app_name=context.app_name) - step_run_cli_command(context, formatted) +@step("I wait for {seconds:d} seconds") +def step_wait_seconds(context, seconds): + """Sleep, e.g. to let a mock run reach a terminal state.""" + time.sleep(seconds) -@step('I run "{command}" via CLI and capture run number') -def step_run_cli_command_capture_run_number(context, command): - """Run a Tower CLI command and capture the run number from its output.""" - step_run_cli_command(context, command) - output = re.sub(r"\x1b\[[0-9;]*[A-Za-z]", "", context.cli_output) - match = re.search(r"Run #(?P\d+)", output) - if not match: - raise AssertionError(f"Expected run number in output, got: {output}") - context.run_number = match.group("number") +@step('the JSON app short description should be "{expected}"') +def step_json_app_short_description(context, expected): + """Verify the app's short_description in JSON output.""" + data = parse_cli_json(context) + app = None + if isinstance(data, dict): + if "app" in data: + app = data["app"] + elif "data" in data and isinstance(data["data"], dict): + app = data["data"].get("app") + assert app is not None, f"Could not find app object in JSON response: {data}" + actual = app.get("short_description") + assert ( + actual == expected + ), f"Expected short_description '{expected}', got '{actual}'" -@step('I run "{command}" via CLI using created app name and run number') -def step_run_cli_command_with_app_name_and_run(context, command): - """Run a Tower CLI command with the generated app name and run number injected.""" - if not hasattr(context, "app_name"): - raise AssertionError("Expected context.app_name to be set by app setup step") - if not hasattr(context, "run_number"): - raise AssertionError("Expected context.run_number to be set by run step") - formatted = command.format(app_name=context.app_name, run_number=context.run_number) - step_run_cli_command(context, formatted) +@step("no session.json should exist in the temp home") +def step_no_session_json(context): + """Verify that API key auth did not create a session.json file""" + session_path = Path(context.temp_dir) / ".config" / "tower" / "session.json" + assert ( + not session_path.exists() + ), f"session.json should not exist but found at {session_path}" @step("timestamps should be yellow colored") @@ -432,38 +466,6 @@ def step_app_name_should_be(context, expected_name): ), f"Expected app name '{expected_name}', got '{actual_name}'" -@step('the app description should be "{expected_description}"') -def step_app_description_should_be(context, expected_description): - """Verify app description matches expected value""" - data = parse_cli_json(context) - candidates = [] - - if "app" in data: - candidates.append(data["app"]) - if "data" in data and "app" in data["data"]: - candidates.append(data["data"]["app"]) - - if not candidates: - candidates.append(data) - - actual_description = None - for candidate in candidates: - if isinstance(candidate, dict): - if "short_description" in candidate: - actual_description = candidate["short_description"] - break - if "description" in candidate: - actual_description = candidate["description"] - break - - assert ( - actual_description is not None - ), f"Could not find app description in JSON response: {data}" - assert ( - actual_description == expected_description - ), f"Expected description '{expected_description}', got '{actual_description}'" - - # Pagination test steps diff --git a/tests/integration/features/steps/mcp_steps.py b/tests/integration/features/steps/mcp_steps.py index d0751db7..5b1f3b75 100644 --- a/tests/integration/features/steps/mcp_steps.py +++ b/tests/integration/features/steps/mcp_steps.py @@ -127,6 +127,8 @@ def create_towerfile( """Create a Towerfile for testing - pure function with no side effects beyond file creation""" app_name = unique_app_name(context, app_name, force_new=True) + # Remember the generated name so later steps can substitute it into + # commands (see "via CLI with the created app name" in cli_steps.py). context.app_name = app_name template_dir = Path(__file__).parents[2] / "templates" diff --git a/tests/mock-api-server/main.py b/tests/mock-api-server/main.py index 2cce0c64..f16d829c 100644 --- a/tests/mock-api-server/main.py +++ b/tests/mock-api-server/main.py @@ -67,6 +67,7 @@ async def log_requests(request: Request, call_next): "created_at": datetime.datetime.now().isoformat(), "next_run_at": None, "health_status": "healthy", + "is_example": False, "pending_timeout": 300, "running_timeout": 0, "run_results": { @@ -150,13 +151,10 @@ async def create_app(app_data: Dict[str, Any]): if app_name in mock_apps_db: return {"app": mock_apps_db[app_name]} - description = app_data.get("description") - if description is None: - description = app_data.get("short_description", "") - new_app = { "created_at": datetime.datetime.now().isoformat(), "health_status": "healthy", + "is_example": False, "is_externally_accessible": True, "name": app_name, "next_run_at": None, @@ -174,7 +172,11 @@ async def create_app(app_data: Dict[str, Any]): "starting": 0, }, "schedule": None, - "short_description": description or "", + # Accept either field spelling for the description: the API calls it + # short_description, the Towerfile/CLI vocabulary is description. + "short_description": app_data.get("short_description") + or app_data.get("description") + or "", "status": "active", "subdomain": "", "version": None, @@ -198,23 +200,17 @@ async def describe_app(name: str, response: Response): @app.put("/v1/apps/{name}") -async def update_app(name: str, app_data: Dict[str, Any], response: Response): - app_info = mock_apps_db.get(name) - if not app_info: - response.status_code = 404 - return { - "$schema": "https://api.tower.dev/v1/schemas/ErrorModel.json", - "title": "Not Found", - "status": 404, - "detail": f"App '{name}' not found", - } +async def update_app(name: str, app_data: Dict[str, Any]): + """Mock endpoint for updating an app (e.g. its short_description).""" + if name not in mock_apps_db: + raise HTTPException(status_code=404, detail=f"App '{name}' not found") - if "description" in app_data: - app_info["short_description"] = app_data.get("description") or "" - elif "short_description" in app_data: - app_info["short_description"] = app_data.get("short_description") or "" + app_info = mock_apps_db[name] + if "short_description" in app_data: + app_info["short_description"] = app_data["short_description"] + elif "description" in app_data: + app_info["short_description"] = app_data["description"] - mock_apps_db[name] = app_info return {"app": app_info} @@ -368,11 +364,7 @@ async def describe_run(name: str, seq: int): # For logs-after-completion test apps, complete quickly to test log draining # Use 1 second so CLI has time to start streaming before completion - completion_threshold = ( - 1.0 - if "logs-after-completion" in name or "logs-warning" in name - else 5.0 - ) + completion_threshold = 1.0 if "logs-after-completion" in name else 5.0 if elapsed > completion_threshold: run_data["status"] = "exited" @@ -675,6 +667,8 @@ def make_log_event(seq: int, line_num: int, content: str, timestamp: str): def make_warning_event(content: str, timestamp: str): + """A warning SSE event. Matching the real server, the data field carries + the bare warning payload (not an enveloped {event, data, ...} object).""" data = {"content": content, "reported_at": timestamp} return f"event: warning\ndata: {json.dumps(data)}\n\n" @@ -694,38 +688,30 @@ async def describe_run_logs(name: str, seq: int): async def generate_logs_after_completion_test_stream(seq: int): - """Emit realistic runner logs then close, matching real server behavior.""" - yield make_log_event(seq, 1, "Using CPython 3.12.9", "2025-08-22T12:00:00Z") - yield make_log_event( - seq, 2, "Creating virtual environment at: .venv", "2025-08-22T12:00:00Z" - ) - await asyncio.sleep(0.5) - yield make_log_event( - seq, 3, "Activate with: source .venv/bin/activate", "2025-08-22T12:00:01Z" - ) - yield make_log_event(seq, 4, "Hello, World!", "2025-08-22T12:00:01Z") - + """Emit a log before the run completes and one after, then close. -async def generate_warning_log_stream(seq: int): - """Stream logs then emit warning before closing, matching real server behavior.""" - yield make_log_event(seq, 1, "Using CPython 3.12.9", "2025-08-22T12:00:00Z") + Runs whose app name contains "logs-after-completion" flip to "exited" + after about 1 second (see describe_run), so the second line arrives after + the CLI has already observed completion — exercising the post-completion + log drain. + """ yield make_log_event( - seq, 2, "Creating virtual environment at: .venv", "2025-08-22T12:00:00Z" + seq, 1, "First log before run completes", "2025-08-22T12:00:00Z" ) - await asyncio.sleep(0.5) + await asyncio.sleep(2.5) yield make_log_event( - seq, 3, "Activate with: source .venv/bin/activate", "2025-08-22T12:00:00Z" + seq, 2, "Second log after run completes", "2025-08-22T12:00:02Z" ) - yield make_log_event(seq, 4, "Hello, World!", "2025-08-22T12:00:01Z") - await asyncio.sleep(0.5) - yield make_warning_event("No new logs available", "2025-08-22T12:00:02Z") async def generate_normal_log_stream(seq: int): - """Normal log stream for regular tests.""" + """Normal log stream for regular tests, including a warning event.""" for line_num, content, timestamp in NORMAL_LOG_ENTRIES: yield make_log_event(seq, line_num, content, timestamp) await asyncio.sleep(0.1) + yield make_warning_event( + "This run is using a deprecated runtime", "2025-08-22T12:00:03Z" + ) @app.get("/v1/apps/{name}/runs/{seq}/logs/stream") @@ -735,9 +721,7 @@ async def stream_run_logs(name: str, seq: int): if name not in mock_apps_db: raise HTTPException(status_code=404, detail=f"App '{name}' not found") - if "logs-warning" in name: - stream = generate_warning_log_stream(seq) - elif "logs-after-completion" in name: + if "logs-after-completion" in name: stream = generate_logs_after_completion_test_stream(seq) else: stream = generate_normal_log_stream(seq) diff --git a/tests/tower/test_dbt.py b/tests/tower/test_dbt.py index e871959b..0367aec7 100644 --- a/tests/tower/test_dbt.py +++ b/tests/tower/test_dbt.py @@ -1,20 +1,20 @@ import os +import pytest import tempfile from pathlib import Path -from unittest.mock import MagicMock, patch - -import pytest +from unittest.mock import patch, MagicMock from tower._dbt import ( - COMMANDS_WITH_SELECT, - DEFAULT_COMMAND_PLAN, + dbt, DbtCommand, DbtRunnerConfig, DbtWorkflow, - dbt, - load_profile_from_env, parse_command_plan, + load_profile_from_env, run_dbt_workflow, + DEFAULT_COMMAND_PLAN, + SELECT_SUPPORTED_COMMANDS, + _log_run_results, ) @@ -337,7 +337,7 @@ def test_run_workflow_success( def test_run_workflow_with_selector( self, temp_dbt_project, sample_profile, mock_dbt_runner ): - """Test workflow with selector adds --select flag to commands that support it.""" + """Test workflow with selector adds --select flag.""" with patch("tower._dbt.dbtRunner", return_value=mock_dbt_runner): config = DbtRunnerConfig( project_path=temp_dbt_project, @@ -352,173 +352,106 @@ def test_run_workflow_with_selector( assert "--select" in call_args[0][0] assert "tag:daily" in call_args[0][0] - def test_run_workflow_selector_not_added_to_unsupported_commands( + def test_run_workflow_with_full_refresh( self, temp_dbt_project, sample_profile, mock_dbt_runner ): - """Test workflow with selector does NOT add --select to commands that don't support it.""" + """Test workflow with full_refresh adds flag.""" with patch("tower._dbt.dbtRunner", return_value=mock_dbt_runner): config = DbtRunnerConfig( project_path=temp_dbt_project, profile_payload=sample_profile, - commands=(DbtCommand("deps"),), - selector="tag:daily", + commands=(DbtCommand("build"),), + full_refresh=True, ) run_dbt_workflow(config) - # Verify --select was NOT added for deps command + # Verify --full-refresh was added call_args = mock_dbt_runner.invoke.call_args - assert "--select" not in call_args[0][0] + assert "--full-refresh" in call_args[0][0] - def test_run_workflow_selector_not_added_if_already_present( + def test_run_workflow_with_vars( self, temp_dbt_project, sample_profile, mock_dbt_runner ): - """Test workflow doesn't add --select if it's already in command args.""" + """Test workflow with vars adds --vars flag.""" with patch("tower._dbt.dbtRunner", return_value=mock_dbt_runner): config = DbtRunnerConfig( project_path=temp_dbt_project, profile_payload=sample_profile, - commands=(DbtCommand("build", ("--select", "models/")),), - selector="tag:daily", + commands=(DbtCommand("build"),), + vars_payload={"key": "value"}, ) run_dbt_workflow(config) - # Verify only one --select flag is present (from command args) - call_args = mock_dbt_runner.invoke.call_args[0][0] - select_count = call_args.count("--select") - assert select_count == 1 - assert "models/" in call_args - assert "tag:daily" not in call_args + # Verify --vars was added + call_args = mock_dbt_runner.invoke.call_args + assert "--vars" in call_args[0][0] - def test_run_workflow_with_multiple_commands_mixed_select_support( + def test_selector_not_added_for_unsupported_command( self, temp_dbt_project, sample_profile, mock_dbt_runner ): - """Test workflow with multiple commands, some supporting --select and some not.""" + """Selector must not be injected into commands that reject --select.""" with patch("tower._dbt.dbtRunner", return_value=mock_dbt_runner): config = DbtRunnerConfig( project_path=temp_dbt_project, profile_payload=sample_profile, - commands=( - DbtCommand("deps"), - DbtCommand("build"), - DbtCommand("docs", ("generate",)), - ), + commands=(DbtCommand("deps"),), selector="tag:daily", ) run_dbt_workflow(config) - # Check all three invocations - assert mock_dbt_runner.invoke.call_count == 3 - - # First call (deps) should NOT have --select - deps_args = mock_dbt_runner.invoke.call_args_list[0][0][0] - assert "--select" not in deps_args - - # Second call (build) should have --select - build_args = mock_dbt_runner.invoke.call_args_list[1][0][0] - assert "--select" in build_args - assert "tag:daily" in build_args - - # Third call (docs generate) - docs is in COMMANDS_WITH_SELECT - # so it will have --select (even though docs generate may not need it) - docs_args = mock_dbt_runner.invoke.call_args_list[2][0][0] - assert "--select" in docs_args - assert "tag:daily" in docs_args - assert "generate" in docs_args - - @pytest.mark.parametrize( - "command_name", - [ - "run", - "test", - "build", - "compile", - "seed", - "snapshot", - "docs", - "list", - "ls", - "show", - "source", - ], - ) - def test_commands_with_select_support( - self, temp_dbt_project, sample_profile, mock_dbt_runner, command_name - ): - """Test that all commands in COMMANDS_WITH_SELECT get --select flag when selector is provided.""" - # Verify the command is actually in COMMANDS_WITH_SELECT - assert command_name in COMMANDS_WITH_SELECT - - with patch("tower._dbt.dbtRunner", return_value=mock_dbt_runner): - config = DbtRunnerConfig( - project_path=temp_dbt_project, - profile_payload=sample_profile, - commands=(DbtCommand(command_name),), - selector="tag:daily", - ) - run_dbt_workflow(config) + call_args = mock_dbt_runner.invoke.call_args + assert "--select" not in call_args[0][0] + assert "tag:daily" not in call_args[0][0] - # Verify --select was added - call_args = mock_dbt_runner.invoke.call_args[0][0] - assert "--select" in call_args - assert "tag:daily" in call_args - - @pytest.mark.parametrize( - "command_name", - ["deps", "clean", "debug", "init"], - ) - def test_commands_without_select_support( - self, temp_dbt_project, sample_profile, mock_dbt_runner, command_name + def test_selector_not_added_when_select_already_present( + self, temp_dbt_project, sample_profile, mock_dbt_runner ): - """Test that commands not in COMMANDS_WITH_SELECT do NOT get --select flag even when selector is provided.""" - # Verify the command is NOT in COMMANDS_WITH_SELECT - assert command_name not in COMMANDS_WITH_SELECT - + """Selector must not be injected when --select is already given.""" with patch("tower._dbt.dbtRunner", return_value=mock_dbt_runner): config = DbtRunnerConfig( project_path=temp_dbt_project, profile_payload=sample_profile, - commands=(DbtCommand(command_name),), + commands=(DbtCommand("build", ("--select", "tag:hourly")),), selector="tag:daily", ) run_dbt_workflow(config) - # Verify --select was NOT added - call_args = mock_dbt_runner.invoke.call_args[0][0] - assert "--select" not in call_args + args = mock_dbt_runner.invoke.call_args[0][0] + assert args.count("--select") == 1 + assert "tag:daily" not in args - def test_run_workflow_with_full_refresh( + def test_selector_not_added_when_short_select_already_present( self, temp_dbt_project, sample_profile, mock_dbt_runner ): - """Test workflow with full_refresh adds flag.""" + """Selector must not be injected when -s is already given.""" with patch("tower._dbt.dbtRunner", return_value=mock_dbt_runner): config = DbtRunnerConfig( project_path=temp_dbt_project, profile_payload=sample_profile, - commands=(DbtCommand("build"),), - full_refresh=True, + commands=(DbtCommand("run", ("-s", "tag:hourly")),), + selector="tag:daily", ) run_dbt_workflow(config) - # Verify --full-refresh was added - call_args = mock_dbt_runner.invoke.call_args - assert "--full-refresh" in call_args[0][0] - - def test_run_workflow_with_vars( - self, temp_dbt_project, sample_profile, mock_dbt_runner - ): - """Test workflow with vars adds --vars flag.""" - with patch("tower._dbt.dbtRunner", return_value=mock_dbt_runner): - config = DbtRunnerConfig( - project_path=temp_dbt_project, - profile_payload=sample_profile, - commands=(DbtCommand("build"),), - vars_payload={"key": "value"}, - ) - run_dbt_workflow(config) + args = mock_dbt_runner.invoke.call_args[0][0] + assert "--select" not in args + assert "tag:daily" not in args - # Verify --vars was added - call_args = mock_dbt_runner.invoke.call_args - assert "--vars" in call_args[0][0] + def test_select_supported_commands_cover_node_selection_syntax(self): + """The supported set matches dbt's documented node-selection commands.""" + assert SELECT_SUPPORTED_COMMANDS == { + "run", + "test", + "build", + "compile", + "seed", + "snapshot", + "docs", + "list", + "ls", + "show", + "source", + } def test_run_workflow_failure(self, temp_dbt_project, sample_profile): """Test workflow failure raises RuntimeError.""" @@ -538,6 +471,66 @@ def test_run_workflow_failure(self, temp_dbt_project, sample_profile): run_dbt_workflow(config) +class TestLogRunResults: + """Tests for _log_run_results handling of heterogeneous dbt payloads.""" + + def _make_entry(self, name, status): + entry = MagicMock() + entry.node.name = name + entry.status = status + return entry + + def test_none_payload_is_a_no_op(self): + log = MagicMock() + _log_run_results(log, None) + log.info.assert_not_called() + + def test_bool_payload_is_a_no_op(self): + log = MagicMock() + _log_run_results(log, True) + _log_run_results(log, False) + log.info.assert_not_called() + + def test_string_payload_is_treated_as_non_iterable(self): + log = MagicMock() + _log_run_results(log, "some catalog artifact") + log.info.assert_not_called() + # The skip is diagnosable: a debug note includes the payload type name. + assert any( + "str" in str(call) for call in log.debug.call_args_list + ), f"Expected a debug note mentioning 'str', got: {log.debug.call_args_list}" + + def test_bytes_payload_is_treated_as_non_iterable(self): + log = MagicMock() + _log_run_results(log, b"artifact bytes") + log.info.assert_not_called() + + def test_non_iterable_object_payload_logs_debug_with_type_name(self): + class Manifest: + pass + + log = MagicMock() + _log_run_results(log, Manifest()) + log.info.assert_not_called() + assert any( + "Manifest" in str(call) for call in log.debug.call_args_list + ), f"Expected a debug note mentioning 'Manifest', got: {log.debug.call_args_list}" + + def test_iterable_payload_logs_per_node_entries(self): + log = MagicMock() + entries = [ + self._make_entry("model_a", "success"), + self._make_entry("model_b", "error"), + ] + _log_run_results(log, entries) + assert log.info.call_count == 2 + + def test_empty_iterable_payload_is_a_no_op(self): + log = MagicMock() + _log_run_results(log, []) + log.info.assert_not_called() + + class TestIntegrationWithTower: """Integration tests for tower.dbt() interface.""" diff --git a/tests/tower/test_storage.py b/tests/tower/test_storage.py index 43c70842..2f29c6e1 100644 --- a/tests/tower/test_storage.py +++ b/tests/tower/test_storage.py @@ -95,7 +95,10 @@ def test_get_tower_catalog_credentials_caches_vended_credentials(monkeypatch): def vend(ctx, name, environment, mode): calls.append((name, environment, mode)) - return VendCatalogCredentialsResponse(credentials=credentials) + return VendCatalogCredentialsResponse( + credentials=credentials, + environment=environment, + ) monkeypatch.setattr(_storage.TowerContext, "build", staticmethod(lambda: ctx)) monkeypatch.setattr(_storage, "_vend_catalog_credentials", vend) @@ -135,7 +138,10 @@ def test_get_tower_catalog_credentials_prunes_expired_cache_entries(monkeypatch) ) def vend(ctx, name, environment, mode): - return VendCatalogCredentialsResponse(credentials=fresh_credentials) + return VendCatalogCredentialsResponse( + credentials=fresh_credentials, + environment=environment, + ) monkeypatch.setattr(_storage.TowerContext, "build", staticmethod(lambda: ctx)) monkeypatch.setattr(_storage, "_vend_catalog_credentials", vend) @@ -164,7 +170,10 @@ def test_default_catalog_vend_retries_after_legacy_provisioning(monkeypatch): responses = [ ErrorModel(status=404, detail="not found"), ErrorModel(status=404, detail="still provisioning"), - VendCatalogCredentialsResponse(credentials=credentials), + VendCatalogCredentialsResponse( + credentials=credentials, + environment="default", + ), ] legacy_calls = [] diff --git a/tests/tower/test_table_filters.py b/tests/tower/test_table_filters.py new file mode 100644 index 00000000..b75c3b1c --- /dev/null +++ b/tests/tower/test_table_filters.py @@ -0,0 +1,167 @@ +import operator + +import pyarrow.compute as pc +import pytest +from pyiceberg.expressions import ( + And, + EqualTo, + GreaterThan, + GreaterThanOrEqual, + LessThan, + LessThanOrEqual, + NotEqualTo, + Or, +) +from pyiceberg.schema import Schema +from pyiceberg.types import IntegerType, NestedField, StringType, StructType + +import tower._tables as tables_module +from tower._context import TowerContext +from tower.exceptions import PyArrowFilterMigrationError + + +class FakeFilterTable: + def __init__(self): + self.delete_calls = [] + self._schema = Schema( + NestedField(1, "age", IntegerType(), required=False), + NestedField(2, "brand", StringType(), required=False), + NestedField(3, "origin", StringType(), required=False), + NestedField(4, "notice", StringType(), required=False), + NestedField( + 5, + "profile", + StructType( + NestedField(6, "name", StringType(), required=False), + ), + required=False, + ), + ) + + def schema(self): + return self._schema + + def delete(self, **kwargs): + self.delete_calls.append(kwargs) + + def refresh(self): + raise AssertionError("a successful delete must not refresh") + + +def make_table(): + context = TowerContext( + tower_url="https://api.example.com", + environment="production", + ) + iceberg_table = FakeFilterTable() + return tables_module.Table(context, iceberg_table), iceberg_table + + +@pytest.mark.parametrize( + ("comparison", "expected"), + [ + (lambda column: operator.eq(column, 18), EqualTo("age", 18)), + (lambda column: operator.ne(column, 18), NotEqualTo("age", 18)), + (lambda column: operator.gt(column, 18), GreaterThan("age", 18)), + (lambda column: operator.ge(column, 18), GreaterThanOrEqual("age", 18)), + (lambda column: operator.lt(column, 18), LessThan("age", 18)), + (lambda column: operator.le(column, 18), LessThanOrEqual("age", 18)), + ], +) +def test_table_column_builds_all_pyiceberg_comparisons(comparison, expected): + table, _ = make_table() + + assert comparison(table.column("age")) == expected + + +def test_table_column_expressions_compose_structurally(): + table, _ = make_table() + + expression = ( + (table.column("brand") == "candy or not") + & (table.column("origin") != "north and west") + ) | ~(table.column("notice") >= "not available") + + assert expression == Or( + And( + EqualTo("brand", "candy or not"), + NotEqualTo("origin", "north and west"), + ), + LessThan("notice", "not available"), + ) + + +def test_table_column_validates_nested_names_case_sensitively(): + table, _ = make_table() + + assert table.column("profile.name").name == "profile.name" + + with pytest.raises(ValueError, match="Column Profile.name not found"): + table.column("Profile.name") + + with pytest.raises(ValueError, match="Column profile.missing not found"): + table.column("profile.missing") + + +@pytest.mark.parametrize( + "delete_filter", + [ + "age >= 18 AND brand = 'candy'", + GreaterThanOrEqual("age", 18), + ], +) +def test_delete_forwards_canonical_filters_unchanged(delete_filter): + table, iceberg_table = make_table() + + result = table.delete(filters=delete_filter, max_retries=0) + + assert result is table + assert len(iceberg_table.delete_calls) == 1 + assert iceberg_table.delete_calls[0]["delete_filter"] is delete_filter + assert iceberg_table.delete_calls[0]["case_sensitive"] is True + + +@pytest.mark.parametrize( + "legacy_filter", + [ + pc.field("age") >= 18, + [pc.field("age") >= 18, pc.field("brand") == "candy"], + ], +) +def test_pyarrow_filters_raise_migration_error_before_write_escalation( + monkeypatch, legacy_filter +): + table, iceberg_table = make_table() + + def unexpected_escalation(mode): + raise AssertionError("invalid filters must fail before credential vending") + + monkeypatch.setattr(table, "_ensure_read_write_table", unexpected_escalation) + + with pytest.raises( + PyArrowFilterMigrationError, + match=r'table\.column\("age"\) >= 18.*a & b', + ): + table.delete(legacy_filter) + + assert iceberg_table.delete_calls == [] + + +@pytest.mark.parametrize("invalid_filter", [None, True, 42, ("age = 18",)]) +def test_delete_rejects_other_filter_types_before_write_escalation( + monkeypatch, invalid_filter +): + table, iceberg_table = make_table() + + def unexpected_escalation(mode): + raise AssertionError("invalid filters must fail before credential vending") + + monkeypatch.setattr(table, "_ensure_read_write_table", unexpected_escalation) + + with pytest.raises( + TypeError, + match="filters must be a SQL-like string or a PyIceberg BooleanExpression", + ): + table.delete(invalid_filter) + + assert iceberg_table.delete_calls == [] diff --git a/tests/tower/test_table_retries.py b/tests/tower/test_table_retries.py new file mode 100644 index 00000000..6de9a76e --- /dev/null +++ b/tests/tower/test_table_retries.py @@ -0,0 +1,284 @@ +from types import SimpleNamespace + +import httpx +import pyarrow as pa +import pytest +from pyiceberg.exceptions import ( + AuthorizationExpiredError, + BadRequestError, + CommitFailedException, + CommitStateUnknownException, + ForbiddenError, + NoSuchTableError, + ServerError, + ServiceUnavailableError, + UnauthorizedError, + WaitingForLockException, +) + +import tower._tables as tables_module +from tower._context import TowerContext + + +class FakeMutationTable: + def __init__(self, failures=(), refresh_error=None): + self.failures = list(failures) + self.refresh_error = refresh_error + self.mutations = [] + self.refresh_calls = 0 + self.events = [] + + def _mutate(self, operation): + self.mutations.append(operation) + self.events.append(("mutation", operation)) + if self.failures: + raise self.failures.pop(0) + return SimpleNamespace(rows_inserted=1, rows_updated=2) + + def append(self, data): + return self._mutate("insert") + + def upsert(self, data, **kwargs): + return self._mutate("upsert") + + def delete(self, **kwargs): + return self._mutate("delete") + + def refresh(self): + self.refresh_calls += 1 + self.events.append(("refresh",)) + if self.refresh_error is not None: + raise self.refresh_error + + +def make_table(iceberg_table): + context = TowerContext( + tower_url="https://api.example.com", + environment="production", + ) + return tables_module.Table(context, iceberg_table) + + +def run_mutation(table, operation, max_retries, retry_delay_seconds): + if operation == "insert": + return table.insert( + pa.table({"id": [1, 2, 3]}), + max_retries=max_retries, + retry_delay_seconds=retry_delay_seconds, + ) + if operation == "upsert": + return table.upsert( + pa.table({"id": [1, 2, 3]}), + join_cols=["id"], + max_retries=max_retries, + retry_delay_seconds=retry_delay_seconds, + ) + if operation == "delete": + return table.delete( + "id = 1", + max_retries=max_retries, + retry_delay_seconds=retry_delay_seconds, + ) + raise AssertionError(f"unknown operation: {operation}") + + +@pytest.mark.parametrize( + ("operation", "expected_inserts", "expected_updates"), + [ + ("insert", 3, 0), + ("upsert", 1, 2), + ("delete", 0, 0), + ], +) +def test_mutations_retry_commit_conflicts_with_exponential_full_jitter( + monkeypatch, operation, expected_inserts, expected_updates +): + iceberg_table = FakeMutationTable( + [CommitFailedException("conflict 1"), CommitFailedException("conflict 2")] + ) + table = make_table(iceberg_table) + uniform_calls = [] + sleep_calls = [] + + def uniform(low, high): + uniform_calls.append((low, high)) + iceberg_table.events.append(("jitter", low, high)) + return high / 2 + + def sleep(delay): + sleep_calls.append(delay) + iceberg_table.events.append(("sleep", delay)) + + monkeypatch.setattr(tables_module.random, "uniform", uniform) + monkeypatch.setattr(tables_module.time, "sleep", sleep) + + result = run_mutation(table, operation, max_retries=2, retry_delay_seconds=0.5) + + assert result is table + assert iceberg_table.mutations == [operation, operation, operation] + assert iceberg_table.refresh_calls == 2 + assert uniform_calls == [(0.0, 0.5), (0.0, 1.0)] + assert sleep_calls == [0.25, 0.5] + assert iceberg_table.events == [ + ("mutation", operation), + ("jitter", 0.0, 0.5), + ("sleep", 0.25), + ("refresh",), + ("mutation", operation), + ("jitter", 0.0, 1.0), + ("sleep", 0.5), + ("refresh",), + ("mutation", operation), + ] + assert table.rows_affected() == tables_module.RowsAffectedInformation( + inserts=expected_inserts, + updates=expected_updates, + ) + + +def test_commit_retry_backoff_is_capped(monkeypatch): + iceberg_table = FakeMutationTable( + [CommitFailedException(f"conflict {attempt}") for attempt in range(5)] + ) + table = make_table(iceberg_table) + uniform_calls = [] + + def uniform(low, high): + uniform_calls.append((low, high)) + return 0.0 + + monkeypatch.setattr(tables_module.random, "uniform", uniform) + monkeypatch.setattr(tables_module.time, "sleep", lambda delay: None) + + table.insert(pa.table({"id": [1]}), max_retries=5, retry_delay_seconds=10.0) + + assert uniform_calls == [ + (0.0, 10.0), + (0.0, 20.0), + (0.0, 30.0), + (0.0, 30.0), + (0.0, 30.0), + ] + + +def test_commit_retry_initial_ceiling_is_clamped(monkeypatch): + iceberg_table = FakeMutationTable([CommitFailedException("conflict")]) + table = make_table(iceberg_table) + uniform_calls = [] + + def uniform(low, high): + uniform_calls.append((low, high)) + return 0.0 + + monkeypatch.setattr(tables_module.random, "uniform", uniform) + monkeypatch.setattr(tables_module.time, "sleep", lambda delay: None) + + table.insert(pa.table({"id": [1]}), max_retries=1, retry_delay_seconds=300.0) + + assert uniform_calls == [(0.0, 30.0)] + + +@pytest.mark.parametrize("max_retries", [0, 2]) +def test_commit_retry_exhaustion_preserves_final_exception(monkeypatch, max_retries): + failures = [ + CommitFailedException(f"conflict {attempt}") + for attempt in range(max_retries + 1) + ] + iceberg_table = FakeMutationTable(failures) + table = make_table(iceberg_table) + sleep_calls = [] + + monkeypatch.setattr(tables_module.random, "uniform", lambda low, high: 0.0) + monkeypatch.setattr(tables_module.time, "sleep", sleep_calls.append) + + with pytest.raises(CommitFailedException) as exc_info: + table.insert( + pa.table({"id": [1]}), + max_retries=max_retries, + retry_delay_seconds=0.5, + ) + + assert exc_info.value is failures[-1] + assert iceberg_table.mutations == ["insert"] * (max_retries + 1) + assert iceberg_table.refresh_calls == max_retries + assert len(sleep_calls) == max_retries + assert table.rows_affected().inserts == 0 + + +@pytest.mark.parametrize( + "exception_type", + [ + CommitStateUnknownException, + ServiceUnavailableError, + AuthorizationExpiredError, + UnauthorizedError, + ForbiddenError, + NoSuchTableError, + ServerError, + BadRequestError, + WaitingForLockException, + httpx.TimeoutException, + httpx.ConnectError, + ValueError, + ], +) +def test_mutation_errors_other_than_commit_conflicts_are_not_retried( + monkeypatch, exception_type +): + failure = exception_type("not retryable") + iceberg_table = FakeMutationTable([failure]) + table = make_table(iceberg_table) + + def unexpected_call(*args, **kwargs): + raise AssertionError("non-retryable errors must not back off") + + monkeypatch.setattr(tables_module.random, "uniform", unexpected_call) + monkeypatch.setattr(tables_module.time, "sleep", unexpected_call) + + with pytest.raises(exception_type) as exc_info: + table.insert( + pa.table({"id": [1]}), + max_retries=5, + retry_delay_seconds=0.5, + ) + + assert exc_info.value is failure + assert iceberg_table.mutations == ["insert"] + assert iceberg_table.refresh_calls == 0 + assert table.rows_affected().inserts == 0 + + +def test_refresh_failure_is_not_retried(monkeypatch): + refresh_failure = RuntimeError("refresh failed") + iceberg_table = FakeMutationTable( + [CommitFailedException("conflict")], refresh_error=refresh_failure + ) + table = make_table(iceberg_table) + + monkeypatch.setattr(tables_module.random, "uniform", lambda low, high: 0.0) + monkeypatch.setattr(tables_module.time, "sleep", lambda delay: None) + + with pytest.raises(RuntimeError) as exc_info: + table.insert( + pa.table({"id": [1]}), + max_retries=5, + retry_delay_seconds=0.5, + ) + + assert exc_info.value is refresh_failure + assert iceberg_table.mutations == ["insert"] + assert iceberg_table.refresh_calls == 1 + assert table.rows_affected().inserts == 0 + + +@pytest.mark.parametrize( + "retry_delay_seconds", [float("nan"), float("inf"), float("-inf")] +) +def test_commit_retry_rejects_non_finite_delay(retry_delay_seconds): + iceberg_table = FakeMutationTable() + table = make_table(iceberg_table) + + with pytest.raises(ValueError, match="must be finite and >= 0"): + table.insert(pa.table({"id": [1]}), retry_delay_seconds=retry_delay_seconds) + + assert iceberg_table.mutations == [] diff --git a/tests/tower/test_table_schemas.py b/tests/tower/test_table_schemas.py new file mode 100644 index 00000000..2683f05e --- /dev/null +++ b/tests/tower/test_table_schemas.py @@ -0,0 +1,229 @@ +import pyarrow as pa +import pytest +from pyiceberg import types as iceberg_types +from pyiceberg.catalog.memory import InMemoryCatalog +from pyiceberg.exceptions import ValidationError as IcebergValidationError +from pyiceberg.io.pyarrow import UnsupportedPyArrowTypeException + +import tower._tables as tables_module +from tower._context import TowerContext + + +class RecordingCatalog: + def __init__(self): + self.schemas = [] + + def create_namespace_if_not_exists(self, namespace): + pass + + def create_table(self, identifier, schema): + self.schemas.append(schema) + return object() + + def create_table_if_not_exists(self, identifier, schema): + self.schemas.append(schema) + return object() + + +def make_reference(catalog, name="events"): + context = TowerContext( + tower_url="https://api.example.com", + environment="production", + ) + return tables_module.TableReference( + context, + catalog, + name, + namespace="default", + ) + + +@pytest.fixture +def in_memory_schema_catalog(tmp_path, monkeypatch): + monkeypatch.setenv( + "PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE", + "false", + ) + catalog = InMemoryCatalog("schema-tests", warehouse=tmp_path.as_uri()) + catalog.create_namespace("default") + return catalog + + +@pytest.mark.parametrize("method", ["create", "create_if_not_exists"]) +def test_table_creation_passes_original_arrow_schema_to_catalog(method): + catalog = RecordingCatalog() + schema = pa.schema([pa.field("id", pa.int64(), nullable=False)]) + reference = make_reference(catalog) + + getattr(reference, method)(schema) + + assert catalog.schemas == [schema] + assert catalog.schemas[0] is schema + + +@pytest.mark.parametrize("catalog_type", ["s3-tables", "apache-polaris"]) +def test_external_string_catalog_creation_preserves_original_arrow_schema( + monkeypatch, catalog_type +): + context = TowerContext( + tower_url="https://api.example.com", + environment="production", + api_key="api-key", + ) + catalog = RecordingCatalog() + schema = pa.schema([pa.field("id", pa.int64(), nullable=False)]) + loaded_catalogs = [] + + def unexpected_call(*args, **kwargs): + raise AssertionError("external catalogs must not vend Tower credentials") + + def load_catalog(name): + loaded_catalogs.append(name) + return catalog + + monkeypatch.setattr( + tables_module.TowerContext, "build", staticmethod(lambda: context) + ) + monkeypatch.setattr( + tables_module, + "_describe_tower_catalog_type", + lambda ctx, name, environment: catalog_type, + ) + monkeypatch.setattr(tables_module, "_has_pyiceberg_catalog_config", unexpected_call) + monkeypatch.setattr(tables_module, "get_tower_catalog_credentials", unexpected_call) + monkeypatch.setattr(tables_module, "load_catalog", load_catalog) + + reference = tables_module.tables("events", catalog="external", namespace="default") + reference.create(schema) + + assert loaded_catalogs == ["external"] + assert reference._tower_vended is False + assert reference._catalog is catalog + assert catalog.schemas == [schema] + assert catalog.schemas[0] is schema + + +def test_pyiceberg_assigns_nested_field_ids_docs_and_nullability( + in_memory_schema_catalog, +): + schema = pa.schema( + [ + pa.field( + "id", + pa.int64(), + nullable=False, + metadata={b"doc": b"identifier"}, + ), + pa.field( + "profile", + pa.struct( + [ + pa.field( + "name", + pa.string(), + nullable=False, + metadata={b"doc": b"display name"}, + ), + pa.field( + "tags", + pa.list_(pa.field("element", pa.string(), nullable=True)), + nullable=True, + ), + ] + ), + nullable=True, + metadata={b"doc": b"profile doc"}, + ), + pa.field( + "attributes", + pa.map_( + pa.string(), + pa.field("value", pa.int32(), nullable=True), + ), + nullable=True, + ), + ] + ) + + make_reference(in_memory_schema_catalog, "nested").create(schema) + iceberg_schema = in_memory_schema_catalog.load_table("default.nested").schema() + + assert {field.name: field.field_id for field in iceberg_schema.fields} == { + "id": 1, + "profile": 2, + "attributes": 3, + } + + assert iceberg_schema.find_field("id").required is True + assert iceberg_schema.find_field("id").doc == "identifier" + assert iceberg_schema.find_field("profile").required is False + assert iceberg_schema.find_field("profile").doc == "profile doc" + assert iceberg_schema.find_field("profile.name").field_id == 4 + assert iceberg_schema.find_field("profile.name").required is True + assert iceberg_schema.find_field("profile.name").doc == "display name" + assert iceberg_schema.find_field("profile.tags").field_id == 5 + assert iceberg_schema.find_field("profile.tags.element").field_id == 6 + assert iceberg_schema.find_field("profile.tags.element").required is False + assert iceberg_schema.find_field("attributes.key").field_id == 7 + assert iceberg_schema.find_field("attributes.key").required is True + assert iceberg_schema.find_field("attributes.value").field_id == 8 + assert iceberg_schema.find_field("attributes.value").required is False + + +@pytest.mark.parametrize( + ("name", "arrow_type", "iceberg_type"), + [ + ("timestamp_s", pa.timestamp("s"), iceberg_types.TimestampType()), + ("timestamp_ms", pa.timestamp("ms"), iceberg_types.TimestampType()), + ("timestamp_us", pa.timestamp("us"), iceberg_types.TimestampType()), + ( + "timestamp_utc", + pa.timestamp("us", tz="UTC"), + iceberg_types.TimestamptzType(), + ), + ("time_us", pa.time64("us"), iceberg_types.TimeType()), + ("date", pa.date32(), iceberg_types.DateType()), + ( + "decimal", + pa.decimal128(38, 10), + iceberg_types.DecimalType(38, 10), + ), + ], +) +def test_pyiceberg_accepts_supported_arrow_precision( + in_memory_schema_catalog, name, arrow_type, iceberg_type +): + schema = pa.schema([pa.field("value", arrow_type)]) + + make_reference(in_memory_schema_catalog, name).create(schema) + + table = in_memory_schema_catalog.load_table(f"default.{name}") + assert table.schema().find_field("value").field_type == iceberg_type + + +@pytest.mark.parametrize( + ("name", "arrow_type"), + [ + ("timestamp_ns", pa.timestamp("ns")), + ("timestamp_non_utc", pa.timestamp("us", tz="Europe/Berlin")), + ("time32", pa.time32("s")), + ("time_ns", pa.time64("ns")), + ("float16", pa.float16()), + ("date64", pa.date64()), + ("decimal256", pa.decimal256(38, 10)), + ], +) +def test_pyiceberg_rejects_lossy_or_unsupported_arrow_types( + in_memory_schema_catalog, name, arrow_type +): + schema = pa.schema([pa.field("value", arrow_type)]) + + with pytest.raises(UnsupportedPyArrowTypeException): + make_reference(in_memory_schema_catalog, name).create(schema) + + +def test_pyiceberg_rejects_negative_decimal_scale(in_memory_schema_catalog): + schema = pa.schema([pa.field("value", pa.decimal128(10, -2))]) + + with pytest.raises(IcebergValidationError, match=r"decimal\(10, -2\)"): + make_reference(in_memory_schema_catalog, "negative_scale").create(schema) diff --git a/tests/tower/test_tables.py b/tests/tower/test_tables.py index 946e1b11..4bec5997 100644 --- a/tests/tower/test_tables.py +++ b/tests/tower/test_tables.py @@ -144,6 +144,8 @@ def sql_catalog(): [ (None, "tower-catalog", True, "vend"), (None, "s3-tables", True, "load_catalog"), + (None, "s3-tables", False, "load_catalog"), + (None, "apache-polaris", True, "load_catalog"), (None, None, True, "load_catalog"), (None, None, False, "vend"), (True, "s3-tables", True, "vend"), @@ -214,6 +216,8 @@ def has_pyiceberg_catalog_config(name): assert ("describe_catalog", "default", "production") in calls if catalog_type is None: assert ("has_pyiceberg_config", "default") in calls + else: + assert ("has_pyiceberg_config", "default") not in calls else: assert ("describe_catalog", "default", "production") not in calls assert ("has_pyiceberg_config", "default") not in calls @@ -226,6 +230,132 @@ def test_pyiceberg_catalog_config_detects_runner_env(monkeypatch): assert tables_module._has_pyiceberg_catalog_config("other") is False +def test_pyiceberg_catalog_config_detects_loaded_config(monkeypatch): + from pyiceberg.catalog import _ENV_CONFIG + + monkeypatch.setattr( + _ENV_CONFIG, + "get_catalog_config", + lambda name: {"uri": "https://example.com"} if name == "external" else None, + ) + + assert tables_module._has_pyiceberg_catalog_config("external") is True + assert tables_module._has_pyiceberg_catalog_config("other") is False + + +@pytest.mark.parametrize("tower_credentials", [None, True, False]) +def test_explicit_catalog_bypasses_string_catalog_resolution( + monkeypatch, in_memory_catalog, tower_credentials +): + patch_tower_context(monkeypatch, api_key=None) + + def unexpected_call(*args, **kwargs): + raise AssertionError("explicit catalogs must bypass string catalog resolution") + + monkeypatch.setattr( + tables_module, "_should_vend_tower_credentials", unexpected_call + ) + monkeypatch.setattr(tables_module, "load_catalog", unexpected_call) + monkeypatch.setattr(tables_module, "_load_tower_catalog", unexpected_call) + monkeypatch.setattr(tables_module, "_describe_tower_catalog_type", unexpected_call) + monkeypatch.setattr(tables_module, "_has_pyiceberg_catalog_config", unexpected_call) + + ref = tables_module.tables( + "events", catalog=in_memory_catalog, tower_credentials=tower_credentials + ) + + assert ref._catalog is in_memory_catalog + assert ref._catalog_name is None + assert ref._tower_vended is False + assert ref._ensure_catalog_mode("read-write") is in_memory_catalog + + +def test_no_tower_auth_preserves_ambient_pyiceberg_catalog(monkeypatch): + _storage._clear_credential_cache() + patch_tower_context(monkeypatch, api_key=None) + monkeypatch.setenv( + "PYICEBERG_CATALOG__S3_TABLES__URI", "https://s3tables.example.com" + ) + catalog = FakeCatalog("configured") + calls = [] + + def unexpected_call(*args, **kwargs): + raise AssertionError("ambient catalogs must not call Tower without Tower auth") + + def load_catalog(name): + calls.append(("load_catalog", name)) + return catalog + + monkeypatch.setattr(_storage.describe_catalog_api, "sync", unexpected_call) + monkeypatch.setattr(tables_module, "get_tower_catalog_credentials", unexpected_call) + monkeypatch.setattr(tables_module, "load_catalog", load_catalog) + + ref = tables_module.tables("events", catalog="s3-tables") + + assert calls == [("load_catalog", "s3-tables")] + assert ref._tower_vended is False + assert ref._ensure_catalog_mode("read-write") is catalog + + +def test_managed_catalog_vend_failure_does_not_fall_back_to_pyiceberg(monkeypatch): + _storage._clear_credential_cache() + patch_tower_context(monkeypatch) + calls = [] + + def describe_catalog_api_sync(name, client, environment): + return make_describe_catalog_response(name, "tower-catalog") + + def get_tower_catalog_credentials(name, environment=None, mode="read"): + calls.append(("vend", name, environment, mode)) + raise RuntimeError("credential vending failed") + + def load_catalog(name): + calls.append(("load_catalog", name)) + return FakeCatalog("configured") + + monkeypatch.setattr( + _storage.describe_catalog_api, "sync", describe_catalog_api_sync + ) + monkeypatch.setattr( + tables_module, "get_tower_catalog_credentials", get_tower_catalog_credentials + ) + monkeypatch.setattr(tables_module, "load_catalog", load_catalog) + monkeypatch.setattr( + tables_module, "_has_pyiceberg_catalog_config", lambda name: True + ) + + with pytest.raises(RuntimeError, match="credential vending failed"): + tables_module.tables("events", catalog="analytics") + + assert calls == [("vend", "analytics", "production", "read")] + + +@pytest.mark.parametrize("catalog_type", ["s3-tables", "apache-polaris"]) +def test_external_catalog_write_mode_keeps_ambient_pyiceberg_catalog( + monkeypatch, catalog_type +): + _storage._clear_credential_cache() + patch_tower_context(monkeypatch) + catalog = FakeCatalog("configured") + + def describe_catalog_api_sync(name, client, environment): + return make_describe_catalog_response(name, catalog_type) + + def unexpected_vend(*args, **kwargs): + raise AssertionError("external catalogs must not vend Tower credentials") + + monkeypatch.setattr( + _storage.describe_catalog_api, "sync", describe_catalog_api_sync + ) + monkeypatch.setattr(tables_module, "load_catalog", lambda name: catalog) + monkeypatch.setattr(tables_module, "get_tower_catalog_credentials", unexpected_vend) + + ref = tables_module.tables("events", catalog="external") + + assert ref._tower_vended is False + assert ref._ensure_catalog_mode("read-write") is catalog + + def test_string_catalog_type_describe_is_cached(monkeypatch): _storage._clear_credential_cache() patch_tower_context(monkeypatch) @@ -813,7 +943,7 @@ def test_delete_from_tables(in_memory_catalog): assert table.rows_affected().inserts == 3 # Perform the underlying delete from the table... - table.delete(filters=[table.column("username") == "bobb"]) + table.delete(filters=table.column("username") == "bobb") # ...and let's make sure that record is actually gone. df = table.to_polars() diff --git a/uv.lock b/uv.lock index 1403d801..9b74ba24 100644 --- a/uv.lock +++ b/uv.lock @@ -2264,7 +2264,7 @@ wheels = [ [[package]] name = "tower" -version = "0.3.70" +version = "0.3.71" source = { editable = "." } dependencies = [ { name = "attrs" },