├── .github ├── license-header.txt └── workflows │ ├── backport.yml │ ├── publish.yml │ └── pull-request.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── README_PYPI.md ├── assets ├── logo_dark.svg └── logo_light.svg ├── codegen ├── README.md ├── api-rename-mapping.json ├── generator.py ├── requirements.txt └── templates │ ├── data_ask.tpl │ ├── data_ask_follow_up.tpl │ ├── data_query.tpl │ ├── database_create.tpl │ ├── database_rename.tpl │ ├── endpoint.tpl │ ├── namespace.tpl │ ├── sql_query.tpl │ └── workspace_create.tpl ├── docs ├── Makefile ├── api.rst ├── conf.py ├── index.rst └── make.bat ├── examples ├── README.md ├── bulk.py ├── datasets │ ├── README.md │ ├── access_logs.csv │ ├── airbyte │ │ ├── companies.csv │ │ └── schema.json │ └── stock-prices │ │ ├── companies_large_50.csv │ │ ├── companies_med_250.csv │ │ ├── companies_small_25.csv │ │ ├── prices_large_10000.csv │ │ ├── prices_med_2500.csv │ │ ├── prices_small_250.csv │ │ ├── schema.json │ │ └── schema_companies.json ├── pagination.py ├── rabbitmq │ ├── .env.sample │ ├── Makefile │ ├── README.md │ ├── consumer │ │ ├── Dockerfile │ │ ├── consumer.py │ │ └── requirements.txt │ ├── docker-compose.yml │ ├── install.py │ ├── producer │ │ ├── Dockerfile │ │ ├── producer.py │ │ └── requirements.txt │ └── requirements.txt └── transactions.py ├── poetry.lock ├── pyproject.toml ├── setup.cfg ├── tests ├── data │ └── attachments │ │ ├── archives │ │ └── assets.zip │ │ ├── images │ │ ├── 01.gif │ │ ├── 02.gif │ │ ├── 03.png │ │ └── logo.svg │ │ └── text │ │ └── stocks.csv ├── integration-tests │ ├── api_request_test.py │ ├── api_response_test.py │ ├── authentication_test.py │ ├── branch_test.py │ ├── conftest.py │ ├── databases_test.py │ ├── files_access.py │ ├── files_multiple_files_test.py │ ├── files_single_file_test.py │ ├── files_transormations_test.py │ ├── files_upload_url_test.py │ ├── helpers_bulkprocessor_test.py │ ├── helpers_transaction_test.py │ ├── records_branch_transactions_test.py │ ├── records_file_operations_test.py │ ├── records_test.py │ ├── search_and_filter_ask_table_test.py │ ├── search_and_filter_query_test.py │ ├── search_and_filter_revlinks_test.py │ ├── search_and_filter_test.py │ ├── search_and_filter_vector_search_test.py │ ├── search_and_filter_with_alias_test.py │ ├── sql_query_test.py │ ├── table_test.py │ ├── type_json_test.py │ ├── users_test.py │ ├── utils.py │ └── workspaces_test.py └── unit-tests │ ├── api_request_domains_test.py │ ├── api_request_internals_test.py │ ├── api_response_test.py │ ├── client_config_getters.py │ ├── client_headers_test.py │ ├── client_init_test.py │ ├── client_internals_test.py │ ├── client_telemetry_test.py │ ├── client_with_custom_domains_test.py │ ├── files_transformations_test.py │ ├── helpers_bulk_processor_test.py │ ├── helpers_to_rfc3339.py │ ├── helpers_transaction_test.py │ └── utils.py └── xata ├── __init__.py ├── api ├── __init__.py ├── authentication.py ├── branch.py ├── databases.py ├── files.py ├── invites.py ├── migrations.py ├── oauth.py ├── records.py ├── search_and_filter.py ├── sql.py ├── table.py ├── users.py └── workspaces.py ├── api_request.py ├── api_response.py ├── client.py ├── errors.py └── helpers.py /.github/license-header.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | -------------------------------------------------------------------------------- /.github/workflows/backport.yml: -------------------------------------------------------------------------------- 1 | name: Backport 2 | on: 3 | pull_request: 4 | types: 5 | - closed 6 | - labeled 7 | 8 | jobs: 9 | backport: 10 | runs-on: ubuntu-latest 11 | name: Backport 12 | steps: 13 | - name: Backport 14 | uses: tibdex/backport@v1 15 | with: 16 | github_token: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Poetry when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Publish Package 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | deploy: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v3 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: "3.x" 24 | 25 | - name: Install dependencies 26 | run: | 27 | python3 -m pip install -U poetry 28 | poetry install 29 | 30 | - name: Publish package 31 | run: poetry publish --build -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Pull Request 5 | 6 | on: 7 | push: 8 | branches: ["main", "0.x"] 9 | pull_request: 10 | branches: ["main", "0.x"] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Python 3.10 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: "3.10" 22 | 23 | # credit: https://stackoverflow.com/a/75640388 24 | - name: readme synced 25 | run: diff <(tail -n +8 README.md) <(tail -n +4 README_PYPI.md) 26 | 27 | 28 | - name: Install dependencies 29 | run: | 30 | python3 -m pip install -U poetry 31 | poetry install 32 | 33 | - name: Install license-header-checker 34 | run: curl -s https://raw.githubusercontent.com/lluissm/license-header-checker/master/install.sh | bash 35 | - name: Run license check 36 | run: ./bin/license-header-checker -a -r .github/license-header.txt . py && [[ -z `git status -s` ]] 37 | 38 | # - name: Lint 39 | # run: | 40 | # export PIP_USER=0; poetry run pre-commit run --all-files 41 | 42 | - name: Unit Tests 43 | run: | 44 | poetry run pytest -v --tb=short tests/unit-tests/ 45 | 46 | - name: Integration Tests 47 | env: 48 | XATA_WORKSPACE_ID: ${{ secrets.INTEGRATION_TEST_WORKSPACE }} 49 | XATA_API_KEY: ${{ secrets.INTEGRATION_TEST_API_KEY }} 50 | run: | 51 | poetry run pytest -v --tb=short -W ignore::DeprecationWarning tests/integration-tests/ 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python ### 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Xata 11 | .xatarc 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | /xata.egg-info/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # poetry 103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 104 | # This is especially recommended for binary packages to ensure reproducibility, and is more 105 | # commonly ignored for libraries. 106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 107 | #poetry.lock 108 | 109 | # pdm 110 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 111 | #pdm.lock 112 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 113 | # in version control. 114 | # https://pdm.fming.dev/#use-with-ide 115 | .pdm.toml 116 | 117 | codegen/ws/* 118 | .DS_Store 119 | api-docs/* 120 | bin 121 | 122 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 123 | __pypackages__/ 124 | 125 | # Celery stuff 126 | celerybeat-schedule 127 | celerybeat.pid 128 | 129 | # SageMath parsed files 130 | *.sage.py 131 | 132 | # Environments 133 | .env 134 | .venv 135 | env/ 136 | venv/ 137 | ENV/ 138 | env.bak/ 139 | venv.bak/ 140 | 141 | # Spyder project settings 142 | .spyderproject 143 | .spyproject 144 | 145 | # Rope project settings 146 | .ropeproject 147 | 148 | # mkdocs documentation 149 | /site 150 | 151 | # mypy 152 | .mypy_cache/ 153 | .dmypy.json 154 | dmypy.json 155 | 156 | # Pyre type checker 157 | .pyre/ 158 | 159 | # pytype static type analyzer 160 | .pytype/ 161 | 162 | # Cython debug symbols 163 | cython_debug/ 164 | 165 | # PyCharm 166 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 167 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 168 | # and can be added to the global gitignore or merged into this file. For a more nuclear 169 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 170 | #.idea/ 171 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: check-toml 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | 10 | - repo: https://github.com/psf/black 11 | rev: 22.10.0 12 | hooks: 13 | - id: black 14 | language_version: python3.10 15 | args: 16 | - --line-length=120 17 | - --color 18 | 19 | - repo: https://github.com/pycqa/flake8 20 | rev: 5.0.4 21 | hooks: 22 | - id: flake8 23 | args: 24 | - "--max-line-length=120" 25 | - "--max-complexity=18" 26 | additional_dependencies: [pep8-naming] 27 | 28 | - repo: https://github.com/pycqa/isort 29 | rev: 5.12.0 30 | hooks: 31 | - id: isort 32 | name: isort 33 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.9" 13 | python: 14 | install: 15 | - method: pip 16 | path: . 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | install: ## Install dependencies 4 | poetry install 5 | 6 | lint: ## Linter 7 | export PIP_USER=0; poetry run pre-commit run --all-files 8 | 9 | api-docs: ## Generate rtd 10 | poetry install 11 | cd docs 12 | poetry run make html 13 | 14 | check-license-header: ## Check if all *.py files have a license header 15 | curl -s https://raw.githubusercontent.com/lluissm/license-header-checker/master/install.sh | bash 16 | ./bin/license-header-checker -a .github/license-header.txt . py 17 | 18 | code-gen: ## Generate endpoints from OpenAPI specs 19 | mkdir -vp codegen/ws/ 20 | rm -Rfv codegen/ws/* 21 | python codegen/generator.py 22 | cp -fv codegen/ws/*.py xata/api/. 23 | rm -Rfv codegen/ws/*.py 24 | 25 | test: | unit-tests integration-tests ## Run unit & integration tests 26 | 27 | unit-tests: ## Run unit tests 28 | poetry run pytest -v --tb=short tests/unit-tests/ 29 | 30 | unit-tests-cov: ## Unit tests coverage 31 | poetry run pytest --cov=xata tests/unit-tests 32 | 33 | integration-tests: ## Run integration tests 34 | poetry run pytest -v --tb=short -W ignore::DeprecationWarning tests/integration-tests/ 35 | 36 | integration-tests-cov: ## Integration tests coverage 37 | poetry run pytest --cov=xata tests/integration-tests/ 38 | 39 | help: ## Display help 40 | @awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 41 | #------------- -------------- 42 | 43 | .DEFAULT_GOAL := help 44 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Xata Python SDK 2 | Copyright 2022 Xatabase Inc. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | Xata 6 | 7 |

8 | 9 | # Python SDK for Xata 10 | 11 | [![Documentation Status](https://readthedocs.org/projects/xata-py/badge/?version=latest)](https://xata-py.readthedocs.io/en/latest/?badge=latest) [![PyPI version](https://badge.fury.io/py/xata.svg)](https://badge.fury.io/py/xata) 12 | 13 | Xata is a serverless data platform, based on PostgreSQL. The data platform brings full-text search, vector similarity search, and file/image attachments on top of PostgreSQL. Xata supports also native database branches and a developer workflow that integrates seamlessly with GitHub and platforms like Vercel and Netlify. 14 | 15 | The Python SDK uses type annotations and requires Python 3.8 or higher. 16 | 17 | To install, run: 18 | 19 | ``` 20 | pip install xata 21 | ``` 22 | 23 | To learn more about Xata, visit [xata.io](https://xata.io). 24 | 25 | - [SDK documentation](https://xata.io/docs/sdk/python/overview) 26 | - [API Reference](https://xata-py.readthedocs.io/en/latest/api.html) 27 | -------------------------------------------------------------------------------- /README_PYPI.md: -------------------------------------------------------------------------------- 1 |

2 | Xata 3 |

