├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ ├── ci.yml │ ├── publish-pypi.yml │ └── release-doctor.yml ├── .gitignore ├── .python-version ├── .release-please-manifest.json ├── .stats.yml ├── Brewfile ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── api.md ├── bin ├── check-release-environment └── publish-pypi ├── examples └── .keep ├── mypy.ini ├── noxfile.py ├── pyproject.toml ├── release-please-config.json ├── requirements-dev.lock ├── requirements.lock ├── scripts ├── bootstrap ├── format ├── lint ├── mock ├── test └── utils │ ├── ruffen-docs.py │ └── upload-artifact.sh ├── src └── arcadepy │ ├── __init__.py │ ├── _base_client.py │ ├── _client.py │ ├── _compat.py │ ├── _constants.py │ ├── _exceptions.py │ ├── _files.py │ ├── _models.py │ ├── _qs.py │ ├── _resource.py │ ├── _response.py │ ├── _streaming.py │ ├── _types.py │ ├── _utils │ ├── __init__.py │ ├── _logs.py │ ├── _proxy.py │ ├── _reflection.py │ ├── _resources_proxy.py │ ├── _streams.py │ ├── _sync.py │ ├── _transform.py │ ├── _typing.py │ └── _utils.py │ ├── _version.py │ ├── lib │ └── .keep │ ├── pagination.py │ ├── py.typed │ ├── resources │ ├── __init__.py │ ├── auth.py │ ├── chat │ │ ├── __init__.py │ │ ├── chat.py │ │ └── completions.py │ ├── health.py │ ├── tools │ │ ├── __init__.py │ │ ├── formatted.py │ │ ├── scheduled.py │ │ └── tools.py │ └── workers.py │ └── types │ ├── __init__.py │ ├── auth_authorize_params.py │ ├── auth_status_params.py │ ├── chat │ ├── __init__.py │ └── completion_create_params.py │ ├── chat_message.py │ ├── chat_message_param.py │ ├── chat_response.py │ ├── choice.py │ ├── execute_tool_response.py │ ├── health_schema.py │ ├── shared │ ├── __init__.py │ ├── authorization_context.py │ ├── authorization_response.py │ └── error.py │ ├── tool_authorize_params.py │ ├── tool_definition.py │ ├── tool_execute_params.py │ ├── tool_execution.py │ ├── tool_execution_attempt.py │ ├── tool_get_params.py │ ├── tool_get_response.py │ ├── tool_list_params.py │ ├── tool_list_response.py │ ├── tools │ ├── __init__.py │ ├── formatted_get_params.py │ ├── formatted_list_params.py │ ├── scheduled_get_response.py │ └── scheduled_list_params.py │ ├── usage.py │ ├── value_schema.py │ ├── worker_create_params.py │ ├── worker_health_response.py │ ├── worker_list_params.py │ ├── worker_response.py │ ├── worker_tools_params.py │ └── worker_update_params.py └── tests ├── __init__.py ├── api_resources ├── __init__.py ├── chat │ ├── __init__.py │ └── test_completions.py ├── test_auth.py ├── test_auth_start.py ├── test_auth_wait.py ├── test_health.py ├── test_tools.py ├── test_workers.py └── tools │ ├── __init__.py │ ├── test_formatted.py │ └── test_scheduled.py ├── conftest.py ├── sample_file.txt ├── test_client.py ├── test_deepcopy.py ├── test_extract_files.py ├── test_files.py ├── test_models.py ├── test_qs.py ├── test_required_args.py ├── test_response.py ├── test_streaming.py ├── test_transform.py ├── test_utils ├── test_proxy.py └── test_typing.py └── utils.py /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VARIANT="3.9" 2 | FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} 3 | 4 | USER vscode 5 | 6 | RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.44.0" RYE_INSTALL_OPTION="--yes" bash 7 | ENV PATH=/home/vscode/.rye/shims:$PATH 8 | 9 | RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc 10 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/debian 3 | { 4 | "name": "Debian", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "context": ".." 8 | }, 9 | 10 | "postStartCommand": "rye sync --all-features", 11 | 12 | "customizations": { 13 | "vscode": { 14 | "extensions": [ 15 | "ms-python.python" 16 | ], 17 | "settings": { 18 | "terminal.integrated.shell.linux": "/bin/bash", 19 | "python.pythonPath": ".venv/bin/python", 20 | "python.defaultInterpreterPath": ".venv/bin/python", 21 | "python.typeChecking": "basic", 22 | "terminal.integrated.env.linux": { 23 | "PATH": "/home/vscode/.rye/shims:${env:PATH}" 24 | } 25 | } 26 | } 27 | }, 28 | "features": { 29 | "ghcr.io/devcontainers/features/node:1": {} 30 | } 31 | 32 | // Features to add to the dev container. More info: https://containers.dev/features. 33 | // "features": {}, 34 | 35 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 36 | // "forwardPorts": [], 37 | 38 | // Configure tool-specific properties. 39 | // "customizations": {}, 40 | 41 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 42 | // "remoteUser": "root" 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches-ignore: 5 | - 'generated' 6 | - 'codegen/**' 7 | - 'integrated/**' 8 | - 'stl-preview-head/**' 9 | - 'stl-preview-base/**' 10 | 11 | jobs: 12 | lint: 13 | timeout-minutes: 10 14 | name: lint 15 | runs-on: ${{ github.repository == 'stainless-sdks/arcade-engine-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Install Rye 20 | run: | 21 | curl -sSf https://rye.astral.sh/get | bash 22 | echo "$HOME/.rye/shims" >> $GITHUB_PATH 23 | env: 24 | RYE_VERSION: '0.44.0' 25 | RYE_INSTALL_OPTION: '--yes' 26 | 27 | - name: Install dependencies 28 | run: rye sync --all-features 29 | 30 | - name: Run lints 31 | run: ./scripts/lint 32 | 33 | upload: 34 | if: github.repository == 'stainless-sdks/arcade-engine-python' 35 | timeout-minutes: 10 36 | name: upload 37 | permissions: 38 | contents: read 39 | id-token: write 40 | runs-on: depot-ubuntu-24.04 41 | steps: 42 | - uses: actions/checkout@v4 43 | 44 | - name: Get GitHub OIDC Token 45 | id: github-oidc 46 | uses: actions/github-script@v6 47 | with: 48 | script: core.setOutput('github_token', await core.getIDToken()); 49 | 50 | - name: Upload tarball 51 | env: 52 | URL: https://pkg.stainless.com/s 53 | AUTH: ${{ steps.github-oidc.outputs.github_token }} 54 | SHA: ${{ github.sha }} 55 | run: ./scripts/utils/upload-artifact.sh 56 | 57 | test: 58 | timeout-minutes: 10 59 | name: test 60 | runs-on: ${{ github.repository == 'stainless-sdks/arcade-engine-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} 61 | steps: 62 | - uses: actions/checkout@v4 63 | 64 | - name: Install Rye 65 | run: | 66 | curl -sSf https://rye.astral.sh/get | bash 67 | echo "$HOME/.rye/shims" >> $GITHUB_PATH 68 | env: 69 | RYE_VERSION: '0.44.0' 70 | RYE_INSTALL_OPTION: '--yes' 71 | 72 | - name: Bootstrap 73 | run: ./scripts/bootstrap 74 | 75 | - name: Run tests 76 | run: ./scripts/test 77 | -------------------------------------------------------------------------------- /.github/workflows/publish-pypi.yml: -------------------------------------------------------------------------------- 1 | # This workflow is triggered when a GitHub release is created. 2 | # It can also be run manually to re-publish to PyPI in case it failed for some reason. 3 | # You can run this workflow by navigating to https://www.github.com/ArcadeAI/arcade-py/actions/workflows/publish-pypi.yml 4 | name: Publish PyPI 5 | on: 6 | workflow_dispatch: 7 | 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | publish: 13 | name: publish 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Install Rye 20 | run: | 21 | curl -sSf https://rye.astral.sh/get | bash 22 | echo "$HOME/.rye/shims" >> $GITHUB_PATH 23 | env: 24 | RYE_VERSION: '0.44.0' 25 | RYE_INSTALL_OPTION: '--yes' 26 | 27 | - name: Publish to PyPI 28 | run: | 29 | bash ./bin/publish-pypi 30 | env: 31 | PYPI_TOKEN: ${{ secrets.ARCADE_PYPI_TOKEN || secrets.PYPI_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/release-doctor.yml: -------------------------------------------------------------------------------- 1 | name: Release Doctor 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | 8 | jobs: 9 | release_doctor: 10 | name: release doctor 11 | runs-on: ubuntu-latest 12 | if: github.repository == 'ArcadeAI/arcade-py' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Check release environment 18 | run: | 19 | bash ./bin/check-release-environment 20 | env: 21 | PYPI_TOKEN: ${{ secrets.ARCADE_PYPI_TOKEN || secrets.PYPI_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .prism.log 2 | .vscode 3 | _dev 4 | 5 | __pycache__ 6 | .mypy_cache 7 | 8 | dist 9 | 10 | .venv 11 | .idea 12 | 13 | .env 14 | .envrc 15 | codegen.log 16 | Brewfile.lock.json 17 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.9.18 2 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "1.5.0" 3 | } -------------------------------------------------------------------------------- /.stats.yml: -------------------------------------------------------------------------------- 1 | configured_endpoints: 19 2 | openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-8c15fa101ceb4bf0f446fce918d2092361770e32ce43e6f88e1ece57244e8fa6.yml 3 | openapi_spec_hash: 008132037e04c877822a61f59c789647 4 | config_hash: 214997e49eded7cde9997ecb3bb24f68 5 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew "rye" 2 | 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Setting up the environment 2 | 3 | ### With Rye 4 | 5 | We use [Rye](https://rye.astral.sh/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: 6 | 7 | ```sh 8 | $ ./scripts/bootstrap 9 | ``` 10 | 11 | Or [install Rye manually](https://rye.astral.sh/guide/installation/) and run: 12 | 13 | ```sh 14 | $ rye sync --all-features 15 | ``` 16 | 17 | You can then run scripts using `rye run python script.py` or by activating the virtual environment: 18 | 19 | ```sh 20 | $ rye shell 21 | # or manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work 22 | $ source .venv/bin/activate 23 | 24 | # now you can omit the `rye run` prefix 25 | $ python script.py 26 | ``` 27 | 28 | ### Without Rye 29 | 30 | Alternatively if you don't want to install `Rye`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: 31 | 32 | ```sh 33 | $ pip install -r requirements-dev.lock 34 | ``` 35 | 36 | ## Modifying/Adding code 37 | 38 | Most of the SDK is generated code. Modifications to code will be persisted between generations, but may 39 | result in merge conflicts between manual patches and changes from the generator. The generator will never 40 | modify the contents of the `src/arcadepy/lib/` and `examples/` directories. 41 | 42 | ## Adding and running examples 43 | 44 | All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. 45 | 46 | ```py 47 | # add an example to examples/.py 48 | 49 | #!/usr/bin/env -S rye run python 50 | … 51 | ``` 52 | 53 | ```sh 54 | $ chmod +x examples/.py 55 | # run the example against your api 56 | $ ./examples/.py 57 | ``` 58 | 59 | ## Using the repository from source 60 | 61 | If you’d like to use the repository from source, you can either install from git or link to a cloned repository: 62 | 63 | To install via git: 64 | 65 | ```sh 66 | $ pip install git+ssh://git@github.com/ArcadeAI/arcade-py.git 67 | ``` 68 | 69 | Alternatively, you can build from source and install the wheel file: 70 | 71 | Building this package will create two files in the `dist/` directory, a `.tar.gz` containing the source files and a `.whl` that can be used to install the package efficiently. 72 | 73 | To create a distributable version of the library, all you have to do is run this command: 74 | 75 | ```sh 76 | $ rye build 77 | # or 78 | $ python -m build 79 | ``` 80 | 81 | Then to install: 82 | 83 | ```sh 84 | $ pip install ./path-to-wheel-file.whl 85 | ``` 86 | 87 | ## Running tests 88 | 89 | Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. 90 | 91 | ```sh 92 | # you will need npm installed 93 | $ npx prism mock path/to/your/openapi.yml 94 | ``` 95 | 96 | ```sh 97 | $ ./scripts/test 98 | ``` 99 | 100 | ## Linting and formatting 101 | 102 | This repository uses [ruff](https://github.com/astral-sh/ruff) and 103 | [black](https://github.com/psf/black) to format the code in the repository. 104 | 105 | To lint: 106 | 107 | ```sh 108 | $ ./scripts/lint 109 | ``` 110 | 111 | To format and fix all ruff issues automatically: 112 | 113 | ```sh 114 | $ ./scripts/format 115 | ``` 116 | 117 | ## Publishing and releases 118 | 119 | Changes made to this repository via the automated release PR pipeline should publish to PyPI automatically. If 120 | the changes aren't made through the automated pipeline, you may want to make releases manually. 121 | 122 | ### Publish with a GitHub workflow 123 | 124 | You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/ArcadeAI/arcade-py/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. 125 | 126 | ### Publish manually 127 | 128 | If you need to manually release a package, you can run the `bin/publish-pypi` script with a `PYPI_TOKEN` set on 129 | the environment. 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Arcade 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting Security Issues 4 | 5 | This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. 6 | 7 | To report a security issue, please contact the Stainless team at security@stainless.com. 8 | 9 | ## Responsible Disclosure 10 | 11 | We appreciate the efforts of security researchers and individuals who help us maintain the security of 12 | SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible 13 | disclosure practices by allowing us a reasonable amount of time to investigate and address the issue 14 | before making any information public. 15 | 16 | ## Reporting Non-SDK Related Security Issues 17 | 18 | If you encounter security issues that are not directly related to SDKs but pertain to the services 19 | or products provided by Arcade, please follow the respective company's security reporting guidelines. 20 | 21 | ### Arcade Terms and Policies 22 | 23 | Please contact dev@arcade.dev for any questions or concerns regarding the security of our services. 24 | 25 | --- 26 | 27 | Thank you for helping us keep the SDKs and systems they interact with secure. 28 | -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | # Shared Types 2 | 3 | ```python 4 | from arcadepy.types import AuthorizationContext, AuthorizationResponse, Error 5 | ``` 6 | 7 | # Auth 8 | 9 | Types: 10 | 11 | ```python 12 | from arcadepy.types import AuthRequest 13 | ``` 14 | 15 | Methods: 16 | 17 | - client.auth.authorize(\*\*params) -> AuthorizationResponse 18 | - client.auth.status(\*\*params) -> AuthorizationResponse 19 | 20 | # Health 21 | 22 | Types: 23 | 24 | ```python 25 | from arcadepy.types import HealthSchema 26 | ``` 27 | 28 | Methods: 29 | 30 | - client.health.check() -> HealthSchema 31 | 32 | # Chat 33 | 34 | Types: 35 | 36 | ```python 37 | from arcadepy.types import ChatMessage, ChatRequest, ChatResponse, Choice, Usage 38 | ``` 39 | 40 | ## Completions 41 | 42 | Methods: 43 | 44 | - client.chat.completions.create(\*\*params) -> ChatResponse 45 | 46 | # Tools 47 | 48 | Types: 49 | 50 | ```python 51 | from arcadepy.types import ( 52 | AuthorizeToolRequest, 53 | ExecuteToolRequest, 54 | ExecuteToolResponse, 55 | ToolDefinition, 56 | ToolExecution, 57 | ToolExecutionAttempt, 58 | ValueSchema, 59 | ) 60 | ``` 61 | 62 | Methods: 63 | 64 | - client.tools.list(\*\*params) -> SyncOffsetPage[ToolDefinition] 65 | - client.tools.authorize(\*\*params) -> AuthorizationResponse 66 | - client.tools.execute(\*\*params) -> ExecuteToolResponse 67 | - client.tools.get(name, \*\*params) -> ToolDefinition 68 | 69 | ## Scheduled 70 | 71 | Types: 72 | 73 | ```python 74 | from arcadepy.types.tools import ScheduledGetResponse 75 | ``` 76 | 77 | Methods: 78 | 79 | - client.tools.scheduled.list(\*\*params) -> SyncOffsetPage[ToolExecution] 80 | - client.tools.scheduled.get(id) -> ScheduledGetResponse 81 | 82 | ## Formatted 83 | 84 | Methods: 85 | 86 | - client.tools.formatted.list(\*\*params) -> SyncOffsetPage[object] 87 | - client.tools.formatted.get(name, \*\*params) -> object 88 | 89 | # Workers 90 | 91 | Types: 92 | 93 | ```python 94 | from arcadepy.types import ( 95 | CreateWorkerRequest, 96 | UpdateWorkerRequest, 97 | WorkerHealthResponse, 98 | WorkerResponse, 99 | ) 100 | ``` 101 | 102 | Methods: 103 | 104 | - client.workers.create(\*\*params) -> WorkerResponse 105 | - client.workers.update(id, \*\*params) -> WorkerResponse 106 | - client.workers.list(\*\*params) -> SyncOffsetPage[WorkerResponse] 107 | - client.workers.delete(id) -> None 108 | - client.workers.get(id) -> WorkerResponse 109 | - client.workers.health(id) -> WorkerHealthResponse 110 | - client.workers.tools(id, \*\*params) -> SyncOffsetPage[ToolDefinition] 111 | -------------------------------------------------------------------------------- /bin/check-release-environment: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | errors=() 4 | 5 | if [ -z "${PYPI_TOKEN}" ]; then 6 | errors+=("The ARCADE_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") 7 | fi 8 | 9 | lenErrors=${#errors[@]} 10 | 11 | if [[ lenErrors -gt 0 ]]; then 12 | echo -e "Found the following errors in the release environment:\n" 13 | 14 | for error in "${errors[@]}"; do 15 | echo -e "- $error\n" 16 | done 17 | 18 | exit 1 19 | fi 20 | 21 | echo "The environment is ready to push releases!" 22 | -------------------------------------------------------------------------------- /bin/publish-pypi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | mkdir -p dist 5 | rye build --clean 6 | rye publish --yes --token=$PYPI_TOKEN 7 | -------------------------------------------------------------------------------- /examples/.keep: -------------------------------------------------------------------------------- 1 | File generated from our OpenAPI spec by Stainless. 2 | 3 | This directory can be used to store example files demonstrating usage of this SDK. 4 | It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | pretty = True 3 | show_error_codes = True 4 | 5 | # Exclude _files.py because mypy isn't smart enough to apply 6 | # the correct type narrowing and as this is an internal module 7 | # it's fine to just use Pyright. 8 | # 9 | # We also exclude our `tests` as mypy doesn't always infer 10 | # types correctly and Pyright will still catch any type errors. 11 | exclude = ^(src/arcadepy/_files\.py|_dev/.*\.py|tests/.*)$ 12 | 13 | strict_equality = True 14 | implicit_reexport = True 15 | check_untyped_defs = True 16 | no_implicit_optional = True 17 | 18 | warn_return_any = True 19 | warn_unreachable = True 20 | warn_unused_configs = True 21 | 22 | # Turn these options off as it could cause conflicts 23 | # with the Pyright options. 24 | warn_unused_ignores = False 25 | warn_redundant_casts = False 26 | 27 | disallow_any_generics = True 28 | disallow_untyped_defs = True 29 | disallow_untyped_calls = True 30 | disallow_subclassing_any = True 31 | disallow_incomplete_defs = True 32 | disallow_untyped_decorators = True 33 | cache_fine_grained = True 34 | 35 | # By default, mypy reports an error if you assign a value to the result 36 | # of a function call that doesn't return anything. We do this in our test 37 | # cases: 38 | # ``` 39 | # result = ... 40 | # assert result is None 41 | # ``` 42 | # Changing this codegen to make mypy happy would increase complexity 43 | # and would not be worth it. 44 | disable_error_code = func-returns-value,overload-cannot-match 45 | 46 | # https://github.com/python/mypy/issues/12162 47 | [mypy.overrides] 48 | module = "black.files.*" 49 | ignore_errors = true 50 | ignore_missing_imports = true 51 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import nox 2 | 3 | 4 | @nox.session(reuse_venv=True, name="test-pydantic-v1") 5 | def test_pydantic_v1(session: nox.Session) -> None: 6 | session.install("-r", "requirements-dev.lock") 7 | session.install("pydantic<2") 8 | 9 | session.run("pytest", "--showlocals", "--ignore=tests/functional", *session.posargs) 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "arcadepy" 3 | version = "1.5.0" 4 | description = "The official Python library for the Arcade API" 5 | dynamic = ["readme"] 6 | license = "MIT" 7 | authors = [ 8 | { name = "Arcade", email = "dev@arcade.dev" }, 9 | ] 10 | dependencies = [ 11 | "httpx>=0.23.0, <1", 12 | "pydantic>=1.9.0, <3", 13 | "typing-extensions>=4.10, <5", 14 | "anyio>=3.5.0, <5", 15 | "distro>=1.7.0, <2", 16 | "sniffio", 17 | ] 18 | requires-python = ">= 3.8" 19 | classifiers = [ 20 | "Typing :: Typed", 21 | "Intended Audience :: Developers", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "Operating System :: OS Independent", 28 | "Operating System :: POSIX", 29 | "Operating System :: MacOS", 30 | "Operating System :: POSIX :: Linux", 31 | "Operating System :: Microsoft :: Windows", 32 | "Topic :: Software Development :: Libraries :: Python Modules", 33 | "License :: OSI Approved :: MIT License" 34 | ] 35 | 36 | [project.urls] 37 | Homepage = "https://github.com/ArcadeAI/arcade-py" 38 | Repository = "https://github.com/ArcadeAI/arcade-py" 39 | 40 | 41 | [tool.rye] 42 | managed = true 43 | # version pins are in requirements-dev.lock 44 | dev-dependencies = [ 45 | "pyright==1.1.399", 46 | "mypy", 47 | "respx", 48 | "pytest", 49 | "pytest-asyncio", 50 | "ruff", 51 | "time-machine", 52 | "nox", 53 | "dirty-equals>=0.6.0", 54 | "importlib-metadata>=6.7.0", 55 | "rich>=13.7.1", 56 | "nest_asyncio==1.6.0", 57 | ] 58 | 59 | [tool.rye.scripts] 60 | format = { chain = [ 61 | "format:ruff", 62 | "format:docs", 63 | "fix:ruff", 64 | # run formatting again to fix any inconsistencies when imports are stripped 65 | "format:ruff", 66 | ]} 67 | "format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" 68 | "format:ruff" = "ruff format" 69 | 70 | "lint" = { chain = [ 71 | "check:ruff", 72 | "typecheck", 73 | "check:importable", 74 | ]} 75 | "check:ruff" = "ruff check ." 76 | "fix:ruff" = "ruff check --fix ." 77 | 78 | "check:importable" = "python -c 'import arcadepy'" 79 | 80 | typecheck = { chain = [ 81 | "typecheck:pyright", 82 | "typecheck:mypy" 83 | ]} 84 | "typecheck:pyright" = "pyright" 85 | "typecheck:verify-types" = "pyright --verifytypes arcadepy --ignoreexternal" 86 | "typecheck:mypy" = "mypy ." 87 | 88 | [build-system] 89 | requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] 90 | build-backend = "hatchling.build" 91 | 92 | [tool.hatch.build] 93 | include = [ 94 | "src/*" 95 | ] 96 | 97 | [tool.hatch.build.targets.wheel] 98 | packages = ["src/arcadepy"] 99 | 100 | [tool.hatch.build.targets.sdist] 101 | # Basically everything except hidden files/directories (such as .github, .devcontainers, .python-version, etc) 102 | include = [ 103 | "/*.toml", 104 | "/*.json", 105 | "/*.lock", 106 | "/*.md", 107 | "/mypy.ini", 108 | "/noxfile.py", 109 | "bin/*", 110 | "examples/*", 111 | "src/*", 112 | "tests/*", 113 | ] 114 | 115 | [tool.hatch.metadata.hooks.fancy-pypi-readme] 116 | content-type = "text/markdown" 117 | 118 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] 119 | path = "README.md" 120 | 121 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] 122 | # replace relative links with absolute links 123 | pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' 124 | replacement = '[\1](https://github.com/ArcadeAI/arcade-py/tree/main/\g<2>)' 125 | 126 | [tool.pytest.ini_options] 127 | testpaths = ["tests"] 128 | addopts = "--tb=short" 129 | xfail_strict = true 130 | asyncio_mode = "auto" 131 | asyncio_default_fixture_loop_scope = "session" 132 | filterwarnings = [ 133 | "error" 134 | ] 135 | 136 | [tool.pyright] 137 | # this enables practically every flag given by pyright. 138 | # there are a couple of flags that are still disabled by 139 | # default in strict mode as they are experimental and niche. 140 | typeCheckingMode = "strict" 141 | pythonVersion = "3.8" 142 | 143 | exclude = [ 144 | "_dev", 145 | ".venv", 146 | ".nox", 147 | ] 148 | 149 | reportImplicitOverride = true 150 | reportOverlappingOverload = false 151 | 152 | reportImportCycles = false 153 | reportPrivateUsage = false 154 | 155 | [tool.ruff] 156 | line-length = 120 157 | output-format = "grouped" 158 | target-version = "py37" 159 | 160 | [tool.ruff.format] 161 | docstring-code-format = true 162 | 163 | [tool.ruff.lint] 164 | select = [ 165 | # isort 166 | "I", 167 | # bugbear rules 168 | "B", 169 | # remove unused imports 170 | "F401", 171 | # bare except statements 172 | "E722", 173 | # unused arguments 174 | "ARG", 175 | # print statements 176 | "T201", 177 | "T203", 178 | # misuse of typing.TYPE_CHECKING 179 | "TC004", 180 | # import rules 181 | "TID251", 182 | ] 183 | ignore = [ 184 | # mutable defaults 185 | "B006", 186 | ] 187 | unfixable = [ 188 | # disable auto fix for print statements 189 | "T201", 190 | "T203", 191 | ] 192 | 193 | [tool.ruff.lint.flake8-tidy-imports.banned-api] 194 | "functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" 195 | 196 | [tool.ruff.lint.isort] 197 | length-sort = true 198 | length-sort-straight = true 199 | combine-as-imports = true 200 | extra-standard-library = ["typing_extensions"] 201 | known-first-party = ["arcadepy", "tests"] 202 | 203 | [tool.ruff.lint.per-file-ignores] 204 | "bin/**.py" = ["T201", "T203"] 205 | "scripts/**.py" = ["T201", "T203"] 206 | "tests/**.py" = ["T201", "T203"] 207 | "examples/**.py" = ["T201", "T203"] 208 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": {} 4 | }, 5 | "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", 6 | "include-v-in-tag": true, 7 | "include-component-in-tag": false, 8 | "versioning": "prerelease", 9 | "prerelease": true, 10 | "bump-minor-pre-major": true, 11 | "bump-patch-for-minor-pre-major": false, 12 | "pull-request-header": "Automated Release PR", 13 | "pull-request-title-pattern": "release: ${version}", 14 | "changelog-sections": [ 15 | { 16 | "type": "feat", 17 | "section": "Features" 18 | }, 19 | { 20 | "type": "fix", 21 | "section": "Bug Fixes" 22 | }, 23 | { 24 | "type": "perf", 25 | "section": "Performance Improvements" 26 | }, 27 | { 28 | "type": "revert", 29 | "section": "Reverts" 30 | }, 31 | { 32 | "type": "chore", 33 | "section": "Chores" 34 | }, 35 | { 36 | "type": "docs", 37 | "section": "Documentation" 38 | }, 39 | { 40 | "type": "style", 41 | "section": "Styles" 42 | }, 43 | { 44 | "type": "refactor", 45 | "section": "Refactors" 46 | }, 47 | { 48 | "type": "test", 49 | "section": "Tests", 50 | "hidden": true 51 | }, 52 | { 53 | "type": "build", 54 | "section": "Build System" 55 | }, 56 | { 57 | "type": "ci", 58 | "section": "Continuous Integration", 59 | "hidden": true 60 | } 61 | ], 62 | "release-type": "python", 63 | "extra-files": [ 64 | "src/arcadepy/_version.py" 65 | ] 66 | } -------------------------------------------------------------------------------- /requirements-dev.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: true 8 | # with-sources: false 9 | # generate-hashes: false 10 | # universal: false 11 | 12 | -e file:. 13 | annotated-types==0.6.0 14 | # via pydantic 15 | anyio==4.4.0 16 | # via arcadepy 17 | # via httpx 18 | argcomplete==3.1.2 19 | # via nox 20 | certifi==2023.7.22 21 | # via httpcore 22 | # via httpx 23 | colorlog==6.7.0 24 | # via nox 25 | dirty-equals==0.6.0 26 | distlib==0.3.7 27 | # via virtualenv 28 | distro==1.8.0 29 | # via arcadepy 30 | exceptiongroup==1.2.2 31 | # via anyio 32 | # via pytest 33 | filelock==3.12.4 34 | # via virtualenv 35 | h11==0.14.0 36 | # via httpcore 37 | httpcore==1.0.2 38 | # via httpx 39 | httpx==0.28.1 40 | # via arcadepy 41 | # via respx 42 | idna==3.4 43 | # via anyio 44 | # via httpx 45 | importlib-metadata==7.0.0 46 | iniconfig==2.0.0 47 | # via pytest 48 | markdown-it-py==3.0.0 49 | # via rich 50 | mdurl==0.1.2 51 | # via markdown-it-py 52 | mypy==1.14.1 53 | mypy-extensions==1.0.0 54 | # via mypy 55 | nest-asyncio==1.6.0 56 | nodeenv==1.8.0 57 | # via pyright 58 | nox==2023.4.22 59 | packaging==23.2 60 | # via nox 61 | # via pytest 62 | platformdirs==3.11.0 63 | # via virtualenv 64 | pluggy==1.5.0 65 | # via pytest 66 | pydantic==2.10.3 67 | # via arcadepy 68 | pydantic-core==2.27.1 69 | # via pydantic 70 | pygments==2.18.0 71 | # via rich 72 | pyright==1.1.399 73 | pytest==8.3.3 74 | # via pytest-asyncio 75 | pytest-asyncio==0.24.0 76 | python-dateutil==2.8.2 77 | # via time-machine 78 | pytz==2023.3.post1 79 | # via dirty-equals 80 | respx==0.22.0 81 | rich==13.7.1 82 | ruff==0.9.4 83 | setuptools==68.2.2 84 | # via nodeenv 85 | six==1.16.0 86 | # via python-dateutil 87 | sniffio==1.3.0 88 | # via anyio 89 | # via arcadepy 90 | time-machine==2.9.0 91 | tomli==2.0.2 92 | # via mypy 93 | # via pytest 94 | typing-extensions==4.12.2 95 | # via anyio 96 | # via arcadepy 97 | # via mypy 98 | # via pydantic 99 | # via pydantic-core 100 | # via pyright 101 | virtualenv==20.24.5 102 | # via nox 103 | zipp==3.17.0 104 | # via importlib-metadata 105 | -------------------------------------------------------------------------------- /requirements.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: true 8 | # with-sources: false 9 | # generate-hashes: false 10 | # universal: false 11 | 12 | -e file:. 13 | annotated-types==0.6.0 14 | # via pydantic 15 | anyio==4.4.0 16 | # via arcadepy 17 | # via httpx 18 | certifi==2023.7.22 19 | # via httpcore 20 | # via httpx 21 | distro==1.8.0 22 | # via arcadepy 23 | exceptiongroup==1.2.2 24 | # via anyio 25 | h11==0.14.0 26 | # via httpcore 27 | httpcore==1.0.2 28 | # via httpx 29 | httpx==0.28.1 30 | # via arcadepy 31 | idna==3.4 32 | # via anyio 33 | # via httpx 34 | pydantic==2.10.3 35 | # via arcadepy 36 | pydantic-core==2.27.1 37 | # via pydantic 38 | sniffio==1.3.0 39 | # via anyio 40 | # via arcadepy 41 | typing-extensions==4.12.2 42 | # via anyio 43 | # via arcadepy 44 | # via pydantic 45 | # via pydantic-core 46 | -------------------------------------------------------------------------------- /scripts/bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | if ! command -v rye >/dev/null 2>&1 && [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then 8 | brew bundle check >/dev/null 2>&1 || { 9 | echo "==> Installing Homebrew dependencies…" 10 | brew bundle 11 | } 12 | fi 13 | 14 | echo "==> Installing Python dependencies…" 15 | 16 | # experimental uv support makes installations significantly faster 17 | rye config --set-bool behavior.use-uv=true 18 | 19 | rye sync --all-features 20 | -------------------------------------------------------------------------------- /scripts/format: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | echo "==> Running formatters" 8 | rye run format 9 | -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | echo "==> Running lints" 8 | rye run lint 9 | 10 | echo "==> Making sure it imports" 11 | rye run python -c 'import arcadepy' 12 | -------------------------------------------------------------------------------- /scripts/mock: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | if [[ -n "$1" && "$1" != '--'* ]]; then 8 | URL="$1" 9 | shift 10 | else 11 | URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" 12 | fi 13 | 14 | # Check if the URL is empty 15 | if [ -z "$URL" ]; then 16 | echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" 17 | exit 1 18 | fi 19 | 20 | echo "==> Starting mock server with URL ${URL}" 21 | 22 | # Run prism mock on the given spec 23 | if [ "$1" == "--daemon" ]; then 24 | npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" &> .prism.log & 25 | 26 | # Wait for server to come online 27 | echo -n "Waiting for server" 28 | while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do 29 | echo -n "." 30 | sleep 0.1 31 | done 32 | 33 | if grep -q "✖ fatal" ".prism.log"; then 34 | cat .prism.log 35 | exit 1 36 | fi 37 | 38 | echo 39 | else 40 | npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" 41 | fi 42 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | RED='\033[0;31m' 8 | GREEN='\033[0;32m' 9 | YELLOW='\033[0;33m' 10 | NC='\033[0m' # No Color 11 | 12 | function prism_is_running() { 13 | curl --silent "http://localhost:4010" >/dev/null 2>&1 14 | } 15 | 16 | kill_server_on_port() { 17 | pids=$(lsof -t -i tcp:"$1" || echo "") 18 | if [ "$pids" != "" ]; then 19 | kill "$pids" 20 | echo "Stopped $pids." 21 | fi 22 | } 23 | 24 | function is_overriding_api_base_url() { 25 | [ -n "$TEST_API_BASE_URL" ] 26 | } 27 | 28 | if ! is_overriding_api_base_url && ! prism_is_running ; then 29 | # When we exit this script, make sure to kill the background mock server process 30 | trap 'kill_server_on_port 4010' EXIT 31 | 32 | # Start the dev server 33 | ./scripts/mock --daemon 34 | fi 35 | 36 | if is_overriding_api_base_url ; then 37 | echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" 38 | echo 39 | elif ! prism_is_running ; then 40 | echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" 41 | echo -e "running against your OpenAPI spec." 42 | echo 43 | echo -e "To run the server, pass in the path or url of your OpenAPI" 44 | echo -e "spec to the prism command:" 45 | echo 46 | echo -e " \$ ${YELLOW}npm exec --package=@stoplight/prism-cli@~5.3.2 -- prism mock path/to/your.openapi.yml${NC}" 47 | echo 48 | 49 | exit 1 50 | else 51 | echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" 52 | echo 53 | fi 54 | 55 | export DEFER_PYDANTIC_BUILD=false 56 | 57 | echo "==> Running tests" 58 | rye run pytest "$@" 59 | 60 | echo "==> Running Pydantic v1 tests" 61 | rye run nox -s test-pydantic-v1 -- "$@" 62 | -------------------------------------------------------------------------------- /scripts/utils/ruffen-docs.py: -------------------------------------------------------------------------------- 1 | # fork of https://github.com/asottile/blacken-docs adapted for ruff 2 | from __future__ import annotations 3 | 4 | import re 5 | import sys 6 | import argparse 7 | import textwrap 8 | import contextlib 9 | import subprocess 10 | from typing import Match, Optional, Sequence, Generator, NamedTuple, cast 11 | 12 | MD_RE = re.compile( 13 | r"(?P^(?P *)```\s*python\n)" r"(?P.*?)" r"(?P^(?P=indent)```\s*$)", 14 | re.DOTALL | re.MULTILINE, 15 | ) 16 | MD_PYCON_RE = re.compile( 17 | r"(?P^(?P *)```\s*pycon\n)" r"(?P.*?)" r"(?P^(?P=indent)```.*$)", 18 | re.DOTALL | re.MULTILINE, 19 | ) 20 | PYCON_PREFIX = ">>> " 21 | PYCON_CONTINUATION_PREFIX = "..." 22 | PYCON_CONTINUATION_RE = re.compile( 23 | rf"^{re.escape(PYCON_CONTINUATION_PREFIX)}( |$)", 24 | ) 25 | DEFAULT_LINE_LENGTH = 100 26 | 27 | 28 | class CodeBlockError(NamedTuple): 29 | offset: int 30 | exc: Exception 31 | 32 | 33 | def format_str( 34 | src: str, 35 | ) -> tuple[str, Sequence[CodeBlockError]]: 36 | errors: list[CodeBlockError] = [] 37 | 38 | @contextlib.contextmanager 39 | def _collect_error(match: Match[str]) -> Generator[None, None, None]: 40 | try: 41 | yield 42 | except Exception as e: 43 | errors.append(CodeBlockError(match.start(), e)) 44 | 45 | def _md_match(match: Match[str]) -> str: 46 | code = textwrap.dedent(match["code"]) 47 | with _collect_error(match): 48 | code = format_code_block(code) 49 | code = textwrap.indent(code, match["indent"]) 50 | return f"{match['before']}{code}{match['after']}" 51 | 52 | def _pycon_match(match: Match[str]) -> str: 53 | code = "" 54 | fragment = cast(Optional[str], None) 55 | 56 | def finish_fragment() -> None: 57 | nonlocal code 58 | nonlocal fragment 59 | 60 | if fragment is not None: 61 | with _collect_error(match): 62 | fragment = format_code_block(fragment) 63 | fragment_lines = fragment.splitlines() 64 | code += f"{PYCON_PREFIX}{fragment_lines[0]}\n" 65 | for line in fragment_lines[1:]: 66 | # Skip blank lines to handle Black adding a blank above 67 | # functions within blocks. A blank line would end the REPL 68 | # continuation prompt. 69 | # 70 | # >>> if True: 71 | # ... def f(): 72 | # ... pass 73 | # ... 74 | if line: 75 | code += f"{PYCON_CONTINUATION_PREFIX} {line}\n" 76 | if fragment_lines[-1].startswith(" "): 77 | code += f"{PYCON_CONTINUATION_PREFIX}\n" 78 | fragment = None 79 | 80 | indentation = None 81 | for line in match["code"].splitlines(): 82 | orig_line, line = line, line.lstrip() 83 | if indentation is None and line: 84 | indentation = len(orig_line) - len(line) 85 | continuation_match = PYCON_CONTINUATION_RE.match(line) 86 | if continuation_match and fragment is not None: 87 | fragment += line[continuation_match.end() :] + "\n" 88 | else: 89 | finish_fragment() 90 | if line.startswith(PYCON_PREFIX): 91 | fragment = line[len(PYCON_PREFIX) :] + "\n" 92 | else: 93 | code += orig_line[indentation:] + "\n" 94 | finish_fragment() 95 | return code 96 | 97 | def _md_pycon_match(match: Match[str]) -> str: 98 | code = _pycon_match(match) 99 | code = textwrap.indent(code, match["indent"]) 100 | return f"{match['before']}{code}{match['after']}" 101 | 102 | src = MD_RE.sub(_md_match, src) 103 | src = MD_PYCON_RE.sub(_md_pycon_match, src) 104 | return src, errors 105 | 106 | 107 | def format_code_block(code: str) -> str: 108 | return subprocess.check_output( 109 | [ 110 | sys.executable, 111 | "-m", 112 | "ruff", 113 | "format", 114 | "--stdin-filename=script.py", 115 | f"--line-length={DEFAULT_LINE_LENGTH}", 116 | ], 117 | encoding="utf-8", 118 | input=code, 119 | ) 120 | 121 | 122 | def format_file( 123 | filename: str, 124 | skip_errors: bool, 125 | ) -> int: 126 | with open(filename, encoding="UTF-8") as f: 127 | contents = f.read() 128 | new_contents, errors = format_str(contents) 129 | for error in errors: 130 | lineno = contents[: error.offset].count("\n") + 1 131 | print(f"{filename}:{lineno}: code block parse error {error.exc}") 132 | if errors and not skip_errors: 133 | return 1 134 | if contents != new_contents: 135 | print(f"{filename}: Rewriting...") 136 | with open(filename, "w", encoding="UTF-8") as f: 137 | f.write(new_contents) 138 | return 0 139 | else: 140 | return 0 141 | 142 | 143 | def main(argv: Sequence[str] | None = None) -> int: 144 | parser = argparse.ArgumentParser() 145 | parser.add_argument( 146 | "-l", 147 | "--line-length", 148 | type=int, 149 | default=DEFAULT_LINE_LENGTH, 150 | ) 151 | parser.add_argument( 152 | "-S", 153 | "--skip-string-normalization", 154 | action="store_true", 155 | ) 156 | parser.add_argument("-E", "--skip-errors", action="store_true") 157 | parser.add_argument("filenames", nargs="*") 158 | args = parser.parse_args(argv) 159 | 160 | retv = 0 161 | for filename in args.filenames: 162 | retv |= format_file(filename, skip_errors=args.skip_errors) 163 | return retv 164 | 165 | 166 | if __name__ == "__main__": 167 | raise SystemExit(main()) 168 | -------------------------------------------------------------------------------- /scripts/utils/upload-artifact.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -exuo pipefail 3 | 4 | RESPONSE=$(curl -X POST "$URL" \ 5 | -H "Authorization: Bearer $AUTH" \ 6 | -H "Content-Type: application/json") 7 | 8 | SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url') 9 | 10 | if [[ "$SIGNED_URL" == "null" ]]; then 11 | echo -e "\033[31mFailed to get signed URL.\033[0m" 12 | exit 1 13 | fi 14 | 15 | UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ 16 | -H "Content-Type: application/gzip" \ 17 | --data-binary @- "$SIGNED_URL" 2>&1) 18 | 19 | if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then 20 | echo -e "\033[32mUploaded build to Stainless storage.\033[0m" 21 | echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/arcade-engine-python/$SHA'\033[0m" 22 | else 23 | echo -e "\033[31mFailed to upload artifact.\033[0m" 24 | exit 1 25 | fi 26 | -------------------------------------------------------------------------------- /src/arcadepy/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | import typing as _t 4 | 5 | from . import types 6 | from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes 7 | from ._utils import file_from_path 8 | from ._client import Arcade, Client, Stream, Timeout, Transport, AsyncArcade, AsyncClient, AsyncStream, RequestOptions 9 | from ._models import BaseModel 10 | from ._version import __title__, __version__ 11 | from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse 12 | from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS 13 | from ._exceptions import ( 14 | APIError, 15 | ArcadeError, 16 | ConflictError, 17 | NotFoundError, 18 | APIStatusError, 19 | RateLimitError, 20 | APITimeoutError, 21 | BadRequestError, 22 | APIConnectionError, 23 | AuthenticationError, 24 | InternalServerError, 25 | PermissionDeniedError, 26 | UnprocessableEntityError, 27 | APIResponseValidationError, 28 | ) 29 | from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient 30 | from ._utils._logs import setup_logging as _setup_logging 31 | 32 | __all__ = [ 33 | "types", 34 | "__version__", 35 | "__title__", 36 | "NoneType", 37 | "Transport", 38 | "ProxiesTypes", 39 | "NotGiven", 40 | "NOT_GIVEN", 41 | "Omit", 42 | "ArcadeError", 43 | "APIError", 44 | "APIStatusError", 45 | "APITimeoutError", 46 | "APIConnectionError", 47 | "APIResponseValidationError", 48 | "BadRequestError", 49 | "AuthenticationError", 50 | "PermissionDeniedError", 51 | "NotFoundError", 52 | "ConflictError", 53 | "UnprocessableEntityError", 54 | "RateLimitError", 55 | "InternalServerError", 56 | "Timeout", 57 | "RequestOptions", 58 | "Client", 59 | "AsyncClient", 60 | "Stream", 61 | "AsyncStream", 62 | "Arcade", 63 | "AsyncArcade", 64 | "file_from_path", 65 | "BaseModel", 66 | "DEFAULT_TIMEOUT", 67 | "DEFAULT_MAX_RETRIES", 68 | "DEFAULT_CONNECTION_LIMITS", 69 | "DefaultHttpxClient", 70 | "DefaultAsyncHttpxClient", 71 | ] 72 | 73 | if not _t.TYPE_CHECKING: 74 | from ._utils._resources_proxy import resources as resources 75 | 76 | _setup_logging() 77 | 78 | # Update the __module__ attribute for exported symbols so that 79 | # error messages point to this module instead of the module 80 | # it was originally defined in, e.g. 81 | # arcadepy._exceptions.NotFoundError -> arcadepy.NotFoundError 82 | __locals = locals() 83 | for __name in __all__: 84 | if not __name.startswith("__"): 85 | try: 86 | __locals[__name].__module__ = "arcadepy" 87 | except (TypeError, AttributeError): 88 | # Some of our exported symbols are builtins which we can't set attributes for. 89 | pass 90 | -------------------------------------------------------------------------------- /src/arcadepy/_compat.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload 4 | from datetime import date, datetime 5 | from typing_extensions import Self, Literal 6 | 7 | import pydantic 8 | from pydantic.fields import FieldInfo 9 | 10 | from ._types import IncEx, StrBytesIntFloat 11 | 12 | _T = TypeVar("_T") 13 | _ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) 14 | 15 | # --------------- Pydantic v2 compatibility --------------- 16 | 17 | # Pyright incorrectly reports some of our functions as overriding a method when they don't 18 | # pyright: reportIncompatibleMethodOverride=false 19 | 20 | PYDANTIC_V2 = pydantic.VERSION.startswith("2.") 21 | 22 | # v1 re-exports 23 | if TYPE_CHECKING: 24 | 25 | def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001 26 | ... 27 | 28 | def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: # noqa: ARG001 29 | ... 30 | 31 | def get_args(t: type[Any]) -> tuple[Any, ...]: # noqa: ARG001 32 | ... 33 | 34 | def is_union(tp: type[Any] | None) -> bool: # noqa: ARG001 35 | ... 36 | 37 | def get_origin(t: type[Any]) -> type[Any] | None: # noqa: ARG001 38 | ... 39 | 40 | def is_literal_type(type_: type[Any]) -> bool: # noqa: ARG001 41 | ... 42 | 43 | def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001 44 | ... 45 | 46 | else: 47 | if PYDANTIC_V2: 48 | from pydantic.v1.typing import ( 49 | get_args as get_args, 50 | is_union as is_union, 51 | get_origin as get_origin, 52 | is_typeddict as is_typeddict, 53 | is_literal_type as is_literal_type, 54 | ) 55 | from pydantic.v1.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime 56 | else: 57 | from pydantic.typing import ( 58 | get_args as get_args, 59 | is_union as is_union, 60 | get_origin as get_origin, 61 | is_typeddict as is_typeddict, 62 | is_literal_type as is_literal_type, 63 | ) 64 | from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime 65 | 66 | 67 | # refactored config 68 | if TYPE_CHECKING: 69 | from pydantic import ConfigDict as ConfigDict 70 | else: 71 | if PYDANTIC_V2: 72 | from pydantic import ConfigDict 73 | else: 74 | # TODO: provide an error message here? 75 | ConfigDict = None 76 | 77 | 78 | # renamed methods / properties 79 | def parse_obj(model: type[_ModelT], value: object) -> _ModelT: 80 | if PYDANTIC_V2: 81 | return model.model_validate(value) 82 | else: 83 | return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] 84 | 85 | 86 | def field_is_required(field: FieldInfo) -> bool: 87 | if PYDANTIC_V2: 88 | return field.is_required() 89 | return field.required # type: ignore 90 | 91 | 92 | def field_get_default(field: FieldInfo) -> Any: 93 | value = field.get_default() 94 | if PYDANTIC_V2: 95 | from pydantic_core import PydanticUndefined 96 | 97 | if value == PydanticUndefined: 98 | return None 99 | return value 100 | return value 101 | 102 | 103 | def field_outer_type(field: FieldInfo) -> Any: 104 | if PYDANTIC_V2: 105 | return field.annotation 106 | return field.outer_type_ # type: ignore 107 | 108 | 109 | def get_model_config(model: type[pydantic.BaseModel]) -> Any: 110 | if PYDANTIC_V2: 111 | return model.model_config 112 | return model.__config__ # type: ignore 113 | 114 | 115 | def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: 116 | if PYDANTIC_V2: 117 | return model.model_fields 118 | return model.__fields__ # type: ignore 119 | 120 | 121 | def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: 122 | if PYDANTIC_V2: 123 | return model.model_copy(deep=deep) 124 | return model.copy(deep=deep) # type: ignore 125 | 126 | 127 | def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: 128 | if PYDANTIC_V2: 129 | return model.model_dump_json(indent=indent) 130 | return model.json(indent=indent) # type: ignore 131 | 132 | 133 | def model_dump( 134 | model: pydantic.BaseModel, 135 | *, 136 | exclude: IncEx | None = None, 137 | exclude_unset: bool = False, 138 | exclude_defaults: bool = False, 139 | warnings: bool = True, 140 | mode: Literal["json", "python"] = "python", 141 | ) -> dict[str, Any]: 142 | if PYDANTIC_V2 or hasattr(model, "model_dump"): 143 | return model.model_dump( 144 | mode=mode, 145 | exclude=exclude, 146 | exclude_unset=exclude_unset, 147 | exclude_defaults=exclude_defaults, 148 | # warnings are not supported in Pydantic v1 149 | warnings=warnings if PYDANTIC_V2 else True, 150 | ) 151 | return cast( 152 | "dict[str, Any]", 153 | model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] 154 | exclude=exclude, 155 | exclude_unset=exclude_unset, 156 | exclude_defaults=exclude_defaults, 157 | ), 158 | ) 159 | 160 | 161 | def model_parse(model: type[_ModelT], data: Any) -> _ModelT: 162 | if PYDANTIC_V2: 163 | return model.model_validate(data) 164 | return model.parse_obj(data) # pyright: ignore[reportDeprecated] 165 | 166 | 167 | # generic models 168 | if TYPE_CHECKING: 169 | 170 | class GenericModel(pydantic.BaseModel): ... 171 | 172 | else: 173 | if PYDANTIC_V2: 174 | # there no longer needs to be a distinction in v2 but 175 | # we still have to create our own subclass to avoid 176 | # inconsistent MRO ordering errors 177 | class GenericModel(pydantic.BaseModel): ... 178 | 179 | else: 180 | import pydantic.generics 181 | 182 | class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... 183 | 184 | 185 | # cached properties 186 | if TYPE_CHECKING: 187 | cached_property = property 188 | 189 | # we define a separate type (copied from typeshed) 190 | # that represents that `cached_property` is `set`able 191 | # at runtime, which differs from `@property`. 192 | # 193 | # this is a separate type as editors likely special case 194 | # `@property` and we don't want to cause issues just to have 195 | # more helpful internal types. 196 | 197 | class typed_cached_property(Generic[_T]): 198 | func: Callable[[Any], _T] 199 | attrname: str | None 200 | 201 | def __init__(self, func: Callable[[Any], _T]) -> None: ... 202 | 203 | @overload 204 | def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... 205 | 206 | @overload 207 | def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: ... 208 | 209 | def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self: 210 | raise NotImplementedError() 211 | 212 | def __set_name__(self, owner: type[Any], name: str) -> None: ... 213 | 214 | # __set__ is not defined at runtime, but @cached_property is designed to be settable 215 | def __set__(self, instance: object, value: _T) -> None: ... 216 | else: 217 | from functools import cached_property as cached_property 218 | 219 | typed_cached_property = cached_property 220 | -------------------------------------------------------------------------------- /src/arcadepy/_constants.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | import httpx 4 | 5 | RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response" 6 | OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" 7 | 8 | # default timeout is 1 minute 9 | DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0) 10 | DEFAULT_MAX_RETRIES = 2 11 | DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) 12 | 13 | INITIAL_RETRY_DELAY = 0.5 14 | MAX_RETRY_DELAY = 8.0 15 | -------------------------------------------------------------------------------- /src/arcadepy/_exceptions.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import Literal 6 | 7 | import httpx 8 | 9 | __all__ = [ 10 | "BadRequestError", 11 | "AuthenticationError", 12 | "PermissionDeniedError", 13 | "NotFoundError", 14 | "ConflictError", 15 | "UnprocessableEntityError", 16 | "RateLimitError", 17 | "InternalServerError", 18 | ] 19 | 20 | 21 | class ArcadeError(Exception): 22 | pass 23 | 24 | 25 | class APIError(ArcadeError): 26 | message: str 27 | request: httpx.Request 28 | 29 | body: object | None 30 | """The API response body. 31 | 32 | If the API responded with a valid JSON structure then this property will be the 33 | decoded result. 34 | 35 | If it isn't a valid JSON structure then this will be the raw response. 36 | 37 | If there was no response associated with this error then it will be `None`. 38 | """ 39 | 40 | def __init__(self, message: str, request: httpx.Request, *, body: object | None) -> None: # noqa: ARG002 41 | super().__init__(message) 42 | self.request = request 43 | self.message = message 44 | self.body = body 45 | 46 | 47 | class APIResponseValidationError(APIError): 48 | response: httpx.Response 49 | status_code: int 50 | 51 | def __init__(self, response: httpx.Response, body: object | None, *, message: str | None = None) -> None: 52 | super().__init__(message or "Data returned by API invalid for expected schema.", response.request, body=body) 53 | self.response = response 54 | self.status_code = response.status_code 55 | 56 | 57 | class APIStatusError(APIError): 58 | """Raised when an API response has a status code of 4xx or 5xx.""" 59 | 60 | response: httpx.Response 61 | status_code: int 62 | 63 | def __init__(self, message: str, *, response: httpx.Response, body: object | None) -> None: 64 | super().__init__(message, response.request, body=body) 65 | self.response = response 66 | self.status_code = response.status_code 67 | 68 | 69 | class APIConnectionError(APIError): 70 | def __init__(self, *, message: str = "Connection error.", request: httpx.Request) -> None: 71 | super().__init__(message, request, body=None) 72 | 73 | 74 | class APITimeoutError(APIConnectionError): 75 | def __init__(self, request: httpx.Request) -> None: 76 | super().__init__(message="Request timed out.", request=request) 77 | 78 | 79 | class BadRequestError(APIStatusError): 80 | status_code: Literal[400] = 400 # pyright: ignore[reportIncompatibleVariableOverride] 81 | 82 | 83 | class AuthenticationError(APIStatusError): 84 | status_code: Literal[401] = 401 # pyright: ignore[reportIncompatibleVariableOverride] 85 | 86 | 87 | class PermissionDeniedError(APIStatusError): 88 | status_code: Literal[403] = 403 # pyright: ignore[reportIncompatibleVariableOverride] 89 | 90 | 91 | class NotFoundError(APIStatusError): 92 | status_code: Literal[404] = 404 # pyright: ignore[reportIncompatibleVariableOverride] 93 | 94 | 95 | class ConflictError(APIStatusError): 96 | status_code: Literal[409] = 409 # pyright: ignore[reportIncompatibleVariableOverride] 97 | 98 | 99 | class UnprocessableEntityError(APIStatusError): 100 | status_code: Literal[422] = 422 # pyright: ignore[reportIncompatibleVariableOverride] 101 | 102 | 103 | class RateLimitError(APIStatusError): 104 | status_code: Literal[429] = 429 # pyright: ignore[reportIncompatibleVariableOverride] 105 | 106 | 107 | class InternalServerError(APIStatusError): 108 | pass 109 | -------------------------------------------------------------------------------- /src/arcadepy/_files.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import io 4 | import os 5 | import pathlib 6 | from typing import overload 7 | from typing_extensions import TypeGuard 8 | 9 | import anyio 10 | 11 | from ._types import ( 12 | FileTypes, 13 | FileContent, 14 | RequestFiles, 15 | HttpxFileTypes, 16 | Base64FileInput, 17 | HttpxFileContent, 18 | HttpxRequestFiles, 19 | ) 20 | from ._utils import is_tuple_t, is_mapping_t, is_sequence_t 21 | 22 | 23 | def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: 24 | return isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) 25 | 26 | 27 | def is_file_content(obj: object) -> TypeGuard[FileContent]: 28 | return ( 29 | isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) 30 | ) 31 | 32 | 33 | def assert_is_file_content(obj: object, *, key: str | None = None) -> None: 34 | if not is_file_content(obj): 35 | prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" 36 | raise RuntimeError( 37 | f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." 38 | ) from None 39 | 40 | 41 | @overload 42 | def to_httpx_files(files: None) -> None: ... 43 | 44 | 45 | @overload 46 | def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... 47 | 48 | 49 | def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: 50 | if files is None: 51 | return None 52 | 53 | if is_mapping_t(files): 54 | files = {key: _transform_file(file) for key, file in files.items()} 55 | elif is_sequence_t(files): 56 | files = [(key, _transform_file(file)) for key, file in files] 57 | else: 58 | raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") 59 | 60 | return files 61 | 62 | 63 | def _transform_file(file: FileTypes) -> HttpxFileTypes: 64 | if is_file_content(file): 65 | if isinstance(file, os.PathLike): 66 | path = pathlib.Path(file) 67 | return (path.name, path.read_bytes()) 68 | 69 | return file 70 | 71 | if is_tuple_t(file): 72 | return (file[0], _read_file_content(file[1]), *file[2:]) 73 | 74 | raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") 75 | 76 | 77 | def _read_file_content(file: FileContent) -> HttpxFileContent: 78 | if isinstance(file, os.PathLike): 79 | return pathlib.Path(file).read_bytes() 80 | return file 81 | 82 | 83 | @overload 84 | async def async_to_httpx_files(files: None) -> None: ... 85 | 86 | 87 | @overload 88 | async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... 89 | 90 | 91 | async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: 92 | if files is None: 93 | return None 94 | 95 | if is_mapping_t(files): 96 | files = {key: await _async_transform_file(file) for key, file in files.items()} 97 | elif is_sequence_t(files): 98 | files = [(key, await _async_transform_file(file)) for key, file in files] 99 | else: 100 | raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") 101 | 102 | return files 103 | 104 | 105 | async def _async_transform_file(file: FileTypes) -> HttpxFileTypes: 106 | if is_file_content(file): 107 | if isinstance(file, os.PathLike): 108 | path = anyio.Path(file) 109 | return (path.name, await path.read_bytes()) 110 | 111 | return file 112 | 113 | if is_tuple_t(file): 114 | return (file[0], await _async_read_file_content(file[1]), *file[2:]) 115 | 116 | raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") 117 | 118 | 119 | async def _async_read_file_content(file: FileContent) -> HttpxFileContent: 120 | if isinstance(file, os.PathLike): 121 | return await anyio.Path(file).read_bytes() 122 | 123 | return file 124 | -------------------------------------------------------------------------------- /src/arcadepy/_qs.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, List, Tuple, Union, Mapping, TypeVar 4 | from urllib.parse import parse_qs, urlencode 5 | from typing_extensions import Literal, get_args 6 | 7 | from ._types import NOT_GIVEN, NotGiven, NotGivenOr 8 | from ._utils import flatten 9 | 10 | _T = TypeVar("_T") 11 | 12 | 13 | ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] 14 | NestedFormat = Literal["dots", "brackets"] 15 | 16 | PrimitiveData = Union[str, int, float, bool, None] 17 | # this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] 18 | # https://github.com/microsoft/pyright/issues/3555 19 | Data = Union[PrimitiveData, List[Any], Tuple[Any], "Mapping[str, Any]"] 20 | Params = Mapping[str, Data] 21 | 22 | 23 | class Querystring: 24 | array_format: ArrayFormat 25 | nested_format: NestedFormat 26 | 27 | def __init__( 28 | self, 29 | *, 30 | array_format: ArrayFormat = "repeat", 31 | nested_format: NestedFormat = "brackets", 32 | ) -> None: 33 | self.array_format = array_format 34 | self.nested_format = nested_format 35 | 36 | def parse(self, query: str) -> Mapping[str, object]: 37 | # Note: custom format syntax is not supported yet 38 | return parse_qs(query) 39 | 40 | def stringify( 41 | self, 42 | params: Params, 43 | *, 44 | array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, 45 | nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, 46 | ) -> str: 47 | return urlencode( 48 | self.stringify_items( 49 | params, 50 | array_format=array_format, 51 | nested_format=nested_format, 52 | ) 53 | ) 54 | 55 | def stringify_items( 56 | self, 57 | params: Params, 58 | *, 59 | array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, 60 | nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, 61 | ) -> list[tuple[str, str]]: 62 | opts = Options( 63 | qs=self, 64 | array_format=array_format, 65 | nested_format=nested_format, 66 | ) 67 | return flatten([self._stringify_item(key, value, opts) for key, value in params.items()]) 68 | 69 | def _stringify_item( 70 | self, 71 | key: str, 72 | value: Data, 73 | opts: Options, 74 | ) -> list[tuple[str, str]]: 75 | if isinstance(value, Mapping): 76 | items: list[tuple[str, str]] = [] 77 | nested_format = opts.nested_format 78 | for subkey, subvalue in value.items(): 79 | items.extend( 80 | self._stringify_item( 81 | # TODO: error if unknown format 82 | f"{key}.{subkey}" if nested_format == "dots" else f"{key}[{subkey}]", 83 | subvalue, 84 | opts, 85 | ) 86 | ) 87 | return items 88 | 89 | if isinstance(value, (list, tuple)): 90 | array_format = opts.array_format 91 | if array_format == "comma": 92 | return [ 93 | ( 94 | key, 95 | ",".join(self._primitive_value_to_str(item) for item in value if item is not None), 96 | ), 97 | ] 98 | elif array_format == "repeat": 99 | items = [] 100 | for item in value: 101 | items.extend(self._stringify_item(key, item, opts)) 102 | return items 103 | elif array_format == "indices": 104 | raise NotImplementedError("The array indices format is not supported yet") 105 | elif array_format == "brackets": 106 | items = [] 107 | key = key + "[]" 108 | for item in value: 109 | items.extend(self._stringify_item(key, item, opts)) 110 | return items 111 | else: 112 | raise NotImplementedError( 113 | f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" 114 | ) 115 | 116 | serialised = self._primitive_value_to_str(value) 117 | if not serialised: 118 | return [] 119 | return [(key, serialised)] 120 | 121 | def _primitive_value_to_str(self, value: PrimitiveData) -> str: 122 | # copied from httpx 123 | if value is True: 124 | return "true" 125 | elif value is False: 126 | return "false" 127 | elif value is None: 128 | return "" 129 | return str(value) 130 | 131 | 132 | _qs = Querystring() 133 | parse = _qs.parse 134 | stringify = _qs.stringify 135 | stringify_items = _qs.stringify_items 136 | 137 | 138 | class Options: 139 | array_format: ArrayFormat 140 | nested_format: NestedFormat 141 | 142 | def __init__( 143 | self, 144 | qs: Querystring = _qs, 145 | *, 146 | array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, 147 | nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, 148 | ) -> None: 149 | self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format 150 | self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format 151 | -------------------------------------------------------------------------------- /src/arcadepy/_resource.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | import time 6 | from typing import TYPE_CHECKING 7 | 8 | import anyio 9 | 10 | if TYPE_CHECKING: 11 | from ._client import Arcade, AsyncArcade 12 | 13 | 14 | class SyncAPIResource: 15 | _client: Arcade 16 | 17 | def __init__(self, client: Arcade) -> None: 18 | self._client = client 19 | self._get = client.get 20 | self._post = client.post 21 | self._patch = client.patch 22 | self._put = client.put 23 | self._delete = client.delete 24 | self._get_api_list = client.get_api_list 25 | 26 | def _sleep(self, seconds: float) -> None: 27 | time.sleep(seconds) 28 | 29 | 30 | class AsyncAPIResource: 31 | _client: AsyncArcade 32 | 33 | def __init__(self, client: AsyncArcade) -> None: 34 | self._client = client 35 | self._get = client.get 36 | self._post = client.post 37 | self._patch = client.patch 38 | self._put = client.put 39 | self._delete = client.delete 40 | self._get_api_list = client.get_api_list 41 | 42 | async def _sleep(self, seconds: float) -> None: 43 | await anyio.sleep(seconds) 44 | -------------------------------------------------------------------------------- /src/arcadepy/_types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from os import PathLike 4 | from typing import ( 5 | IO, 6 | TYPE_CHECKING, 7 | Any, 8 | Dict, 9 | List, 10 | Type, 11 | Tuple, 12 | Union, 13 | Mapping, 14 | TypeVar, 15 | Callable, 16 | Optional, 17 | Sequence, 18 | ) 19 | from typing_extensions import Set, Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable 20 | 21 | import httpx 22 | import pydantic 23 | from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport 24 | 25 | if TYPE_CHECKING: 26 | from ._models import BaseModel 27 | from ._response import APIResponse, AsyncAPIResponse 28 | 29 | Transport = BaseTransport 30 | AsyncTransport = AsyncBaseTransport 31 | Query = Mapping[str, object] 32 | Body = object 33 | AnyMapping = Mapping[str, object] 34 | ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) 35 | _T = TypeVar("_T") 36 | 37 | 38 | # Approximates httpx internal ProxiesTypes and RequestFiles types 39 | # while adding support for `PathLike` instances 40 | ProxiesDict = Dict["str | URL", Union[None, str, URL, Proxy]] 41 | ProxiesTypes = Union[str, Proxy, ProxiesDict] 42 | if TYPE_CHECKING: 43 | Base64FileInput = Union[IO[bytes], PathLike[str]] 44 | FileContent = Union[IO[bytes], bytes, PathLike[str]] 45 | else: 46 | Base64FileInput = Union[IO[bytes], PathLike] 47 | FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. 48 | FileTypes = Union[ 49 | # file (or bytes) 50 | FileContent, 51 | # (filename, file (or bytes)) 52 | Tuple[Optional[str], FileContent], 53 | # (filename, file (or bytes), content_type) 54 | Tuple[Optional[str], FileContent, Optional[str]], 55 | # (filename, file (or bytes), content_type, headers) 56 | Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], 57 | ] 58 | RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]] 59 | 60 | # duplicate of the above but without our custom file support 61 | HttpxFileContent = Union[IO[bytes], bytes] 62 | HttpxFileTypes = Union[ 63 | # file (or bytes) 64 | HttpxFileContent, 65 | # (filename, file (or bytes)) 66 | Tuple[Optional[str], HttpxFileContent], 67 | # (filename, file (or bytes), content_type) 68 | Tuple[Optional[str], HttpxFileContent, Optional[str]], 69 | # (filename, file (or bytes), content_type, headers) 70 | Tuple[Optional[str], HttpxFileContent, Optional[str], Mapping[str, str]], 71 | ] 72 | HttpxRequestFiles = Union[Mapping[str, HttpxFileTypes], Sequence[Tuple[str, HttpxFileTypes]]] 73 | 74 | # Workaround to support (cast_to: Type[ResponseT]) -> ResponseT 75 | # where ResponseT includes `None`. In order to support directly 76 | # passing `None`, overloads would have to be defined for every 77 | # method that uses `ResponseT` which would lead to an unacceptable 78 | # amount of code duplication and make it unreadable. See _base_client.py 79 | # for example usage. 80 | # 81 | # This unfortunately means that you will either have 82 | # to import this type and pass it explicitly: 83 | # 84 | # from arcadepy import NoneType 85 | # client.get('/foo', cast_to=NoneType) 86 | # 87 | # or build it yourself: 88 | # 89 | # client.get('/foo', cast_to=type(None)) 90 | if TYPE_CHECKING: 91 | NoneType: Type[None] 92 | else: 93 | NoneType = type(None) 94 | 95 | 96 | class RequestOptions(TypedDict, total=False): 97 | headers: Headers 98 | max_retries: int 99 | timeout: float | Timeout | None 100 | params: Query 101 | extra_json: AnyMapping 102 | idempotency_key: str 103 | 104 | 105 | # Sentinel class used until PEP 0661 is accepted 106 | class NotGiven: 107 | """ 108 | A sentinel singleton class used to distinguish omitted keyword arguments 109 | from those passed in with the value None (which may have different behavior). 110 | 111 | For example: 112 | 113 | ```py 114 | def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ... 115 | 116 | 117 | get(timeout=1) # 1s timeout 118 | get(timeout=None) # No timeout 119 | get() # Default timeout behavior, which may not be statically known at the method definition. 120 | ``` 121 | """ 122 | 123 | def __bool__(self) -> Literal[False]: 124 | return False 125 | 126 | @override 127 | def __repr__(self) -> str: 128 | return "NOT_GIVEN" 129 | 130 | 131 | NotGivenOr = Union[_T, NotGiven] 132 | NOT_GIVEN = NotGiven() 133 | 134 | 135 | class Omit: 136 | """In certain situations you need to be able to represent a case where a default value has 137 | to be explicitly removed and `None` is not an appropriate substitute, for example: 138 | 139 | ```py 140 | # as the default `Content-Type` header is `application/json` that will be sent 141 | client.post("/upload/files", files={"file": b"my raw file content"}) 142 | 143 | # you can't explicitly override the header as it has to be dynamically generated 144 | # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983' 145 | client.post(..., headers={"Content-Type": "multipart/form-data"}) 146 | 147 | # instead you can remove the default `application/json` header by passing Omit 148 | client.post(..., headers={"Content-Type": Omit()}) 149 | ``` 150 | """ 151 | 152 | def __bool__(self) -> Literal[False]: 153 | return False 154 | 155 | 156 | @runtime_checkable 157 | class ModelBuilderProtocol(Protocol): 158 | @classmethod 159 | def build( 160 | cls: type[_T], 161 | *, 162 | response: Response, 163 | data: object, 164 | ) -> _T: ... 165 | 166 | 167 | Headers = Mapping[str, Union[str, Omit]] 168 | 169 | 170 | class HeadersLikeProtocol(Protocol): 171 | def get(self, __key: str) -> str | None: ... 172 | 173 | 174 | HeadersLike = Union[Headers, HeadersLikeProtocol] 175 | 176 | ResponseT = TypeVar( 177 | "ResponseT", 178 | bound=Union[ 179 | object, 180 | str, 181 | None, 182 | "BaseModel", 183 | List[Any], 184 | Dict[str, Any], 185 | Response, 186 | ModelBuilderProtocol, 187 | "APIResponse[Any]", 188 | "AsyncAPIResponse[Any]", 189 | ], 190 | ) 191 | 192 | StrBytesIntFloat = Union[str, bytes, int, float] 193 | 194 | # Note: copied from Pydantic 195 | # https://github.com/pydantic/pydantic/blob/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79 196 | IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]] 197 | 198 | PostParser = Callable[[Any], Any] 199 | 200 | 201 | @runtime_checkable 202 | class InheritsGeneric(Protocol): 203 | """Represents a type that has inherited from `Generic` 204 | 205 | The `__orig_bases__` property can be used to determine the resolved 206 | type variable for a given base class. 207 | """ 208 | 209 | __orig_bases__: tuple[_GenericAlias] 210 | 211 | 212 | class _GenericAlias(Protocol): 213 | __origin__: type[object] 214 | 215 | 216 | class HttpxSendArgs(TypedDict, total=False): 217 | auth: httpx.Auth 218 | -------------------------------------------------------------------------------- /src/arcadepy/_utils/__init__.py: -------------------------------------------------------------------------------- 1 | from ._sync import asyncify as asyncify 2 | from ._proxy import LazyProxy as LazyProxy 3 | from ._utils import ( 4 | flatten as flatten, 5 | is_dict as is_dict, 6 | is_list as is_list, 7 | is_given as is_given, 8 | is_tuple as is_tuple, 9 | json_safe as json_safe, 10 | lru_cache as lru_cache, 11 | is_mapping as is_mapping, 12 | is_tuple_t as is_tuple_t, 13 | parse_date as parse_date, 14 | is_iterable as is_iterable, 15 | is_sequence as is_sequence, 16 | coerce_float as coerce_float, 17 | is_mapping_t as is_mapping_t, 18 | removeprefix as removeprefix, 19 | removesuffix as removesuffix, 20 | extract_files as extract_files, 21 | is_sequence_t as is_sequence_t, 22 | required_args as required_args, 23 | coerce_boolean as coerce_boolean, 24 | coerce_integer as coerce_integer, 25 | file_from_path as file_from_path, 26 | parse_datetime as parse_datetime, 27 | strip_not_given as strip_not_given, 28 | deepcopy_minimal as deepcopy_minimal, 29 | get_async_library as get_async_library, 30 | maybe_coerce_float as maybe_coerce_float, 31 | get_required_header as get_required_header, 32 | maybe_coerce_boolean as maybe_coerce_boolean, 33 | maybe_coerce_integer as maybe_coerce_integer, 34 | ) 35 | from ._typing import ( 36 | is_list_type as is_list_type, 37 | is_union_type as is_union_type, 38 | extract_type_arg as extract_type_arg, 39 | is_iterable_type as is_iterable_type, 40 | is_required_type as is_required_type, 41 | is_annotated_type as is_annotated_type, 42 | is_type_alias_type as is_type_alias_type, 43 | strip_annotated_type as strip_annotated_type, 44 | extract_type_var_from_base as extract_type_var_from_base, 45 | ) 46 | from ._streams import consume_sync_iterator as consume_sync_iterator, consume_async_iterator as consume_async_iterator 47 | from ._transform import ( 48 | PropertyInfo as PropertyInfo, 49 | transform as transform, 50 | async_transform as async_transform, 51 | maybe_transform as maybe_transform, 52 | async_maybe_transform as async_maybe_transform, 53 | ) 54 | from ._reflection import ( 55 | function_has_argument as function_has_argument, 56 | assert_signatures_in_sync as assert_signatures_in_sync, 57 | ) 58 | -------------------------------------------------------------------------------- /src/arcadepy/_utils/_logs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | logger: logging.Logger = logging.getLogger("arcadepy") 5 | httpx_logger: logging.Logger = logging.getLogger("httpx") 6 | 7 | 8 | def _basic_config() -> None: 9 | # e.g. [2023-10-05 14:12:26 - arcadepy._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK" 10 | logging.basicConfig( 11 | format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s", 12 | datefmt="%Y-%m-%d %H:%M:%S", 13 | ) 14 | 15 | 16 | def setup_logging() -> None: 17 | env = os.environ.get("ARCADE_LOG") 18 | if env == "debug": 19 | _basic_config() 20 | logger.setLevel(logging.DEBUG) 21 | httpx_logger.setLevel(logging.DEBUG) 22 | elif env == "info": 23 | _basic_config() 24 | logger.setLevel(logging.INFO) 25 | httpx_logger.setLevel(logging.INFO) 26 | -------------------------------------------------------------------------------- /src/arcadepy/_utils/_proxy.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from typing import Generic, TypeVar, Iterable, cast 5 | from typing_extensions import override 6 | 7 | T = TypeVar("T") 8 | 9 | 10 | class LazyProxy(Generic[T], ABC): 11 | """Implements data methods to pretend that an instance is another instance. 12 | 13 | This includes forwarding attribute access and other methods. 14 | """ 15 | 16 | # Note: we have to special case proxies that themselves return proxies 17 | # to support using a proxy as a catch-all for any random access, e.g. `proxy.foo.bar.baz` 18 | 19 | def __getattr__(self, attr: str) -> object: 20 | proxied = self.__get_proxied__() 21 | if isinstance(proxied, LazyProxy): 22 | return proxied # pyright: ignore 23 | return getattr(proxied, attr) 24 | 25 | @override 26 | def __repr__(self) -> str: 27 | proxied = self.__get_proxied__() 28 | if isinstance(proxied, LazyProxy): 29 | return proxied.__class__.__name__ 30 | return repr(self.__get_proxied__()) 31 | 32 | @override 33 | def __str__(self) -> str: 34 | proxied = self.__get_proxied__() 35 | if isinstance(proxied, LazyProxy): 36 | return proxied.__class__.__name__ 37 | return str(proxied) 38 | 39 | @override 40 | def __dir__(self) -> Iterable[str]: 41 | proxied = self.__get_proxied__() 42 | if isinstance(proxied, LazyProxy): 43 | return [] 44 | return proxied.__dir__() 45 | 46 | @property # type: ignore 47 | @override 48 | def __class__(self) -> type: # pyright: ignore 49 | try: 50 | proxied = self.__get_proxied__() 51 | except Exception: 52 | return type(self) 53 | if issubclass(type(proxied), LazyProxy): 54 | return type(proxied) 55 | return proxied.__class__ 56 | 57 | def __get_proxied__(self) -> T: 58 | return self.__load__() 59 | 60 | def __as_proxied__(self) -> T: 61 | """Helper method that returns the current proxy, typed as the loaded object""" 62 | return cast(T, self) 63 | 64 | @abstractmethod 65 | def __load__(self) -> T: ... 66 | -------------------------------------------------------------------------------- /src/arcadepy/_utils/_reflection.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import inspect 4 | from typing import Any, Callable 5 | 6 | 7 | def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool: 8 | """Returns whether or not the given function has a specific parameter""" 9 | sig = inspect.signature(func) 10 | return arg_name in sig.parameters 11 | 12 | 13 | def assert_signatures_in_sync( 14 | source_func: Callable[..., Any], 15 | check_func: Callable[..., Any], 16 | *, 17 | exclude_params: set[str] = set(), 18 | ) -> None: 19 | """Ensure that the signature of the second function matches the first.""" 20 | 21 | check_sig = inspect.signature(check_func) 22 | source_sig = inspect.signature(source_func) 23 | 24 | errors: list[str] = [] 25 | 26 | for name, source_param in source_sig.parameters.items(): 27 | if name in exclude_params: 28 | continue 29 | 30 | custom_param = check_sig.parameters.get(name) 31 | if not custom_param: 32 | errors.append(f"the `{name}` param is missing") 33 | continue 34 | 35 | if custom_param.annotation != source_param.annotation: 36 | errors.append( 37 | f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(custom_param.annotation)}" 38 | ) 39 | continue 40 | 41 | if errors: 42 | raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors)) 43 | -------------------------------------------------------------------------------- /src/arcadepy/_utils/_resources_proxy.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | from typing_extensions import override 5 | 6 | from ._proxy import LazyProxy 7 | 8 | 9 | class ResourcesProxy(LazyProxy[Any]): 10 | """A proxy for the `arcadepy.resources` module. 11 | 12 | This is used so that we can lazily import `arcadepy.resources` only when 13 | needed *and* so that users can just import `arcadepy` and reference `arcadepy.resources` 14 | """ 15 | 16 | @override 17 | def __load__(self) -> Any: 18 | import importlib 19 | 20 | mod = importlib.import_module("arcadepy.resources") 21 | return mod 22 | 23 | 24 | resources = ResourcesProxy().__as_proxied__() 25 | -------------------------------------------------------------------------------- /src/arcadepy/_utils/_streams.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from typing_extensions import Iterator, AsyncIterator 3 | 4 | 5 | def consume_sync_iterator(iterator: Iterator[Any]) -> None: 6 | for _ in iterator: 7 | ... 8 | 9 | 10 | async def consume_async_iterator(iterator: AsyncIterator[Any]) -> None: 11 | async for _ in iterator: 12 | ... 13 | -------------------------------------------------------------------------------- /src/arcadepy/_utils/_sync.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | import asyncio 5 | import functools 6 | import contextvars 7 | from typing import Any, TypeVar, Callable, Awaitable 8 | from typing_extensions import ParamSpec 9 | 10 | import anyio 11 | import sniffio 12 | import anyio.to_thread 13 | 14 | T_Retval = TypeVar("T_Retval") 15 | T_ParamSpec = ParamSpec("T_ParamSpec") 16 | 17 | 18 | if sys.version_info >= (3, 9): 19 | _asyncio_to_thread = asyncio.to_thread 20 | else: 21 | # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread 22 | # for Python 3.8 support 23 | async def _asyncio_to_thread( 24 | func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs 25 | ) -> Any: 26 | """Asynchronously run function *func* in a separate thread. 27 | 28 | Any *args and **kwargs supplied for this function are directly passed 29 | to *func*. Also, the current :class:`contextvars.Context` is propagated, 30 | allowing context variables from the main thread to be accessed in the 31 | separate thread. 32 | 33 | Returns a coroutine that can be awaited to get the eventual result of *func*. 34 | """ 35 | loop = asyncio.events.get_running_loop() 36 | ctx = contextvars.copy_context() 37 | func_call = functools.partial(ctx.run, func, *args, **kwargs) 38 | return await loop.run_in_executor(None, func_call) 39 | 40 | 41 | async def to_thread( 42 | func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs 43 | ) -> T_Retval: 44 | if sniffio.current_async_library() == "asyncio": 45 | return await _asyncio_to_thread(func, *args, **kwargs) 46 | 47 | return await anyio.to_thread.run_sync( 48 | functools.partial(func, *args, **kwargs), 49 | ) 50 | 51 | 52 | # inspired by `asyncer`, https://github.com/tiangolo/asyncer 53 | def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: 54 | """ 55 | Take a blocking function and create an async one that receives the same 56 | positional and keyword arguments. For python version 3.9 and above, it uses 57 | asyncio.to_thread to run the function in a separate thread. For python version 58 | 3.8, it uses locally defined copy of the asyncio.to_thread function which was 59 | introduced in python 3.9. 60 | 61 | Usage: 62 | 63 | ```python 64 | def blocking_func(arg1, arg2, kwarg1=None): 65 | # blocking code 66 | return result 67 | 68 | 69 | result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1) 70 | ``` 71 | 72 | ## Arguments 73 | 74 | `function`: a blocking regular callable (e.g. a function) 75 | 76 | ## Return 77 | 78 | An async function that takes the same positional and keyword arguments as the 79 | original one, that when called runs the same original function in a thread worker 80 | and returns the result. 81 | """ 82 | 83 | async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: 84 | return await to_thread(function, *args, **kwargs) 85 | 86 | return wrapper 87 | -------------------------------------------------------------------------------- /src/arcadepy/_utils/_typing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | import typing 5 | import typing_extensions 6 | from typing import Any, TypeVar, Iterable, cast 7 | from collections import abc as _c_abc 8 | from typing_extensions import ( 9 | TypeIs, 10 | Required, 11 | Annotated, 12 | get_args, 13 | get_origin, 14 | ) 15 | 16 | from ._utils import lru_cache 17 | from .._types import InheritsGeneric 18 | from .._compat import is_union as _is_union 19 | 20 | 21 | def is_annotated_type(typ: type) -> bool: 22 | return get_origin(typ) == Annotated 23 | 24 | 25 | def is_list_type(typ: type) -> bool: 26 | return (get_origin(typ) or typ) == list 27 | 28 | 29 | def is_iterable_type(typ: type) -> bool: 30 | """If the given type is `typing.Iterable[T]`""" 31 | origin = get_origin(typ) or typ 32 | return origin == Iterable or origin == _c_abc.Iterable 33 | 34 | 35 | def is_union_type(typ: type) -> bool: 36 | return _is_union(get_origin(typ)) 37 | 38 | 39 | def is_required_type(typ: type) -> bool: 40 | return get_origin(typ) == Required 41 | 42 | 43 | def is_typevar(typ: type) -> bool: 44 | # type ignore is required because type checkers 45 | # think this expression will always return False 46 | return type(typ) == TypeVar # type: ignore 47 | 48 | 49 | _TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,) 50 | if sys.version_info >= (3, 12): 51 | _TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType) 52 | 53 | 54 | def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: 55 | """Return whether the provided argument is an instance of `TypeAliasType`. 56 | 57 | ```python 58 | type Int = int 59 | is_type_alias_type(Int) 60 | # > True 61 | Str = TypeAliasType("Str", str) 62 | is_type_alias_type(Str) 63 | # > True 64 | ``` 65 | """ 66 | return isinstance(tp, _TYPE_ALIAS_TYPES) 67 | 68 | 69 | # Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] 70 | @lru_cache(maxsize=8096) 71 | def strip_annotated_type(typ: type) -> type: 72 | if is_required_type(typ) or is_annotated_type(typ): 73 | return strip_annotated_type(cast(type, get_args(typ)[0])) 74 | 75 | return typ 76 | 77 | 78 | def extract_type_arg(typ: type, index: int) -> type: 79 | args = get_args(typ) 80 | try: 81 | return cast(type, args[index]) 82 | except IndexError as err: 83 | raise RuntimeError(f"Expected type {typ} to have a type argument at index {index} but it did not") from err 84 | 85 | 86 | def extract_type_var_from_base( 87 | typ: type, 88 | *, 89 | generic_bases: tuple[type, ...], 90 | index: int, 91 | failure_message: str | None = None, 92 | ) -> type: 93 | """Given a type like `Foo[T]`, returns the generic type variable `T`. 94 | 95 | This also handles the case where a concrete subclass is given, e.g. 96 | ```py 97 | class MyResponse(Foo[bytes]): 98 | ... 99 | 100 | extract_type_var(MyResponse, bases=(Foo,), index=0) -> bytes 101 | ``` 102 | 103 | And where a generic subclass is given: 104 | ```py 105 | _T = TypeVar('_T') 106 | class MyResponse(Foo[_T]): 107 | ... 108 | 109 | extract_type_var(MyResponse[bytes], bases=(Foo,), index=0) -> bytes 110 | ``` 111 | """ 112 | cls = cast(object, get_origin(typ) or typ) 113 | if cls in generic_bases: # pyright: ignore[reportUnnecessaryContains] 114 | # we're given the class directly 115 | return extract_type_arg(typ, index) 116 | 117 | # if a subclass is given 118 | # --- 119 | # this is needed as __orig_bases__ is not present in the typeshed stubs 120 | # because it is intended to be for internal use only, however there does 121 | # not seem to be a way to resolve generic TypeVars for inherited subclasses 122 | # without using it. 123 | if isinstance(cls, InheritsGeneric): 124 | target_base_class: Any | None = None 125 | for base in cls.__orig_bases__: 126 | if base.__origin__ in generic_bases: 127 | target_base_class = base 128 | break 129 | 130 | if target_base_class is None: 131 | raise RuntimeError( 132 | "Could not find the generic base class;\n" 133 | "This should never happen;\n" 134 | f"Does {cls} inherit from one of {generic_bases} ?" 135 | ) 136 | 137 | extracted = extract_type_arg(target_base_class, index) 138 | if is_typevar(extracted): 139 | # If the extracted type argument is itself a type variable 140 | # then that means the subclass itself is generic, so we have 141 | # to resolve the type argument from the class itself, not 142 | # the base class. 143 | # 144 | # Note: if there is more than 1 type argument, the subclass could 145 | # change the ordering of the type arguments, this is not currently 146 | # supported. 147 | return extract_type_arg(typ, index) 148 | 149 | return extracted 150 | 151 | raise RuntimeError(failure_message or f"Could not resolve inner type variable at index {index} for {typ}") 152 | -------------------------------------------------------------------------------- /src/arcadepy/_version.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | __title__ = "arcadepy" 4 | __version__ = "1.5.0" # x-release-please-version 5 | -------------------------------------------------------------------------------- /src/arcadepy/lib/.keep: -------------------------------------------------------------------------------- 1 | File generated from our OpenAPI spec by Stainless. 2 | 3 | This directory can be used to store custom files to expand the SDK. 4 | It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. -------------------------------------------------------------------------------- /src/arcadepy/pagination.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List, Generic, TypeVar, Optional 4 | from typing_extensions import override 5 | 6 | from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage 7 | 8 | __all__ = ["SyncOffsetPage", "AsyncOffsetPage"] 9 | 10 | _T = TypeVar("_T") 11 | 12 | 13 | class SyncOffsetPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): 14 | items: List[_T] 15 | total_count: Optional[int] = None 16 | offset: Optional[int] = None 17 | 18 | @override 19 | def _get_page_items(self) -> List[_T]: 20 | items = self.items 21 | if not items: 22 | return [] 23 | return items 24 | 25 | @override 26 | def next_page_info(self) -> Optional[PageInfo]: 27 | offset = self.offset 28 | if offset is None: 29 | return None # type: ignore[unreachable] 30 | 31 | length = len(self._get_page_items()) 32 | current_count = offset + length 33 | 34 | total_count = self.total_count 35 | if total_count is None: 36 | return None 37 | 38 | if current_count < total_count: 39 | return PageInfo(params={"offset": current_count}) 40 | 41 | return None 42 | 43 | 44 | class AsyncOffsetPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): 45 | items: List[_T] 46 | total_count: Optional[int] = None 47 | offset: Optional[int] = None 48 | 49 | @override 50 | def _get_page_items(self) -> List[_T]: 51 | items = self.items 52 | if not items: 53 | return [] 54 | return items 55 | 56 | @override 57 | def next_page_info(self) -> Optional[PageInfo]: 58 | offset = self.offset 59 | if offset is None: 60 | return None # type: ignore[unreachable] 61 | 62 | length = len(self._get_page_items()) 63 | current_count = offset + length 64 | 65 | total_count = self.total_count 66 | if total_count is None: 67 | return None 68 | 69 | if current_count < total_count: 70 | return PageInfo(params={"offset": current_count}) 71 | 72 | return None 73 | -------------------------------------------------------------------------------- /src/arcadepy/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArcadeAI/arcade-py/0918ad881d2d1bed3c4152e9e9f78a6edd21253e/src/arcadepy/py.typed -------------------------------------------------------------------------------- /src/arcadepy/resources/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from .auth import ( 4 | AuthResource, 5 | AsyncAuthResource, 6 | AuthResourceWithRawResponse, 7 | AsyncAuthResourceWithRawResponse, 8 | AuthResourceWithStreamingResponse, 9 | AsyncAuthResourceWithStreamingResponse, 10 | ) 11 | from .chat import ( 12 | ChatResource, 13 | AsyncChatResource, 14 | ChatResourceWithRawResponse, 15 | AsyncChatResourceWithRawResponse, 16 | ChatResourceWithStreamingResponse, 17 | AsyncChatResourceWithStreamingResponse, 18 | ) 19 | from .tools import ( 20 | ToolsResource, 21 | AsyncToolsResource, 22 | ToolsResourceWithRawResponse, 23 | AsyncToolsResourceWithRawResponse, 24 | ToolsResourceWithStreamingResponse, 25 | AsyncToolsResourceWithStreamingResponse, 26 | ) 27 | from .health import ( 28 | HealthResource, 29 | AsyncHealthResource, 30 | HealthResourceWithRawResponse, 31 | AsyncHealthResourceWithRawResponse, 32 | HealthResourceWithStreamingResponse, 33 | AsyncHealthResourceWithStreamingResponse, 34 | ) 35 | from .workers import ( 36 | WorkersResource, 37 | AsyncWorkersResource, 38 | WorkersResourceWithRawResponse, 39 | AsyncWorkersResourceWithRawResponse, 40 | WorkersResourceWithStreamingResponse, 41 | AsyncWorkersResourceWithStreamingResponse, 42 | ) 43 | 44 | __all__ = [ 45 | "AuthResource", 46 | "AsyncAuthResource", 47 | "AuthResourceWithRawResponse", 48 | "AsyncAuthResourceWithRawResponse", 49 | "AuthResourceWithStreamingResponse", 50 | "AsyncAuthResourceWithStreamingResponse", 51 | "HealthResource", 52 | "AsyncHealthResource", 53 | "HealthResourceWithRawResponse", 54 | "AsyncHealthResourceWithRawResponse", 55 | "HealthResourceWithStreamingResponse", 56 | "AsyncHealthResourceWithStreamingResponse", 57 | "ChatResource", 58 | "AsyncChatResource", 59 | "ChatResourceWithRawResponse", 60 | "AsyncChatResourceWithRawResponse", 61 | "ChatResourceWithStreamingResponse", 62 | "AsyncChatResourceWithStreamingResponse", 63 | "ToolsResource", 64 | "AsyncToolsResource", 65 | "ToolsResourceWithRawResponse", 66 | "AsyncToolsResourceWithRawResponse", 67 | "ToolsResourceWithStreamingResponse", 68 | "AsyncToolsResourceWithStreamingResponse", 69 | "WorkersResource", 70 | "AsyncWorkersResource", 71 | "WorkersResourceWithRawResponse", 72 | "AsyncWorkersResourceWithRawResponse", 73 | "WorkersResourceWithStreamingResponse", 74 | "AsyncWorkersResourceWithStreamingResponse", 75 | ] 76 | -------------------------------------------------------------------------------- /src/arcadepy/resources/chat/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from .chat import ( 4 | ChatResource, 5 | AsyncChatResource, 6 | ChatResourceWithRawResponse, 7 | AsyncChatResourceWithRawResponse, 8 | ChatResourceWithStreamingResponse, 9 | AsyncChatResourceWithStreamingResponse, 10 | ) 11 | from .completions import ( 12 | CompletionsResource, 13 | AsyncCompletionsResource, 14 | CompletionsResourceWithRawResponse, 15 | AsyncCompletionsResourceWithRawResponse, 16 | CompletionsResourceWithStreamingResponse, 17 | AsyncCompletionsResourceWithStreamingResponse, 18 | ) 19 | 20 | __all__ = [ 21 | "CompletionsResource", 22 | "AsyncCompletionsResource", 23 | "CompletionsResourceWithRawResponse", 24 | "AsyncCompletionsResourceWithRawResponse", 25 | "CompletionsResourceWithStreamingResponse", 26 | "AsyncCompletionsResourceWithStreamingResponse", 27 | "ChatResource", 28 | "AsyncChatResource", 29 | "ChatResourceWithRawResponse", 30 | "AsyncChatResourceWithRawResponse", 31 | "ChatResourceWithStreamingResponse", 32 | "AsyncChatResourceWithStreamingResponse", 33 | ] 34 | -------------------------------------------------------------------------------- /src/arcadepy/resources/chat/chat.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from ..._compat import cached_property 6 | from ..._resource import SyncAPIResource, AsyncAPIResource 7 | from .completions import ( 8 | CompletionsResource, 9 | AsyncCompletionsResource, 10 | CompletionsResourceWithRawResponse, 11 | AsyncCompletionsResourceWithRawResponse, 12 | CompletionsResourceWithStreamingResponse, 13 | AsyncCompletionsResourceWithStreamingResponse, 14 | ) 15 | 16 | __all__ = ["ChatResource", "AsyncChatResource"] 17 | 18 | 19 | class ChatResource(SyncAPIResource): 20 | @cached_property 21 | def completions(self) -> CompletionsResource: 22 | return CompletionsResource(self._client) 23 | 24 | @cached_property 25 | def with_raw_response(self) -> ChatResourceWithRawResponse: 26 | """ 27 | This property can be used as a prefix for any HTTP method call to return 28 | the raw response object instead of the parsed content. 29 | 30 | For more information, see https://www.github.com/ArcadeAI/arcade-py#accessing-raw-response-data-eg-headers 31 | """ 32 | return ChatResourceWithRawResponse(self) 33 | 34 | @cached_property 35 | def with_streaming_response(self) -> ChatResourceWithStreamingResponse: 36 | """ 37 | An alternative to `.with_raw_response` that doesn't eagerly read the response body. 38 | 39 | For more information, see https://www.github.com/ArcadeAI/arcade-py#with_streaming_response 40 | """ 41 | return ChatResourceWithStreamingResponse(self) 42 | 43 | 44 | class AsyncChatResource(AsyncAPIResource): 45 | @cached_property 46 | def completions(self) -> AsyncCompletionsResource: 47 | return AsyncCompletionsResource(self._client) 48 | 49 | @cached_property 50 | def with_raw_response(self) -> AsyncChatResourceWithRawResponse: 51 | """ 52 | This property can be used as a prefix for any HTTP method call to return 53 | the raw response object instead of the parsed content. 54 | 55 | For more information, see https://www.github.com/ArcadeAI/arcade-py#accessing-raw-response-data-eg-headers 56 | """ 57 | return AsyncChatResourceWithRawResponse(self) 58 | 59 | @cached_property 60 | def with_streaming_response(self) -> AsyncChatResourceWithStreamingResponse: 61 | """ 62 | An alternative to `.with_raw_response` that doesn't eagerly read the response body. 63 | 64 | For more information, see https://www.github.com/ArcadeAI/arcade-py#with_streaming_response 65 | """ 66 | return AsyncChatResourceWithStreamingResponse(self) 67 | 68 | 69 | class ChatResourceWithRawResponse: 70 | def __init__(self, chat: ChatResource) -> None: 71 | self._chat = chat 72 | 73 | @cached_property 74 | def completions(self) -> CompletionsResourceWithRawResponse: 75 | return CompletionsResourceWithRawResponse(self._chat.completions) 76 | 77 | 78 | class AsyncChatResourceWithRawResponse: 79 | def __init__(self, chat: AsyncChatResource) -> None: 80 | self._chat = chat 81 | 82 | @cached_property 83 | def completions(self) -> AsyncCompletionsResourceWithRawResponse: 84 | return AsyncCompletionsResourceWithRawResponse(self._chat.completions) 85 | 86 | 87 | class ChatResourceWithStreamingResponse: 88 | def __init__(self, chat: ChatResource) -> None: 89 | self._chat = chat 90 | 91 | @cached_property 92 | def completions(self) -> CompletionsResourceWithStreamingResponse: 93 | return CompletionsResourceWithStreamingResponse(self._chat.completions) 94 | 95 | 96 | class AsyncChatResourceWithStreamingResponse: 97 | def __init__(self, chat: AsyncChatResource) -> None: 98 | self._chat = chat 99 | 100 | @cached_property 101 | def completions(self) -> AsyncCompletionsResourceWithStreamingResponse: 102 | return AsyncCompletionsResourceWithStreamingResponse(self._chat.completions) 103 | -------------------------------------------------------------------------------- /src/arcadepy/resources/health.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | import httpx 6 | 7 | from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven 8 | from .._compat import cached_property 9 | from .._resource import SyncAPIResource, AsyncAPIResource 10 | from .._response import ( 11 | to_raw_response_wrapper, 12 | to_streamed_response_wrapper, 13 | async_to_raw_response_wrapper, 14 | async_to_streamed_response_wrapper, 15 | ) 16 | from .._base_client import make_request_options 17 | from ..types.health_schema import HealthSchema 18 | 19 | __all__ = ["HealthResource", "AsyncHealthResource"] 20 | 21 | 22 | class HealthResource(SyncAPIResource): 23 | @cached_property 24 | def with_raw_response(self) -> HealthResourceWithRawResponse: 25 | """ 26 | This property can be used as a prefix for any HTTP method call to return 27 | the raw response object instead of the parsed content. 28 | 29 | For more information, see https://www.github.com/ArcadeAI/arcade-py#accessing-raw-response-data-eg-headers 30 | """ 31 | return HealthResourceWithRawResponse(self) 32 | 33 | @cached_property 34 | def with_streaming_response(self) -> HealthResourceWithStreamingResponse: 35 | """ 36 | An alternative to `.with_raw_response` that doesn't eagerly read the response body. 37 | 38 | For more information, see https://www.github.com/ArcadeAI/arcade-py#with_streaming_response 39 | """ 40 | return HealthResourceWithStreamingResponse(self) 41 | 42 | def check( 43 | self, 44 | *, 45 | # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. 46 | # The extra values given here take precedence over values defined on the client or passed to this method. 47 | extra_headers: Headers | None = None, 48 | extra_query: Query | None = None, 49 | extra_body: Body | None = None, 50 | timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, 51 | ) -> HealthSchema: 52 | """Check if Arcade Engine is healthy""" 53 | return self._get( 54 | "/v1/health", 55 | options=make_request_options( 56 | extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout 57 | ), 58 | cast_to=HealthSchema, 59 | ) 60 | 61 | 62 | class AsyncHealthResource(AsyncAPIResource): 63 | @cached_property 64 | def with_raw_response(self) -> AsyncHealthResourceWithRawResponse: 65 | """ 66 | This property can be used as a prefix for any HTTP method call to return 67 | the raw response object instead of the parsed content. 68 | 69 | For more information, see https://www.github.com/ArcadeAI/arcade-py#accessing-raw-response-data-eg-headers 70 | """ 71 | return AsyncHealthResourceWithRawResponse(self) 72 | 73 | @cached_property 74 | def with_streaming_response(self) -> AsyncHealthResourceWithStreamingResponse: 75 | """ 76 | An alternative to `.with_raw_response` that doesn't eagerly read the response body. 77 | 78 | For more information, see https://www.github.com/ArcadeAI/arcade-py#with_streaming_response 79 | """ 80 | return AsyncHealthResourceWithStreamingResponse(self) 81 | 82 | async def check( 83 | self, 84 | *, 85 | # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. 86 | # The extra values given here take precedence over values defined on the client or passed to this method. 87 | extra_headers: Headers | None = None, 88 | extra_query: Query | None = None, 89 | extra_body: Body | None = None, 90 | timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, 91 | ) -> HealthSchema: 92 | """Check if Arcade Engine is healthy""" 93 | return await self._get( 94 | "/v1/health", 95 | options=make_request_options( 96 | extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout 97 | ), 98 | cast_to=HealthSchema, 99 | ) 100 | 101 | 102 | class HealthResourceWithRawResponse: 103 | def __init__(self, health: HealthResource) -> None: 104 | self._health = health 105 | 106 | self.check = to_raw_response_wrapper( 107 | health.check, 108 | ) 109 | 110 | 111 | class AsyncHealthResourceWithRawResponse: 112 | def __init__(self, health: AsyncHealthResource) -> None: 113 | self._health = health 114 | 115 | self.check = async_to_raw_response_wrapper( 116 | health.check, 117 | ) 118 | 119 | 120 | class HealthResourceWithStreamingResponse: 121 | def __init__(self, health: HealthResource) -> None: 122 | self._health = health 123 | 124 | self.check = to_streamed_response_wrapper( 125 | health.check, 126 | ) 127 | 128 | 129 | class AsyncHealthResourceWithStreamingResponse: 130 | def __init__(self, health: AsyncHealthResource) -> None: 131 | self._health = health 132 | 133 | self.check = async_to_streamed_response_wrapper( 134 | health.check, 135 | ) 136 | -------------------------------------------------------------------------------- /src/arcadepy/resources/tools/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from .tools import ( 4 | ToolsResource, 5 | AsyncToolsResource, 6 | ToolsResourceWithRawResponse, 7 | AsyncToolsResourceWithRawResponse, 8 | ToolsResourceWithStreamingResponse, 9 | AsyncToolsResourceWithStreamingResponse, 10 | ) 11 | from .formatted import ( 12 | FormattedResource, 13 | AsyncFormattedResource, 14 | FormattedResourceWithRawResponse, 15 | AsyncFormattedResourceWithRawResponse, 16 | FormattedResourceWithStreamingResponse, 17 | AsyncFormattedResourceWithStreamingResponse, 18 | ) 19 | from .scheduled import ( 20 | ScheduledResource, 21 | AsyncScheduledResource, 22 | ScheduledResourceWithRawResponse, 23 | AsyncScheduledResourceWithRawResponse, 24 | ScheduledResourceWithStreamingResponse, 25 | AsyncScheduledResourceWithStreamingResponse, 26 | ) 27 | 28 | __all__ = [ 29 | "ScheduledResource", 30 | "AsyncScheduledResource", 31 | "ScheduledResourceWithRawResponse", 32 | "AsyncScheduledResourceWithRawResponse", 33 | "ScheduledResourceWithStreamingResponse", 34 | "AsyncScheduledResourceWithStreamingResponse", 35 | "FormattedResource", 36 | "AsyncFormattedResource", 37 | "FormattedResourceWithRawResponse", 38 | "AsyncFormattedResourceWithRawResponse", 39 | "FormattedResourceWithStreamingResponse", 40 | "AsyncFormattedResourceWithStreamingResponse", 41 | "ToolsResource", 42 | "AsyncToolsResource", 43 | "ToolsResourceWithRawResponse", 44 | "AsyncToolsResourceWithRawResponse", 45 | "ToolsResourceWithStreamingResponse", 46 | "AsyncToolsResourceWithStreamingResponse", 47 | ] 48 | -------------------------------------------------------------------------------- /src/arcadepy/types/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from .usage import Usage as Usage 6 | from .choice import Choice as Choice 7 | from .shared import ( 8 | Error as Error, 9 | AuthorizationContext as AuthorizationContext, 10 | AuthorizationResponse as AuthorizationResponse, 11 | ) 12 | from .chat_message import ChatMessage as ChatMessage 13 | from .value_schema import ValueSchema as ValueSchema 14 | from .chat_response import ChatResponse as ChatResponse 15 | from .health_schema import HealthSchema as HealthSchema 16 | from .tool_execution import ToolExecution as ToolExecution 17 | from .tool_definition import ToolDefinition as ToolDefinition 18 | from .tool_get_params import ToolGetParams as ToolGetParams 19 | from .worker_response import WorkerResponse as WorkerResponse 20 | from .tool_list_params import ToolListParams as ToolListParams 21 | from .tool_get_response import ToolGetResponse as ToolGetResponse 22 | from .auth_status_params import AuthStatusParams as AuthStatusParams 23 | from .chat_message_param import ChatMessageParam as ChatMessageParam 24 | from .worker_list_params import WorkerListParams as WorkerListParams 25 | from .tool_execute_params import ToolExecuteParams as ToolExecuteParams 26 | from .worker_tools_params import WorkerToolsParams as WorkerToolsParams 27 | from .worker_create_params import WorkerCreateParams as WorkerCreateParams 28 | from .worker_update_params import WorkerUpdateParams as WorkerUpdateParams 29 | from .auth_authorize_params import AuthAuthorizeParams as AuthAuthorizeParams 30 | from .execute_tool_response import ExecuteToolResponse as ExecuteToolResponse 31 | from .tool_authorize_params import ToolAuthorizeParams as ToolAuthorizeParams 32 | from .tool_execution_attempt import ToolExecutionAttempt as ToolExecutionAttempt 33 | from .worker_health_response import WorkerHealthResponse as WorkerHealthResponse 34 | -------------------------------------------------------------------------------- /src/arcadepy/types/auth_authorize_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import List 6 | from typing_extensions import Required, TypedDict 7 | 8 | __all__ = ["AuthAuthorizeParams", "AuthRequirement", "AuthRequirementOauth2"] 9 | 10 | 11 | class AuthAuthorizeParams(TypedDict, total=False): 12 | auth_requirement: Required[AuthRequirement] 13 | 14 | user_id: Required[str] 15 | 16 | next_uri: str 17 | """ 18 | Optional: if provided, the user will be redirected to this URI after 19 | authorization 20 | """ 21 | 22 | 23 | class AuthRequirementOauth2(TypedDict, total=False): 24 | scopes: List[str] 25 | 26 | 27 | class AuthRequirement(TypedDict, total=False): 28 | id: str 29 | """one of ID or ProviderID must be set""" 30 | 31 | oauth2: AuthRequirementOauth2 32 | 33 | provider_id: str 34 | """one of ID or ProviderID must be set""" 35 | 36 | provider_type: str 37 | -------------------------------------------------------------------------------- /src/arcadepy/types/auth_status_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import Required, TypedDict 6 | 7 | __all__ = ["AuthStatusParams"] 8 | 9 | 10 | class AuthStatusParams(TypedDict, total=False): 11 | id: Required[str] 12 | """Authorization ID""" 13 | 14 | wait: int 15 | """Timeout in seconds (max 59)""" 16 | -------------------------------------------------------------------------------- /src/arcadepy/types/chat/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from .completion_create_params import CompletionCreateParams as CompletionCreateParams 6 | -------------------------------------------------------------------------------- /src/arcadepy/types/chat/completion_create_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Dict, List, Iterable 6 | from typing_extensions import Literal, TypedDict 7 | 8 | from ..chat_message_param import ChatMessageParam 9 | 10 | __all__ = ["CompletionCreateParams", "ResponseFormat", "StreamOptions"] 11 | 12 | 13 | class CompletionCreateParams(TypedDict, total=False): 14 | frequency_penalty: float 15 | 16 | logit_bias: Dict[str, int] 17 | """ 18 | LogitBias is must be a token id string (specified by their token ID in the 19 | tokenizer), not a word string. incorrect: `"logit_bias":{"You": 6}`, correct: 20 | `"logit_bias":{"1639": 6}` refs: 21 | https://platform.openai.com/docs/api-reference/chat/create#chat/create-logit_bias 22 | """ 23 | 24 | logprobs: bool 25 | """ 26 | LogProbs indicates whether to return log probabilities of the output tokens or 27 | not. If true, returns the log probabilities of each output token returned in the 28 | content of message. This option is currently not available on the 29 | gpt-4-vision-preview model. 30 | """ 31 | 32 | max_tokens: int 33 | 34 | messages: Iterable[ChatMessageParam] 35 | 36 | model: str 37 | 38 | n: int 39 | 40 | parallel_tool_calls: bool 41 | """Disable the default behavior of parallel tool calls by setting it: false.""" 42 | 43 | presence_penalty: float 44 | 45 | response_format: ResponseFormat 46 | 47 | seed: int 48 | 49 | stop: List[str] 50 | 51 | stream: bool 52 | 53 | stream_options: StreamOptions 54 | """Options for streaming response. Only set this when you set stream: true.""" 55 | 56 | temperature: float 57 | 58 | tool_choice: object 59 | """This can be either a string or an ToolChoice object.""" 60 | 61 | tools: object 62 | 63 | top_logprobs: int 64 | """ 65 | TopLogProbs is an integer between 0 and 5 specifying the number of most likely 66 | tokens to return at each token position, each with an associated log 67 | probability. logprobs must be set to true if this parameter is used. 68 | """ 69 | 70 | top_p: float 71 | 72 | user: str 73 | 74 | 75 | class ResponseFormat(TypedDict, total=False): 76 | type: Literal["json_object", "text"] 77 | 78 | 79 | class StreamOptions(TypedDict, total=False): 80 | include_usage: bool 81 | """ 82 | If set, an additional chunk will be streamed before the data: [DONE] message. 83 | The usage field on this chunk shows the token usage statistics for the entire 84 | request, and the choices field will always be an empty array. All other chunks 85 | will also include a usage field, but with a null value. 86 | """ 87 | -------------------------------------------------------------------------------- /src/arcadepy/types/chat_message.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List, Optional 4 | from typing_extensions import Literal 5 | 6 | from .._models import BaseModel 7 | 8 | __all__ = ["ChatMessage", "ToolCall", "ToolCallFunction"] 9 | 10 | 11 | class ToolCallFunction(BaseModel): 12 | arguments: Optional[str] = None 13 | 14 | name: Optional[str] = None 15 | 16 | 17 | class ToolCall(BaseModel): 18 | id: Optional[str] = None 19 | 20 | function: Optional[ToolCallFunction] = None 21 | 22 | type: Optional[Literal["function"]] = None 23 | 24 | 25 | class ChatMessage(BaseModel): 26 | content: str 27 | """The content of the message.""" 28 | 29 | role: str 30 | """The role of the author of this message. 31 | 32 | One of system, user, tool, or assistant. 33 | """ 34 | 35 | name: Optional[str] = None 36 | """tool Name""" 37 | 38 | tool_call_id: Optional[str] = None 39 | """tool_call_id""" 40 | 41 | tool_calls: Optional[List[ToolCall]] = None 42 | """tool calls if any""" 43 | -------------------------------------------------------------------------------- /src/arcadepy/types/chat_message_param.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Iterable 6 | from typing_extensions import Literal, Required, TypedDict 7 | 8 | __all__ = ["ChatMessageParam", "ToolCall", "ToolCallFunction"] 9 | 10 | 11 | class ToolCallFunction(TypedDict, total=False): 12 | arguments: str 13 | 14 | name: str 15 | 16 | 17 | class ToolCall(TypedDict, total=False): 18 | id: str 19 | 20 | function: ToolCallFunction 21 | 22 | type: Literal["function"] 23 | 24 | 25 | class ChatMessageParam(TypedDict, total=False): 26 | content: Required[str] 27 | """The content of the message.""" 28 | 29 | role: Required[str] 30 | """The role of the author of this message. 31 | 32 | One of system, user, tool, or assistant. 33 | """ 34 | 35 | name: str 36 | """tool Name""" 37 | 38 | tool_call_id: str 39 | """tool_call_id""" 40 | 41 | tool_calls: Iterable[ToolCall] 42 | """tool calls if any""" 43 | -------------------------------------------------------------------------------- /src/arcadepy/types/chat_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List, Optional 4 | 5 | from .usage import Usage 6 | from .choice import Choice 7 | from .._models import BaseModel 8 | 9 | __all__ = ["ChatResponse"] 10 | 11 | 12 | class ChatResponse(BaseModel): 13 | id: Optional[str] = None 14 | 15 | choices: Optional[List[Choice]] = None 16 | 17 | created: Optional[int] = None 18 | 19 | model: Optional[str] = None 20 | 21 | object: Optional[str] = None 22 | 23 | system_fingerprint: Optional[str] = None 24 | 25 | usage: Optional[Usage] = None 26 | -------------------------------------------------------------------------------- /src/arcadepy/types/choice.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List, Optional 4 | 5 | from .._models import BaseModel 6 | from .chat_message import ChatMessage 7 | from .shared.authorization_response import AuthorizationResponse 8 | 9 | __all__ = ["Choice"] 10 | 11 | 12 | class Choice(BaseModel): 13 | finish_reason: Optional[str] = None 14 | 15 | index: Optional[int] = None 16 | 17 | logprobs: Optional[object] = None 18 | 19 | message: Optional[ChatMessage] = None 20 | 21 | tool_authorizations: Optional[List[AuthorizationResponse]] = None 22 | 23 | tool_messages: Optional[List[ChatMessage]] = None 24 | -------------------------------------------------------------------------------- /src/arcadepy/types/execute_tool_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List, Optional 4 | 5 | from .._models import BaseModel 6 | from .shared.authorization_response import AuthorizationResponse 7 | 8 | __all__ = ["ExecuteToolResponse", "Output", "OutputError", "OutputLog"] 9 | 10 | 11 | class OutputError(BaseModel): 12 | message: str 13 | 14 | additional_prompt_content: Optional[str] = None 15 | 16 | can_retry: Optional[bool] = None 17 | 18 | developer_message: Optional[str] = None 19 | 20 | retry_after_ms: Optional[int] = None 21 | 22 | 23 | class OutputLog(BaseModel): 24 | level: str 25 | 26 | message: str 27 | 28 | subtype: Optional[str] = None 29 | 30 | 31 | class Output(BaseModel): 32 | authorization: Optional[AuthorizationResponse] = None 33 | 34 | error: Optional[OutputError] = None 35 | 36 | logs: Optional[List[OutputLog]] = None 37 | 38 | value: Optional[object] = None 39 | 40 | 41 | class ExecuteToolResponse(BaseModel): 42 | id: Optional[str] = None 43 | 44 | duration: Optional[float] = None 45 | 46 | execution_id: Optional[str] = None 47 | 48 | execution_type: Optional[str] = None 49 | 50 | finished_at: Optional[str] = None 51 | 52 | output: Optional[Output] = None 53 | 54 | run_at: Optional[str] = None 55 | 56 | status: Optional[str] = None 57 | 58 | success: Optional[bool] = None 59 | """ 60 | Whether the request was successful. For immediately-executed requests, this will 61 | be true if the tool call succeeded. For scheduled requests, this will be true if 62 | the request was scheduled successfully. 63 | """ 64 | -------------------------------------------------------------------------------- /src/arcadepy/types/health_schema.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import Optional 4 | 5 | from .._models import BaseModel 6 | 7 | __all__ = ["HealthSchema"] 8 | 9 | 10 | class HealthSchema(BaseModel): 11 | healthy: Optional[bool] = None 12 | -------------------------------------------------------------------------------- /src/arcadepy/types/shared/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from .error import Error as Error 4 | from .authorization_context import AuthorizationContext as AuthorizationContext 5 | from .authorization_response import AuthorizationResponse as AuthorizationResponse 6 | -------------------------------------------------------------------------------- /src/arcadepy/types/shared/authorization_context.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import Dict, Optional 4 | 5 | from ..._models import BaseModel 6 | 7 | __all__ = ["AuthorizationContext"] 8 | 9 | 10 | class AuthorizationContext(BaseModel): 11 | token: Optional[str] = None 12 | 13 | user_info: Optional[Dict[str, object]] = None 14 | -------------------------------------------------------------------------------- /src/arcadepy/types/shared/authorization_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List, Optional 4 | from typing_extensions import Literal 5 | 6 | from ..._models import BaseModel 7 | from .authorization_context import AuthorizationContext 8 | 9 | __all__ = ["AuthorizationResponse"] 10 | 11 | 12 | class AuthorizationResponse(BaseModel): 13 | id: Optional[str] = None 14 | 15 | context: Optional[AuthorizationContext] = None 16 | 17 | provider_id: Optional[str] = None 18 | 19 | scopes: Optional[List[str]] = None 20 | 21 | status: Optional[Literal["not_started", "pending", "completed", "failed"]] = None 22 | 23 | url: Optional[str] = None 24 | 25 | user_id: Optional[str] = None 26 | -------------------------------------------------------------------------------- /src/arcadepy/types/shared/error.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import Optional 4 | 5 | from ..._models import BaseModel 6 | 7 | __all__ = ["Error"] 8 | 9 | 10 | class Error(BaseModel): 11 | message: Optional[str] = None 12 | 13 | name: Optional[str] = None 14 | -------------------------------------------------------------------------------- /src/arcadepy/types/tool_authorize_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import Required, TypedDict 6 | 7 | __all__ = ["ToolAuthorizeParams"] 8 | 9 | 10 | class ToolAuthorizeParams(TypedDict, total=False): 11 | tool_name: Required[str] 12 | 13 | next_uri: str 14 | """ 15 | Optional: if provided, the user will be redirected to this URI after 16 | authorization 17 | """ 18 | 19 | tool_version: str 20 | """Optional: if not provided, any version is used""" 21 | 22 | user_id: str 23 | """Required only when calling with an API key""" 24 | -------------------------------------------------------------------------------- /src/arcadepy/types/tool_definition.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import Dict, List, Optional 4 | from typing_extensions import Literal 5 | 6 | from .._models import BaseModel 7 | from .value_schema import ValueSchema 8 | 9 | __all__ = [ 10 | "ToolDefinition", 11 | "Input", 12 | "InputParameter", 13 | "Toolkit", 14 | "Output", 15 | "Requirements", 16 | "RequirementsAuthorization", 17 | "RequirementsAuthorizationOauth2", 18 | "RequirementsSecret", 19 | ] 20 | 21 | 22 | class InputParameter(BaseModel): 23 | name: str 24 | 25 | value_schema: ValueSchema 26 | 27 | description: Optional[str] = None 28 | 29 | inferrable: Optional[bool] = None 30 | 31 | required: Optional[bool] = None 32 | 33 | 34 | class Input(BaseModel): 35 | parameters: Optional[List[InputParameter]] = None 36 | 37 | 38 | class Toolkit(BaseModel): 39 | name: str 40 | 41 | description: Optional[str] = None 42 | 43 | version: Optional[str] = None 44 | 45 | 46 | class Output(BaseModel): 47 | available_modes: Optional[List[str]] = None 48 | 49 | description: Optional[str] = None 50 | 51 | value_schema: Optional[ValueSchema] = None 52 | 53 | 54 | class RequirementsAuthorizationOauth2(BaseModel): 55 | scopes: Optional[List[str]] = None 56 | 57 | 58 | class RequirementsAuthorization(BaseModel): 59 | id: Optional[str] = None 60 | 61 | oauth2: Optional[RequirementsAuthorizationOauth2] = None 62 | 63 | provider_id: Optional[str] = None 64 | 65 | provider_type: Optional[str] = None 66 | 67 | status: Optional[Literal["active", "inactive"]] = None 68 | 69 | status_reason: Optional[str] = None 70 | 71 | token_status: Optional[Literal["not_started", "pending", "completed", "failed"]] = None 72 | 73 | 74 | class RequirementsSecret(BaseModel): 75 | key: str 76 | 77 | met: Optional[bool] = None 78 | 79 | status_reason: Optional[str] = None 80 | 81 | 82 | class Requirements(BaseModel): 83 | authorization: Optional[RequirementsAuthorization] = None 84 | 85 | met: Optional[bool] = None 86 | 87 | secrets: Optional[List[RequirementsSecret]] = None 88 | 89 | 90 | class ToolDefinition(BaseModel): 91 | fully_qualified_name: str 92 | 93 | input: Input 94 | 95 | name: str 96 | 97 | qualified_name: str 98 | 99 | toolkit: Toolkit 100 | 101 | description: Optional[str] = None 102 | 103 | formatted_schema: Optional[Dict[str, object]] = None 104 | 105 | output: Optional[Output] = None 106 | 107 | requirements: Optional[Requirements] = None 108 | -------------------------------------------------------------------------------- /src/arcadepy/types/tool_execute_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Dict 6 | from typing_extensions import Required, TypedDict 7 | 8 | __all__ = ["ToolExecuteParams"] 9 | 10 | 11 | class ToolExecuteParams(TypedDict, total=False): 12 | tool_name: Required[str] 13 | 14 | input: Dict[str, object] 15 | """JSON input to the tool, if any""" 16 | 17 | run_at: str 18 | """The time at which the tool should be run (optional). 19 | 20 | If not provided, the tool is run immediately. Format ISO 8601: 21 | YYYY-MM-DDTHH:MM:SS 22 | """ 23 | 24 | tool_version: str 25 | """The tool version to use (optional). If not provided, any version is used""" 26 | 27 | user_id: str 28 | -------------------------------------------------------------------------------- /src/arcadepy/types/tool_execution.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import Optional 4 | 5 | from .._models import BaseModel 6 | 7 | __all__ = ["ToolExecution"] 8 | 9 | 10 | class ToolExecution(BaseModel): 11 | id: Optional[str] = None 12 | 13 | created_at: Optional[str] = None 14 | 15 | execution_status: Optional[str] = None 16 | 17 | execution_type: Optional[str] = None 18 | 19 | finished_at: Optional[str] = None 20 | 21 | run_at: Optional[str] = None 22 | 23 | started_at: Optional[str] = None 24 | 25 | tool_name: Optional[str] = None 26 | 27 | toolkit_name: Optional[str] = None 28 | 29 | toolkit_version: Optional[str] = None 30 | 31 | updated_at: Optional[str] = None 32 | 33 | user_id: Optional[str] = None 34 | -------------------------------------------------------------------------------- /src/arcadepy/types/tool_execution_attempt.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List, Optional 4 | 5 | from .._models import BaseModel 6 | from .shared.authorization_response import AuthorizationResponse 7 | 8 | __all__ = ["ToolExecutionAttempt", "Output", "OutputError", "OutputLog"] 9 | 10 | 11 | class OutputError(BaseModel): 12 | message: str 13 | 14 | additional_prompt_content: Optional[str] = None 15 | 16 | can_retry: Optional[bool] = None 17 | 18 | developer_message: Optional[str] = None 19 | 20 | retry_after_ms: Optional[int] = None 21 | 22 | 23 | class OutputLog(BaseModel): 24 | level: str 25 | 26 | message: str 27 | 28 | subtype: Optional[str] = None 29 | 30 | 31 | class Output(BaseModel): 32 | authorization: Optional[AuthorizationResponse] = None 33 | 34 | error: Optional[OutputError] = None 35 | 36 | logs: Optional[List[OutputLog]] = None 37 | 38 | value: Optional[object] = None 39 | 40 | 41 | class ToolExecutionAttempt(BaseModel): 42 | id: Optional[str] = None 43 | 44 | finished_at: Optional[str] = None 45 | 46 | output: Optional[Output] = None 47 | 48 | started_at: Optional[str] = None 49 | 50 | success: Optional[bool] = None 51 | 52 | system_error_message: Optional[str] = None 53 | -------------------------------------------------------------------------------- /src/arcadepy/types/tool_get_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import List 6 | from typing_extensions import Literal, TypedDict 7 | 8 | __all__ = ["ToolGetParams"] 9 | 10 | 11 | class ToolGetParams(TypedDict, total=False): 12 | include_format: List[Literal["arcade", "openai", "anthropic"]] 13 | """Comma separated tool formats that will be included in the response.""" 14 | 15 | user_id: str 16 | """User ID""" 17 | -------------------------------------------------------------------------------- /src/arcadepy/types/tool_get_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List, Optional 4 | 5 | from .._models import BaseModel 6 | from .value_schema import ValueSchema 7 | 8 | __all__ = [ 9 | "ToolGetResponse", 10 | "Input", 11 | "InputParameter", 12 | "Toolkit", 13 | "Output", 14 | "Requirements", 15 | "RequirementsAuthorization", 16 | "RequirementsAuthorizationOauth2", 17 | ] 18 | 19 | 20 | class InputParameter(BaseModel): 21 | name: str 22 | 23 | value_schema: ValueSchema 24 | 25 | description: Optional[str] = None 26 | 27 | inferrable: Optional[bool] = None 28 | 29 | required: Optional[bool] = None 30 | 31 | 32 | class Input(BaseModel): 33 | parameters: Optional[List[InputParameter]] = None 34 | 35 | 36 | class Toolkit(BaseModel): 37 | name: str 38 | 39 | description: Optional[str] = None 40 | 41 | version: Optional[str] = None 42 | 43 | 44 | class Output(BaseModel): 45 | available_modes: Optional[List[str]] = None 46 | 47 | description: Optional[str] = None 48 | 49 | value_schema: Optional[ValueSchema] = None 50 | 51 | 52 | class RequirementsAuthorizationOauth2(BaseModel): 53 | scopes: Optional[List[str]] = None 54 | 55 | 56 | class RequirementsAuthorization(BaseModel): 57 | oauth2: Optional[RequirementsAuthorizationOauth2] = None 58 | 59 | provider_id: Optional[str] = None 60 | 61 | provider_type: Optional[str] = None 62 | 63 | 64 | class Requirements(BaseModel): 65 | authorization: Optional[RequirementsAuthorization] = None 66 | 67 | 68 | class ToolGetResponse(BaseModel): 69 | input: Input 70 | 71 | name: str 72 | 73 | toolkit: Toolkit 74 | 75 | description: Optional[str] = None 76 | 77 | fully_qualified_name: Optional[str] = None 78 | 79 | output: Optional[Output] = None 80 | 81 | requirements: Optional[Requirements] = None 82 | -------------------------------------------------------------------------------- /src/arcadepy/types/tool_list_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import List 6 | from typing_extensions import Literal, TypedDict 7 | 8 | __all__ = ["ToolListParams"] 9 | 10 | 11 | class ToolListParams(TypedDict, total=False): 12 | include_format: List[Literal["arcade", "openai", "anthropic"]] 13 | """Comma separated tool formats that will be included in the response.""" 14 | 15 | limit: int 16 | """Number of items to return (default: 25, max: 100)""" 17 | 18 | offset: int 19 | """Offset from the start of the list (default: 0)""" 20 | 21 | toolkit: str 22 | """Toolkit name""" 23 | 24 | user_id: str 25 | """User ID""" 26 | -------------------------------------------------------------------------------- /src/arcadepy/types/tool_list_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List, Optional 4 | 5 | from .._models import BaseModel 6 | from .value_schema import ValueSchema 7 | 8 | __all__ = [ 9 | "Input", 10 | "InputParameter", 11 | "Toolkit", 12 | "Output", 13 | "Requirements", 14 | "RequirementsAuthorization", 15 | "RequirementsAuthorizationOauth2", 16 | ] 17 | 18 | 19 | class InputParameter(BaseModel): 20 | name: str 21 | 22 | value_schema: ValueSchema 23 | 24 | description: Optional[str] = None 25 | 26 | inferrable: Optional[bool] = None 27 | 28 | required: Optional[bool] = None 29 | 30 | 31 | class Input(BaseModel): 32 | parameters: Optional[List[InputParameter]] = None 33 | 34 | 35 | class Toolkit(BaseModel): 36 | name: str 37 | 38 | description: Optional[str] = None 39 | 40 | version: Optional[str] = None 41 | 42 | 43 | class Output(BaseModel): 44 | available_modes: Optional[List[str]] = None 45 | 46 | description: Optional[str] = None 47 | 48 | value_schema: Optional[ValueSchema] = None 49 | 50 | 51 | class RequirementsAuthorizationOauth2(BaseModel): 52 | scopes: Optional[List[str]] = None 53 | 54 | 55 | class RequirementsAuthorization(BaseModel): 56 | oauth2: Optional[RequirementsAuthorizationOauth2] = None 57 | 58 | provider_id: Optional[str] = None 59 | 60 | provider_type: Optional[str] = None 61 | 62 | 63 | class Requirements(BaseModel): 64 | authorization: Optional[RequirementsAuthorization] = None 65 | -------------------------------------------------------------------------------- /src/arcadepy/types/tools/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from .formatted_get_params import FormattedGetParams as FormattedGetParams 6 | from .formatted_list_params import FormattedListParams as FormattedListParams 7 | from .scheduled_list_params import ScheduledListParams as ScheduledListParams 8 | from .scheduled_get_response import ScheduledGetResponse as ScheduledGetResponse 9 | -------------------------------------------------------------------------------- /src/arcadepy/types/tools/formatted_get_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import TypedDict 6 | 7 | __all__ = ["FormattedGetParams"] 8 | 9 | 10 | class FormattedGetParams(TypedDict, total=False): 11 | format: str 12 | """Provider format""" 13 | 14 | user_id: str 15 | """User ID""" 16 | -------------------------------------------------------------------------------- /src/arcadepy/types/tools/formatted_list_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import TypedDict 6 | 7 | __all__ = ["FormattedListParams"] 8 | 9 | 10 | class FormattedListParams(TypedDict, total=False): 11 | format: str 12 | """Provider format""" 13 | 14 | limit: int 15 | """Number of items to return (default: 25, max: 100)""" 16 | 17 | offset: int 18 | """Offset from the start of the list (default: 0)""" 19 | 20 | toolkit: str 21 | """Toolkit name""" 22 | 23 | user_id: str 24 | """User ID""" 25 | -------------------------------------------------------------------------------- /src/arcadepy/types/tools/scheduled_get_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import Dict, List, Optional 4 | 5 | from ..._models import BaseModel 6 | from ..tool_execution_attempt import ToolExecutionAttempt 7 | 8 | __all__ = ["ScheduledGetResponse"] 9 | 10 | 11 | class ScheduledGetResponse(BaseModel): 12 | id: Optional[str] = None 13 | 14 | attempts: Optional[List[ToolExecutionAttempt]] = None 15 | 16 | created_at: Optional[str] = None 17 | 18 | execution_status: Optional[str] = None 19 | 20 | execution_type: Optional[str] = None 21 | 22 | finished_at: Optional[str] = None 23 | 24 | input: Optional[Dict[str, object]] = None 25 | 26 | run_at: Optional[str] = None 27 | 28 | started_at: Optional[str] = None 29 | 30 | tool_name: Optional[str] = None 31 | 32 | toolkit_name: Optional[str] = None 33 | 34 | toolkit_version: Optional[str] = None 35 | 36 | updated_at: Optional[str] = None 37 | 38 | user_id: Optional[str] = None 39 | -------------------------------------------------------------------------------- /src/arcadepy/types/tools/scheduled_list_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import TypedDict 6 | 7 | __all__ = ["ScheduledListParams"] 8 | 9 | 10 | class ScheduledListParams(TypedDict, total=False): 11 | limit: int 12 | """Number of items to return (default: 25, max: 100)""" 13 | 14 | offset: int 15 | """Offset from the start of the list (default: 0)""" 16 | -------------------------------------------------------------------------------- /src/arcadepy/types/usage.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import Optional 4 | 5 | from .._models import BaseModel 6 | 7 | __all__ = ["Usage"] 8 | 9 | 10 | class Usage(BaseModel): 11 | completion_tokens: Optional[int] = None 12 | 13 | prompt_tokens: Optional[int] = None 14 | 15 | total_tokens: Optional[int] = None 16 | -------------------------------------------------------------------------------- /src/arcadepy/types/value_schema.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List, Optional 4 | 5 | from .._models import BaseModel 6 | 7 | __all__ = ["ValueSchema"] 8 | 9 | 10 | class ValueSchema(BaseModel): 11 | val_type: str 12 | 13 | enum: Optional[List[str]] = None 14 | 15 | inner_val_type: Optional[str] = None 16 | -------------------------------------------------------------------------------- /src/arcadepy/types/worker_create_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import Required, TypedDict 6 | 7 | __all__ = ["WorkerCreateParams", "HTTP", "Mcp"] 8 | 9 | 10 | class WorkerCreateParams(TypedDict, total=False): 11 | id: Required[str] 12 | 13 | enabled: bool 14 | 15 | http: HTTP 16 | 17 | mcp: Mcp 18 | 19 | type: str 20 | 21 | 22 | class HTTP(TypedDict, total=False): 23 | retry: Required[int] 24 | 25 | secret: Required[str] 26 | 27 | timeout: Required[int] 28 | 29 | uri: Required[str] 30 | 31 | 32 | class Mcp(TypedDict, total=False): 33 | retry: Required[int] 34 | 35 | timeout: Required[int] 36 | 37 | uri: Required[str] 38 | -------------------------------------------------------------------------------- /src/arcadepy/types/worker_health_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import Optional 4 | 5 | from .._models import BaseModel 6 | 7 | __all__ = ["WorkerHealthResponse"] 8 | 9 | 10 | class WorkerHealthResponse(BaseModel): 11 | id: Optional[str] = None 12 | 13 | enabled: Optional[bool] = None 14 | 15 | healthy: Optional[bool] = None 16 | 17 | message: Optional[str] = None 18 | -------------------------------------------------------------------------------- /src/arcadepy/types/worker_list_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import TypedDict 6 | 7 | __all__ = ["WorkerListParams"] 8 | 9 | 10 | class WorkerListParams(TypedDict, total=False): 11 | limit: int 12 | """Number of items to return (default: 25, max: 100)""" 13 | 14 | offset: int 15 | """Offset from the start of the list (default: 0)""" 16 | -------------------------------------------------------------------------------- /src/arcadepy/types/worker_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import Optional 4 | from typing_extensions import Literal 5 | 6 | from .._models import BaseModel 7 | 8 | __all__ = ["WorkerResponse", "Binding", "HTTP", "HTTPSecret", "Mcp", "Oxp", "OxpSecret"] 9 | 10 | 11 | class Binding(BaseModel): 12 | id: Optional[str] = None 13 | 14 | type: Optional[Literal["static", "tenant", "project", "account"]] = None 15 | 16 | 17 | class HTTPSecret(BaseModel): 18 | binding: Optional[Literal["static", "tenant", "project", "account"]] = None 19 | 20 | editable: Optional[bool] = None 21 | 22 | exists: Optional[bool] = None 23 | 24 | hint: Optional[str] = None 25 | 26 | value: Optional[str] = None 27 | 28 | 29 | class HTTP(BaseModel): 30 | retry: Optional[int] = None 31 | 32 | secret: Optional[HTTPSecret] = None 33 | 34 | timeout: Optional[int] = None 35 | 36 | uri: Optional[str] = None 37 | 38 | 39 | class Mcp(BaseModel): 40 | retry: Optional[int] = None 41 | 42 | timeout: Optional[int] = None 43 | 44 | uri: Optional[str] = None 45 | 46 | 47 | class OxpSecret(BaseModel): 48 | binding: Optional[Literal["static", "tenant", "project", "account"]] = None 49 | 50 | editable: Optional[bool] = None 51 | 52 | exists: Optional[bool] = None 53 | 54 | hint: Optional[str] = None 55 | 56 | value: Optional[str] = None 57 | 58 | 59 | class Oxp(BaseModel): 60 | retry: Optional[int] = None 61 | 62 | secret: Optional[OxpSecret] = None 63 | 64 | timeout: Optional[int] = None 65 | 66 | uri: Optional[str] = None 67 | 68 | 69 | class WorkerResponse(BaseModel): 70 | id: Optional[str] = None 71 | 72 | binding: Optional[Binding] = None 73 | 74 | enabled: Optional[bool] = None 75 | 76 | http: Optional[HTTP] = None 77 | 78 | mcp: Optional[Mcp] = None 79 | 80 | oxp: Optional[Oxp] = None 81 | 82 | type: Optional[Literal["http", "mcp", "unknown"]] = None 83 | -------------------------------------------------------------------------------- /src/arcadepy/types/worker_tools_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import TypedDict 6 | 7 | __all__ = ["WorkerToolsParams"] 8 | 9 | 10 | class WorkerToolsParams(TypedDict, total=False): 11 | limit: int 12 | """Number of items to return (default: 25, max: 100)""" 13 | 14 | offset: int 15 | """Offset from the start of the list (default: 0)""" 16 | -------------------------------------------------------------------------------- /src/arcadepy/types/worker_update_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import TypedDict 6 | 7 | __all__ = ["WorkerUpdateParams", "HTTP", "Mcp"] 8 | 9 | 10 | class WorkerUpdateParams(TypedDict, total=False): 11 | enabled: bool 12 | 13 | http: HTTP 14 | 15 | mcp: Mcp 16 | 17 | 18 | class HTTP(TypedDict, total=False): 19 | retry: int 20 | 21 | secret: str 22 | 23 | timeout: int 24 | 25 | uri: str 26 | 27 | 28 | class Mcp(TypedDict, total=False): 29 | retry: int 30 | 31 | timeout: int 32 | 33 | uri: str 34 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | -------------------------------------------------------------------------------- /tests/api_resources/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | -------------------------------------------------------------------------------- /tests/api_resources/chat/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | -------------------------------------------------------------------------------- /tests/api_resources/chat/test_completions.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | from typing import Any, cast 7 | 8 | import pytest 9 | 10 | from arcadepy import Arcade, AsyncArcade 11 | from tests.utils import assert_matches_type 12 | from arcadepy.types import ChatResponse 13 | 14 | base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") 15 | 16 | 17 | class TestCompletions: 18 | parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) 19 | 20 | @parametrize 21 | def test_method_create(self, client: Arcade) -> None: 22 | completion = client.chat.completions.create() 23 | assert_matches_type(ChatResponse, completion, path=["response"]) 24 | 25 | @parametrize 26 | def test_method_create_with_all_params(self, client: Arcade) -> None: 27 | completion = client.chat.completions.create( 28 | frequency_penalty=0, 29 | logit_bias={"foo": 0}, 30 | logprobs=True, 31 | max_tokens=0, 32 | messages=[ 33 | { 34 | "content": "content", 35 | "role": "role", 36 | "name": "name", 37 | "tool_call_id": "tool_call_id", 38 | "tool_calls": [ 39 | { 40 | "id": "id", 41 | "function": { 42 | "arguments": "arguments", 43 | "name": "name", 44 | }, 45 | "type": "function", 46 | } 47 | ], 48 | } 49 | ], 50 | model="model", 51 | n=0, 52 | parallel_tool_calls=True, 53 | presence_penalty=0, 54 | response_format={"type": "json_object"}, 55 | seed=0, 56 | stop=["string"], 57 | stream=True, 58 | stream_options={"include_usage": True}, 59 | temperature=0, 60 | tool_choice={}, 61 | tools={}, 62 | top_logprobs=0, 63 | top_p=0, 64 | user="user", 65 | ) 66 | assert_matches_type(ChatResponse, completion, path=["response"]) 67 | 68 | @parametrize 69 | def test_raw_response_create(self, client: Arcade) -> None: 70 | response = client.chat.completions.with_raw_response.create() 71 | 72 | assert response.is_closed is True 73 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 74 | completion = response.parse() 75 | assert_matches_type(ChatResponse, completion, path=["response"]) 76 | 77 | @parametrize 78 | def test_streaming_response_create(self, client: Arcade) -> None: 79 | with client.chat.completions.with_streaming_response.create() as response: 80 | assert not response.is_closed 81 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 82 | 83 | completion = response.parse() 84 | assert_matches_type(ChatResponse, completion, path=["response"]) 85 | 86 | assert cast(Any, response.is_closed) is True 87 | 88 | 89 | class TestAsyncCompletions: 90 | parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) 91 | 92 | @parametrize 93 | async def test_method_create(self, async_client: AsyncArcade) -> None: 94 | completion = await async_client.chat.completions.create() 95 | assert_matches_type(ChatResponse, completion, path=["response"]) 96 | 97 | @parametrize 98 | async def test_method_create_with_all_params(self, async_client: AsyncArcade) -> None: 99 | completion = await async_client.chat.completions.create( 100 | frequency_penalty=0, 101 | logit_bias={"foo": 0}, 102 | logprobs=True, 103 | max_tokens=0, 104 | messages=[ 105 | { 106 | "content": "content", 107 | "role": "role", 108 | "name": "name", 109 | "tool_call_id": "tool_call_id", 110 | "tool_calls": [ 111 | { 112 | "id": "id", 113 | "function": { 114 | "arguments": "arguments", 115 | "name": "name", 116 | }, 117 | "type": "function", 118 | } 119 | ], 120 | } 121 | ], 122 | model="model", 123 | n=0, 124 | parallel_tool_calls=True, 125 | presence_penalty=0, 126 | response_format={"type": "json_object"}, 127 | seed=0, 128 | stop=["string"], 129 | stream=True, 130 | stream_options={"include_usage": True}, 131 | temperature=0, 132 | tool_choice={}, 133 | tools={}, 134 | top_logprobs=0, 135 | top_p=0, 136 | user="user", 137 | ) 138 | assert_matches_type(ChatResponse, completion, path=["response"]) 139 | 140 | @parametrize 141 | async def test_raw_response_create(self, async_client: AsyncArcade) -> None: 142 | response = await async_client.chat.completions.with_raw_response.create() 143 | 144 | assert response.is_closed is True 145 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 146 | completion = await response.parse() 147 | assert_matches_type(ChatResponse, completion, path=["response"]) 148 | 149 | @parametrize 150 | async def test_streaming_response_create(self, async_client: AsyncArcade) -> None: 151 | async with async_client.chat.completions.with_streaming_response.create() as response: 152 | assert not response.is_closed 153 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 154 | 155 | completion = await response.parse() 156 | assert_matches_type(ChatResponse, completion, path=["response"]) 157 | 158 | assert cast(Any, response.is_closed) is True 159 | -------------------------------------------------------------------------------- /tests/api_resources/test_auth.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | from typing import Any, cast 7 | 8 | import pytest 9 | 10 | from arcadepy import Arcade, AsyncArcade 11 | from tests.utils import assert_matches_type 12 | from arcadepy.types.shared import AuthorizationResponse 13 | 14 | base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") 15 | 16 | 17 | class TestAuth: 18 | parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) 19 | 20 | @parametrize 21 | def test_method_authorize(self, client: Arcade) -> None: 22 | auth = client.auth.authorize( 23 | auth_requirement={}, 24 | user_id="user_id", 25 | ) 26 | assert_matches_type(AuthorizationResponse, auth, path=["response"]) 27 | 28 | @parametrize 29 | def test_method_authorize_with_all_params(self, client: Arcade) -> None: 30 | auth = client.auth.authorize( 31 | auth_requirement={ 32 | "id": "id", 33 | "oauth2": {"scopes": ["string"]}, 34 | "provider_id": "provider_id", 35 | "provider_type": "provider_type", 36 | }, 37 | user_id="user_id", 38 | next_uri="next_uri", 39 | ) 40 | assert_matches_type(AuthorizationResponse, auth, path=["response"]) 41 | 42 | @parametrize 43 | def test_raw_response_authorize(self, client: Arcade) -> None: 44 | response = client.auth.with_raw_response.authorize( 45 | auth_requirement={}, 46 | user_id="user_id", 47 | ) 48 | 49 | assert response.is_closed is True 50 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 51 | auth = response.parse() 52 | assert_matches_type(AuthorizationResponse, auth, path=["response"]) 53 | 54 | @parametrize 55 | def test_streaming_response_authorize(self, client: Arcade) -> None: 56 | with client.auth.with_streaming_response.authorize( 57 | auth_requirement={}, 58 | user_id="user_id", 59 | ) as response: 60 | assert not response.is_closed 61 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 62 | 63 | auth = response.parse() 64 | assert_matches_type(AuthorizationResponse, auth, path=["response"]) 65 | 66 | assert cast(Any, response.is_closed) is True 67 | 68 | @parametrize 69 | def test_method_status(self, client: Arcade) -> None: 70 | auth = client.auth.status( 71 | id="id", 72 | ) 73 | assert_matches_type(AuthorizationResponse, auth, path=["response"]) 74 | 75 | @parametrize 76 | def test_method_status_with_all_params(self, client: Arcade) -> None: 77 | auth = client.auth.status( 78 | id="id", 79 | wait=0, 80 | ) 81 | assert_matches_type(AuthorizationResponse, auth, path=["response"]) 82 | 83 | @parametrize 84 | def test_raw_response_status(self, client: Arcade) -> None: 85 | response = client.auth.with_raw_response.status( 86 | id="id", 87 | ) 88 | 89 | assert response.is_closed is True 90 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 91 | auth = response.parse() 92 | assert_matches_type(AuthorizationResponse, auth, path=["response"]) 93 | 94 | @parametrize 95 | def test_streaming_response_status(self, client: Arcade) -> None: 96 | with client.auth.with_streaming_response.status( 97 | id="id", 98 | ) as response: 99 | assert not response.is_closed 100 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 101 | 102 | auth = response.parse() 103 | assert_matches_type(AuthorizationResponse, auth, path=["response"]) 104 | 105 | assert cast(Any, response.is_closed) is True 106 | 107 | 108 | class TestAsyncAuth: 109 | parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) 110 | 111 | @parametrize 112 | async def test_method_authorize(self, async_client: AsyncArcade) -> None: 113 | auth = await async_client.auth.authorize( 114 | auth_requirement={}, 115 | user_id="user_id", 116 | ) 117 | assert_matches_type(AuthorizationResponse, auth, path=["response"]) 118 | 119 | @parametrize 120 | async def test_method_authorize_with_all_params(self, async_client: AsyncArcade) -> None: 121 | auth = await async_client.auth.authorize( 122 | auth_requirement={ 123 | "id": "id", 124 | "oauth2": {"scopes": ["string"]}, 125 | "provider_id": "provider_id", 126 | "provider_type": "provider_type", 127 | }, 128 | user_id="user_id", 129 | next_uri="next_uri", 130 | ) 131 | assert_matches_type(AuthorizationResponse, auth, path=["response"]) 132 | 133 | @parametrize 134 | async def test_raw_response_authorize(self, async_client: AsyncArcade) -> None: 135 | response = await async_client.auth.with_raw_response.authorize( 136 | auth_requirement={}, 137 | user_id="user_id", 138 | ) 139 | 140 | assert response.is_closed is True 141 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 142 | auth = await response.parse() 143 | assert_matches_type(AuthorizationResponse, auth, path=["response"]) 144 | 145 | @parametrize 146 | async def test_streaming_response_authorize(self, async_client: AsyncArcade) -> None: 147 | async with async_client.auth.with_streaming_response.authorize( 148 | auth_requirement={}, 149 | user_id="user_id", 150 | ) as response: 151 | assert not response.is_closed 152 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 153 | 154 | auth = await response.parse() 155 | assert_matches_type(AuthorizationResponse, auth, path=["response"]) 156 | 157 | assert cast(Any, response.is_closed) is True 158 | 159 | @parametrize 160 | async def test_method_status(self, async_client: AsyncArcade) -> None: 161 | auth = await async_client.auth.status( 162 | id="id", 163 | ) 164 | assert_matches_type(AuthorizationResponse, auth, path=["response"]) 165 | 166 | @parametrize 167 | async def test_method_status_with_all_params(self, async_client: AsyncArcade) -> None: 168 | auth = await async_client.auth.status( 169 | id="id", 170 | wait=0, 171 | ) 172 | assert_matches_type(AuthorizationResponse, auth, path=["response"]) 173 | 174 | @parametrize 175 | async def test_raw_response_status(self, async_client: AsyncArcade) -> None: 176 | response = await async_client.auth.with_raw_response.status( 177 | id="id", 178 | ) 179 | 180 | assert response.is_closed is True 181 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 182 | auth = await response.parse() 183 | assert_matches_type(AuthorizationResponse, auth, path=["response"]) 184 | 185 | @parametrize 186 | async def test_streaming_response_status(self, async_client: AsyncArcade) -> None: 187 | async with async_client.auth.with_streaming_response.status( 188 | id="id", 189 | ) as response: 190 | assert not response.is_closed 191 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 192 | 193 | auth = await response.parse() 194 | assert_matches_type(AuthorizationResponse, auth, path=["response"]) 195 | 196 | assert cast(Any, response.is_closed) is True 197 | -------------------------------------------------------------------------------- /tests/api_resources/test_auth_start.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from unittest.mock import Mock, AsyncMock 3 | 4 | import pytest 5 | 6 | from arcadepy._client import Arcade, AsyncArcade 7 | from arcadepy.types.shared import AuthorizationResponse 8 | from arcadepy.resources.auth import AuthResource, AsyncAuthResource 9 | from arcadepy.types.auth_authorize_params import AuthRequirement, AuthRequirementOauth2 10 | 11 | parametrize_provider_type = pytest.mark.parametrize( 12 | "provider_type, expected_provider_type", 13 | [ 14 | (None, "oauth2"), 15 | ("oauth2", "oauth2"), 16 | ("custom_type", "custom_type"), 17 | ], 18 | ) 19 | 20 | parametrize_scopes = pytest.mark.parametrize( 21 | "scopes, expected_scopes", 22 | [ 23 | (["scope1", "scope2"], ["scope1", "scope2"]), 24 | (None, []), 25 | ], 26 | ) 27 | 28 | 29 | @pytest.fixture 30 | def sync_auth_resource() -> AuthResource: 31 | client = Arcade(api_key="test") 32 | auth = AuthResource(client) 33 | return auth 34 | 35 | 36 | @pytest.fixture 37 | def async_auth_resource() -> AsyncAuthResource: 38 | client = AsyncArcade(api_key="test") 39 | auth = AsyncAuthResource(client) 40 | return auth 41 | 42 | 43 | @parametrize_provider_type 44 | @parametrize_scopes 45 | def test_start_calls_authorize_with_correct_params( 46 | sync_auth_resource: AuthResource, 47 | provider_type: Optional[str], 48 | expected_provider_type: str, 49 | scopes: Optional[List[str]], 50 | expected_scopes: List[str], 51 | ) -> None: 52 | auth = sync_auth_resource 53 | auth.authorize = Mock(return_value=AuthorizationResponse(status="pending")) # type: ignore 54 | 55 | user_id = "user_id" 56 | provider = "github" 57 | 58 | auth.start(user_id, provider, provider_type=provider_type, scopes=scopes) 59 | 60 | auth.authorize.assert_called_with( 61 | auth_requirement=AuthRequirement( 62 | provider_id=provider, 63 | provider_type=expected_provider_type, 64 | oauth2=AuthRequirementOauth2(scopes=expected_scopes), 65 | ), 66 | user_id=user_id, 67 | ) 68 | 69 | 70 | @pytest.mark.asyncio 71 | @parametrize_provider_type 72 | @parametrize_scopes 73 | async def test_async_start_calls_authorize_with_correct_params( 74 | async_auth_resource: AsyncAuthResource, 75 | provider_type: Optional[str], 76 | expected_provider_type: str, 77 | scopes: Optional[List[str]], 78 | expected_scopes: List[str], 79 | ) -> None: 80 | auth = async_auth_resource 81 | auth.authorize = AsyncMock(return_value=AuthorizationResponse(status="pending")) # type: ignore 82 | 83 | user_id = "user_id" 84 | provider = "github" 85 | 86 | await auth.start(user_id, provider, provider_type=provider_type, scopes=scopes) 87 | 88 | auth.authorize.assert_called_with( 89 | auth_requirement=AuthRequirement( 90 | provider_id=provider, 91 | provider_type=expected_provider_type, 92 | oauth2=AuthRequirementOauth2(scopes=expected_scopes), 93 | ), 94 | user_id=user_id, 95 | ) 96 | -------------------------------------------------------------------------------- /tests/api_resources/test_auth_wait.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, AsyncMock 2 | 3 | import pytest 4 | 5 | from arcadepy._client import Arcade, AsyncArcade 6 | from arcadepy.types.shared import AuthorizationResponse 7 | from arcadepy.resources.auth import AuthResource, AsyncAuthResource 8 | 9 | 10 | @pytest.fixture 11 | def sync_auth_resource() -> AuthResource: 12 | client = Arcade(api_key="test") 13 | auth = AuthResource(client) 14 | return auth 15 | 16 | 17 | @pytest.fixture 18 | def async_auth_resource() -> AsyncAuthResource: 19 | client = AsyncArcade(api_key="test") 20 | auth = AsyncAuthResource(client) 21 | return auth 22 | 23 | 24 | def test_wait_for_completion_calls_status_from_auth_response(sync_auth_resource: AuthResource) -> None: 25 | auth = sync_auth_resource 26 | auth.status = Mock(return_value=AuthorizationResponse(status="completed")) # type: ignore 27 | 28 | auth_response = AuthorizationResponse(status="pending", id="auth_id123") 29 | 30 | auth.wait_for_completion(auth_response) 31 | 32 | auth.status.assert_called_with(id="auth_id123", wait=45) 33 | 34 | 35 | def test_wait_for_completion_raises_value_error_for_empty_authorization_id(sync_auth_resource: AuthResource) -> None: 36 | auth = sync_auth_resource 37 | auth_response = AuthorizationResponse(status="pending", id="", scopes=["scope1"]) 38 | 39 | with pytest.raises(ValueError, match="Authorization ID is required"): 40 | auth.wait_for_completion(auth_response) 41 | 42 | 43 | def test_wait_for_completion_calls_status_with_auth_id(sync_auth_resource: AuthResource) -> None: 44 | auth = sync_auth_resource 45 | auth.status = Mock(return_value=AuthorizationResponse(status="completed")) # type: ignore 46 | 47 | auth.wait_for_completion("auth_id456") 48 | 49 | auth.status.assert_called_with(id="auth_id456", wait=45) 50 | 51 | 52 | @pytest.mark.asyncio 53 | async def test_async_wait_for_completion_calls_status_from_auth_response( 54 | async_auth_resource: AsyncAuthResource, 55 | ) -> None: 56 | auth = async_auth_resource 57 | auth.status = AsyncMock(return_value=AuthorizationResponse(status="completed")) # type: ignore 58 | 59 | auth_response = AuthorizationResponse(status="pending", id="auth_id789") 60 | 61 | await auth.wait_for_completion(auth_response) 62 | 63 | auth.status.assert_called_with(id="auth_id789", wait=45) 64 | 65 | 66 | @pytest.mark.asyncio 67 | async def test_async_wait_for_completion_raises_value_error_for_empty_authorization_id( 68 | async_auth_resource: AsyncAuthResource, 69 | ) -> None: 70 | auth = async_auth_resource 71 | auth_response = AuthorizationResponse(status="pending", id="", scopes=["scope1"]) 72 | 73 | with pytest.raises(ValueError, match="Authorization ID is required"): 74 | await auth.wait_for_completion(auth_response) 75 | 76 | 77 | @pytest.mark.asyncio 78 | async def test_async_wait_for_completion_calls_status_with_auth_id(async_auth_resource: AsyncAuthResource) -> None: 79 | auth = async_auth_resource 80 | auth.status = AsyncMock(return_value=AuthorizationResponse(status="completed")) # type: ignore 81 | 82 | await auth.wait_for_completion("auth_id321") 83 | 84 | auth.status.assert_called_with(id="auth_id321", wait=45) 85 | -------------------------------------------------------------------------------- /tests/api_resources/test_health.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | from typing import Any, cast 7 | 8 | import pytest 9 | 10 | from arcadepy import Arcade, AsyncArcade 11 | from tests.utils import assert_matches_type 12 | from arcadepy.types import HealthSchema 13 | 14 | base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") 15 | 16 | 17 | class TestHealth: 18 | parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) 19 | 20 | @parametrize 21 | def test_method_check(self, client: Arcade) -> None: 22 | health = client.health.check() 23 | assert_matches_type(HealthSchema, health, path=["response"]) 24 | 25 | @parametrize 26 | def test_raw_response_check(self, client: Arcade) -> None: 27 | response = client.health.with_raw_response.check() 28 | 29 | assert response.is_closed is True 30 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 31 | health = response.parse() 32 | assert_matches_type(HealthSchema, health, path=["response"]) 33 | 34 | @parametrize 35 | def test_streaming_response_check(self, client: Arcade) -> None: 36 | with client.health.with_streaming_response.check() as response: 37 | assert not response.is_closed 38 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 39 | 40 | health = response.parse() 41 | assert_matches_type(HealthSchema, health, path=["response"]) 42 | 43 | assert cast(Any, response.is_closed) is True 44 | 45 | 46 | class TestAsyncHealth: 47 | parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) 48 | 49 | @parametrize 50 | async def test_method_check(self, async_client: AsyncArcade) -> None: 51 | health = await async_client.health.check() 52 | assert_matches_type(HealthSchema, health, path=["response"]) 53 | 54 | @parametrize 55 | async def test_raw_response_check(self, async_client: AsyncArcade) -> None: 56 | response = await async_client.health.with_raw_response.check() 57 | 58 | assert response.is_closed is True 59 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 60 | health = await response.parse() 61 | assert_matches_type(HealthSchema, health, path=["response"]) 62 | 63 | @parametrize 64 | async def test_streaming_response_check(self, async_client: AsyncArcade) -> None: 65 | async with async_client.health.with_streaming_response.check() as response: 66 | assert not response.is_closed 67 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 68 | 69 | health = await response.parse() 70 | assert_matches_type(HealthSchema, health, path=["response"]) 71 | 72 | assert cast(Any, response.is_closed) is True 73 | -------------------------------------------------------------------------------- /tests/api_resources/tools/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | -------------------------------------------------------------------------------- /tests/api_resources/tools/test_formatted.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | from typing import Any, cast 7 | 8 | import pytest 9 | 10 | from arcadepy import Arcade, AsyncArcade 11 | from tests.utils import assert_matches_type 12 | from arcadepy.pagination import SyncOffsetPage, AsyncOffsetPage 13 | 14 | base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") 15 | 16 | 17 | class TestFormatted: 18 | parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) 19 | 20 | @parametrize 21 | def test_method_list(self, client: Arcade) -> None: 22 | formatted = client.tools.formatted.list() 23 | assert_matches_type(SyncOffsetPage[object], formatted, path=["response"]) 24 | 25 | @parametrize 26 | def test_method_list_with_all_params(self, client: Arcade) -> None: 27 | formatted = client.tools.formatted.list( 28 | format="format", 29 | limit=0, 30 | offset=0, 31 | toolkit="toolkit", 32 | user_id="user_id", 33 | ) 34 | assert_matches_type(SyncOffsetPage[object], formatted, path=["response"]) 35 | 36 | @parametrize 37 | def test_raw_response_list(self, client: Arcade) -> None: 38 | response = client.tools.formatted.with_raw_response.list() 39 | 40 | assert response.is_closed is True 41 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 42 | formatted = response.parse() 43 | assert_matches_type(SyncOffsetPage[object], formatted, path=["response"]) 44 | 45 | @parametrize 46 | def test_streaming_response_list(self, client: Arcade) -> None: 47 | with client.tools.formatted.with_streaming_response.list() as response: 48 | assert not response.is_closed 49 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 50 | 51 | formatted = response.parse() 52 | assert_matches_type(SyncOffsetPage[object], formatted, path=["response"]) 53 | 54 | assert cast(Any, response.is_closed) is True 55 | 56 | @parametrize 57 | def test_method_get(self, client: Arcade) -> None: 58 | formatted = client.tools.formatted.get( 59 | name="name", 60 | ) 61 | assert_matches_type(object, formatted, path=["response"]) 62 | 63 | @parametrize 64 | def test_method_get_with_all_params(self, client: Arcade) -> None: 65 | formatted = client.tools.formatted.get( 66 | name="name", 67 | format="format", 68 | user_id="user_id", 69 | ) 70 | assert_matches_type(object, formatted, path=["response"]) 71 | 72 | @parametrize 73 | def test_raw_response_get(self, client: Arcade) -> None: 74 | response = client.tools.formatted.with_raw_response.get( 75 | name="name", 76 | ) 77 | 78 | assert response.is_closed is True 79 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 80 | formatted = response.parse() 81 | assert_matches_type(object, formatted, path=["response"]) 82 | 83 | @parametrize 84 | def test_streaming_response_get(self, client: Arcade) -> None: 85 | with client.tools.formatted.with_streaming_response.get( 86 | name="name", 87 | ) as response: 88 | assert not response.is_closed 89 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 90 | 91 | formatted = response.parse() 92 | assert_matches_type(object, formatted, path=["response"]) 93 | 94 | assert cast(Any, response.is_closed) is True 95 | 96 | @parametrize 97 | def test_path_params_get(self, client: Arcade) -> None: 98 | with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): 99 | client.tools.formatted.with_raw_response.get( 100 | name="", 101 | ) 102 | 103 | 104 | class TestAsyncFormatted: 105 | parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) 106 | 107 | @parametrize 108 | async def test_method_list(self, async_client: AsyncArcade) -> None: 109 | formatted = await async_client.tools.formatted.list() 110 | assert_matches_type(AsyncOffsetPage[object], formatted, path=["response"]) 111 | 112 | @parametrize 113 | async def test_method_list_with_all_params(self, async_client: AsyncArcade) -> None: 114 | formatted = await async_client.tools.formatted.list( 115 | format="format", 116 | limit=0, 117 | offset=0, 118 | toolkit="toolkit", 119 | user_id="user_id", 120 | ) 121 | assert_matches_type(AsyncOffsetPage[object], formatted, path=["response"]) 122 | 123 | @parametrize 124 | async def test_raw_response_list(self, async_client: AsyncArcade) -> None: 125 | response = await async_client.tools.formatted.with_raw_response.list() 126 | 127 | assert response.is_closed is True 128 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 129 | formatted = await response.parse() 130 | assert_matches_type(AsyncOffsetPage[object], formatted, path=["response"]) 131 | 132 | @parametrize 133 | async def test_streaming_response_list(self, async_client: AsyncArcade) -> None: 134 | async with async_client.tools.formatted.with_streaming_response.list() as response: 135 | assert not response.is_closed 136 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 137 | 138 | formatted = await response.parse() 139 | assert_matches_type(AsyncOffsetPage[object], formatted, path=["response"]) 140 | 141 | assert cast(Any, response.is_closed) is True 142 | 143 | @parametrize 144 | async def test_method_get(self, async_client: AsyncArcade) -> None: 145 | formatted = await async_client.tools.formatted.get( 146 | name="name", 147 | ) 148 | assert_matches_type(object, formatted, path=["response"]) 149 | 150 | @parametrize 151 | async def test_method_get_with_all_params(self, async_client: AsyncArcade) -> None: 152 | formatted = await async_client.tools.formatted.get( 153 | name="name", 154 | format="format", 155 | user_id="user_id", 156 | ) 157 | assert_matches_type(object, formatted, path=["response"]) 158 | 159 | @parametrize 160 | async def test_raw_response_get(self, async_client: AsyncArcade) -> None: 161 | response = await async_client.tools.formatted.with_raw_response.get( 162 | name="name", 163 | ) 164 | 165 | assert response.is_closed is True 166 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 167 | formatted = await response.parse() 168 | assert_matches_type(object, formatted, path=["response"]) 169 | 170 | @parametrize 171 | async def test_streaming_response_get(self, async_client: AsyncArcade) -> None: 172 | async with async_client.tools.formatted.with_streaming_response.get( 173 | name="name", 174 | ) as response: 175 | assert not response.is_closed 176 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 177 | 178 | formatted = await response.parse() 179 | assert_matches_type(object, formatted, path=["response"]) 180 | 181 | assert cast(Any, response.is_closed) is True 182 | 183 | @parametrize 184 | async def test_path_params_get(self, async_client: AsyncArcade) -> None: 185 | with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): 186 | await async_client.tools.formatted.with_raw_response.get( 187 | name="", 188 | ) 189 | -------------------------------------------------------------------------------- /tests/api_resources/tools/test_scheduled.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | from typing import Any, cast 7 | 8 | import pytest 9 | 10 | from arcadepy import Arcade, AsyncArcade 11 | from tests.utils import assert_matches_type 12 | from arcadepy.types import ToolExecution 13 | from arcadepy.pagination import SyncOffsetPage, AsyncOffsetPage 14 | from arcadepy.types.tools import ScheduledGetResponse 15 | 16 | base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") 17 | 18 | 19 | class TestScheduled: 20 | parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) 21 | 22 | @parametrize 23 | def test_method_list(self, client: Arcade) -> None: 24 | scheduled = client.tools.scheduled.list() 25 | assert_matches_type(SyncOffsetPage[ToolExecution], scheduled, path=["response"]) 26 | 27 | @parametrize 28 | def test_method_list_with_all_params(self, client: Arcade) -> None: 29 | scheduled = client.tools.scheduled.list( 30 | limit=0, 31 | offset=0, 32 | ) 33 | assert_matches_type(SyncOffsetPage[ToolExecution], scheduled, path=["response"]) 34 | 35 | @parametrize 36 | def test_raw_response_list(self, client: Arcade) -> None: 37 | response = client.tools.scheduled.with_raw_response.list() 38 | 39 | assert response.is_closed is True 40 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 41 | scheduled = response.parse() 42 | assert_matches_type(SyncOffsetPage[ToolExecution], scheduled, path=["response"]) 43 | 44 | @parametrize 45 | def test_streaming_response_list(self, client: Arcade) -> None: 46 | with client.tools.scheduled.with_streaming_response.list() as response: 47 | assert not response.is_closed 48 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 49 | 50 | scheduled = response.parse() 51 | assert_matches_type(SyncOffsetPage[ToolExecution], scheduled, path=["response"]) 52 | 53 | assert cast(Any, response.is_closed) is True 54 | 55 | @parametrize 56 | def test_method_get(self, client: Arcade) -> None: 57 | scheduled = client.tools.scheduled.get( 58 | "id", 59 | ) 60 | assert_matches_type(ScheduledGetResponse, scheduled, path=["response"]) 61 | 62 | @parametrize 63 | def test_raw_response_get(self, client: Arcade) -> None: 64 | response = client.tools.scheduled.with_raw_response.get( 65 | "id", 66 | ) 67 | 68 | assert response.is_closed is True 69 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 70 | scheduled = response.parse() 71 | assert_matches_type(ScheduledGetResponse, scheduled, path=["response"]) 72 | 73 | @parametrize 74 | def test_streaming_response_get(self, client: Arcade) -> None: 75 | with client.tools.scheduled.with_streaming_response.get( 76 | "id", 77 | ) as response: 78 | assert not response.is_closed 79 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 80 | 81 | scheduled = response.parse() 82 | assert_matches_type(ScheduledGetResponse, scheduled, path=["response"]) 83 | 84 | assert cast(Any, response.is_closed) is True 85 | 86 | @parametrize 87 | def test_path_params_get(self, client: Arcade) -> None: 88 | with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): 89 | client.tools.scheduled.with_raw_response.get( 90 | "", 91 | ) 92 | 93 | 94 | class TestAsyncScheduled: 95 | parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) 96 | 97 | @parametrize 98 | async def test_method_list(self, async_client: AsyncArcade) -> None: 99 | scheduled = await async_client.tools.scheduled.list() 100 | assert_matches_type(AsyncOffsetPage[ToolExecution], scheduled, path=["response"]) 101 | 102 | @parametrize 103 | async def test_method_list_with_all_params(self, async_client: AsyncArcade) -> None: 104 | scheduled = await async_client.tools.scheduled.list( 105 | limit=0, 106 | offset=0, 107 | ) 108 | assert_matches_type(AsyncOffsetPage[ToolExecution], scheduled, path=["response"]) 109 | 110 | @parametrize 111 | async def test_raw_response_list(self, async_client: AsyncArcade) -> None: 112 | response = await async_client.tools.scheduled.with_raw_response.list() 113 | 114 | assert response.is_closed is True 115 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 116 | scheduled = await response.parse() 117 | assert_matches_type(AsyncOffsetPage[ToolExecution], scheduled, path=["response"]) 118 | 119 | @parametrize 120 | async def test_streaming_response_list(self, async_client: AsyncArcade) -> None: 121 | async with async_client.tools.scheduled.with_streaming_response.list() as response: 122 | assert not response.is_closed 123 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 124 | 125 | scheduled = await response.parse() 126 | assert_matches_type(AsyncOffsetPage[ToolExecution], scheduled, path=["response"]) 127 | 128 | assert cast(Any, response.is_closed) is True 129 | 130 | @parametrize 131 | async def test_method_get(self, async_client: AsyncArcade) -> None: 132 | scheduled = await async_client.tools.scheduled.get( 133 | "id", 134 | ) 135 | assert_matches_type(ScheduledGetResponse, scheduled, path=["response"]) 136 | 137 | @parametrize 138 | async def test_raw_response_get(self, async_client: AsyncArcade) -> None: 139 | response = await async_client.tools.scheduled.with_raw_response.get( 140 | "id", 141 | ) 142 | 143 | assert response.is_closed is True 144 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 145 | scheduled = await response.parse() 146 | assert_matches_type(ScheduledGetResponse, scheduled, path=["response"]) 147 | 148 | @parametrize 149 | async def test_streaming_response_get(self, async_client: AsyncArcade) -> None: 150 | async with async_client.tools.scheduled.with_streaming_response.get( 151 | "id", 152 | ) as response: 153 | assert not response.is_closed 154 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 155 | 156 | scheduled = await response.parse() 157 | assert_matches_type(ScheduledGetResponse, scheduled, path=["response"]) 158 | 159 | assert cast(Any, response.is_closed) is True 160 | 161 | @parametrize 162 | async def test_path_params_get(self, async_client: AsyncArcade) -> None: 163 | with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): 164 | await async_client.tools.scheduled.with_raw_response.get( 165 | "", 166 | ) 167 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import logging 5 | from typing import TYPE_CHECKING, Iterator, AsyncIterator 6 | 7 | import pytest 8 | from pytest_asyncio import is_async_test 9 | 10 | from arcadepy import Arcade, AsyncArcade 11 | 12 | if TYPE_CHECKING: 13 | from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] 14 | 15 | pytest.register_assert_rewrite("tests.utils") 16 | 17 | logging.getLogger("arcadepy").setLevel(logging.DEBUG) 18 | 19 | 20 | # automatically add `pytest.mark.asyncio()` to all of our async tests 21 | # so we don't have to add that boilerplate everywhere 22 | def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: 23 | pytest_asyncio_tests = (item for item in items if is_async_test(item)) 24 | session_scope_marker = pytest.mark.asyncio(loop_scope="session") 25 | for async_test in pytest_asyncio_tests: 26 | async_test.add_marker(session_scope_marker, append=False) 27 | 28 | 29 | base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") 30 | 31 | api_key = "My API Key" 32 | 33 | 34 | @pytest.fixture(scope="session") 35 | def client(request: FixtureRequest) -> Iterator[Arcade]: 36 | strict = getattr(request, "param", True) 37 | if not isinstance(strict, bool): 38 | raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") 39 | 40 | with Arcade(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: 41 | yield client 42 | 43 | 44 | @pytest.fixture(scope="session") 45 | async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncArcade]: 46 | strict = getattr(request, "param", True) 47 | if not isinstance(strict, bool): 48 | raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") 49 | 50 | async with AsyncArcade(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: 51 | yield client 52 | -------------------------------------------------------------------------------- /tests/sample_file.txt: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | -------------------------------------------------------------------------------- /tests/test_deepcopy.py: -------------------------------------------------------------------------------- 1 | from arcadepy._utils import deepcopy_minimal 2 | 3 | 4 | def assert_different_identities(obj1: object, obj2: object) -> None: 5 | assert obj1 == obj2 6 | assert id(obj1) != id(obj2) 7 | 8 | 9 | def test_simple_dict() -> None: 10 | obj1 = {"foo": "bar"} 11 | obj2 = deepcopy_minimal(obj1) 12 | assert_different_identities(obj1, obj2) 13 | 14 | 15 | def test_nested_dict() -> None: 16 | obj1 = {"foo": {"bar": True}} 17 | obj2 = deepcopy_minimal(obj1) 18 | assert_different_identities(obj1, obj2) 19 | assert_different_identities(obj1["foo"], obj2["foo"]) 20 | 21 | 22 | def test_complex_nested_dict() -> None: 23 | obj1 = {"foo": {"bar": [{"hello": "world"}]}} 24 | obj2 = deepcopy_minimal(obj1) 25 | assert_different_identities(obj1, obj2) 26 | assert_different_identities(obj1["foo"], obj2["foo"]) 27 | assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) 28 | assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) 29 | 30 | 31 | def test_simple_list() -> None: 32 | obj1 = ["a", "b", "c"] 33 | obj2 = deepcopy_minimal(obj1) 34 | assert_different_identities(obj1, obj2) 35 | 36 | 37 | def test_nested_list() -> None: 38 | obj1 = ["a", [1, 2, 3]] 39 | obj2 = deepcopy_minimal(obj1) 40 | assert_different_identities(obj1, obj2) 41 | assert_different_identities(obj1[1], obj2[1]) 42 | 43 | 44 | class MyObject: ... 45 | 46 | 47 | def test_ignores_other_types() -> None: 48 | # custom classes 49 | my_obj = MyObject() 50 | obj1 = {"foo": my_obj} 51 | obj2 = deepcopy_minimal(obj1) 52 | assert_different_identities(obj1, obj2) 53 | assert obj1["foo"] is my_obj 54 | 55 | # tuples 56 | obj3 = ("a", "b") 57 | obj4 = deepcopy_minimal(obj3) 58 | assert obj3 is obj4 59 | -------------------------------------------------------------------------------- /tests/test_extract_files.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Sequence 4 | 5 | import pytest 6 | 7 | from arcadepy._types import FileTypes 8 | from arcadepy._utils import extract_files 9 | 10 | 11 | def test_removes_files_from_input() -> None: 12 | query = {"foo": "bar"} 13 | assert extract_files(query, paths=[]) == [] 14 | assert query == {"foo": "bar"} 15 | 16 | query2 = {"foo": b"Bar", "hello": "world"} 17 | assert extract_files(query2, paths=[["foo"]]) == [("foo", b"Bar")] 18 | assert query2 == {"hello": "world"} 19 | 20 | query3 = {"foo": {"foo": {"bar": b"Bar"}}, "hello": "world"} 21 | assert extract_files(query3, paths=[["foo", "foo", "bar"]]) == [("foo[foo][bar]", b"Bar")] 22 | assert query3 == {"foo": {"foo": {}}, "hello": "world"} 23 | 24 | query4 = {"foo": {"bar": b"Bar", "baz": "foo"}, "hello": "world"} 25 | assert extract_files(query4, paths=[["foo", "bar"]]) == [("foo[bar]", b"Bar")] 26 | assert query4 == {"hello": "world", "foo": {"baz": "foo"}} 27 | 28 | 29 | def test_multiple_files() -> None: 30 | query = {"documents": [{"file": b"My first file"}, {"file": b"My second file"}]} 31 | assert extract_files(query, paths=[["documents", "", "file"]]) == [ 32 | ("documents[][file]", b"My first file"), 33 | ("documents[][file]", b"My second file"), 34 | ] 35 | assert query == {"documents": [{}, {}]} 36 | 37 | 38 | @pytest.mark.parametrize( 39 | "query,paths,expected", 40 | [ 41 | [ 42 | {"foo": {"bar": "baz"}}, 43 | [["foo", "", "bar"]], 44 | [], 45 | ], 46 | [ 47 | {"foo": ["bar", "baz"]}, 48 | [["foo", "bar"]], 49 | [], 50 | ], 51 | [ 52 | {"foo": {"bar": "baz"}}, 53 | [["foo", "foo"]], 54 | [], 55 | ], 56 | ], 57 | ids=["dict expecting array", "array expecting dict", "unknown keys"], 58 | ) 59 | def test_ignores_incorrect_paths( 60 | query: dict[str, object], 61 | paths: Sequence[Sequence[str]], 62 | expected: list[tuple[str, FileTypes]], 63 | ) -> None: 64 | assert extract_files(query, paths=paths) == expected 65 | -------------------------------------------------------------------------------- /tests/test_files.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import anyio 4 | import pytest 5 | from dirty_equals import IsDict, IsList, IsBytes, IsTuple 6 | 7 | from arcadepy._files import to_httpx_files, async_to_httpx_files 8 | 9 | readme_path = Path(__file__).parent.parent.joinpath("README.md") 10 | 11 | 12 | def test_pathlib_includes_file_name() -> None: 13 | result = to_httpx_files({"file": readme_path}) 14 | print(result) 15 | assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) 16 | 17 | 18 | def test_tuple_input() -> None: 19 | result = to_httpx_files([("file", readme_path)]) 20 | print(result) 21 | assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) 22 | 23 | 24 | @pytest.mark.asyncio 25 | async def test_async_pathlib_includes_file_name() -> None: 26 | result = await async_to_httpx_files({"file": readme_path}) 27 | print(result) 28 | assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) 29 | 30 | 31 | @pytest.mark.asyncio 32 | async def test_async_supports_anyio_path() -> None: 33 | result = await async_to_httpx_files({"file": anyio.Path(readme_path)}) 34 | print(result) 35 | assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) 36 | 37 | 38 | @pytest.mark.asyncio 39 | async def test_async_tuple_input() -> None: 40 | result = await async_to_httpx_files([("file", readme_path)]) 41 | print(result) 42 | assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) 43 | 44 | 45 | def test_string_not_allowed() -> None: 46 | with pytest.raises(TypeError, match="Expected file types input to be a FileContent type or to be a tuple"): 47 | to_httpx_files( 48 | { 49 | "file": "foo", # type: ignore 50 | } 51 | ) 52 | -------------------------------------------------------------------------------- /tests/test_qs.py: -------------------------------------------------------------------------------- 1 | from typing import Any, cast 2 | from functools import partial 3 | from urllib.parse import unquote 4 | 5 | import pytest 6 | 7 | from arcadepy._qs import Querystring, stringify 8 | 9 | 10 | def test_empty() -> None: 11 | assert stringify({}) == "" 12 | assert stringify({"a": {}}) == "" 13 | assert stringify({"a": {"b": {"c": {}}}}) == "" 14 | 15 | 16 | def test_basic() -> None: 17 | assert stringify({"a": 1}) == "a=1" 18 | assert stringify({"a": "b"}) == "a=b" 19 | assert stringify({"a": True}) == "a=true" 20 | assert stringify({"a": False}) == "a=false" 21 | assert stringify({"a": 1.23456}) == "a=1.23456" 22 | assert stringify({"a": None}) == "" 23 | 24 | 25 | @pytest.mark.parametrize("method", ["class", "function"]) 26 | def test_nested_dotted(method: str) -> None: 27 | if method == "class": 28 | serialise = Querystring(nested_format="dots").stringify 29 | else: 30 | serialise = partial(stringify, nested_format="dots") 31 | 32 | assert unquote(serialise({"a": {"b": "c"}})) == "a.b=c" 33 | assert unquote(serialise({"a": {"b": "c", "d": "e", "f": "g"}})) == "a.b=c&a.d=e&a.f=g" 34 | assert unquote(serialise({"a": {"b": {"c": {"d": "e"}}}})) == "a.b.c.d=e" 35 | assert unquote(serialise({"a": {"b": True}})) == "a.b=true" 36 | 37 | 38 | def test_nested_brackets() -> None: 39 | assert unquote(stringify({"a": {"b": "c"}})) == "a[b]=c" 40 | assert unquote(stringify({"a": {"b": "c", "d": "e", "f": "g"}})) == "a[b]=c&a[d]=e&a[f]=g" 41 | assert unquote(stringify({"a": {"b": {"c": {"d": "e"}}}})) == "a[b][c][d]=e" 42 | assert unquote(stringify({"a": {"b": True}})) == "a[b]=true" 43 | 44 | 45 | @pytest.mark.parametrize("method", ["class", "function"]) 46 | def test_array_comma(method: str) -> None: 47 | if method == "class": 48 | serialise = Querystring(array_format="comma").stringify 49 | else: 50 | serialise = partial(stringify, array_format="comma") 51 | 52 | assert unquote(serialise({"in": ["foo", "bar"]})) == "in=foo,bar" 53 | assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b]=true,false" 54 | assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b]=true,false,true" 55 | 56 | 57 | def test_array_repeat() -> None: 58 | assert unquote(stringify({"in": ["foo", "bar"]})) == "in=foo&in=bar" 59 | assert unquote(stringify({"a": {"b": [True, False]}})) == "a[b]=true&a[b]=false" 60 | assert unquote(stringify({"a": {"b": [True, False, None, True]}})) == "a[b]=true&a[b]=false&a[b]=true" 61 | assert unquote(stringify({"in": ["foo", {"b": {"c": ["d", "e"]}}]})) == "in=foo&in[b][c]=d&in[b][c]=e" 62 | 63 | 64 | @pytest.mark.parametrize("method", ["class", "function"]) 65 | def test_array_brackets(method: str) -> None: 66 | if method == "class": 67 | serialise = Querystring(array_format="brackets").stringify 68 | else: 69 | serialise = partial(stringify, array_format="brackets") 70 | 71 | assert unquote(serialise({"in": ["foo", "bar"]})) == "in[]=foo&in[]=bar" 72 | assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b][]=true&a[b][]=false" 73 | assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b][]=true&a[b][]=false&a[b][]=true" 74 | 75 | 76 | def test_unknown_array_format() -> None: 77 | with pytest.raises(NotImplementedError, match="Unknown array_format value: foo, choose from comma, repeat"): 78 | stringify({"a": ["foo", "bar"]}, array_format=cast(Any, "foo")) 79 | -------------------------------------------------------------------------------- /tests/test_required_args.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from arcadepy._utils import required_args 6 | 7 | 8 | def test_too_many_positional_params() -> None: 9 | @required_args(["a"]) 10 | def foo(a: str | None = None) -> str | None: 11 | return a 12 | 13 | with pytest.raises(TypeError, match=r"foo\(\) takes 1 argument\(s\) but 2 were given"): 14 | foo("a", "b") # type: ignore 15 | 16 | 17 | def test_positional_param() -> None: 18 | @required_args(["a"]) 19 | def foo(a: str | None = None) -> str | None: 20 | return a 21 | 22 | assert foo("a") == "a" 23 | assert foo(None) is None 24 | assert foo(a="b") == "b" 25 | 26 | with pytest.raises(TypeError, match="Missing required argument: 'a'"): 27 | foo() 28 | 29 | 30 | def test_keyword_only_param() -> None: 31 | @required_args(["a"]) 32 | def foo(*, a: str | None = None) -> str | None: 33 | return a 34 | 35 | assert foo(a="a") == "a" 36 | assert foo(a=None) is None 37 | assert foo(a="b") == "b" 38 | 39 | with pytest.raises(TypeError, match="Missing required argument: 'a'"): 40 | foo() 41 | 42 | 43 | def test_multiple_params() -> None: 44 | @required_args(["a", "b", "c"]) 45 | def foo(a: str = "", *, b: str = "", c: str = "") -> str | None: 46 | return f"{a} {b} {c}" 47 | 48 | assert foo(a="a", b="b", c="c") == "a b c" 49 | 50 | error_message = r"Missing required arguments.*" 51 | 52 | with pytest.raises(TypeError, match=error_message): 53 | foo() 54 | 55 | with pytest.raises(TypeError, match=error_message): 56 | foo(a="a") 57 | 58 | with pytest.raises(TypeError, match=error_message): 59 | foo(b="b") 60 | 61 | with pytest.raises(TypeError, match=error_message): 62 | foo(c="c") 63 | 64 | with pytest.raises(TypeError, match=r"Missing required argument: 'a'"): 65 | foo(b="a", c="c") 66 | 67 | with pytest.raises(TypeError, match=r"Missing required argument: 'b'"): 68 | foo("a", c="c") 69 | 70 | 71 | def test_multiple_variants() -> None: 72 | @required_args(["a"], ["b"]) 73 | def foo(*, a: str | None = None, b: str | None = None) -> str | None: 74 | return a if a is not None else b 75 | 76 | assert foo(a="foo") == "foo" 77 | assert foo(b="bar") == "bar" 78 | assert foo(a=None) is None 79 | assert foo(b=None) is None 80 | 81 | # TODO: this error message could probably be improved 82 | with pytest.raises( 83 | TypeError, 84 | match=r"Missing required arguments; Expected either \('a'\) or \('b'\) arguments to be given", 85 | ): 86 | foo() 87 | 88 | 89 | def test_multiple_params_multiple_variants() -> None: 90 | @required_args(["a", "b"], ["c"]) 91 | def foo(*, a: str | None = None, b: str | None = None, c: str | None = None) -> str | None: 92 | if a is not None: 93 | return a 94 | if b is not None: 95 | return b 96 | return c 97 | 98 | error_message = r"Missing required arguments; Expected either \('a' and 'b'\) or \('c'\) arguments to be given" 99 | 100 | with pytest.raises(TypeError, match=error_message): 101 | foo(a="foo") 102 | 103 | with pytest.raises(TypeError, match=error_message): 104 | foo(b="bar") 105 | 106 | with pytest.raises(TypeError, match=error_message): 107 | foo() 108 | 109 | assert foo(a=None, b="bar") == "bar" 110 | assert foo(c=None) is None 111 | assert foo(c="foo") == "foo" 112 | -------------------------------------------------------------------------------- /tests/test_streaming.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Iterator, AsyncIterator 4 | 5 | import httpx 6 | import pytest 7 | 8 | from arcadepy import Arcade, AsyncArcade 9 | from arcadepy._streaming import Stream, AsyncStream, ServerSentEvent 10 | 11 | 12 | @pytest.mark.asyncio 13 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 14 | async def test_basic(sync: bool, client: Arcade, async_client: AsyncArcade) -> None: 15 | def body() -> Iterator[bytes]: 16 | yield b"event: completion\n" 17 | yield b'data: {"foo":true}\n' 18 | yield b"\n" 19 | 20 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 21 | 22 | sse = await iter_next(iterator) 23 | assert sse.event == "completion" 24 | assert sse.json() == {"foo": True} 25 | 26 | await assert_empty_iter(iterator) 27 | 28 | 29 | @pytest.mark.asyncio 30 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 31 | async def test_data_missing_event(sync: bool, client: Arcade, async_client: AsyncArcade) -> None: 32 | def body() -> Iterator[bytes]: 33 | yield b'data: {"foo":true}\n' 34 | yield b"\n" 35 | 36 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 37 | 38 | sse = await iter_next(iterator) 39 | assert sse.event is None 40 | assert sse.json() == {"foo": True} 41 | 42 | await assert_empty_iter(iterator) 43 | 44 | 45 | @pytest.mark.asyncio 46 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 47 | async def test_event_missing_data(sync: bool, client: Arcade, async_client: AsyncArcade) -> None: 48 | def body() -> Iterator[bytes]: 49 | yield b"event: ping\n" 50 | yield b"\n" 51 | 52 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 53 | 54 | sse = await iter_next(iterator) 55 | assert sse.event == "ping" 56 | assert sse.data == "" 57 | 58 | await assert_empty_iter(iterator) 59 | 60 | 61 | @pytest.mark.asyncio 62 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 63 | async def test_multiple_events(sync: bool, client: Arcade, async_client: AsyncArcade) -> None: 64 | def body() -> Iterator[bytes]: 65 | yield b"event: ping\n" 66 | yield b"\n" 67 | yield b"event: completion\n" 68 | yield b"\n" 69 | 70 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 71 | 72 | sse = await iter_next(iterator) 73 | assert sse.event == "ping" 74 | assert sse.data == "" 75 | 76 | sse = await iter_next(iterator) 77 | assert sse.event == "completion" 78 | assert sse.data == "" 79 | 80 | await assert_empty_iter(iterator) 81 | 82 | 83 | @pytest.mark.asyncio 84 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 85 | async def test_multiple_events_with_data(sync: bool, client: Arcade, async_client: AsyncArcade) -> None: 86 | def body() -> Iterator[bytes]: 87 | yield b"event: ping\n" 88 | yield b'data: {"foo":true}\n' 89 | yield b"\n" 90 | yield b"event: completion\n" 91 | yield b'data: {"bar":false}\n' 92 | yield b"\n" 93 | 94 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 95 | 96 | sse = await iter_next(iterator) 97 | assert sse.event == "ping" 98 | assert sse.json() == {"foo": True} 99 | 100 | sse = await iter_next(iterator) 101 | assert sse.event == "completion" 102 | assert sse.json() == {"bar": False} 103 | 104 | await assert_empty_iter(iterator) 105 | 106 | 107 | @pytest.mark.asyncio 108 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 109 | async def test_multiple_data_lines_with_empty_line(sync: bool, client: Arcade, async_client: AsyncArcade) -> None: 110 | def body() -> Iterator[bytes]: 111 | yield b"event: ping\n" 112 | yield b"data: {\n" 113 | yield b'data: "foo":\n' 114 | yield b"data: \n" 115 | yield b"data:\n" 116 | yield b"data: true}\n" 117 | yield b"\n\n" 118 | 119 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 120 | 121 | sse = await iter_next(iterator) 122 | assert sse.event == "ping" 123 | assert sse.json() == {"foo": True} 124 | assert sse.data == '{\n"foo":\n\n\ntrue}' 125 | 126 | await assert_empty_iter(iterator) 127 | 128 | 129 | @pytest.mark.asyncio 130 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 131 | async def test_data_json_escaped_double_new_line(sync: bool, client: Arcade, async_client: AsyncArcade) -> None: 132 | def body() -> Iterator[bytes]: 133 | yield b"event: ping\n" 134 | yield b'data: {"foo": "my long\\n\\ncontent"}' 135 | yield b"\n\n" 136 | 137 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 138 | 139 | sse = await iter_next(iterator) 140 | assert sse.event == "ping" 141 | assert sse.json() == {"foo": "my long\n\ncontent"} 142 | 143 | await assert_empty_iter(iterator) 144 | 145 | 146 | @pytest.mark.asyncio 147 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 148 | async def test_multiple_data_lines(sync: bool, client: Arcade, async_client: AsyncArcade) -> None: 149 | def body() -> Iterator[bytes]: 150 | yield b"event: ping\n" 151 | yield b"data: {\n" 152 | yield b'data: "foo":\n' 153 | yield b"data: true}\n" 154 | yield b"\n\n" 155 | 156 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 157 | 158 | sse = await iter_next(iterator) 159 | assert sse.event == "ping" 160 | assert sse.json() == {"foo": True} 161 | 162 | await assert_empty_iter(iterator) 163 | 164 | 165 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 166 | async def test_special_new_line_character( 167 | sync: bool, 168 | client: Arcade, 169 | async_client: AsyncArcade, 170 | ) -> None: 171 | def body() -> Iterator[bytes]: 172 | yield b'data: {"content":" culpa"}\n' 173 | yield b"\n" 174 | yield b'data: {"content":" \xe2\x80\xa8"}\n' 175 | yield b"\n" 176 | yield b'data: {"content":"foo"}\n' 177 | yield b"\n" 178 | 179 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 180 | 181 | sse = await iter_next(iterator) 182 | assert sse.event is None 183 | assert sse.json() == {"content": " culpa"} 184 | 185 | sse = await iter_next(iterator) 186 | assert sse.event is None 187 | assert sse.json() == {"content": " 
"} 188 | 189 | sse = await iter_next(iterator) 190 | assert sse.event is None 191 | assert sse.json() == {"content": "foo"} 192 | 193 | await assert_empty_iter(iterator) 194 | 195 | 196 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 197 | async def test_multi_byte_character_multiple_chunks( 198 | sync: bool, 199 | client: Arcade, 200 | async_client: AsyncArcade, 201 | ) -> None: 202 | def body() -> Iterator[bytes]: 203 | yield b'data: {"content":"' 204 | # bytes taken from the string 'известни' and arbitrarily split 205 | # so that some multi-byte characters span multiple chunks 206 | yield b"\xd0" 207 | yield b"\xb8\xd0\xb7\xd0" 208 | yield b"\xb2\xd0\xb5\xd1\x81\xd1\x82\xd0\xbd\xd0\xb8" 209 | yield b'"}\n' 210 | yield b"\n" 211 | 212 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 213 | 214 | sse = await iter_next(iterator) 215 | assert sse.event is None 216 | assert sse.json() == {"content": "известни"} 217 | 218 | 219 | async def to_aiter(iter: Iterator[bytes]) -> AsyncIterator[bytes]: 220 | for chunk in iter: 221 | yield chunk 222 | 223 | 224 | async def iter_next(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> ServerSentEvent: 225 | if isinstance(iter, AsyncIterator): 226 | return await iter.__anext__() 227 | 228 | return next(iter) 229 | 230 | 231 | async def assert_empty_iter(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> None: 232 | with pytest.raises((StopAsyncIteration, RuntimeError)): 233 | await iter_next(iter) 234 | 235 | 236 | def make_event_iterator( 237 | content: Iterator[bytes], 238 | *, 239 | sync: bool, 240 | client: Arcade, 241 | async_client: AsyncArcade, 242 | ) -> Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]: 243 | if sync: 244 | return Stream(cast_to=object, client=client, response=httpx.Response(200, content=content))._iter_events() 245 | 246 | return AsyncStream( 247 | cast_to=object, client=async_client, response=httpx.Response(200, content=to_aiter(content)) 248 | )._iter_events() 249 | -------------------------------------------------------------------------------- /tests/test_utils/test_proxy.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from typing import Any 3 | from typing_extensions import override 4 | 5 | from arcadepy._utils import LazyProxy 6 | 7 | 8 | class RecursiveLazyProxy(LazyProxy[Any]): 9 | @override 10 | def __load__(self) -> Any: 11 | return self 12 | 13 | def __call__(self, *_args: Any, **_kwds: Any) -> Any: 14 | raise RuntimeError("This should never be called!") 15 | 16 | 17 | def test_recursive_proxy() -> None: 18 | proxy = RecursiveLazyProxy() 19 | assert repr(proxy) == "RecursiveLazyProxy" 20 | assert str(proxy) == "RecursiveLazyProxy" 21 | assert dir(proxy) == [] 22 | assert type(proxy).__name__ == "RecursiveLazyProxy" 23 | assert type(operator.attrgetter("name.foo.bar.baz")(proxy)).__name__ == "RecursiveLazyProxy" 24 | 25 | 26 | def test_isinstance_does_not_error() -> None: 27 | class AlwaysErrorProxy(LazyProxy[Any]): 28 | @override 29 | def __load__(self) -> Any: 30 | raise RuntimeError("Mocking missing dependency") 31 | 32 | proxy = AlwaysErrorProxy() 33 | assert not isinstance(proxy, dict) 34 | assert isinstance(proxy, LazyProxy) 35 | -------------------------------------------------------------------------------- /tests/test_utils/test_typing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Generic, TypeVar, cast 4 | 5 | from arcadepy._utils import extract_type_var_from_base 6 | 7 | _T = TypeVar("_T") 8 | _T2 = TypeVar("_T2") 9 | _T3 = TypeVar("_T3") 10 | 11 | 12 | class BaseGeneric(Generic[_T]): ... 13 | 14 | 15 | class SubclassGeneric(BaseGeneric[_T]): ... 16 | 17 | 18 | class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]): ... 19 | 20 | 21 | class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]): ... 22 | 23 | 24 | class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]): ... 25 | 26 | 27 | def test_extract_type_var() -> None: 28 | assert ( 29 | extract_type_var_from_base( 30 | BaseGeneric[int], 31 | index=0, 32 | generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), 33 | ) 34 | == int 35 | ) 36 | 37 | 38 | def test_extract_type_var_generic_subclass() -> None: 39 | assert ( 40 | extract_type_var_from_base( 41 | SubclassGeneric[int], 42 | index=0, 43 | generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), 44 | ) 45 | == int 46 | ) 47 | 48 | 49 | def test_extract_type_var_multiple() -> None: 50 | typ = BaseGenericMultipleTypeArgs[int, str, None] 51 | 52 | generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) 53 | assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int 54 | assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str 55 | assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) 56 | 57 | 58 | def test_extract_type_var_generic_subclass_multiple() -> None: 59 | typ = SubclassGenericMultipleTypeArgs[int, str, None] 60 | 61 | generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) 62 | assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int 63 | assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str 64 | assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) 65 | 66 | 67 | def test_extract_type_var_generic_subclass_different_ordering_multiple() -> None: 68 | typ = SubclassDifferentOrderGenericMultipleTypeArgs[int, str, None] 69 | 70 | generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) 71 | assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int 72 | assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str 73 | assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) 74 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import inspect 5 | import traceback 6 | import contextlib 7 | from typing import Any, TypeVar, Iterator, cast 8 | from datetime import date, datetime 9 | from typing_extensions import Literal, get_args, get_origin, assert_type 10 | 11 | from arcadepy._types import Omit, NoneType 12 | from arcadepy._utils import ( 13 | is_dict, 14 | is_list, 15 | is_list_type, 16 | is_union_type, 17 | extract_type_arg, 18 | is_annotated_type, 19 | is_type_alias_type, 20 | ) 21 | from arcadepy._compat import PYDANTIC_V2, field_outer_type, get_model_fields 22 | from arcadepy._models import BaseModel 23 | 24 | BaseModelT = TypeVar("BaseModelT", bound=BaseModel) 25 | 26 | 27 | def assert_matches_model(model: type[BaseModelT], value: BaseModelT, *, path: list[str]) -> bool: 28 | for name, field in get_model_fields(model).items(): 29 | field_value = getattr(value, name) 30 | if PYDANTIC_V2: 31 | allow_none = False 32 | else: 33 | # in v1 nullability was structured differently 34 | # https://docs.pydantic.dev/2.0/migration/#required-optional-and-nullable-fields 35 | allow_none = getattr(field, "allow_none", False) 36 | 37 | assert_matches_type( 38 | field_outer_type(field), 39 | field_value, 40 | path=[*path, name], 41 | allow_none=allow_none, 42 | ) 43 | 44 | return True 45 | 46 | 47 | # Note: the `path` argument is only used to improve error messages when `--showlocals` is used 48 | def assert_matches_type( 49 | type_: Any, 50 | value: object, 51 | *, 52 | path: list[str], 53 | allow_none: bool = False, 54 | ) -> None: 55 | if is_type_alias_type(type_): 56 | type_ = type_.__value__ 57 | 58 | # unwrap `Annotated[T, ...]` -> `T` 59 | if is_annotated_type(type_): 60 | type_ = extract_type_arg(type_, 0) 61 | 62 | if allow_none and value is None: 63 | return 64 | 65 | if type_ is None or type_ is NoneType: 66 | assert value is None 67 | return 68 | 69 | origin = get_origin(type_) or type_ 70 | 71 | if is_list_type(type_): 72 | return _assert_list_type(type_, value) 73 | 74 | if origin == str: 75 | assert isinstance(value, str) 76 | elif origin == int: 77 | assert isinstance(value, int) 78 | elif origin == bool: 79 | assert isinstance(value, bool) 80 | elif origin == float: 81 | assert isinstance(value, float) 82 | elif origin == bytes: 83 | assert isinstance(value, bytes) 84 | elif origin == datetime: 85 | assert isinstance(value, datetime) 86 | elif origin == date: 87 | assert isinstance(value, date) 88 | elif origin == object: 89 | # nothing to do here, the expected type is unknown 90 | pass 91 | elif origin == Literal: 92 | assert value in get_args(type_) 93 | elif origin == dict: 94 | assert is_dict(value) 95 | 96 | args = get_args(type_) 97 | key_type = args[0] 98 | items_type = args[1] 99 | 100 | for key, item in value.items(): 101 | assert_matches_type(key_type, key, path=[*path, ""]) 102 | assert_matches_type(items_type, item, path=[*path, ""]) 103 | elif is_union_type(type_): 104 | variants = get_args(type_) 105 | 106 | try: 107 | none_index = variants.index(type(None)) 108 | except ValueError: 109 | pass 110 | else: 111 | # special case Optional[T] for better error messages 112 | if len(variants) == 2: 113 | if value is None: 114 | # valid 115 | return 116 | 117 | return assert_matches_type(type_=variants[not none_index], value=value, path=path) 118 | 119 | for i, variant in enumerate(variants): 120 | try: 121 | assert_matches_type(variant, value, path=[*path, f"variant {i}"]) 122 | return 123 | except AssertionError: 124 | traceback.print_exc() 125 | continue 126 | 127 | raise AssertionError("Did not match any variants") 128 | elif issubclass(origin, BaseModel): 129 | assert isinstance(value, type_) 130 | assert assert_matches_model(type_, cast(Any, value), path=path) 131 | elif inspect.isclass(origin) and origin.__name__ == "HttpxBinaryResponseContent": 132 | assert value.__class__.__name__ == "HttpxBinaryResponseContent" 133 | else: 134 | assert None, f"Unhandled field type: {type_}" 135 | 136 | 137 | def _assert_list_type(type_: type[object], value: object) -> None: 138 | assert is_list(value) 139 | 140 | inner_type = get_args(type_)[0] 141 | for entry in value: 142 | assert_type(inner_type, entry) # type: ignore 143 | 144 | 145 | @contextlib.contextmanager 146 | def update_env(**new_env: str | Omit) -> Iterator[None]: 147 | old = os.environ.copy() 148 | 149 | try: 150 | for name, value in new_env.items(): 151 | if isinstance(value, Omit): 152 | os.environ.pop(name, None) 153 | else: 154 | os.environ[name] = value 155 | 156 | yield None 157 | finally: 158 | os.environ.clear() 159 | os.environ.update(old) 160 | --------------------------------------------------------------------------------