├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ ├── ci.yml │ ├── create-releases.yml │ ├── handle-release-pr-title-edit.yml │ ├── publish-pypi.yml │ └── release-doctor.yml ├── .gitignore ├── .python-version ├── .release-please-manifest.json ├── .stats.yml ├── Brewfile ├── 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 ├── src └── unitycatalog │ ├── __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 │ ├── _streams.py │ ├── _sync.py │ ├── _transform.py │ ├── _typing.py │ └── _utils.py │ ├── _version.py │ ├── lib │ └── .keep │ ├── py.typed │ ├── resources │ ├── __init__.py │ ├── catalogs.py │ ├── functions.py │ ├── schemas.py │ ├── tables.py │ ├── temporary_table_credentials.py │ ├── temporary_volume_credentials.py │ └── volumes.py │ └── types │ ├── __init__.py │ ├── catalog_create_params.py │ ├── catalog_delete_params.py │ ├── catalog_info.py │ ├── catalog_list_params.py │ ├── catalog_list_response.py │ ├── catalog_update_params.py │ ├── function_create_params.py │ ├── function_info.py │ ├── function_list_params.py │ ├── function_list_response.py │ ├── generate_temporary_table_credential_response.py │ ├── generate_temporary_volume_credential_response.py │ ├── schema_create_params.py │ ├── schema_info.py │ ├── schema_list_params.py │ ├── schema_list_response.py │ ├── schema_update_params.py │ ├── table_create_params.py │ ├── table_info.py │ ├── table_list_params.py │ ├── table_list_response.py │ ├── temporary_table_credential_create_params.py │ ├── temporary_volume_credential_create_params.py │ ├── volume_create_params.py │ ├── volume_info.py │ ├── volume_list_params.py │ ├── volume_list_response.py │ └── volume_update_params.py └── tests ├── __init__.py ├── api_resources ├── __init__.py ├── test_catalogs.py ├── test_functions.py ├── test_schemas.py ├── test_tables.py ├── test_temporary_table_credentials.py ├── test_temporary_volume_credentials.py └── test_volumes.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.24.0" RYE_INSTALL_OPTION="--yes" bash 7 | ENV PATH=/home/vscode/.rye/shims:$PATH 8 | 9 | RUN echo "[[ -d .venv ]] && source .venv/bin/activate" >> /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 | 29 | // Features to add to the dev container. More info: https://containers.dev/features. 30 | // "features": {}, 31 | 32 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 33 | // "forwardPorts": [], 34 | 35 | // Configure tool-specific properties. 36 | // "customizations": {}, 37 | 38 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 39 | // "remoteUser": "root" 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | lint: 12 | name: lint 13 | runs-on: ubuntu-latest 14 | 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.24.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 | test: 33 | name: test 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | 39 | - name: Install Rye 40 | run: | 41 | curl -sSf https://rye.astral.sh/get | bash 42 | echo "$HOME/.rye/shims" >> $GITHUB_PATH 43 | env: 44 | RYE_VERSION: 0.24.0 45 | RYE_INSTALL_OPTION: '--yes' 46 | 47 | - name: Bootstrap 48 | run: ./scripts/bootstrap 49 | 50 | - name: Run tests 51 | run: ./scripts/test 52 | 53 | -------------------------------------------------------------------------------- /.github/workflows/create-releases.yml: -------------------------------------------------------------------------------- 1 | name: Create releases 2 | on: 3 | schedule: 4 | - cron: '0 5 * * *' # every day at 5am UTC 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | release: 11 | name: release 12 | if: github.ref == 'refs/heads/main' && github.repository == 'undefined/unitycatalog-python' 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - uses: stainless-api/trigger-release-please@v1 19 | id: release 20 | with: 21 | repo: ${{ github.event.repository.full_name }} 22 | stainless-api-key: ${{ secrets.STAINLESS_API_KEY }} 23 | 24 | - name: Install Rye 25 | if: ${{ steps.release.outputs.releases_created }} 26 | run: | 27 | curl -sSf https://rye.astral.sh/get | bash 28 | echo "$HOME/.rye/shims" >> $GITHUB_PATH 29 | env: 30 | RYE_VERSION: 0.24.0 31 | RYE_INSTALL_OPTION: "--yes" 32 | 33 | - name: Publish to PyPI 34 | if: ${{ steps.release.outputs.releases_created }} 35 | run: | 36 | bash ./bin/publish-pypi 37 | env: 38 | PYPI_TOKEN: ${{ secrets.UNITYCATALOG_PYPI_TOKEN || secrets.PYPI_TOKEN }} 39 | -------------------------------------------------------------------------------- /.github/workflows/handle-release-pr-title-edit.yml: -------------------------------------------------------------------------------- 1 | name: Handle release PR title edits 2 | on: 3 | pull_request: 4 | types: 5 | - edited 6 | - unlabeled 7 | 8 | jobs: 9 | update_pr_content: 10 | name: Update pull request content 11 | if: | 12 | ((github.event.action == 'edited' && github.event.changes.title.from != github.event.pull_request.title) || 13 | (github.event.action == 'unlabeled' && github.event.label.name == 'autorelease: custom version')) && 14 | startsWith(github.event.pull_request.head.ref, 'release-please--') && 15 | github.event.pull_request.state == 'open' && 16 | github.event.sender.login != 'stainless-bot' && 17 | github.event.sender.login != 'stainless-app' && 18 | github.repository == 'undefined/unitycatalog-python' 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: stainless-api/trigger-release-please@v1 23 | with: 24 | repo: ${{ github.event.repository.full_name }} 25 | stainless-api-key: ${{ secrets.STAINLESS_API_KEY }} 26 | -------------------------------------------------------------------------------- /.github/workflows/publish-pypi.yml: -------------------------------------------------------------------------------- 1 | # workflow for re-running publishing to PyPI in case it fails for some reason 2 | # you can run this workflow by navigating to https://www.github.com/undefined/unitycatalog-python/actions/workflows/publish-pypi.yml 3 | name: Publish PyPI 4 | on: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | publish: 9 | name: publish 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Install Rye 16 | run: | 17 | curl -sSf https://rye.astral.sh/get | bash 18 | echo "$HOME/.rye/shims" >> $GITHUB_PATH 19 | env: 20 | RYE_VERSION: 0.24.0 21 | RYE_INSTALL_OPTION: "--yes" 22 | 23 | - name: Publish to PyPI 24 | run: | 25 | bash ./bin/publish-pypi 26 | env: 27 | PYPI_TOKEN: ${{ secrets.UNITYCATALOG_PYPI_TOKEN || secrets.PYPI_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/release-doctor.yml: -------------------------------------------------------------------------------- 1 | name: Release Doctor 2 | on: 3 | pull_request: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | release_doctor: 8 | name: release doctor 9 | runs-on: ubuntu-latest 10 | if: github.repository == 'undefined/unitycatalog-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Check release environment 16 | run: | 17 | bash ./bin/check-release-environment 18 | env: 19 | STAINLESS_API_KEY: ${{ secrets.STAINLESS_API_KEY }} 20 | PYPI_TOKEN: ${{ secrets.UNITYCATALOG_PYPI_TOKEN || secrets.PYPI_TOKEN }} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | _dev 3 | 4 | __pycache__ 5 | .mypy_cache 6 | 7 | dist 8 | 9 | .venv 10 | .idea 11 | 12 | .env 13 | .envrc 14 | codegen.log 15 | Brewfile.lock.json 16 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.9.18 2 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "0.0.1-alpha.0" 3 | } -------------------------------------------------------------------------------- /.stats.yml: -------------------------------------------------------------------------------- 1 | configured_endpoints: 25 2 | openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/eventual%2Funitycatalog-afb7536b3c70b699dd3090dc47209959c1591beae2945ee5fe14e1b53139fe83.yml 3 | -------------------------------------------------------------------------------- /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 so we highly recommend [installing it](https://rye.astral.sh/guide/installation/) as it will automatically provision a Python environment with the expected Python version. 6 | 7 | After installing Rye, you'll just have to run this command: 8 | 9 | ```sh 10 | $ rye sync --all-features 11 | ``` 12 | 13 | You can then run scripts using `rye run python script.py` or by activating the virtual environment: 14 | 15 | ```sh 16 | $ rye shell 17 | # or manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work 18 | $ source .venv/bin/activate 19 | 20 | # now you can omit the `rye run` prefix 21 | $ python script.py 22 | ``` 23 | 24 | ### Without Rye 25 | 26 | 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: 27 | 28 | ```sh 29 | $ pip install -r requirements-dev.lock 30 | ``` 31 | 32 | ## Modifying/Adding code 33 | 34 | Most of the SDK is generated code, and any modified code will be overridden on the next generation. The 35 | `src/unitycatalog/lib/` and `examples/` directories are exceptions and will never be overridden. 36 | 37 | ## Adding and running examples 38 | 39 | All files in the `examples/` directory are not modified by the Stainless generator and can be freely edited or 40 | added to. 41 | 42 | ```bash 43 | # add an example to examples/.py 44 | 45 | #!/usr/bin/env -S rye run python 46 | … 47 | ``` 48 | 49 | ``` 50 | chmod +x examples/.py 51 | # run the example against your api 52 | ./examples/.py 53 | ``` 54 | 55 | ## Using the repository from source 56 | 57 | If you’d like to use the repository from source, you can either install from git or link to a cloned repository: 58 | 59 | To install via git: 60 | 61 | ```bash 62 | pip install git+ssh://git@github.com/undefined/unitycatalog-python.git 63 | ``` 64 | 65 | Alternatively, you can build from source and install the wheel file: 66 | 67 | 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. 68 | 69 | To create a distributable version of the library, all you have to do is run this command: 70 | 71 | ```bash 72 | rye build 73 | # or 74 | python -m build 75 | ``` 76 | 77 | Then to install: 78 | 79 | ```sh 80 | pip install ./path-to-wheel-file.whl 81 | ``` 82 | 83 | ## Running tests 84 | 85 | Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. 86 | 87 | ```bash 88 | # you will need npm installed 89 | npx prism mock path/to/your/openapi.yml 90 | ``` 91 | 92 | ```bash 93 | rye run pytest 94 | ``` 95 | 96 | ## Linting and formatting 97 | 98 | This repository uses [ruff](https://github.com/astral-sh/ruff) and 99 | [black](https://github.com/psf/black) to format the code in the repository. 100 | 101 | To lint: 102 | 103 | ```bash 104 | rye run lint 105 | ``` 106 | 107 | To format and fix all ruff issues automatically: 108 | 109 | ```bash 110 | rye run format 111 | ``` 112 | 113 | ## Publishing and releases 114 | 115 | Changes made to this repository via the automated release PR pipeline should publish to PyPI automatically. If 116 | the changes aren't made through the automated pipeline, you may want to make releases manually. 117 | 118 | ### Publish with a GitHub workflow 119 | 120 | You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/undefined/unitycatalog-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. 121 | 122 | ### Publish manually 123 | 124 | If you need to manually release a package, you can run the `bin/publish-pypi` script with a `PYPI_TOKEN` set on 125 | the environment. 126 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting Security Issues 4 | 5 | This SDK is generated by [Stainless Software Inc](http://stainlessapi.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. 6 | 7 | To report a security issue, please contact the Stainless team at security@stainlessapi.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 Unitycatalog please follow the respective company's security reporting guidelines. 20 | 21 | ### Unitycatalog Terms and Policies 22 | 23 | Please contact dev-feedback@unitycatalog.com for any questions or concerns regarding 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 | # Catalogs 2 | 3 | Types: 4 | 5 | ```python 6 | from unitycatalog.types import CatalogInfo, CatalogListResponse, CatalogDeleteResponse 7 | ``` 8 | 9 | Methods: 10 | 11 | - client.catalogs.create(\*\*params) -> CatalogInfo 12 | - client.catalogs.retrieve(name) -> CatalogInfo 13 | - client.catalogs.update(name, \*\*params) -> CatalogInfo 14 | - client.catalogs.list(\*\*params) -> CatalogListResponse 15 | - client.catalogs.delete(name, \*\*params) -> object 16 | 17 | # Schemas 18 | 19 | Types: 20 | 21 | ```python 22 | from unitycatalog.types import SchemaInfo, SchemaListResponse, SchemaDeleteResponse 23 | ``` 24 | 25 | Methods: 26 | 27 | - client.schemas.create(\*\*params) -> SchemaInfo 28 | - client.schemas.retrieve(full_name) -> SchemaInfo 29 | - client.schemas.update(full_name, \*\*params) -> SchemaInfo 30 | - client.schemas.list(\*\*params) -> SchemaListResponse 31 | - client.schemas.delete(full_name) -> object 32 | 33 | # Tables 34 | 35 | Types: 36 | 37 | ```python 38 | from unitycatalog.types import TableInfo, TableListResponse, TableDeleteResponse 39 | ``` 40 | 41 | Methods: 42 | 43 | - client.tables.create(\*\*params) -> TableInfo 44 | - client.tables.retrieve(full_name) -> TableInfo 45 | - client.tables.list(\*\*params) -> TableListResponse 46 | - client.tables.delete(full_name) -> object 47 | 48 | # Volumes 49 | 50 | Types: 51 | 52 | ```python 53 | from unitycatalog.types import VolumeInfo, VolumeListResponse, VolumeDeleteResponse 54 | ``` 55 | 56 | Methods: 57 | 58 | - client.volumes.create(\*\*params) -> VolumeInfo 59 | - client.volumes.retrieve(name) -> VolumeInfo 60 | - client.volumes.update(name, \*\*params) -> VolumeInfo 61 | - client.volumes.list(\*\*params) -> VolumeListResponse 62 | - client.volumes.delete(name) -> object 63 | 64 | # TemporaryTableCredentials 65 | 66 | Types: 67 | 68 | ```python 69 | from unitycatalog.types import GenerateTemporaryTableCredentialResponse 70 | ``` 71 | 72 | Methods: 73 | 74 | - client.temporary_table_credentials.create(\*\*params) -> GenerateTemporaryTableCredentialResponse 75 | 76 | # TemporaryVolumeCredentials 77 | 78 | Types: 79 | 80 | ```python 81 | from unitycatalog.types import GenerateTemporaryVolumeCredentialResponse 82 | ``` 83 | 84 | Methods: 85 | 86 | - client.temporary_volume_credentials.create(\*\*params) -> GenerateTemporaryVolumeCredentialResponse 87 | 88 | # Functions 89 | 90 | Types: 91 | 92 | ```python 93 | from unitycatalog.types import FunctionInfo, FunctionListResponse, FunctionDeleteResponse 94 | ``` 95 | 96 | Methods: 97 | 98 | - client.functions.create(\*\*params) -> FunctionInfo 99 | - client.functions.retrieve(name) -> FunctionInfo 100 | - client.functions.list(\*\*params) -> FunctionListResponse 101 | - client.functions.delete(name) -> object 102 | -------------------------------------------------------------------------------- /bin/check-release-environment: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | warnings=() 4 | errors=() 5 | 6 | if [ -z "${STAINLESS_API_KEY}" ]; then 7 | errors+=("The STAINLESS_API_KEY secret has not been set. Please contact Stainless for an API key & set it in your organization secrets on GitHub.") 8 | fi 9 | 10 | if [ -z "${PYPI_TOKEN}" ]; then 11 | warnings+=("The UNITYCATALOG_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") 12 | fi 13 | 14 | lenWarnings=${#warnings[@]} 15 | 16 | if [[ lenWarnings -gt 0 ]]; then 17 | echo -e "Found the following warnings in the release environment:\n" 18 | 19 | for warning in "${warnings[@]}"; do 20 | echo -e "- $warning\n" 21 | done 22 | fi 23 | 24 | lenErrors=${#errors[@]} 25 | 26 | if [[ lenErrors -gt 0 ]]; then 27 | echo -e "Found the following errors in the release environment:\n" 28 | 29 | for error in "${errors[@]}"; do 30 | echo -e "- $error\n" 31 | done 32 | 33 | exit 1 34 | fi 35 | 36 | echo "The environment is ready to push releases!" 37 | -------------------------------------------------------------------------------- /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 | exclude = ^(src/unitycatalog/_files\.py|_dev/.*\.py)$ 9 | 10 | strict_equality = True 11 | implicit_reexport = True 12 | check_untyped_defs = True 13 | no_implicit_optional = True 14 | 15 | warn_return_any = True 16 | warn_unreachable = True 17 | warn_unused_configs = True 18 | 19 | # Turn these options off as it could cause conflicts 20 | # with the Pyright options. 21 | warn_unused_ignores = False 22 | warn_redundant_casts = False 23 | 24 | disallow_any_generics = True 25 | disallow_untyped_defs = True 26 | disallow_untyped_calls = True 27 | disallow_subclassing_any = True 28 | disallow_incomplete_defs = True 29 | disallow_untyped_decorators = True 30 | cache_fine_grained = True 31 | 32 | # By default, mypy reports an error if you assign a value to the result 33 | # of a function call that doesn't return anything. We do this in our test 34 | # cases: 35 | # ``` 36 | # result = ... 37 | # assert result is None 38 | # ``` 39 | # Changing this codegen to make mypy happy would increase complexity 40 | # and would not be worth it. 41 | disable_error_code = func-returns-value 42 | 43 | # https://github.com/python/mypy/issues/12162 44 | [mypy.overrides] 45 | module = "black.files.*" 46 | ignore_errors = true 47 | ignore_missing_imports = true 48 | -------------------------------------------------------------------------------- /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 = "unitycatalog" 3 | version = "0.1.1" 4 | description = "The official Python library for the unitycatalog API" 5 | dynamic = ["readme"] 6 | license = "Apache-2.0" 7 | authors = [ 8 | { name = "Unitycatalog", email = "dev-feedback@unitycatalog.com" }, 9 | ] 10 | dependencies = [ 11 | "httpx>=0.23.0, <1", 12 | "pydantic>=1.9.0, <3", 13 | "typing-extensions>=4.7, <5", 14 | "anyio>=3.5.0, <5", 15 | "distro>=1.7.0, <2", 16 | "sniffio", 17 | "cached-property; python_version < '3.8'", 18 | 19 | ] 20 | requires-python = ">= 3.7" 21 | classifiers = [ 22 | "Typing :: Typed", 23 | "Intended Audience :: Developers", 24 | "Programming Language :: Python :: 3.7", 25 | "Programming Language :: Python :: 3.8", 26 | "Programming Language :: Python :: 3.9", 27 | "Programming Language :: Python :: 3.10", 28 | "Programming Language :: Python :: 3.11", 29 | "Programming Language :: Python :: 3.12", 30 | "Operating System :: OS Independent", 31 | "Operating System :: POSIX", 32 | "Operating System :: MacOS", 33 | "Operating System :: POSIX :: Linux", 34 | "Operating System :: Microsoft :: Windows", 35 | "Topic :: Software Development :: Libraries :: Python Modules", 36 | "License :: OSI Approved :: Apache Software License" 37 | ] 38 | 39 | 40 | 41 | [project.urls] 42 | Homepage = "https://github.com/undefined/unitycatalog-python" 43 | Repository = "https://github.com/undefined/unitycatalog-python" 44 | 45 | 46 | 47 | [tool.rye] 48 | managed = true 49 | # version pins are in requirements-dev.lock 50 | dev-dependencies = [ 51 | "pyright>=1.1.359", 52 | "mypy", 53 | "respx", 54 | "pytest", 55 | "pytest-asyncio", 56 | "ruff", 57 | "time-machine", 58 | "nox", 59 | "dirty-equals>=0.6.0", 60 | "importlib-metadata>=6.7.0", 61 | 62 | ] 63 | 64 | [tool.rye.scripts] 65 | format = { chain = [ 66 | "format:ruff", 67 | "format:docs", 68 | "fix:ruff", 69 | ]} 70 | "format:black" = "black ." 71 | "format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" 72 | "format:ruff" = "ruff format" 73 | "format:isort" = "isort ." 74 | 75 | "lint" = { chain = [ 76 | "check:ruff", 77 | "typecheck", 78 | ]} 79 | "check:ruff" = "ruff ." 80 | "fix:ruff" = "ruff --fix ." 81 | 82 | typecheck = { chain = [ 83 | "typecheck:pyright", 84 | "typecheck:mypy" 85 | ]} 86 | "typecheck:pyright" = "pyright" 87 | "typecheck:verify-types" = "pyright --verifytypes unitycatalog --ignoreexternal" 88 | "typecheck:mypy" = "mypy ." 89 | 90 | [build-system] 91 | requires = ["hatchling", "hatch-fancy-pypi-readme"] 92 | build-backend = "hatchling.build" 93 | 94 | [tool.hatch.build] 95 | include = [ 96 | "src/*" 97 | ] 98 | 99 | [tool.hatch.build.targets.wheel] 100 | packages = ["src/unitycatalog"] 101 | 102 | [tool.hatch.metadata.hooks.fancy-pypi-readme] 103 | content-type = "text/markdown" 104 | 105 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] 106 | path = "README.md" 107 | 108 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] 109 | # replace relative links with absolute links 110 | pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' 111 | replacement = '[\1](https://github.com/undefined/unitycatalog-python/tree/main/\g<2>)' 112 | 113 | [tool.black] 114 | line-length = 120 115 | target-version = ["py37"] 116 | 117 | [tool.pytest.ini_options] 118 | testpaths = ["tests"] 119 | addopts = "--tb=short" 120 | xfail_strict = true 121 | asyncio_mode = "auto" 122 | filterwarnings = [ 123 | "error" 124 | ] 125 | 126 | [tool.pyright] 127 | # this enables practically every flag given by pyright. 128 | # there are a couple of flags that are still disabled by 129 | # default in strict mode as they are experimental and niche. 130 | typeCheckingMode = "strict" 131 | pythonVersion = "3.7" 132 | 133 | exclude = [ 134 | "_dev", 135 | ".venv", 136 | ".nox", 137 | ] 138 | 139 | reportImplicitOverride = true 140 | 141 | reportImportCycles = false 142 | reportPrivateUsage = false 143 | 144 | 145 | [tool.ruff] 146 | line-length = 120 147 | output-format = "grouped" 148 | target-version = "py37" 149 | select = [ 150 | # isort 151 | "I", 152 | # bugbear rules 153 | "B", 154 | # remove unused imports 155 | "F401", 156 | # bare except statements 157 | "E722", 158 | # unused arguments 159 | "ARG", 160 | # print statements 161 | "T201", 162 | "T203", 163 | # misuse of typing.TYPE_CHECKING 164 | "TCH004", 165 | # import rules 166 | "TID251", 167 | ] 168 | ignore = [ 169 | # mutable defaults 170 | "B006", 171 | ] 172 | unfixable = [ 173 | # disable auto fix for print statements 174 | "T201", 175 | "T203", 176 | ] 177 | ignore-init-module-imports = true 178 | 179 | [tool.ruff.format] 180 | docstring-code-format = true 181 | 182 | [tool.ruff.lint.flake8-tidy-imports.banned-api] 183 | "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" 184 | 185 | [tool.ruff.lint.isort] 186 | length-sort = true 187 | length-sort-straight = true 188 | combine-as-imports = true 189 | extra-standard-library = ["typing_extensions"] 190 | known-first-party = ["unitycatalog", "tests"] 191 | 192 | [tool.ruff.per-file-ignores] 193 | "bin/**.py" = ["T201", "T203"] 194 | "scripts/**.py" = ["T201", "T203"] 195 | "tests/**.py" = ["T201", "T203"] 196 | "examples/**.py" = ["T201", "T203"] 197 | -------------------------------------------------------------------------------- /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/unitycatalog/_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 | 10 | -e file:. 11 | annotated-types==0.6.0 12 | # via pydantic 13 | anyio==4.1.0 14 | # via httpx 15 | # via unitycatalog 16 | argcomplete==3.1.2 17 | # via nox 18 | attrs==23.1.0 19 | # via pytest 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 unitycatalog 30 | exceptiongroup==1.1.3 31 | # via anyio 32 | filelock==3.12.4 33 | # via virtualenv 34 | h11==0.14.0 35 | # via httpcore 36 | httpcore==1.0.2 37 | # via httpx 38 | httpx==0.25.2 39 | # via respx 40 | # via unitycatalog 41 | idna==3.4 42 | # via anyio 43 | # via httpx 44 | importlib-metadata==7.0.0 45 | iniconfig==2.0.0 46 | # via pytest 47 | mypy==1.7.1 48 | mypy-extensions==1.0.0 49 | # via mypy 50 | nodeenv==1.8.0 51 | # via pyright 52 | nox==2023.4.22 53 | packaging==23.2 54 | # via nox 55 | # via pytest 56 | platformdirs==3.11.0 57 | # via virtualenv 58 | pluggy==1.3.0 59 | # via pytest 60 | py==1.11.0 61 | # via pytest 62 | pydantic==2.7.1 63 | # via unitycatalog 64 | pydantic-core==2.18.2 65 | # via pydantic 66 | pyright==1.1.364 67 | pytest==7.1.1 68 | # via pytest-asyncio 69 | pytest-asyncio==0.21.1 70 | python-dateutil==2.8.2 71 | # via time-machine 72 | pytz==2023.3.post1 73 | # via dirty-equals 74 | respx==0.20.2 75 | ruff==0.1.9 76 | setuptools==68.2.2 77 | # via nodeenv 78 | six==1.16.0 79 | # via python-dateutil 80 | sniffio==1.3.0 81 | # via anyio 82 | # via httpx 83 | # via unitycatalog 84 | time-machine==2.9.0 85 | tomli==2.0.1 86 | # via mypy 87 | # via pytest 88 | typing-extensions==4.8.0 89 | # via mypy 90 | # via pydantic 91 | # via pydantic-core 92 | # via unitycatalog 93 | virtualenv==20.24.5 94 | # via nox 95 | zipp==3.17.0 96 | # via importlib-metadata 97 | -------------------------------------------------------------------------------- /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 | 10 | -e file:. 11 | annotated-types==0.6.0 12 | # via pydantic 13 | anyio==4.1.0 14 | # via httpx 15 | # via unitycatalog 16 | certifi==2023.7.22 17 | # via httpcore 18 | # via httpx 19 | distro==1.8.0 20 | # via unitycatalog 21 | exceptiongroup==1.1.3 22 | # via anyio 23 | h11==0.14.0 24 | # via httpcore 25 | httpcore==1.0.2 26 | # via httpx 27 | httpx==0.25.2 28 | # via unitycatalog 29 | idna==3.4 30 | # via anyio 31 | # via httpx 32 | pydantic==2.7.1 33 | # via unitycatalog 34 | pydantic-core==2.18.2 35 | # via pydantic 36 | sniffio==1.3.0 37 | # via anyio 38 | # via httpx 39 | # via unitycatalog 40 | typing-extensions==4.8.0 41 | # via pydantic 42 | # via pydantic-core 43 | # via unitycatalog 44 | -------------------------------------------------------------------------------- /scripts/bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | if [ -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 unitycatalog' 12 | 13 | -------------------------------------------------------------------------------- /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=@stoplight/prism-cli@~5.8 -- 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=@stoplight/prism-cli@~5.8 -- 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 | echo "==> Running tests" 56 | rye run pytest "$@" 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/unitycatalog/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from . import types 4 | from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes 5 | from ._utils import file_from_path 6 | from ._client import ( 7 | Client, 8 | Stream, 9 | Timeout, 10 | Transport, 11 | AsyncClient, 12 | AsyncStream, 13 | Unitycatalog, 14 | RequestOptions, 15 | AsyncUnitycatalog, 16 | ) 17 | from ._models import BaseModel 18 | from ._version import __title__, __version__ 19 | from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse 20 | from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS 21 | from ._exceptions import ( 22 | APIError, 23 | ConflictError, 24 | NotFoundError, 25 | APIStatusError, 26 | RateLimitError, 27 | APITimeoutError, 28 | BadRequestError, 29 | UnitycatalogError, 30 | APIConnectionError, 31 | AuthenticationError, 32 | InternalServerError, 33 | PermissionDeniedError, 34 | UnprocessableEntityError, 35 | APIResponseValidationError, 36 | ) 37 | from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient 38 | from ._utils._logs import setup_logging as _setup_logging 39 | 40 | __all__ = [ 41 | "types", 42 | "__version__", 43 | "__title__", 44 | "NoneType", 45 | "Transport", 46 | "ProxiesTypes", 47 | "NotGiven", 48 | "NOT_GIVEN", 49 | "UnitycatalogError", 50 | "APIError", 51 | "APIStatusError", 52 | "APITimeoutError", 53 | "APIConnectionError", 54 | "APIResponseValidationError", 55 | "BadRequestError", 56 | "AuthenticationError", 57 | "PermissionDeniedError", 58 | "NotFoundError", 59 | "ConflictError", 60 | "UnprocessableEntityError", 61 | "RateLimitError", 62 | "InternalServerError", 63 | "Timeout", 64 | "RequestOptions", 65 | "Client", 66 | "AsyncClient", 67 | "Stream", 68 | "AsyncStream", 69 | "Unitycatalog", 70 | "AsyncUnitycatalog", 71 | "file_from_path", 72 | "BaseModel", 73 | "DEFAULT_TIMEOUT", 74 | "DEFAULT_MAX_RETRIES", 75 | "DEFAULT_CONNECTION_LIMITS", 76 | "DefaultHttpxClient", 77 | "DefaultAsyncHttpxClient", 78 | ] 79 | 80 | _setup_logging() 81 | 82 | # Update the __module__ attribute for exported symbols so that 83 | # error messages point to this module instead of the module 84 | # it was originally defined in, e.g. 85 | # unitycatalog._exceptions.NotFoundError -> unitycatalog.NotFoundError 86 | __locals = locals() 87 | for __name in __all__: 88 | if not __name.startswith("__"): 89 | try: 90 | __locals[__name].__module__ = "unitycatalog" 91 | except (TypeError, AttributeError): 92 | # Some of our exported symbols are builtins which we can't set attributes for. 93 | pass 94 | -------------------------------------------------------------------------------- /src/unitycatalog/_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 6 | 7 | import pydantic 8 | from pydantic.fields import FieldInfo 9 | 10 | from ._types import 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) -> _ModelT: 122 | if PYDANTIC_V2: 123 | return model.model_copy() 124 | return model.copy() # 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_unset: bool = False, 137 | exclude_defaults: bool = False, 138 | ) -> dict[str, Any]: 139 | if PYDANTIC_V2: 140 | return model.model_dump( 141 | exclude_unset=exclude_unset, 142 | exclude_defaults=exclude_defaults, 143 | ) 144 | return cast( 145 | "dict[str, Any]", 146 | model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] 147 | exclude_unset=exclude_unset, 148 | exclude_defaults=exclude_defaults, 149 | ), 150 | ) 151 | 152 | 153 | def model_parse(model: type[_ModelT], data: Any) -> _ModelT: 154 | if PYDANTIC_V2: 155 | return model.model_validate(data) 156 | return model.parse_obj(data) # pyright: ignore[reportDeprecated] 157 | 158 | 159 | # generic models 160 | if TYPE_CHECKING: 161 | 162 | class GenericModel(pydantic.BaseModel): 163 | ... 164 | 165 | else: 166 | if PYDANTIC_V2: 167 | # there no longer needs to be a distinction in v2 but 168 | # we still have to create our own subclass to avoid 169 | # inconsistent MRO ordering errors 170 | class GenericModel(pydantic.BaseModel): 171 | ... 172 | 173 | else: 174 | import pydantic.generics 175 | 176 | class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): 177 | ... 178 | 179 | 180 | # cached properties 181 | if TYPE_CHECKING: 182 | cached_property = property 183 | 184 | # we define a separate type (copied from typeshed) 185 | # that represents that `cached_property` is `set`able 186 | # at runtime, which differs from `@property`. 187 | # 188 | # this is a separate type as editors likely special case 189 | # `@property` and we don't want to cause issues just to have 190 | # more helpful internal types. 191 | 192 | class typed_cached_property(Generic[_T]): 193 | func: Callable[[Any], _T] 194 | attrname: str | None 195 | 196 | def __init__(self, func: Callable[[Any], _T]) -> None: 197 | ... 198 | 199 | @overload 200 | def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: 201 | ... 202 | 203 | @overload 204 | def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: 205 | ... 206 | 207 | def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self: 208 | raise NotImplementedError() 209 | 210 | def __set_name__(self, owner: type[Any], name: str) -> None: 211 | ... 212 | 213 | # __set__ is not defined at runtime, but @cached_property is designed to be settable 214 | def __set__(self, instance: object, value: _T) -> None: 215 | ... 216 | else: 217 | try: 218 | from functools import cached_property as cached_property 219 | except ImportError: 220 | from cached_property import cached_property as cached_property 221 | 222 | typed_cached_property = cached_property 223 | -------------------------------------------------------------------------------- /src/unitycatalog/_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.0, 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/unitycatalog/_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 UnitycatalogError(Exception): 22 | pass 23 | 24 | 25 | class APIError(UnitycatalogError): 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/unitycatalog/_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 | 46 | @overload 47 | def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: 48 | ... 49 | 50 | 51 | def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: 52 | if files is None: 53 | return None 54 | 55 | if is_mapping_t(files): 56 | files = {key: _transform_file(file) for key, file in files.items()} 57 | elif is_sequence_t(files): 58 | files = [(key, _transform_file(file)) for key, file in files] 59 | else: 60 | raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") 61 | 62 | return files 63 | 64 | 65 | def _transform_file(file: FileTypes) -> HttpxFileTypes: 66 | if is_file_content(file): 67 | if isinstance(file, os.PathLike): 68 | path = pathlib.Path(file) 69 | return (path.name, path.read_bytes()) 70 | 71 | return file 72 | 73 | if is_tuple_t(file): 74 | return (file[0], _read_file_content(file[1]), *file[2:]) 75 | 76 | raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") 77 | 78 | 79 | def _read_file_content(file: FileContent) -> HttpxFileContent: 80 | if isinstance(file, os.PathLike): 81 | return pathlib.Path(file).read_bytes() 82 | return file 83 | 84 | 85 | @overload 86 | async def async_to_httpx_files(files: None) -> None: 87 | ... 88 | 89 | 90 | @overload 91 | async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: 92 | ... 93 | 94 | 95 | async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: 96 | if files is None: 97 | return None 98 | 99 | if is_mapping_t(files): 100 | files = {key: await _async_transform_file(file) for key, file in files.items()} 101 | elif is_sequence_t(files): 102 | files = [(key, await _async_transform_file(file)) for key, file in files] 103 | else: 104 | raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") 105 | 106 | return files 107 | 108 | 109 | async def _async_transform_file(file: FileTypes) -> HttpxFileTypes: 110 | if is_file_content(file): 111 | if isinstance(file, os.PathLike): 112 | path = anyio.Path(file) 113 | return (path.name, await path.read_bytes()) 114 | 115 | return file 116 | 117 | if is_tuple_t(file): 118 | return (file[0], await _async_read_file_content(file[1]), *file[2:]) 119 | 120 | raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") 121 | 122 | 123 | async def _async_read_file_content(file: FileContent) -> HttpxFileContent: 124 | if isinstance(file, os.PathLike): 125 | return await anyio.Path(file).read_bytes() 126 | 127 | return file 128 | -------------------------------------------------------------------------------- /src/unitycatalog/_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/unitycatalog/_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 Unitycatalog, AsyncUnitycatalog 12 | 13 | 14 | class SyncAPIResource: 15 | _client: Unitycatalog 16 | 17 | def __init__(self, client: Unitycatalog) -> 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: AsyncUnitycatalog 32 | 33 | def __init__(self, client: AsyncUnitycatalog) -> 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/unitycatalog/_streaming.py: -------------------------------------------------------------------------------- 1 | # Note: initially copied from https://github.com/florimondmanca/httpx-sse/blob/master/src/httpx_sse/_decoders.py 2 | from __future__ import annotations 3 | 4 | import json 5 | import inspect 6 | from types import TracebackType 7 | from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast 8 | from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable 9 | 10 | import httpx 11 | 12 | from ._utils import extract_type_var_from_base 13 | 14 | if TYPE_CHECKING: 15 | from ._client import Unitycatalog, AsyncUnitycatalog 16 | 17 | 18 | _T = TypeVar("_T") 19 | 20 | 21 | class Stream(Generic[_T]): 22 | """Provides the core interface to iterate over a synchronous stream response.""" 23 | 24 | response: httpx.Response 25 | 26 | _decoder: SSEBytesDecoder 27 | 28 | def __init__( 29 | self, 30 | *, 31 | cast_to: type[_T], 32 | response: httpx.Response, 33 | client: Unitycatalog, 34 | ) -> None: 35 | self.response = response 36 | self._cast_to = cast_to 37 | self._client = client 38 | self._decoder = client._make_sse_decoder() 39 | self._iterator = self.__stream__() 40 | 41 | def __next__(self) -> _T: 42 | return self._iterator.__next__() 43 | 44 | def __iter__(self) -> Iterator[_T]: 45 | for item in self._iterator: 46 | yield item 47 | 48 | def _iter_events(self) -> Iterator[ServerSentEvent]: 49 | yield from self._decoder.iter_bytes(self.response.iter_bytes()) 50 | 51 | def __stream__(self) -> Iterator[_T]: 52 | cast_to = cast(Any, self._cast_to) 53 | response = self.response 54 | process_data = self._client._process_response_data 55 | iterator = self._iter_events() 56 | 57 | for sse in iterator: 58 | yield process_data(data=sse.json(), cast_to=cast_to, response=response) 59 | 60 | # Ensure the entire stream is consumed 61 | for _sse in iterator: 62 | ... 63 | 64 | def __enter__(self) -> Self: 65 | return self 66 | 67 | def __exit__( 68 | self, 69 | exc_type: type[BaseException] | None, 70 | exc: BaseException | None, 71 | exc_tb: TracebackType | None, 72 | ) -> None: 73 | self.close() 74 | 75 | def close(self) -> None: 76 | """ 77 | Close the response and release the connection. 78 | 79 | Automatically called if the response body is read to completion. 80 | """ 81 | self.response.close() 82 | 83 | 84 | class AsyncStream(Generic[_T]): 85 | """Provides the core interface to iterate over an asynchronous stream response.""" 86 | 87 | response: httpx.Response 88 | 89 | _decoder: SSEDecoder | SSEBytesDecoder 90 | 91 | def __init__( 92 | self, 93 | *, 94 | cast_to: type[_T], 95 | response: httpx.Response, 96 | client: AsyncUnitycatalog, 97 | ) -> None: 98 | self.response = response 99 | self._cast_to = cast_to 100 | self._client = client 101 | self._decoder = client._make_sse_decoder() 102 | self._iterator = self.__stream__() 103 | 104 | async def __anext__(self) -> _T: 105 | return await self._iterator.__anext__() 106 | 107 | async def __aiter__(self) -> AsyncIterator[_T]: 108 | async for item in self._iterator: 109 | yield item 110 | 111 | async def _iter_events(self) -> AsyncIterator[ServerSentEvent]: 112 | async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()): 113 | yield sse 114 | 115 | async def __stream__(self) -> AsyncIterator[_T]: 116 | cast_to = cast(Any, self._cast_to) 117 | response = self.response 118 | process_data = self._client._process_response_data 119 | iterator = self._iter_events() 120 | 121 | async for sse in iterator: 122 | yield process_data(data=sse.json(), cast_to=cast_to, response=response) 123 | 124 | # Ensure the entire stream is consumed 125 | async for _sse in iterator: 126 | ... 127 | 128 | async def __aenter__(self) -> Self: 129 | return self 130 | 131 | async def __aexit__( 132 | self, 133 | exc_type: type[BaseException] | None, 134 | exc: BaseException | None, 135 | exc_tb: TracebackType | None, 136 | ) -> None: 137 | await self.close() 138 | 139 | async def close(self) -> None: 140 | """ 141 | Close the response and release the connection. 142 | 143 | Automatically called if the response body is read to completion. 144 | """ 145 | await self.response.aclose() 146 | 147 | 148 | class ServerSentEvent: 149 | def __init__( 150 | self, 151 | *, 152 | event: str | None = None, 153 | data: str | None = None, 154 | id: str | None = None, 155 | retry: int | None = None, 156 | ) -> None: 157 | if data is None: 158 | data = "" 159 | 160 | self._id = id 161 | self._data = data 162 | self._event = event or None 163 | self._retry = retry 164 | 165 | @property 166 | def event(self) -> str | None: 167 | return self._event 168 | 169 | @property 170 | def id(self) -> str | None: 171 | return self._id 172 | 173 | @property 174 | def retry(self) -> int | None: 175 | return self._retry 176 | 177 | @property 178 | def data(self) -> str: 179 | return self._data 180 | 181 | def json(self) -> Any: 182 | return json.loads(self.data) 183 | 184 | @override 185 | def __repr__(self) -> str: 186 | return f"ServerSentEvent(event={self.event}, data={self.data}, id={self.id}, retry={self.retry})" 187 | 188 | 189 | class SSEDecoder: 190 | _data: list[str] 191 | _event: str | None 192 | _retry: int | None 193 | _last_event_id: str | None 194 | 195 | def __init__(self) -> None: 196 | self._event = None 197 | self._data = [] 198 | self._last_event_id = None 199 | self._retry = None 200 | 201 | def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: 202 | """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" 203 | for chunk in self._iter_chunks(iterator): 204 | # Split before decoding so splitlines() only uses \r and \n 205 | for raw_line in chunk.splitlines(): 206 | line = raw_line.decode("utf-8") 207 | sse = self.decode(line) 208 | if sse: 209 | yield sse 210 | 211 | def _iter_chunks(self, iterator: Iterator[bytes]) -> Iterator[bytes]: 212 | """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" 213 | data = b"" 214 | for chunk in iterator: 215 | for line in chunk.splitlines(keepends=True): 216 | data += line 217 | if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): 218 | yield data 219 | data = b"" 220 | if data: 221 | yield data 222 | 223 | async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: 224 | """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" 225 | async for chunk in self._aiter_chunks(iterator): 226 | # Split before decoding so splitlines() only uses \r and \n 227 | for raw_line in chunk.splitlines(): 228 | line = raw_line.decode("utf-8") 229 | sse = self.decode(line) 230 | if sse: 231 | yield sse 232 | 233 | async def _aiter_chunks(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[bytes]: 234 | """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" 235 | data = b"" 236 | async for chunk in iterator: 237 | for line in chunk.splitlines(keepends=True): 238 | data += line 239 | if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): 240 | yield data 241 | data = b"" 242 | if data: 243 | yield data 244 | 245 | def decode(self, line: str) -> ServerSentEvent | None: 246 | # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 247 | 248 | if not line: 249 | if not self._event and not self._data and not self._last_event_id and self._retry is None: 250 | return None 251 | 252 | sse = ServerSentEvent( 253 | event=self._event, 254 | data="\n".join(self._data), 255 | id=self._last_event_id, 256 | retry=self._retry, 257 | ) 258 | 259 | # NOTE: as per the SSE spec, do not reset last_event_id. 260 | self._event = None 261 | self._data = [] 262 | self._retry = None 263 | 264 | return sse 265 | 266 | if line.startswith(":"): 267 | return None 268 | 269 | fieldname, _, value = line.partition(":") 270 | 271 | if value.startswith(" "): 272 | value = value[1:] 273 | 274 | if fieldname == "event": 275 | self._event = value 276 | elif fieldname == "data": 277 | self._data.append(value) 278 | elif fieldname == "id": 279 | if "\0" in value: 280 | pass 281 | else: 282 | self._last_event_id = value 283 | elif fieldname == "retry": 284 | try: 285 | self._retry = int(value) 286 | except (TypeError, ValueError): 287 | pass 288 | else: 289 | pass # Field is ignored. 290 | 291 | return None 292 | 293 | 294 | @runtime_checkable 295 | class SSEBytesDecoder(Protocol): 296 | def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: 297 | """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" 298 | ... 299 | 300 | def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: 301 | """Given an async iterator that yields raw binary data, iterate over it & yield every event encountered""" 302 | ... 303 | 304 | 305 | def is_stream_class_type(typ: type) -> TypeGuard[type[Stream[object]] | type[AsyncStream[object]]]: 306 | """TypeGuard for determining whether or not the given type is a subclass of `Stream` / `AsyncStream`""" 307 | origin = get_origin(typ) or typ 308 | return inspect.isclass(origin) and issubclass(origin, (Stream, AsyncStream)) 309 | 310 | 311 | def extract_stream_chunk_type( 312 | stream_cls: type, 313 | *, 314 | failure_message: str | None = None, 315 | ) -> type: 316 | """Given a type like `Stream[T]`, returns the generic type variable `T`. 317 | 318 | This also handles the case where a concrete subclass is given, e.g. 319 | ```py 320 | class MyStream(Stream[bytes]): 321 | ... 322 | 323 | extract_stream_chunk_type(MyStream) -> bytes 324 | ``` 325 | """ 326 | from ._base_client import Stream, AsyncStream 327 | 328 | return extract_type_var_from_base( 329 | stream_cls, 330 | index=0, 331 | generic_bases=cast("tuple[type, ...]", (Stream, AsyncStream)), 332 | failure_message=failure_message, 333 | ) 334 | -------------------------------------------------------------------------------- /src/unitycatalog/_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 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 unitycatalog 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 | 118 | get(timeout=1) # 1s timeout 119 | get(timeout=None) # No timeout 120 | get() # Default timeout behavior, which may not be statically known at the method definition. 121 | ``` 122 | """ 123 | 124 | def __bool__(self) -> Literal[False]: 125 | return False 126 | 127 | @override 128 | def __repr__(self) -> str: 129 | return "NOT_GIVEN" 130 | 131 | 132 | NotGivenOr = Union[_T, NotGiven] 133 | NOT_GIVEN = NotGiven() 134 | 135 | 136 | class Omit: 137 | """In certain situations you need to be able to represent a case where a default value has 138 | to be explicitly removed and `None` is not an appropriate substitute, for example: 139 | 140 | ```py 141 | # as the default `Content-Type` header is `application/json` that will be sent 142 | client.post("/upload/files", files={"file": b"my raw file content"}) 143 | 144 | # you can't explicitly override the header as it has to be dynamically generated 145 | # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983' 146 | client.post(..., headers={"Content-Type": "multipart/form-data"}) 147 | 148 | # instead you can remove the default `application/json` header by passing Omit 149 | client.post(..., headers={"Content-Type": Omit()}) 150 | ``` 151 | """ 152 | 153 | def __bool__(self) -> Literal[False]: 154 | return False 155 | 156 | 157 | @runtime_checkable 158 | class ModelBuilderProtocol(Protocol): 159 | @classmethod 160 | def build( 161 | cls: type[_T], 162 | *, 163 | response: Response, 164 | data: object, 165 | ) -> _T: 166 | ... 167 | 168 | 169 | Headers = Mapping[str, Union[str, Omit]] 170 | 171 | 172 | class HeadersLikeProtocol(Protocol): 173 | def get(self, __key: str) -> str | None: 174 | ... 175 | 176 | 177 | HeadersLike = Union[Headers, HeadersLikeProtocol] 178 | 179 | ResponseT = TypeVar( 180 | "ResponseT", 181 | bound=Union[ 182 | object, 183 | str, 184 | None, 185 | "BaseModel", 186 | List[Any], 187 | Dict[str, Any], 188 | Response, 189 | ModelBuilderProtocol, 190 | "APIResponse[Any]", 191 | "AsyncAPIResponse[Any]", 192 | ], 193 | ) 194 | 195 | StrBytesIntFloat = Union[str, bytes, int, float] 196 | 197 | # Note: copied from Pydantic 198 | # https://github.com/pydantic/pydantic/blob/32ea570bf96e84234d2992e1ddf40ab8a565925a/pydantic/main.py#L49 199 | IncEx: TypeAlias = "set[int] | set[str] | dict[int, Any] | dict[str, Any] | None" 200 | 201 | PostParser = Callable[[Any], Any] 202 | 203 | 204 | @runtime_checkable 205 | class InheritsGeneric(Protocol): 206 | """Represents a type that has inherited from `Generic` 207 | 208 | The `__orig_bases__` property can be used to determine the resolved 209 | type variable for a given base class. 210 | """ 211 | 212 | __orig_bases__: tuple[_GenericAlias] 213 | 214 | 215 | class _GenericAlias(Protocol): 216 | __origin__: type[object] 217 | 218 | 219 | class HttpxSendArgs(TypedDict, total=False): 220 | auth: httpx.Auth 221 | -------------------------------------------------------------------------------- /src/unitycatalog/_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 | lru_cache as lru_cache, 10 | is_mapping as is_mapping, 11 | is_tuple_t as is_tuple_t, 12 | parse_date as parse_date, 13 | is_iterable as is_iterable, 14 | is_sequence as is_sequence, 15 | coerce_float as coerce_float, 16 | is_mapping_t as is_mapping_t, 17 | removeprefix as removeprefix, 18 | removesuffix as removesuffix, 19 | extract_files as extract_files, 20 | is_sequence_t as is_sequence_t, 21 | required_args as required_args, 22 | coerce_boolean as coerce_boolean, 23 | coerce_integer as coerce_integer, 24 | file_from_path as file_from_path, 25 | parse_datetime as parse_datetime, 26 | strip_not_given as strip_not_given, 27 | deepcopy_minimal as deepcopy_minimal, 28 | get_async_library as get_async_library, 29 | maybe_coerce_float as maybe_coerce_float, 30 | get_required_header as get_required_header, 31 | maybe_coerce_boolean as maybe_coerce_boolean, 32 | maybe_coerce_integer as maybe_coerce_integer, 33 | ) 34 | from ._typing import ( 35 | is_list_type as is_list_type, 36 | is_union_type as is_union_type, 37 | extract_type_arg as extract_type_arg, 38 | is_iterable_type as is_iterable_type, 39 | is_required_type as is_required_type, 40 | is_annotated_type as is_annotated_type, 41 | strip_annotated_type as strip_annotated_type, 42 | extract_type_var_from_base as extract_type_var_from_base, 43 | ) 44 | from ._streams import consume_sync_iterator as consume_sync_iterator, consume_async_iterator as consume_async_iterator 45 | from ._transform import ( 46 | PropertyInfo as PropertyInfo, 47 | transform as transform, 48 | async_transform as async_transform, 49 | maybe_transform as maybe_transform, 50 | async_maybe_transform as async_maybe_transform, 51 | ) 52 | -------------------------------------------------------------------------------- /src/unitycatalog/_utils/_logs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | logger: logging.Logger = logging.getLogger("unitycatalog") 5 | httpx_logger: logging.Logger = logging.getLogger("httpx") 6 | 7 | 8 | def _basic_config() -> None: 9 | # e.g. [2023-10-05 14:12:26 - unitycatalog._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("UNITYCATALOG_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/unitycatalog/_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 | proxied = self.__get_proxied__() 50 | if issubclass(type(proxied), LazyProxy): 51 | return type(proxied) 52 | return proxied.__class__ 53 | 54 | def __get_proxied__(self) -> T: 55 | return self.__load__() 56 | 57 | def __as_proxied__(self) -> T: 58 | """Helper method that returns the current proxy, typed as the loaded object""" 59 | return cast(T, self) 60 | 61 | @abstractmethod 62 | def __load__(self) -> T: 63 | ... 64 | -------------------------------------------------------------------------------- /src/unitycatalog/_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/unitycatalog/_utils/_sync.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import functools 4 | from typing import TypeVar, Callable, Awaitable 5 | from typing_extensions import ParamSpec 6 | 7 | import anyio 8 | import anyio.to_thread 9 | 10 | T_Retval = TypeVar("T_Retval") 11 | T_ParamSpec = ParamSpec("T_ParamSpec") 12 | 13 | 14 | # copied from `asyncer`, https://github.com/tiangolo/asyncer 15 | def asyncify( 16 | function: Callable[T_ParamSpec, T_Retval], 17 | *, 18 | cancellable: bool = False, 19 | limiter: anyio.CapacityLimiter | None = None, 20 | ) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: 21 | """ 22 | Take a blocking function and create an async one that receives the same 23 | positional and keyword arguments, and that when called, calls the original function 24 | in a worker thread using `anyio.to_thread.run_sync()`. Internally, 25 | `asyncer.asyncify()` uses the same `anyio.to_thread.run_sync()`, but it supports 26 | keyword arguments additional to positional arguments and it adds better support for 27 | autocompletion and inline errors for the arguments of the function called and the 28 | return value. 29 | 30 | If the `cancellable` option is enabled and the task waiting for its completion is 31 | cancelled, the thread will still run its course but its return value (or any raised 32 | exception) will be ignored. 33 | 34 | Use it like this: 35 | 36 | ```Python 37 | def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str: 38 | # Do work 39 | return "Some result" 40 | 41 | 42 | result = await to_thread.asyncify(do_work)("spam", "ham", kwarg1="a", kwarg2="b") 43 | print(result) 44 | ``` 45 | 46 | ## Arguments 47 | 48 | `function`: a blocking regular callable (e.g. a function) 49 | `cancellable`: `True` to allow cancellation of the operation 50 | `limiter`: capacity limiter to use to limit the total amount of threads running 51 | (if omitted, the default limiter is used) 52 | 53 | ## Return 54 | 55 | An async function that takes the same positional and keyword arguments as the 56 | original one, that when called runs the same original function in a thread worker 57 | and returns the result. 58 | """ 59 | 60 | async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: 61 | partial_f = functools.partial(function, *args, **kwargs) 62 | return await anyio.to_thread.run_sync(partial_f, cancellable=cancellable, limiter=limiter) 63 | 64 | return wrapper 65 | -------------------------------------------------------------------------------- /src/unitycatalog/_utils/_typing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, TypeVar, Iterable, cast 4 | from collections import abc as _c_abc 5 | from typing_extensions import Required, Annotated, get_args, get_origin 6 | 7 | from .._types import InheritsGeneric 8 | from .._compat import is_union as _is_union 9 | 10 | 11 | def is_annotated_type(typ: type) -> bool: 12 | return get_origin(typ) == Annotated 13 | 14 | 15 | def is_list_type(typ: type) -> bool: 16 | return (get_origin(typ) or typ) == list 17 | 18 | 19 | def is_iterable_type(typ: type) -> bool: 20 | """If the given type is `typing.Iterable[T]`""" 21 | origin = get_origin(typ) or typ 22 | return origin == Iterable or origin == _c_abc.Iterable 23 | 24 | 25 | def is_union_type(typ: type) -> bool: 26 | return _is_union(get_origin(typ)) 27 | 28 | 29 | def is_required_type(typ: type) -> bool: 30 | return get_origin(typ) == Required 31 | 32 | 33 | def is_typevar(typ: type) -> bool: 34 | # type ignore is required because type checkers 35 | # think this expression will always return False 36 | return type(typ) == TypeVar # type: ignore 37 | 38 | 39 | # Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] 40 | def strip_annotated_type(typ: type) -> type: 41 | if is_required_type(typ) or is_annotated_type(typ): 42 | return strip_annotated_type(cast(type, get_args(typ)[0])) 43 | 44 | return typ 45 | 46 | 47 | def extract_type_arg(typ: type, index: int) -> type: 48 | args = get_args(typ) 49 | try: 50 | return cast(type, args[index]) 51 | except IndexError as err: 52 | raise RuntimeError(f"Expected type {typ} to have a type argument at index {index} but it did not") from err 53 | 54 | 55 | def extract_type_var_from_base( 56 | typ: type, 57 | *, 58 | generic_bases: tuple[type, ...], 59 | index: int, 60 | failure_message: str | None = None, 61 | ) -> type: 62 | """Given a type like `Foo[T]`, returns the generic type variable `T`. 63 | 64 | This also handles the case where a concrete subclass is given, e.g. 65 | ```py 66 | class MyResponse(Foo[bytes]): 67 | ... 68 | 69 | extract_type_var(MyResponse, bases=(Foo,), index=0) -> bytes 70 | ``` 71 | 72 | And where a generic subclass is given: 73 | ```py 74 | _T = TypeVar('_T') 75 | class MyResponse(Foo[_T]): 76 | ... 77 | 78 | extract_type_var(MyResponse[bytes], bases=(Foo,), index=0) -> bytes 79 | ``` 80 | """ 81 | cls = cast(object, get_origin(typ) or typ) 82 | if cls in generic_bases: 83 | # we're given the class directly 84 | return extract_type_arg(typ, index) 85 | 86 | # if a subclass is given 87 | # --- 88 | # this is needed as __orig_bases__ is not present in the typeshed stubs 89 | # because it is intended to be for internal use only, however there does 90 | # not seem to be a way to resolve generic TypeVars for inherited subclasses 91 | # without using it. 92 | if isinstance(cls, InheritsGeneric): 93 | target_base_class: Any | None = None 94 | for base in cls.__orig_bases__: 95 | if base.__origin__ in generic_bases: 96 | target_base_class = base 97 | break 98 | 99 | if target_base_class is None: 100 | raise RuntimeError( 101 | "Could not find the generic base class;\n" 102 | "This should never happen;\n" 103 | f"Does {cls} inherit from one of {generic_bases} ?" 104 | ) 105 | 106 | extracted = extract_type_arg(target_base_class, index) 107 | if is_typevar(extracted): 108 | # If the extracted type argument is itself a type variable 109 | # then that means the subclass itself is generic, so we have 110 | # to resolve the type argument from the class itself, not 111 | # the base class. 112 | # 113 | # Note: if there is more than 1 type argument, the subclass could 114 | # change the ordering of the type arguments, this is not currently 115 | # supported. 116 | return extract_type_arg(typ, index) 117 | 118 | return extracted 119 | 120 | raise RuntimeError(failure_message or f"Could not resolve inner type variable at index {index} for {typ}") 121 | -------------------------------------------------------------------------------- /src/unitycatalog/_version.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | __title__ = "unitycatalog" 4 | __version__ = "0.0.1-alpha.0" # x-release-please-version 5 | -------------------------------------------------------------------------------- /src/unitycatalog/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/unitycatalog/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unitycatalog/unitycatalog-python/8225a781dbdb680a3ca180689ad31e35915ad3eb/src/unitycatalog/py.typed -------------------------------------------------------------------------------- /src/unitycatalog/resources/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from .tables import ( 4 | TablesResource, 5 | AsyncTablesResource, 6 | TablesResourceWithRawResponse, 7 | AsyncTablesResourceWithRawResponse, 8 | TablesResourceWithStreamingResponse, 9 | AsyncTablesResourceWithStreamingResponse, 10 | ) 11 | from .schemas import ( 12 | SchemasResource, 13 | AsyncSchemasResource, 14 | SchemasResourceWithRawResponse, 15 | AsyncSchemasResourceWithRawResponse, 16 | SchemasResourceWithStreamingResponse, 17 | AsyncSchemasResourceWithStreamingResponse, 18 | ) 19 | from .volumes import ( 20 | VolumesResource, 21 | AsyncVolumesResource, 22 | VolumesResourceWithRawResponse, 23 | AsyncVolumesResourceWithRawResponse, 24 | VolumesResourceWithStreamingResponse, 25 | AsyncVolumesResourceWithStreamingResponse, 26 | ) 27 | from .catalogs import ( 28 | CatalogsResource, 29 | AsyncCatalogsResource, 30 | CatalogsResourceWithRawResponse, 31 | AsyncCatalogsResourceWithRawResponse, 32 | CatalogsResourceWithStreamingResponse, 33 | AsyncCatalogsResourceWithStreamingResponse, 34 | ) 35 | from .functions import ( 36 | FunctionsResource, 37 | AsyncFunctionsResource, 38 | FunctionsResourceWithRawResponse, 39 | AsyncFunctionsResourceWithRawResponse, 40 | FunctionsResourceWithStreamingResponse, 41 | AsyncFunctionsResourceWithStreamingResponse, 42 | ) 43 | from .temporary_table_credentials import ( 44 | TemporaryTableCredentialsResource, 45 | AsyncTemporaryTableCredentialsResource, 46 | TemporaryTableCredentialsResourceWithRawResponse, 47 | AsyncTemporaryTableCredentialsResourceWithRawResponse, 48 | TemporaryTableCredentialsResourceWithStreamingResponse, 49 | AsyncTemporaryTableCredentialsResourceWithStreamingResponse, 50 | ) 51 | from .temporary_volume_credentials import ( 52 | TemporaryVolumeCredentialsResource, 53 | AsyncTemporaryVolumeCredentialsResource, 54 | TemporaryVolumeCredentialsResourceWithRawResponse, 55 | AsyncTemporaryVolumeCredentialsResourceWithRawResponse, 56 | TemporaryVolumeCredentialsResourceWithStreamingResponse, 57 | AsyncTemporaryVolumeCredentialsResourceWithStreamingResponse, 58 | ) 59 | 60 | __all__ = [ 61 | "CatalogsResource", 62 | "AsyncCatalogsResource", 63 | "CatalogsResourceWithRawResponse", 64 | "AsyncCatalogsResourceWithRawResponse", 65 | "CatalogsResourceWithStreamingResponse", 66 | "AsyncCatalogsResourceWithStreamingResponse", 67 | "SchemasResource", 68 | "AsyncSchemasResource", 69 | "SchemasResourceWithRawResponse", 70 | "AsyncSchemasResourceWithRawResponse", 71 | "SchemasResourceWithStreamingResponse", 72 | "AsyncSchemasResourceWithStreamingResponse", 73 | "TablesResource", 74 | "AsyncTablesResource", 75 | "TablesResourceWithRawResponse", 76 | "AsyncTablesResourceWithRawResponse", 77 | "TablesResourceWithStreamingResponse", 78 | "AsyncTablesResourceWithStreamingResponse", 79 | "VolumesResource", 80 | "AsyncVolumesResource", 81 | "VolumesResourceWithRawResponse", 82 | "AsyncVolumesResourceWithRawResponse", 83 | "VolumesResourceWithStreamingResponse", 84 | "AsyncVolumesResourceWithStreamingResponse", 85 | "TemporaryTableCredentialsResource", 86 | "AsyncTemporaryTableCredentialsResource", 87 | "TemporaryTableCredentialsResourceWithRawResponse", 88 | "AsyncTemporaryTableCredentialsResourceWithRawResponse", 89 | "TemporaryTableCredentialsResourceWithStreamingResponse", 90 | "AsyncTemporaryTableCredentialsResourceWithStreamingResponse", 91 | "TemporaryVolumeCredentialsResource", 92 | "AsyncTemporaryVolumeCredentialsResource", 93 | "TemporaryVolumeCredentialsResourceWithRawResponse", 94 | "AsyncTemporaryVolumeCredentialsResourceWithRawResponse", 95 | "TemporaryVolumeCredentialsResourceWithStreamingResponse", 96 | "AsyncTemporaryVolumeCredentialsResourceWithStreamingResponse", 97 | "FunctionsResource", 98 | "AsyncFunctionsResource", 99 | "FunctionsResourceWithRawResponse", 100 | "AsyncFunctionsResourceWithRawResponse", 101 | "FunctionsResourceWithStreamingResponse", 102 | "AsyncFunctionsResourceWithStreamingResponse", 103 | ] 104 | -------------------------------------------------------------------------------- /src/unitycatalog/resources/temporary_table_credentials.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 | from ..types import temporary_table_credential_create_params 10 | from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven 11 | from .._utils import ( 12 | maybe_transform, 13 | async_maybe_transform, 14 | ) 15 | from .._compat import cached_property 16 | from .._resource import SyncAPIResource, AsyncAPIResource 17 | from .._response import ( 18 | to_raw_response_wrapper, 19 | to_streamed_response_wrapper, 20 | async_to_raw_response_wrapper, 21 | async_to_streamed_response_wrapper, 22 | ) 23 | from .._base_client import ( 24 | make_request_options, 25 | ) 26 | from ..types.generate_temporary_table_credential_response import GenerateTemporaryTableCredentialResponse 27 | 28 | __all__ = ["TemporaryTableCredentialsResource", "AsyncTemporaryTableCredentialsResource"] 29 | 30 | 31 | class TemporaryTableCredentialsResource(SyncAPIResource): 32 | @cached_property 33 | def with_raw_response(self) -> TemporaryTableCredentialsResourceWithRawResponse: 34 | return TemporaryTableCredentialsResourceWithRawResponse(self) 35 | 36 | @cached_property 37 | def with_streaming_response(self) -> TemporaryTableCredentialsResourceWithStreamingResponse: 38 | return TemporaryTableCredentialsResourceWithStreamingResponse(self) 39 | 40 | def create( 41 | self, 42 | *, 43 | operation: Literal["UNKNOWN_TABLE_OPERATION", "READ", "READ_WRITE"], 44 | table_id: str, 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 | ) -> GenerateTemporaryTableCredentialResponse: 52 | """ 53 | Generate temporary table credentials. 54 | 55 | Args: 56 | table_id: Table id for which temporary credentials are generated. Can be obtained from 57 | tables/{full_name} (get table info) API. 58 | 59 | extra_headers: Send extra headers 60 | 61 | extra_query: Add additional query parameters to the request 62 | 63 | extra_body: Add additional JSON properties to the request 64 | 65 | timeout: Override the client-level default timeout for this request, in seconds 66 | """ 67 | return self._post( 68 | "/temporary-table-credentials", 69 | body=maybe_transform( 70 | { 71 | "operation": operation, 72 | "table_id": table_id, 73 | }, 74 | temporary_table_credential_create_params.TemporaryTableCredentialCreateParams, 75 | ), 76 | options=make_request_options( 77 | extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout 78 | ), 79 | cast_to=GenerateTemporaryTableCredentialResponse, 80 | ) 81 | 82 | 83 | class AsyncTemporaryTableCredentialsResource(AsyncAPIResource): 84 | @cached_property 85 | def with_raw_response(self) -> AsyncTemporaryTableCredentialsResourceWithRawResponse: 86 | return AsyncTemporaryTableCredentialsResourceWithRawResponse(self) 87 | 88 | @cached_property 89 | def with_streaming_response(self) -> AsyncTemporaryTableCredentialsResourceWithStreamingResponse: 90 | return AsyncTemporaryTableCredentialsResourceWithStreamingResponse(self) 91 | 92 | async def create( 93 | self, 94 | *, 95 | operation: Literal["UNKNOWN_TABLE_OPERATION", "READ", "READ_WRITE"], 96 | table_id: str, 97 | # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. 98 | # The extra values given here take precedence over values defined on the client or passed to this method. 99 | extra_headers: Headers | None = None, 100 | extra_query: Query | None = None, 101 | extra_body: Body | None = None, 102 | timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, 103 | ) -> GenerateTemporaryTableCredentialResponse: 104 | """ 105 | Generate temporary table credentials. 106 | 107 | Args: 108 | table_id: Table id for which temporary credentials are generated. Can be obtained from 109 | tables/{full_name} (get table info) API. 110 | 111 | extra_headers: Send extra headers 112 | 113 | extra_query: Add additional query parameters to the request 114 | 115 | extra_body: Add additional JSON properties to the request 116 | 117 | timeout: Override the client-level default timeout for this request, in seconds 118 | """ 119 | return await self._post( 120 | "/temporary-table-credentials", 121 | body=await async_maybe_transform( 122 | { 123 | "operation": operation, 124 | "table_id": table_id, 125 | }, 126 | temporary_table_credential_create_params.TemporaryTableCredentialCreateParams, 127 | ), 128 | options=make_request_options( 129 | extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout 130 | ), 131 | cast_to=GenerateTemporaryTableCredentialResponse, 132 | ) 133 | 134 | 135 | class TemporaryTableCredentialsResourceWithRawResponse: 136 | def __init__(self, temporary_table_credentials: TemporaryTableCredentialsResource) -> None: 137 | self._temporary_table_credentials = temporary_table_credentials 138 | 139 | self.create = to_raw_response_wrapper( 140 | temporary_table_credentials.create, 141 | ) 142 | 143 | 144 | class AsyncTemporaryTableCredentialsResourceWithRawResponse: 145 | def __init__(self, temporary_table_credentials: AsyncTemporaryTableCredentialsResource) -> None: 146 | self._temporary_table_credentials = temporary_table_credentials 147 | 148 | self.create = async_to_raw_response_wrapper( 149 | temporary_table_credentials.create, 150 | ) 151 | 152 | 153 | class TemporaryTableCredentialsResourceWithStreamingResponse: 154 | def __init__(self, temporary_table_credentials: TemporaryTableCredentialsResource) -> None: 155 | self._temporary_table_credentials = temporary_table_credentials 156 | 157 | self.create = to_streamed_response_wrapper( 158 | temporary_table_credentials.create, 159 | ) 160 | 161 | 162 | class AsyncTemporaryTableCredentialsResourceWithStreamingResponse: 163 | def __init__(self, temporary_table_credentials: AsyncTemporaryTableCredentialsResource) -> None: 164 | self._temporary_table_credentials = temporary_table_credentials 165 | 166 | self.create = async_to_streamed_response_wrapper( 167 | temporary_table_credentials.create, 168 | ) 169 | -------------------------------------------------------------------------------- /src/unitycatalog/resources/temporary_volume_credentials.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 | from ..types import temporary_volume_credential_create_params 10 | from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven 11 | from .._utils import ( 12 | maybe_transform, 13 | async_maybe_transform, 14 | ) 15 | from .._compat import cached_property 16 | from .._resource import SyncAPIResource, AsyncAPIResource 17 | from .._response import ( 18 | to_raw_response_wrapper, 19 | to_streamed_response_wrapper, 20 | async_to_raw_response_wrapper, 21 | async_to_streamed_response_wrapper, 22 | ) 23 | from .._base_client import ( 24 | make_request_options, 25 | ) 26 | from ..types.generate_temporary_volume_credential_response import GenerateTemporaryVolumeCredentialResponse 27 | 28 | __all__ = ["TemporaryVolumeCredentialsResource", "AsyncTemporaryVolumeCredentialsResource"] 29 | 30 | 31 | class TemporaryVolumeCredentialsResource(SyncAPIResource): 32 | @cached_property 33 | def with_raw_response(self) -> TemporaryVolumeCredentialsResourceWithRawResponse: 34 | return TemporaryVolumeCredentialsResourceWithRawResponse(self) 35 | 36 | @cached_property 37 | def with_streaming_response(self) -> TemporaryVolumeCredentialsResourceWithStreamingResponse: 38 | return TemporaryVolumeCredentialsResourceWithStreamingResponse(self) 39 | 40 | def create( 41 | self, 42 | *, 43 | operation: Literal["UNKNOWN_VOLUME_OPERATION", "READ_VOLUME", "WRITE_VOLUME"], 44 | volume_id: str, 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 | ) -> GenerateTemporaryVolumeCredentialResponse: 52 | """ 53 | Generate temporary volume credentials. 54 | 55 | Args: 56 | volume_id: Volume id for which temporary credentials are generated. Can be obtained from 57 | volumes/{full_name} (get volume info) API. 58 | 59 | extra_headers: Send extra headers 60 | 61 | extra_query: Add additional query parameters to the request 62 | 63 | extra_body: Add additional JSON properties to the request 64 | 65 | timeout: Override the client-level default timeout for this request, in seconds 66 | """ 67 | return self._post( 68 | "/temporary-volume-credentials", 69 | body=maybe_transform( 70 | { 71 | "operation": operation, 72 | "volume_id": volume_id, 73 | }, 74 | temporary_volume_credential_create_params.TemporaryVolumeCredentialCreateParams, 75 | ), 76 | options=make_request_options( 77 | extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout 78 | ), 79 | cast_to=GenerateTemporaryVolumeCredentialResponse, 80 | ) 81 | 82 | 83 | class AsyncTemporaryVolumeCredentialsResource(AsyncAPIResource): 84 | @cached_property 85 | def with_raw_response(self) -> AsyncTemporaryVolumeCredentialsResourceWithRawResponse: 86 | return AsyncTemporaryVolumeCredentialsResourceWithRawResponse(self) 87 | 88 | @cached_property 89 | def with_streaming_response(self) -> AsyncTemporaryVolumeCredentialsResourceWithStreamingResponse: 90 | return AsyncTemporaryVolumeCredentialsResourceWithStreamingResponse(self) 91 | 92 | async def create( 93 | self, 94 | *, 95 | operation: Literal["UNKNOWN_VOLUME_OPERATION", "READ_VOLUME", "WRITE_VOLUME"], 96 | volume_id: str, 97 | # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. 98 | # The extra values given here take precedence over values defined on the client or passed to this method. 99 | extra_headers: Headers | None = None, 100 | extra_query: Query | None = None, 101 | extra_body: Body | None = None, 102 | timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, 103 | ) -> GenerateTemporaryVolumeCredentialResponse: 104 | """ 105 | Generate temporary volume credentials. 106 | 107 | Args: 108 | volume_id: Volume id for which temporary credentials are generated. Can be obtained from 109 | volumes/{full_name} (get volume info) API. 110 | 111 | extra_headers: Send extra headers 112 | 113 | extra_query: Add additional query parameters to the request 114 | 115 | extra_body: Add additional JSON properties to the request 116 | 117 | timeout: Override the client-level default timeout for this request, in seconds 118 | """ 119 | return await self._post( 120 | "/temporary-volume-credentials", 121 | body=await async_maybe_transform( 122 | { 123 | "operation": operation, 124 | "volume_id": volume_id, 125 | }, 126 | temporary_volume_credential_create_params.TemporaryVolumeCredentialCreateParams, 127 | ), 128 | options=make_request_options( 129 | extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout 130 | ), 131 | cast_to=GenerateTemporaryVolumeCredentialResponse, 132 | ) 133 | 134 | 135 | class TemporaryVolumeCredentialsResourceWithRawResponse: 136 | def __init__(self, temporary_volume_credentials: TemporaryVolumeCredentialsResource) -> None: 137 | self._temporary_volume_credentials = temporary_volume_credentials 138 | 139 | self.create = to_raw_response_wrapper( 140 | temporary_volume_credentials.create, 141 | ) 142 | 143 | 144 | class AsyncTemporaryVolumeCredentialsResourceWithRawResponse: 145 | def __init__(self, temporary_volume_credentials: AsyncTemporaryVolumeCredentialsResource) -> None: 146 | self._temporary_volume_credentials = temporary_volume_credentials 147 | 148 | self.create = async_to_raw_response_wrapper( 149 | temporary_volume_credentials.create, 150 | ) 151 | 152 | 153 | class TemporaryVolumeCredentialsResourceWithStreamingResponse: 154 | def __init__(self, temporary_volume_credentials: TemporaryVolumeCredentialsResource) -> None: 155 | self._temporary_volume_credentials = temporary_volume_credentials 156 | 157 | self.create = to_streamed_response_wrapper( 158 | temporary_volume_credentials.create, 159 | ) 160 | 161 | 162 | class AsyncTemporaryVolumeCredentialsResourceWithStreamingResponse: 163 | def __init__(self, temporary_volume_credentials: AsyncTemporaryVolumeCredentialsResource) -> None: 164 | self._temporary_volume_credentials = temporary_volume_credentials 165 | 166 | self.create = async_to_streamed_response_wrapper( 167 | temporary_volume_credentials.create, 168 | ) 169 | -------------------------------------------------------------------------------- /src/unitycatalog/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 .table_info import TableInfo as TableInfo 6 | from .schema_info import SchemaInfo as SchemaInfo 7 | from .volume_info import VolumeInfo as VolumeInfo 8 | from .catalog_info import CatalogInfo as CatalogInfo 9 | from .function_info import FunctionInfo as FunctionInfo 10 | from .table_list_params import TableListParams as TableListParams 11 | from .schema_list_params import SchemaListParams as SchemaListParams 12 | from .volume_list_params import VolumeListParams as VolumeListParams 13 | from .catalog_list_params import CatalogListParams as CatalogListParams 14 | from .table_create_params import TableCreateParams as TableCreateParams 15 | from .table_list_response import TableListResponse as TableListResponse 16 | from .function_list_params import FunctionListParams as FunctionListParams 17 | from .schema_create_params import SchemaCreateParams as SchemaCreateParams 18 | from .schema_list_response import SchemaListResponse as SchemaListResponse 19 | from .schema_update_params import SchemaUpdateParams as SchemaUpdateParams 20 | from .volume_create_params import VolumeCreateParams as VolumeCreateParams 21 | from .volume_list_response import VolumeListResponse as VolumeListResponse 22 | from .volume_update_params import VolumeUpdateParams as VolumeUpdateParams 23 | from .catalog_create_params import CatalogCreateParams as CatalogCreateParams 24 | from .catalog_delete_params import CatalogDeleteParams as CatalogDeleteParams 25 | from .catalog_list_response import CatalogListResponse as CatalogListResponse 26 | from .catalog_update_params import CatalogUpdateParams as CatalogUpdateParams 27 | from .function_create_params import FunctionCreateParams as FunctionCreateParams 28 | from .function_list_response import FunctionListResponse as FunctionListResponse 29 | from .temporary_table_credential_create_params import ( 30 | TemporaryTableCredentialCreateParams as TemporaryTableCredentialCreateParams, 31 | ) 32 | from .temporary_volume_credential_create_params import ( 33 | TemporaryVolumeCredentialCreateParams as TemporaryVolumeCredentialCreateParams, 34 | ) 35 | from .generate_temporary_table_credential_response import ( 36 | GenerateTemporaryTableCredentialResponse as GenerateTemporaryTableCredentialResponse, 37 | ) 38 | from .generate_temporary_volume_credential_response import ( 39 | GenerateTemporaryVolumeCredentialResponse as GenerateTemporaryVolumeCredentialResponse, 40 | ) 41 | -------------------------------------------------------------------------------- /src/unitycatalog/types/catalog_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 6 | from typing_extensions import Required, TypedDict 7 | 8 | __all__ = ["CatalogCreateParams"] 9 | 10 | 11 | class CatalogCreateParams(TypedDict, total=False): 12 | name: Required[str] 13 | """Name of catalog.""" 14 | 15 | comment: str 16 | """User-provided free-form text description.""" 17 | 18 | properties: Dict[str, str] 19 | """A map of key-value properties attached to the securable.""" 20 | -------------------------------------------------------------------------------- /src/unitycatalog/types/catalog_delete_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__ = ["CatalogDeleteParams"] 8 | 9 | 10 | class CatalogDeleteParams(TypedDict, total=False): 11 | force: bool 12 | """Force deletion even if the catalog is not empty.""" 13 | -------------------------------------------------------------------------------- /src/unitycatalog/types/catalog_info.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__ = ["CatalogInfo"] 8 | 9 | 10 | class CatalogInfo(BaseModel): 11 | id: Optional[str] = None 12 | """Unique identifier for the catalog.""" 13 | 14 | comment: Optional[str] = None 15 | """User-provided free-form text description.""" 16 | 17 | created_at: Optional[int] = None 18 | """Time at which this catalog was created, in epoch milliseconds.""" 19 | 20 | name: Optional[str] = None 21 | """Name of catalog.""" 22 | 23 | properties: Optional[Dict[str, str]] = None 24 | """A map of key-value properties attached to the securable.""" 25 | 26 | updated_at: Optional[int] = None 27 | """Time at which this catalog was last modified, in epoch milliseconds.""" 28 | -------------------------------------------------------------------------------- /src/unitycatalog/types/catalog_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__ = ["CatalogListParams"] 8 | 9 | 10 | class CatalogListParams(TypedDict, total=False): 11 | max_results: int 12 | """Maximum number of catalogs to return. 13 | 14 | - when set to a value greater than 0, the page length is the minimum of this 15 | value and a server configured value; 16 | - when set to 0, the page length is set to a server configured value; 17 | - when set to a value less than 0, an invalid parameter error is returned; 18 | """ 19 | 20 | page_token: str 21 | """Opaque pagination token to go to next page based on previous query.""" 22 | -------------------------------------------------------------------------------- /src/unitycatalog/types/catalog_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 .catalog_info import CatalogInfo 7 | 8 | __all__ = ["CatalogListResponse"] 9 | 10 | 11 | class CatalogListResponse(BaseModel): 12 | catalogs: Optional[List[CatalogInfo]] = None 13 | """An array of catalog information objects.""" 14 | 15 | next_page_token: Optional[str] = None 16 | """Opaque token to retrieve the next page of results. 17 | 18 | Absent if there are no more pages. **page_token** should be set to this value 19 | for the next request (for the next page of results). 20 | """ 21 | -------------------------------------------------------------------------------- /src/unitycatalog/types/catalog_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 import Dict 6 | from typing_extensions import TypedDict 7 | 8 | __all__ = ["CatalogUpdateParams"] 9 | 10 | 11 | class CatalogUpdateParams(TypedDict, total=False): 12 | comment: str 13 | """User-provided free-form text description.""" 14 | 15 | new_name: str 16 | """New name for the catalog.""" 17 | 18 | properties: Dict[str, str] 19 | """A map of key-value properties attached to the securable.""" 20 | -------------------------------------------------------------------------------- /src/unitycatalog/types/function_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 Iterable 6 | from typing_extensions import Literal, Required, TypedDict 7 | 8 | __all__ = [ 9 | "FunctionCreateParams", 10 | "FunctionInfo", 11 | "FunctionInfoInputParams", 12 | "FunctionInfoInputParamsParameter", 13 | "FunctionInfoReturnParams", 14 | "FunctionInfoReturnParamsParameter", 15 | "FunctionInfoRoutineDependencies", 16 | "FunctionInfoRoutineDependenciesDependency", 17 | "FunctionInfoRoutineDependenciesDependencyFunction", 18 | "FunctionInfoRoutineDependenciesDependencyTable", 19 | ] 20 | 21 | 22 | class FunctionCreateParams(TypedDict, total=False): 23 | function_info: Required[FunctionInfo] 24 | 25 | 26 | class FunctionInfoInputParamsParameter(TypedDict, total=False): 27 | name: Required[str] 28 | """Name of parameter.""" 29 | 30 | position: Required[int] 31 | """Ordinal position of column (starting at position 0).""" 32 | 33 | type_json: Required[str] 34 | """Full data type spec, JSON-serialized.""" 35 | 36 | type_name: Required[ 37 | Literal[ 38 | "BOOLEAN", 39 | "BYTE", 40 | "SHORT", 41 | "INT", 42 | "LONG", 43 | "FLOAT", 44 | "DOUBLE", 45 | "DATE", 46 | "TIMESTAMP", 47 | "TIMESTAMP_NTZ", 48 | "STRING", 49 | "BINARY", 50 | "DECIMAL", 51 | "INTERVAL", 52 | "ARRAY", 53 | "STRUCT", 54 | "MAP", 55 | "CHAR", 56 | "NULL", 57 | "USER_DEFINED_TYPE", 58 | "TABLE_TYPE", 59 | ] 60 | ] 61 | """Name of type (INT, STRUCT, MAP, etc.).""" 62 | 63 | type_text: Required[str] 64 | """Full data type spec, SQL/catalogString text.""" 65 | 66 | comment: str 67 | """User-provided free-form text description.""" 68 | 69 | parameter_default: str 70 | """Default value of the parameter.""" 71 | 72 | parameter_mode: Literal["IN"] 73 | """The mode of the function parameter.""" 74 | 75 | parameter_type: Literal["PARAM", "COLUMN"] 76 | """The type of function parameter.""" 77 | 78 | type_interval_type: str 79 | """Format of IntervalType.""" 80 | 81 | type_precision: int 82 | """Digits of precision; required on Create for DecimalTypes.""" 83 | 84 | type_scale: int 85 | """Digits to right of decimal; Required on Create for DecimalTypes.""" 86 | 87 | 88 | class FunctionInfoInputParams(TypedDict, total=False): 89 | parameters: Iterable[FunctionInfoInputParamsParameter] 90 | """ 91 | The array of **FunctionParameterInfo** definitions of the function's parameters. 92 | """ 93 | 94 | 95 | class FunctionInfoReturnParamsParameter(TypedDict, total=False): 96 | name: Required[str] 97 | """Name of parameter.""" 98 | 99 | position: Required[int] 100 | """Ordinal position of column (starting at position 0).""" 101 | 102 | type_json: Required[str] 103 | """Full data type spec, JSON-serialized.""" 104 | 105 | type_name: Required[ 106 | Literal[ 107 | "BOOLEAN", 108 | "BYTE", 109 | "SHORT", 110 | "INT", 111 | "LONG", 112 | "FLOAT", 113 | "DOUBLE", 114 | "DATE", 115 | "TIMESTAMP", 116 | "TIMESTAMP_NTZ", 117 | "STRING", 118 | "BINARY", 119 | "DECIMAL", 120 | "INTERVAL", 121 | "ARRAY", 122 | "STRUCT", 123 | "MAP", 124 | "CHAR", 125 | "NULL", 126 | "USER_DEFINED_TYPE", 127 | "TABLE_TYPE", 128 | ] 129 | ] 130 | """Name of type (INT, STRUCT, MAP, etc.).""" 131 | 132 | type_text: Required[str] 133 | """Full data type spec, SQL/catalogString text.""" 134 | 135 | comment: str 136 | """User-provided free-form text description.""" 137 | 138 | parameter_default: str 139 | """Default value of the parameter.""" 140 | 141 | parameter_mode: Literal["IN"] 142 | """The mode of the function parameter.""" 143 | 144 | parameter_type: Literal["PARAM", "COLUMN"] 145 | """The type of function parameter.""" 146 | 147 | type_interval_type: str 148 | """Format of IntervalType.""" 149 | 150 | type_precision: int 151 | """Digits of precision; required on Create for DecimalTypes.""" 152 | 153 | type_scale: int 154 | """Digits to right of decimal; Required on Create for DecimalTypes.""" 155 | 156 | 157 | class FunctionInfoReturnParams(TypedDict, total=False): 158 | parameters: Iterable[FunctionInfoReturnParamsParameter] 159 | """ 160 | The array of **FunctionParameterInfo** definitions of the function's parameters. 161 | """ 162 | 163 | 164 | class FunctionInfoRoutineDependenciesDependencyFunction(TypedDict, total=False): 165 | function_full_name: Required[str] 166 | """ 167 | Full name of the dependent function, in the form of 168 | **catalog_name**.**schema_name**.**function_name**. 169 | """ 170 | 171 | 172 | class FunctionInfoRoutineDependenciesDependencyTable(TypedDict, total=False): 173 | table_full_name: Required[str] 174 | """ 175 | Full name of the dependent table, in the form of 176 | **catalog_name**.**schema_name**.**table_name**. 177 | """ 178 | 179 | 180 | class FunctionInfoRoutineDependenciesDependency(TypedDict, total=False): 181 | function: FunctionInfoRoutineDependenciesDependencyFunction 182 | """A function that is dependent on a SQL object.""" 183 | 184 | table: FunctionInfoRoutineDependenciesDependencyTable 185 | """A table that is dependent on a SQL object.""" 186 | 187 | 188 | class FunctionInfoRoutineDependencies(TypedDict, total=False): 189 | dependencies: Iterable[FunctionInfoRoutineDependenciesDependency] 190 | """Array of dependencies.""" 191 | 192 | 193 | class FunctionInfo(TypedDict, total=False): 194 | catalog_name: Required[str] 195 | """Name of parent catalog.""" 196 | 197 | data_type: Required[ 198 | Literal[ 199 | "BOOLEAN", 200 | "BYTE", 201 | "SHORT", 202 | "INT", 203 | "LONG", 204 | "FLOAT", 205 | "DOUBLE", 206 | "DATE", 207 | "TIMESTAMP", 208 | "TIMESTAMP_NTZ", 209 | "STRING", 210 | "BINARY", 211 | "DECIMAL", 212 | "INTERVAL", 213 | "ARRAY", 214 | "STRUCT", 215 | "MAP", 216 | "CHAR", 217 | "NULL", 218 | "USER_DEFINED_TYPE", 219 | "TABLE_TYPE", 220 | ] 221 | ] 222 | """Name of type (INT, STRUCT, MAP, etc.).""" 223 | 224 | full_data_type: Required[str] 225 | """Pretty printed function data type.""" 226 | 227 | input_params: Required[FunctionInfoInputParams] 228 | 229 | is_deterministic: Required[bool] 230 | """Whether the function is deterministic.""" 231 | 232 | is_null_call: Required[bool] 233 | """Function null call.""" 234 | 235 | name: Required[str] 236 | """Name of function, relative to parent schema.""" 237 | 238 | parameter_style: Required[Literal["S"]] 239 | """Function parameter style. **S** is the value for SQL.""" 240 | 241 | properties: Required[str] 242 | """JSON-serialized key-value pair map, encoded (escaped) as a string.""" 243 | 244 | routine_body: Required[Literal["SQL", "EXTERNAL"]] 245 | """Function language. 246 | 247 | When **EXTERNAL** is used, the language of the routine function should be 248 | specified in the **external_language** field, and the **return_params** of the 249 | function cannot be used (as **TABLE** return type is not supported), and the 250 | **sql_data_access** field must be **NO_SQL**. 251 | """ 252 | 253 | routine_definition: Required[str] 254 | """Function body.""" 255 | 256 | schema_name: Required[str] 257 | """Name of parent schema relative to its parent catalog.""" 258 | 259 | security_type: Required[Literal["DEFINER"]] 260 | """Function security type.""" 261 | 262 | specific_name: Required[str] 263 | """Specific name of the function; Reserved for future use.""" 264 | 265 | sql_data_access: Required[Literal["CONTAINS_SQL", "READS_SQL_DATA", "NO_SQL"]] 266 | """Function SQL data access.""" 267 | 268 | comment: str 269 | """User-provided free-form text description.""" 270 | 271 | external_language: str 272 | """External language of the function.""" 273 | 274 | return_params: FunctionInfoReturnParams 275 | 276 | routine_dependencies: FunctionInfoRoutineDependencies 277 | """A list of dependencies.""" 278 | -------------------------------------------------------------------------------- /src/unitycatalog/types/function_info.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__ = [ 9 | "FunctionInfo", 10 | "InputParams", 11 | "InputParamsParameter", 12 | "ReturnParams", 13 | "ReturnParamsParameter", 14 | "RoutineDependencies", 15 | "RoutineDependenciesDependency", 16 | "RoutineDependenciesDependencyFunction", 17 | "RoutineDependenciesDependencyTable", 18 | ] 19 | 20 | 21 | class InputParamsParameter(BaseModel): 22 | name: str 23 | """Name of parameter.""" 24 | 25 | position: int 26 | """Ordinal position of column (starting at position 0).""" 27 | 28 | type_json: str 29 | """Full data type spec, JSON-serialized.""" 30 | 31 | type_name: Literal[ 32 | "BOOLEAN", 33 | "BYTE", 34 | "SHORT", 35 | "INT", 36 | "LONG", 37 | "FLOAT", 38 | "DOUBLE", 39 | "DATE", 40 | "TIMESTAMP", 41 | "TIMESTAMP_NTZ", 42 | "STRING", 43 | "BINARY", 44 | "DECIMAL", 45 | "INTERVAL", 46 | "ARRAY", 47 | "STRUCT", 48 | "MAP", 49 | "CHAR", 50 | "NULL", 51 | "USER_DEFINED_TYPE", 52 | "TABLE_TYPE", 53 | ] 54 | """Name of type (INT, STRUCT, MAP, etc.).""" 55 | 56 | type_text: str 57 | """Full data type spec, SQL/catalogString text.""" 58 | 59 | comment: Optional[str] = None 60 | """User-provided free-form text description.""" 61 | 62 | parameter_default: Optional[str] = None 63 | """Default value of the parameter.""" 64 | 65 | parameter_mode: Optional[Literal["IN"]] = None 66 | """The mode of the function parameter.""" 67 | 68 | parameter_type: Optional[Literal["PARAM", "COLUMN"]] = None 69 | """The type of function parameter.""" 70 | 71 | type_interval_type: Optional[str] = None 72 | """Format of IntervalType.""" 73 | 74 | type_precision: Optional[int] = None 75 | """Digits of precision; required on Create for DecimalTypes.""" 76 | 77 | type_scale: Optional[int] = None 78 | """Digits to right of decimal; Required on Create for DecimalTypes.""" 79 | 80 | 81 | class InputParams(BaseModel): 82 | parameters: Optional[List[InputParamsParameter]] = None 83 | """ 84 | The array of **FunctionParameterInfo** definitions of the function's parameters. 85 | """ 86 | 87 | 88 | class ReturnParamsParameter(BaseModel): 89 | name: str 90 | """Name of parameter.""" 91 | 92 | position: int 93 | """Ordinal position of column (starting at position 0).""" 94 | 95 | type_json: str 96 | """Full data type spec, JSON-serialized.""" 97 | 98 | type_name: Literal[ 99 | "BOOLEAN", 100 | "BYTE", 101 | "SHORT", 102 | "INT", 103 | "LONG", 104 | "FLOAT", 105 | "DOUBLE", 106 | "DATE", 107 | "TIMESTAMP", 108 | "TIMESTAMP_NTZ", 109 | "STRING", 110 | "BINARY", 111 | "DECIMAL", 112 | "INTERVAL", 113 | "ARRAY", 114 | "STRUCT", 115 | "MAP", 116 | "CHAR", 117 | "NULL", 118 | "USER_DEFINED_TYPE", 119 | "TABLE_TYPE", 120 | ] 121 | """Name of type (INT, STRUCT, MAP, etc.).""" 122 | 123 | type_text: str 124 | """Full data type spec, SQL/catalogString text.""" 125 | 126 | comment: Optional[str] = None 127 | """User-provided free-form text description.""" 128 | 129 | parameter_default: Optional[str] = None 130 | """Default value of the parameter.""" 131 | 132 | parameter_mode: Optional[Literal["IN"]] = None 133 | """The mode of the function parameter.""" 134 | 135 | parameter_type: Optional[Literal["PARAM", "COLUMN"]] = None 136 | """The type of function parameter.""" 137 | 138 | type_interval_type: Optional[str] = None 139 | """Format of IntervalType.""" 140 | 141 | type_precision: Optional[int] = None 142 | """Digits of precision; required on Create for DecimalTypes.""" 143 | 144 | type_scale: Optional[int] = None 145 | """Digits to right of decimal; Required on Create for DecimalTypes.""" 146 | 147 | 148 | class ReturnParams(BaseModel): 149 | parameters: Optional[List[ReturnParamsParameter]] = None 150 | """ 151 | The array of **FunctionParameterInfo** definitions of the function's parameters. 152 | """ 153 | 154 | 155 | class RoutineDependenciesDependencyFunction(BaseModel): 156 | function_full_name: str 157 | """ 158 | Full name of the dependent function, in the form of 159 | **catalog_name**.**schema_name**.**function_name**. 160 | """ 161 | 162 | 163 | class RoutineDependenciesDependencyTable(BaseModel): 164 | table_full_name: str 165 | """ 166 | Full name of the dependent table, in the form of 167 | **catalog_name**.**schema_name**.**table_name**. 168 | """ 169 | 170 | 171 | class RoutineDependenciesDependency(BaseModel): 172 | function: Optional[RoutineDependenciesDependencyFunction] = None 173 | """A function that is dependent on a SQL object.""" 174 | 175 | table: Optional[RoutineDependenciesDependencyTable] = None 176 | """A table that is dependent on a SQL object.""" 177 | 178 | 179 | class RoutineDependencies(BaseModel): 180 | dependencies: Optional[List[RoutineDependenciesDependency]] = None 181 | """Array of dependencies.""" 182 | 183 | 184 | class FunctionInfo(BaseModel): 185 | catalog_name: Optional[str] = None 186 | """Name of parent catalog.""" 187 | 188 | comment: Optional[str] = None 189 | """User-provided free-form text description.""" 190 | 191 | created_at: Optional[int] = None 192 | """Time at which this function was created, in epoch milliseconds.""" 193 | 194 | data_type: Optional[ 195 | Literal[ 196 | "BOOLEAN", 197 | "BYTE", 198 | "SHORT", 199 | "INT", 200 | "LONG", 201 | "FLOAT", 202 | "DOUBLE", 203 | "DATE", 204 | "TIMESTAMP", 205 | "TIMESTAMP_NTZ", 206 | "STRING", 207 | "BINARY", 208 | "DECIMAL", 209 | "INTERVAL", 210 | "ARRAY", 211 | "STRUCT", 212 | "MAP", 213 | "CHAR", 214 | "NULL", 215 | "USER_DEFINED_TYPE", 216 | "TABLE_TYPE", 217 | ] 218 | ] = None 219 | """Name of type (INT, STRUCT, MAP, etc.).""" 220 | 221 | external_language: Optional[str] = None 222 | """External language of the function.""" 223 | 224 | full_data_type: Optional[str] = None 225 | """Pretty printed function data type.""" 226 | 227 | full_name: Optional[str] = None 228 | """ 229 | Full name of function, in form of 230 | **catalog_name**.**schema_name**.**function**name\\__\\__ 231 | """ 232 | 233 | function_id: Optional[str] = None 234 | """Id of Function, relative to parent schema.""" 235 | 236 | input_params: Optional[InputParams] = None 237 | 238 | is_deterministic: Optional[bool] = None 239 | """Whether the function is deterministic.""" 240 | 241 | is_null_call: Optional[bool] = None 242 | """Function null call.""" 243 | 244 | name: Optional[str] = None 245 | """Name of function, relative to parent schema.""" 246 | 247 | parameter_style: Optional[Literal["S"]] = None 248 | """Function parameter style. **S** is the value for SQL.""" 249 | 250 | properties: Optional[str] = None 251 | """JSON-serialized key-value pair map, encoded (escaped) as a string.""" 252 | 253 | return_params: Optional[ReturnParams] = None 254 | 255 | routine_body: Optional[Literal["SQL", "EXTERNAL"]] = None 256 | """Function language. 257 | 258 | When **EXTERNAL** is used, the language of the routine function should be 259 | specified in the **external_language** field, and the **return_params** of the 260 | function cannot be used (as **TABLE** return type is not supported), and the 261 | **sql_data_access** field must be **NO_SQL**. 262 | """ 263 | 264 | routine_definition: Optional[str] = None 265 | """Function body.""" 266 | 267 | routine_dependencies: Optional[RoutineDependencies] = None 268 | """A list of dependencies.""" 269 | 270 | schema_name: Optional[str] = None 271 | """Name of parent schema relative to its parent catalog.""" 272 | 273 | security_type: Optional[Literal["DEFINER"]] = None 274 | """Function security type.""" 275 | 276 | specific_name: Optional[str] = None 277 | """Specific name of the function; Reserved for future use.""" 278 | 279 | sql_data_access: Optional[Literal["CONTAINS_SQL", "READS_SQL_DATA", "NO_SQL"]] = None 280 | """Function SQL data access.""" 281 | 282 | updated_at: Optional[int] = None 283 | """Time at which this function was last updated, in epoch milliseconds.""" 284 | -------------------------------------------------------------------------------- /src/unitycatalog/types/function_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 Required, TypedDict 6 | 7 | __all__ = ["FunctionListParams"] 8 | 9 | 10 | class FunctionListParams(TypedDict, total=False): 11 | catalog_name: Required[str] 12 | """Name of parent catalog for functions of interest.""" 13 | 14 | schema_name: Required[str] 15 | """Parent schema of functions.""" 16 | 17 | max_results: int 18 | """Maximum number of functions to return. 19 | 20 | - when set to a value greater than 0, the page length is the minimum of this 21 | value and a server configured value; 22 | - when set to 0, the page length is set to a server configured value; 23 | - when set to a value less than 0, an invalid parameter error is returned; 24 | """ 25 | 26 | page_token: str 27 | """Opaque pagination token to go to next page based on previous query.""" 28 | -------------------------------------------------------------------------------- /src/unitycatalog/types/function_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 .function_info import FunctionInfo 7 | 8 | __all__ = ["FunctionListResponse"] 9 | 10 | 11 | class FunctionListResponse(BaseModel): 12 | functions: Optional[List[FunctionInfo]] = None 13 | """An array of function information objects.""" 14 | 15 | next_page_token: Optional[str] = None 16 | """Opaque token to retrieve the next page of results. 17 | 18 | Absent if there are no more pages. **page_token** should be set to this value 19 | for the next request (for the next page of results). 20 | """ 21 | -------------------------------------------------------------------------------- /src/unitycatalog/types/generate_temporary_table_credential_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__ = ["GenerateTemporaryTableCredentialResponse", "AwsTempCredentials"] 8 | 9 | 10 | class AwsTempCredentials(BaseModel): 11 | access_key_id: Optional[str] = None 12 | """The access key ID that identifies the temporary credentials.""" 13 | 14 | secret_access_key: Optional[str] = None 15 | """The secret access key that can be used to sign AWS API requests.""" 16 | 17 | session_token: Optional[str] = None 18 | """The token that users must pass to AWS API to use the temporary credentials.""" 19 | 20 | 21 | class GenerateTemporaryTableCredentialResponse(BaseModel): 22 | aws_temp_credentials: Optional[AwsTempCredentials] = None 23 | 24 | expiration_time: Optional[int] = None 25 | """ 26 | Server time when the credential will expire, in epoch milliseconds. The API 27 | client is advised to cache the credential given this expiration time. 28 | """ 29 | -------------------------------------------------------------------------------- /src/unitycatalog/types/generate_temporary_volume_credential_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__ = ["GenerateTemporaryVolumeCredentialResponse", "AwsTempCredentials"] 8 | 9 | 10 | class AwsTempCredentials(BaseModel): 11 | access_key_id: Optional[str] = None 12 | """The access key ID that identifies the temporary credentials.""" 13 | 14 | secret_access_key: Optional[str] = None 15 | """The secret access key that can be used to sign AWS API requests.""" 16 | 17 | session_token: Optional[str] = None 18 | """The token that users must pass to AWS API to use the temporary credentials.""" 19 | 20 | 21 | class GenerateTemporaryVolumeCredentialResponse(BaseModel): 22 | aws_temp_credentials: Optional[AwsTempCredentials] = None 23 | 24 | expiration_time: Optional[int] = None 25 | """ 26 | Server time when the credential will expire, in epoch milliseconds. The API 27 | client is advised to cache the credential given this expiration time. 28 | """ 29 | -------------------------------------------------------------------------------- /src/unitycatalog/types/schema_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 6 | from typing_extensions import Required, TypedDict 7 | 8 | __all__ = ["SchemaCreateParams"] 9 | 10 | 11 | class SchemaCreateParams(TypedDict, total=False): 12 | catalog_name: Required[str] 13 | """Name of parent catalog.""" 14 | 15 | name: Required[str] 16 | """Name of schema, relative to parent catalog.""" 17 | 18 | comment: str 19 | """User-provided free-form text description.""" 20 | 21 | properties: Dict[str, str] 22 | """A map of key-value properties attached to the securable.""" 23 | -------------------------------------------------------------------------------- /src/unitycatalog/types/schema_info.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__ = ["SchemaInfo"] 8 | 9 | 10 | class SchemaInfo(BaseModel): 11 | catalog_name: Optional[str] = None 12 | """Name of parent catalog.""" 13 | 14 | comment: Optional[str] = None 15 | """User-provided free-form text description.""" 16 | 17 | created_at: Optional[int] = None 18 | """Time at which this schema was created, in epoch milliseconds.""" 19 | 20 | full_name: Optional[str] = None 21 | """Full name of schema, in form of **catalog_name**.**schema_name**.""" 22 | 23 | name: Optional[str] = None 24 | """Name of schema, relative to parent catalog.""" 25 | 26 | properties: Optional[Dict[str, str]] = None 27 | """A map of key-value properties attached to the securable.""" 28 | 29 | schema_id: Optional[str] = None 30 | """Unique identifier for the schema.""" 31 | 32 | updated_at: Optional[int] = None 33 | """Time at which this schema was last modified, in epoch milliseconds.""" 34 | -------------------------------------------------------------------------------- /src/unitycatalog/types/schema_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 Required, TypedDict 6 | 7 | __all__ = ["SchemaListParams"] 8 | 9 | 10 | class SchemaListParams(TypedDict, total=False): 11 | catalog_name: Required[str] 12 | """Parent catalog for schemas of interest.""" 13 | 14 | max_results: int 15 | """Maximum number of schemas to return. 16 | 17 | - when set to a value greater than 0, the page length is the minimum of this 18 | value and a server configured value; 19 | - when set to 0, the page length is set to a server configured value; 20 | - when set to a value less than 0, an invalid parameter error is returned; 21 | """ 22 | 23 | page_token: str 24 | """Opaque pagination token to go to next page based on previous query.""" 25 | -------------------------------------------------------------------------------- /src/unitycatalog/types/schema_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 .schema_info import SchemaInfo 7 | 8 | __all__ = ["SchemaListResponse"] 9 | 10 | 11 | class SchemaListResponse(BaseModel): 12 | next_page_token: Optional[str] = None 13 | """Opaque token to retrieve the next page of results. 14 | 15 | Absent if there are no more pages. **page_token** should be set to this value 16 | for the next request (for the next page of results). 17 | """ 18 | 19 | schemas: Optional[List[SchemaInfo]] = None 20 | """An array of schema information objects.""" 21 | -------------------------------------------------------------------------------- /src/unitycatalog/types/schema_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 import Dict 6 | from typing_extensions import TypedDict 7 | 8 | __all__ = ["SchemaUpdateParams"] 9 | 10 | 11 | class SchemaUpdateParams(TypedDict, total=False): 12 | comment: str 13 | """User-provided free-form text description.""" 14 | 15 | new_name: str 16 | """New name for the schema.""" 17 | 18 | properties: Dict[str, str] 19 | """A map of key-value properties attached to the securable.""" 20 | -------------------------------------------------------------------------------- /src/unitycatalog/types/table_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, Iterable 6 | from typing_extensions import Literal, Required, TypedDict 7 | 8 | __all__ = ["TableCreateParams", "Column"] 9 | 10 | 11 | class TableCreateParams(TypedDict, total=False): 12 | catalog_name: Required[str] 13 | """Name of parent catalog.""" 14 | 15 | columns: Required[Iterable[Column]] 16 | """The array of **ColumnInfo** definitions of the table's columns.""" 17 | 18 | data_source_format: Required[Literal["DELTA", "CSV", "JSON", "AVRO", "PARQUET", "ORC", "TEXT"]] 19 | """Data source format""" 20 | 21 | name: Required[str] 22 | """Name of table, relative to parent schema.""" 23 | 24 | schema_name: Required[str] 25 | """Name of parent schema relative to its parent catalog.""" 26 | 27 | table_type: Required[Literal["MANAGED", "EXTERNAL"]] 28 | 29 | comment: str 30 | """User-provided free-form text description.""" 31 | 32 | properties: Dict[str, str] 33 | """A map of key-value properties attached to the securable.""" 34 | 35 | storage_location: str 36 | """Storage root URL for table (for **MANAGED**, **EXTERNAL** tables)""" 37 | 38 | 39 | class Column(TypedDict, total=False): 40 | comment: str 41 | """User-provided free-form text description.""" 42 | 43 | name: str 44 | """Name of Column.""" 45 | 46 | nullable: bool 47 | """Whether field may be Null.""" 48 | 49 | partition_index: int 50 | """Partition index for column.""" 51 | 52 | position: int 53 | """Ordinal position of column (starting at position 0).""" 54 | 55 | type_interval_type: str 56 | """Format of IntervalType.""" 57 | 58 | type_json: str 59 | """Full data type specification, JSON-serialized.""" 60 | 61 | type_name: Literal[ 62 | "BOOLEAN", 63 | "BYTE", 64 | "SHORT", 65 | "INT", 66 | "LONG", 67 | "FLOAT", 68 | "DOUBLE", 69 | "DATE", 70 | "TIMESTAMP", 71 | "TIMESTAMP_NTZ", 72 | "STRING", 73 | "BINARY", 74 | "DECIMAL", 75 | "INTERVAL", 76 | "ARRAY", 77 | "STRUCT", 78 | "MAP", 79 | "CHAR", 80 | "NULL", 81 | "USER_DEFINED_TYPE", 82 | "TABLE_TYPE", 83 | ] 84 | """Name of type (INT, STRUCT, MAP, etc.).""" 85 | 86 | type_precision: int 87 | """Digits of precision; required for DecimalTypes.""" 88 | 89 | type_scale: int 90 | """Digits to right of decimal; Required for DecimalTypes.""" 91 | 92 | type_text: str 93 | """Full data type specification as SQL/catalogString text.""" 94 | -------------------------------------------------------------------------------- /src/unitycatalog/types/table_info.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 | 8 | __all__ = ["TableInfo", "Column"] 9 | 10 | 11 | class Column(BaseModel): 12 | comment: Optional[str] = None 13 | """User-provided free-form text description.""" 14 | 15 | name: Optional[str] = None 16 | """Name of Column.""" 17 | 18 | nullable: Optional[bool] = None 19 | """Whether field may be Null.""" 20 | 21 | partition_index: Optional[int] = None 22 | """Partition index for column.""" 23 | 24 | position: Optional[int] = None 25 | """Ordinal position of column (starting at position 0).""" 26 | 27 | type_interval_type: Optional[str] = None 28 | """Format of IntervalType.""" 29 | 30 | type_json: Optional[str] = None 31 | """Full data type specification, JSON-serialized.""" 32 | 33 | type_name: Optional[ 34 | Literal[ 35 | "BOOLEAN", 36 | "BYTE", 37 | "SHORT", 38 | "INT", 39 | "LONG", 40 | "FLOAT", 41 | "DOUBLE", 42 | "DATE", 43 | "TIMESTAMP", 44 | "TIMESTAMP_NTZ", 45 | "STRING", 46 | "BINARY", 47 | "DECIMAL", 48 | "INTERVAL", 49 | "ARRAY", 50 | "STRUCT", 51 | "MAP", 52 | "CHAR", 53 | "NULL", 54 | "USER_DEFINED_TYPE", 55 | "TABLE_TYPE", 56 | ] 57 | ] = None 58 | """Name of type (INT, STRUCT, MAP, etc.).""" 59 | 60 | type_precision: Optional[int] = None 61 | """Digits of precision; required for DecimalTypes.""" 62 | 63 | type_scale: Optional[int] = None 64 | """Digits to right of decimal; Required for DecimalTypes.""" 65 | 66 | type_text: Optional[str] = None 67 | """Full data type specification as SQL/catalogString text.""" 68 | 69 | 70 | class TableInfo(BaseModel): 71 | catalog_name: Optional[str] = None 72 | """Name of parent catalog.""" 73 | 74 | columns: Optional[List[Column]] = None 75 | """The array of **ColumnInfo** definitions of the table's columns.""" 76 | 77 | comment: Optional[str] = None 78 | """User-provided free-form text description.""" 79 | 80 | created_at: Optional[int] = None 81 | """Time at which this table was created, in epoch milliseconds.""" 82 | 83 | data_source_format: Optional[Literal["DELTA", "CSV", "JSON", "AVRO", "PARQUET", "ORC", "TEXT"]] = None 84 | """Data source format""" 85 | 86 | name: Optional[str] = None 87 | """Name of table, relative to parent schema.""" 88 | 89 | properties: Optional[Dict[str, str]] = None 90 | """A map of key-value properties attached to the securable.""" 91 | 92 | schema_name: Optional[str] = None 93 | """Name of parent schema relative to its parent catalog.""" 94 | 95 | storage_location: Optional[str] = None 96 | """Storage root URL for table (for **MANAGED**, **EXTERNAL** tables)""" 97 | 98 | table_id: Optional[str] = None 99 | """Unique identifier for the table.""" 100 | 101 | table_type: Optional[Literal["MANAGED", "EXTERNAL"]] = None 102 | 103 | updated_at: Optional[int] = None 104 | """Time at which this table was last modified, in epoch milliseconds.""" 105 | -------------------------------------------------------------------------------- /src/unitycatalog/types/table_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 Required, TypedDict 6 | 7 | __all__ = ["TableListParams"] 8 | 9 | 10 | class TableListParams(TypedDict, total=False): 11 | catalog_name: Required[str] 12 | """Name of parent catalog for tables of interest.""" 13 | 14 | schema_name: Required[str] 15 | """Parent schema of tables.""" 16 | 17 | max_results: int 18 | """Maximum number of tables to return. 19 | 20 | - when set to a value greater than 0, the page length is the minimum of this 21 | value and a server configured value; 22 | - when set to 0, the page length is set to a server configured value; 23 | - when set to a value less than 0, an invalid parameter error is returned; 24 | """ 25 | 26 | page_token: str 27 | """Opaque token to send for the next page of results (pagination).""" 28 | -------------------------------------------------------------------------------- /src/unitycatalog/types/table_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 .table_info import TableInfo 7 | 8 | __all__ = ["TableListResponse"] 9 | 10 | 11 | class TableListResponse(BaseModel): 12 | next_page_token: Optional[str] = None 13 | """Opaque token to retrieve the next page of results. 14 | 15 | Absent if there are no more pages. **page_token** should be set to this value 16 | for the next request (for the next page of results). 17 | """ 18 | 19 | tables: Optional[List[TableInfo]] = None 20 | """An array of table information objects.""" 21 | -------------------------------------------------------------------------------- /src/unitycatalog/types/temporary_table_credential_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 Literal, Required, TypedDict 6 | 7 | __all__ = ["TemporaryTableCredentialCreateParams"] 8 | 9 | 10 | class TemporaryTableCredentialCreateParams(TypedDict, total=False): 11 | operation: Required[Literal["UNKNOWN_TABLE_OPERATION", "READ", "READ_WRITE"]] 12 | 13 | table_id: Required[str] 14 | """Table id for which temporary credentials are generated. 15 | 16 | Can be obtained from tables/{full_name} (get table info) API. 17 | """ 18 | -------------------------------------------------------------------------------- /src/unitycatalog/types/temporary_volume_credential_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 Literal, Required, TypedDict 6 | 7 | __all__ = ["TemporaryVolumeCredentialCreateParams"] 8 | 9 | 10 | class TemporaryVolumeCredentialCreateParams(TypedDict, total=False): 11 | operation: Required[Literal["UNKNOWN_VOLUME_OPERATION", "READ_VOLUME", "WRITE_VOLUME"]] 12 | 13 | volume_id: Required[str] 14 | """Volume id for which temporary credentials are generated. 15 | 16 | Can be obtained from volumes/{full_name} (get volume info) API. 17 | """ 18 | -------------------------------------------------------------------------------- /src/unitycatalog/types/volume_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 Literal, Required, TypedDict 6 | 7 | __all__ = ["VolumeCreateParams"] 8 | 9 | 10 | class VolumeCreateParams(TypedDict, total=False): 11 | catalog_name: Required[str] 12 | """The name of the catalog where the schema and the volume are""" 13 | 14 | name: Required[str] 15 | """The name of the volume""" 16 | 17 | schema_name: Required[str] 18 | """The name of the schema where the volume is""" 19 | 20 | storage_location: Required[str] 21 | """The storage location of the volume""" 22 | 23 | volume_type: Required[Literal["MANAGED", "EXTERNAL"]] 24 | """The type of the volume""" 25 | 26 | comment: str 27 | """The comment attached to the volume""" 28 | -------------------------------------------------------------------------------- /src/unitycatalog/types/volume_info.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__ = ["VolumeInfo"] 9 | 10 | 11 | class VolumeInfo(BaseModel): 12 | catalog_name: Optional[str] = None 13 | """The name of the catalog where the schema and the volume are""" 14 | 15 | comment: Optional[str] = None 16 | """The comment attached to the volume""" 17 | 18 | created_at: Optional[int] = None 19 | """Time at which this volume was created, in epoch milliseconds.""" 20 | 21 | full_name: Optional[str] = None 22 | """ 23 | Full name of volume, in form of 24 | **catalog_name**.**schema_name**.**volume_name**. 25 | """ 26 | 27 | name: Optional[str] = None 28 | """The name of the volume""" 29 | 30 | schema_name: Optional[str] = None 31 | """The name of the schema where the volume is""" 32 | 33 | storage_location: Optional[str] = None 34 | """The storage location of the volume""" 35 | 36 | updated_at: Optional[int] = None 37 | """Time at which this volume was last modified, in epoch milliseconds.""" 38 | 39 | volume_id: Optional[str] = None 40 | """Unique identifier for the volume""" 41 | 42 | volume_type: Optional[Literal["MANAGED", "EXTERNAL"]] = None 43 | """The type of the volume""" 44 | -------------------------------------------------------------------------------- /src/unitycatalog/types/volume_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 Required, TypedDict 6 | 7 | __all__ = ["VolumeListParams"] 8 | 9 | 10 | class VolumeListParams(TypedDict, total=False): 11 | catalog_name: Required[str] 12 | """The identifier of the catalog""" 13 | 14 | schema_name: Required[str] 15 | """The identifier of the schema""" 16 | 17 | max_results: int 18 | """Maximum number of volumes to return (page length). 19 | 20 | If not set, the page length is set to a server configured value. 21 | 22 | - when set to a value greater than 0, the page length is the minimum of this 23 | value and a server configured value; 24 | - when set to 0, the page length is set to a server configured value; 25 | - when set to a value less than 0, an invalid parameter error is returned; 26 | 27 | Note: this parameter controls only the maximum number of volumes to return. The 28 | actual number of volumes returned in a page may be smaller than this value, 29 | including 0, even if there are more pages. 30 | """ 31 | 32 | page_token: str 33 | """Opaque token returned by a previous request. 34 | 35 | It must be included in the request to retrieve the next page of results 36 | (pagination). 37 | """ 38 | -------------------------------------------------------------------------------- /src/unitycatalog/types/volume_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 .volume_info import VolumeInfo 7 | 8 | __all__ = ["VolumeListResponse"] 9 | 10 | 11 | class VolumeListResponse(BaseModel): 12 | next_page_token: Optional[str] = None 13 | """Opaque token to retrieve the next page of results. 14 | 15 | Absent if there are no more pages. **page_token** should be set to this value 16 | for the next request to retrieve the next page of results. 17 | """ 18 | 19 | volumes: Optional[List[VolumeInfo]] = None 20 | -------------------------------------------------------------------------------- /src/unitycatalog/types/volume_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__ = ["VolumeUpdateParams"] 8 | 9 | 10 | class VolumeUpdateParams(TypedDict, total=False): 11 | comment: str 12 | """The comment attached to the volume""" 13 | 14 | new_name: str 15 | """New name for the volume.""" 16 | -------------------------------------------------------------------------------- /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/test_temporary_table_credentials.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 tests.utils import assert_matches_type 11 | from unitycatalog import Unitycatalog, AsyncUnitycatalog 12 | from unitycatalog.types import GenerateTemporaryTableCredentialResponse 13 | 14 | base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") 15 | 16 | 17 | class TestTemporaryTableCredentials: 18 | parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) 19 | 20 | @parametrize 21 | def test_method_create(self, client: Unitycatalog) -> None: 22 | temporary_table_credential = client.temporary_table_credentials.create( 23 | operation="UNKNOWN_TABLE_OPERATION", 24 | table_id="table_id", 25 | ) 26 | assert_matches_type(GenerateTemporaryTableCredentialResponse, temporary_table_credential, path=["response"]) 27 | 28 | @parametrize 29 | def test_raw_response_create(self, client: Unitycatalog) -> None: 30 | response = client.temporary_table_credentials.with_raw_response.create( 31 | operation="UNKNOWN_TABLE_OPERATION", 32 | table_id="table_id", 33 | ) 34 | 35 | assert response.is_closed is True 36 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 37 | temporary_table_credential = response.parse() 38 | assert_matches_type(GenerateTemporaryTableCredentialResponse, temporary_table_credential, path=["response"]) 39 | 40 | @parametrize 41 | def test_streaming_response_create(self, client: Unitycatalog) -> None: 42 | with client.temporary_table_credentials.with_streaming_response.create( 43 | operation="UNKNOWN_TABLE_OPERATION", 44 | table_id="table_id", 45 | ) as response: 46 | assert not response.is_closed 47 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 48 | 49 | temporary_table_credential = response.parse() 50 | assert_matches_type(GenerateTemporaryTableCredentialResponse, temporary_table_credential, path=["response"]) 51 | 52 | assert cast(Any, response.is_closed) is True 53 | 54 | 55 | class TestAsyncTemporaryTableCredentials: 56 | parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) 57 | 58 | @parametrize 59 | async def test_method_create(self, async_client: AsyncUnitycatalog) -> None: 60 | temporary_table_credential = await async_client.temporary_table_credentials.create( 61 | operation="UNKNOWN_TABLE_OPERATION", 62 | table_id="table_id", 63 | ) 64 | assert_matches_type(GenerateTemporaryTableCredentialResponse, temporary_table_credential, path=["response"]) 65 | 66 | @parametrize 67 | async def test_raw_response_create(self, async_client: AsyncUnitycatalog) -> None: 68 | response = await async_client.temporary_table_credentials.with_raw_response.create( 69 | operation="UNKNOWN_TABLE_OPERATION", 70 | table_id="table_id", 71 | ) 72 | 73 | assert response.is_closed is True 74 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 75 | temporary_table_credential = await response.parse() 76 | assert_matches_type(GenerateTemporaryTableCredentialResponse, temporary_table_credential, path=["response"]) 77 | 78 | @parametrize 79 | async def test_streaming_response_create(self, async_client: AsyncUnitycatalog) -> None: 80 | async with async_client.temporary_table_credentials.with_streaming_response.create( 81 | operation="UNKNOWN_TABLE_OPERATION", 82 | table_id="table_id", 83 | ) as response: 84 | assert not response.is_closed 85 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 86 | 87 | temporary_table_credential = await response.parse() 88 | assert_matches_type(GenerateTemporaryTableCredentialResponse, temporary_table_credential, path=["response"]) 89 | 90 | assert cast(Any, response.is_closed) is True 91 | -------------------------------------------------------------------------------- /tests/api_resources/test_temporary_volume_credentials.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 tests.utils import assert_matches_type 11 | from unitycatalog import Unitycatalog, AsyncUnitycatalog 12 | from unitycatalog.types import GenerateTemporaryVolumeCredentialResponse 13 | 14 | base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") 15 | 16 | 17 | class TestTemporaryVolumeCredentials: 18 | parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) 19 | 20 | @parametrize 21 | def test_method_create(self, client: Unitycatalog) -> None: 22 | temporary_volume_credential = client.temporary_volume_credentials.create( 23 | operation="UNKNOWN_VOLUME_OPERATION", 24 | volume_id="string", 25 | ) 26 | assert_matches_type(GenerateTemporaryVolumeCredentialResponse, temporary_volume_credential, path=["response"]) 27 | 28 | @parametrize 29 | def test_raw_response_create(self, client: Unitycatalog) -> None: 30 | response = client.temporary_volume_credentials.with_raw_response.create( 31 | operation="UNKNOWN_VOLUME_OPERATION", 32 | volume_id="string", 33 | ) 34 | 35 | assert response.is_closed is True 36 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 37 | temporary_volume_credential = response.parse() 38 | assert_matches_type(GenerateTemporaryVolumeCredentialResponse, temporary_volume_credential, path=["response"]) 39 | 40 | @parametrize 41 | def test_streaming_response_create(self, client: Unitycatalog) -> None: 42 | with client.temporary_volume_credentials.with_streaming_response.create( 43 | operation="UNKNOWN_VOLUME_OPERATION", 44 | volume_id="string", 45 | ) as response: 46 | assert not response.is_closed 47 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 48 | 49 | temporary_volume_credential = response.parse() 50 | assert_matches_type( 51 | GenerateTemporaryVolumeCredentialResponse, temporary_volume_credential, path=["response"] 52 | ) 53 | 54 | assert cast(Any, response.is_closed) is True 55 | 56 | 57 | class TestAsyncTemporaryVolumeCredentials: 58 | parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) 59 | 60 | @parametrize 61 | async def test_method_create(self, async_client: AsyncUnitycatalog) -> None: 62 | temporary_volume_credential = await async_client.temporary_volume_credentials.create( 63 | operation="UNKNOWN_VOLUME_OPERATION", 64 | volume_id="string", 65 | ) 66 | assert_matches_type(GenerateTemporaryVolumeCredentialResponse, temporary_volume_credential, path=["response"]) 67 | 68 | @parametrize 69 | async def test_raw_response_create(self, async_client: AsyncUnitycatalog) -> None: 70 | response = await async_client.temporary_volume_credentials.with_raw_response.create( 71 | operation="UNKNOWN_VOLUME_OPERATION", 72 | volume_id="string", 73 | ) 74 | 75 | assert response.is_closed is True 76 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 77 | temporary_volume_credential = await response.parse() 78 | assert_matches_type(GenerateTemporaryVolumeCredentialResponse, temporary_volume_credential, path=["response"]) 79 | 80 | @parametrize 81 | async def test_streaming_response_create(self, async_client: AsyncUnitycatalog) -> None: 82 | async with async_client.temporary_volume_credentials.with_streaming_response.create( 83 | operation="UNKNOWN_VOLUME_OPERATION", 84 | volume_id="string", 85 | ) as response: 86 | assert not response.is_closed 87 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 88 | 89 | temporary_volume_credential = await response.parse() 90 | assert_matches_type( 91 | GenerateTemporaryVolumeCredentialResponse, temporary_volume_credential, path=["response"] 92 | ) 93 | 94 | assert cast(Any, response.is_closed) is True 95 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import asyncio 5 | import logging 6 | from typing import TYPE_CHECKING, Iterator, AsyncIterator 7 | 8 | import pytest 9 | 10 | from unitycatalog import Unitycatalog, AsyncUnitycatalog 11 | 12 | if TYPE_CHECKING: 13 | from _pytest.fixtures import FixtureRequest 14 | 15 | pytest.register_assert_rewrite("tests.utils") 16 | 17 | logging.getLogger("unitycatalog").setLevel(logging.DEBUG) 18 | 19 | 20 | @pytest.fixture(scope="session") 21 | def event_loop() -> Iterator[asyncio.AbstractEventLoop]: 22 | loop = asyncio.new_event_loop() 23 | yield loop 24 | loop.close() 25 | 26 | 27 | base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") 28 | 29 | 30 | @pytest.fixture(scope="session") 31 | def client(request: FixtureRequest) -> Iterator[Unitycatalog]: 32 | strict = getattr(request, "param", True) 33 | if not isinstance(strict, bool): 34 | raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") 35 | 36 | with Unitycatalog(base_url=base_url, _strict_response_validation=strict) as client: 37 | yield client 38 | 39 | 40 | @pytest.fixture(scope="session") 41 | async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncUnitycatalog]: 42 | strict = getattr(request, "param", True) 43 | if not isinstance(strict, bool): 44 | raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") 45 | 46 | async with AsyncUnitycatalog(base_url=base_url, _strict_response_validation=strict) as client: 47 | yield client 48 | -------------------------------------------------------------------------------- /tests/sample_file.txt: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | -------------------------------------------------------------------------------- /tests/test_deepcopy.py: -------------------------------------------------------------------------------- 1 | from unitycatalog._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 | 48 | def test_ignores_other_types() -> None: 49 | # custom classes 50 | my_obj = MyObject() 51 | obj1 = {"foo": my_obj} 52 | obj2 = deepcopy_minimal(obj1) 53 | assert_different_identities(obj1, obj2) 54 | assert obj1["foo"] is my_obj 55 | 56 | # tuples 57 | obj3 = ("a", "b") 58 | obj4 = deepcopy_minimal(obj3) 59 | assert obj3 is obj4 60 | -------------------------------------------------------------------------------- /tests/test_extract_files.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Sequence 4 | 5 | import pytest 6 | 7 | from unitycatalog._types import FileTypes 8 | from unitycatalog._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 unitycatalog._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 unitycatalog._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 unitycatalog._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_response.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import List, cast 3 | from typing_extensions import Annotated 4 | 5 | import httpx 6 | import pytest 7 | import pydantic 8 | 9 | from unitycatalog import BaseModel, Unitycatalog, AsyncUnitycatalog 10 | from unitycatalog._response import ( 11 | APIResponse, 12 | BaseAPIResponse, 13 | AsyncAPIResponse, 14 | BinaryAPIResponse, 15 | AsyncBinaryAPIResponse, 16 | extract_response_type, 17 | ) 18 | from unitycatalog._streaming import Stream 19 | from unitycatalog._base_client import FinalRequestOptions 20 | 21 | 22 | class ConcreteBaseAPIResponse(APIResponse[bytes]): 23 | ... 24 | 25 | 26 | class ConcreteAPIResponse(APIResponse[List[str]]): 27 | ... 28 | 29 | 30 | class ConcreteAsyncAPIResponse(APIResponse[httpx.Response]): 31 | ... 32 | 33 | 34 | def test_extract_response_type_direct_classes() -> None: 35 | assert extract_response_type(BaseAPIResponse[str]) == str 36 | assert extract_response_type(APIResponse[str]) == str 37 | assert extract_response_type(AsyncAPIResponse[str]) == str 38 | 39 | 40 | def test_extract_response_type_direct_class_missing_type_arg() -> None: 41 | with pytest.raises( 42 | RuntimeError, 43 | match="Expected type to have a type argument at index 0 but it did not", 44 | ): 45 | extract_response_type(AsyncAPIResponse) 46 | 47 | 48 | def test_extract_response_type_concrete_subclasses() -> None: 49 | assert extract_response_type(ConcreteBaseAPIResponse) == bytes 50 | assert extract_response_type(ConcreteAPIResponse) == List[str] 51 | assert extract_response_type(ConcreteAsyncAPIResponse) == httpx.Response 52 | 53 | 54 | def test_extract_response_type_binary_response() -> None: 55 | assert extract_response_type(BinaryAPIResponse) == bytes 56 | assert extract_response_type(AsyncBinaryAPIResponse) == bytes 57 | 58 | 59 | class PydanticModel(pydantic.BaseModel): 60 | ... 61 | 62 | 63 | def test_response_parse_mismatched_basemodel(client: Unitycatalog) -> None: 64 | response = APIResponse( 65 | raw=httpx.Response(200, content=b"foo"), 66 | client=client, 67 | stream=False, 68 | stream_cls=None, 69 | cast_to=str, 70 | options=FinalRequestOptions.construct(method="get", url="/foo"), 71 | ) 72 | 73 | with pytest.raises( 74 | TypeError, 75 | match="Pydantic models must subclass our base model type, e.g. `from unitycatalog import BaseModel`", 76 | ): 77 | response.parse(to=PydanticModel) 78 | 79 | 80 | @pytest.mark.asyncio 81 | async def test_async_response_parse_mismatched_basemodel(async_client: AsyncUnitycatalog) -> None: 82 | response = AsyncAPIResponse( 83 | raw=httpx.Response(200, content=b"foo"), 84 | client=async_client, 85 | stream=False, 86 | stream_cls=None, 87 | cast_to=str, 88 | options=FinalRequestOptions.construct(method="get", url="/foo"), 89 | ) 90 | 91 | with pytest.raises( 92 | TypeError, 93 | match="Pydantic models must subclass our base model type, e.g. `from unitycatalog import BaseModel`", 94 | ): 95 | await response.parse(to=PydanticModel) 96 | 97 | 98 | def test_response_parse_custom_stream(client: Unitycatalog) -> None: 99 | response = APIResponse( 100 | raw=httpx.Response(200, content=b"foo"), 101 | client=client, 102 | stream=True, 103 | stream_cls=None, 104 | cast_to=str, 105 | options=FinalRequestOptions.construct(method="get", url="/foo"), 106 | ) 107 | 108 | stream = response.parse(to=Stream[int]) 109 | assert stream._cast_to == int 110 | 111 | 112 | @pytest.mark.asyncio 113 | async def test_async_response_parse_custom_stream(async_client: AsyncUnitycatalog) -> None: 114 | response = AsyncAPIResponse( 115 | raw=httpx.Response(200, content=b"foo"), 116 | client=async_client, 117 | stream=True, 118 | stream_cls=None, 119 | cast_to=str, 120 | options=FinalRequestOptions.construct(method="get", url="/foo"), 121 | ) 122 | 123 | stream = await response.parse(to=Stream[int]) 124 | assert stream._cast_to == int 125 | 126 | 127 | class CustomModel(BaseModel): 128 | foo: str 129 | bar: int 130 | 131 | 132 | def test_response_parse_custom_model(client: Unitycatalog) -> None: 133 | response = APIResponse( 134 | raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), 135 | client=client, 136 | stream=False, 137 | stream_cls=None, 138 | cast_to=str, 139 | options=FinalRequestOptions.construct(method="get", url="/foo"), 140 | ) 141 | 142 | obj = response.parse(to=CustomModel) 143 | assert obj.foo == "hello!" 144 | assert obj.bar == 2 145 | 146 | 147 | @pytest.mark.asyncio 148 | async def test_async_response_parse_custom_model(async_client: AsyncUnitycatalog) -> None: 149 | response = AsyncAPIResponse( 150 | raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), 151 | client=async_client, 152 | stream=False, 153 | stream_cls=None, 154 | cast_to=str, 155 | options=FinalRequestOptions.construct(method="get", url="/foo"), 156 | ) 157 | 158 | obj = await response.parse(to=CustomModel) 159 | assert obj.foo == "hello!" 160 | assert obj.bar == 2 161 | 162 | 163 | def test_response_parse_annotated_type(client: Unitycatalog) -> None: 164 | response = APIResponse( 165 | raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), 166 | client=client, 167 | stream=False, 168 | stream_cls=None, 169 | cast_to=str, 170 | options=FinalRequestOptions.construct(method="get", url="/foo"), 171 | ) 172 | 173 | obj = response.parse( 174 | to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]), 175 | ) 176 | assert obj.foo == "hello!" 177 | assert obj.bar == 2 178 | 179 | 180 | async def test_async_response_parse_annotated_type(async_client: AsyncUnitycatalog) -> None: 181 | response = AsyncAPIResponse( 182 | raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), 183 | client=async_client, 184 | stream=False, 185 | stream_cls=None, 186 | cast_to=str, 187 | options=FinalRequestOptions.construct(method="get", url="/foo"), 188 | ) 189 | 190 | obj = await response.parse( 191 | to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]), 192 | ) 193 | assert obj.foo == "hello!" 194 | assert obj.bar == 2 195 | -------------------------------------------------------------------------------- /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 unitycatalog import Unitycatalog, AsyncUnitycatalog 9 | from unitycatalog._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: Unitycatalog, async_client: AsyncUnitycatalog) -> 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: Unitycatalog, async_client: AsyncUnitycatalog) -> 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: Unitycatalog, async_client: AsyncUnitycatalog) -> 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: Unitycatalog, async_client: AsyncUnitycatalog) -> 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: Unitycatalog, async_client: AsyncUnitycatalog) -> 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( 110 | sync: bool, client: Unitycatalog, async_client: AsyncUnitycatalog 111 | ) -> None: 112 | def body() -> Iterator[bytes]: 113 | yield b"event: ping\n" 114 | yield b"data: {\n" 115 | yield b'data: "foo":\n' 116 | yield b"data: \n" 117 | yield b"data:\n" 118 | yield b"data: true}\n" 119 | yield b"\n\n" 120 | 121 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 122 | 123 | sse = await iter_next(iterator) 124 | assert sse.event == "ping" 125 | assert sse.json() == {"foo": True} 126 | assert sse.data == '{\n"foo":\n\n\ntrue}' 127 | 128 | await assert_empty_iter(iterator) 129 | 130 | 131 | @pytest.mark.asyncio 132 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 133 | async def test_data_json_escaped_double_new_line( 134 | sync: bool, client: Unitycatalog, async_client: AsyncUnitycatalog 135 | ) -> None: 136 | def body() -> Iterator[bytes]: 137 | yield b"event: ping\n" 138 | yield b'data: {"foo": "my long\\n\\ncontent"}' 139 | yield b"\n\n" 140 | 141 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 142 | 143 | sse = await iter_next(iterator) 144 | assert sse.event == "ping" 145 | assert sse.json() == {"foo": "my long\n\ncontent"} 146 | 147 | await assert_empty_iter(iterator) 148 | 149 | 150 | @pytest.mark.asyncio 151 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 152 | async def test_multiple_data_lines(sync: bool, client: Unitycatalog, async_client: AsyncUnitycatalog) -> None: 153 | def body() -> Iterator[bytes]: 154 | yield b"event: ping\n" 155 | yield b"data: {\n" 156 | yield b'data: "foo":\n' 157 | yield b"data: true}\n" 158 | yield b"\n\n" 159 | 160 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 161 | 162 | sse = await iter_next(iterator) 163 | assert sse.event == "ping" 164 | assert sse.json() == {"foo": True} 165 | 166 | await assert_empty_iter(iterator) 167 | 168 | 169 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 170 | async def test_special_new_line_character( 171 | sync: bool, 172 | client: Unitycatalog, 173 | async_client: AsyncUnitycatalog, 174 | ) -> None: 175 | def body() -> Iterator[bytes]: 176 | yield b'data: {"content":" culpa"}\n' 177 | yield b"\n" 178 | yield b'data: {"content":" \xe2\x80\xa8"}\n' 179 | yield b"\n" 180 | yield b'data: {"content":"foo"}\n' 181 | yield b"\n" 182 | 183 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 184 | 185 | sse = await iter_next(iterator) 186 | assert sse.event is None 187 | assert sse.json() == {"content": " culpa"} 188 | 189 | sse = await iter_next(iterator) 190 | assert sse.event is None 191 | assert sse.json() == {"content": " 
"} 192 | 193 | sse = await iter_next(iterator) 194 | assert sse.event is None 195 | assert sse.json() == {"content": "foo"} 196 | 197 | await assert_empty_iter(iterator) 198 | 199 | 200 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 201 | async def test_multi_byte_character_multiple_chunks( 202 | sync: bool, 203 | client: Unitycatalog, 204 | async_client: AsyncUnitycatalog, 205 | ) -> None: 206 | def body() -> Iterator[bytes]: 207 | yield b'data: {"content":"' 208 | # bytes taken from the string 'известни' and arbitrarily split 209 | # so that some multi-byte characters span multiple chunks 210 | yield b"\xd0" 211 | yield b"\xb8\xd0\xb7\xd0" 212 | yield b"\xb2\xd0\xb5\xd1\x81\xd1\x82\xd0\xbd\xd0\xb8" 213 | yield b'"}\n' 214 | yield b"\n" 215 | 216 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 217 | 218 | sse = await iter_next(iterator) 219 | assert sse.event is None 220 | assert sse.json() == {"content": "известни"} 221 | 222 | 223 | async def to_aiter(iter: Iterator[bytes]) -> AsyncIterator[bytes]: 224 | for chunk in iter: 225 | yield chunk 226 | 227 | 228 | async def iter_next(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> ServerSentEvent: 229 | if isinstance(iter, AsyncIterator): 230 | return await iter.__anext__() 231 | 232 | return next(iter) 233 | 234 | 235 | async def assert_empty_iter(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> None: 236 | with pytest.raises((StopAsyncIteration, RuntimeError)): 237 | await iter_next(iter) 238 | 239 | 240 | def make_event_iterator( 241 | content: Iterator[bytes], 242 | *, 243 | sync: bool, 244 | client: Unitycatalog, 245 | async_client: AsyncUnitycatalog, 246 | ) -> Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]: 247 | if sync: 248 | return Stream(cast_to=object, client=client, response=httpx.Response(200, content=content))._iter_events() 249 | 250 | return AsyncStream( 251 | cast_to=object, client=async_client, response=httpx.Response(200, content=to_aiter(content)) 252 | )._iter_events() 253 | -------------------------------------------------------------------------------- /tests/test_utils/test_proxy.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from typing import Any 3 | from typing_extensions import override 4 | 5 | from unitycatalog._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 | -------------------------------------------------------------------------------- /tests/test_utils/test_typing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Generic, TypeVar, cast 4 | 5 | from unitycatalog._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 | 16 | class SubclassGeneric(BaseGeneric[_T]): 17 | ... 18 | 19 | 20 | class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]): 21 | ... 22 | 23 | 24 | class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]): 25 | ... 26 | 27 | 28 | class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]): 29 | ... 30 | 31 | 32 | def test_extract_type_var() -> None: 33 | assert ( 34 | extract_type_var_from_base( 35 | BaseGeneric[int], 36 | index=0, 37 | generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), 38 | ) 39 | == int 40 | ) 41 | 42 | 43 | def test_extract_type_var_generic_subclass() -> None: 44 | assert ( 45 | extract_type_var_from_base( 46 | SubclassGeneric[int], 47 | index=0, 48 | generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), 49 | ) 50 | == int 51 | ) 52 | 53 | 54 | def test_extract_type_var_multiple() -> None: 55 | typ = BaseGenericMultipleTypeArgs[int, str, None] 56 | 57 | generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) 58 | assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int 59 | assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str 60 | assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) 61 | 62 | 63 | def test_extract_type_var_generic_subclass_multiple() -> None: 64 | typ = SubclassGenericMultipleTypeArgs[int, str, None] 65 | 66 | generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) 67 | assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int 68 | assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str 69 | assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) 70 | 71 | 72 | def test_extract_type_var_generic_subclass_different_ordering_multiple() -> None: 73 | typ = SubclassDifferentOrderGenericMultipleTypeArgs[int, str, None] 74 | 75 | generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) 76 | assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int 77 | assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str 78 | assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) 79 | -------------------------------------------------------------------------------- /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 unitycatalog._types import NoneType 12 | from unitycatalog._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 | ) 20 | from unitycatalog._compat import PYDANTIC_V2, field_outer_type, get_model_fields 21 | from unitycatalog._models import BaseModel 22 | 23 | BaseModelT = TypeVar("BaseModelT", bound=BaseModel) 24 | 25 | 26 | def assert_matches_model(model: type[BaseModelT], value: BaseModelT, *, path: list[str]) -> bool: 27 | for name, field in get_model_fields(model).items(): 28 | field_value = getattr(value, name) 29 | if PYDANTIC_V2: 30 | allow_none = False 31 | else: 32 | # in v1 nullability was structured differently 33 | # https://docs.pydantic.dev/2.0/migration/#required-optional-and-nullable-fields 34 | allow_none = getattr(field, "allow_none", False) 35 | 36 | assert_matches_type( 37 | field_outer_type(field), 38 | field_value, 39 | path=[*path, name], 40 | allow_none=allow_none, 41 | ) 42 | 43 | return True 44 | 45 | 46 | # Note: the `path` argument is only used to improve error messages when `--showlocals` is used 47 | def assert_matches_type( 48 | type_: Any, 49 | value: object, 50 | *, 51 | path: list[str], 52 | allow_none: bool = False, 53 | ) -> None: 54 | # unwrap `Annotated[T, ...]` -> `T` 55 | if is_annotated_type(type_): 56 | type_ = extract_type_arg(type_, 0) 57 | 58 | if allow_none and value is None: 59 | return 60 | 61 | if type_ is None or type_ is NoneType: 62 | assert value is None 63 | return 64 | 65 | origin = get_origin(type_) or type_ 66 | 67 | if is_list_type(type_): 68 | return _assert_list_type(type_, value) 69 | 70 | if origin == str: 71 | assert isinstance(value, str) 72 | elif origin == int: 73 | assert isinstance(value, int) 74 | elif origin == bool: 75 | assert isinstance(value, bool) 76 | elif origin == float: 77 | assert isinstance(value, float) 78 | elif origin == bytes: 79 | assert isinstance(value, bytes) 80 | elif origin == datetime: 81 | assert isinstance(value, datetime) 82 | elif origin == date: 83 | assert isinstance(value, date) 84 | elif origin == object: 85 | # nothing to do here, the expected type is unknown 86 | pass 87 | elif origin == Literal: 88 | assert value in get_args(type_) 89 | elif origin == dict: 90 | assert is_dict(value) 91 | 92 | args = get_args(type_) 93 | key_type = args[0] 94 | items_type = args[1] 95 | 96 | for key, item in value.items(): 97 | assert_matches_type(key_type, key, path=[*path, ""]) 98 | assert_matches_type(items_type, item, path=[*path, ""]) 99 | elif is_union_type(type_): 100 | variants = get_args(type_) 101 | 102 | try: 103 | none_index = variants.index(type(None)) 104 | except ValueError: 105 | pass 106 | else: 107 | # special case Optional[T] for better error messages 108 | if len(variants) == 2: 109 | if value is None: 110 | # valid 111 | return 112 | 113 | return assert_matches_type(type_=variants[not none_index], value=value, path=path) 114 | 115 | for i, variant in enumerate(variants): 116 | try: 117 | assert_matches_type(variant, value, path=[*path, f"variant {i}"]) 118 | return 119 | except AssertionError: 120 | traceback.print_exc() 121 | continue 122 | 123 | raise AssertionError("Did not match any variants") 124 | elif issubclass(origin, BaseModel): 125 | assert isinstance(value, type_) 126 | assert assert_matches_model(type_, cast(Any, value), path=path) 127 | elif inspect.isclass(origin) and origin.__name__ == "HttpxBinaryResponseContent": 128 | assert value.__class__.__name__ == "HttpxBinaryResponseContent" 129 | else: 130 | assert None, f"Unhandled field type: {type_}" 131 | 132 | 133 | def _assert_list_type(type_: type[object], value: object) -> None: 134 | assert is_list(value) 135 | 136 | inner_type = get_args(type_)[0] 137 | for entry in value: 138 | assert_type(inner_type, entry) # type: ignore 139 | 140 | 141 | @contextlib.contextmanager 142 | def update_env(**new_env: str) -> Iterator[None]: 143 | old = os.environ.copy() 144 | 145 | try: 146 | os.environ.update(new_env) 147 | 148 | yield None 149 | finally: 150 | os.environ.clear() 151 | os.environ.update(old) 152 | --------------------------------------------------------------------------------