4 | 5 | # Python SDK for Xata 6 | 7 | [![Documentation Status](https://readthedocs.org/projects/xata-py/badge/?version=latest)](https://xata-py.readthedocs.io/en/latest/?badge=latest) [![PyPI version](https://badge.fury.io/py/xata.svg)](https://badge.fury.io/py/xata) 8 | 9 | Xata is a serverless data platform, based on PostgreSQL. The data platform brings full-text search, vector similarity search, and file/image attachments on top of PostgreSQL. Xata supports also native database branches and a developer workflow that integrates seamlessly with GitHub and platforms like Vercel and Netlify. 10 | 11 | The Python SDK uses type annotations and requires Python 3.8 or higher. 12 | 13 | To install, run: 14 | 15 | ``` 16 | pip install xata 17 | ``` 18 | 19 | To learn more about Xata, visit [xata.io](https://xata.io). 20 | 21 | - [SDK documentation](https://xata.io/docs/sdk/python/overview) 22 | - [API Reference](https://xata-py.readthedocs.io/en/latest/api.html) 23 | -------------------------------------------------------------------------------- /assets/logo_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /codegen/README.md: -------------------------------------------------------------------------------- 1 | # Code Generator 2 | 3 | We use an OpenAPI specification to define Xata's server API, which is divided in two scopes: 4 | * `core`: Control Plane ([spec](https://xata.io/api/openapi?scope=core)) 5 | * `workspace`: Data Plane ([spec](https://xata.io/api/openapi?scope=workspace)) 6 | 7 | Please refer to our [documentation](https://xata.io/docs/rest-api/openapi) for more information. 8 | 9 | To run the generator go to the project root and execute the following make targets: 10 | ``` 11 | make code-gen code-gen-copy lint scope=workspace 12 | ``` 13 | 14 | `code-gen`: generates the endpoints based on the provide `scope` 15 | `code-gen-copy`: copies the generated classes into the target directory 16 | `lint`: runs linting in order to comply with coding standards 17 | -------------------------------------------------------------------------------- /codegen/requirements.txt: -------------------------------------------------------------------------------- 1 | Mako 2 | requests 3 | coloredlogs 4 | xata 5 | -------------------------------------------------------------------------------- /codegen/templates/data_ask.tpl: -------------------------------------------------------------------------------- 1 | def ${operation_id}(self, table_name: str, question: str, rules: list[str] = [], options: dict = {}, streaming_results: bool = False, db_name: str = None, branch_name: str = None) -> ApiResponse: 2 | """ 3 | ${description} 4 | 5 | Reference: ${docs_url} 6 | Path: ${path} 7 | Method: ${http_method} 8 | Response status codes: 9 | - 200: Response to the question 10 | - 400: Bad Request 11 | - 401: Authentication Error 12 | - 404: Example response 13 | - 429: Rate limit exceeded 14 | - 503: ServiceUnavailable 15 | - 5XX: Unexpected Error 16 | Responses: 17 | - application/json 18 | - text/event-stream 19 | 20 | :param table_name: str The Table name 21 | :param question: str follow up question to ask 22 | :param rules: list[str] specific rules you want to apply, default: [] 23 | :param options: dict more options to adjust the query, default: {} 24 | :param streaming_results: bool get the results streamed, default: False 25 | :param db_name: str = None The name of the database to query. Default: database name from the client. 26 | :param branch_name: str = None The name of the branch to query. Default: branch name from the client. 27 | 28 | :returns ApiResponse 29 | """ 30 | db_branch_name = self.client.get_db_branch_name(db_name, branch_name) 31 | url_path = f"/db/{db_branch_name}/tables/{table_name}/ask" 32 | payload = { 33 | "question": question, 34 | } 35 | headers = { 36 | "content-type": "application/json", 37 | "accept": "text/event-stream" if streaming_results else "application/json", 38 | } 39 | return self.request("POST", url_path, headers, payload, is_streaming=streaming_results) 40 | -------------------------------------------------------------------------------- /codegen/templates/data_ask_follow_up.tpl: -------------------------------------------------------------------------------- 1 | def ${operation_id}(self, table_name: str, session_id: str, question: str, streaming_results: bool = False, db_name: str = None, branch_name: str = None) -> ApiResponse: 2 | """ 3 | ${description} 4 | 5 | Reference: ${docs_url} 6 | Path: ${path} 7 | Method: ${http_method} 8 | Response status codes: 9 | - 200: Response to the question 10 | - 400: Bad Request 11 | - 401: Authentication Error 12 | - 404: Example response 13 | - 429: Rate limit exceeded 14 | - 503: ServiceUnavailable 15 | - 5XX: Unexpected Error 16 | Responses: 17 | - application/json 18 | - text/event-stream 19 | 20 | :param table_name: str The Table name 21 | :param session_id: str Session id from initial question 22 | :param question: str follow up question to ask 23 | :param streaming_results: bool get the results streamed, default: False 24 | :param db_name: str = None The name of the database to query. Default: database name from the client. 25 | :param branch_name: str = None The name of the branch to query. Default: branch name from the client. 26 | 27 | :returns ApiResponse 28 | """ 29 | db_branch_name = self.client.get_db_branch_name(db_name, branch_name) 30 | url_path = f"/db/{db_branch_name}/tables/{table_name}/ask/{session_id}" 31 | payload = { 32 | "message": question, 33 | } 34 | headers = { 35 | "content-type": "application/json", 36 | "accept": "text/event-stream" if streaming_results else "application/json", 37 | } 38 | return self.request("POST", url_path, headers, payload, is_streaming=streaming_results) 39 | -------------------------------------------------------------------------------- /codegen/templates/data_query.tpl: -------------------------------------------------------------------------------- 1 | 2 | %if params['list']: 3 | def ${operation_id}(self, table_name: str, payload: dict = None, db_name: str = None, branch_name: str = None) -> ApiResponse: 4 | %else: 5 | def ${operation_id}(self) -> ApiResponse: 6 | %endif 7 | """ 8 | ${description} 9 | 10 | Reference: ${docs_url} 11 | Path: ${path} 12 | Method: ${http_method} 13 | % if status == "experimental": 14 | Status: Experimental 15 | % endif 16 | Response status codes: 17 | % for rc in params['response_codes']: 18 | - ${rc["code"]}: ${rc["description"]} 19 | % endfor 20 | % if len(params['response_content_types']) > 1 : 21 | Responses: 22 | % for rc in params['response_content_types']: 23 | - ${rc["content_type"]} 24 | % endfor 25 | % elif len(params['response_content_types']) == 1 : 26 | Response: ${params['response_content_types'][0]["content_type"]} 27 | % endif 28 | 29 | % for param in params['list']: 30 | :param ${param['nameParam']}: ${param['type']} ${param['description']} 31 | % endfor 32 | 33 | :returns ApiResponse 34 | """ 35 | db_branch_name = self.client.get_db_branch_name(db_name, branch_name) 36 | url_path = f"/db/{db_branch_name}/tables/{table_name}/query" 37 | headers = {"content-type": "application/json"} 38 | if not payload: 39 | payload = {} 40 | return self.request("POST", url_path, headers, payload) 41 | -------------------------------------------------------------------------------- /codegen/templates/database_create.tpl: -------------------------------------------------------------------------------- 1 | 2 | def ${operation_id}(self, db_name: str, workspace_id: str = None, region: str = None, branch_name: str = None) -> ApiResponse: 3 | """ 4 | ${description} 5 | 6 | Reference: ${docs_url} 7 | Path: ${path} 8 | Method: ${http_method} 9 | Response status codes: 10 | % for rc in params['response_codes']: 11 | - ${rc["code"]}: ${rc["description"]} 12 | % endfor 13 | % if len(params['response_content_types']) > 1 : 14 | Responses: 15 | % for rc in params['response_content_types']: 16 | - ${rc["content_type"]} 17 | % endfor 18 | % elif len(params['response_content_types']) == 1 : 19 | Response: ${params['response_content_types'][0]["content_type"]} 20 | % endif 21 | 22 | :param db_name: str The Database Name 23 | :param workspace_id: str = None The workspace identifier. Default: workspace Id from the client. 24 | :param region: str = None Which region to deploy. Default: region defined in the client, if not specified: us-east-1 25 | :param branch_name: str = None Which branch to create. Default: branch name used from the client, if not speicifed: main 26 | 27 | :return Response 28 | """ 29 | if workspace_id is None: 30 | workspace_id = self.client.get_workspace_id() 31 | payload = { 32 | "region": region if region else self.client.get_region(), 33 | "branchName": branch_name if branch_name else self.client.get_branch_name(), 34 | } 35 | url_path = f"${path}" 36 | headers = {"content-type": "application/json"} 37 | return self.request("${http_method}", url_path, headers, payload) 38 | -------------------------------------------------------------------------------- /codegen/templates/database_rename.tpl: -------------------------------------------------------------------------------- 1 | 2 | def ${operation_id}(self, db_name: str, new_name: str, workspace_id: str = None) -> ApiResponse: 3 | """ 4 | ${description} 5 | 6 | Reference: ${docs_url} 7 | Path: ${path} 8 | Method: ${http_method} 9 | Response status codes: 10 | % for rc in params['response_codes']: 11 | - ${rc["code"]}: ${rc["description"]} 12 | % endfor 13 | % if len(params['response_content_types']) > 1 : 14 | Responses: 15 | % for rc in params['response_content_types']: 16 | - ${rc["content_type"]} 17 | % endfor 18 | % elif len(params['response_content_types']) == 1 : 19 | Response: ${params['response_content_types'][0]["content_type"]} 20 | % endif 21 | 22 | :param db_name: str Current database name 23 | :param new_name: str New database name 24 | :param workspace_id: str = None The workspace identifier. Default: workspace Id from the client. 25 | 26 | :return Response 27 | """ 28 | if workspace_id is None: 29 | workspace_id = self.client.get_workspace_id() 30 | payload = {"newName": new_name} 31 | url_path = f"${path}" 32 | headers = {"content-type": "application/json"} 33 | return self.request("${http_method}", url_path, headers, payload) 34 | -------------------------------------------------------------------------------- /codegen/templates/endpoint.tpl: -------------------------------------------------------------------------------- 1 | 2 | %if params['list']: 3 | def ${operation_id}(self, ${', '.join([f"{p['nameParam']}: {p['type']}" for p in params['list']])}) -> ApiResponse: 4 | %else: 5 | def ${operation_id}(self) -> ApiResponse: 6 | %endif 7 | """ 8 | ${description} 9 | 10 | Reference: ${docs_url} 11 | Path: ${path} 12 | Method: ${http_method} 13 | % if status == "experimental": 14 | Status: Experimental 15 | % endif 16 | Response status codes: 17 | % for rc in params['response_codes']: 18 | - ${rc["code"]}: ${rc["description"]} 19 | % endfor 20 | % if len(params['response_content_types']) > 1 : 21 | Responses: 22 | % for rc in params['response_content_types']: 23 | - ${rc["content_type"]} 24 | % endfor 25 | % elif len(params['response_content_types']) == 1 : 26 | Response: ${params['response_content_types'][0]["content_type"]} 27 | % endif 28 | 29 | % for param in params['list']: 30 | :param ${param['nameParam']}: ${param['type']} ${param['description']} 31 | % endfor 32 | 33 | :returns ApiResponse 34 | """ 35 | % if params['smart_db_branch_name'] : 36 | db_branch_name = self.client.get_db_branch_name(db_name, branch_name) 37 | % endif 38 | % if params['smart_workspace_id'] : 39 | if workspace_id is None: 40 | workspace_id = self.client.get_workspace_id() 41 | % endif 42 | % if params['has_path_params'] : 43 | url_path = f"${path}" 44 | % else : 45 | url_path = "${path}" 46 | % endif 47 | % if params['has_query_params'] > 1: 48 | query_params = [] 49 | % for param in params['list']: 50 | % if param['in'] == 'query': 51 | if ${param['nameParam']} is not None: 52 | % if param['trueType'] == 'list' : 53 | query_params.append("${param['name']}=%s" % ",".join(${param['nameParam']})) 54 | % else : 55 | query_params.append(f"${param['name']}={${param['nameParam']}}") 56 | % endif 57 | % endif 58 | % endfor 59 | if query_params: 60 | url_path += "?" + "&".join(query_params) 61 | % endif 62 | % if params['has_query_params'] == 1 : 63 | % for param in params['list']: 64 | % if param['in'] == 'query': 65 | if ${param['nameParam']} is not None: 66 | % if param['trueType'] == 'list' : 67 | url_path += "?${param['name']}=%s" % ",".join(${param['nameParam']}) 68 | % else : 69 | url_path += f"?${param['name']}={${param['nameParam']}}" 70 | % endif 71 | % endif 72 | % endfor 73 | % endif 74 | % if params['has_payload'] and len(params['response_content_types']) > 1: 75 | headers = { 76 | "content-type": "application/json", 77 | "accept": response_content_type, 78 | } 79 | return self.request("${http_method}", url_path, headers, payload) 80 | % elif params['has_payload']: 81 | headers = {"content-type": "application/json"} 82 | return self.request("${http_method}", url_path, headers, payload) 83 | % elif len(params['response_content_types']) > 1: 84 | headers = {"accept": response_content_type} 85 | return self.request("${http_method}", url_path, headers) 86 | % else : 87 | return self.request("${http_method}", url_path) 88 | % endif 89 | -------------------------------------------------------------------------------- /codegen/templates/namespace.tpl: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | # ------------------------------------------------------- # 21 | # ${class_name} 22 | # ${class_description} 23 | # Specification: ${spec_scope}:v${spec_version} 24 | # ------------------------------------------------------- # 25 | 26 | from xata.api_request import ApiRequest 27 | from xata.api_response import ApiResponse 28 | 29 | class ${class_name}(ApiRequest): 30 | 31 | scope = "${spec_scope}" 32 | -------------------------------------------------------------------------------- /codegen/templates/sql_query.tpl: -------------------------------------------------------------------------------- 1 | 2 | def ${operation_id}( 3 | self, 4 | statement: str, 5 | params: list = None, 6 | consistency: str = "strong", 7 | db_name: str = None, 8 | branch_name: str = None, 9 | ) -> ApiResponse: 10 | """ 11 | ${description} 12 | 13 | Reference: ${docs_url} 14 | Path: ${path} 15 | Method: ${http_method} 16 | Response status codes: 17 | % for rc in params['response_codes']: 18 | - ${rc["code"]}: ${rc["description"]} 19 | % endfor 20 | % if len(params['response_content_types']) > 1 : 21 | Responses: 22 | % for rc in params['response_content_types']: 23 | - ${rc["content_type"]} 24 | % endfor 25 | % elif len(params['response_content_types']) == 1 : 26 | Response: ${params['response_content_types'][0]["content_type"]} 27 | % endif 28 | 29 | :param statement: str The statement to run 30 | :param params: dict The query parameters list. default: None 31 | :param consistency: str The consistency level for this request. default: strong 32 | :param db_name: str = None The name of the database to query. Default: database name from the client. 33 | :param branch_name: str = None The name of the branch to query. Default: branch name from the client. 34 | 35 | :returns ApiResponse 36 | """ 37 | db_branch_name = self.client.get_db_branch_name(db_name, branch_name) 38 | url_path = f"/db/{db_branch_name}/sql" 39 | headers = {"content-type": "application/json"} 40 | payload = { 41 | "statement": statement, 42 | "params": params, 43 | "consistency": consistency, 44 | } 45 | return self.request("POST", url_path, headers, payload) 46 | -------------------------------------------------------------------------------- /codegen/templates/workspace_create.tpl: -------------------------------------------------------------------------------- 1 | 2 | def ${operation_id}(self, name: str, slug: str = None) -> ApiResponse: 3 | """ 4 | ${description} 5 | 6 | Path: ${path} 7 | Method: ${http_method} 8 | Response status codes: 9 | % for rc in params['response_codes']: 10 | - ${rc["code"]}: ${rc["description"]} 11 | % endfor 12 | % if len(params['response_content_types']) > 1 : 13 | Responses: 14 | % for rc in params['response_content_types']: 15 | - ${rc["content_type"]} 16 | % endfor 17 | % elif len(params['response_content_types']) == 1 : 18 | Response: ${params['response_content_types'][0]["content_type"]} 19 | % endif 20 | 21 | :param name: str Workspace name 22 | :param slug: str = None Slug to use 23 | 24 | :return Response 25 | """ 26 | payload = {"name": name} 27 | if slug: 28 | payload["slug"] = slug 29 | url_path = "${path}" 30 | headers = {"content-type": "application/json"} 31 | return self.request("${http_method}", url_path, headers, payload) 32 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | Xata Python SDK API Reference 4 | ============================= 5 | 6 | Xata Client 7 | ----------- 8 | 9 | .. py:module:: xata 10 | .. autoclass:: XataClient 11 | :members: 12 | 13 | .. py:module:: xata.api_request 14 | .. autoclass:: ApiRequest 15 | :members: 16 | 17 | .. py:module:: xata.api_response 18 | .. autoclass:: ApiResponse 19 | :members: 20 | 21 | Data API 22 | -------- 23 | .. py:module:: xata.api.search_and_filter 24 | .. autoclass:: SearchAndFilter 25 | :members: 26 | 27 | .. py:module:: xata.api.records 28 | .. autoclass:: Records 29 | :members: 30 | 31 | .. py:module:: xata.api.files 32 | .. autoclass:: Files 33 | :members: 34 | 35 | .. py:module:: xata.api.sql 36 | .. autoclass:: SQL 37 | :members: 38 | 39 | Management API 40 | -------------- 41 | 42 | 43 | .. py:module:: xata.api.databases 44 | .. autoclass:: Databases 45 | :members: 46 | 47 | .. py:module:: xata.api.branch 48 | .. autoclass:: Branch 49 | :members: 50 | 51 | .. py:module:: xata.api.table 52 | .. autoclass:: Table 53 | :members: 54 | 55 | .. py:module:: xata.api.authentication 56 | .. autoclass:: Authentication 57 | :members: 58 | 59 | .. py:module:: xata.api.users 60 | .. autoclass:: Users 61 | :members: 62 | 63 | .. py:module:: xata.api.workspaces 64 | .. autoclass:: Workspaces 65 | :members: 66 | 67 | .. py:module:: xata.api.invites 68 | .. autoclass:: Invites 69 | :members: 70 | 71 | Helpers 72 | ------- 73 | 74 | .. py:module:: xata.helpers 75 | .. autoclass:: BulkProcessor 76 | :members: 77 | .. autoclass:: Transaction 78 | :members: 79 | 80 | Errors 81 | ------ 82 | 83 | .. py:module:: xata.errors 84 | .. autoclass:: XataServerError 85 | :members: 86 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | # Configuration file for the Sphinx documentation builder. 21 | # 22 | # For the full list of built-in configuration values, see the documentation: 23 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 24 | 25 | # -- Project information ----------------------------------------------------- 26 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 27 | 28 | import os 29 | import sys 30 | 31 | # import sphinx_rtd_theme 32 | 33 | project = "xata-py" 34 | copyright = "2022, Xatabase Inc." 35 | author = "Tudor Golubenco" 36 | 37 | # -- General configuration --------------------------------------------------- 38 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 39 | 40 | extensions = ["sphinx.ext.autodoc"] 41 | 42 | templates_path = ["_templates"] 43 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 44 | 45 | 46 | # -- Options for HTML output ------------------------------------------------- 47 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 48 | 49 | html_theme = "sphinx_rtd_theme" 50 | # html_static_path = ['_static'] 51 | 52 | sys.path.insert(0, os.path.abspath("..")) 53 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. xata-py documentation master file, created by 2 | sphinx-quickstart on Sat Oct 1 12:34:50 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Python SDK for Xata 7 | =================== 8 | 9 | This is a the official low-level Python client for the Xata_ service. 10 | Xata is a Serverless Database that is as easy to use as a spreadsheet, has the 11 | data integrity of PostgreSQL, and the search and analytics functionality of 12 | Elasticsearch. 13 | 14 | 15 | Installation 16 | ------------ 17 | 18 | Install the `xata` package from PyPI: 19 | 20 | .. code-block:: bash 21 | 22 | $ pip install xata 23 | 24 | While not strictly required, we recommend installing the Xata_CLI_ and initializing 25 | your project with the `xata init` command: 26 | 27 | .. code-block:: bash 28 | 29 | $ xata init 30 | 31 | This will then interactively ask you to select a Database, and store the required 32 | connection information in `.xatarc` and `.env`. The Python SDK automatically reads those 33 | files, but it's also possible to pass the connection information directly to the 34 | `xata.Client` constructor. 35 | 36 | See more information about the installation in the Xata_Python_SDK_Install_Docs_. 37 | 38 | Example Usage 39 | ------------- 40 | .. code-block:: python 41 | 42 | from xata.client import XataClient 43 | 44 | resp = xata.data().query("Avengers", { 45 | "columns": ["name", "thumbnail"], # the columns we want returned 46 | "filter": { "job": "spiderman" }, # optional filters to apply 47 | "sort": { "name": "desc" } # optional sorting key and order (asc/desc) 48 | }) 49 | assert resp.is_success() 50 | print(resp["records"]) 51 | # [{"id": "spidey", "name": "Peter Parker", "job": "spiderman"}] 52 | # Note it will be an array, even if there is only one record matching the filters 53 | 54 | See more examples in the Xata_Python_SDK_Examples_Docs_. 55 | 56 | .. toctree:: 57 | :maxdepth: 3 58 | :caption: Contents: 59 | 60 | api 61 | 62 | 63 | 64 | Indices and tables 65 | ================== 66 | 67 | * :ref:`genindex` 68 | * :ref:`modindex` 69 | * :ref:`search` 70 | 71 | .. _Xata: https://xata.io 72 | .. _Xata_CLI: https://xata.io/docs/getting-started/cli 73 | .. _Xata_Python_SDK_Install_Docs: https://xata.io/docs/python-sdk/overview 74 | .. _Xata_Python_SDK_Examples_Docs: https://xata.io/docs/python-sdk/examples 75 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | The following examples are demonstrating how to solve a specific task, if you want more examples how the specific endpoints behave, please head over to our [documentation](https://xata.io/docs/python-sdk/examples). 4 | 5 | * [Ingest records from a queue](/examples/rabbitmq): This is a full example that you can run with docker. 6 | * [Leveraging transactions](transactions.py): How to bulk insert, update, delete or get records with the Transaction helper. 7 | * [Pagination](pagination.py): How use cursors to paginate over result sets. 8 | * [Bulk insert](bulk.py): How to insert batches or large amounts of data. 9 | 10 | ## Datasets 11 | 12 | You can find datasets in the [datasets](/examples/datasets) folder. All data is generated with [Faker](https://pypi.org/project/Faker/0.7.4/), any resemblance is purely coincidental, [see notice](/datasets/README.md). 13 | -------------------------------------------------------------------------------- /examples/bulk.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | from faker import Faker 21 | 22 | from xata.client import XataClient 23 | from xata.helpers import BulkProcessor 24 | 25 | # This example demonstrates how to use the BulkProcessor. 26 | # We are going to generate 500 records whith random data and ingest 27 | # them to Xata with the BulkProcessor 28 | # 29 | # @link https://xata.io/docs/python-sdk/bulk-processor 30 | 31 | fake = Faker() 32 | xata = XataClient() 33 | bp = BulkProcessor(xata) 34 | 35 | 36 | def generate_records(n: int): 37 | return [ 38 | { 39 | "name": fake.name(), 40 | "email": fake.unique.email(), 41 | "age": fake.random_int(min=0, max=100), 42 | "is_active": fake.boolean(), 43 | "icon": fake.image_url(), 44 | } 45 | for i in range(n) 46 | ] 47 | 48 | 49 | # Generate records 50 | records = generate_records(500) 51 | 52 | # Put multiple records to the ingestion queue 53 | bp.put_records("people", records) 54 | 55 | # Put a single records 56 | records = generate_records(2) 57 | bp.put_record("people", records[0]) 58 | bp.put_record("people", records[1]) 59 | 60 | # Ensure the queue is flushed before exiting the process 61 | bp.flush_queue() 62 | 63 | # Execution stats 64 | print(bp.get_stats()) 65 | # { 66 | # 'total': 502, 67 | # 'queue': 0, 68 | # 'failed_batches': 0, 69 | # 'tables': { 70 | # 'people': 502 71 | # } 72 | # } 73 | -------------------------------------------------------------------------------- /examples/datasets/README.md: -------------------------------------------------------------------------------- 1 | # Datasets 2 | 3 | _All datapoints are generated from random data, any resemblance to real persons or other real-life entities is purely coincidental_ 4 | 5 | ## Stock Prices 6 | These datasets contain fictive companies and stock prices that move over time within an interval, small & medium have 15 seconds gap between the datapoints and the large dataset an hour. 7 | 8 | ## Access Logs 9 | The dataset was taken from [Kaggle](https://www.kaggle.com/datasets/vishnu0399/server-logs), under the public domain license [CC0](https://creativecommons.org/publicdomain/zero/1.0/). The original dataset got truncated to 1000 rows and formatted to CSV. 10 | -------------------------------------------------------------------------------- /examples/datasets/airbyte/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "tables": [ 3 | { 4 | "name": "Companies", 5 | "columns": [ 6 | { 7 | "name": "name", 8 | "type": "string" 9 | }, 10 | { 11 | "name": "address", 12 | "type": "string" 13 | }, 14 | { 15 | "name": "catch_phrase", 16 | "type": "string" 17 | }, 18 | { 19 | "name": "ceo", 20 | "type": "string" 21 | }, 22 | { 23 | "name": "phone", 24 | "type": "string" 25 | }, 26 | { 27 | "name": "email", 28 | "type": "string" 29 | }, 30 | { 31 | "name": "exchange", 32 | "type": "string" 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /examples/datasets/stock-prices/companies_small_25.csv: -------------------------------------------------------------------------------- 1 | id;name;address;catch_phrase;ceo;phone;email;exchange 2 | GLEN;Glenn Ltd;5939 Brittney Point Apt. 066Cookville, GA 56492;Pre-emptive mission-critical instruction set;Aaron Martinez;7350577135;tiffany71@david-harrington.com;DAX 3 | WAR;Warren-Sanders;USCGC HarrisFPO AP 99012;Stand-alone multi-tasking matrices;Alexis Combs;(754)035-1324x710;garciaryan@warren-castillo.biz;SSX 4 | FI;Fields PLC;811 Ashley Junction Apt. 875Lake Monique, WV 07723;Re-engineered real-time utilization;Susan Jones;311-240-3292;thompsontiffany@sanchez.biz;DAX 5 | HAY;Hayden PLC;276 Hayley Radial Suite 411Brittanybury, MH 00755;Multi-channeled neutral time-frame;Anna Mclaughlin;571.085.5711;emily32@mays.net;DOW-JONES 6 | PETE;Petersen, Mendoza and Davidson;917 Lisa Plain Apt. 509South Robertfort, NV 93919;Expanded clear-thinking portal;Sheila Rogers;+1-782-138-5186x88889;jessica50@jenkins.net;NASDAQ 7 | MOOR;Moore-Anderson;76604 Santos RoadEast Victoriaborough, VI 86658;Proactive high-level task-force;Kevin Smith;4059610642;davidperez@duncan-jenkins.com;FTSE 8 | WIN;Winters and Sons;0600 Lloyd Pass Suite 690East James, NC 97773;Intuitive zero-defect firmware;George Smith;(370)669-7338x797;jennifer29@king.biz;NASDAQ 9 | ST;Steele-Hunt;0662 Stephanie LodgeSarabury, MN 46524;Quality-focused heuristic flexibility;Ryan Hunt;(591)368-6863;lrichardson@wells-myers.com;DAX 10 | DO;Douglas LLC;82625 Danny Route Suite 458Rebeccaburgh, NV 01954;Right-sized uniform superstructure;Jim Green;926-906-9968;mcculloughmichelle@anderson.com;DAX 11 | WE;Wells-Cooper;29085 Michael ViewsNew Lisafurt, FM 14607;Exclusive intermediate circuit;Steven Hanson;(432)719-1586x47588;simoncorey@briggs-dixon.com;DAX 12 | WHE;Wheeler-Cantrell;77418 Soto Ford Apt. 709Gardnerfurt, FM 30716;Sharable 4thgeneration open system;Kenneth Burgess;125.847.1169x58049;blackmichael@hanson-bush.com;DAX 13 | WOOD;Wood-Savage;21865 Blake Cliffs Apt. 814South Alexander, MO 85528;Vision-oriented even-keeled Local Area Network;Paula Ballard;868-426-6014x431;evan71@vincent-bond.com;DAX 14 | WH;White Inc;944 Janice Park Apt. 728Lake Stevenville, GU 58024;Advanced tangible data-warehouse;Terri Bailey;(348)256-1335x148;albert72@meadows.net;DOW-JONES 15 | ALVA;Alvarado, Butler and Reed;391 Mcneil Ramp Apt. 885East Tracy, TX 15699;Synergistic mission-critical structure;April Wagner;250-754-2012;michael69@hoffman.com;NASDAQ 16 | MOR;Morris-Martin;Unit 8769 Box 5881DPO AA 83041;Enhanced encompassing model;Brittany Sullivan;539.341.9520x297;jenniferbaker@rivera.org;SSX 17 | OLS;Olson, Wilson and Eaton;49759 Lauren CrestLisaville, NC 36529;Monitored system-worthy groupware;Jacob Roy;+1-176-636-8843x05715;awilson@brown-reeves.org;DAX 18 | HARR;Harris-Zimmerman;606 James SpurFrazierchester, IL 01897;Open-architected maximized data-warehouse;Theresa Holland;+1-970-741-1457x133;jasonday@potter.info;DAX 19 | YOU;Young, Holmes and Thompson;01845 Grant Haven Suite 033Georgestad, VI 91239;Versatile disintermediate implementation;Henry Bell;001-263-212-2929x834;amata@rush-washington.com;DOW-JONES 20 | KELL;Kelly, Randall and Cortez;851 Shannon MissionNew Williamton, MA 65809;Innovative actuating project;Charles Camacho;001-612-038-2023x15863;johnnyrodriguez@torres-soto.biz;DAX 21 | RUIZ;Ruiz-Watkins;9929 Zamora Locks Apt. 061Cassandraberg, VA 63104;Open-source heuristic infrastructure;Sean Garcia;006.979.7674x118;josephhunter@garcia.com;SSX 22 | CH;Chapman and Sons;04929 Sullivan Lakes Suite 572North Amy, TX 52037;Quality-focused foreground hierarchy;Traci Zimmerman;(596)047-9763x9279;watsonautumn@garcia.com;DOW-JONES 23 | COOK;Cook Group;593 Sosa Hill Apt. 165Richardsonburgh, NC 33493;Proactive multi-state architecture;Blake Morris;001-329-565-1914x19550;lisachandler@richardson.com;FTSE 24 | STAN;Stanton, Flores and Scott;6536 Jon Drives Apt. 332Lake Melissa, PW 28866;Multi-layered executive installation;Andrew Rogers;001-915-354-1873;davilacynthia@simpson-hernandez.com;FTSE 25 | NUNE;Nunez LLC;9095 Sutton Tunnel Apt. 653South Marc, KS 35593;Customizable cohesive core;Scott Barry;(834)737-9776x23500;reeseryan@andrews-walker.biz;DAX 26 | RIC;Rice-Sparks;13735 Charles SpringsDustinchester, TX 46226;Open-source homogeneous instruction set;Andrew Mason;958.591.7682x9536;gregorywalker@keller.com;SSX 27 | -------------------------------------------------------------------------------- /examples/datasets/stock-prices/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "tables": [ 3 | { 4 | "name": "companies", 5 | "columns": [ 6 | { 7 | "name": "id", 8 | "type": "string" 9 | }, 10 | { 11 | "name": "name", 12 | "type": "string" 13 | }, 14 | { 15 | "name": "address", 16 | "type": "string" 17 | }, 18 | { 19 | "name": "catch_phrase", 20 | "type": "string" 21 | }, 22 | { 23 | "name": "ceo", 24 | "type": "string" 25 | }, 26 | { 27 | "name": "phone", 28 | "type": "string" 29 | }, 30 | { 31 | "name": "email", 32 | "type": "string" 33 | }, 34 | { 35 | "name": "exchange", 36 | "type": "string" 37 | } 38 | ] 39 | }, 40 | { 41 | "name": "prices", 42 | "columns": [ 43 | { 44 | "name": "timestamp", 45 | "type": "datetime" 46 | }, 47 | { 48 | "name": "symbol", 49 | "type": "link", 50 | "link": { 51 | "table": "companies" 52 | } 53 | }, 54 | { 55 | "name": "price", 56 | "type": "float" 57 | }, 58 | { 59 | "name": "delta", 60 | "type": "float" 61 | }, 62 | { 63 | "name": "percentage", 64 | "type": "float" 65 | } 66 | ] 67 | } 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /examples/datasets/stock-prices/schema_companies.json: -------------------------------------------------------------------------------- 1 | { 2 | "tables": [ 3 | { 4 | "name": "Companies", 5 | "columns": [ 6 | { 7 | "name": "name", 8 | "type": "string" 9 | }, 10 | { 11 | "name": "address", 12 | "type": "string" 13 | }, 14 | { 15 | "name": "catch_phrase", 16 | "type": "string" 17 | }, 18 | { 19 | "name": "ceo", 20 | "type": "string" 21 | }, 22 | { 23 | "name": "phone", 24 | "type": "string" 25 | }, 26 | { 27 | "name": "email", 28 | "type": "string" 29 | }, 30 | { 31 | "name": "exchange", 32 | "type": "string" 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /examples/pagination.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | from xata.client import XataClient 21 | 22 | xata = XataClient() 23 | 24 | # Page through table `nba_teams` and get 5 records per page. 25 | # Sort by state asc, city asc and team name desc 26 | # Filter out teams that are not in the western conference 27 | # 28 | # Please refer to https://xata.io/docs/api-reference/db/db_branch_name/tables/table_name/query#query-table 29 | # for more options for options on pagination, sorting, filters, or query conditions 30 | 31 | # Initalize controls 32 | more = True 33 | cursor = None 34 | 35 | # Loop through the pages 36 | while more: 37 | # Build query statement 38 | query = { 39 | "columns": ["*"], # Return all columns 40 | "page": {"size": 5, "after": cursor}, # Page size # Cursor for next page 41 | "filter": {"conference": {"$is": "west"}}, # Filter for conference = west 42 | "sort": [ 43 | {"state": "asc"}, # sort by state asc 44 | {"city": "asc"}, # sort by city asc 45 | {"name": "desc"}, # sort by name desc 46 | ], 47 | } 48 | 49 | # Only the first request can have sorting defined. Every following 50 | # cursor request will have the sort implied by the first request 51 | if cursor: 52 | del query["sort"] 53 | del query["filter"] 54 | 55 | # Query the data 56 | resp = xata.data().query("nba_teams", query) 57 | 58 | # Print teams 59 | for team in resp["records"]: 60 | print("[%s] %s: %s, %s" % (team["conference"], team["state"], team["name"], team["city"])) 61 | 62 | # Update controls 63 | more = resp.has_more_results() # has another page with results 64 | cursor = resp.get_cursor() # save next cursor for results 65 | 66 | # Output: 67 | # [west] California: LA Lakers, Los Angeles 68 | # [west] California: LA Clippers, Los Angeles 69 | # [west] California: Sacramento Kings, Sacramento 70 | # [west] California: Golden State Warriors, San Francisco 71 | # [west] Colorado: Denver Nuggets, Denver 72 | # [west] Oklahoma: OKC Thunder, Oklahoma City 73 | # [west] Oregon: Portland Trailblazers, Portland 74 | # [west] Texas: Dallas Mavericks, Dallas 75 | # [west] Texas: Houston Rockets, Houston 76 | -------------------------------------------------------------------------------- /examples/rabbitmq/.env.sample: -------------------------------------------------------------------------------- 1 | # My API key -- CHANGE ME 2 | XATA_API_KEY=my-api-key 3 | 4 | # My Workspace Id -- CHANGE ME 5 | XATA_WORKSPACE_ID=my-ws-id 6 | 7 | # Database Name 8 | XATA_DB_NAME=examples 9 | 10 | # Region to use 11 | XATA_REGION=us-east-1 12 | 13 | # How many worker threads the Bulk Processor should have 14 | XATA_BP_THREADS=2 15 | 16 | # Timeout between forced flushes 17 | XATA_BP_FLUSH_INTERVAL=5 18 | 19 | # Min. records to ingest 20 | XATA_BP_BATCH_SIZE=10 21 | 22 | # RabbitMQ config, please don't change 23 | RABBITMQ_HOST=xata_examples_rabbitmq 24 | RABBITMQ_QUEUE=xata-examples-stock-prices 25 | 26 | # How many companies to generate 27 | N_COMPANIES=25 28 | 29 | # How many seconds between every price change 30 | N_TICK_INTERVAL=15 31 | -------------------------------------------------------------------------------- /examples/rabbitmq/Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | run: ## Run the system 4 | docker-compose --compatibility up --no-deps --build 5 | 6 | clean: ## Run Example 7 | docker-compose rm --stop --force xata_examples_rabbitmq xata_examples_producer xata_examples_consumers 8 | docker-compose rm --force --volumes xata_examples_rabbitmq xata_examples_producer xata_examples_consumers 9 | 10 | setup: ## Setup Xata for running the example 11 | pip3 install -r requirements.txt 12 | python3 install.py 13 | 14 | help: ## Display help 15 | @awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 16 | #------------- -------------- 17 | 18 | .DEFAULT_GOAL := help 19 | -------------------------------------------------------------------------------- /examples/rabbitmq/README.md: -------------------------------------------------------------------------------- 1 | # Reading and Storing Stock Prices from RabbitMQ in Xata 2 | 3 | This example is intended to show you how to read events from a [RabbitMQ](https://rabbitmq.com) queue and store them in [Xata](https://xata.io). 4 | We are generating fictive companies and simulate stock prices for each company, whom move up and down with each tick. 5 | 6 | A **producer** creates the companies and pushes every `env.N_TICK_INTERVAL` seconds stock price changes to a queue. 7 | 8 | Multiple **consumers** read from the queue and ingest the data in Xata with the [`BulkProcessor`](https://xata.io/docs/python-sdk/bulk-processor). 9 | 10 | ## Prerequisites 11 | 12 | - You need to have a Xata account, please use this link [here](https://app.xata.io/) to create one. You can use your Gmail or GitHub accounts to sign in. 13 | - An API Key to access Xata, please follow the instructions [how to generate an API key](https://xata.io/docs/getting-started/api-keys). 14 | 15 | ## Getting Started 16 | 17 | Three steps are required in order to run this example: 18 | 19 | 1. Rename `.env.sample` to `.env`, or make a copy. 20 | 2. Personalize the values for `XATA_API_KEY` and `XATA_WORKSPACE_ID` in the `.env`. 21 | 3. Run `make setup`. This will create the necessary tables. 22 | 23 | You're all set to run the example! 24 | 25 | ## Run the Example 26 | 27 | Open the Xata UI to see data trickle in. Next, open a terminal window and enter `make run` in this directory to let the example "run". 28 | -------------------------------------------------------------------------------- /examples/rabbitmq/consumer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim-buster 2 | 3 | RUN pip install --upgrade pip 4 | 5 | WORKDIR /consumer-app 6 | 7 | COPY requirements.txt requirements.txt 8 | COPY consumer.py consumer.py 9 | 10 | RUN pip install --user -r requirements.txt 11 | 12 | CMD ["python", "consumer.py"] 13 | -------------------------------------------------------------------------------- /examples/rabbitmq/consumer/consumer.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import json 21 | import logging 22 | import os 23 | import sys 24 | 25 | import pika 26 | 27 | from xata.client import XataClient 28 | from xata.helpers import BulkProcessor 29 | 30 | logging.basicConfig(format="%(asctime)s [%(process)d] %(levelname)s: %(message)s", level=logging.INFO) 31 | 32 | RABBITMQ_HOST = os.environ.get("RABBITMQ_HOST", "localhost") 33 | RABBITMQ_QUEUE = os.environ.get("RABBITMQ_QUEUE", "task_queue") 34 | 35 | # setup Xata 36 | xata = XataClient(db_name=os.environ.get("XATA_DB_NAME")) 37 | bp = BulkProcessor( 38 | xata, 39 | thread_pool_size=int(os.environ.get("XATA_BP_THREADS")), 40 | flush_interval=int(os.environ.get("XATA_BP_FLUSH_INTERVAL")), 41 | batch_size=int(os.environ.get("XATA_BP_BATCH_SIZE")), 42 | ) 43 | 44 | 45 | def callback(ch, method, props, body): 46 | msg = json.loads(body.decode()) 47 | ch.basic_ack(delivery_tag=method.delivery_tag) 48 | bp.put_record(msg["table"], msg["record"]) 49 | 50 | logging.debug("received message: %s" % msg) 51 | 52 | 53 | if __name__ == "__main__": 54 | try: 55 | connection = pika.BlockingConnection(pika.ConnectionParameters(host=RABBITMQ_HOST)) 56 | channel = connection.channel() 57 | channel.queue_declare(queue=RABBITMQ_QUEUE, durable=True) 58 | channel.basic_qos(prefetch_count=1) 59 | channel.basic_consume(queue=RABBITMQ_QUEUE, on_message_callback=callback) 60 | logging.info("ready to start ingesting ..") 61 | channel.start_consuming() 62 | except KeyboardInterrupt: 63 | print("done.") 64 | connection.close() 65 | bp.flush_queue() 66 | sys.exit(0) 67 | -------------------------------------------------------------------------------- /examples/rabbitmq/consumer/requirements.txt: -------------------------------------------------------------------------------- 1 | pika==1.* 2 | xata 3 | -------------------------------------------------------------------------------- /examples/rabbitmq/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.2" 2 | services: 3 | xata_examples_rabbitmq: 4 | image: rabbitmq:3-management-alpine 5 | ports: 6 | - 5672:5672 7 | - 15672:15672 8 | volumes: 9 | - ~/.docker-conf/xata_examples_rabbitmq/data/:/var/lib/rabbitmq/ 10 | - ~/.docker-conf/xata_examples_rabbitmq/log/:/var/log/rabbitmq 11 | networks: 12 | - xata_examples_stock_prices 13 | healthcheck: 14 | test: rabbitmq-diagnostics check_port_connectivity 15 | interval: 5s 16 | timeout: 30s 17 | retries: 4 18 | 19 | xata_examples_producer: 20 | restart: always 21 | build: 22 | context: ./producer 23 | dockerfile: Dockerfile 24 | depends_on: 25 | xata_examples_rabbitmq: 26 | condition: service_healthy 27 | volumes: 28 | - ~/.docker-conf/xata_examples_producer/:/usr/src/app 29 | environment: 30 | - RABBITMQ_HOST 31 | - RABBITMQ_QUEUE 32 | - N_COMPANIES 33 | - N_TICK_INTERVAL 34 | networks: 35 | - xata_examples_stock_prices 36 | 37 | xata_examples_consumers: 38 | restart: always 39 | build: 40 | context: ./consumer 41 | dockerfile: Dockerfile 42 | depends_on: 43 | xata_examples_rabbitmq: 44 | condition: service_healthy 45 | volumes: 46 | - ~/.docker-conf/xata_examples_consumers/:/usr/src/app 47 | environment: 48 | - XATA_API_KEY 49 | - XATA_WORKSPACE_ID 50 | - XATA_DB_NAME 51 | - XATA_REGION 52 | - RABBITMQ_HOST 53 | - RABBITMQ_QUEUE 54 | - XATA_BP_THREADS 55 | - XATA_BP_FLUSH_INTERVAL 56 | - XATA_BP_BATCH_SIZE 57 | networks: 58 | - xata_examples_stock_prices 59 | deploy: 60 | mode: replicated 61 | replicas: 2 62 | 63 | networks: 64 | xata_examples_stock_prices: 65 | driver: bridge 66 | -------------------------------------------------------------------------------- /examples/rabbitmq/install.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import logging 21 | import os 22 | 23 | from dotenv import load_dotenv 24 | 25 | from xata.client import XataClient 26 | 27 | logging.basicConfig(format="%(asctime)s [%(process)d] %(levelname)s: %(message)s", level=logging.INFO) 28 | load_dotenv() 29 | 30 | XATA_DATABASE_NAME = os.environ.get("XATA_DB_NAME") 31 | assert XATA_DATABASE_NAME, "env.XATA_DB_NAME not set." 32 | 33 | xata = XataClient(db_name=XATA_DATABASE_NAME) 34 | 35 | logging.info("checking credentials ..") 36 | assert xata.users().getUser().status_code == 200, "Unable to connect to Xata. Please check credentials" 37 | 38 | logging.info("checking if database exists ..") 39 | r = xata.databases().getDatabaseMetadata(XATA_DATABASE_NAME) 40 | if r.status_code == 404: 41 | logging.info("creating database '%s' .." % XATA_DATABASE_NAME) 42 | r = xata.databases().create(XATA_DATABASE_NAME, {"region": xata.get_config()["region"]}) 43 | assert r.status_code == 201, "Unable to create database '%s': %s" % (XATA_DATABASE_NAME, r.json()) 44 | else: 45 | logging.info("database '%s' found, skipping creation step." % XATA_DATABASE_NAME) 46 | 47 | logging.info("creating table 'companies' ..") 48 | r = xata.table().create("companies") 49 | assert r.status_code == 201, "Unable to create table 'companies'" 50 | 51 | logging.info("creating table 'prices' ..") 52 | r = xata.table().create("prices") 53 | assert r.status_code == 201, "Unable to create table 'prices'" 54 | 55 | logging.info("setting table schema of table 'companies' ..") 56 | r = xata.table().setSchema( 57 | "companies", 58 | { 59 | "columns": [ 60 | {"name": "name", "type": "string"}, 61 | {"name": "address", "type": "string"}, 62 | {"name": "catch_phrase", "type": "string"}, 63 | {"name": "ceo", "type": "string"}, 64 | {"name": "phone", "type": "string"}, 65 | {"name": "email", "type": "string"}, 66 | {"name": "exchange", "type": "string"}, 67 | { 68 | "name": "created_at", 69 | "type": "datetime", 70 | "notNull": True, 71 | "defaultValue": "now", 72 | }, 73 | ] 74 | }, 75 | ) 76 | assert r.status_code == 200, "Unable to set table schema: %s" % r.json() 77 | 78 | logging.info("setting table schema of table 'prices' ..") 79 | r = xata.table().setSchema( 80 | "prices", 81 | { 82 | "columns": [ 83 | {"name": "symbol", "type": "link", "link": {"table": "companies"}}, 84 | {"name": "timestamp", "type": "datetime"}, 85 | {"name": "price", "type": "float"}, 86 | {"name": "delta", "type": "float"}, 87 | {"name": "percentage", "type": "float"}, 88 | { 89 | "name": "created_at", 90 | "type": "datetime", 91 | "notNull": True, 92 | "defaultValue": "now", 93 | }, 94 | ] 95 | }, 96 | ) 97 | assert r.status_code == 200, "Unable to set table schema: %s" % r.json() 98 | 99 | logging.info("setup done.") 100 | cfg = xata.get_config() 101 | url = "https://app.xata.io/workspaces/%s/dbs/%s:%s" % (cfg["workspaceId"], cfg["dbName"], cfg["region"]) 102 | logging.info(f"UI: {url}") 103 | logging.info("`make run` to start generating events") 104 | -------------------------------------------------------------------------------- /examples/rabbitmq/producer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim-buster 2 | 3 | RUN pip install --upgrade pip 4 | 5 | WORKDIR /producer-app 6 | 7 | COPY requirements.txt requirements.txt 8 | COPY producer.py producer.py 9 | 10 | RUN pip install --user -r requirements.txt 11 | 12 | CMD ["python", "producer.py"] 13 | -------------------------------------------------------------------------------- /examples/rabbitmq/producer/producer.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | 21 | import json 22 | import logging 23 | import os 24 | import random 25 | import sys 26 | import time 27 | from datetime import datetime, timezone 28 | from random import randrange 29 | 30 | import pika 31 | from faker import Faker 32 | 33 | logging.basicConfig(format="%(asctime)s [%(process)d] %(levelname)s: %(message)s", level=logging.INFO) 34 | 35 | N_COMPANIES = int(os.environ.get("N_COMPANIES", 25)) 36 | N_TICK_INTERVAL = int(os.environ.get("N_TICK_INTERVAL", 15)) 37 | RABBITMQ_HOST = os.environ.get("RABBITMQ_HOST", "localhost") 38 | RABBITMQ_QUEUE = os.environ.get("RABBITMQ_QUEUE", "task_queue") 39 | EXCHANGES = ["DAX", "DOW-JONES", "NASDAQ", "FTSE", "DAX", "SSX"] 40 | 41 | fake = Faker() 42 | 43 | # setup RabbitMQ 44 | connection = pika.BlockingConnection(pika.ConnectionParameters(host=RABBITMQ_HOST)) 45 | channel = connection.channel() 46 | channel.queue_declare(queue=RABBITMQ_QUEUE, durable=True) 47 | 48 | 49 | def main(): 50 | # Generate company symbols 51 | logging.info("generating %d companies.", N_COMPANIES) 52 | companies = {} 53 | while len(companies.keys()) < N_COMPANIES: 54 | name = fake.company() 55 | symbol = name[0 : randrange(2, 5)].upper() 56 | if symbol.isalnum() and symbol not in companies: 57 | companies[symbol] = { 58 | "id": symbol, 59 | "name": name, 60 | "address": fake.address(), 61 | "catch_phrase": fake.catch_phrase(), 62 | "ceo": fake.name(), 63 | "phone": fake.phone_number(), 64 | "email": fake.company_email(), 65 | "exchange": random.choice(EXCHANGES), 66 | } 67 | 68 | msg = json.dumps({"table": "companies", "record": companies[symbol]}) 69 | channel.basic_publish( 70 | exchange="", 71 | routing_key=RABBITMQ_QUEUE, 72 | body=msg, 73 | properties=pika.BasicProperties(delivery_mode=pika.spec.PERSISTENT_DELIVERY_MODE), 74 | ) 75 | # wait for consistency 76 | time.sleep(N_TICK_INTERVAL) 77 | 78 | # Init Prices 79 | logging.info("generate baseline for individual stock prices.") 80 | prices = {} 81 | for key in companies.keys(): 82 | prices[key] = { 83 | "timestamp": datetime.now(timezone.utc).astimezone().isoformat(), 84 | "symbol": key, 85 | "price": round(randrange(100, 9999) / randrange(9, 99), 3), 86 | "delta": 0.0, 87 | "percentage": 0.0, 88 | } 89 | 90 | msg = json.dumps({"table": "prices", "record": prices[key]}) 91 | channel.basic_publish( 92 | exchange="", 93 | routing_key=RABBITMQ_QUEUE, 94 | body=msg, 95 | properties=pika.BasicProperties(delivery_mode=pika.spec.PERSISTENT_DELIVERY_MODE), 96 | ) 97 | 98 | # Move prices 99 | while True: 100 | time.sleep(N_TICK_INTERVAL) 101 | logging.info("generating price movements.") 102 | for key in companies.keys(): 103 | c = (randrange(1, 111) / 100) * (-1 if fake.boolean(chance_of_getting_true=25) else 1) 104 | previous_price = float(prices[key]["price"]) 105 | p = previous_price + ((previous_price / 100) * c) 106 | d = p - previous_price 107 | prices[key] = { 108 | "timestamp": datetime.now(timezone.utc).astimezone().isoformat(), 109 | "symbol": key, 110 | "price": round(p, 3), 111 | "delta": round(d, 3), 112 | "percentage": c, 113 | } 114 | 115 | msg = json.dumps({"table": "prices", "record": prices[key]}) 116 | channel.basic_publish( 117 | exchange="", 118 | routing_key=RABBITMQ_QUEUE, 119 | body=msg, 120 | properties=pika.BasicProperties(delivery_mode=pika.spec.PERSISTENT_DELIVERY_MODE), 121 | ) 122 | 123 | 124 | if __name__ == "__main__": 125 | try: 126 | main() 127 | except KeyboardInterrupt: 128 | print("done.") 129 | connection.close() 130 | sys.exit(0) 131 | -------------------------------------------------------------------------------- /examples/rabbitmq/producer/requirements.txt: -------------------------------------------------------------------------------- 1 | pika==1.* 2 | Faker==16.* 3 | -------------------------------------------------------------------------------- /examples/rabbitmq/requirements.txt: -------------------------------------------------------------------------------- 1 | xata 2 | python-dotenv 3 | -------------------------------------------------------------------------------- /examples/transactions.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | from xata.client import XataClient 21 | from xata.helpers import Transaction 22 | 23 | # We can now add insert, update, delete or get operations to 24 | # the transaction helper. Max is 1000, if you exceed this treshold 25 | # an exception is thrown. 26 | # Please ensure the SDK version is > 0.10.0 27 | 28 | xata = XataClient() 29 | trx = Transaction(xata) 30 | 31 | # We want to get records by their id from the table "Avengers" 32 | trx.get("Marvel", "hulk") 33 | trx.get("Marvel", "spiderman", ["name"]) # return the name column 34 | trx.get("Marvel", "dr_who") # this record does not exist 35 | 36 | # Update the record of batman in table DC 37 | trx.update("DC", "batman", {"name": "Bruce Wayne"}) 38 | 39 | # Delete records from different tables 40 | trx.delete("nba_teams", "seattle_super_sonics") 41 | trx.delete("nba_teams", "fc_barcelona", ["name", "city"]) # return two fields before deleting 42 | trx.delete("nba_teams", "dallas_cowboys") # does not exist 43 | 44 | # Insert records 45 | trx.insert("Marvel", {"id": "dr_strange", "name": "Dr Stephen Strange"}) 46 | 47 | # How many transaction operations are scheduled? 48 | print(trx.size()) 49 | # 8 50 | 51 | # Run the Transactions 52 | result = trx.run() 53 | print(result) 54 | 55 | # The order of the results is the same as the transaction operations 56 | # Meaning, you can always reference back. 57 | # { 58 | # 'status_code': 200, 59 | # 'results': [ 60 | # {'columns': {'id': 'hulk', 'xata': {'version': 0}}, 'operation': 'get'}, 61 | # {'columns': {'name': 'Peter Parker'}, 'operation': 'get'}, 62 | # {'columns': {}, 'operation': 'get'}, 63 | # {'columns': {}, 'id': 'batman', 'operation': 'update', 'rows': 1}, 64 | # {'operation': 'delete', 'rows': 1}, 65 | # {'columns': {'city': 'Barcelona', 'name': 'FC Barcelona'}, 'operation': 'delete', 'rows': 1}, 66 | # {'operation': 'delete', 'rows': 0}, 67 | # {'columns': {}, 'id': 'dr_strange', 'operation': 'insert', 'rows': 1} 68 | # ], 69 | # 'has_errors': False, 70 | # 'errors': [] 71 | # } 72 | 73 | # Transactions are flushed after a `run` 74 | print(trx.size()) 75 | # 0 76 | 77 | # If the transaction has error it's aborted and no operation is executed 78 | trx.insert("Marvel", {"field_that_does_not_exist": "superpower"}) 79 | trx.get("Marvel", "hulk") 80 | results = trx.run() 81 | 82 | print(results) 83 | # { 84 | # 'status_code': 400, 85 | # 'results': [], 86 | # 'has_errors': True, 87 | # 'errors': [ 88 | # {'index': 0, 'message': 'table [Marvel]: invalid record: column [field_that_does_not_exist]: column not found'} 89 | # ] 90 | # } 91 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "xata" 3 | version = "1.3.5" 4 | description = "Python SDK for Xata.io" 5 | authors = ["Xata "] 6 | license = "Apache-2.0" 7 | readme = "README_PYPI.md" 8 | documentation = "https://xata-py.readthedocs.io" 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.8" 12 | requests = "^2.28.1" 13 | python-dotenv = ">=0.21,<2.0" 14 | orjson = "^3.8.1" 15 | deprecation = "^2.1.0" 16 | 17 | [tool.poetry.group.dev.dependencies] 18 | pytest = "^7.2.1" 19 | pytest-cov = "^4.0.0" 20 | black = ">=22.12,<25.0" 21 | flake8 = "^5.0.4" 22 | pre-commit = "^2.21.0" 23 | isort = "^5.12.0" 24 | flake8-bugbear = "^22.10.27" 25 | flake8-annotations = "^2.9.1" 26 | pdoc3 = "^0.10.0" 27 | Faker = "^17.0.0" 28 | sphinx-rtd-theme = "^1.2.0" 29 | mako = "^1.2.4" 30 | pytz = "^2022.7.1" 31 | Pillow = ">=9.5,<11.0" 32 | python-magic = "^0.4.22" 33 | pep8-naming = "^0.13.3" 34 | 35 | [build-system] 36 | requires = ["poetry-core"] 37 | build-backend = "poetry.core.masonry.api" 38 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | ignore = 4 | E203, 5 | W503, 6 | ANN101 # Typing of self is not required 7 | 8 | [isort] 9 | profile = black 10 | -------------------------------------------------------------------------------- /tests/data/attachments/archives/assets.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xataio/xata-py/11f9b183503de14923b655fef2c8f3ab63b7eb7d/tests/data/attachments/archives/assets.zip -------------------------------------------------------------------------------- /tests/data/attachments/images/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xataio/xata-py/11f9b183503de14923b655fef2c8f3ab63b7eb7d/tests/data/attachments/images/03.png -------------------------------------------------------------------------------- /tests/data/attachments/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/integration-tests/api_request_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import utils 21 | 22 | from xata.client import XataClient 23 | 24 | 25 | class TestApiRequest(object): 26 | def test_direct_instance_in_namespace_without_client_dependencies(self): 27 | """ 28 | Direct namespace invocation without implicit config 29 | :link https://github.com/xataio/xata-py/issues/57 30 | """ 31 | users = XataClient().users() 32 | r1 = users.get() 33 | assert r1.is_success() 34 | 35 | client = XataClient() 36 | r2 = client.users().get() 37 | assert r2.is_success() 38 | 39 | assert r1 == r2 40 | 41 | def test_direct_instance_in_namespace_wit_client_dependencies(self): 42 | """ 43 | Direct namespace invocation with dependency on internal client config 44 | :link https://github.com/xataio/xata-py/issues/57 45 | """ 46 | db_name = utils.get_db_name() 47 | databases = XataClient().databases() 48 | 49 | # workspace_id should be provided by the the client implicitly 50 | assert databases.create(db_name, region="eu-west-1").is_success() 51 | assert databases.get_metadata(db_name).is_success() 52 | assert databases.delete(db_name).is_success() 53 | -------------------------------------------------------------------------------- /tests/integration-tests/api_response_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import utils 21 | 22 | from xata.client import XataClient 23 | 24 | 25 | class TestApiResponse(object): 26 | def setup_class(self): 27 | self.db_name = utils.get_db_name() 28 | self.client = XataClient(db_name=self.db_name) 29 | assert self.client.databases().create(self.db_name).is_success() 30 | assert self.client.table().create("Posts").is_success() 31 | assert self.client.table().set_schema("Posts", utils.get_posts_schema()).is_success() 32 | 33 | payload = { 34 | "operations": [ 35 | {"insert": {"table": "Posts", "record": utils.get_post()}}, 36 | {"insert": {"table": "Posts", "record": utils.get_post()}}, 37 | {"insert": {"table": "Posts", "record": utils.get_post()}}, 38 | {"insert": {"table": "Posts", "record": utils.get_post()}}, 39 | {"insert": {"table": "Posts", "record": utils.get_post()}}, 40 | {"insert": {"table": "Posts", "record": utils.get_post()}}, 41 | {"insert": {"table": "Posts", "record": utils.get_post()}}, 42 | {"insert": {"table": "Posts", "record": utils.get_post()}}, 43 | {"insert": {"table": "Posts", "record": utils.get_post()}}, 44 | {"insert": {"table": "Posts", "record": utils.get_post()}}, 45 | ] 46 | } 47 | assert self.client.records().transaction(payload).is_success() 48 | 49 | def teardown_class(self): 50 | assert self.client.databases().delete(self.db_name).is_success() 51 | 52 | def test_is_success_true(self): 53 | user = XataClient().users().get() 54 | assert user.is_success() 55 | assert user.status_code >= 200 56 | assert user.status_code < 300 57 | 58 | def test_is_success_false_with_unknown_table(self): 59 | user = XataClient().records().get("Nope", "nope^2") 60 | assert not user.is_success() 61 | assert user.status_code == 404 62 | 63 | def test_direct_repr_and_json_are_the_same(self): 64 | user = XataClient().users().get() 65 | assert user.is_success() 66 | assert user == user.json() 67 | 68 | def test_get_cursor_and_has_more_results(self): 69 | query = { 70 | "columns": ["*"], 71 | "page": {"size": 9, "after": None}, 72 | } 73 | posts = self.client.data().query("Posts", query) 74 | assert posts.is_success() 75 | assert len(posts["records"]) == 9 76 | assert posts.has_more_results() 77 | assert posts.get_cursor() is not None 78 | 79 | query = { 80 | "columns": ["*"], 81 | "page": {"size": 6, "after": posts.get_cursor()}, 82 | } 83 | posts = self.client.data().query("Posts", query) 84 | assert posts.is_success() 85 | assert len(posts["records"]) == 1 86 | assert not posts.has_more_results() 87 | 88 | def test_error_message(self): 89 | user = self.client.records().get("Nope", "nope^2") 90 | assert not user.is_success() 91 | assert user.status_code > 299 92 | assert user.error_message is not None 93 | assert user.error_message.endswith("not found") 94 | 95 | def test_error_message_should_not_be_set(self): 96 | user = self.client.users().get() 97 | assert user.is_success() 98 | assert user.status_code < 300 99 | assert not user.error_message 100 | -------------------------------------------------------------------------------- /tests/integration-tests/authentication_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import utils 21 | 22 | from xata.client import XataClient 23 | 24 | 25 | class TestAuthenticateNamespace(object): 26 | def setup_class(self): 27 | self.db_name = utils.get_db_name() 28 | self.branch_name = "main" 29 | self.new_api_key = "one-key-to-rule-them-all-%s" % utils.get_random_string(6) 30 | self.client = XataClient(db_name=self.db_name, branch_name=self.branch_name) 31 | 32 | def test_get_user_api_keys(self): 33 | r = self.client.authentication().get_user_api_keys() 34 | assert r.is_success() 35 | assert "keys" in r 36 | assert len(r["keys"]) > 0 37 | assert "name" in r["keys"][0] 38 | assert "createdAt" in r["keys"][0] 39 | 40 | def test_create_user_api_keys(self): 41 | r = self.client.authentication().get_user_api_keys() 42 | assert r.is_success() 43 | count = len(r["keys"]) 44 | 45 | r = self.client.authentication().create_user_api_keys(self.new_api_key) 46 | assert r.is_success() 47 | assert "name" in r 48 | assert "key" in r 49 | assert "createdAt" in r 50 | assert self.new_api_key == r["name"] 51 | 52 | r = self.client.authentication().get_user_api_keys() 53 | assert len(r["keys"]) == (count + 1) 54 | 55 | r = self.client.authentication().create_user_api_keys(self.new_api_key) 56 | assert not r.is_success() 57 | 58 | r = self.client.authentication().create_user_api_keys("") 59 | assert not r.is_success() 60 | 61 | def test_delete_user_api_key(self): 62 | r = self.client.authentication().delete_user_api_keys(self.new_api_key) 63 | assert r.is_success() 64 | 65 | r = self.client.authentication().delete_user_api_keys("NonExistingApiKey") 66 | assert not r.is_success() 67 | assert r.status_code == 404 68 | -------------------------------------------------------------------------------- /tests/integration-tests/conftest.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import pytest 21 | 22 | 23 | def pytest_configure(): 24 | pytest.workspaces = { 25 | "workspace": None, 26 | "member": None, 27 | } 28 | pytest.branch = { 29 | "branch": None, 30 | } 31 | pytest.table = { 32 | "client": None, 33 | "db_name": None, 34 | } 35 | pytest.branch_transactions = { 36 | "record_ids": [], 37 | "hardcoded_ids": ["1", "2", "42"], 38 | } 39 | -------------------------------------------------------------------------------- /tests/integration-tests/databases_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import pytest 21 | import utils 22 | 23 | from xata.client import XataClient 24 | from xata.errors import UnauthorizedError 25 | 26 | 27 | class TestDatabasesNamespace(object): 28 | def setup_class(self): 29 | self.db_name = utils.get_db_name() 30 | self.branch_name = "main" 31 | self.client = XataClient(db_name=self.db_name, branch_name=self.branch_name) 32 | 33 | def test_create_database(self): 34 | assert self.client.databases().create(self.db_name).is_success() 35 | 36 | def test_list_databases(self): 37 | r = self.client.databases().list() 38 | assert r.is_success() 39 | assert "databases" in r 40 | assert len(r["databases"]) > 0 41 | assert "name" in r["databases"][0] 42 | assert "region" in r["databases"][0] 43 | assert "createdAt" in r["databases"][0] 44 | 45 | # TODO find db record in list 46 | # assert r["databases"][0]["name"] == self.db_name 47 | # assert r["databases"][0]["region"] == self.get_config()["region"] 48 | 49 | with pytest.raises(UnauthorizedError) as e: 50 | self.client.databases().list("NonExistingWorkspaceId") 51 | assert str(e.value)[0:23] == "code: 401, unauthorized" 52 | 53 | def test_get_database_metadata(self): 54 | r = self.client.databases().get_metadata(self.db_name) 55 | assert r.is_success() 56 | assert "name" in r 57 | assert "region" in r 58 | assert "createdAt" in r 59 | assert r["name"] == self.db_name 60 | assert r["region"] == self.client.get_config()["region"] 61 | 62 | with pytest.raises(UnauthorizedError) as e: 63 | self.client.databases().get_metadata(self.db_name, workspace_id="NonExistingWorkspaceId") 64 | assert str(e.value)[0:23] == "code: 401, unauthorized" 65 | 66 | r = self.client.databases().get_metadata("NonExistingDatabase") 67 | assert r.status_code == 404 68 | 69 | def test_update_database_metadata(self): 70 | metadata = {"ui": {"color": "green"}} 71 | r_old = self.client.databases().get_metadata(self.db_name) 72 | assert r_old.is_success() 73 | r_new = self.client.databases().update_metadata(self.db_name, metadata) 74 | assert r_new.is_success() 75 | assert "name" in r_new 76 | assert "region" in r_new 77 | assert "createdAt" in r_new 78 | assert "ui" in r_new 79 | assert r_new["name"] == self.db_name 80 | assert r_new["region"] == self.client.get_config()["region"] 81 | assert r_old != r_new 82 | assert r_new["ui"] == metadata["ui"] 83 | 84 | r = self.client.databases().update_metadata(self.db_name, {}) 85 | assert r.status_code == 400 86 | r = self.client.databases().update_metadata("NonExistingDatabase", metadata) 87 | assert r.status_code == 400 88 | 89 | with pytest.raises(UnauthorizedError) as e: 90 | self.client.databases().update_metadata(self.db_name, metadata, workspace_id="NonExistingWorkspaceId") 91 | assert str(e.value)[0:23] == "code: 401, unauthorized" 92 | 93 | def test_delete_database(self): 94 | r = self.client.databases().delete(self.db_name) 95 | assert r.is_success() 96 | assert r["status"] == "completed" 97 | 98 | r = self.client.databases().delete("NonExistingDatabase") 99 | assert r.status_code == 404 100 | 101 | with pytest.raises(UnauthorizedError) as e: 102 | self.client.databases().delete(self.db_name, workspace_id="NonExistingWorkspaceId") 103 | assert str(e.value)[0:23] == "code: 401, unauthorized" 104 | 105 | def test_rename_database(self): 106 | h = utils.get_random_string(6) 107 | new_name = "rename_new-db-test-%s" % h 108 | orig_name = "rename_orig-db-test-%s" % h 109 | assert self.client.databases().create(orig_name).is_success() 110 | 111 | r = self.client.databases().rename(orig_name, new_name) 112 | assert r.is_success() 113 | assert "name" in r 114 | assert "region" in r 115 | assert r["name"] == new_name 116 | assert r["region"] == self.client.get_region() 117 | 118 | assert not self.client.databases().rename("NonExistingDatabase", new_name).is_success() 119 | assert not self.client.databases().rename("NonExistingDatabase", orig_name).is_success() 120 | 121 | with pytest.raises(UnauthorizedError) as e: 122 | self.client.databases().rename(new_name, orig_name, workspace_id="NonExistingWorkspaceId") 123 | assert str(e.value)[0:23] == "code: 401, unauthorized" 124 | 125 | assert self.client.databases().delete(new_name).is_success() 126 | 127 | def test_get_available_regions(self): 128 | r = self.client.databases().get_regions() 129 | assert r.is_success() 130 | assert "regions" in r 131 | assert len(r["regions"]) == 5 132 | assert "id" in r["regions"][0] 133 | assert "name" in r["regions"][0] 134 | 135 | with pytest.raises(UnauthorizedError) as e: 136 | self.client.databases().get_regions(workspace_id="NonExistingWorkspaceId") 137 | assert str(e.value)[0:23] == "code: 401, unauthorized" 138 | -------------------------------------------------------------------------------- /tests/integration-tests/files_access.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import time 21 | 22 | import utils 23 | from requests import request 24 | 25 | from xata.client import XataClient 26 | 27 | 28 | class TestFilesAccess(object): 29 | def setup_class(self): 30 | self.db_name = utils.get_db_name() 31 | self.branch_name = "main" 32 | self.client = XataClient(db_name=self.db_name, branch_name=self.branch_name) 33 | self.fake = utils.get_faker() 34 | 35 | assert self.client.databases().create(self.db_name).is_success() 36 | assert self.client.table().create("Attachments").is_success() 37 | assert ( 38 | self.client.table() 39 | .set_schema( 40 | "Attachments", 41 | utils.get_attachments_schema(), 42 | db_name=self.db_name, 43 | branch_name=self.branch_name, 44 | ) 45 | .is_success() 46 | ) 47 | 48 | def teardown_class(self): 49 | assert self.client.databases().delete(self.db_name).is_success() 50 | 51 | def test_public_flag_true(self): 52 | payload = { 53 | "title": self.fake.catch_phrase(), 54 | "one_file": utils.get_file("images/01.gif", public_url=True), 55 | } 56 | r = self.client.records().insert("Attachments", payload) 57 | assert r.is_success() 58 | rid = r["id"] 59 | 60 | file = self.client.records().get("Attachments", rid, columns=["one_file.signedUrl", "one_file.url"]) 61 | assert file.is_success() 62 | assert "signedUrl" in file["one_file"] 63 | assert "url" in file["one_file"] 64 | 65 | proof_public = request("GET", file["one_file"]["url"]) 66 | assert proof_public.status_code == 200 67 | 68 | proof_signed = request("GET", file["one_file"]["signedUrl"]) 69 | assert proof_signed.status_code == 200 70 | 71 | img = utils.get_file_content(utils.get_file_name("images/01.gif")) 72 | assert img == proof_public.content 73 | assert img == proof_signed.content 74 | 75 | def test_public_flag_false(self): 76 | payload = { 77 | "title": self.fake.catch_phrase(), 78 | "one_file": utils.get_file("images/01.gif", public_url=False), 79 | } 80 | r = self.client.records().insert("Attachments", payload) 81 | assert r.is_success() 82 | rid = r["id"] 83 | 84 | file = self.client.records().get("Attachments", rid, columns=["one_file.signedUrl", "one_file.url"]) 85 | assert file.is_success() 86 | assert "signedUrl" in file["one_file"] 87 | assert "url" in file["one_file"] 88 | 89 | proof_public = request("GET", file["one_file"]["url"]) 90 | assert proof_public.status_code == 403 91 | 92 | proof_signed = request("GET", file["one_file"]["signedUrl"]) 93 | assert proof_signed.status_code == 200 94 | 95 | img = utils.get_file_content(utils.get_file_name("images/01.gif")) 96 | assert img == proof_signed.content 97 | 98 | def test_signed_url_expired(self): 99 | payload = { 100 | "title": self.fake.catch_phrase(), 101 | "one_file": utils.get_file("images/01.gif", public_url=False, signed_url_timeout=1), 102 | } 103 | r = self.client.records().insert("Attachments", payload) 104 | assert r.is_success() 105 | rid = r["id"] 106 | 107 | file = self.client.records().get("Attachments", rid, columns=["one_file.signedUrl"]) 108 | assert file.is_success() 109 | assert "signedUrl" in file["one_file"] 110 | 111 | time.sleep(1) 112 | 113 | proof_signed = request("GET", file["one_file"]["signedUrl"]) 114 | assert proof_signed.status_code == 403 115 | -------------------------------------------------------------------------------- /tests/integration-tests/files_multiple_files_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import utils 21 | from requests import request 22 | 23 | from xata.client import XataClient 24 | 25 | 26 | class TestFilesMultipleFiles(object): 27 | def setup_class(self): 28 | self.db_name = utils.get_db_name() 29 | self.branch_name = "main" 30 | self.client = XataClient(db_name=self.db_name, branch_name=self.branch_name) 31 | self.fake = utils.get_faker() 32 | 33 | assert self.client.databases().create(self.db_name).is_success() 34 | assert self.client.table().create("Attachments").is_success() 35 | assert ( 36 | self.client.table() 37 | .set_schema( 38 | "Attachments", 39 | utils.get_attachments_schema(), 40 | db_name=self.db_name, 41 | branch_name=self.branch_name, 42 | ) 43 | .is_success() 44 | ) 45 | 46 | def teardown_class(self): 47 | assert self.client.databases().delete(self.db_name).is_success() 48 | 49 | def test_put_file_item(self): 50 | payload = { 51 | "title": self.fake.catch_phrase(), 52 | "many_files": [ 53 | utils.get_file("images/01.gif", public_url=True), 54 | utils.get_file("images/02.gif", public_url=True), 55 | ], 56 | } 57 | r = self.client.records().insert("Attachments", payload) 58 | assert r.is_success() 59 | 60 | rid = r["id"] 61 | record = self.client.records().get("Attachments", rid, columns=["many_files.id", "many_files.url"]) 62 | assert record.is_success() 63 | assert len(record["many_files"]) == 2 64 | 65 | proof_1 = request("GET", record["many_files"][0]["url"]) 66 | assert proof_1.status_code == 200 67 | 68 | proof_2 = request("GET", record["many_files"][1]["url"]) 69 | assert proof_2.status_code == 200 70 | 71 | img_1 = utils.get_file_content(utils.get_file_name("images/01.gif")) 72 | img_2 = utils.get_file_content(utils.get_file_name("images/02.gif")) 73 | assert img_1 == proof_1.content 74 | assert img_2 == proof_2.content 75 | 76 | # overwrite item 1 with image 2 77 | file_1 = self.client.files().put_item("Attachments", rid, "many_files", record["many_files"][0]["id"], img_2) 78 | assert file_1.is_success() 79 | assert "attributes" in file_1 80 | assert "mediaType" in file_1 81 | assert "name" in file_1 82 | assert "size" in file_1 83 | 84 | prev_url = record["many_files"][0]["url"] 85 | record = self.client.records().get("Attachments", rid, columns=["many_files.id", "many_files.url"]) 86 | assert prev_url != record["many_files"][0]["url"] 87 | 88 | proof = request("GET", record["many_files"][0]["url"]) 89 | assert proof.status_code == 200 90 | assert proof.content == img_2 91 | 92 | def test_delete_file(self): 93 | payload = { 94 | "title": self.fake.catch_phrase(), 95 | "many_files": [ 96 | utils.get_file("images/01.gif", public_url=True), 97 | utils.get_file("images/02.gif", public_url=True), 98 | ], 99 | } 100 | r = self.client.records().insert("Attachments", payload) 101 | assert r.is_success() 102 | 103 | rid = r["id"] 104 | record = self.client.records().get("Attachments", rid, columns=["many_files.id"]) 105 | assert record.is_success() 106 | assert len(record["many_files"]) == 2 107 | 108 | r = self.client.files().delete_item("Attachments", rid, "many_files", record["many_files"][0]["id"]) 109 | assert r.is_success() 110 | prev_id = record["many_files"][0]["id"] 111 | 112 | record = self.client.records().get("Attachments", rid, columns=["many_files.id"]) 113 | assert record.is_success() 114 | assert len(record["many_files"]) == 1 115 | 116 | proof = self.client.files().get_item("Attachements", rid, "many_files", prev_id) 117 | assert proof.status_code == 404 118 | 119 | def test_get_item(self): 120 | payload = { 121 | "title": self.fake.catch_phrase(), 122 | "many_files": [ 123 | utils.get_file("images/01.gif", public_url=True), 124 | utils.get_file("images/02.gif", public_url=True), 125 | ], 126 | } 127 | r = self.client.records().insert("Attachments", payload) 128 | assert r.is_success() 129 | 130 | rid = r["id"] 131 | record = self.client.records().get("Attachments", rid, columns=["many_files.id"]) 132 | assert record.is_success() 133 | 134 | item = self.client.files().get_item("Attachments", rid, "many_files", record["many_files"][0]["id"]) 135 | assert item.is_success() 136 | assert item.content == utils.get_file_content(utils.get_file_name("images/01.gif")) 137 | -------------------------------------------------------------------------------- /tests/integration-tests/files_transormations_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import io 21 | import json 22 | 23 | import pytest 24 | import utils 25 | from faker import Faker 26 | 27 | from xata.client import XataClient 28 | 29 | # from PIL import Image, ImageChops 30 | 31 | 32 | class TestFilesTransformations(object): 33 | def setup_class(self): 34 | self.db_name = utils.get_db_name() 35 | self.client = XataClient(db_name=self.db_name) 36 | self.fake = Faker() 37 | 38 | assert self.client.databases().create(self.db_name).is_success() 39 | assert self.client.table().create("Attachments").is_success() 40 | assert ( 41 | self.client.table() 42 | .set_schema( 43 | "Attachments", 44 | utils.get_attachments_schema(), 45 | db_name=self.db_name, 46 | ) 47 | .is_success() 48 | ) 49 | 50 | def teardown_class(self): 51 | assert self.client.databases().delete(self.db_name).is_success() 52 | 53 | def test_rotate_public_file(self): 54 | payload = { 55 | "title": self.fake.catch_phrase(), 56 | "one_file": utils.get_file("images/03.png", public_url=True), 57 | } 58 | upload = self.client.records().insert("Attachments", payload, columns=["one_file.url"]) 59 | assert upload.is_success() 60 | 61 | img = utils.get_file_content(utils.get_file_name("images/03.png")) 62 | rot_180 = self.client.files().transform(upload["one_file"]["url"], {"rotate": 180}) 63 | assert img != rot_180 64 | 65 | # proof_rot_180 = Image.open(utils.get_file_name("images/03.png")).rotate(180) 66 | # rot_180_pil = Image.open(io.BytesIO(rot_180)) 67 | # diff = ImageChops.difference(proof_rot_180, rot_180_pil) 68 | # assert diff.getbbox() 69 | 70 | def test_rotate_file_with_signed_url(self): 71 | payload = { 72 | "title": self.fake.catch_phrase(), 73 | "one_file": utils.get_file("images/03.png", public_url=False), 74 | } 75 | upload = self.client.records().insert("Attachments", payload, columns=["one_file.signedUrl"]) 76 | assert upload.is_success() 77 | 78 | img = utils.get_file_content(utils.get_file_name("images/03.png")) 79 | rot_180 = self.client.files().transform(upload["one_file"]["signedUrl"], {"rotate": 180}) 80 | assert img != rot_180 81 | 82 | # rot_180_pil = Image.open(io.BytesIO(rot_180)) 83 | # proof_rot_180 = Image.open(utils.get_file_name("images/03.png")).rotate(180) 84 | # assert rot_180_pil == proof_rot_180 85 | 86 | def test_with_nested_operations(self): 87 | payload = { 88 | "title": self.fake.catch_phrase(), 89 | "one_file": utils.get_file("images/03.png", public_url=True, signed_url_timeout=120), 90 | } 91 | upload = self.client.records().insert("Attachments", payload, columns=["one_file.url"]) 92 | assert upload.is_success() 93 | 94 | # img = utils.get_file_content(utils.get_file_name("images/03.png")) 95 | self.client.files().transform( 96 | upload["one_file"]["url"], 97 | {"rotate": 180, "blur": 50, "trim": {"top": 20, "right": 30, "bottom": 20, "left": 0}}, 98 | ) 99 | 100 | # rot_180_pil = Image.open(io.BytesIO(rot_180)) 101 | # proof_rot_180 = Image.open(utils.get_file_name("images/03.png")).rotate(180) 102 | # assert rot_180_pil == proof_rot_180 103 | 104 | def test_format_json_operations(self): 105 | payload = { 106 | "title": self.fake.catch_phrase(), 107 | "one_file": utils.get_file("images/03.png", public_url=True, signed_url_timeout=120), 108 | } 109 | upload = self.client.records().insert("Attachments", payload, columns=["one_file.url"]) 110 | assert upload.is_success() 111 | 112 | resp = self.client.files().transform( 113 | upload["one_file"]["url"], 114 | {"dpr": 2, "height": 200, "fit": "contain", "background": "pink", "format": "json"}, 115 | ) 116 | resp = json.load(io.BytesIO(resp)) 117 | assert resp == { 118 | "height": 400, 119 | "original": {"file_size": 72786, "format": "image/png", "height": 1646, "width": 1504}, 120 | "width": 365, 121 | } 122 | 123 | def test_unknown_operations(self): 124 | payload = { 125 | "title": self.fake.catch_phrase(), 126 | "one_file": utils.get_file("images/03.png", public_url=True), 127 | } 128 | upload = self.client.records().insert("Attachments", payload, columns=["one_file.url"]) 129 | assert upload.is_success() 130 | 131 | with pytest.raises(Exception): 132 | self.client.files().transform(upload["one_file"]["url"], {}) 133 | 134 | with pytest.raises(Exception): 135 | self.client.files().transform(upload["one_file"]["url"], {"donkey": "kong"}) 136 | 137 | def test_unknown_image_id(self): 138 | # must fail with a 403 139 | with pytest.raises(Exception): 140 | self.client.files().transform("https://us-east-1.storage.xata.sh/lalala", {"rotate": 90}) 141 | 142 | def test_invalid_url(self): 143 | # must fail with a 403 144 | with pytest.raises(Exception): 145 | self.client.files().transform("https:/xata.sh/oh-hello", {"rotate": 90}) 146 | -------------------------------------------------------------------------------- /tests/integration-tests/files_upload_url_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import utils 21 | 22 | from requests import request 23 | from xata.client import XataClient 24 | 25 | class TestFilesSingleFile(object): 26 | def setup_class(self): 27 | self.db_name = utils.get_db_name() 28 | self.branch_name = "main" 29 | self.client = XataClient(db_name=self.db_name, branch_name=self.branch_name) 30 | self.fake = utils.get_faker() 31 | 32 | assert self.client.databases().create(self.db_name).is_success() 33 | assert self.client.table().create("Attachments").is_success() 34 | assert ( 35 | self.client.table() 36 | .set_schema( 37 | "Attachments", 38 | utils.get_attachments_schema(), 39 | db_name=self.db_name, 40 | branch_name=self.branch_name, 41 | ) 42 | .is_success() 43 | ) 44 | 45 | def teardown_class(self): 46 | assert self.client.databases().delete(self.db_name).is_success() 47 | 48 | def test_upload_file(self): 49 | payload = {"title": self.fake.catch_phrase()} 50 | r = self.client.records().insert("Attachments", payload) 51 | assert r.is_success() 52 | 53 | rid = r["id"] 54 | gif = utils.get_file_content(utils.get_file_name("images/01.gif")) 55 | 56 | resp = self.client.files().put("Attachments", rid, "one_file", gif) 57 | assert resp.is_success() 58 | 59 | record = self.client.records().get("Attachments", rid, columns=["one_file.*", "one_file.uploadUrl"]) 60 | assert record.is_success() 61 | assert "uploadUrl" in record["one_file"] 62 | 63 | # upload 64 | url = record["one_file"]["uploadUrl"] 65 | bin = utils.get_file_content(utils.get_file_name("images/02.gif")) 66 | resp = request("PUT", url, headers={"content-type": "image/gif"}, data=bin) 67 | assert resp.status_code == 201, resp.json() 68 | 69 | # validate 70 | file = self.client.files().get("Attachments", rid, "one_file") 71 | assert file.is_success() 72 | assert file.headers.get("content-type") == "image/gif" 73 | assert bin == file.content 74 | assert bin != gif 75 | -------------------------------------------------------------------------------- /tests/integration-tests/records_file_operations_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import utils 21 | 22 | from xata.client import XataClient 23 | 24 | 25 | class TestRecordsFileOperations(object): 26 | def setup_class(self): 27 | self.db_name = utils.get_db_name() 28 | self.client = XataClient(db_name=self.db_name) 29 | self.fake = utils.get_faker() 30 | 31 | assert self.client.databases().create(self.db_name).is_success() 32 | assert self.client.table().create("Attachments").is_success() 33 | assert ( 34 | self.client.table() 35 | .set_schema( 36 | "Attachments", 37 | utils.get_attachments_schema(), 38 | db_name=self.db_name, 39 | ) 40 | .is_success() 41 | ) 42 | 43 | def teardown_class(self): 44 | assert self.client.databases().delete(self.db_name).is_success() 45 | 46 | def test_insert_record_with_files_and_read_it(self): 47 | payload = { 48 | "title": self.fake.catch_phrase(), 49 | "one_file": utils.get_file("images/01.gif"), 50 | "many_files": [utils.get_file("images/02.gif") for it in range(3)], 51 | } 52 | 53 | r = self.client.records().insert("Attachments", payload) 54 | assert r.is_success(), r 55 | assert "id" in r 56 | 57 | r = self.client.records().get("Attachments", r["id"]) 58 | assert r.is_success() 59 | 60 | record = r 61 | assert "id" not in record["one_file"] 62 | assert len(record["many_files"]) == len(payload["many_files"]) 63 | # the id is used to address the file within the map 64 | assert "id" in record["many_files"][0] 65 | assert "id" in record["many_files"][1] 66 | assert "id" in record["many_files"][2] 67 | 68 | assert "name" in record["one_file"] 69 | assert "mediaType" in record["one_file"] 70 | # assert "size" in record["one_file"] # TODO should be here 71 | assert "name" in record["many_files"][0] 72 | assert "mediaType" in record["many_files"][0] 73 | assert record["title"] == payload["title"] 74 | 75 | r = self.client.records().get( 76 | "Attachments", r["id"], columns=["one_file.base64Content", "many_files.base64Content"] 77 | ) 78 | assert r.is_success() 79 | record = r 80 | 81 | assert payload["one_file"]["base64Content"] == record["one_file"]["base64Content"] 82 | assert payload["many_files"][0]["base64Content"] == record["many_files"][0]["base64Content"] 83 | assert payload["many_files"][1]["base64Content"] == record["many_files"][1]["base64Content"] 84 | assert payload["many_files"][2]["base64Content"] == record["many_files"][2]["base64Content"] 85 | -------------------------------------------------------------------------------- /tests/integration-tests/search_and_filter_ask_table_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | from xata.client import XataClient 21 | 22 | 23 | class TestSearchAndFilterAskTableEndpoint(object): 24 | def setup_class(self): 25 | self.client = XataClient(workspace_id="sample-databases-v0sn1n", db_name="docs") 26 | 27 | """ 28 | def test_ask_table_for_response_shape(self): 29 | answer = self.client.data().ask("xata", "does xata have a python sdk") 30 | assert answer.is_success() 31 | 32 | assert "answer" in answer 33 | assert "records" in answer 34 | assert "sessionId" in answer 35 | 36 | assert answer["answer"] is not None 37 | assert answer["sessionId"] is not None 38 | assert len(answer["records"]) > 0 39 | 40 | assert answer.headers["content-type"].lower().startswith("application/json") 41 | 42 | def test_ask_table_for_response_shape_with_rules(self): 43 | answer = self.client.data().ask("xata", "what database technology is used at xata", ["postgres is a database"]) 44 | assert answer.is_success() 45 | 46 | assert "answer" in answer 47 | assert "records" in answer 48 | assert "sessionId" in answer 49 | 50 | assert answer["answer"] is not None 51 | assert answer["sessionId"] is not None 52 | assert len(answer["records"]) > 0 53 | 54 | def test_ask_table_for_response_shape_with_options(self): 55 | opts = { 56 | "searchType": "keyword", 57 | "search": { 58 | "fuzziness": 1, 59 | "prefix": "phrase", 60 | "target": [ 61 | "slug", 62 | {"column": "title", "weight": 4}, 63 | "content", 64 | "section", 65 | {"column": "keywords", "weight": 4}, 66 | ], 67 | "boosters": [ 68 | { 69 | "valueBooster": { 70 | "column": "section", 71 | "value": "guide", 72 | "factor": 18, 73 | }, 74 | }, 75 | ], 76 | }, 77 | } 78 | answer = self.client.data().ask("xata", "how to do full text search?", options=opts) 79 | assert answer.is_success() 80 | 81 | assert "answer" in answer 82 | assert "records" in answer 83 | assert "sessionId" in answer 84 | 85 | assert answer["answer"] is not None 86 | assert answer["sessionId"] is not None 87 | assert len(answer["records"]) > 0 88 | 89 | def test_ask_table_with_streaming_response(self): 90 | answer = self.client.data().ask("xata", "does the data model have link type?", streaming_results=True) 91 | assert answer.is_success() 92 | 93 | assert "transfer-encoding" in answer.headers 94 | assert answer.headers.get("transfer-encoding") == "chunked" 95 | """ 96 | 97 | def test_ask_follow_up_question(self): 98 | opts = { 99 | "searchType": "keyword", 100 | "search": { 101 | "fuzziness": 1, 102 | "prefix": "phrase", 103 | "target": [ 104 | "slug", 105 | {"column": "title", "weight": 4}, 106 | "content", 107 | "section", 108 | {"column": "keywords", "weight": 4}, 109 | ], 110 | "boosters": [ 111 | { 112 | "valueBooster": { 113 | "column": "section", 114 | "value": "guide", 115 | "factor": 18, 116 | }, 117 | }, 118 | ], 119 | }, 120 | } 121 | 122 | first_answer = self.client.data().ask("xata", "does xata have a python sdk", options=opts) 123 | assert first_answer.is_success() 124 | 125 | assert "answer" in first_answer 126 | assert "records" in first_answer 127 | assert "sessionId" in first_answer 128 | 129 | assert first_answer["answer"] is not None 130 | assert first_answer["sessionId"] is not None 131 | assert len(first_answer["records"]) > 0 132 | 133 | assert first_answer.headers["content-type"].lower().startswith("application/json") 134 | 135 | # follow up 136 | session_id = first_answer["sessionId"] 137 | second_answer = self.client.data().ask_follow_up("xata", session_id, "what is the best way to do bulk?") 138 | assert second_answer.is_success() 139 | assert "answer" in second_answer 140 | assert second_answer["answer"] is not None 141 | -------------------------------------------------------------------------------- /tests/integration-tests/search_and_filter_query_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import utils 21 | from faker import Faker 22 | 23 | from xata.client import XataClient 24 | 25 | 26 | class TestSearchAndFilterQueryApi(object): 27 | """ 28 | POST /db/{db_branch_name}/tables/{table_name}/query 29 | """ 30 | 31 | def setup_class(self): 32 | self.fake = Faker() 33 | self.db_name = utils.get_db_name() 34 | self.record_id = utils.get_random_string(24) 35 | self.posts = utils.get_posts(50) 36 | self.client = XataClient(db_name=self.db_name) 37 | 38 | assert self.client.databases().create(self.db_name).is_success() 39 | assert self.client.table().create("Posts").is_success() 40 | assert self.client.table().set_schema("Posts", utils.get_posts_schema()).is_success() 41 | assert self.client.records().bulk_insert("Posts", {"records": self.posts}).is_success() 42 | utils.wait_until_records_are_indexed("Posts", "title", self.client) 43 | 44 | def teardown_class(self): 45 | assert self.client.databases().delete(self.db_name).is_success() 46 | 47 | def test_query_table(self): 48 | payload = { 49 | "columns": ["title", "slug"], 50 | "sort": {"slug": "desc"}, 51 | "page": {"size": 5}, 52 | } 53 | r = self.client.data().query("Posts", payload) 54 | assert r.is_success() 55 | assert "records" in r 56 | assert len(r["records"]) > 0 57 | assert "meta" in r 58 | assert "id" in r["records"][0] 59 | assert "xata" in r["records"][0] 60 | assert "title" in r["records"][0] 61 | assert "slug" in r["records"][0] 62 | assert "text" not in r["records"][0] 63 | 64 | def test_find_all(self): 65 | all_1 = self.client.data().query("Posts") 66 | assert all_1.is_success() 67 | 68 | all_2 = self.client.data().query("Posts", {}) 69 | assert all_2.is_success() 70 | 71 | assert len(all_1) == len(all_2) 72 | 73 | def test_unknown_table(self): 74 | assert not self.client.data().query("NonExistingTable", {"columns": ["title", "slug"]}).is_success() 75 | 76 | def test_query_unknown_columns(self): 77 | assert not self.client.data().query("Posts", {"columns": ["does", "not", "exist"]}).is_success() 78 | 79 | def test_query_empty_columns(self): 80 | r = self.client.data().query("Posts", {"columns": ["*"]}) 81 | assert r.is_success() 82 | assert len(r["records"]) > 0 83 | -------------------------------------------------------------------------------- /tests/integration-tests/search_and_filter_revlinks_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import random 21 | 22 | import utils 23 | from faker import Faker 24 | 25 | from xata.client import XataClient 26 | 27 | 28 | class TestSearchAndFilterRevLinks(object): 29 | def setup_class(self): 30 | self.fake = Faker() 31 | self.db_name = utils.get_db_name() 32 | self.record_id = utils.get_random_string(24) 33 | self.client = XataClient(db_name=self.db_name) 34 | 35 | assert self.client.databases().create(self.db_name).is_success() 36 | 37 | assert self.client.table().create("Users").is_success() 38 | assert self.client.table().create("Posts").is_success() 39 | 40 | r = self.client.table().set_schema( 41 | "Users", 42 | { 43 | "columns": [ 44 | {"name": "name", "type": "string"}, 45 | {"name": "email", "type": "string"}, 46 | ] 47 | }, 48 | ) 49 | assert r.is_success() 50 | r = self.client.table().set_schema( 51 | "Posts", 52 | { 53 | "columns": [ 54 | {"name": "title", "type": "string"}, 55 | {"name": "author", "type": "link", "link": {"table": "Users"}}, 56 | {"name": "slug", "type": "string"}, 57 | ] 58 | }, 59 | ) 60 | assert r.is_success() 61 | 62 | ids = [self.fake.isbn13() for i in range(10)] 63 | self.users = [ 64 | { 65 | "id": ids[i], 66 | "name": self.fake.name(), 67 | "email": self.fake.email(), 68 | } 69 | for i in range(len(ids)) 70 | ] 71 | assert self.client.records().bulk_insert("Users", {"records": self.users}).is_success() 72 | self.posts = [ 73 | { 74 | "title": self.fake.company(), 75 | "author": random.choice(ids), 76 | "slug": self.fake.catch_phrase(), 77 | } 78 | for i in range(50) 79 | ] 80 | assert self.client.records().bulk_insert("Posts", {"records": self.posts}).is_success() 81 | 82 | def teardown_class(self): 83 | assert self.client.databases().delete(self.db_name).is_success() 84 | 85 | def test_revlinks_with_alias(self): 86 | payload = { 87 | "columns": [ 88 | "name", 89 | { 90 | "name": "<-Posts.author", 91 | "columns": ["title"], 92 | "as": "posts", 93 | "limit": 5, 94 | "offset": 0, 95 | "order": [{"column": "createdAt", "order": "desc"}], 96 | }, 97 | ], 98 | "page": {"size": 5}, 99 | } 100 | r = self.client.data().query("Users", payload) 101 | assert r.is_success() 102 | assert "records" in r 103 | assert len(r["records"]) == 5 104 | assert "name" in r["records"][0] 105 | assert "posts" in r["records"][0] 106 | assert "records" in r["records"][0]["posts"] 107 | 108 | posts = r["records"][0]["posts"]["records"] 109 | assert len(posts) >= 1 110 | assert len(posts) <= 5 111 | 112 | post = posts[0] 113 | assert "id" in post 114 | assert "title" in post 115 | 116 | def test_revlinks_without_alias(self): 117 | payload = { 118 | "columns": ["name", {"name": "<-Posts.author", "columns": ["title"]}], 119 | "page": {"size": 1}, 120 | } 121 | r = self.client.data().query("Users", payload) 122 | assert r.is_success() 123 | assert "records" in r 124 | assert "name" in r["records"][0] 125 | assert "Postsauthor" in r["records"][0] 126 | assert "records" in r["records"][0]["Postsauthor"] 127 | 128 | def test_revlinks_with_limit_control(self): 129 | payload = { 130 | "columns": ["name", {"name": "<-Posts.author", "columns": ["title"], "as": "posts", "limit": 1}], 131 | "page": {"size": 5}, 132 | } 133 | r = self.client.data().query("Users", payload) 134 | assert r.is_success() 135 | posts = r["records"][0]["posts"]["records"] 136 | assert len(posts) == 1 137 | -------------------------------------------------------------------------------- /tests/integration-tests/search_and_filter_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import utils 21 | from faker import Faker 22 | 23 | from xata.client import XataClient 24 | 25 | 26 | class TestSearchAndFilterNamespace(object): 27 | def setup_class(self): 28 | self.fake = Faker() 29 | self.db_name = utils.get_db_name() 30 | self.record_id = utils.get_random_string(24) 31 | self.posts = utils.get_posts(50) 32 | self.client = XataClient(db_name=self.db_name) 33 | 34 | assert self.client.databases().create(self.db_name).is_success() 35 | assert self.client.table().create("Posts").is_success() 36 | assert self.client.table().set_schema("Posts", utils.get_posts_schema()).is_success() 37 | assert self.client.records().bulk_insert("Posts", {"records": self.posts}).is_success() 38 | utils.wait_until_records_are_indexed("Posts", "title", self.client) 39 | 40 | def teardown_class(self): 41 | assert self.client.databases().delete(self.db_name).is_success() 42 | 43 | def test_search_branch(self): 44 | """ 45 | POST /db/{db_branch_name}/search 46 | """ 47 | payload = {"query": self.posts[0]["title"]} 48 | r = self.client.data().search_branch(payload) 49 | assert r.is_success() 50 | assert "records" in r 51 | assert len(r["records"]) >= 1 52 | assert "id" in r["records"][0] 53 | assert "xata" in r["records"][0] 54 | assert "title" in r["records"][0] 55 | assert r["records"][0]["title"] == self.posts[0]["title"] 56 | 57 | r = self.client.data().search_branch({"tables": [""], "query": "woopsie!"}) 58 | assert r.status_code == 400 59 | 60 | r = self.client.data().search_branch({"invalid": "query"}) 61 | assert r.status_code == 400 62 | 63 | r = self.client.data().search_branch(payload=payload, branch_name="NonExisting") 64 | assert r.status_code == 404 65 | 66 | def test_search_table(self): 67 | """ 68 | POST /db/{db_branch_name}/tables/{table_name}/search 69 | """ 70 | payload = {"query": self.posts[0]["title"]} 71 | r = self.client.data().search_table("Posts", payload) 72 | assert r.is_success() 73 | assert "records" in r 74 | assert len(r["records"]) >= 1 75 | assert "id" in r["records"][0] 76 | assert "xata" in r["records"][0] 77 | assert "title" in r["records"][0] 78 | assert r["records"][0]["title"] == self.posts[0]["title"] 79 | 80 | r = self.client.data().search_table("Posts", {}) 81 | assert r.is_success() 82 | 83 | # TODO enable again when fixed 84 | # r = self.client.data().search_table("NonExistingTable", payload) 85 | # assert r.status_code == 404 86 | 87 | r = self.client.data().search_table("Posts", {"invalid": "query"}) 88 | assert r.status_code == 400 89 | 90 | def test_summarize_table(self): 91 | """ 92 | POST /db/{db_branch_name}/tables/{table_name}/summarize 93 | """ 94 | payload = {"columns": ["title", "slug"]} 95 | r = self.client.data().summarize("Posts", payload) 96 | assert r.is_success() 97 | assert "summaries" in r 98 | assert len(r["summaries"]) > 1 99 | 100 | r = self.client.data().summarize("NonExistingTable", payload) 101 | assert r.status_code == 404 102 | 103 | def test_aggregate_table(self): 104 | """ 105 | POST /db/{db_branch_name}/tables/{table_name}/aggregate 106 | """ 107 | payload = {"aggs": {"titles": {"count": "*"}}} 108 | r = self.client.data().aggregate("Posts", payload) 109 | assert r.is_success() 110 | assert "aggs" in r 111 | assert "titles" in r["aggs"] 112 | assert r["aggs"]["titles"] == len(self.posts) 113 | 114 | # TODO enable again when fixed 115 | # r = self.client.data().aggregate("NonExistingTable", payload) 116 | # assert r.status_code == 404 117 | 118 | r = self.client.data().aggregate("Posts", {"aggs": {"foo": "bar"}}) 119 | assert r.status_code == 400 120 | -------------------------------------------------------------------------------- /tests/integration-tests/search_and_filter_with_alias_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import utils 21 | from faker import Faker 22 | 23 | from xata.client import XataClient 24 | 25 | 26 | class TestSearchAndFilterWithAliasNamespace(object): 27 | def setup_class(self): 28 | self.fake = Faker() 29 | self.db_name = utils.get_db_name() 30 | self.branch_name = "main" 31 | self.record_id = utils.get_random_string(24) 32 | self.client = XataClient(db_name=self.db_name, branch_name=self.branch_name) 33 | 34 | assert self.client.databases().create(self.db_name).is_success() 35 | assert self.client.table().create("Posts").is_success() 36 | 37 | # create schema 38 | r = self.client.table().set_schema( 39 | "Posts", 40 | { 41 | "columns": [ 42 | {"name": "title", "type": "string"}, 43 | {"name": "labels", "type": "multiple"}, 44 | {"name": "slug", "type": "string"}, 45 | {"name": "text", "type": "text"}, 46 | ] 47 | }, 48 | ) 49 | assert r.is_success() 50 | 51 | # ingests posts 52 | self.posts = [ 53 | { 54 | "title": self.fake.company(), 55 | "labels": [self.fake.domain_word(), self.fake.domain_word()], 56 | "slug": self.fake.catch_phrase(), 57 | "text": self.fake.text(), 58 | } 59 | for i in range(10) 60 | ] 61 | r = self.client.records().bulk_insert("Posts", {"records": self.posts}) 62 | assert r.is_success() 63 | utils.wait_until_records_are_indexed("Posts", "title", self.client) 64 | 65 | def teardown_class(self): 66 | assert self.client.databases().delete(self.db_name).is_success() 67 | 68 | def test_query_table(self): 69 | """ 70 | POST /db/{db_branch_name}/tables/{table_name}/query 71 | """ 72 | payload = { 73 | "columns": ["title", "slug"], 74 | "sort": {"slug": "desc"}, 75 | "page": {"size": 5}, 76 | } 77 | r = self.client.data().query("Posts", payload) 78 | assert r.is_success() 79 | assert "records" in r 80 | assert len(r["records"]) == 5 81 | assert "meta" in r 82 | assert "id" in r["records"][0] 83 | assert "xata" in r["records"][0] 84 | assert "title" in r["records"][0] 85 | assert "slug" in r["records"][0] 86 | assert "text" not in r["records"][0] 87 | 88 | def test_search_branch(self): 89 | """ 90 | POST /db/{db_branch_name}/search 91 | """ 92 | payload = {"query": self.posts[0]["title"]} 93 | r = self.client.data().search_branch(payload) 94 | assert r.is_success() 95 | assert "records" in r 96 | assert len(r["records"]) >= 1 97 | assert "id" in r["records"][0] 98 | assert "xata" in r["records"][0] 99 | assert "title" in r["records"][0] 100 | assert r["records"][0]["title"] == self.posts[0]["title"] 101 | 102 | def test_search_table(self): 103 | """ 104 | POST /db/{db_branch_name}/tables/{table_name}/search 105 | """ 106 | payload = {"query": self.posts[0]["title"]} 107 | r = self.client.data().search_table("Posts", payload) 108 | assert r.is_success() 109 | assert "records" in r 110 | assert len(r["records"]) >= 1 111 | assert "id" in r["records"][0] 112 | assert "xata" in r["records"][0] 113 | assert "title" in r["records"][0] 114 | assert r["records"][0]["title"] == self.posts[0]["title"] 115 | -------------------------------------------------------------------------------- /tests/integration-tests/sql_query_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import utils 21 | 22 | from xata.client import XataClient 23 | 24 | 25 | class TestSqlQuery(object): 26 | def setup_class(self): 27 | self.db_name = utils.get_db_name() 28 | self.client = XataClient(db_name=self.db_name) 29 | assert self.client.databases().create(self.db_name).is_success() 30 | assert self.client.table().create("Users").is_success() 31 | assert ( 32 | self.client.table() 33 | .set_schema( 34 | "Users", 35 | {"columns": [{"name": "name", "type": "string"}, {"name": "email", "type": "string"}]}, 36 | ) 37 | .is_success() 38 | ) 39 | users = [ 40 | { 41 | "name": utils.get_faker().name(), 42 | "email": utils.get_faker().email(), 43 | } 44 | for i in range(50) 45 | ] 46 | assert self.client.records().bulk_insert("Users", {"records": users}).is_success() 47 | 48 | def teardown_class(self): 49 | assert self.client.databases().delete(self.db_name).is_success() 50 | 51 | def test_query(self): 52 | r = self.client.sql().query('SELECT * FROM "Users" LIMIT 5') 53 | assert r.is_success() 54 | assert "records" in r 55 | assert "columns" in r 56 | assert "total" in r 57 | 58 | assert len(r["records"]) == 5 59 | assert "id" in r["records"][0] 60 | assert "name" in r["records"][0] 61 | assert "email" in r["records"][0] 62 | 63 | assert len(r["columns"]) == 2 + 4 # name & email + special xata_ columns 64 | assert "name" in r["columns"][0] 65 | assert "type" in r["columns"][0] 66 | 67 | def test_query_on_non_existing_table(self): 68 | r = self.client.sql().query('SELECT * FROM "DudeWhereIsMyTable"') 69 | assert not r.is_success() 70 | 71 | def test_query_with_invalid_statement(self): 72 | r = self.client.sql().query("SELECT ' fr_o-") 73 | assert not r.is_success() 74 | 75 | def test_query_statement_with_missing_params(self): 76 | r = self.client.sql().query("SELECT * FROM \"Users\" WHERE email = '$1'") 77 | assert r.is_success() 78 | assert len(r["records"]) == 0 79 | assert r["total"] == 0 80 | 81 | def test_query_statement_with_params_and_no_param_references(self): 82 | r = self.client.sql().query('SELECT * FROM "Users"', ["This is important"]) 83 | assert not r.is_success() 84 | 85 | def test_query_statement_with_too_many_params(self): 86 | r = self.client.sql().query( 87 | 'INSERT INTO "Users" (name, email) VALUES ($1, $2)', ["Shrek", "shrek@example.com", "Hi, I'm too much!"] 88 | ) 89 | assert not r.is_success() 90 | 91 | def test_query_statement_with_not_enough_params(self): 92 | r = self.client.sql().query('INSERT INTO "Users" (name, email) VALUES ($1, $2)', ["Shrek"]) 93 | assert not r.is_success() 94 | 95 | def test_insert(self): 96 | r = self.client.sql().query( 97 | "INSERT INTO \"Users\" (name, email) VALUES ('Leslie Nielsen', 'leslie@example.com')" 98 | ) 99 | assert r.is_success() 100 | assert not r["records"] 101 | assert r["total"] == 0 102 | 103 | def test_insert_with_params(self): 104 | r = self.client.sql().query( 105 | 'INSERT INTO "Users" (name, email) VALUES ($1, $2)', ["Keanu Reeves", "keanu@example.com"] 106 | ) 107 | assert r.is_success() 108 | assert not r["records"] 109 | assert r["total"] == 0 110 | 111 | def test_query_with_params(self): 112 | r = self.client.sql().query('SELECT * FROM "Users" WHERE email = $1', ["keanu@example.com"]) 113 | assert r.is_success() 114 | assert "records" in r 115 | assert len(r["records"]) == 1 116 | -------------------------------------------------------------------------------- /tests/integration-tests/type_json_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import json 21 | 22 | import utils 23 | from faker import Faker 24 | 25 | from xata.client import XataClient 26 | 27 | 28 | class TestJsonColumnType(object): 29 | def setup_class(self): 30 | self.fake = Faker() 31 | self.db_name = utils.get_db_name() 32 | self.record_id = utils.get_random_string(24) 33 | self.client = XataClient(db_name=self.db_name) 34 | 35 | assert self.client.databases().create(self.db_name).is_success() 36 | 37 | def teardown_class(self): 38 | assert self.client.databases().delete(self.db_name).is_success() 39 | 40 | def test_create_table_with_type(self): 41 | assert self.client.table().create("Posts").is_success() 42 | assert ( 43 | self.client.table() 44 | .set_schema( 45 | "Posts", 46 | { 47 | "columns": [ 48 | {"name": "title", "type": "string"}, 49 | {"name": "meta", "type": "json"}, 50 | ] 51 | }, 52 | ) 53 | .is_success() 54 | ) 55 | 56 | def test_insert_records(self): 57 | result = self.client.records().bulk_insert( 58 | "Posts", 59 | { 60 | "records": [ 61 | { 62 | "title": self.fake.catch_phrase(), 63 | "meta": json.dumps({"number": it, "nested": {"random": it * it, "a": "b"}}), 64 | } 65 | for it in range(25) 66 | ] 67 | }, 68 | ) 69 | assert result.is_success() 70 | 71 | def test_query_records(self): 72 | result = self.client.data().query("Posts") 73 | assert result.is_success() 74 | assert len(result["records"]) > 5 75 | 76 | assert "meta" in result["records"][5] 77 | j = json.loads(result["records"][5]["meta"]) 78 | assert "number" in j 79 | assert "nested" in j 80 | assert "random" in j["nested"] 81 | assert "a" in j["nested"] 82 | 83 | assert j["number"] == 5 84 | assert j["nested"]["random"] == 5 * 5 85 | assert j["nested"]["a"] == "b" 86 | -------------------------------------------------------------------------------- /tests/integration-tests/users_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import utils 21 | 22 | from xata.client import XataClient 23 | 24 | 25 | class TestUsersNamespace(object): 26 | def setup_class(self): 27 | self.db_name = utils.get_db_name() 28 | self.branch_name = "main" 29 | self.client = XataClient(db_name=self.db_name, branch_name=self.branch_name) 30 | 31 | def test_get_user_api_keys(self): 32 | r = self.client.users().get() 33 | assert r.is_success() 34 | assert "id" in r 35 | assert "email" in r 36 | assert "fullname" in r 37 | assert "image" in r 38 | 39 | def test_create_user_api_keys(self): 40 | prev = self.client.users().get() 41 | assert prev.is_success() 42 | 43 | """ 44 | user = {"fullname": "test-suite-%s" % utils.get_random_string(4)} 45 | assert user["fullname"] != prev["fullname"] 46 | 47 | now = self.client.users().update(user) 48 | assert now.is_success() 49 | assert "id" in now 50 | assert "email" in now 51 | assert "fullname" in now 52 | assert "image" in now 53 | assert now["fullname"] == user["fullname"] 54 | 55 | user["fullname"] = prev["fullname"] 56 | r = self.client.users().update(user) 57 | assert r.is_success() 58 | assert r["fullname"] == prev["fullname"] 59 | 60 | r = self.client.users().update({}) 61 | assert not r.is_success() 62 | """ 63 | -------------------------------------------------------------------------------- /tests/integration-tests/utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import base64 21 | import os 22 | import random 23 | import string 24 | import time 25 | 26 | import magic 27 | from faker import Faker 28 | 29 | from xata.client import XataClient 30 | 31 | faker = Faker() 32 | #Faker.seed(412) 33 | 34 | 35 | def get_faker() -> Faker: 36 | return faker 37 | 38 | 39 | def get_db_name() -> str: 40 | return f"sdk-integration-py-{get_random_string(6)}" 41 | 42 | 43 | def wait_until_records_are_indexed(table: str, col: str, client: XataClient): 44 | """ 45 | Wait for the records to be index in order to able to search them 46 | """ 47 | is_empty = True 48 | counter = 24 49 | while is_empty: 50 | r = client.data().aggregate(table, {"aggs": {col: {"count": "*"}}}) 51 | if r["aggs"][col] or counter <= 1: 52 | is_empty = False 53 | else: 54 | counter -= 1 # escape endless loop 55 | time.sleep(5) 56 | 57 | 58 | def get_random_string(length): 59 | letters = string.ascii_lowercase 60 | return "".join(random.choice(letters) for i in range(length)) 61 | 62 | 63 | def get_attachments_schema() -> dict: 64 | return { 65 | "columns": [ 66 | {"name": "title", "type": "string"}, 67 | {"name": "one_file", "type": "file"}, 68 | {"name": "many_files", "type": "file[]"}, 69 | ] 70 | } 71 | 72 | 73 | def get_file_name(file_name: str) -> str: 74 | return "%s/tests/data/attachments/%s" % (os.getcwd(), file_name) 75 | 76 | 77 | def get_file_content(file_name: str) -> bytes: 78 | with open(file_name, "rb") as f: 79 | return f.read() 80 | 81 | 82 | def get_file(file_name: str, public_url: bool = False, signed_url_timeout: int = 30): 83 | file_name = get_file_name(file_name) 84 | file_content = get_file_content(file_name) 85 | 86 | return { 87 | "name": file_name.replace("/", "_"), 88 | "mediaType": magic.from_file(file_name, mime=True), 89 | "base64Content": base64.b64encode(file_content).decode("ascii"), 90 | "enablePublicUrl": public_url, 91 | "signedUrlTimeout": signed_url_timeout, 92 | } 93 | 94 | 95 | def get_post() -> dict: 96 | return { 97 | "title": get_faker().company(), 98 | "labels": [get_faker().domain_word(), get_faker().domain_word()], 99 | "slug": get_faker().catch_phrase(), 100 | "content": get_faker().text(), 101 | } 102 | 103 | 104 | def get_posts(n: int = 3) -> list[str]: 105 | """ 106 | List of three Posts 107 | """ 108 | return [get_post() for i in range(n)] 109 | 110 | 111 | def get_posts_schema() -> dict: 112 | return { 113 | "columns": [ 114 | {"name": "title", "type": "string"}, 115 | {"name": "labels", "type": "multiple"}, 116 | {"name": "slug", "type": "string"}, 117 | {"name": "content", "type": "text"}, 118 | ] 119 | } 120 | -------------------------------------------------------------------------------- /tests/integration-tests/workspaces_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import pytest 21 | import utils 22 | 23 | from xata.client import XataClient 24 | 25 | 26 | class TestWorkspacesNamespace(object): 27 | def setup_class(self): 28 | self.db_name = utils.get_db_name() 29 | self.branch_name = "main" 30 | self.client = XataClient(db_name=self.db_name, branch_name=self.branch_name) 31 | self.workspace_name = "py-sdk-tests-%s" % utils.get_random_string(6) 32 | 33 | def test_list_workspaces(self): 34 | r = self.client.workspaces().list() 35 | assert r.is_success() 36 | assert "workspaces" in r 37 | assert len(r["workspaces"]) > 0 38 | assert "id" in r["workspaces"][0] 39 | assert "name" in r["workspaces"][0] 40 | assert "slug" in r["workspaces"][0] 41 | assert "role" in r["workspaces"][0] 42 | 43 | def test_create_new_workspace(self): 44 | r = self.client.workspaces().create(self.workspace_name, "sluginator") 45 | assert r.is_success() 46 | assert "id" in r 47 | assert "name" in r 48 | assert "slug" in r 49 | assert "plan" in r 50 | assert "memberCount" in r 51 | assert r["name"] == self.workspace_name 52 | assert r["slug"] == "sluginator" 53 | 54 | pytest.workspaces["workspace"] = r 55 | 56 | def test_create_new_workspace_without_slug(self): 57 | ws_id = "py-sdk-test-ws-without-slug-%s" % utils.get_random_string(4) 58 | r = self.client.workspaces().create(ws_id) 59 | assert r.is_success() 60 | assert r["name"] == ws_id 61 | assert r["slug"] == ws_id 62 | assert self.client.workspaces().delete(r["id"]).is_success() 63 | 64 | def test_get_workspace(self): 65 | r = self.client.workspaces().get(workspace_id=pytest.workspaces["workspace"]["id"]) 66 | assert r.is_success() 67 | assert "name" in r 68 | assert "slug" in r 69 | assert "id" in r 70 | assert "memberCount" in r 71 | assert "plan" in r 72 | assert r["name"] == pytest.workspaces["workspace"]["name"] 73 | assert r["slug"] == pytest.workspaces["workspace"]["slug"] 74 | assert r["plan"] == pytest.workspaces["workspace"]["plan"] 75 | assert r["memberCount"] == pytest.workspaces["workspace"]["memberCount"] 76 | assert r["id"] == pytest.workspaces["workspace"]["id"] 77 | 78 | assert not self.client.workspaces().get("NonExistingWorkspaceId").is_success() 79 | 80 | def test_update_workspace(self): 81 | payload = { 82 | "name": "new-workspace-name", 83 | "slug": "super-duper-new-slug", 84 | } 85 | r = self.client.workspaces().update( 86 | payload, 87 | workspace_id=pytest.workspaces["workspace"]["id"], 88 | ) 89 | assert r.is_success() 90 | assert "name" in r 91 | assert "slug" in r 92 | assert "id" in r 93 | assert "memberCount" in r 94 | assert "plan" in r 95 | assert r["name"] == payload["name"] 96 | assert r["slug"] == payload["slug"] 97 | 98 | r = self.client.workspaces().update( 99 | {"name": "only-a-name"}, 100 | workspace_id=pytest.workspaces["workspace"]["id"], 101 | ) 102 | assert r.is_success() 103 | 104 | assert ( 105 | not self.client.workspaces() 106 | .update( 107 | {"slug": "only-a-slug"}, 108 | workspace_id=pytest.workspaces["workspace"]["id"], 109 | ) 110 | .is_success() 111 | ) 112 | 113 | def test_delete_workspace(self): 114 | assert self.client.workspaces().delete(workspace_id=pytest.workspaces["workspace"]["id"]).is_success() 115 | pytest.workspaces["workspace"] = None 116 | 117 | assert not self.client.workspaces().delete(workspace_id="NonExistingWorkspace").is_success() 118 | 119 | # 120 | # Workspace Member Ops 121 | # 122 | def test_get_workspace_members(self): 123 | r = self.client.workspaces().get_members() 124 | assert r.is_success() 125 | assert "members" in r 126 | assert "invites" in r 127 | assert len(r["members"]) > 0 128 | assert "userId" in r["members"][0] 129 | assert "fullname" in r["members"][0] 130 | assert "email" in r["members"][0] 131 | assert "role" in r["members"][0] 132 | 133 | pytest.workspaces["member"] = r["members"][0] 134 | 135 | assert not self.client.workspaces().get_members(workspace_id="NonExistingWorkspaceId").is_success() 136 | 137 | def test_update_workspace_member(self): 138 | payload = {"role": "owner" if pytest.workspaces["member"]["role"] == "maintainer" else "owner"} 139 | 140 | assert ( 141 | not self.client.workspaces() 142 | .update_member(pytest.workspaces["member"]["userId"], {"role": "spiderman"}) 143 | .is_success() 144 | ) 145 | assert ( 146 | not self.client.workspaces() 147 | .update_member( 148 | pytest.workspaces["member"]["userId"], 149 | payload, 150 | workspace_id="NonExistingWorkspaceId", 151 | ) 152 | .is_success() 153 | ) 154 | assert not self.client.workspaces().update_member("NonExistingUserId", payload).is_success() 155 | assert not self.client.workspaces().update_member(pytest.workspaces["member"]["userId"], {}).is_success() 156 | 157 | pytest.workspaces["member"] = None 158 | -------------------------------------------------------------------------------- /tests/unit-tests/api_request_domains_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import unittest 21 | 22 | from xata.client import DEFAULT_REGION, XataClient 23 | 24 | 25 | class TestApiRequestCustomDomains(unittest.TestCase): 26 | def test_core_domain(self): 27 | domain = "api.hallo.hey" 28 | client = XataClient( 29 | api_key="123", db_url="https://py-sdk-unit-test-12345.eu-west-1.xata.sh/db/testopia-042", domain_core=domain 30 | ) 31 | assert "https://" + domain == client.databases().get_base_url() 32 | 33 | def test_workspace_domain(self): 34 | domain = "hello.is.it.me-you-are-looking.for" 35 | ws_id = "testopia-042" 36 | client = XataClient(workspace_id=ws_id, domain_workspace=domain, api_key="123") 37 | 38 | expected = "https://%s.%s.%s" % (ws_id, DEFAULT_REGION, domain) 39 | assert expected == client.table().get_base_url() 40 | 41 | def test_upload_base_url(self): 42 | client = XataClient(api_key="123", db_url="https://12345.region-42.staging-xata.dev/db/testopia-042") 43 | assert "https://12345.region-42.upload.staging-xata.dev" == client.databases().get_upload_base_url() 44 | -------------------------------------------------------------------------------- /tests/unit-tests/api_request_internals_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import unittest 21 | 22 | from xata.client import DEFAULT_REGION, XataClient 23 | 24 | 25 | class TestApiRequestInternals(unittest.TestCase): 26 | def test_get_scope(self): 27 | client = XataClient(api_key="123", db_url="https://py-sdk-unit-test-12345.eu-west-1.xata.sh/db/testopia-042") 28 | 29 | assert "core" == client.databases().get_scope() 30 | assert "workspace" == client.table().get_scope() 31 | 32 | def test_is_control_plane(self): 33 | client = XataClient(api_key="123", db_url="https://py-sdk-unit-test-12345.eu-west-1.xata.sh/db/testopia-042") 34 | 35 | assert client.databases().is_control_plane() 36 | assert not client.table().is_control_plane() 37 | 38 | def test_get_base_url(self): 39 | c = XataClient( 40 | api_key="123", 41 | workspace_id="testopia-ab2", 42 | domain_core="custom.name", 43 | domain_workspace="sub.subsub.name.lol", 44 | ) 45 | 46 | assert "https://custom.name" == c.authentication().get_base_url() 47 | 48 | expected = "https://testopia-ab2.%s.sub.subsub.name.lol" % DEFAULT_REGION 49 | assert expected == c.branch().get_base_url() 50 | -------------------------------------------------------------------------------- /tests/unit-tests/api_response_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import unittest 21 | 22 | 23 | class TestApiResponse(unittest.TestCase): 24 | # TODO 25 | pass 26 | -------------------------------------------------------------------------------- /tests/unit-tests/client_config_getters.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import unittest 21 | 22 | from xata.client import XataClient 23 | 24 | 25 | class TestClientConfigGetters(unittest.TestCase): 26 | def test_get_region(self): 27 | region = "eu-south-42" 28 | client = XataClient(api_key="api_key", workspace_id="ws_id", region=region) 29 | assert client.get_region() == client.get_config()["region"] 30 | assert region == client.get_region() 31 | 32 | def test_get_database_name(self): 33 | db_name = "the-marvelous-world-of-dbs-001" 34 | client = XataClient(api_key="api_key", workspace_id="ws_id", db_name=db_name) 35 | assert client.get_database_name() == client.get_config()["dbName"] 36 | assert db_name == client.get_database_name() 37 | 38 | def test_get_branch_name(self): 39 | branch_name = "my-amazing/new-feature" 40 | client = XataClient(api_key="api_key", workspace_id="ws_id", branch_name=branch_name) 41 | assert client.get_branch_name() == client.get_config()["branchName"] 42 | assert branch_name == client.get_branch_name() 43 | 44 | def test_get_workspace_id(self): 45 | ws_id = "a-workspace-01se45" 46 | client = XataClient(api_key="api_key", workspace_id=ws_id) 47 | assert client.get_workspace_id() == client.get_config()["workspaceId"] 48 | assert ws_id == client.get_workspace_id() 49 | -------------------------------------------------------------------------------- /tests/unit-tests/client_headers_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import unittest 21 | 22 | from xata.client import XataClient, __version__ 23 | 24 | 25 | class TestClientHeaders(unittest.TestCase): 26 | def test_client_init_headers(self): 27 | api_key = "this-key-42" 28 | client1 = XataClient(api_key=api_key, workspace_id="ws_id") 29 | headers1 = client1.get_headers() 30 | 31 | assert len(headers1) == 6 32 | assert "authorization" in headers1 33 | assert headers1["authorization"] == f"Bearer {api_key}" 34 | assert "user-agent" in headers1 35 | assert headers1["user-agent"] == f"xataio/xata-py:{__version__}" 36 | 37 | api_key = "this-key-42" 38 | client2 = XataClient(api_key=api_key, workspace_id="ws_id") 39 | headers2 = client2.get_headers() 40 | 41 | assert len(headers1) == len(headers2) 42 | assert headers1["x-xata-agent"] == headers2["x-xata-agent"] 43 | assert headers1["user-agent"] == headers2["user-agent"] 44 | 45 | def test_delete_header(self): 46 | client = XataClient(api_key="api_key", workspace_id="ws_id") 47 | proof = client.get_headers() 48 | assert "user-agent" in proof 49 | assert "x-xata-client-id" in proof 50 | 51 | assert client.delete_header("user-agent") 52 | assert "user-agent" not in client.get_headers() 53 | 54 | assert client.delete_header("X-XaTa-cLienT-Id") 55 | assert "x-xata-client-id" not in client.get_headers() 56 | 57 | assert not client.delete_header("user-agent") 58 | assert not client.delete_header("Not-Existing-Header") 59 | 60 | def test_set_and_delete_header(self): 61 | client = XataClient(api_key="api_key_123", workspace_id="ws_id") 62 | proof = client.get_headers() 63 | assert "new-header" not in proof 64 | assert "x-proxy-timeout" not in proof 65 | 66 | client.set_header("new-header", "testing") 67 | new_headers = client.get_headers() 68 | 69 | # assert proof != new_headers 70 | assert "new-header" in new_headers 71 | assert new_headers["new-header"] == "testing" 72 | 73 | assert client.delete_header("new-header") 74 | assert proof == client.get_headers() 75 | # assert new_headers != client.get_headers() 76 | 77 | client.set_header("X-PROXY-TIMEOUT", "1200") 78 | assert "x-proxy-timeout" in client.get_headers() 79 | assert "X-PROXY-TIMEOUT" not in client.get_headers() 80 | -------------------------------------------------------------------------------- /tests/unit-tests/client_telemetry_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import unittest 21 | 22 | import utils 23 | 24 | from xata.client import XataClient, __version__ 25 | 26 | 27 | class TestClientTelemetry(unittest.TestCase): 28 | def test_telemetry_headers(self): 29 | api_key = "this-key-42" 30 | client1 = XataClient(api_key=api_key, workspace_id="ws_id") 31 | headers1 = client1.get_headers() 32 | 33 | assert "x-xata-client-id" in headers1 34 | assert utils.PATTERNS_UUID4.match(headers1["x-xata-client-id"]) 35 | assert "x-xata-session-id" in headers1 36 | assert utils.PATTERNS_UUID4.match(headers1["x-xata-session-id"]) 37 | assert headers1["x-xata-client-id"] != headers1["x-xata-session-id"] 38 | assert "x-xata-agent" in headers1 39 | assert headers1["x-xata-agent"] == f"client=PY_SDK; version={__version__}" 40 | 41 | api_key = "this-key-42" 42 | client2 = XataClient(api_key=api_key, workspace_id="ws_id") 43 | headers2 = client2.get_headers() 44 | 45 | assert headers1["x-xata-client-id"] != headers2["x-xata-client-id"] 46 | assert headers1["x-xata-session-id"] != headers2["x-xata-session-id"] 47 | 48 | assert headers1["x-xata-agent"] == headers2["x-xata-agent"] 49 | assert headers1["user-agent"] == headers2["user-agent"] 50 | 51 | def test_telemetry_sessions(self): 52 | api_key = "this-key-42" 53 | client1 = XataClient(api_key=api_key, workspace_id="ws_id") 54 | headers1 = client1.get_headers() 55 | 56 | api_key = "this-key-42" 57 | client2 = XataClient(api_key=api_key, workspace_id="ws_id") 58 | headers2 = client2.get_headers() 59 | 60 | assert headers1 != headers2 61 | 62 | assert headers1["x-xata-client-id"] != headers2["x-xata-client-id"] 63 | assert headers1["x-xata-session-id"] != headers2["x-xata-session-id"] 64 | 65 | assert headers1["x-xata-agent"] == headers2["x-xata-agent"] 66 | assert headers1["user-agent"] == headers2["user-agent"] 67 | -------------------------------------------------------------------------------- /tests/unit-tests/client_with_custom_domains_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import os 21 | import unittest 22 | 23 | from xata.client import ( 24 | DEFAULT_CONTROL_PLANE_DOMAIN, 25 | DEFAULT_DATA_PLANE_DOMAIN, 26 | XataClient, 27 | ) 28 | 29 | 30 | class TestClientWithCustomDomains(unittest.TestCase): 31 | def test_control_plane_param_init(self): 32 | domain = "a-control-plane.lol" 33 | client = XataClient(api_key="my-key", workspace_id="ws_id", domain_core=domain) 34 | assert domain == client.get_config()["domain_core"] 35 | assert DEFAULT_DATA_PLANE_DOMAIN == client.get_config()["domain_workspace"] 36 | 37 | def test_data_plane_param_init(self): 38 | domain = "super-custom-domain.com" 39 | client = XataClient(api_key="my-key", workspace_id="ws_id", domain_workspace=domain) 40 | assert domain == client.get_config()["domain_workspace"] 41 | assert DEFAULT_CONTROL_PLANE_DOMAIN == client.get_config()["domain_core"] 42 | 43 | def test_data_plane_db_url_param_init(self): 44 | db_url = "https://py-sdk-abc1234.my-region.super-custom-domain.com/db/custom-01" 45 | client = XataClient(api_key="my-key", db_url=db_url) 46 | assert "super-custom-domain.com" == client.get_config()["domain_workspace"] 47 | 48 | def test_data_plane_init_with_db_url_envar(self): 49 | os.environ["XATA_DATABASE_URL"] = "https://py-sdk-abc1234.my-region.envvar-domain.io/db/custom-01" 50 | client = XataClient(api_key="my-key") 51 | assert "envvar-domain.io" == client.get_config()["domain_workspace"] 52 | os.environ.pop("XATA_DATABASE_URL") 53 | 54 | def test_data_plane_db_url_complex_ones(self): 55 | db_url = "https://py-sdk-abc1234.my-region.super.custom-domain.co.at/db/custom-01" 56 | client = XataClient(api_key="my-key", db_url=db_url) 57 | assert "super.custom-domain.co.at" == client.get_config()["domain_workspace"] 58 | -------------------------------------------------------------------------------- /tests/unit-tests/files_transformations_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import unittest 21 | 22 | from xata.client import XataClient 23 | 24 | 25 | class TestFileTransformations(unittest.TestCase): 26 | def test_single_transform_url(self): 27 | client = XataClient(api_key="api_key", workspace_id="ws_id") 28 | 29 | url = client.files().transform_url( 30 | "https://us-east-1.storage.xata.sh/4u1fh2o6p10blbutjnphcste94", 31 | { 32 | "height": 100, 33 | }, 34 | ) 35 | 36 | expected = "https://us-east-1.storage.xata.sh/transform/height=100/4u1fh2o6p10blbutjnphcste94" 37 | assert url == expected 38 | 39 | def test_multiple_transform_url(self): 40 | client = XataClient(api_key="api_key", workspace_id="ws_id") 41 | 42 | url = client.files().transform_url( 43 | "https://us-east-1.storage.xata.sh/4u1fh2o6p10blbutjnphcste94", 44 | {"width": 100, "height": 100, "fit": "cover", "gravity": {"x": 0, "y": 1}}, 45 | ) 46 | 47 | excepted = "https://us-east-1.storage.xata.sh/transform/width=100,height=100,fit=cover,gravity=0x1/4u1fh2o6p10blbutjnphcste94" 48 | 49 | assert url == excepted 50 | -------------------------------------------------------------------------------- /tests/unit-tests/helpers_bulk_processor_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import unittest 21 | 22 | import pytest 23 | 24 | from xata.client import XataClient 25 | from xata.helpers import BulkProcessor 26 | 27 | 28 | class TestHelpersBulkProcessor(unittest.TestCase): 29 | def test_bulk_processor_init(self): 30 | client = XataClient(api_key="api_key", workspace_id="ws_id") 31 | 32 | with pytest.raises(Exception) as e: 33 | BulkProcessor(client, thread_pool_size=-1) 34 | assert str(e.value) == "thread pool size must be greater than 0, default: 4" 35 | 36 | with pytest.raises(Exception) as e: 37 | BulkProcessor(client, batch_size=-1) 38 | assert str(e.value) == "batch size can not be less than one, default: 50" 39 | 40 | with pytest.raises(Exception) as e: 41 | BulkProcessor(client, flush_interval=-1) 42 | assert str(e.value) == "flush interval can not be negative, default: 2.000000" 43 | 44 | with pytest.raises(Exception) as e: 45 | BulkProcessor(client, processing_timeout=-1) 46 | assert str(e.value) == "processing timeout can not be negative, default: 0.050000" 47 | 48 | def test_bulk_processor_stats(self): 49 | client = XataClient(api_key="api_key", workspace_id="ws_id") 50 | bp = BulkProcessor(client) 51 | sts = bp.get_stats() 52 | 53 | assert "total" in sts 54 | assert "queue" in sts 55 | assert "failed_batches" in sts 56 | assert "tables" in sts 57 | assert sts["total"] == 0 58 | assert sts["queue"] == 0 59 | assert sts["failed_batches"] == 0 60 | assert sts["tables"] == {} 61 | -------------------------------------------------------------------------------- /tests/unit-tests/helpers_to_rfc3339.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import unittest 21 | from datetime import datetime 22 | 23 | from pytz import timezone, utc 24 | 25 | from xata.helpers import to_rfc339 26 | 27 | 28 | class TestHelpersToRfc3339(unittest.TestCase): 29 | def test_to_rfc3339(self): 30 | dt1 = datetime.strptime("2023-03-20 13:42:00", "%Y-%m-%d %H:%M:%S") 31 | assert to_rfc339(dt1) == "2023-03-20T13:42:00+00:00" 32 | 33 | dt2 = datetime.strptime("2023-03-20 13:42", "%Y-%m-%d %H:%M") 34 | assert to_rfc339(dt2) == "2023-03-20T13:42:00+00:00" 35 | 36 | dt3 = datetime.strptime("2023-03-20", "%Y-%m-%d") 37 | assert to_rfc339(dt3) == "2023-03-20T00:00:00+00:00" 38 | 39 | dt4 = datetime.strptime("2023-03-20 13:42:16", "%Y-%m-%d %H:%M:%S") 40 | tz4 = timezone("Europe/Vienna") 41 | assert to_rfc339(dt4, tz4) == "2023-03-20T13:42:16+01:05" 42 | 43 | dt4 = datetime.strptime("2023-03-20 13:42:16", "%Y-%m-%d %H:%M:%S") 44 | tz4 = timezone("America/New_York") 45 | assert to_rfc339(dt4, tz4) == "2023-03-20T13:42:16-04:56" 46 | 47 | dt6 = datetime.strptime("2023-03-20 13:42:16", "%Y-%m-%d %H:%M:%S") 48 | assert to_rfc339(dt6, utc) == "2023-03-20T13:42:16+00:00" 49 | -------------------------------------------------------------------------------- /tests/unit-tests/helpers_transaction_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import unittest 21 | 22 | from xata.client import XataClient 23 | from xata.helpers import Transaction 24 | 25 | 26 | class TestHelpersTranaction(unittest.TestCase): 27 | def test_operations_size(self): 28 | client = XataClient(api_key="api_key", workspace_id="ws_id") 29 | trx = Transaction(client) 30 | 31 | assert trx.size() == 0 32 | trx.get("posts", "abc") 33 | assert trx.size() == 1 34 | -------------------------------------------------------------------------------- /tests/unit-tests/utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import re 21 | 22 | PATTERNS_UUID4 = re.compile(r"^[\da-f]{8}-([\da-f]{4}-){3}[\da-f]{12}$", re.IGNORECASE) 23 | PATTERNS_SDK_VERSION = re.compile(r"^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}(.?[ab][0-9]{1,3})*$") 24 | -------------------------------------------------------------------------------- /xata/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | from .client import XataClient 21 | 22 | __all__ = ("XataClient", "BulkProcessor", "to_rfc3339", "Transaction") 23 | -------------------------------------------------------------------------------- /xata/api/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | -------------------------------------------------------------------------------- /xata/api/authentication.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | # ------------------------------------------------------- # 21 | # Authentication 22 | # Authentication and API Key management. 23 | # Specification: core:v1.0 24 | # ------------------------------------------------------- # 25 | 26 | from xata.api_request import ApiRequest 27 | from xata.api_response import ApiResponse 28 | 29 | 30 | class Authentication(ApiRequest): 31 | 32 | scope = "core" 33 | 34 | def get_user_api_keys(self) -> ApiResponse: 35 | """ 36 | Retrieve a list of existing user API keys 37 | 38 | Reference: https://xata.io/docs/api-reference/user/keys#get-the-list-of-user-api-keys 39 | Path: /user/keys 40 | Method: GET 41 | Response status codes: 42 | - 200: OK 43 | - 400: Bad Request 44 | - 401: Authentication Error 45 | - 404: Example response 46 | - 5XX: Unexpected Error 47 | Response: application/json 48 | 49 | 50 | :returns ApiResponse 51 | """ 52 | url_path = "/user/keys" 53 | return self.request("GET", url_path) 54 | 55 | def create_user_api_keys(self, key_name: str) -> ApiResponse: 56 | """ 57 | Create and return new API key 58 | 59 | Reference: https://xata.io/docs/api-reference/user/keys/key_name#create-and-return-new-api-key 60 | Path: /user/keys/{key_name} 61 | Method: POST 62 | Response status codes: 63 | - 201: OK 64 | - 400: Bad Request 65 | - 401: Authentication Error 66 | - 404: Example response 67 | - 5XX: Unexpected Error 68 | Response: application/json 69 | 70 | :param key_name: str API Key name 71 | 72 | :returns ApiResponse 73 | """ 74 | url_path = f"/user/keys/{key_name}" 75 | return self.request("POST", url_path) 76 | 77 | def delete_user_api_keys(self, key_name: str) -> ApiResponse: 78 | """ 79 | Delete an existing API key 80 | 81 | Reference: https://xata.io/docs/api-reference/user/keys/key_name#delete-an-existing-api-key 82 | Path: /user/keys/{key_name} 83 | Method: DELETE 84 | Response status codes: 85 | - 204: No Content 86 | - 400: Bad Request 87 | - 401: Authentication Error 88 | - 404: Example response 89 | - 5XX: Unexpected Error 90 | 91 | :param key_name: str API Key name 92 | 93 | :returns ApiResponse 94 | """ 95 | url_path = f"/user/keys/{key_name}" 96 | return self.request("DELETE", url_path) 97 | -------------------------------------------------------------------------------- /xata/api/oauth.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | # ------------------------------------------------------- # 21 | # Oauth 22 | # OAuth 23 | # Specification: core:v1.0 24 | # ------------------------------------------------------- # 25 | 26 | from xata.api_request import ApiRequest 27 | from xata.api_response import ApiResponse 28 | 29 | 30 | class Oauth(ApiRequest): 31 | 32 | scope = "core" 33 | 34 | def get_clients(self) -> ApiResponse: 35 | """ 36 | Retrieve the list of OAuth Clients that a user has authorized 37 | 38 | Reference: https://xata.io/docs/api-reference/user/oauth/clients#get-the-list-of-user-oauth-clients 39 | Path: /user/oauth/clients 40 | Method: GET 41 | Response status codes: 42 | - 200: OK 43 | - 400: Bad Request 44 | - 401: Authentication Error 45 | - 404: Example response 46 | - 5XX: Unexpected Error 47 | Response: application/json 48 | 49 | 50 | :returns ApiResponse 51 | """ 52 | url_path = "/user/oauth/clients" 53 | return self.request("GET", url_path) 54 | 55 | def delete_clients(self, client_id: str) -> ApiResponse: 56 | """ 57 | Delete the oauth client for the user and revoke all access 58 | 59 | Reference: https://xata.io/docs/api-reference/user/oauth/clients/client_id#delete-the-oauth-client-for-the-user 60 | Path: /user/oauth/clients/{client_id} 61 | Method: DELETE 62 | Response status codes: 63 | - 204: No Content 64 | - 400: Bad Request 65 | - 401: Authentication Error 66 | - 404: Example response 67 | - 5XX: Unexpected Error 68 | 69 | :param client_id: str 70 | 71 | :returns ApiResponse 72 | """ 73 | url_path = f"/user/oauth/clients/{client_id}" 74 | return self.request("DELETE", url_path) 75 | 76 | def get_access_tokens(self) -> ApiResponse: 77 | """ 78 | Retrieve the list of valid OAuth Access Tokens on the current user's account 79 | 80 | Reference: https://xata.io/docs/api-reference/user/oauth/tokens#get-the-list-of-user-oauth-access-tokens 81 | Path: /user/oauth/tokens 82 | Method: GET 83 | Response status codes: 84 | - 200: OK 85 | - 400: Bad Request 86 | - 401: Authentication Error 87 | - 404: Example response 88 | - 5XX: Unexpected Error 89 | Response: application/json 90 | 91 | 92 | :returns ApiResponse 93 | """ 94 | url_path = "/user/oauth/tokens" 95 | return self.request("GET", url_path) 96 | 97 | def delete_access_tokens(self, token: str) -> ApiResponse: 98 | """ 99 | Expires the access token for a third party app 100 | 101 | Reference: https://xata.io/docs/api-reference/user/oauth/tokens/token#delete-an-access-token-for-a-third-party-app 102 | Path: /user/oauth/tokens/{token} 103 | Method: DELETE 104 | Response status codes: 105 | - 204: No Content 106 | - 400: Bad Request 107 | - 401: Authentication Error 108 | - 404: Example response 109 | - 409: Example response 110 | - 5XX: Unexpected Error 111 | 112 | :param token: str 113 | 114 | :returns ApiResponse 115 | """ 116 | url_path = f"/user/oauth/tokens/{token}" 117 | return self.request("DELETE", url_path) 118 | 119 | def update_access_tokens(self, token: str, payload: dict) -> ApiResponse: 120 | """ 121 | Updates partially the access token for a third party app 122 | 123 | Reference: https://xata.io/docs/api-reference/user/oauth/tokens/token#updates-an-access-token-for-a-third-party-app 124 | Path: /user/oauth/tokens/{token} 125 | Method: PATCH 126 | Response status codes: 127 | - 200: OK 128 | - 400: Bad Request 129 | - 401: Authentication Error 130 | - 404: Example response 131 | - 409: Example response 132 | - 5XX: Unexpected Error 133 | Response: application/json 134 | 135 | :param token: str 136 | :param payload: dict content 137 | 138 | :returns ApiResponse 139 | """ 140 | url_path = f"/user/oauth/tokens/{token}" 141 | headers = {"content-type": "application/json"} 142 | return self.request("PATCH", url_path, headers, payload) 143 | -------------------------------------------------------------------------------- /xata/api/sql.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | # ------------------------------------------------------- # 21 | # Sql 22 | # SQL service access 23 | # Specification: workspace:v1.0 24 | # ------------------------------------------------------- # 25 | 26 | from xata.api_request import ApiRequest 27 | from xata.api_response import ApiResponse 28 | 29 | 30 | class Sql(ApiRequest): 31 | 32 | scope = "workspace" 33 | 34 | def query( 35 | self, 36 | statement: str, 37 | params: list = None, 38 | consistency: str = "strong", 39 | db_name: str = None, 40 | branch_name: str = None, 41 | ) -> ApiResponse: 42 | """ 43 | Run an SQL query across the database branch. 44 | 45 | Reference: https://xata.io/docs/api-reference/db/db_branch_name/sql#sql-query 46 | Path: /db/{db_branch_name}/sql 47 | Method: POST 48 | Response status codes: 49 | - 200: OK 50 | - 201: OK 51 | - 400: Bad Request 52 | - 401: Authentication Error 53 | - 404: Example response 54 | - 503: ServiceUnavailable 55 | - 5XX: Unexpected Error 56 | - default: Unexpected Error 57 | 58 | :param statement: str The statement to run 59 | :param params: dict The query parameters list. default: None 60 | :param consistency: str The consistency level for this request. default: strong 61 | :param db_name: str = None The name of the database to query. Default: database name from the client. 62 | :param branch_name: str = None The name of the branch to query. Default: branch name from the client. 63 | 64 | :returns ApiResponse 65 | """ 66 | db_branch_name = self.client.get_db_branch_name(db_name, branch_name) 67 | url_path = f"/db/{db_branch_name}/sql" 68 | headers = {"content-type": "application/json"} 69 | payload = { 70 | "statement": statement, 71 | "params": params, 72 | "consistency": consistency, 73 | } 74 | return self.request("POST", url_path, headers, payload) 75 | -------------------------------------------------------------------------------- /xata/api/users.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | # ------------------------------------------------------- # 21 | # Users 22 | # Users management 23 | # Specification: core:v1.0 24 | # ------------------------------------------------------- # 25 | 26 | from xata.api_request import ApiRequest 27 | from xata.api_response import ApiResponse 28 | 29 | 30 | class Users(ApiRequest): 31 | 32 | scope = "core" 33 | 34 | def get(self) -> ApiResponse: 35 | """ 36 | Return details of the user making the request 37 | 38 | Reference: https://xata.io/docs/api-reference/user#get-user-details 39 | Path: /user 40 | Method: GET 41 | Response status codes: 42 | - 200: OK 43 | - 400: Bad Request 44 | - 401: Authentication Error 45 | - 404: Example response 46 | - 5XX: Unexpected Error 47 | Response: application/json 48 | 49 | 50 | :returns ApiResponse 51 | """ 52 | url_path = "/user" 53 | return self.request("GET", url_path) 54 | 55 | def update(self, payload: dict) -> ApiResponse: 56 | """ 57 | Update user info 58 | 59 | Reference: https://xata.io/docs/api-reference/user#update-user-info 60 | Path: /user 61 | Method: PUT 62 | Response status codes: 63 | - 200: OK 64 | - 400: Bad Request 65 | - 401: Authentication Error 66 | - 404: Example response 67 | - 5XX: Unexpected Error 68 | Response: application/json 69 | 70 | :param payload: dict content 71 | 72 | :returns ApiResponse 73 | """ 74 | url_path = "/user" 75 | headers = {"content-type": "application/json"} 76 | return self.request("PUT", url_path, headers, payload) 77 | 78 | def delete(self) -> ApiResponse: 79 | """ 80 | Delete the user making the request 81 | 82 | Reference: https://xata.io/docs/api-reference/user#delete-user 83 | Path: /user 84 | Method: DELETE 85 | Response status codes: 86 | - 204: No Content 87 | - 400: Bad Request 88 | - 401: Authentication Error 89 | - 404: Example response 90 | - 5XX: Unexpected Error 91 | 92 | 93 | :returns ApiResponse 94 | """ 95 | url_path = "/user" 96 | return self.request("DELETE", url_path) 97 | -------------------------------------------------------------------------------- /xata/api_request.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import logging 21 | 22 | from requests import Session, request 23 | 24 | from xata.api_response import ApiResponse 25 | 26 | from .errors import RateLimitError, UnauthorizedError, XataServerError 27 | 28 | 29 | class ApiRequest: 30 | def __init__(self, client): 31 | self.session = Session() 32 | self.client = client 33 | self.logger = logging.getLogger(self.__class__.__name__) 34 | 35 | def get_scope(self) -> str: 36 | return self.scope 37 | 38 | def is_control_plane(self) -> bool: 39 | return self.get_scope() == "core" 40 | 41 | def get_base_url(self) -> str: 42 | if self.is_control_plane(): 43 | return "https://" + self.client.get_config()["domain_core"] 44 | # Base URL must be build on the fly as the region & workspace Id can change 45 | cfg = self.client.get_config() 46 | return "https://%s.%s.%s" % (cfg["workspaceId"], cfg["region"], cfg["domain_workspace"]) 47 | 48 | def get_upload_base_url(self) -> str: 49 | """ 50 | Upload of files has dedidcated URL that is not represented in the OAS 51 | 52 | :returns str 53 | """ 54 | cfg = self.client.get_config() 55 | return "https://%s.%s.upload.%s" % (cfg["workspaceId"], cfg["region"], cfg["domain_workspace"]) 56 | 57 | def request( 58 | self, 59 | http_method: str, 60 | url_path: str, 61 | headers: dict = {}, 62 | payload: dict = None, 63 | data: bytes = None, 64 | is_streaming: bool = False, 65 | override_base_url=None, 66 | ) -> ApiResponse: 67 | """ 68 | :param http_method: str 69 | :param url_path: str 70 | :headers: dict = {} 71 | :param payload: dict = None 72 | :param data: bytes = None 73 | :param is_streaming: bool = False 74 | :param override_base_url = None Set alternative base URL 75 | 76 | :returns ApiResponse 77 | 78 | :raises RateLimitError 79 | :raises UnauthorizedError 80 | :raises ServerError 81 | """ 82 | headers = {**headers, **self.client.get_headers()} 83 | base_url = self.get_base_url() if override_base_url is None else override_base_url 84 | url = "%s/%s" % (base_url, url_path.lstrip("/")) 85 | 86 | # In order not exhaust the connection pool with open connections from unread streams 87 | # we opt for Session usage on all non-stream requests 88 | # https://requests.readthedocs.io/en/latest/user/advanced/#body-content-workflow 89 | if is_streaming: 90 | if payload is None and data is None: 91 | resp = request(http_method, url, headers=headers, stream=True) 92 | elif data is not None: 93 | resp = request(http_method, url, headers=headers, data=data, stream=True) 94 | else: 95 | resp = request(http_method, url, headers=headers, json=payload, stream=True) 96 | else: 97 | if payload is None and data is None: 98 | resp = self.session.request(http_method, url, headers=headers) 99 | elif data is not None: 100 | resp = self.session.request(http_method, url, headers=headers, data=data) 101 | else: 102 | resp = self.session.request(http_method, url, headers=headers, json=payload) 103 | 104 | # Any special status code we can raise an exception for ? 105 | if resp.status_code == 429: 106 | raise RateLimitError(f"code: {resp.status_code}, rate limited: {resp.json()}") 107 | if resp.status_code == 401: 108 | raise UnauthorizedError(f"code: {resp.status_code}, unauthorized: {resp.json()}") 109 | elif resp.status_code >= 500: 110 | raise XataServerError(f"code: {resp.status_code}, server error: {resp.text}") 111 | 112 | return ApiResponse(resp) 113 | -------------------------------------------------------------------------------- /xata/api_response.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | import logging 21 | from typing import Union 22 | 23 | import deprecation 24 | from requests import Response 25 | from requests.exceptions import JSONDecodeError 26 | 27 | 28 | class ApiResponse(dict): 29 | def __init__(self, response: Response): 30 | self.response = response 31 | self.logger = logging.getLogger(self.__class__.__name__) 32 | 33 | # Don't serialize an empty response 34 | try: 35 | self.update(self.response.json()) 36 | except JSONDecodeError: 37 | pass 38 | 39 | # log server message 40 | if "x-xata-message" in self.headers: 41 | self.logger.warn(self.headers["x-xata-message"]) 42 | 43 | def server_message(self) -> Union[str, None]: 44 | """ 45 | Get the server message from the response, if you need the error message 46 | please the property error_message. This channel is only relevant for 47 | deprecation messages or other meta information from Xata. 48 | :returns str | None 49 | """ 50 | return self.headers["x-xata-message"] if "x-xata-message" in self.headers else None 51 | 52 | def is_success(self) -> bool: 53 | """ 54 | Was the request successful? 55 | :returns bool 56 | """ 57 | return 200 <= self.status_code < 300 58 | 59 | def get_cursor(self) -> Union[str, None]: 60 | """ 61 | If the response has a cursor, return it 62 | :returns str or None 63 | """ 64 | try: 65 | return self.response.json()["meta"]["page"]["cursor"] 66 | except Exception: 67 | return None 68 | 69 | def has_more_results(self) -> bool: 70 | """ 71 | Are there more result pages available? 72 | :return bool 73 | """ 74 | try: 75 | return self.response.json()["meta"]["page"].get("more", False) 76 | except Exception: 77 | return False 78 | 79 | @deprecation.deprecated( 80 | deprecated_in="1.0.0a2", 81 | removed_in="2.0.0", 82 | details="This method is obsolete as this class directly returns a dict", 83 | ) 84 | def json(self) -> dict: 85 | """ 86 | Legacy support for requests.Response from 0.x 87 | :returns dict 88 | """ 89 | return self.response.json() 90 | 91 | @property 92 | def status_code(self) -> int: 93 | """ 94 | Get the status code of the response 95 | :returns int 96 | """ 97 | return self.response.status_code 98 | 99 | @property 100 | def error_message(self) -> Union[str, None]: 101 | """ 102 | Get the error message if it is set, otherwise None 103 | :returns str | None 104 | """ 105 | if self.status_code < 300: 106 | return None 107 | return self.response.json().get("message", None) 108 | 109 | @property 110 | def headers(self) -> dict: 111 | """ 112 | Get the response headers 113 | :returns dict 114 | """ 115 | return self.response.headers 116 | 117 | @property 118 | def content(self) -> bytes: 119 | """ 120 | For files support, to get the file content 121 | :returns bytes 122 | """ 123 | return self.response.content 124 | -------------------------------------------------------------------------------- /xata/errors.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Xatabase, Inc under one or more contributor 3 | # license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright 5 | # ownership. Xatabase, Inc licenses this file to you under the 6 | # Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You 8 | # may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | 21 | class UnauthorizedError(Exception): 22 | pass 23 | 24 | 25 | class RateLimitError(Exception): 26 | pass 27 | 28 | 29 | class XataServerError(Exception): 30 | status_code: int 31 | message: str 32 | 33 | def __init__(self, status_code, message="n/a"): 34 | self.status_code = status_code 35 | self.message = message 36 | super().__init__(message) 37 | 38 | def __str__(self) -> str: 39 | return f"Server error: {self.status_code} {self.message}" 40 | --------------------------------------------------------------------------------