From 736d0483675923bf3b98b96dbac700c1b3f39478 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 11:28:13 -0700 Subject: [PATCH] release: 0.1.0-alpha.7 (#20) * feat(api): OpenAPI spec update via Stainless API (#19) * feat(api): OpenAPI spec update via Stainless API (#21) * release: 0.1.0-alpha.7 --------- Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 31 +++++++++----- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- .gitignore | 1 + .release-please-manifest.json | 2 +- .stats.yml | 1 + Brewfile | 2 + CHANGELOG.md | 9 ++++ README.md | 4 +- SECURITY.md | 27 ++++++++++++ bin/check-env-state.py | 40 ------------------ bin/check-test-server | 50 ---------------------- bin/test | 3 -- pyproject.toml | 5 ++- requirements-dev.lock | 4 +- requirements.lock | 4 +- scripts/bootstrap | 19 +++++++++ scripts/format | 8 ++++ scripts/lint | 12 ++++++ scripts/mock | 41 ++++++++++++++++++ scripts/test | 56 +++++++++++++++++++++++++ {bin => scripts/utils}/ruffen-docs.py | 0 src/rizaio/_base_client.py | 9 +++- src/rizaio/_client.py | 16 +++---- src/rizaio/_models.py | 20 +++++++-- src/rizaio/_version.py | 2 +- src/rizaio/resources/__init__.py | 24 +++++------ src/rizaio/resources/command.py | 49 +++++++++++++--------- src/rizaio/types/command_exec_params.py | 3 ++ tests/api_resources/test_command.py | 2 + tests/test_client.py | 5 +-- tests/test_models.py | 8 ++-- tests/test_transform.py | 22 +++++----- tests/utils.py | 17 +++++++- 34 files changed, 321 insertions(+), 179 deletions(-) create mode 100644 Brewfile create mode 100644 SECURITY.md delete mode 100644 bin/check-env-state.py delete mode 100755 bin/check-test-server delete mode 100755 bin/test create mode 100755 scripts/bootstrap create mode 100755 scripts/format create mode 100755 scripts/lint create mode 100755 scripts/mock create mode 100755 scripts/test rename {bin => scripts/utils}/ruffen-docs.py (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce0f38e..6fcd6ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,19 +25,28 @@ jobs: RYE_INSTALL_OPTION: '--yes' - name: Install dependencies - run: | - rye sync --all-features + run: rye sync --all-features - - name: Run ruff - run: | - rye run check:ruff + - name: Run lints + run: ./scripts/lint + test: + name: test + runs-on: ubuntu-latest - - name: Run type checking - run: | - rye run typecheck + steps: + - uses: actions/checkout@v4 - - name: Ensure importable + - name: Install Rye run: | - rye run python -c 'import rizaio' + curl -sSf https://rye-up.com/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: 0.24.0 + RYE_INSTALL_OPTION: '--yes' + + - name: Bootstrap + run: ./scripts/bootstrap + + - name: Run tests + run: ./scripts/test - diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index eeeb0fe..841ba89 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index a5a9365..76e7827 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -10,7 +10,7 @@ jobs: if: github.repository == 'riza-io/riza-api-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Check release environment run: | diff --git a/.gitignore b/.gitignore index a4b2f8c..0f9a66a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ dist .env .envrc codegen.log +Brewfile.lock.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4f9005e..b5db7ce 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.6" + ".": "0.1.0-alpha.7" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 7c8fc49..cbc2895 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1 +1,2 @@ configured_endpoints: 1 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/riza%2Friza-api-7e044297a8d3c5c64964532d19e0667b059ef5a121cf3c1e64b806eec55e0767.yml diff --git a/Brewfile b/Brewfile new file mode 100644 index 0000000..492ca37 --- /dev/null +++ b/Brewfile @@ -0,0 +1,2 @@ +brew "rye" + diff --git a/CHANGELOG.md b/CHANGELOG.md index 12a1c63..831ec72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.1.0-alpha.7 (2024-05-22) + +Full Changelog: [v0.1.0-alpha.6...v0.1.0-alpha.7](https://github.com/riza-io/riza-api-python/compare/v0.1.0-alpha.6...v0.1.0-alpha.7) + +### Features + +* **api:** OpenAPI spec update via Stainless API ([#19](https://github.com/riza-io/riza-api-python/issues/19)) ([0264f17](https://github.com/riza-io/riza-api-python/commit/0264f1720a39108489060c2621c5d0f1eead298f)) +* **api:** OpenAPI spec update via Stainless API ([#21](https://github.com/riza-io/riza-api-python/issues/21)) ([307af07](https://github.com/riza-io/riza-api-python/commit/307af072f1c6babe273f106e77487ab4be84f01d)) + ## 0.1.0-alpha.6 (2024-04-23) Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/riza-io/riza-api-python/compare/v0.1.0-alpha.5...v0.1.0-alpha.6) diff --git a/README.md b/README.md index aab1cf9..8c65ebc 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ client = Riza( ) # Override per-request: -client.with_options(timeout=5 * 1000).command.exec( +client.with_options(timeout=5.0).command.exec( code='print("Hello world!")', language="PYTHON", ) @@ -246,7 +246,7 @@ The context manager is required so that the response will reliably be closed. ### Making custom/undocumented requests -This library is typed for convenient access the documented API. +This library is typed for convenient access to the documented API. If you need to access undocumented endpoints, params, or response properties, the library can still be used. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..3038b1f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,27 @@ +# Security Policy + +## Reporting Security Issues + +This SDK is generated by [Stainless Software Inc](http://stainlessapi.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. + +To report a security issue, please contact the Stainless team at security@stainlessapi.com. + +## Responsible Disclosure + +We appreciate the efforts of security researchers and individuals who help us maintain the security of +SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible +disclosure practices by allowing us a reasonable amount of time to investigate and address the issue +before making any information public. + +## Reporting Non-SDK Related Security Issues + +If you encounter security issues that are not directly related to SDKs but pertain to the services +or products provided by Riza please follow the respective company's security reporting guidelines. + +### Riza Terms and Policies + +Please contact hello@riza.io for any questions or concerns regarding security of our services. + +--- + +Thank you for helping us keep the SDKs and systems they interact with secure. diff --git a/bin/check-env-state.py b/bin/check-env-state.py deleted file mode 100644 index e1b8b6c..0000000 --- a/bin/check-env-state.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Script that exits 1 if the current environment is not -in sync with the `requirements-dev.lock` file. -""" - -from pathlib import Path - -import importlib_metadata - - -def should_run_sync() -> bool: - dev_lock = Path(__file__).parent.parent.joinpath("requirements-dev.lock") - - for line in dev_lock.read_text().splitlines(): - if not line or line.startswith("#") or line.startswith("-e"): - continue - - dep, lock_version = line.split("==") - - try: - version = importlib_metadata.version(dep) - - if lock_version != version: - print(f"mismatch for {dep} current={version} lock={lock_version}") - return True - except Exception: - print(f"could not import {dep}") - return True - - return False - - -def main() -> None: - if should_run_sync(): - exit(1) - else: - exit(0) - - -if __name__ == "__main__": - main() diff --git a/bin/check-test-server b/bin/check-test-server deleted file mode 100755 index a6fa349..0000000 --- a/bin/check-test-server +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' # No Color - -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 -} - -function is_overriding_api_base_url() { - [ -n "$TEST_API_BASE_URL" ] -} - -if is_overriding_api_base_url ; then - # If someone is running the tests against the live API, we can trust they know - # what they're doing and exit early. - echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" - - exit 0 -elif prism_is_running ; then - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" - echo - - exit 0 -else - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" - echo -e "running against your OpenAPI spec." - echo - echo -e "${YELLOW}To fix:${NC}" - echo - echo -e "1. Install Prism (requires Node 16+):" - echo - echo -e " With npm:" - echo -e " \$ ${YELLOW}npm install -g @stoplight/prism-cli${NC}" - echo - echo -e " With yarn:" - echo -e " \$ ${YELLOW}yarn global add @stoplight/prism-cli${NC}" - echo - echo -e "2. Run the mock server" - echo - echo -e " To run the server, pass in the path of your OpenAPI" - echo -e " spec to the prism command:" - echo - echo -e " \$ ${YELLOW}prism mock path/to/your.openapi.yml${NC}" - echo - - exit 1 -fi diff --git a/bin/test b/bin/test deleted file mode 100755 index 60ede7a..0000000 --- a/bin/test +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -bin/check-test-server && rye run pytest "$@" diff --git a/pyproject.toml b/pyproject.toml index 7261017..bc7a6c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "rizaio" -version = "0.1.0-alpha.6" +version = "0.1.0-alpha.7" description = "The official Python library for the riza API" dynamic = ["readme"] license = "Apache-2.0" @@ -68,7 +68,7 @@ format = { chain = [ "fix:ruff", ]} "format:black" = "black ." -"format:docs" = "python bin/ruffen-docs.py README.md api.md" +"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" "format:ruff" = "ruff format" "format:isort" = "isort ." @@ -191,5 +191,6 @@ known-first-party = ["rizaio", "tests"] [tool.ruff.per-file-ignores] "bin/**.py" = ["T201", "T203"] +"scripts/**.py" = ["T201", "T203"] "tests/**.py" = ["T201", "T203"] "examples/**.py" = ["T201", "T203"] diff --git a/requirements-dev.lock b/requirements-dev.lock index fa36611..1ed1da0 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -59,9 +59,9 @@ pluggy==1.3.0 # via pytest py==1.11.0 # via pytest -pydantic==2.4.2 +pydantic==2.7.1 # via rizaio -pydantic-core==2.10.1 +pydantic-core==2.18.2 # via pydantic pyright==1.1.359 pytest==7.1.1 diff --git a/requirements.lock b/requirements.lock index 067cb30..cea545b 100644 --- a/requirements.lock +++ b/requirements.lock @@ -29,9 +29,9 @@ httpx==0.25.2 idna==3.4 # via anyio # via httpx -pydantic==2.4.2 +pydantic==2.7.1 # via rizaio -pydantic-core==2.10.1 +pydantic-core==2.18.2 # via pydantic sniffio==1.3.0 # via anyio diff --git a/scripts/bootstrap b/scripts/bootstrap new file mode 100755 index 0000000..29df07e --- /dev/null +++ b/scripts/bootstrap @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then + brew bundle check >/dev/null 2>&1 || { + echo "==> Installing Homebrew dependencies…" + brew bundle + } +fi + +echo "==> Installing Python dependencies…" + +# experimental uv support makes installations significantly faster +rye config --set-bool behavior.use-uv=true + +rye sync diff --git a/scripts/format b/scripts/format new file mode 100755 index 0000000..667ec2d --- /dev/null +++ b/scripts/format @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running formatters" +rye run format diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 0000000..e73f311 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running lints" +rye run lint + +echo "==> Making sure it imports" +rye run python -c 'import rizaio' + diff --git a/scripts/mock b/scripts/mock new file mode 100755 index 0000000..fe89a1d --- /dev/null +++ b/scripts/mock @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [[ -n "$1" && "$1" != '--'* ]]; then + URL="$1" + shift +else + URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" +fi + +# Check if the URL is empty +if [ -z "$URL" ]; then + echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" + exit 1 +fi + +echo "==> Starting mock server with URL ${URL}" + +# Run prism mock on the given spec +if [ "$1" == "--daemon" ]; then + npm exec --package=@stoplight/prism-cli@~5.8 -- prism mock "$URL" &> .prism.log & + + # Wait for server to come online + echo -n "Waiting for server" + while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + echo -n "." + sleep 0.1 + done + + if grep -q "✖ fatal" ".prism.log"; then + cat .prism.log + exit 1 + fi + + echo +else + npm exec --package=@stoplight/prism-cli@~5.8 -- prism mock "$URL" +fi diff --git a/scripts/test b/scripts/test new file mode 100755 index 0000000..b3ace90 --- /dev/null +++ b/scripts/test @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +function prism_is_running() { + curl --silent "http://localhost:4010" >/dev/null 2>&1 +} + +kill_server_on_port() { + pids=$(lsof -t -i tcp:"$1" || echo "") + if [ "$pids" != "" ]; then + kill "$pids" + echo "Stopped $pids." + fi +} + +function is_overriding_api_base_url() { + [ -n "$TEST_API_BASE_URL" ] +} + +if ! is_overriding_api_base_url && ! prism_is_running ; then + # When we exit this script, make sure to kill the background mock server process + trap 'kill_server_on_port 4010' EXIT + + # Start the dev server + ./scripts/mock --daemon +fi + +if is_overriding_api_base_url ; then + echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" + echo +elif ! prism_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" + echo -e "running against your OpenAPI spec." + echo + echo -e "To run the server, pass in the path or url of your OpenAPI" + echo -e "spec to the prism command:" + echo + echo -e " \$ ${YELLOW}npm exec --package=@stoplight/prism-cli@~5.3.2 -- prism mock path/to/your.openapi.yml${NC}" + echo + + exit 1 +else + echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo +fi + +echo "==> Running tests" +rye run pytest "$@" diff --git a/bin/ruffen-docs.py b/scripts/utils/ruffen-docs.py similarity index 100% rename from bin/ruffen-docs.py rename to scripts/utils/ruffen-docs.py diff --git a/src/rizaio/_base_client.py b/src/rizaio/_base_client.py index 1a12542..43c59ca 100644 --- a/src/rizaio/_base_client.py +++ b/src/rizaio/_base_client.py @@ -945,6 +945,8 @@ def _request( if self.custom_auth is not None: kwargs["auth"] = self.custom_auth + log.debug("Sending HTTP Request: %s %s", request.method, request.url) + try: response = self._client.send( request, @@ -983,7 +985,12 @@ def _request( raise APIConnectionError(request=request) from err log.debug( - 'HTTP Request: %s %s "%i %s"', request.method, request.url, response.status_code, response.reason_phrase + 'HTTP Response: %s %s "%i %s" %s', + request.method, + request.url, + response.status_code, + response.reason_phrase, + response.headers, ) try: diff --git a/src/rizaio/_client.py b/src/rizaio/_client.py index 1f0a3c1..397825d 100644 --- a/src/rizaio/_client.py +++ b/src/rizaio/_client.py @@ -46,7 +46,7 @@ class Riza(SyncAPIClient): - command: resources.Command + command: resources.CommandResource with_raw_response: RizaWithRawResponse with_streaming_response: RizaWithStreamedResponse @@ -104,7 +104,7 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.command = resources.Command(self) + self.command = resources.CommandResource(self) self.with_raw_response = RizaWithRawResponse(self) self.with_streaming_response = RizaWithStreamedResponse(self) @@ -214,7 +214,7 @@ def _make_status_error( class AsyncRiza(AsyncAPIClient): - command: resources.AsyncCommand + command: resources.AsyncCommandResource with_raw_response: AsyncRizaWithRawResponse with_streaming_response: AsyncRizaWithStreamedResponse @@ -272,7 +272,7 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.command = resources.AsyncCommand(self) + self.command = resources.AsyncCommandResource(self) self.with_raw_response = AsyncRizaWithRawResponse(self) self.with_streaming_response = AsyncRizaWithStreamedResponse(self) @@ -383,22 +383,22 @@ def _make_status_error( class RizaWithRawResponse: def __init__(self, client: Riza) -> None: - self.command = resources.CommandWithRawResponse(client.command) + self.command = resources.CommandResourceWithRawResponse(client.command) class AsyncRizaWithRawResponse: def __init__(self, client: AsyncRiza) -> None: - self.command = resources.AsyncCommandWithRawResponse(client.command) + self.command = resources.AsyncCommandResourceWithRawResponse(client.command) class RizaWithStreamedResponse: def __init__(self, client: Riza) -> None: - self.command = resources.CommandWithStreamingResponse(client.command) + self.command = resources.CommandResourceWithStreamingResponse(client.command) class AsyncRizaWithStreamedResponse: def __init__(self, client: AsyncRiza) -> None: - self.command = resources.AsyncCommandWithStreamingResponse(client.command) + self.command = resources.AsyncCommandResourceWithStreamingResponse(client.command) Client = Riza diff --git a/src/rizaio/_models.py b/src/rizaio/_models.py index ff3f54e..75c68cc 100644 --- a/src/rizaio/_models.py +++ b/src/rizaio/_models.py @@ -62,7 +62,7 @@ from ._constants import RAW_RESPONSE_HEADER if TYPE_CHECKING: - from pydantic_core.core_schema import ModelField, ModelFieldsSchema + from pydantic_core.core_schema import ModelField, LiteralSchema, ModelFieldsSchema __all__ = ["BaseModel", "GenericModel"] @@ -251,7 +251,9 @@ def model_dump( exclude_defaults: bool = False, exclude_none: bool = False, round_trip: bool = False, - warnings: bool = True, + warnings: bool | Literal["none", "warn", "error"] = True, + context: dict[str, Any] | None = None, + serialize_as_any: bool = False, ) -> dict[str, Any]: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump @@ -279,6 +281,10 @@ def model_dump( raise ValueError("round_trip is only supported in Pydantic v2") if warnings != True: raise ValueError("warnings is only supported in Pydantic v2") + if context is not None: + raise ValueError("context is only supported in Pydantic v2") + if serialize_as_any != False: + raise ValueError("serialize_as_any is only supported in Pydantic v2") return super().dict( # pyright: ignore[reportDeprecated] include=include, exclude=exclude, @@ -300,7 +306,9 @@ def model_dump_json( exclude_defaults: bool = False, exclude_none: bool = False, round_trip: bool = False, - warnings: bool = True, + warnings: bool | Literal["none", "warn", "error"] = True, + context: dict[str, Any] | None = None, + serialize_as_any: bool = False, ) -> str: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json @@ -324,6 +332,10 @@ def model_dump_json( raise ValueError("round_trip is only supported in Pydantic v2") if warnings != True: raise ValueError("warnings is only supported in Pydantic v2") + if context is not None: + raise ValueError("context is only supported in Pydantic v2") + if serialize_as_any != False: + raise ValueError("serialize_as_any is only supported in Pydantic v2") return super().json( # type: ignore[reportDeprecated] indent=indent, include=include, @@ -550,7 +562,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, field_schema = field["schema"] if field_schema["type"] == "literal": - for entry in field_schema["expected"]: + for entry in cast("LiteralSchema", field_schema)["expected"]: if isinstance(entry, str): mapping[entry] = variant else: diff --git a/src/rizaio/_version.py b/src/rizaio/_version.py index d1bd86c..a248a0b 100644 --- a/src/rizaio/_version.py +++ b/src/rizaio/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "rizaio" -__version__ = "0.1.0-alpha.6" # x-release-please-version +__version__ = "0.1.0-alpha.7" # x-release-please-version diff --git a/src/rizaio/resources/__init__.py b/src/rizaio/resources/__init__.py index f388eda..dcf4412 100644 --- a/src/rizaio/resources/__init__.py +++ b/src/rizaio/resources/__init__.py @@ -1,19 +1,19 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from .command import ( - Command, - AsyncCommand, - CommandWithRawResponse, - AsyncCommandWithRawResponse, - CommandWithStreamingResponse, - AsyncCommandWithStreamingResponse, + CommandResource, + AsyncCommandResource, + CommandResourceWithRawResponse, + AsyncCommandResourceWithRawResponse, + CommandResourceWithStreamingResponse, + AsyncCommandResourceWithStreamingResponse, ) __all__ = [ - "Command", - "AsyncCommand", - "CommandWithRawResponse", - "AsyncCommandWithRawResponse", - "CommandWithStreamingResponse", - "AsyncCommandWithStreamingResponse", + "CommandResource", + "AsyncCommandResource", + "CommandResourceWithRawResponse", + "AsyncCommandResourceWithRawResponse", + "CommandResourceWithStreamingResponse", + "AsyncCommandResourceWithStreamingResponse", ] diff --git a/src/rizaio/resources/command.py b/src/rizaio/resources/command.py index 760245f..623eea6 100644 --- a/src/rizaio/resources/command.py +++ b/src/rizaio/resources/command.py @@ -7,7 +7,7 @@ import httpx -from ..types import CommandExecResponse, command_exec_params +from ..types import command_exec_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._utils import ( maybe_transform, @@ -24,24 +24,26 @@ from .._base_client import ( make_request_options, ) +from ..types.command_exec_response import CommandExecResponse -__all__ = ["Command", "AsyncCommand"] +__all__ = ["CommandResource", "AsyncCommandResource"] -class Command(SyncAPIResource): +class CommandResource(SyncAPIResource): @cached_property - def with_raw_response(self) -> CommandWithRawResponse: - return CommandWithRawResponse(self) + def with_raw_response(self) -> CommandResourceWithRawResponse: + return CommandResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> CommandWithStreamingResponse: - return CommandWithStreamingResponse(self) + def with_streaming_response(self) -> CommandResourceWithStreamingResponse: + return CommandResourceWithStreamingResponse(self) def exec( self, *, code: str, language: Literal["PYTHON", "JAVASCRIPT", "TYPESCRIPT", "RUBY", "PHP"], + allow_http_hosts: List[str] | NotGiven = NOT_GIVEN, args: List[str] | NotGiven = NOT_GIVEN, env: Dict[str, str] | NotGiven = NOT_GIVEN, stdin: str | NotGiven = NOT_GIVEN, @@ -63,6 +65,8 @@ def exec( language: The interpreter to use when executing code. + allow_http_hosts: List of allowed hosts for HTTP requests + args: List of command line arguments to pass to the script. env: Set of key-value pairs to add to the script's execution environment. @@ -83,6 +87,7 @@ def exec( { "code": code, "language": language, + "allow_http_hosts": allow_http_hosts, "args": args, "env": env, "stdin": stdin, @@ -96,20 +101,21 @@ def exec( ) -class AsyncCommand(AsyncAPIResource): +class AsyncCommandResource(AsyncAPIResource): @cached_property - def with_raw_response(self) -> AsyncCommandWithRawResponse: - return AsyncCommandWithRawResponse(self) + def with_raw_response(self) -> AsyncCommandResourceWithRawResponse: + return AsyncCommandResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> AsyncCommandWithStreamingResponse: - return AsyncCommandWithStreamingResponse(self) + def with_streaming_response(self) -> AsyncCommandResourceWithStreamingResponse: + return AsyncCommandResourceWithStreamingResponse(self) async def exec( self, *, code: str, language: Literal["PYTHON", "JAVASCRIPT", "TYPESCRIPT", "RUBY", "PHP"], + allow_http_hosts: List[str] | NotGiven = NOT_GIVEN, args: List[str] | NotGiven = NOT_GIVEN, env: Dict[str, str] | NotGiven = NOT_GIVEN, stdin: str | NotGiven = NOT_GIVEN, @@ -131,6 +137,8 @@ async def exec( language: The interpreter to use when executing code. + allow_http_hosts: List of allowed hosts for HTTP requests + args: List of command line arguments to pass to the script. env: Set of key-value pairs to add to the script's execution environment. @@ -151,6 +159,7 @@ async def exec( { "code": code, "language": language, + "allow_http_hosts": allow_http_hosts, "args": args, "env": env, "stdin": stdin, @@ -164,8 +173,8 @@ async def exec( ) -class CommandWithRawResponse: - def __init__(self, command: Command) -> None: +class CommandResourceWithRawResponse: + def __init__(self, command: CommandResource) -> None: self._command = command self.exec = to_raw_response_wrapper( @@ -173,8 +182,8 @@ def __init__(self, command: Command) -> None: ) -class AsyncCommandWithRawResponse: - def __init__(self, command: AsyncCommand) -> None: +class AsyncCommandResourceWithRawResponse: + def __init__(self, command: AsyncCommandResource) -> None: self._command = command self.exec = async_to_raw_response_wrapper( @@ -182,8 +191,8 @@ def __init__(self, command: AsyncCommand) -> None: ) -class CommandWithStreamingResponse: - def __init__(self, command: Command) -> None: +class CommandResourceWithStreamingResponse: + def __init__(self, command: CommandResource) -> None: self._command = command self.exec = to_streamed_response_wrapper( @@ -191,8 +200,8 @@ def __init__(self, command: Command) -> None: ) -class AsyncCommandWithStreamingResponse: - def __init__(self, command: AsyncCommand) -> None: +class AsyncCommandResourceWithStreamingResponse: + def __init__(self, command: AsyncCommandResource) -> None: self._command = command self.exec = async_to_streamed_response_wrapper( diff --git a/src/rizaio/types/command_exec_params.py b/src/rizaio/types/command_exec_params.py index 6b35431..5c3a8c6 100644 --- a/src/rizaio/types/command_exec_params.py +++ b/src/rizaio/types/command_exec_params.py @@ -15,6 +15,9 @@ class CommandExecParams(TypedDict, total=False): language: Required[Literal["PYTHON", "JAVASCRIPT", "TYPESCRIPT", "RUBY", "PHP"]] """The interpreter to use when executing code.""" + allow_http_hosts: List[str] + """List of allowed hosts for HTTP requests""" + args: List[str] """List of command line arguments to pass to the script.""" diff --git a/tests/api_resources/test_command.py b/tests/api_resources/test_command.py index 3b74668..38999eb 100644 --- a/tests/api_resources/test_command.py +++ b/tests/api_resources/test_command.py @@ -30,6 +30,7 @@ def test_method_exec_with_all_params(self, client: Riza) -> None: command = client.command.exec( code='print("Hello world!")', language="PYTHON", + allow_http_hosts=["string", "string", "string"], args=["string", "string", "string"], env={"foo": "string"}, stdin="string", @@ -79,6 +80,7 @@ async def test_method_exec_with_all_params(self, async_client: AsyncRiza) -> Non command = await async_client.command.exec( code='print("Hello world!")', language="PYTHON", + allow_http_hosts=["string", "string", "string"], args=["string", "string", "string"], env={"foo": "string"}, stdin="string", diff --git a/tests/test_client.py b/tests/test_client.py index a8b33c4..7fb087f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -17,7 +17,6 @@ from pydantic import ValidationError from rizaio import Riza, AsyncRiza, APIResponseValidationError -from rizaio._client import Riza, AsyncRiza from rizaio._models import BaseModel, FinalRequestOptions from rizaio._constants import RAW_RESPONSE_HEADER from rizaio._exceptions import RizaError, APIStatusError, APITimeoutError, APIResponseValidationError @@ -47,7 +46,7 @@ def _get_open_connections(client: Riza | AsyncRiza) -> int: return len(pool._requests) -class TestRizaio: +class TestRiza: client = Riza(base_url=base_url, api_key=api_key, _strict_response_validation=True) @pytest.mark.respx(base_url=base_url) @@ -720,7 +719,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non assert _get_open_connections(self.client) == 0 -class TestAsyncRizaio: +class TestAsyncRiza: client = AsyncRiza(base_url=base_url, api_key=api_key, _strict_response_validation=True) @pytest.mark.respx(base_url=base_url) diff --git a/tests/test_models.py b/tests/test_models.py index aa49b05..97528cc 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -31,7 +31,7 @@ class NestedModel(BaseModel): # mismatched types m = NestedModel.construct(nested="hello!") - assert m.nested == "hello!" + assert cast(Any, m.nested) == "hello!" def test_optional_nested_model() -> None: @@ -48,7 +48,7 @@ class NestedModel(BaseModel): # mismatched types m3 = NestedModel.construct(nested={"foo"}) assert isinstance(cast(Any, m3.nested), set) - assert m3.nested == {"foo"} + assert cast(Any, m3.nested) == {"foo"} def test_list_nested_model() -> None: @@ -323,7 +323,7 @@ class Model(BaseModel): assert len(m.items) == 2 assert isinstance(m.items[0], Submodel1) assert m.items[0].level == -1 - assert m.items[1] == 156 + assert cast(Any, m.items[1]) == 156 def test_union_of_lists() -> None: @@ -355,7 +355,7 @@ class Model(BaseModel): assert len(m.items) == 2 assert isinstance(m.items[0], SubModel1) assert m.items[0].level == -1 - assert m.items[1] == 156 + assert cast(Any, m.items[1]) == 156 def test_dict_of_union() -> None: diff --git a/tests/test_transform.py b/tests/test_transform.py index 017aadd..cb2abe3 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -260,20 +260,22 @@ class MyModel(BaseModel): @parametrize @pytest.mark.asyncio async def test_pydantic_model_to_dictionary(use_async: bool) -> None: - assert await transform(MyModel(foo="hi!"), Any, use_async) == {"foo": "hi!"} - assert await transform(MyModel.construct(foo="hi!"), Any, use_async) == {"foo": "hi!"} + assert cast(Any, await transform(MyModel(foo="hi!"), Any, use_async)) == {"foo": "hi!"} + assert cast(Any, await transform(MyModel.construct(foo="hi!"), Any, use_async)) == {"foo": "hi!"} @parametrize @pytest.mark.asyncio async def test_pydantic_empty_model(use_async: bool) -> None: - assert await transform(MyModel.construct(), Any, use_async) == {} + assert cast(Any, await transform(MyModel.construct(), Any, use_async)) == {} @parametrize @pytest.mark.asyncio async def test_pydantic_unknown_field(use_async: bool) -> None: - assert await transform(MyModel.construct(my_untyped_field=True), Any, use_async) == {"my_untyped_field": True} + assert cast(Any, await transform(MyModel.construct(my_untyped_field=True), Any, use_async)) == { + "my_untyped_field": True + } @parametrize @@ -285,7 +287,7 @@ async def test_pydantic_mismatched_types(use_async: bool) -> None: params = await transform(model, Any, use_async) else: params = await transform(model, Any, use_async) - assert params == {"foo": True} + assert cast(Any, params) == {"foo": True} @parametrize @@ -297,7 +299,7 @@ async def test_pydantic_mismatched_object_type(use_async: bool) -> None: params = await transform(model, Any, use_async) else: params = await transform(model, Any, use_async) - assert params == {"foo": {"hello": "world"}} + assert cast(Any, params) == {"foo": {"hello": "world"}} class ModelNestedObjects(BaseModel): @@ -309,7 +311,7 @@ class ModelNestedObjects(BaseModel): async def test_pydantic_nested_objects(use_async: bool) -> None: model = ModelNestedObjects.construct(nested={"foo": "stainless"}) assert isinstance(model.nested, MyModel) - assert await transform(model, Any, use_async) == {"nested": {"foo": "stainless"}} + assert cast(Any, await transform(model, Any, use_async)) == {"nested": {"foo": "stainless"}} class ModelWithDefaultField(BaseModel): @@ -325,19 +327,19 @@ async def test_pydantic_default_field(use_async: bool) -> None: model = ModelWithDefaultField.construct() assert model.with_none_default is None assert model.with_str_default == "foo" - assert await transform(model, Any, use_async) == {} + assert cast(Any, await transform(model, Any, use_async)) == {} # should be included when the default value is explicitly given model = ModelWithDefaultField.construct(with_none_default=None, with_str_default="foo") assert model.with_none_default is None assert model.with_str_default == "foo" - assert await transform(model, Any, use_async) == {"with_none_default": None, "with_str_default": "foo"} + assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": None, "with_str_default": "foo"} # should be included when a non-default value is explicitly given model = ModelWithDefaultField.construct(with_none_default="bar", with_str_default="baz") assert model.with_none_default == "bar" assert model.with_str_default == "baz" - assert await transform(model, Any, use_async) == {"with_none_default": "bar", "with_str_default": "baz"} + assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": "bar", "with_str_default": "baz"} class TypedDictIterableUnion(TypedDict): diff --git a/tests/utils.py b/tests/utils.py index baf80d4..8a4854d 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -97,7 +97,22 @@ def assert_matches_type( assert_matches_type(key_type, key, path=[*path, ""]) assert_matches_type(items_type, item, path=[*path, ""]) elif is_union_type(type_): - for i, variant in enumerate(get_args(type_)): + variants = get_args(type_) + + try: + none_index = variants.index(type(None)) + except ValueError: + pass + else: + # special case Optional[T] for better error messages + if len(variants) == 2: + if value is None: + # valid + return + + return assert_matches_type(type_=variants[not none_index], value=value, path=path) + + for i, variant in enumerate(variants): try: assert_matches_type(variant, value, path=[*path, f"variant {i}"]) return