├── .circleci └── config.yml ├── .github └── workflows │ ├── codeql.yaml │ ├── docs.yaml │ └── pypi.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── arango ├── __init__.py ├── api.py ├── aql.py ├── backup.py ├── client.py ├── cluster.py ├── collection.py ├── connection.py ├── cursor.py ├── database.py ├── errno.py ├── exceptions.py ├── executor.py ├── formatter.py ├── foxx.py ├── graph.py ├── http.py ├── job.py ├── pregel.py ├── py.typed ├── replication.py ├── request.py ├── resolver.py ├── response.py ├── result.py ├── typings.py ├── utils.py └── wal.py ├── docs ├── Makefile ├── admin.rst ├── analyzer.rst ├── aql.rst ├── async.rst ├── auth.rst ├── backup.rst ├── batch.rst ├── certificates.rst ├── cluster.rst ├── collection.rst ├── compression.rst ├── conf.py ├── contributing.rst ├── cursor.rst ├── database.rst ├── document.rst ├── errno.rst ├── errors.rst ├── foxx.rst ├── graph.rst ├── http.rst ├── index.rst ├── indexes.rst ├── logging.rst ├── make.bat ├── overload.rst ├── overview.rst ├── pregel.rst ├── replication.rst ├── requirements.txt ├── schema.rst ├── serializer.rst ├── simple.rst ├── specs.rst ├── static │ └── logo.png ├── task.rst ├── threading.rst ├── transaction.rst ├── user.rst ├── view.rst └── wal.rst ├── pyproject.toml ├── setup.cfg ├── setup.py ├── starter.sh └── tests ├── __init__.py ├── conftest.py ├── executors.py ├── helpers.py ├── static ├── cluster-3.11.conf ├── cluster-3.12.conf ├── keyfile ├── service.zip ├── setup.sh ├── single-3.11.conf └── single-3.12.conf ├── test_analyzer.py ├── test_aql.py ├── test_async.py ├── test_auth.py ├── test_backup.py ├── test_batch.py ├── test_client.py ├── test_cluster.py ├── test_collection.py ├── test_cursor.py ├── test_database.py ├── test_document.py ├── test_exception.py ├── test_foxx.py ├── test_graph.py ├── test_index.py ├── test_overload.py ├── test_permission.py ├── test_pregel.py ├── test_replication.py ├── test_request.py ├── test_resolver.py ├── test_response.py ├── test_task.py ├── test_transaction.py ├── test_user.py ├── test_view.py └── test_wal.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | codecov: codecov/codecov@3.3.0 5 | 6 | workflows: 7 | ci: 8 | jobs: 9 | - lint 10 | - test: 11 | name: Python (<< matrix.python_version >>) - ArangoDB (<< matrix.arangodb_license >>, << matrix.arangodb_version >> << matrix.arangodb_config >>) 12 | matrix: 13 | parameters: 14 | python_version: ["3.9", "3.10", "3.11", "3.12"] 15 | arangodb_config: ["single", "cluster"] 16 | arangodb_license: ["community", "enterprise"] 17 | arangodb_version: ["3.11", "latest"] 18 | 19 | jobs: 20 | lint: 21 | docker: 22 | - image: python:latest 23 | steps: 24 | - checkout 25 | - run: 26 | name: Install Dependencies 27 | command: pip install .[dev] 28 | 29 | - run: 30 | name: Run black 31 | command: black --check --verbose --diff --color --config=pyproject.toml ./arango ./tests/ 32 | 33 | - run: 34 | name: Run flake8 35 | command: flake8 ./arango ./tests 36 | 37 | - run: 38 | name: Run isort 39 | command: isort --check ./arango ./tests 40 | 41 | - run: 42 | name: Run mypy 43 | command: mypy ./arango 44 | 45 | test: 46 | parameters: 47 | python_version: 48 | type: string 49 | arangodb_config: 50 | type: string 51 | arangodb_license: 52 | type: string 53 | arangodb_version: 54 | type: string 55 | # TODO: Reconsider using a docker image instead of a machine 56 | # i.e cimg/python:<< parameters.python_version >> 57 | machine: 58 | image: ubuntu-2204:current 59 | steps: 60 | - checkout 61 | 62 | - run: 63 | name: Set Up ArangoDB 64 | command: | 65 | chmod +x starter.sh 66 | ./starter.sh << parameters.arangodb_config >> << parameters.arangodb_license >> << parameters.arangodb_version >> 67 | 68 | - restore_cache: 69 | key: pip-and-local-cache 70 | 71 | # TODO: Revisit this bottleneck 72 | - run: 73 | name: Setup Python 74 | command: | 75 | pyenv --version 76 | pyenv install -f << parameters.python_version >> 77 | pyenv global << parameters.python_version >> 78 | 79 | - run: 80 | name: "Install Dependencies" 81 | command: pip install -e .[dev] 82 | 83 | - run: docker ps -a 84 | 85 | - run: docker logs arango 86 | 87 | - run: 88 | name: "Run pytest" 89 | command: | 90 | mkdir test-results 91 | 92 | args=("--junitxml=test-results/junit.xml" "--log-cli-level=DEBUG" "--host" "localhost" "--port=8529") 93 | if [ << parameters.arangodb_config >> = "cluster" ]; then 94 | args+=("--cluster" "--port=8539" "--port=8549") 95 | fi 96 | 97 | if [ << parameters.arangodb_license >> = "enterprise" ]; then 98 | args+=("--enterprise") 99 | fi 100 | 101 | echo "Running pytest with args: ${args[@]}" 102 | pytest --cov=arango --cov-report=xml --cov-report term-missing --color=yes --code-highlight=yes "${args[@]}" 103 | 104 | - store_artifacts: 105 | path: test-results 106 | 107 | - store_test_results: 108 | path: test-results 109 | 110 | - codecov/upload: 111 | file: coverage.xml 112 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | schedule: 7 | - cron: '21 2 * * 3' 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | 14 | permissions: 15 | security-events: write 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v3 20 | 21 | - name: Initialize CodeQL 22 | uses: github/codeql-action/init@v2 23 | 24 | - name: Perform CodeQL Analysis 25 | uses: github/codeql-action/analyze@v2 26 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | inputs: 7 | debug_enabled: 8 | type: boolean 9 | description: Debug with tmate 10 | required: false 11 | default: false 12 | 13 | jobs: 14 | # This has been migrated to CircleCI 15 | # test: 16 | # runs-on: ubuntu-latest 17 | 18 | # strategy: 19 | # fail-fast: false 20 | # matrix: 21 | # python_version: ["3.10"] #["3.8", "3.9", "3.10", "3.11", "3.12"] 22 | # arangodb_config: ["single", "cluster"] 23 | # arangodb_license: ["community", "enterprise"] 24 | # arangodb_version: ["3.10.10", "3.11.4", "latest"] 25 | 26 | # name: Test (${{ matrix.python_version }}:${{ matrix.arangodb_config }}:${{ matrix.arangodb_license }}:${{ matrix.arangodb_version }}) 27 | 28 | # steps: 29 | # - name: Checkout code 30 | # uses: actions/checkout@v4 31 | 32 | # - name: Set up Python 33 | # uses: actions/setup-python@v4 34 | # with: 35 | # python-version: ${{ matrix.python_version }} 36 | 37 | # - name: Setup ArangoDB 38 | # run: | 39 | # chmod +x starter.sh 40 | # ./starter.sh ${{ matrix.arangodb_config }} ${{ matrix.arangodb_license }} ${{ matrix.arangodb_version }} 41 | 42 | # - name: Install Dependencies 43 | # run: pip install -e .[dev] 44 | 45 | # - name: List Docker Containers 46 | # run: docker ps -a 47 | 48 | # - name: Pytest 49 | # run: | 50 | # args=("--host" "localhost" "--port=8529") 51 | 52 | # if [ ${{ matrix.arangodb_config }} = "cluster" ]; then 53 | # args+=("--cluster" "--port=8539" "--port=8549") 54 | # fi 55 | 56 | # if [ ${{ matrix.arangodb_license }} = "enterprise" ]; then 57 | # args+=("--enterprise") 58 | # fi 59 | 60 | # echo "Running pytest with args: ${args[@]}" 61 | # pytest --cov=arango --cov-report=xml "${args[@]}" 62 | 63 | docs: 64 | runs-on: ubuntu-latest 65 | 66 | name: Docs 67 | 68 | steps: 69 | - name: Checkout repository 70 | uses: actions/checkout@v4 71 | 72 | - name: Fetch all tags and branches 73 | run: git fetch --prune --unshallow 74 | 75 | - name: Create ArangoDB Docker container 76 | run: > 77 | docker create --name arango -p 8529:8529 -e ARANGO_ROOT_PASSWORD=passwd -v "$(pwd)/tests/static/":/tests/static 78 | arangodb/arangodb:latest --server.jwt-secret-keyfile=/tests/static/keyfile 79 | 80 | - name: Start ArangoDB Docker container 81 | run: docker start arango 82 | 83 | - name: Set up Python 84 | uses: actions/setup-python@v4 85 | with: 86 | python-version: '3.10' 87 | 88 | - name: Debug with tmate 89 | uses: mxschmitt/action-tmate@v3 90 | if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }} 91 | 92 | - name: Run pre-commit checks 93 | uses: pre-commit/action@v3.0.0 94 | 95 | - name: Install dependencies 96 | run: pip install .[dev] 97 | 98 | - name: Run Sphinx doctest 99 | run: python -m sphinx -b doctest docs docs/_build 100 | 101 | # No longer needed as this is handled by Read the Docs 102 | #- name: Generate Sphinx HTML 103 | # run: python -m sphinx -b html -W docs docs/_build 104 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yaml: -------------------------------------------------------------------------------- 1 | name: Upload to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | inputs: 8 | debug_enabled: 9 | type: boolean 10 | description: Debug with tmate 11 | required: false 12 | default: false 13 | 14 | jobs: 15 | upload: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v3 21 | 22 | - name: Fetch all tags and branches 23 | run: git fetch --prune --unshallow 24 | 25 | - name: Set up Python 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: "3.10" 29 | 30 | - name: Debug with tmate 31 | uses: mxschmitt/action-tmate@v3 32 | if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }} 33 | 34 | - name: Install dependencies 35 | run: | 36 | python -m pip install --upgrade pip 37 | pip install build twine 38 | 39 | - name: Build distribution 40 | run: python -m build 41 | 42 | - name: Publish to PyPI Test 43 | env: 44 | TWINE_USERNAME: __token__ 45 | TWINE_PASSWORD: ${{ secrets.PYPI_TEST_TOKEN }} 46 | run: twine upload --repository testpypi dist/* 47 | 48 | - name: Publish to PyPI 49 | env: 50 | TWINE_USERNAME: __token__ 51 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 52 | run: twine upload --repository pypi dist/* 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # MacOS 107 | .DS_Store 108 | 109 | # PyCharm 110 | .idea/ 111 | 112 | # ArangoDB Starter 113 | localdata/ 114 | 115 | # Node Modules 116 | node_modules/ 117 | 118 | # direnv 119 | .envrc 120 | .direnv/ 121 | 122 | # setuptools_scm 123 | arango/version.py 124 | 125 | # test results 126 | *_results.txt 127 | 128 | # devcontainers 129 | .devcontainer 130 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | # See https://pre-commit.com/hooks.html 5 | hooks: 6 | - id: check-case-conflict 7 | - id: check-executables-have-shebangs 8 | - id: check-merge-conflict 9 | - id: check-symlinks 10 | - id: check-toml 11 | - id: check-xml 12 | - id: check-yaml 13 | - id: debug-statements 14 | - id: detect-private-key 15 | - id: end-of-file-fixer 16 | - id: mixed-line-ending 17 | - id: pretty-format-json 18 | - id: trailing-whitespace 19 | 20 | - repo: https://github.com/psf/black 21 | rev: 23.1.0 22 | hooks: 23 | - id: black 24 | 25 | - repo: https://github.com/PyCQA/isort 26 | rev: 5.12.0 27 | hooks: 28 | - id: isort 29 | args: [ --profile, black ] 30 | 31 | - repo: https://github.com/PyCQA/flake8 32 | rev: 6.0.0 33 | hooks: 34 | - id: flake8 35 | 36 | - repo: https://github.com/pre-commit/mirrors-mypy 37 | rev: v0.991 38 | hooks: 39 | - id: mypy 40 | files: ^arango/ 41 | additional_dependencies: ['types-requests', "types-setuptools"] 42 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.11" 12 | 13 | # Build documentation in the "docs/" directory with Sphinx 14 | sphinx: 15 | configuration: docs/conf.py 16 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 17 | # builder: "dirhtml" 18 | builder: html 19 | # Fail on all warnings to avoid broken references 20 | fail_on_warning: true 21 | 22 | # Optionally build your docs in additional formats such as PDF and ePub 23 | # formats: 24 | # - pdf 25 | # - epub 26 | 27 | # Optional but recommended, declare the Python requirements required 28 | # to build your documentation 29 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 30 | python: 31 | install: 32 | - requirements: docs/requirements.txt 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Set up dev environment: 4 | ```shell 5 | cd ~/your/repository/fork # Activate venv if you have one (recommended) 6 | pip install -e .[dev] # Install dev dependencies (e.g. black, mypy, pre-commit) 7 | pre-commit install # Install git pre-commit hooks 8 | ``` 9 | 10 | Run unit tests with coverage: 11 | 12 | ```shell 13 | pytest --cov=arango --cov-report=html # Open htmlcov/index.html in your browser 14 | ``` 15 | 16 | To start and ArangoDB instance locally, run: 17 | 18 | ```shell 19 | ./starter.sh # Requires docker 20 | ``` 21 | 22 | Build and test documentation: 23 | 24 | ```shell 25 | python -m sphinx docs docs/_build # Open docs/_build/index.html in your browser 26 | ``` 27 | 28 | Thank you for your contribution! 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2021 Joohwan Oh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE 2 | prune tests 3 | include arango/py.typed 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://user-images.githubusercontent.com/2701938/108583516-c3576680-72ee-11eb-883f-2d9b52e74e45.png) 2 | 3 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/arangodb/python-arango/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/arangodb/python-arango/tree/main) 4 | [![CodeQL](https://github.com/arangodb/python-arango/actions/workflows/codeql.yaml/badge.svg)](https://github.com/arangodb/python-arango/actions/workflows/codeql.yaml) 5 | [![Docs](https://github.com/arangodb/python-arango/actions/workflows/docs.yaml/badge.svg)](https://github.com/arangodb/python-arango/actions/workflows/docs.yaml) 6 | [![Coverage Status](https://codecov.io/gh/arangodb/python-arango/branch/main/graph/badge.svg?token=M8zrjrzsUY)](https://codecov.io/gh/arangodb/python-arango) 7 | [![Last commit](https://img.shields.io/github/last-commit/arangodb/python-arango)](https://github.com/arangodb/python-arango/commits/main) 8 | 9 | [![PyPI version badge](https://img.shields.io/pypi/v/python-arango?color=3775A9&style=for-the-badge&logo=pypi&logoColor=FFD43B)](https://pypi.org/project/python-arango/) 10 | [![Python versions badge](https://img.shields.io/badge/3.9%2B-3776AB?style=for-the-badge&logo=python&logoColor=FFD43B&label=Python)](https://pypi.org/project/python-arango/) 11 | 12 | [![License](https://img.shields.io/github/license/arangodb/python-arango?color=9E2165&style=for-the-badge)](https://github.com/arangodb/python-arango/blob/main/LICENSE) 13 | [![Code style: black](https://img.shields.io/static/v1?style=for-the-badge&label=code%20style&message=black&color=black)](https://github.com/psf/black) 14 | [![Downloads](https://img.shields.io/pepy/dt/python-arango?style=for-the-badge&color=282661 15 | )](https://pepy.tech/project/python-arango) 16 | 17 | # Python-Arango 18 | 19 | Python driver for [ArangoDB](https://www.arangodb.com), a scalable multi-model 20 | database natively supporting documents, graphs and search. 21 | 22 | If you're interested in using asyncio, please check [python-arango-async](https://github.com/arangodb/python-arango-async). 23 | 24 | ## Requirements 25 | 26 | - ArangoDB version 3.11+ 27 | - Python version 3.9+ 28 | 29 | ## Installation 30 | 31 | ```shell 32 | pip install python-arango --upgrade 33 | ``` 34 | 35 | ## Getting Started 36 | 37 | Here is a simple usage example: 38 | 39 | ```python 40 | from arango import ArangoClient 41 | 42 | # Initialize the client for ArangoDB. 43 | client = ArangoClient(hosts="http://localhost:8529") 44 | 45 | # Connect to "_system" database as root user. 46 | sys_db = client.db("_system", username="root", password="passwd") 47 | 48 | # Create a new database named "test". 49 | sys_db.create_database("test") 50 | 51 | # Connect to "test" database as root user. 52 | db = client.db("test", username="root", password="passwd") 53 | 54 | # Create a new collection named "students". 55 | students = db.create_collection("students") 56 | 57 | # Add a persistent index to the collection. 58 | students.add_index({'type': 'persistent', 'fields': ['name'], 'unique': True}) 59 | 60 | # Insert new documents into the collection. 61 | students.insert({"name": "jane", "age": 39}) 62 | students.insert({"name": "josh", "age": 18}) 63 | students.insert({"name": "judy", "age": 21}) 64 | 65 | # Execute an AQL query and iterate through the result cursor. 66 | cursor = db.aql.execute("FOR doc IN students RETURN doc") 67 | student_names = [document["name"] for document in cursor] 68 | ``` 69 | 70 | Another example with [graphs](https://docs.arangodb.com/stable/graphs/): 71 | 72 | ```python 73 | from arango import ArangoClient 74 | 75 | # Initialize the client for ArangoDB. 76 | client = ArangoClient(hosts="http://localhost:8529") 77 | 78 | # Connect to "test" database as root user. 79 | db = client.db("test", username="root", password="passwd") 80 | 81 | # Create a new graph named "school". 82 | graph = db.create_graph("school") 83 | 84 | # Create a new EnterpriseGraph [Enterprise Edition] 85 | eegraph = db.create_graph( 86 | name="school", 87 | smart=True) 88 | 89 | # Create vertex collections for the graph. 90 | students = graph.create_vertex_collection("students") 91 | lectures = graph.create_vertex_collection("lectures") 92 | 93 | # Create an edge definition (relation) for the graph. 94 | edges = graph.create_edge_definition( 95 | edge_collection="register", 96 | from_vertex_collections=["students"], 97 | to_vertex_collections=["lectures"] 98 | ) 99 | 100 | # Insert vertex documents into "students" (from) vertex collection. 101 | students.insert({"_key": "01", "full_name": "Anna Smith"}) 102 | students.insert({"_key": "02", "full_name": "Jake Clark"}) 103 | students.insert({"_key": "03", "full_name": "Lisa Jones"}) 104 | 105 | # Insert vertex documents into "lectures" (to) vertex collection. 106 | lectures.insert({"_key": "MAT101", "title": "Calculus"}) 107 | lectures.insert({"_key": "STA101", "title": "Statistics"}) 108 | lectures.insert({"_key": "CSC101", "title": "Algorithms"}) 109 | 110 | # Insert edge documents into "register" edge collection. 111 | edges.insert({"_from": "students/01", "_to": "lectures/MAT101"}) 112 | edges.insert({"_from": "students/01", "_to": "lectures/STA101"}) 113 | edges.insert({"_from": "students/01", "_to": "lectures/CSC101"}) 114 | edges.insert({"_from": "students/02", "_to": "lectures/MAT101"}) 115 | edges.insert({"_from": "students/02", "_to": "lectures/STA101"}) 116 | edges.insert({"_from": "students/03", "_to": "lectures/CSC101"}) 117 | 118 | # Traverse the graph in outbound direction, breath-first. 119 | query = """ 120 | FOR v, e, p IN 1..3 OUTBOUND 'students/01' GRAPH 'school' 121 | OPTIONS { bfs: true, uniqueVertices: 'global' } 122 | RETURN {vertex: v, edge: e, path: p} 123 | """ 124 | cursor = db.aql.execute(query) 125 | ``` 126 | 127 | Please see the [documentation](https://docs.python-arango.com) for more details. 128 | -------------------------------------------------------------------------------- /arango/__init__.py: -------------------------------------------------------------------------------- 1 | import arango.errno as errno # noqa: F401 2 | from arango.client import ArangoClient # noqa: F401 3 | from arango.exceptions import * # noqa: F401 F403 4 | from arango.http import * # noqa: F401 F403 5 | -------------------------------------------------------------------------------- /arango/api.py: -------------------------------------------------------------------------------- 1 | __all__ = ["ApiGroup"] 2 | 3 | from typing import Callable, Optional, TypeVar 4 | 5 | from arango.connection import Connection 6 | from arango.executor import ApiExecutor 7 | from arango.request import Request 8 | from arango.response import Response 9 | from arango.result import Result 10 | 11 | T = TypeVar("T") 12 | 13 | 14 | class ApiGroup: 15 | """Base class for API groups. 16 | 17 | :param connection: HTTP connection. 18 | :param executor: API executor. 19 | """ 20 | 21 | def __init__(self, connection: Connection, executor: ApiExecutor) -> None: 22 | self._conn = connection 23 | self._executor = executor 24 | 25 | @property 26 | def conn(self) -> Connection: 27 | """Return the HTTP connection. 28 | 29 | :return: HTTP connection. 30 | :rtype: arango.connection.BasicConnection | arango.connection.JwtConnection | 31 | arango.connection.JwtSuperuserConnection 32 | """ 33 | return self._conn 34 | 35 | @property 36 | def db_name(self) -> str: 37 | """Return the name of the current database. 38 | 39 | :return: Database name. 40 | :rtype: str 41 | """ 42 | return self._conn.db_name 43 | 44 | @property 45 | def username(self) -> Optional[str]: 46 | """Return the username. 47 | 48 | :returns: Username. 49 | :rtype: str 50 | """ 51 | return self._conn.username 52 | 53 | @property 54 | def context(self) -> str: 55 | """Return the API execution context. 56 | 57 | :return: API execution context. Possible values are "default", "async", 58 | "batch" and "transaction". 59 | :rtype: str 60 | """ 61 | return self._executor.context 62 | 63 | def _execute( 64 | self, request: Request, response_handler: Callable[[Response], T] 65 | ) -> Result[T]: 66 | """Execute an API. 67 | 68 | :param request: HTTP request. 69 | :type request: arango.request.Request 70 | :param response_handler: HTTP response handler. 71 | :type response_handler: callable 72 | :return: API execution result. 73 | """ 74 | return self._executor.execute(request, response_handler) 75 | -------------------------------------------------------------------------------- /arango/pregel.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Pregel"] 2 | 3 | from typing import Optional, Sequence 4 | 5 | from arango.api import ApiGroup 6 | from arango.exceptions import ( 7 | PregelJobCreateError, 8 | PregelJobDeleteError, 9 | PregelJobGetError, 10 | ) 11 | from arango.formatter import format_pregel_job_data, format_pregel_job_list 12 | from arango.request import Request 13 | from arango.response import Response 14 | from arango.result import Result 15 | from arango.typings import Json 16 | 17 | 18 | class Pregel(ApiGroup): 19 | """Pregel API wrapper.""" 20 | 21 | def __repr__(self) -> str: 22 | return f"" 23 | 24 | def job(self, job_id: int) -> Result[Json]: 25 | """Return the details of a Pregel job. 26 | 27 | :param job_id: Pregel job ID. 28 | :type job_id: int 29 | :return: Details of the Pregel job. 30 | :rtype: dict 31 | :raise arango.exceptions.PregelJobGetError: If retrieval fails. 32 | """ 33 | request = Request(method="get", endpoint=f"/_api/control_pregel/{job_id}") 34 | 35 | def response_handler(resp: Response) -> Json: 36 | if resp.is_success: 37 | return format_pregel_job_data(resp.body) 38 | raise PregelJobGetError(resp, request) 39 | 40 | return self._execute(request, response_handler) 41 | 42 | def create_job( 43 | self, 44 | graph: str, 45 | algorithm: str, 46 | store: bool = True, 47 | max_gss: Optional[int] = None, 48 | thread_count: Optional[int] = None, 49 | async_mode: Optional[bool] = None, 50 | result_field: Optional[str] = None, 51 | algorithm_params: Optional[Json] = None, 52 | vertexCollections: Optional[Sequence[str]] = None, 53 | edgeCollections: Optional[Sequence[str]] = None, 54 | ) -> Result[int]: 55 | """Start a new Pregel job. 56 | 57 | :param graph: Graph name. 58 | :type graph: str 59 | :param algorithm: Algorithm (e.g. "pagerank"). 60 | :type algorithm: str 61 | :param store: If set to True, Pregel engine writes results back to the 62 | database. If set to False, results can be queried via AQL. 63 | :type store: bool 64 | :param max_gss: Max number of global iterations for the algorithm. 65 | :type max_gss: int | None 66 | :param thread_count: Number of parallel threads to use per worker. 67 | This does not influence the number of threads used to load or store 68 | data from the database (it depends on the number of shards). 69 | :type thread_count: int | None 70 | :param async_mode: If set to True, algorithms which support async mode 71 | run without synchronized global iterations. This might lead to 72 | performance increase if there are load imbalances. 73 | :type async_mode: bool | None 74 | :param result_field: If specified, most algorithms will write their 75 | results into this field. 76 | :type result_field: str | None 77 | :param algorithm_params: Additional algorithm parameters. 78 | :type algorithm_params: dict | None 79 | :param vertexCollections: List of vertex collection names. 80 | :type vertexCollections: Sequence[str] | None 81 | :param edgeCollections: List of edge collection names. 82 | :type edgeCollections: Sequence[str] | None 83 | :return: Pregel job ID. 84 | :rtype: int 85 | :raise arango.exceptions.PregelJobCreateError: If create fails. 86 | """ 87 | data: Json = {"algorithm": algorithm, "graphName": graph} 88 | 89 | if vertexCollections is not None: 90 | data["vertexCollections"] = vertexCollections 91 | if edgeCollections is not None: 92 | data["edgeCollections"] = edgeCollections 93 | 94 | if algorithm_params is None: 95 | algorithm_params = {} 96 | 97 | if store is not None: 98 | algorithm_params["store"] = store 99 | if max_gss is not None: 100 | algorithm_params["maxGSS"] = max_gss 101 | if thread_count is not None: 102 | algorithm_params["parallelism"] = thread_count 103 | if async_mode is not None: 104 | algorithm_params["async"] = async_mode 105 | if result_field is not None: 106 | algorithm_params["resultField"] = result_field 107 | if algorithm_params: 108 | data["params"] = algorithm_params 109 | 110 | request = Request(method="post", endpoint="/_api/control_pregel", data=data) 111 | 112 | def response_handler(resp: Response) -> int: 113 | if resp.is_success: 114 | return int(resp.body) 115 | raise PregelJobCreateError(resp, request) 116 | 117 | return self._execute(request, response_handler) 118 | 119 | def delete_job(self, job_id: int) -> Result[bool]: 120 | """Delete a Pregel job. 121 | 122 | :param job_id: Pregel job ID. 123 | :type job_id: int 124 | :return: True if Pregel job was deleted successfully. 125 | :rtype: bool 126 | :raise arango.exceptions.PregelJobDeleteError: If delete fails. 127 | """ 128 | request = Request(method="delete", endpoint=f"/_api/control_pregel/{job_id}") 129 | 130 | def response_handler(resp: Response) -> bool: 131 | if resp.is_success: 132 | return True 133 | raise PregelJobDeleteError(resp, request) 134 | 135 | return self._execute(request, response_handler) 136 | 137 | def jobs(self) -> Result[Json]: 138 | """Returns a list of currently running and recently 139 | finished Pregel jobs without retrieving their results. 140 | 141 | :return: Details of each running or recently finished Pregel job. 142 | :rtype: dict 143 | :raise arango.exceptions.PregelJobGetError: If retrieval fails. 144 | """ 145 | request = Request(method="get", endpoint="/_api/control_pregel") 146 | 147 | def response_handler(resp: Response) -> Json: 148 | if resp.is_success: 149 | return format_pregel_job_list(resp.body) 150 | raise PregelJobGetError(resp, request) 151 | 152 | return self._execute(request, response_handler) 153 | -------------------------------------------------------------------------------- /arango/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arangodb/python-arango/3769989166098b05849223bdc3eb90d934cb12ba/arango/py.typed -------------------------------------------------------------------------------- /arango/request.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Request"] 2 | 3 | from typing import Any, MutableMapping, Optional 4 | 5 | from arango.typings import DriverFlags, Fields, Headers, Params 6 | 7 | 8 | def normalize_headers( 9 | headers: Optional[Headers], driver_flags: Optional[DriverFlags] = None 10 | ) -> Headers: 11 | flags = "" 12 | if driver_flags is not None: 13 | for flag in driver_flags: 14 | flags = flags + flag + ";" 15 | driver_version = "8.2.0" 16 | driver_header = "python-arango/" + driver_version + " (" + flags + ")" 17 | normalized_headers: Headers = { 18 | "charset": "utf-8", 19 | "content-type": "application/json", 20 | "x-arango-driver": driver_header, 21 | } 22 | if headers is not None: 23 | for key, value in headers.items(): 24 | normalized_headers[key.lower()] = value 25 | 26 | return normalized_headers 27 | 28 | 29 | def normalize_params(params: Optional[Params]) -> MutableMapping[str, str]: 30 | normalized_params: MutableMapping[str, str] = {} 31 | 32 | if params is not None: 33 | for key, value in params.items(): 34 | if isinstance(value, bool): 35 | value = int(value) 36 | 37 | normalized_params[key] = str(value) 38 | 39 | return normalized_params 40 | 41 | 42 | class Request: 43 | """HTTP request. 44 | 45 | :param method: HTTP method in lowercase (e.g. "post"). 46 | :type method: str 47 | :param endpoint: API endpoint. 48 | :type endpoint: str 49 | :param headers: Request headers. 50 | :type headers: dict | None 51 | :param params: URL parameters. 52 | :type params: dict | None 53 | :param data: Request payload. 54 | :type data: str | bool | int | float | list | dict | None | MultipartEncoder 55 | :param read: Names of collections read during transaction. 56 | :type read: str | [str] | None 57 | :param write: Name(s) of collections written to during transaction with 58 | shared access. 59 | :type write: str | [str] | None 60 | :param exclusive: Name(s) of collections written to during transaction 61 | with exclusive access. 62 | :type exclusive: str | [str] | None 63 | :param deserialize: Whether the response body can be deserialized. 64 | :type deserialize: bool 65 | :param driver_flags: List of flags for the driver 66 | :type driver_flags: list 67 | 68 | :ivar method: HTTP method in lowercase (e.g. "post"). 69 | :vartype method: str 70 | :ivar endpoint: API endpoint. 71 | :vartype endpoint: str 72 | :ivar headers: Request headers. 73 | :vartype headers: dict | None 74 | :ivar params: URL (query) parameters. 75 | :vartype params: dict | None 76 | :ivar data: Request payload. 77 | :vartype data: str | bool | int | float | list | dict | None 78 | :ivar read: Names of collections read during transaction. 79 | :vartype read: str | [str] | None 80 | :ivar write: Name(s) of collections written to during transaction with 81 | shared access. 82 | :vartype write: str | [str] | None 83 | :ivar exclusive: Name(s) of collections written to during transaction 84 | with exclusive access. 85 | :vartype exclusive: str | [str] | None 86 | :ivar deserialize: Whether the response body can be deserialized. 87 | :vartype deserialize: bool 88 | :ivar driver_flags: List of flags for the driver 89 | :vartype driver_flags: list 90 | """ 91 | 92 | __slots__ = ( 93 | "method", 94 | "endpoint", 95 | "headers", 96 | "params", 97 | "data", 98 | "read", 99 | "write", 100 | "exclusive", 101 | "deserialize", 102 | "driver_flags", 103 | ) 104 | 105 | def __init__( 106 | self, 107 | method: str, 108 | endpoint: str, 109 | headers: Optional[Headers] = None, 110 | params: Optional[Params] = None, 111 | data: Any = None, 112 | read: Optional[Fields] = None, 113 | write: Optional[Fields] = None, 114 | exclusive: Optional[Fields] = None, 115 | deserialize: bool = True, 116 | driver_flags: Optional[DriverFlags] = None, 117 | ) -> None: 118 | self.method = method 119 | self.endpoint = endpoint 120 | self.headers: Headers = normalize_headers(headers, driver_flags) 121 | self.params: MutableMapping[str, str] = normalize_params(params) 122 | self.data = data 123 | self.read = read 124 | self.write = write 125 | self.exclusive = exclusive 126 | self.deserialize = deserialize 127 | self.driver_flags = driver_flags 128 | -------------------------------------------------------------------------------- /arango/resolver.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "HostResolver", 3 | "FallbackHostResolver", 4 | "PeriodicHostResolver", 5 | "SingleHostResolver", 6 | "RandomHostResolver", 7 | "RoundRobinHostResolver", 8 | ] 9 | 10 | import logging 11 | import random 12 | import time 13 | from abc import ABC, abstractmethod 14 | from typing import Optional, Set 15 | 16 | 17 | class HostResolver(ABC): # pragma: no cover 18 | """Abstract base class for host resolvers.""" 19 | 20 | def __init__(self, host_count: int = 1, max_tries: Optional[int] = None) -> None: 21 | max_tries = max_tries or host_count * 3 22 | if max_tries < host_count: 23 | raise ValueError("max_tries cannot be less than host_count") 24 | 25 | self._host_count = host_count 26 | self._max_tries = max_tries 27 | 28 | @abstractmethod 29 | def get_host_index(self, indexes_to_filter: Optional[Set[int]] = None) -> int: 30 | raise NotImplementedError 31 | 32 | @property 33 | def host_count(self) -> int: 34 | return self._host_count 35 | 36 | @property 37 | def max_tries(self) -> int: 38 | return self._max_tries 39 | 40 | 41 | class SingleHostResolver(HostResolver): 42 | """Single host resolver.""" 43 | 44 | def get_host_index(self, indexes_to_filter: Optional[Set[int]] = None) -> int: 45 | return 0 46 | 47 | 48 | class RandomHostResolver(HostResolver): 49 | """Random host resolver.""" 50 | 51 | def __init__(self, host_count: int, max_tries: Optional[int] = None) -> None: 52 | super().__init__(host_count, max_tries) 53 | 54 | def get_host_index(self, indexes_to_filter: Optional[Set[int]] = None) -> int: 55 | host_index = None 56 | indexes_to_filter = indexes_to_filter or set() 57 | while host_index is None or host_index in indexes_to_filter: 58 | host_index = random.randint(0, self.host_count - 1) 59 | 60 | return host_index 61 | 62 | 63 | class RoundRobinHostResolver(HostResolver): 64 | """Round-robin host resolver.""" 65 | 66 | def __init__(self, host_count: int, max_tries: Optional[int] = None) -> None: 67 | super().__init__(host_count, max_tries) 68 | self._index = -1 69 | 70 | def get_host_index(self, indexes_to_filter: Optional[Set[int]] = None) -> int: 71 | self._index = (self._index + 1) % self.host_count 72 | return self._index 73 | 74 | 75 | class PeriodicHostResolver(HostResolver): 76 | """ 77 | Changes the host every N requests. 78 | An optional timeout may be applied between host changes, 79 | such that all coordinators get a chance to update their view of the agency. 80 | For example, if one coordinator creates a database, the others may not be 81 | immediately aware of it. If the timeout is set to 1 second, then the host 82 | resolver waits for 1 second before changing the host. 83 | """ 84 | 85 | def __init__( 86 | self, 87 | host_count: int, 88 | max_tries: Optional[int] = None, 89 | requests_period: int = 100, 90 | switch_timeout: float = 0, 91 | ) -> None: 92 | super().__init__(host_count, max_tries) 93 | self._requests_period = requests_period 94 | self._switch_timeout = switch_timeout 95 | self._req_count = 0 96 | self._index = 0 97 | 98 | def get_host_index(self, indexes_to_filter: Optional[Set[int]] = None) -> int: 99 | indexes_to_filter = indexes_to_filter or set() 100 | self._req_count = (self._req_count + 1) % self._requests_period 101 | if self._req_count == 0 or self._index in indexes_to_filter: 102 | self._index = (self._index + 1) % self.host_count 103 | while self._index in indexes_to_filter: 104 | self._index = (self._index + 1) % self.host_count 105 | self._req_count = 0 106 | time.sleep(self._switch_timeout) 107 | return self._index 108 | 109 | 110 | class FallbackHostResolver(HostResolver): 111 | """ 112 | Fallback host resolver. 113 | If the current host fails, the next one is used. 114 | """ 115 | 116 | def __init__(self, host_count: int, max_tries: Optional[int] = None) -> None: 117 | super().__init__(host_count, max_tries) 118 | self._index = 0 119 | self._logger = logging.getLogger(self.__class__.__name__) 120 | 121 | def get_host_index(self, indexes_to_filter: Optional[Set[int]] = None) -> int: 122 | indexes_to_filter = indexes_to_filter or set() 123 | while self._index in indexes_to_filter: 124 | self._index = (self._index + 1) % self.host_count 125 | self._logger.debug(f"Trying fallback on host {self._index}") 126 | return self._index 127 | -------------------------------------------------------------------------------- /arango/response.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Response"] 2 | 3 | from typing import Any, MutableMapping, Optional 4 | 5 | 6 | class Response: 7 | """HTTP response. 8 | 9 | :param method: HTTP method in lowercase (e.g. "post"). 10 | :type method: str 11 | :param url: API URL. 12 | :type url: str 13 | :param headers: Response headers. 14 | :type headers: MutableMapping 15 | :param status_code: Response status code. 16 | :type status_code: int 17 | :param status_text: Response status text. 18 | :type status_text: str 19 | :param raw_body: Raw response body. 20 | :type raw_body: str 21 | 22 | :ivar method: HTTP method in lowercase (e.g. "post"). 23 | :vartype method: str 24 | :ivar url: API URL. 25 | :vartype url: str 26 | :ivar headers: Response headers. 27 | :vartype headers: MutableMapping 28 | :ivar status_code: Response status code. 29 | :vartype status_code: int 30 | :ivar status_text: Response status text. 31 | :vartype status_text: str 32 | :ivar raw_body: Raw response body. 33 | :vartype raw_body: str 34 | :ivar body: JSON-deserialized response body. 35 | :vartype body: str | bool | int | float | list | dict | None 36 | :ivar error_code: Error code from ArangoDB server. 37 | :vartype error_code: int 38 | :ivar error_message: Error message from ArangoDB server. 39 | :vartype error_message: str 40 | :ivar is_success: True if response status code was 2XX. 41 | :vartype is_success: bool 42 | """ 43 | 44 | __slots__ = ( 45 | "method", 46 | "url", 47 | "headers", 48 | "status_code", 49 | "status_text", 50 | "body", 51 | "raw_body", 52 | "error_code", 53 | "error_message", 54 | "is_success", 55 | ) 56 | 57 | def __init__( 58 | self, 59 | method: str, 60 | url: str, 61 | headers: MutableMapping[str, str], 62 | status_code: int, 63 | status_text: str, 64 | raw_body: str, 65 | ) -> None: 66 | self.method = method.lower() 67 | self.url = url 68 | self.headers = headers 69 | self.status_code = status_code 70 | self.status_text = status_text 71 | self.raw_body = raw_body 72 | 73 | # Populated later 74 | self.body: Any = None 75 | self.error_code: Optional[int] = None 76 | self.error_message: Optional[str] = None 77 | self.is_success: Optional[bool] = None 78 | -------------------------------------------------------------------------------- /arango/result.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Result"] 2 | 3 | from typing import TypeVar, Union 4 | 5 | from arango.job import AsyncJob, BatchJob 6 | 7 | T = TypeVar("T") 8 | 9 | Result = Union[T, AsyncJob[T], BatchJob[T], None] 10 | -------------------------------------------------------------------------------- /arango/typings.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "Fields", 3 | "Headers", 4 | "Json", 5 | "Jsons", 6 | "Params", 7 | "PrimitiveDataTypes", 8 | "CompoundDataTypes", 9 | "DataTypes", 10 | ] 11 | 12 | from numbers import Number 13 | from typing import Any, Dict, List, MutableMapping, Optional, Sequence, Union 14 | 15 | Json = Dict[str, Any] 16 | Jsons = List[Json] 17 | Params = MutableMapping[str, Union[bool, int, str]] 18 | Headers = MutableMapping[str, str] 19 | Fields = Union[str, Sequence[str]] 20 | DriverFlags = List[str] 21 | PrimitiveDataTypes = Optional[Union[bool, Number, str]] 22 | CompoundDataTypes = Optional[ 23 | Union[ 24 | Sequence[Optional[Union[PrimitiveDataTypes, "CompoundDataTypes"]]], 25 | MutableMapping[str, Optional[Union[PrimitiveDataTypes, "CompoundDataTypes"]]], 26 | ] 27 | ] 28 | DataTypes = Optional[Union[PrimitiveDataTypes, CompoundDataTypes]] 29 | -------------------------------------------------------------------------------- /arango/utils.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "suppress_warning", 3 | "get_col_name", 4 | "get_doc_id", 5 | "is_none_or_int", 6 | "is_none_or_str", 7 | ] 8 | 9 | import json 10 | import logging 11 | from contextlib import contextmanager 12 | from typing import Any, Iterator, Optional, Sequence, Union 13 | 14 | from arango.exceptions import DocumentParseError, SortValidationError 15 | from arango.typings import Json, Jsons 16 | 17 | 18 | @contextmanager 19 | def suppress_warning(logger_name: str) -> Iterator[None]: 20 | """Suppress logger messages. 21 | 22 | :param logger_name: Full name of the logger. 23 | :type logger_name: str 24 | """ 25 | logger = logging.getLogger(logger_name) 26 | original_log_level = logger.getEffectiveLevel() 27 | logger.setLevel(logging.CRITICAL) 28 | yield 29 | logger.setLevel(original_log_level) 30 | 31 | 32 | def get_col_name(doc: Union[str, Json]) -> str: 33 | """Return the collection name from input. 34 | 35 | :param doc: Document ID or body with "_id" field. 36 | :type doc: str | dict 37 | :return: Collection name. 38 | :rtype: str 39 | :raise arango.exceptions.DocumentParseError: If document ID is missing. 40 | """ 41 | try: 42 | doc_id: str = doc["_id"] if isinstance(doc, dict) else doc 43 | except KeyError: 44 | raise DocumentParseError('field "_id" required') 45 | else: 46 | return doc_id.split("/", 1)[0] 47 | 48 | 49 | def get_doc_id(doc: Union[str, Json]) -> str: 50 | """Return the document ID from input. 51 | 52 | :param doc: Document ID or body with "_id" field. 53 | :type doc: str | dict 54 | :return: Document ID. 55 | :rtype: str 56 | :raise arango.exceptions.DocumentParseError: If document ID is missing. 57 | """ 58 | try: 59 | doc_id: str = doc["_id"] if isinstance(doc, dict) else doc 60 | except KeyError: 61 | raise DocumentParseError('field "_id" required') 62 | else: 63 | return doc_id 64 | 65 | 66 | def is_none_or_int(obj: Any) -> bool: 67 | """Check if obj is None or a positive integer. 68 | 69 | :param obj: Object to check. 70 | :type obj: Any 71 | :return: True if object is None or a positive integer. 72 | :rtype: bool 73 | """ 74 | return obj is None or (isinstance(obj, int) and obj >= 0) 75 | 76 | 77 | def is_none_or_str(obj: Any) -> bool: 78 | """Check if obj is None or a string. 79 | 80 | :param obj: Object to check. 81 | :type obj: Any 82 | :return: True if object is None or a string. 83 | :rtype: bool 84 | """ 85 | return obj is None or isinstance(obj, str) 86 | 87 | 88 | def is_none_or_bool(obj: Any) -> bool: 89 | """Check if obj is None or a bool. 90 | 91 | :param obj: Object to check. 92 | :type obj: Any 93 | :return: True if object is None or a bool. 94 | :rtype: bool 95 | """ 96 | return obj is None or isinstance(obj, bool) 97 | 98 | 99 | def get_batches(elements: Sequence[Json], batch_size: int) -> Iterator[Sequence[Json]]: 100 | """Generator to split a list in batches 101 | of (maximum) **batch_size** elements each. 102 | 103 | :param elements: The list of elements. 104 | :type elements: Sequence[Json] 105 | :param batch_size: Max number of elements per batch. 106 | :type batch_size: int 107 | """ 108 | for index in range(0, len(elements), batch_size): 109 | yield elements[index : index + batch_size] 110 | 111 | 112 | def build_filter_conditions(filters: Json) -> str: 113 | """Build a filter condition for an AQL query. 114 | 115 | :param filters: Document filters. 116 | :type filters: Dict[str, Any] 117 | :return: The complete AQL filter condition. 118 | :rtype: str 119 | """ 120 | if not filters: 121 | return "" 122 | 123 | conditions = [] 124 | for k, v in filters.items(): 125 | field = k if "." in k else f"`{k}`" 126 | conditions.append(f"doc.{field} == {json.dumps(v)}") 127 | 128 | return "FILTER " + " AND ".join(conditions) 129 | 130 | 131 | def validate_sort_parameters(sort: Jsons) -> bool: 132 | """Validate sort parameters for an AQL query. 133 | 134 | :param sort: Document sort parameters. 135 | :type sort: Jsons 136 | :return: Validation success. 137 | :rtype: bool 138 | :raise arango.exceptions.SortValidationError: If sort parameters are invalid. 139 | """ 140 | assert isinstance(sort, Sequence) 141 | for param in sort: 142 | if "sort_by" not in param or "sort_order" not in param: 143 | raise SortValidationError( 144 | "Each sort parameter must have 'sort_by' and 'sort_order'." 145 | ) 146 | if param["sort_order"].upper() not in ["ASC", "DESC"]: 147 | raise SortValidationError("'sort_order' must be either 'ASC' or 'DESC'") 148 | return True 149 | 150 | 151 | def build_sort_expression(sort: Optional[Jsons]) -> str: 152 | """Build a sort condition for an AQL query. 153 | 154 | :param sort: Document sort parameters. 155 | :type sort: Jsons | None 156 | :return: The complete AQL sort condition. 157 | :rtype: str 158 | """ 159 | if not sort: 160 | return "" 161 | 162 | sort_chunks = [] 163 | for sort_param in sort: 164 | chunk = f"doc.{sort_param['sort_by']} {sort_param['sort_order']}" 165 | sort_chunks.append(chunk) 166 | 167 | return "SORT " + ", ".join(sort_chunks) 168 | -------------------------------------------------------------------------------- /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/admin.rst: -------------------------------------------------------------------------------- 1 | Server Administration 2 | --------------------- 3 | 4 | Python-arango provides operations for server administration and monitoring. 5 | Most of these operations can only be performed by admin users via ``_system`` 6 | database. 7 | 8 | **Example:** 9 | 10 | .. testcode:: 11 | 12 | from arango import ArangoClient 13 | 14 | # Initialize the ArangoDB client. 15 | client = ArangoClient() 16 | 17 | # Connect to "_system" database as root user. 18 | sys_db = client.db('_system', username='root', password='passwd') 19 | 20 | # Retrieve the server version. 21 | sys_db.version() 22 | 23 | # Retrieve the server details. 24 | sys_db.details() 25 | 26 | # Retrieve the target DB version. 27 | sys_db.required_db_version() 28 | 29 | # Retrieve the database engine. 30 | sys_db.engine() 31 | 32 | # Retrieve the server time. 33 | sys_db.time() 34 | 35 | # Retrieve the server role. 36 | sys_db.role() 37 | 38 | # Retrieve the server role in a cluster. 39 | sys_db.cluster.server_role() 40 | 41 | # Retrieve the server mode. 42 | sys_db.mode() 43 | 44 | # Retrieve the server mode in a cluster. 45 | sys_db.cluster.server_mode() 46 | 47 | # Set the server mode. 48 | sys_db.set_mode('readonly') 49 | sys_db.set_mode('default') 50 | 51 | # Retrieve the server statistics. 52 | sys_db.statistics() 53 | 54 | # Read the server log. 55 | sys_db.read_log(level="debug") 56 | 57 | # Retrieve the log levels. 58 | sys_db.log_levels() 59 | 60 | # Set the log . 61 | sys_db.set_log_levels( 62 | agency='DEBUG', 63 | deprecation='INFO', 64 | threads='WARNING' 65 | ) 66 | 67 | # Echo the last request. 68 | sys_db.echo() 69 | 70 | # Echo a request 71 | sys_db.echo('request goes here') 72 | 73 | # Reload the routing collection. 74 | sys_db.reload_routing() 75 | 76 | # Retrieve server metrics. 77 | sys_db.metrics() 78 | 79 | 80 | Features available in enterprise edition only: 81 | 82 | .. code-block:: python 83 | 84 | from arango import ArangoClient 85 | 86 | # Initialize the ArangoDB client. 87 | client = ArangoClient() 88 | 89 | # Connect to "_system" database as root user using JWT authentication. 90 | sys_db = client.db( 91 | '_system', 92 | username='root', 93 | password='passwd', 94 | auth_method='jwt' 95 | ) 96 | 97 | # Retrieve JWT secrets. 98 | sys_db.jwt_secrets() 99 | 100 | # Hot-reload JWT secrets. 101 | sys_db.reload_jwt_secrets() 102 | 103 | # Rotate the user-supplied keys for encryption. 104 | sys_db.encryption() 105 | 106 | 107 | See :ref:`StandardDatabase` for API specification. 108 | -------------------------------------------------------------------------------- /docs/analyzer.rst: -------------------------------------------------------------------------------- 1 | Analyzers 2 | --------- 3 | 4 | Python-arango supports **analyzers**. For more information on analyzers, refer 5 | to `ArangoDB manual`_. 6 | 7 | .. _ArangoDB manual: https://docs.arangodb.com 8 | 9 | **Example:** 10 | 11 | .. testcode:: 12 | 13 | from arango import ArangoClient 14 | 15 | # Initialize the ArangoDB client. 16 | client = ArangoClient() 17 | 18 | # Connect to "test" database as root user. 19 | db = client.db('test', username='root', password='passwd') 20 | 21 | # Retrieve list of analyzers. 22 | db.analyzers() 23 | 24 | # Create an analyzer. 25 | db.create_analyzer( 26 | name='test_analyzer', 27 | analyzer_type='identity', 28 | properties={}, 29 | features=[] 30 | ) 31 | 32 | # Delete an analyzer. 33 | db.delete_analyzer('test_analyzer', ignore_missing=True) 34 | 35 | Refer to :ref:`StandardDatabase` class for API specification. 36 | -------------------------------------------------------------------------------- /docs/aql.rst: -------------------------------------------------------------------------------- 1 | AQL 2 | ---- 3 | 4 | **ArangoDB Query Language (AQL)** is used to read and write data. It is similar 5 | to SQL for relational databases, but without the support for data definition 6 | operations such as creating or deleting :doc:`databases `, 7 | :doc:`collections ` or :doc:`indexes `. For more 8 | information, refer to `ArangoDB manual`_. 9 | 10 | .. _ArangoDB manual: https://docs.arangodb.com 11 | 12 | AQL Queries 13 | =========== 14 | 15 | AQL queries are invoked from AQL API wrapper. Executing queries returns 16 | :doc:`result cursors `. 17 | 18 | **Example:** 19 | 20 | .. testcode:: 21 | 22 | from arango import ArangoClient, AQLQueryKillError 23 | 24 | # Initialize the ArangoDB client. 25 | client = ArangoClient() 26 | 27 | # Connect to "test" database as root user. 28 | db = client.db('test', username='root', password='passwd') 29 | 30 | # Insert some test documents into "students" collection. 31 | db.collection('students').insert_many([ 32 | {'_key': 'Abby', 'age': 22}, 33 | {'_key': 'John', 'age': 18}, 34 | {'_key': 'Mary', 'age': 21} 35 | ]) 36 | 37 | # Get the AQL API wrapper. 38 | aql = db.aql 39 | 40 | # Retrieve the execution plan without running the query. 41 | aql.explain('FOR doc IN students RETURN doc') 42 | 43 | # Validate the query without executing it. 44 | aql.validate('FOR doc IN students RETURN doc') 45 | 46 | # Execute the query 47 | cursor = db.aql.execute( 48 | 'FOR doc IN students FILTER doc.age < @value RETURN doc', 49 | bind_vars={'value': 19} 50 | ) 51 | # Iterate through the result cursor 52 | student_keys = [doc['_key'] for doc in cursor] 53 | 54 | # List currently running queries. 55 | aql.queries() 56 | 57 | # List any slow queries. 58 | aql.slow_queries() 59 | 60 | # Clear slow AQL queries if any. 61 | aql.clear_slow_queries() 62 | 63 | # Retrieve AQL query tracking properties. 64 | aql.tracking() 65 | 66 | # Configure AQL query tracking properties. 67 | aql.set_tracking( 68 | max_slow_queries=10, 69 | track_bind_vars=True, 70 | track_slow_queries=True 71 | ) 72 | 73 | # Kill a running query (this should fail due to invalid ID). 74 | try: 75 | aql.kill('some_query_id') 76 | except AQLQueryKillError as err: 77 | assert err.http_code == 404 78 | assert err.error_code == 1591 79 | 80 | See :ref:`AQL` for API specification. 81 | 82 | 83 | AQL User Functions 84 | ================== 85 | 86 | **AQL User Functions** are custom functions you define in Javascript to extend 87 | AQL functionality. They are somewhat similar to SQL procedures. 88 | 89 | **Example:** 90 | 91 | .. testcode:: 92 | 93 | from arango import ArangoClient 94 | 95 | # Initialize the ArangoDB client. 96 | client = ArangoClient() 97 | 98 | # Connect to "test" database as root user. 99 | db = client.db('test', username='root', password='passwd') 100 | 101 | # Get the AQL API wrapper. 102 | aql = db.aql 103 | 104 | # Create a new AQL user function. 105 | aql.create_function( 106 | # Grouping by name prefix is supported. 107 | name='functions::temperature::converter', 108 | code='function (celsius) { return celsius * 1.8 + 32; }' 109 | ) 110 | # List AQL user functions. 111 | aql.functions() 112 | 113 | # Delete an existing AQL user function. 114 | aql.delete_function('functions::temperature::converter') 115 | 116 | See :ref:`AQL` for API specification. 117 | 118 | 119 | AQL Query Cache 120 | =============== 121 | 122 | **AQL Query Cache** is used to minimize redundant calculation of the same query 123 | results. It is useful when read queries are issued frequently and write queries 124 | are not. 125 | 126 | **Example:** 127 | 128 | .. testcode:: 129 | 130 | from arango import ArangoClient 131 | 132 | # Initialize the ArangoDB client. 133 | client = ArangoClient() 134 | 135 | # Connect to "test" database as root user. 136 | db = client.db('test', username='root', password='passwd') 137 | 138 | # Get the AQL API wrapper. 139 | aql = db.aql 140 | 141 | # Retrieve AQL query cache properties. 142 | aql.cache.properties() 143 | 144 | # Configure AQL query cache properties 145 | aql.cache.configure(mode='demand', max_results=10000) 146 | 147 | # Clear results in AQL query cache. 148 | aql.cache.clear() 149 | 150 | See :ref:`AQLQueryCache` for API specification. 151 | -------------------------------------------------------------------------------- /docs/async.rst: -------------------------------------------------------------------------------- 1 | Async API Execution 2 | ------------------- 3 | 4 | In **asynchronous API executions**, python-arango sends API requests to ArangoDB in 5 | fire-and-forget style. The server processes the requests in the background, and 6 | the results can be retrieved once available via :ref:`AsyncJob` objects. 7 | 8 | **Example:** 9 | 10 | .. testcode:: 11 | 12 | import time 13 | 14 | from arango import ( 15 | ArangoClient, 16 | AQLQueryExecuteError, 17 | AsyncJobCancelError, 18 | AsyncJobClearError 19 | ) 20 | 21 | # Initialize the ArangoDB client. 22 | client = ArangoClient() 23 | 24 | # Connect to "test" database as root user. 25 | db = client.db('test', username='root', password='passwd') 26 | 27 | # Begin async execution. This returns an instance of AsyncDatabase, a 28 | # database-level API wrapper tailored specifically for async execution. 29 | async_db = db.begin_async_execution(return_result=True) 30 | 31 | # Child wrappers are also tailored for async execution. 32 | async_aql = async_db.aql 33 | async_col = async_db.collection('students') 34 | 35 | # API execution context is always set to "async". 36 | assert async_db.context == 'async' 37 | assert async_aql.context == 'async' 38 | assert async_col.context == 'async' 39 | 40 | # On API execution, AsyncJob objects are returned instead of results. 41 | job1 = async_col.insert({'_key': 'Neal'}) 42 | job2 = async_col.insert({'_key': 'Lily'}) 43 | job3 = async_aql.execute('RETURN 100000') 44 | job4 = async_aql.execute('INVALID QUERY') # Fails due to syntax error. 45 | 46 | # Retrieve the status of each async job. 47 | for job in [job1, job2, job3, job4]: 48 | # Job status can be "pending" or "done". 49 | assert job.status() in {'pending', 'done'} 50 | 51 | # Let's wait until the jobs are finished. 52 | while job.status() != 'done': 53 | time.sleep(0.1) 54 | 55 | # Retrieve the results of successful jobs. 56 | metadata = job1.result() 57 | assert metadata['_id'] == 'students/Neal' 58 | 59 | metadata = job2.result() 60 | assert metadata['_id'] == 'students/Lily' 61 | 62 | cursor = job3.result() 63 | assert cursor.next() == 100000 64 | 65 | # If a job fails, the exception is propagated up during result retrieval. 66 | try: 67 | result = job4.result() 68 | except AQLQueryExecuteError as err: 69 | assert err.http_code == 400 70 | assert err.error_code == 1501 71 | assert 'syntax error' in err.message 72 | 73 | # Cancel a job. Only pending jobs still in queue may be cancelled. 74 | # Since job3 is done, there is nothing to cancel and an exception is raised. 75 | try: 76 | job3.cancel() 77 | except AsyncJobCancelError as err: 78 | assert err.message.endswith(f'job {job3.id} not found') 79 | 80 | # Clear the result of a job from ArangoDB server to free up resources. 81 | # Result of job4 was removed from the server automatically upon retrieval, 82 | # so attempt to clear it raises an exception. 83 | try: 84 | job4.clear() 85 | except AsyncJobClearError as err: 86 | assert err.message.endswith(f'job {job4.id} not found') 87 | 88 | # List the IDs of the first 100 async jobs completed. 89 | db.async_jobs(status='done', count=100) 90 | 91 | # List the IDs of the first 100 async jobs still pending. 92 | db.async_jobs(status='pending', count=100) 93 | 94 | # Clear all async jobs still sitting on the server. 95 | db.clear_async_jobs() 96 | 97 | .. note:: 98 | Be mindful of server-side memory capacity when issuing a large number of 99 | async requests in small time interval. 100 | 101 | See :ref:`AsyncDatabase` and :ref:`AsyncJob` for API specification. 102 | -------------------------------------------------------------------------------- /docs/auth.rst: -------------------------------------------------------------------------------- 1 | Authentication 2 | -------------- 3 | 4 | Python-arango supports two HTTP authentication methods: basic and JSON Web 5 | Tokens (JWT). 6 | 7 | Basic Authentication 8 | ==================== 9 | 10 | This is python-arango's default authentication method. 11 | 12 | **Example:** 13 | 14 | .. testcode:: 15 | from arango import ArangoClient 16 | 17 | # Initialize the ArangoDB client. 18 | client = ArangoClient() 19 | 20 | # Connect to "test" database as root user using basic auth. 21 | db = client.db('test', username='root', password='passwd') 22 | 23 | # The authentication method can be given explicitly. 24 | db = client.db( 25 | 'test', 26 | username='root', 27 | password='passwd', 28 | auth_method='basic' 29 | ) 30 | 31 | JSON Web Tokens (JWT) 32 | ===================== 33 | 34 | Python-arango automatically obtains JSON web tokens from the server using 35 | username and password. It also refreshes expired tokens and retries requests. 36 | The client and server clocks must be synchronized for the automatic refresh 37 | to work correctly. 38 | 39 | **Example:** 40 | 41 | .. testcode:: 42 | from arango import ArangoClient 43 | 44 | # Initialize the ArangoDB client. 45 | client = ArangoClient() 46 | 47 | # Connect to "test" database as root user using JWT. 48 | db = client.db( 49 | 'test', 50 | username='root', 51 | password='passwd', 52 | auth_method='jwt' 53 | ) 54 | 55 | # Manually refresh the token. 56 | db.conn.refresh_token() 57 | 58 | # Override the token expiry compare leeway in seconds (default: 0) to 59 | # compensate for out-of-sync clocks between the client and server. 60 | db.conn.ext_leeway = 2 61 | 62 | User generated JWT token can be used for user and superuser access. 63 | 64 | **Example:** 65 | 66 | .. code-block:: python 67 | 68 | from calendar import timegm 69 | from datetime import datetime 70 | 71 | import jwt 72 | 73 | from arango import ArangoClient 74 | 75 | # Initialize the ArangoDB client. 76 | client = ArangoClient() 77 | 78 | # Generate the JWT token manually. 79 | now = timegm(datetime.utcnow().utctimetuple()) 80 | token = jwt.encode( 81 | payload={ 82 | 'iat': now, 83 | 'exp': now + 3600, 84 | 'iss': 'arangodb', 85 | 'server_id': 'client' 86 | }, 87 | key='secret', 88 | ).decode('utf-8') 89 | 90 | # Connect to "test" database as superuser using the token. 91 | db = client.db('test', superuser_token=token) 92 | 93 | # Connect to "test" database as user using the token. 94 | db = client.db('test', user_token=token) 95 | 96 | User and superuser tokens can be set on the connection object as well. 97 | 98 | **Example:** 99 | 100 | .. code-block:: python 101 | 102 | from arango import ArangoClient 103 | 104 | # Initialize the ArangoDB client. 105 | client = ArangoClient() 106 | 107 | # Connect to "test" database as superuser using the token. 108 | db = client.db('test', user_token='token') 109 | 110 | # Set the user token on the connection object. 111 | db.conn.set_token('new token') 112 | 113 | # Connect to "test" database as superuser using the token. 114 | db = client.db('test', superuser_token='superuser token') 115 | 116 | # Set the user token on the connection object. 117 | db.conn.set_token('new superuser token') 118 | -------------------------------------------------------------------------------- /docs/backup.rst: -------------------------------------------------------------------------------- 1 | Backups 2 | ------- 3 | 4 | Backups are near consistent snapshots of an entire ArangoDB service including 5 | databases, collections, indexes, views and users. For more information, refer 6 | to `ArangoDB manual`_. 7 | 8 | .. _ArangoDB manual: https://docs.arangodb.com 9 | 10 | **Example:** 11 | 12 | .. code-block:: python 13 | 14 | from arango import ArangoClient 15 | 16 | # Initialize the ArangoDB client. 17 | client = ArangoClient() 18 | 19 | # Connect to "_system" database as root user. 20 | sys_db = client.db( 21 | '_system', 22 | username='root', 23 | password='passwd', 24 | auth_method='jwt' 25 | ) 26 | 27 | # Get the backup API wrapper. 28 | backup = sys_db.backup 29 | 30 | # Create a backup. 31 | result = backup.create( 32 | label='foo', 33 | allow_inconsistent=True, 34 | force=False, 35 | timeout=1000 36 | ) 37 | backup_id = result['backup_id'] 38 | 39 | # Retrieve details on all backups 40 | backup.get() 41 | 42 | # Retrieve details on a specific backup. 43 | backup.get(backup_id=backup_id) 44 | 45 | # Upload a backup to a remote repository. 46 | result = backup.upload( 47 | backup_id=backup_id, 48 | repository='local://tmp/backups', 49 | config={'local': {'type': 'local'}} 50 | ) 51 | upload_id = result['upload_id'] 52 | 53 | # Get status of an upload. 54 | backup.upload(upload_id=upload_id) 55 | 56 | # Abort an upload. 57 | backup.upload(upload_id=upload_id, abort=True) 58 | 59 | # Download a backup from a remote repository. 60 | result = backup.download( 61 | backup_id=backup_id, 62 | repository='local://tmp/backups', 63 | config={'local': {'type': 'local'}} 64 | ) 65 | download_id = result['download_id'] 66 | 67 | # Get status of an download. 68 | backup.download(download_id=download_id) 69 | 70 | # Abort an download. 71 | backup.download(download_id=download_id, abort=True) 72 | 73 | # Restore from a backup. 74 | backup.restore(backup_id) 75 | 76 | # Delete a backup. 77 | backup.delete(backup_id) 78 | 79 | See :ref:`Backup` for API specification. 80 | -------------------------------------------------------------------------------- /docs/batch.rst: -------------------------------------------------------------------------------- 1 | Batch API Execution 2 | ------------------- 3 | .. warning:: 4 | 5 | The batch request API is deprecated since ArangoDB 3.8.0. 6 | We discourage its use, as it will be removed in a future release. 7 | It is already slow and seems to regularly create weird errors when 8 | used with recent versions of ArangoDB. 9 | 10 | The driver functionality has been refactored to no longer use the batch API, 11 | but a `ThreadPoolExecutor` instead. For backwards compatibility, 12 | `max_workers` is set to 1 by default, but can be increased to speed up 13 | batch operations. Essentially, the batch API can now be used to send 14 | multiple requests in parallel, but not to send multiple requests in a 15 | single HTTP call. Note that sending multiple requests in parallel may 16 | cause conflicts on the servers side (for example, requests that modify the same document). 17 | 18 | To send multiple documents at once to an ArangoDB instance, 19 | please use any of :class:`arango.collection.Collection` methods 20 | that accept a list of documents as input, such as: 21 | 22 | * :func:`~arango.collection.Collection.insert_many` 23 | * :func:`~arango.collection.Collection.update_many` 24 | * :func:`~arango.collection.Collection.replace_many` 25 | * :func:`~arango.collection.Collection.delete_many` 26 | 27 | After the commit, results can be retrieved later from :ref:`BatchJob` objects. 28 | 29 | **Example:** 30 | 31 | .. code-block:: python 32 | 33 | from arango import ArangoClient, AQLQueryExecuteError 34 | 35 | # Initialize the ArangoDB client. 36 | client = ArangoClient() 37 | 38 | # Connect to "test" database as root user. 39 | db = client.db('test', username='root', password='passwd') 40 | 41 | # Get the API wrapper for "students" collection. 42 | students = db.collection('students') 43 | 44 | # Begin batch execution via context manager. This returns an instance of 45 | # BatchDatabase, a database-level API wrapper tailored specifically for 46 | # batch execution. The batch is automatically committed when exiting the 47 | # context. The BatchDatabase wrapper cannot be reused after commit. 48 | with db.begin_batch_execution(return_result=True) as batch_db: 49 | 50 | # Child wrappers are also tailored for batch execution. 51 | batch_aql = batch_db.aql 52 | batch_col = batch_db.collection('students') 53 | 54 | # API execution context is always set to "batch". 55 | assert batch_db.context == 'batch' 56 | assert batch_aql.context == 'batch' 57 | assert batch_col.context == 'batch' 58 | 59 | # BatchJob objects are returned instead of results. 60 | job1 = batch_col.insert({'_key': 'Kris'}) 61 | job2 = batch_col.insert({'_key': 'Rita'}) 62 | job3 = batch_aql.execute('RETURN 100000') 63 | job4 = batch_aql.execute('INVALID QUERY') # Fails due to syntax error. 64 | 65 | # Upon exiting context, batch is automatically committed. 66 | assert 'Kris' in students 67 | assert 'Rita' in students 68 | 69 | # Retrieve the status of each batch job. 70 | for job in batch_db.queued_jobs(): 71 | # Status is set to either "pending" (transaction is not committed yet 72 | # and result is not available) or "done" (transaction is committed and 73 | # result is available). 74 | assert job.status() in {'pending', 'done'} 75 | 76 | # Retrieve the results of successful jobs. 77 | metadata = job1.result() 78 | assert metadata['_id'] == 'students/Kris' 79 | 80 | metadata = job2.result() 81 | assert metadata['_id'] == 'students/Rita' 82 | 83 | cursor = job3.result() 84 | assert cursor.next() == 100000 85 | 86 | # If a job fails, the exception is propagated up during result retrieval. 87 | try: 88 | result = job4.result() 89 | except AQLQueryExecuteError as err: 90 | assert err.http_code == 400 91 | assert err.error_code == 1501 92 | assert 'syntax error' in err.message 93 | 94 | # Batch execution can be initiated without using a context manager. 95 | # If return_result parameter is set to False, no jobs are returned. 96 | batch_db = db.begin_batch_execution(return_result=False) 97 | batch_db.collection('students').insert({'_key': 'Jake'}) 98 | batch_db.collection('students').insert({'_key': 'Jill'}) 99 | 100 | # The commit must be called explicitly. 101 | batch_db.commit() 102 | assert 'Jake' in students 103 | assert 'Jill' in students 104 | 105 | .. note:: 106 | * Be mindful of client-side memory capacity when issuing a large number of 107 | requests in single batch execution. 108 | * :ref:`BatchDatabase` and :ref:`BatchJob` instances are stateful objects, 109 | and should not be shared across multiple threads. 110 | * :ref:`BatchDatabase` instance cannot be reused after commit. 111 | 112 | See :ref:`BatchDatabase` and :ref:`BatchJob` for API specification. 113 | -------------------------------------------------------------------------------- /docs/certificates.rst: -------------------------------------------------------------------------------- 1 | TLS certificate verification 2 | ---------------------------- 3 | 4 | When connecting against a server using an https/TLS connection, TLS certificates 5 | are verified by default. 6 | By default, self-signed certificates will cause trouble when connecting. 7 | 8 | .. code-block:: python 9 | 10 | client = ArangoClient(hosts="https://localhost:8529") 11 | 12 | To make connections work even when using self-signed certificates, you can 13 | provide the certificate CA bundle or turn the verification off. 14 | 15 | If you want to have fine-grained control over the HTTP connection, you should define 16 | your HTTP client as described in the :ref:`HTTPClients` section. 17 | 18 | The ``ArangoClient`` class provides an option to override the verification behavior, 19 | no matter what has been defined in the underlying HTTP session. 20 | You can use this option to disable verification. 21 | 22 | .. code-block:: python 23 | 24 | client = ArangoClient(hosts="https://localhost:8529", verify_override=False) 25 | 26 | This will allow connecting, but the underlying `urllib3` library may still issue 27 | warnings due to the insecurity of using self-signed certificates. 28 | 29 | To turn off these warnings as well, you can add the following code to your client 30 | application: 31 | 32 | .. code-block:: python 33 | 34 | import requests 35 | requests.packages.urllib3.disable_warnings() 36 | 37 | You can also provide a custom CA bundle without defining a custom HTTP Client: 38 | 39 | .. code-block:: python 40 | 41 | client = ArangoClient(hosts="https://localhost:8529", verify_override="path/to/certfile") 42 | 43 | If `verify_override` is set to a path to a directory, the directory must have been processed using the `c_rehash` utility 44 | supplied with OpenSSL. For more information, see the `requests documentation `_. 45 | 46 | Setting `verify_override` to `True` will use the system's default CA bundle. 47 | 48 | .. code-block:: python 49 | 50 | client = ArangoClient(hosts="https://localhost:8529", verify_override=True) 51 | -------------------------------------------------------------------------------- /docs/cluster.rst: -------------------------------------------------------------------------------- 1 | Clusters 2 | -------- 3 | 4 | Python-arango provides APIs for working with ArangoDB clusters. For more 5 | information on the design and architecture, refer to `ArangoDB manual`_. 6 | 7 | .. _ArangoDB manual: https://docs.arangodb.com 8 | 9 | Coordinators 10 | ============ 11 | 12 | To connect to multiple ArangoDB coordinators, you must provide either a list of 13 | host strings or a comma-separated string during client initialization. 14 | 15 | **Example:** 16 | 17 | .. testcode:: 18 | 19 | from arango import ArangoClient 20 | 21 | # Single host 22 | client = ArangoClient(hosts='http://localhost:8529') 23 | 24 | # Multiple hosts (option 1: list) 25 | client = ArangoClient(hosts=['http://host1:8529', 'http://host2:8529']) 26 | 27 | # Multiple hosts (option 2: comma-separated string) 28 | client = ArangoClient(hosts='http://host1:8529,http://host2:8529') 29 | 30 | By default, a `requests.Session`_ instance is created per coordinator. HTTP 31 | requests to a host are sent using only its corresponding session. For more 32 | information on how to override this behaviour, see :doc:`http`. 33 | 34 | .. _requests.Session: http://docs.python-requests.org/en/master/user/advanced/#session-objects 35 | 36 | Load-Balancing Strategies 37 | ========================= 38 | 39 | There are two load-balancing strategies available: "roundrobin" and "random" 40 | (defaults to "roundrobin" if unspecified). 41 | 42 | **Example:** 43 | 44 | .. testcode:: 45 | 46 | from arango import ArangoClient 47 | 48 | hosts = ['http://host1:8529', 'http://host2:8529'] 49 | 50 | # Round-robin 51 | client = ArangoClient(hosts=hosts, host_resolver='roundrobin') 52 | 53 | # Random 54 | client = ArangoClient(hosts=hosts, host_resolver='random') 55 | 56 | Administration 57 | ============== 58 | 59 | Below is an example on how to manage clusters using python-arango. 60 | 61 | .. code-block:: python 62 | 63 | from arango import ArangoClient 64 | 65 | # Initialize the ArangoDB client. 66 | client = ArangoClient() 67 | 68 | # Connect to "_system" database as root user. 69 | sys_db = client.db('_system', username='root', password='passwd') 70 | 71 | # Get the Cluster API wrapper. 72 | cluster = sys_db.cluster 73 | 74 | # Get this server's ID. 75 | cluster.server_id() 76 | 77 | # Get this server's role. 78 | cluster.server_role() 79 | 80 | # Get the cluster health. 81 | cluster.health() 82 | 83 | # Get cluster server details. 84 | cluster.server_count() 85 | server_id = cluster.server_id() 86 | cluster.server_engine(server_id) 87 | cluster.server_version(server_id) 88 | cluster.server_statistics(server_id) 89 | cluster.server_maintenance_mode(server_id) 90 | 91 | # Toggle Server maintenance mode (allowed values are "normal" and "maintenance"). 92 | cluster.toggle_server_maintenance_mode(server_id, 'normal') 93 | cluster.toggle_server_maintenance_mode(server_id, 'maintenance', timeout=30) 94 | 95 | # Toggle Cluster maintenance mode (allowed values are "on" and "off"). 96 | cluster.toggle_maintenance_mode('on') 97 | cluster.toggle_maintenance_mode('off') 98 | 99 | # Rebalance the distribution of shards. Available with ArangoDB 3.10+. 100 | cluster.rebalance() 101 | 102 | See :ref:`ArangoClient` and :ref:`Cluster` for API specification. 103 | -------------------------------------------------------------------------------- /docs/collection.rst: -------------------------------------------------------------------------------- 1 | Collections 2 | ----------- 3 | 4 | A **collection** contains :doc:`documents `. It is uniquely identified 5 | by its name which must consist only of hyphen, underscore and alphanumeric 6 | characters. There are three types of collections in python-arango: 7 | 8 | * **Standard Collection:** contains regular documents. 9 | * **Vertex Collection:** contains vertex documents for graphs. See 10 | :ref:`here ` for more details. 11 | * **Edge Collection:** contains edge documents for graphs. See 12 | :ref:`here ` for more details. 13 | 14 | Here is an example showing how you can manage standard collections: 15 | 16 | .. testcode:: 17 | 18 | from arango import ArangoClient 19 | 20 | # Initialize the ArangoDB client. 21 | client = ArangoClient() 22 | 23 | # Connect to "test" database as root user. 24 | db = client.db('test', username='root', password='passwd') 25 | 26 | # List all collections in the database. 27 | db.collections() 28 | 29 | # Create a new collection named "students" if it does not exist. 30 | # This returns an API wrapper for "students" collection. 31 | if db.has_collection('students'): 32 | students = db.collection('students') 33 | else: 34 | students = db.create_collection('students') 35 | 36 | # Retrieve collection properties. 37 | students.name 38 | students.db_name 39 | students.properties() 40 | students.revision() 41 | students.statistics() 42 | students.checksum() 43 | students.count() 44 | 45 | # Perform various operations. 46 | students.load() 47 | students.unload() 48 | students.truncate() 49 | students.configure() 50 | 51 | # Delete the collection. 52 | db.delete_collection('students') 53 | 54 | See :ref:`StandardDatabase` and :ref:`StandardCollection` for API specification. 55 | -------------------------------------------------------------------------------- /docs/compression.rst: -------------------------------------------------------------------------------- 1 | Compression 2 | ------------ 3 | 4 | The :ref:`ArangoClient` lets you define the preferred compression policy for request and responses. By default 5 | compression is disabled. You can change this by setting the `request_compression` and `response_compression` parameters 6 | when creating the client. Currently, only the "deflate" compression algorithm is supported. 7 | 8 | .. testcode:: 9 | 10 | from arango import ArangoClient 11 | 12 | from arango.http import DeflateRequestCompression 13 | 14 | client = ArangoClient( 15 | hosts='http://localhost:8529', 16 | request_compression=DeflateRequestCompression(), 17 | response_compression="deflate" 18 | ) 19 | 20 | Furthermore, you can customize the request compression policy by defining the minimum size of the request body that 21 | should be compressed and the desired compression level. For example, the following code sets the minimum size to 2 KB 22 | and the compression level to 8: 23 | 24 | .. code-block:: python 25 | 26 | client = ArangoClient( 27 | hosts='http://localhost:8529', 28 | request_compression=DeflateRequestCompression( 29 | threshold=2048, 30 | level=8), 31 | ) 32 | 33 | If you want to implement your own compression policy, you can do so by implementing the 34 | :class:`arango.http.RequestCompression` interface. 35 | 36 | .. note:: 37 | The `response_compression` parameter is only used to inform the server that the client prefers compressed responses 38 | (in the form of an *Accept-Encoding* header). Note that the server may or may not honor this preference, depending 39 | on how it is configured. This can be controlled by setting the `--http.compress-response-threshold` option to 40 | a value greater than 0 when starting the ArangoDB server. 41 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath("..")) 5 | 6 | project = "python-arango" 7 | copyright = "2016-2025, Joohwan Oh" 8 | author = "Joohwan Oh" 9 | extensions = [ 10 | "sphinx_rtd_theme", 11 | "sphinx.ext.autodoc", 12 | "sphinx.ext.doctest", 13 | "sphinx.ext.viewcode", 14 | ] 15 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 16 | html_static_path = ["static"] 17 | html_theme = "sphinx_rtd_theme" 18 | master_doc = "index" 19 | 20 | # Set canonical URL from the Read the Docs Domain 21 | html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "docs.python-arango.com") 22 | 23 | autodoc_member_order = "bysource" 24 | 25 | doctest_global_setup = """ 26 | from arango import ArangoClient 27 | # Initialize the ArangoDB client. 28 | client = ArangoClient() 29 | # Connect to "_system" database as root user. 30 | sys_db = client.db('_system', username='root', password='passwd') 31 | # Create "test" database if it does not exist. 32 | if not sys_db.has_database('test'): 33 | sys_db.create_database('test') 34 | # Ensure that user "johndoe@gmail.com" does not exist. 35 | if sys_db.has_user('johndoe@gmail.com'): 36 | sys_db.delete_user('johndoe@gmail.com') 37 | # Connect to "test" database as root user. 38 | db = client.db('test', username='root', password='passwd') 39 | # Create "students" collection if it does not exist. 40 | if db.has_collection('students'): 41 | db.collection('students').truncate() 42 | else: 43 | db.create_collection('students') 44 | # Ensure that "cities" collection does not exist. 45 | if db.has_collection('cities'): 46 | db.delete_collection('cities') 47 | # Create "school" graph if it does not exist. 48 | if db.has_graph("school"): 49 | school = db.graph('school') 50 | else: 51 | school = db.create_graph('school') 52 | # Create "teachers" vertex collection if it does not exist. 53 | if school.has_vertex_collection('teachers'): 54 | school.vertex_collection('teachers').truncate() 55 | else: 56 | school.create_vertex_collection('teachers') 57 | # Create "lectures" vertex collection if it does not exist. 58 | if school.has_vertex_collection('lectures'): 59 | school.vertex_collection('lectures').truncate() 60 | else: 61 | school.create_vertex_collection('lectures') 62 | # Create "teach" edge definition if it does not exist. 63 | if school.has_edge_definition('teach'): 64 | school.edge_collection('teach').truncate() 65 | else: 66 | school.create_edge_definition( 67 | edge_collection='teach', 68 | from_vertex_collections=['teachers'], 69 | to_vertex_collections=['lectures'] 70 | ) 71 | """ 72 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ------------ 3 | 4 | Requirements 5 | ============ 6 | 7 | Before submitting a pull request on GitHub_, please make sure you meet the 8 | following requirements: 9 | 10 | * The pull request points to main_ branch. 11 | * Changes are squashed into a single commit. I like to use git rebase for this. 12 | * Commit message is in present tense. For example, "Fix bug" is good while 13 | "Fixed bug" is not. 14 | * Sphinx_-compatible docstrings. 15 | * PEP8_ compliance. 16 | * No missing docstrings or commented-out lines. 17 | * Test coverage remains at %100. If a piece of code is trivial and does not 18 | need unit tests, use this_ to exclude it from coverage. 19 | * No build failures. Builds automatically trigger on pull 20 | request submissions. 21 | * Documentation is kept up-to-date with the new changes (see below). 22 | 23 | .. warning:: 24 | The dev branch is occasionally rebased, and its commit history may be 25 | overwritten in the process. Before you begin your feature work, git fetch 26 | or pull to ensure that your local branch has not diverged. If you see git 27 | conflicts and want to start with a clean slate, run the following commands: 28 | 29 | .. code-block:: bash 30 | 31 | ~$ git checkout dev 32 | ~$ git fetch origin 33 | ~$ git reset --hard origin/dev # THIS WILL WIPE ALL LOCAL CHANGES 34 | 35 | Style 36 | ===== 37 | 38 | To ensure PEP8_ compliance, run flake8_: 39 | 40 | .. code-block:: bash 41 | 42 | ~$ pip install flake8 43 | ~$ git clone https://github.com/arangodb/python-arango.git 44 | ~$ cd python-arango 45 | ~$ flake8 46 | 47 | If there is a good reason to ignore a warning, see here_ on how to exclude it. 48 | 49 | Testing 50 | ======= 51 | 52 | To test your changes, you can run the integration test suite that comes with 53 | **python-arango**. It uses pytest_ and requires an actual ArangoDB instance. 54 | 55 | To run the test suite (use your own host, port and root password): 56 | 57 | .. code-block:: bash 58 | 59 | ~$ pip install pytest 60 | ~$ git clone https://github.com/arangodb/python-arango.git 61 | ~$ cd python-arango 62 | ~$ py.test --complete --host=127.0.0.1 --port=8529 --passwd=passwd 63 | 64 | To run the test suite with coverage report: 65 | 66 | .. code-block:: bash 67 | 68 | ~$ pip install coverage pytest pytest-cov 69 | ~$ git clone https://github.com/arangodb/python-arango.git 70 | ~$ cd python-arango 71 | ~$ py.test --complete --host=127.0.0.1 --port=8529 --passwd=passwd --cov=kq 72 | 73 | As the test suite creates real databases and jobs, it should only be run in 74 | development environments. 75 | 76 | Documentation 77 | ============= 78 | 79 | The documentation including the README is written in reStructuredText_ and uses 80 | Sphinx_. To build an HTML version on your local machine: 81 | 82 | .. code-block:: bash 83 | 84 | ~$ pip install sphinx sphinx_rtd_theme 85 | ~$ git clone https://github.com/arangodb/python-arango.git 86 | ~$ cd python-arango 87 | ~$ python -m sphinx -b html -W docs docs/_build/ # Open build/index.html in a browser 88 | 89 | As always, thank you for your contribution! 90 | 91 | .. _main: https://github.com/arangodb/python-arango/tree/main 92 | .. _GitHub: https://github.com/arangodb/python-arango 93 | .. _PEP8: https://www.python.org/dev/peps/pep-0008/ 94 | .. _this: http://coverage.readthedocs.io/en/latest/excluding.html 95 | .. _Sphinx: https://github.com/sphinx-doc/sphinx 96 | .. _flake8: http://flake8.pycqa.org 97 | .. _here: http://flake8.pycqa.org/en/latest/user/violations.html#in-line-ignoring-errors 98 | .. _pytest: https://github.com/pytest-dev/pytest 99 | .. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText 100 | -------------------------------------------------------------------------------- /docs/cursor.rst: -------------------------------------------------------------------------------- 1 | Cursors 2 | ------- 3 | 4 | Many operations provided by python-arango (e.g. executing :doc:`aql` queries) 5 | return result **cursors** to batch the network communication between ArangoDB 6 | server and python-arango client. Each HTTP request from a cursor fetches the 7 | next batch of results (usually documents). Depending on the query, the total 8 | number of items in the result set may or may not be known in advance. 9 | 10 | **Example:** 11 | 12 | .. testcode:: 13 | 14 | from arango import ArangoClient 15 | 16 | # Initialize the ArangoDB client. 17 | client = ArangoClient() 18 | 19 | # Connect to "test" database as root user. 20 | db = client.db('test', username='root', password='passwd') 21 | 22 | # Set up some test data to query against. 23 | db.collection('students').insert_many([ 24 | {'_key': 'Abby', 'age': 22}, 25 | {'_key': 'John', 'age': 18}, 26 | {'_key': 'Mary', 'age': 21}, 27 | {'_key': 'Suzy', 'age': 23}, 28 | {'_key': 'Dave', 'age': 20} 29 | ]) 30 | 31 | # Execute an AQL query which returns a cursor object. 32 | cursor = db.aql.execute( 33 | 'FOR doc IN students FILTER doc.age > @val RETURN doc', 34 | bind_vars={'val': 17}, 35 | batch_size=2, 36 | count=True 37 | ) 38 | 39 | # Get the cursor ID. 40 | cursor.id 41 | 42 | # Get the items in the current batch. 43 | cursor.batch() 44 | 45 | # Check if the current batch is empty. 46 | cursor.empty() 47 | 48 | # Get the total count of the result set. 49 | cursor.count() 50 | 51 | # Flag indicating if there are more to be fetched from server. 52 | cursor.has_more() 53 | 54 | # Flag indicating if the results are cached. 55 | cursor.cached() 56 | 57 | # Get the cursor statistics. 58 | cursor.statistics() 59 | 60 | # Get the performance profile. 61 | cursor.profile() 62 | 63 | # Get any warnings produced from the query. 64 | cursor.warnings() 65 | 66 | # Return the next item from the cursor. If current batch is depleted, the 67 | # next batch is fetched from the server automatically. 68 | cursor.next() 69 | 70 | # Return the next item from the cursor. If current batch is depleted, an 71 | # exception is thrown. You need to fetch the next batch manually. 72 | cursor.pop() 73 | 74 | # Fetch the next batch and add them to the cursor object. 75 | cursor.fetch() 76 | 77 | # Delete the cursor from the server. 78 | cursor.close() 79 | 80 | See :ref:`Cursor` for API specification. 81 | 82 | If the fetched result batch is depleted while you are iterating over a cursor 83 | (or while calling the method :func:`arango.cursor.Cursor.next`), python-arango 84 | automatically sends an HTTP request to the server to fetch the next batch 85 | (just-in-time style). To control exactly when the fetches occur, you can use 86 | methods :func:`arango.cursor.Cursor.fetch` and :func:`arango.cursor.Cursor.pop` 87 | instead. 88 | 89 | **Example:** 90 | 91 | .. testcode:: 92 | 93 | from arango import ArangoClient 94 | 95 | # Initialize the ArangoDB client. 96 | client = ArangoClient() 97 | 98 | # Connect to "test" database as root user. 99 | db = client.db('test', username='root', password='passwd') 100 | 101 | # Set up some test data to query against. 102 | db.collection('students').insert_many([ 103 | {'_key': 'Abby', 'age': 22}, 104 | {'_key': 'John', 'age': 18}, 105 | {'_key': 'Mary', 'age': 21} 106 | ]) 107 | 108 | # If you iterate over the cursor or call cursor.next(), batches are 109 | # fetched automatically from the server just-in-time style. 110 | cursor = db.aql.execute('FOR doc IN students RETURN doc', batch_size=1) 111 | result = [doc for doc in cursor] 112 | 113 | # Alternatively, you can manually fetch and pop for finer control. 114 | cursor = db.aql.execute('FOR doc IN students RETURN doc', batch_size=1) 115 | while cursor.has_more(): # Fetch until nothing is left on the server. 116 | cursor.fetch() 117 | while not cursor.empty(): # Pop until nothing is left on the cursor. 118 | cursor.pop() 119 | 120 | With ArangoDB 3.11.0 or higher, you can also use the `allow_retry` 121 | parameter of :func:`arango.aql.AQL.execute` to automatically retry 122 | the request if the cursor encountered any issues during the previous 123 | fetch operation. Note that this feature causes the server to cache the 124 | last batch. To allow re-fetching of the very last batch of the query, 125 | the server cannot automatically delete the cursor. Once you have successfully 126 | received the last batch, you should call :func:`arango.cursor.Cursor.close`. 127 | 128 | **Example:** 129 | 130 | .. code-block:: python 131 | 132 | from arango import ArangoClient 133 | 134 | # Initialize the ArangoDB client. 135 | client = ArangoClient() 136 | 137 | # Connect to "test" database as root user. 138 | db = client.db('test', username='root', password='passwd') 139 | 140 | # Set up some test data to query against. 141 | db.collection('students').insert_many([ 142 | {'_key': 'Abby', 'age': 22}, 143 | {'_key': 'John', 'age': 18}, 144 | {'_key': 'Mary', 'age': 21}, 145 | {'_key': 'Suzy', 'age': 23}, 146 | {'_key': 'Dave', 'age': 20} 147 | ]) 148 | 149 | # Execute an AQL query which returns a cursor object. 150 | cursor = db.aql.execute( 151 | 'FOR doc IN students FILTER doc.age > @val RETURN doc', 152 | bind_vars={'val': 17}, 153 | batch_size=2, 154 | count=True, 155 | allow_retry=True 156 | ) 157 | 158 | while cursor.has_more(): 159 | try: 160 | cursor.fetch() 161 | except ConnectionError: 162 | # Retry the request. 163 | continue 164 | 165 | while not cursor.empty(): 166 | cursor.pop() 167 | 168 | # Delete the cursor from the server. 169 | cursor.close() 170 | -------------------------------------------------------------------------------- /docs/database.rst: -------------------------------------------------------------------------------- 1 | Databases 2 | --------- 3 | 4 | ArangoDB server can have an arbitrary number of **databases**. Each database 5 | has its own set of :doc:`collections ` and :doc:`graphs `. 6 | There is a special database named ``_system``, which cannot be dropped and 7 | provides operations for managing users, permissions and other databases. Most 8 | of the operations can only be executed by admin users. See :doc:`user` for more 9 | information. 10 | 11 | **Example:** 12 | 13 | .. testcode:: 14 | 15 | from arango import ArangoClient 16 | 17 | # Initialize the ArangoDB client. 18 | client = ArangoClient() 19 | 20 | # Connect to "_system" database as root user. 21 | # This returns an API wrapper for "_system" database. 22 | sys_db = client.db('_system', username='root', password='passwd') 23 | 24 | # List all databases. 25 | sys_db.databases() 26 | 27 | # Create a new database named "test" if it does not exist. 28 | # Only root user has access to it at time of its creation. 29 | if not sys_db.has_database('test'): 30 | sys_db.create_database('test') 31 | 32 | # Delete the database. 33 | sys_db.delete_database('test') 34 | 35 | # Create a new database named "test" along with a new set of users. 36 | # Only "jane", "john", "jake" and root user have access to it. 37 | if not sys_db.has_database('test'): 38 | sys_db.create_database( 39 | name='test', 40 | users=[ 41 | {'username': 'jane', 'password': 'foo', 'active': True}, 42 | {'username': 'john', 'password': 'bar', 'active': True}, 43 | {'username': 'jake', 'password': 'baz', 'active': True}, 44 | ], 45 | ) 46 | 47 | # Connect to the new "test" database as user "jane". 48 | db = client.db('test', username='jane', password='foo') 49 | 50 | # Make sure that user "jane" has read and write permissions. 51 | sys_db.update_permission(username='jane', permission='rw', database='test') 52 | 53 | # Retrieve various database and server information. 54 | db.name 55 | db.username 56 | db.version() 57 | db.status() 58 | db.details() 59 | db.collections() 60 | db.graphs() 61 | db.engine() 62 | 63 | # Delete the database. Note that the new users will remain. 64 | sys_db.delete_database('test') 65 | 66 | See :ref:`ArangoClient` and :ref:`StandardDatabase` for API specification. 67 | -------------------------------------------------------------------------------- /docs/errno.rst: -------------------------------------------------------------------------------- 1 | Error Codes 2 | ----------- 3 | 4 | Python-arango provides ArangoDB error code constants for convenience. 5 | 6 | **Example** 7 | 8 | .. testcode:: 9 | 10 | from arango import errno 11 | 12 | # Some examples 13 | assert errno.NOT_IMPLEMENTED == 9 14 | assert errno.DOCUMENT_REV_BAD == 1239 15 | assert errno.DOCUMENT_NOT_FOUND == 1202 16 | 17 | For more information, refer to `ArangoDB manual`_. 18 | 19 | .. _ArangoDB manual: https://www.arangodb.com/docs/stable/appendix-error-codes.html 20 | -------------------------------------------------------------------------------- /docs/errors.rst: -------------------------------------------------------------------------------- 1 | Error Handling 2 | -------------- 3 | 4 | All python-arango exceptions inherit :class:`arango.exceptions.ArangoError`, 5 | which splits into subclasses :class:`arango.exceptions.ArangoServerError` and 6 | :class:`arango.exceptions.ArangoClientError`. 7 | 8 | Server Errors 9 | ============= 10 | 11 | :class:`arango.exceptions.ArangoServerError` exceptions lightly wrap non-2xx 12 | HTTP responses coming from ArangoDB. Each exception object contains the error 13 | message, error code and HTTP request response details. 14 | 15 | **Example:** 16 | 17 | .. testcode:: 18 | 19 | from arango import ArangoClient, ArangoServerError, DocumentInsertError 20 | 21 | # Initialize the ArangoDB client. 22 | client = ArangoClient() 23 | 24 | # Connect to "test" database as root user. 25 | db = client.db('test', username='root', password='passwd') 26 | 27 | # Get the API wrapper for "students" collection. 28 | students = db.collection('students') 29 | 30 | try: 31 | students.insert({'_key': 'John'}) 32 | students.insert({'_key': 'John'}) # duplicate key error 33 | 34 | except DocumentInsertError as exc: 35 | 36 | assert isinstance(exc, ArangoServerError) 37 | assert exc.source == 'server' 38 | 39 | exc.message # Exception message usually from ArangoDB 40 | exc.error_message # Raw error message from ArangoDB 41 | exc.error_code # Error code from ArangoDB 42 | exc.url # URL (API endpoint) 43 | exc.http_method # HTTP method (e.g. "POST") 44 | exc.http_headers # Response headers 45 | exc.http_code # Status code (e.g. 200) 46 | 47 | # You can inspect the ArangoDB response directly. 48 | response = exc.response 49 | response.method # HTTP method (e.g. "POST") 50 | response.headers # Response headers 51 | response.url # Full request URL 52 | response.is_success # Set to True if HTTP code is 2XX 53 | response.body # JSON-deserialized response body 54 | response.raw_body # Raw string response body 55 | response.status_text # Status text (e.g "OK") 56 | response.status_code # Status code (e.g. 200) 57 | response.error_code # Error code from ArangoDB 58 | 59 | # You can also inspect the request sent to ArangoDB. 60 | request = exc.request 61 | request.method # HTTP method (e.g. "post") 62 | request.endpoint # API endpoint starting with "/_api" 63 | request.headers # Request headers 64 | request.params # URL parameters 65 | request.data # Request payload 66 | 67 | See :ref:`Response` and :ref:`Request` for reference. 68 | 69 | Client Errors 70 | ============= 71 | 72 | :class:`arango.exceptions.ArangoClientError` exceptions originate from 73 | python-arango client itself. They do not contain error codes nor HTTP request 74 | response details. 75 | 76 | **Example:** 77 | 78 | .. testcode:: 79 | 80 | from arango import ArangoClient, ArangoClientError, DocumentParseError 81 | 82 | # Initialize the ArangoDB client. 83 | client = ArangoClient() 84 | 85 | # Connect to "test" database as root user. 86 | db = client.db('test', username='root', password='passwd') 87 | 88 | # Get the API wrapper for "students" collection. 89 | students = db.collection('students') 90 | 91 | try: 92 | students.get({'_id': 'invalid_id'}) # malformed document 93 | 94 | except DocumentParseError as exc: 95 | 96 | assert isinstance(exc, ArangoClientError) 97 | assert exc.source == 'client' 98 | 99 | # Only the error message is set. 100 | error_message = exc.message 101 | assert exc.error_code is None 102 | assert exc.error_message is None 103 | assert exc.url is None 104 | assert exc.http_method is None 105 | assert exc.http_code is None 106 | assert exc.http_headers is None 107 | assert exc.response is None 108 | assert exc.request is None 109 | 110 | Exceptions 111 | ========== 112 | 113 | Below are all exceptions from python-arango. 114 | 115 | .. automodule:: arango.exceptions 116 | :members: 117 | 118 | 119 | Error Codes 120 | =========== 121 | 122 | The `errno` module contains a constant mapping to `ArangoDB's error codes 123 | `_. 124 | 125 | .. automodule:: arango.errno 126 | :members: 127 | -------------------------------------------------------------------------------- /docs/foxx.rst: -------------------------------------------------------------------------------- 1 | Foxx 2 | ---- 3 | 4 | Python-arango provides support for **Foxx**, a microservice framework which 5 | lets you define custom HTTP endpoints to extend ArangoDB's REST API. For more 6 | information, refer to `ArangoDB manual`_. 7 | 8 | .. _ArangoDB manual: https://docs.arangodb.com 9 | 10 | **Example:** 11 | 12 | .. testcode:: 13 | 14 | from arango import ArangoClient 15 | 16 | # Initialize the ArangoDB client. 17 | client = ArangoClient() 18 | 19 | # Connect to "_system" database as root user. 20 | db = client.db('_system', username='root', password='passwd') 21 | 22 | # Get the Foxx API wrapper. 23 | foxx = db.foxx 24 | 25 | # Define the test mount point. 26 | service_mount = '/test_mount' 27 | 28 | # List services. 29 | foxx.services() 30 | 31 | # Create a service using source on server. 32 | foxx.create_service( 33 | mount=service_mount, 34 | source='/tests/static/service.zip', 35 | config={}, 36 | dependencies={}, 37 | development=True, 38 | setup=True, 39 | legacy=True 40 | ) 41 | 42 | # Update (upgrade) a service. 43 | service = db.foxx.update_service( 44 | mount=service_mount, 45 | source='/tests/static/service.zip', 46 | config={}, 47 | dependencies={}, 48 | teardown=True, 49 | setup=True, 50 | legacy=False 51 | ) 52 | 53 | # Replace (overwrite) a service. 54 | service = db.foxx.replace_service( 55 | mount=service_mount, 56 | source='/tests/static/service.zip', 57 | config={}, 58 | dependencies={}, 59 | teardown=True, 60 | setup=True, 61 | legacy=True, 62 | force=False 63 | ) 64 | 65 | # Get service details. 66 | foxx.service(service_mount) 67 | 68 | # Manage service configuration. 69 | foxx.config(service_mount) 70 | foxx.update_config(service_mount, config={}) 71 | foxx.replace_config(service_mount, config={}) 72 | 73 | # Manage service dependencies. 74 | foxx.dependencies(service_mount) 75 | foxx.update_dependencies(service_mount, dependencies={}) 76 | foxx.replace_dependencies(service_mount, dependencies={}) 77 | 78 | # Toggle development mode for a service. 79 | foxx.enable_development(service_mount) 80 | foxx.disable_development(service_mount) 81 | 82 | # Other miscellaneous functions. 83 | foxx.readme(service_mount) 84 | foxx.swagger(service_mount) 85 | foxx.download(service_mount) 86 | foxx.commit(service_mount) 87 | foxx.scripts(service_mount) 88 | foxx.run_script(service_mount, 'setup', []) 89 | foxx.run_tests(service_mount, reporter='xunit', output_format='xml') 90 | 91 | # Delete a service. 92 | foxx.delete_service(service_mount) 93 | 94 | You can also manage Foxx services by using zip or Javascript files directly: 95 | 96 | .. code-block:: python 97 | 98 | from arango import ArangoClient 99 | 100 | # Initialize the ArangoDB client. 101 | client = ArangoClient() 102 | 103 | # Connect to "_system" database as root user. 104 | db = client.db('_system', username='root', password='passwd') 105 | 106 | # Get the Foxx API wrapper. 107 | foxx = db.foxx 108 | 109 | # Define the test mount point. 110 | service_mount = '/test_mount' 111 | 112 | # Create a service by providing a file directly. 113 | foxx.create_service_with_file( 114 | mount=service_mount, 115 | filename='/home/user/service.zip', 116 | development=True, 117 | setup=True, 118 | legacy=True 119 | ) 120 | 121 | # Update (upgrade) a service by providing a file directly. 122 | foxx.update_service_with_file( 123 | mount=service_mount, 124 | filename='/home/user/service.zip', 125 | teardown=False, 126 | setup=True, 127 | legacy=True, 128 | force=False 129 | ) 130 | 131 | # Replace a service by providing a file directly. 132 | foxx.replace_service_with_file( 133 | mount=service_mount, 134 | filename='/home/user/service.zip', 135 | teardown=False, 136 | setup=True, 137 | legacy=True, 138 | force=False 139 | ) 140 | 141 | # Delete a service. 142 | foxx.delete_service(service_mount) 143 | 144 | See :ref:`Foxx` for API specification. 145 | -------------------------------------------------------------------------------- /docs/http.rst: -------------------------------------------------------------------------------- 1 | .. _HTTPClients: 2 | 3 | HTTP Clients 4 | ------------ 5 | 6 | Python-arango lets you define your own HTTP client for sending requests to 7 | ArangoDB server. The default implementation uses the requests_ library. 8 | 9 | Your HTTP client must inherit :class:`arango.http.HTTPClient` and implement the 10 | following abstract methods: 11 | 12 | * :func:`arango.http.HTTPClient.create_session` 13 | * :func:`arango.http.HTTPClient.send_request` 14 | 15 | The **create_session** method must return a `requests.Session`_ instance per 16 | connected host (coordinator). The session objects are stored in the client. 17 | 18 | The **send_request** method must use the session to send an HTTP request, and 19 | return a fully populated instance of :class:`arango.response.Response`. 20 | 21 | For example, let's say your HTTP client needs: 22 | 23 | * Automatic retries 24 | * Additional HTTP header called ``x-my-header`` 25 | * SSL certificate verification disabled 26 | * Custom logging 27 | 28 | Your ``CustomHTTPClient`` class might look something like this: 29 | 30 | .. testcode:: 31 | 32 | import logging 33 | 34 | from requests.adapters import HTTPAdapter 35 | from requests import Session 36 | from requests.packages.urllib3.util.retry import Retry 37 | 38 | from arango.response import Response 39 | from arango.http import HTTPClient 40 | 41 | 42 | class CustomHTTPClient(HTTPClient): 43 | """My custom HTTP client with cool features.""" 44 | 45 | def __init__(self): 46 | # Initialize your logger. 47 | self._logger = logging.getLogger('my_logger') 48 | 49 | def create_session(self, host): 50 | session = Session() 51 | 52 | # Add request header. 53 | session.headers.update({'x-my-header': 'true'}) 54 | 55 | # Enable retries. 56 | retry_strategy = Retry( 57 | total=3, 58 | backoff_factor=1, 59 | status_forcelist=[429, 500, 502, 503, 504], 60 | method_whitelist=["HEAD", "GET", "OPTIONS"], 61 | ) 62 | http_adapter = HTTPAdapter(max_retries=retry_strategy) 63 | session.mount('https://', http_adapter) 64 | session.mount('http://', http_adapter) 65 | 66 | return session 67 | 68 | def send_request(self, 69 | session, 70 | method, 71 | url, 72 | params=None, 73 | data=None, 74 | headers=None, 75 | auth=None): 76 | # Add your own debug statement. 77 | self._logger.debug(f'Sending request to {url}') 78 | 79 | # Send a request. 80 | response = session.request( 81 | method=method, 82 | url=url, 83 | params=params, 84 | data=data, 85 | headers=headers, 86 | auth=auth, 87 | verify=False, # Disable SSL verification 88 | timeout=5 # Use timeout of 5 seconds 89 | ) 90 | self._logger.debug(f'Got {response.status_code}') 91 | 92 | # Return an instance of arango.response.Response. 93 | return Response( 94 | method=response.request.method, 95 | url=response.url, 96 | headers=response.headers, 97 | status_code=response.status_code, 98 | status_text=response.reason, 99 | raw_body=response.text, 100 | ) 101 | 102 | Then you would inject your client as follows: 103 | 104 | .. code-block:: python 105 | 106 | from arango import ArangoClient 107 | 108 | from my_module import CustomHTTPClient 109 | 110 | client = ArangoClient( 111 | hosts='http://localhost:8529', 112 | http_client=CustomHTTPClient() 113 | ) 114 | 115 | See `requests.Session`_ for more details on how to create and manage sessions. 116 | 117 | .. _requests: https://github.com/requests/requests 118 | .. _requests.Session: http://docs.python-requests.org/en/master/user/advanced/#session-objects 119 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. image:: /static/logo.png 2 | 3 | | 4 | 5 | Python-Arango 6 | ------------- 7 | 8 | Welcome to the documentation for **python-arango**, a Python driver for ArangoDB_. 9 | 10 | If you're interested in using asyncio, please check python-arango-async_ driver. 11 | 12 | Requirements 13 | ============= 14 | 15 | - ArangoDB version 3.11+ 16 | - Python version 3.9+ 17 | 18 | Installation 19 | ============ 20 | 21 | .. code-block:: bash 22 | 23 | ~$ pip install python-arango --upgrade 24 | 25 | Contents 26 | ======== 27 | 28 | Basics 29 | 30 | .. toctree:: 31 | :maxdepth: 1 32 | 33 | overview 34 | database 35 | collection 36 | indexes 37 | document 38 | graph 39 | simple 40 | aql 41 | 42 | Specialized Features 43 | 44 | .. toctree:: 45 | :maxdepth: 1 46 | 47 | pregel 48 | foxx 49 | replication 50 | transaction 51 | cluster 52 | analyzer 53 | view 54 | wal 55 | 56 | API Executions 57 | 58 | .. toctree:: 59 | :maxdepth: 1 60 | 61 | async 62 | batch 63 | overload 64 | 65 | Administration 66 | 67 | .. toctree:: 68 | :maxdepth: 1 69 | 70 | admin 71 | user 72 | 73 | Miscellaneous 74 | 75 | .. toctree:: 76 | :maxdepth: 1 77 | 78 | task 79 | threading 80 | certificates 81 | errors 82 | logging 83 | auth 84 | http 85 | compression 86 | serializer 87 | schema 88 | cursor 89 | backup 90 | errno 91 | 92 | Development 93 | 94 | .. toctree:: 95 | :maxdepth: 1 96 | 97 | contributing 98 | specs 99 | 100 | .. _ArangoDB: https://www.arangodb.com 101 | .. _python-arango-async: https://python-arango-async.readthedocs.io 102 | -------------------------------------------------------------------------------- /docs/indexes.rst: -------------------------------------------------------------------------------- 1 | Indexes 2 | ------- 3 | 4 | **Indexes** can be added to collections to speed up document lookups. Every 5 | collection has a primary hash index on ``_key`` field by default. This index 6 | cannot be deleted or modified. Every edge collection has additional indexes 7 | on fields ``_from`` and ``_to``. For more information on indexes, refer to 8 | `ArangoDB manual`_. 9 | 10 | .. _ArangoDB manual: https://docs.arangodb.com 11 | 12 | **Example:** 13 | 14 | .. testcode:: 15 | 16 | from arango import ArangoClient 17 | 18 | # Initialize the ArangoDB client. 19 | client = ArangoClient() 20 | 21 | # Connect to "test" database as root user. 22 | db = client.db('test', username='root', password='passwd') 23 | 24 | # Create a new collection named "cities". 25 | cities = db.create_collection('cities') 26 | 27 | # List the indexes in the collection. 28 | cities.indexes() 29 | 30 | # Add a new persistent index on document fields "continent" and "country". 31 | persistent_index = {'type': 'persistent', 'fields': ['continent', 'country'], 'unique': True} 32 | index = cities.add_index(persistent_index) 33 | 34 | # Add new fulltext indexes on fields "continent" and "country". 35 | index = cities.add_index({'type': 'fulltext', 'fields': ['continent']}) 36 | index = cities.add_index({'type': 'fulltext', 'fields': ['country']}) 37 | 38 | # Add a new persistent index on field 'population'. 39 | persistent_index = {'type': 'persistent', 'fields': ['population'], 'sparse': False} 40 | index = cities.add_index(persistent_index) 41 | 42 | # Add a new geo-spatial index on field 'coordinates'. 43 | geo_index = {'type': 'geo', 'fields': ['coordinates']} 44 | index = cities.add_index(geo_index) 45 | 46 | # Add a new persistent index on field 'currency'. 47 | persistent_index = {'type': 'persistent', 'fields': ['currency'], 'sparse': True} 48 | index = cities.add_index(persistent_index) 49 | 50 | # Add a new TTL (time-to-live) index on field 'currency'. 51 | ttl_index = {'type': 'ttl', 'fields': ['currency'], 'expireAfter': 200} 52 | index = cities.add_index(ttl_index) 53 | 54 | # Add MDI (multi-dimensional) index on field 'x' and 'y'. 55 | mdi_index = {'type': 'mdi', 'fields': ['x', 'y'], 'fieldValueTypes': 'double'} 56 | index = cities.add_index(mdi_index) 57 | 58 | # Indexes may be added with a name that can be referred to in AQL queries. 59 | persistent_index = {'type': 'persistent', 'fields': ['country'], 'unique': True, 'name': 'my_hash_index'} 60 | index = cities.add_index(persistent_index) 61 | 62 | # Delete the last index from the collection. 63 | cities.delete_index(index['id']) 64 | 65 | See :ref:`StandardCollection` for API specification. 66 | -------------------------------------------------------------------------------- /docs/logging.rst: -------------------------------------------------------------------------------- 1 | Logging 2 | ------- 3 | 4 | To see full HTTP request and response details, you can modify the logger 5 | settings for the Requests_ library, which python-arango uses under the hood: 6 | 7 | .. _Requests: https://github.com/requests/requests 8 | 9 | .. code-block:: python 10 | 11 | import requests 12 | import logging 13 | 14 | try: 15 | # For Python 3 16 | from http.client import HTTPConnection 17 | except ImportError: 18 | # For Python 2 19 | from httplib import HTTPConnection 20 | HTTPConnection.debuglevel = 1 21 | 22 | logging.basicConfig() 23 | logging.getLogger().setLevel(logging.DEBUG) 24 | requests_log = logging.getLogger("requests.packages.urllib3") 25 | requests_log.setLevel(logging.DEBUG) 26 | requests_log.propagate = True 27 | 28 | If python-arango's default HTTP client is overridden, the code snippet above 29 | may not work as expected. See :doc:`http` for more information. 30 | -------------------------------------------------------------------------------- /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 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 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 | -------------------------------------------------------------------------------- /docs/overload.rst: -------------------------------------------------------------------------------- 1 | Overload API Execution 2 | ---------------------- 3 | :ref:`OverloadControlDatabase` is designed to handle time-bound requests. It allows setting a maximum server-side 4 | queuing time for client requests via the *max_queue_time_seconds* parameter. If the server's queueing time for a 5 | request surpasses this defined limit, the request will be rejected. This mechanism provides you with more control over 6 | request handling, enabling your application to react effectively to potential server overloads. 7 | 8 | Additionally, the response from ArangoDB will always include the most recent request queuing/dequeuing time from the 9 | server's perspective. This can be accessed via the :attr:`~.OverloadControlDatabase.last_queue_time` property. 10 | 11 | **Example:** 12 | 13 | .. testcode:: 14 | 15 | from arango import errno 16 | from arango import ArangoClient 17 | from arango.exceptions import OverloadControlExecutorError 18 | 19 | # Initialize the ArangoDB client. 20 | client = ArangoClient() 21 | 22 | # Connect to "test" database as root user. 23 | db = client.db('test', username='root', password='passwd') 24 | 25 | # Begin controlled execution. 26 | controlled_db = db.begin_controlled_execution(max_queue_time_seconds=7.5) 27 | 28 | # All requests surpassing the specified limit will be rejected. 29 | controlled_aql = controlled_db.aql 30 | controlled_col = controlled_db.collection('students') 31 | 32 | # On API execution, the last_queue_time property gets updated. 33 | controlled_col.insert({'_key': 'Neal'}) 34 | 35 | # Retrieve the last recorded queue time. 36 | assert controlled_db.last_queue_time >= 0 37 | 38 | try: 39 | controlled_aql.execute('RETURN 100000') 40 | except OverloadControlExecutorError as err: 41 | assert err.http_code == errno.HTTP_PRECONDITION_FAILED 42 | assert err.error_code == errno.QUEUE_TIME_REQUIREMENT_VIOLATED 43 | 44 | # Retrieve the maximum allowed queue time. 45 | assert controlled_db.max_queue_time == 7.5 46 | 47 | # Adjust the maximum allowed queue time. 48 | controlled_db.adjust_max_queue_time(0.0001) 49 | 50 | # Disable the maximum allowed queue time. 51 | controlled_db.adjust_max_queue_time(None) 52 | 53 | .. note:: 54 | Setting *max_queue_time_seconds* to 0 or a non-numeric value will cause ArangoDB to ignore the header. 55 | 56 | See :ref:`OverloadControlDatabase` for API specification. 57 | See the `official documentation `_ for 58 | details on ArangoDB's overload control options. 59 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | --------------- 3 | 4 | Here is an example showing how **python-arango** client can be used: 5 | 6 | .. testcode:: 7 | 8 | from arango import ArangoClient 9 | 10 | # Initialize the ArangoDB client. 11 | client = ArangoClient(hosts='http://localhost:8529') 12 | 13 | # Connect to "_system" database as root user. 14 | # This returns an API wrapper for "_system" database. 15 | sys_db = client.db('_system', username='root', password='passwd') 16 | 17 | # Create a new database named "test" if it does not exist. 18 | if not sys_db.has_database('test'): 19 | sys_db.create_database('test') 20 | 21 | # Connect to "test" database as root user. 22 | # This returns an API wrapper for "test" database. 23 | db = client.db('test', username='root', password='passwd') 24 | 25 | # Create a new collection named "students" if it does not exist. 26 | # This returns an API wrapper for "students" collection. 27 | if db.has_collection('students'): 28 | students = db.collection('students') 29 | else: 30 | students = db.create_collection('students') 31 | 32 | # Add a persistent index to the collection. 33 | students.add_index({'type': 'persistent', 'fields': ['name'], 'unique': False}) 34 | 35 | # Truncate the collection. 36 | students.truncate() 37 | 38 | # Insert new documents into the collection. 39 | students.insert({'name': 'jane', 'age': 19}) 40 | students.insert({'name': 'josh', 'age': 18}) 41 | students.insert({'name': 'jake', 'age': 21}) 42 | 43 | # Execute an AQL query. This returns a result cursor. 44 | cursor = db.aql.execute('FOR doc IN students RETURN doc') 45 | 46 | # Iterate through the cursor to retrieve the documents. 47 | student_names = [document['name'] for document in cursor] 48 | -------------------------------------------------------------------------------- /docs/pregel.rst: -------------------------------------------------------------------------------- 1 | Pregel 2 | ------ 3 | 4 | .. warning:: 5 | Starting from ArangoDB 3.12, the Pregel API has been dropped. 6 | Currently, the driver still supports it for the 3.10 and 3.11 versions, but note that it will be dropped eventually. 7 | 8 | Python-arango provides support for **Pregel**, ArangoDB module for distributed 9 | iterative graph processing. For more information, refer to `ArangoDB manual`_. 10 | 11 | .. _ArangoDB manual: https://docs.arangodb.com 12 | 13 | **Example:** 14 | 15 | .. code-block:: python 16 | 17 | from arango import ArangoClient 18 | 19 | # Initialize the ArangoDB client. 20 | client = ArangoClient() 21 | 22 | # Connect to "test" database as root user. 23 | db = client.db('test', username='root', password='passwd') 24 | 25 | # Get the Pregel API wrapper. 26 | pregel = db.pregel 27 | 28 | # Start a new Pregel job in "school" graph. 29 | job_id = db.pregel.create_job( 30 | graph='school', 31 | algorithm='pagerank', 32 | store=False, 33 | max_gss=100, 34 | thread_count=1, 35 | async_mode=False, 36 | result_field='result', 37 | algorithm_params={'threshold': 0.000001} 38 | ) 39 | 40 | # Retrieve details of a Pregel job by ID. 41 | job = pregel.job(job_id) 42 | 43 | # Delete a Pregel job by ID. 44 | pregel.delete_job(job_id) 45 | 46 | See :ref:`Pregel` for API specification. 47 | -------------------------------------------------------------------------------- /docs/replication.rst: -------------------------------------------------------------------------------- 1 | Replication 2 | ----------- 3 | 4 | **Replication** allows you to replicate data onto another machine. It forms the 5 | basis of all disaster recovery and failover features ArangoDB offers. For more 6 | information, refer to `ArangoDB manual`_. 7 | 8 | .. _ArangoDB manual: https://www.arangodb.com/docs/stable/architecture-replication.html 9 | 10 | 11 | **Example:** 12 | 13 | .. code-block:: python 14 | 15 | from arango import ArangoClient 16 | 17 | # Initialize the ArangoDB client. 18 | client = ArangoClient() 19 | 20 | # Connect to "test" database as root user. 21 | db = client.db('test', username='root', password='passwd') 22 | 23 | # Get the Replication API wrapper. 24 | replication = db.replication 25 | 26 | # Create a new dump batch. 27 | batch = replication.create_dump_batch(ttl=1000) 28 | 29 | # Extend an existing dump batch. 30 | replication.extend_dump_batch(batch['id'], ttl=1000) 31 | 32 | # Get an overview of collections and indexes. 33 | replication.inventory( 34 | batch_id=batch['id'], 35 | include_system=True, 36 | all_databases=True 37 | ) 38 | 39 | # Get an overview of collections and indexes in a cluster. 40 | replication.cluster_inventory(include_system=True) 41 | 42 | # Get the events data for given collection. 43 | replication.dump( 44 | collection='students', 45 | batch_id=batch['id'], 46 | lower=0, 47 | upper=1000000, 48 | chunk_size=0, 49 | include_system=True, 50 | ticks=0, 51 | flush=True, 52 | ) 53 | 54 | # Delete an existing dump batch. 55 | replication.delete_dump_batch(batch['id']) 56 | 57 | # Get the logger state. 58 | replication.logger_state() 59 | 60 | # Get the logger first tick value. 61 | replication.logger_first_tick() 62 | 63 | # Get the replication applier configuration. 64 | replication.applier_config() 65 | 66 | # Update the replication applier configuration. 67 | result = replication.set_applier_config( 68 | endpoint='http://127.0.0.1:8529', 69 | database='test', 70 | username='root', 71 | password='passwd', 72 | max_connect_retries=120, 73 | connect_timeout=15, 74 | request_timeout=615, 75 | chunk_size=0, 76 | auto_start=True, 77 | adaptive_polling=False, 78 | include_system=True, 79 | auto_resync=True, 80 | auto_resync_retries=3, 81 | initial_sync_max_wait_time=405, 82 | connection_retry_wait_time=25, 83 | idle_min_wait_time=2, 84 | idle_max_wait_time=3, 85 | require_from_present=False, 86 | verbose=True, 87 | restrict_type='include', 88 | restrict_collections=['students'] 89 | ) 90 | 91 | # Get the replication applier state. 92 | replication.applier_state() 93 | 94 | # Start the replication applier. 95 | replication.start_applier() 96 | 97 | # Stop the replication applier. 98 | replication.stop_applier() 99 | 100 | # Get the server ID. 101 | replication.server_id() 102 | 103 | # Synchronize data from a remote (master) endpoint 104 | replication.synchronize( 105 | endpoint='tcp://master:8500', 106 | database='test', 107 | username='root', 108 | password='passwd', 109 | include_system=False, 110 | incremental=False, 111 | restrict_type='include', 112 | restrict_collections=['students'] 113 | ) 114 | 115 | See :ref:`Replication` for API specification. 116 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | requests_toolbelt 2 | importlib_metadata 3 | PyJWT 4 | sphinx_rtd_theme 5 | -------------------------------------------------------------------------------- /docs/schema.rst: -------------------------------------------------------------------------------- 1 | Schema Validation 2 | ----------------- 3 | 4 | ArangoDB supports document validation using JSON schemas. You can use this 5 | feature by providing a schema during collection creation using the ``schema`` 6 | parameter. 7 | 8 | **Example:** 9 | 10 | .. testcode:: 11 | 12 | from arango import ArangoClient 13 | 14 | # Initialize the ArangoDB client. 15 | client = ArangoClient() 16 | 17 | # Connect to "test" database as root user. 18 | db = client.db('test', username='root', password='passwd') 19 | 20 | if db.has_collection('employees'): 21 | db.delete_collection('employees') 22 | 23 | # Create a new collection named "employees" with custom schema. 24 | my_schema = { 25 | 'rule': { 26 | 'type': 'object', 27 | 'properties': { 28 | 'name': {'type': 'string'}, 29 | 'email': {'type': 'string'} 30 | }, 31 | 'required': ['name', 'email'] 32 | }, 33 | 'level': 'moderate', 34 | 'message': 'Schema Validation Failed.' 35 | } 36 | employees = db.create_collection(name='employees', schema=my_schema) 37 | 38 | # Modify the schema. 39 | employees.configure(schema=my_schema) 40 | 41 | # Remove the schema. 42 | employees.configure(schema={}) 43 | -------------------------------------------------------------------------------- /docs/serializer.rst: -------------------------------------------------------------------------------- 1 | JSON Serialization 2 | ------------------ 3 | 4 | You can provide your own JSON serializer and deserializer during client 5 | initialization. They must be callables that take a single argument. 6 | 7 | **Example:** 8 | 9 | .. testcode:: 10 | 11 | import json 12 | 13 | from arango import ArangoClient 14 | 15 | # Initialize the ArangoDB client with custom serializer and deserializer. 16 | client = ArangoClient( 17 | hosts='http://localhost:8529', 18 | serializer=json.dumps, 19 | deserializer=json.loads 20 | ) 21 | 22 | See :ref:`ArangoClient` for API specification. 23 | -------------------------------------------------------------------------------- /docs/simple.rst: -------------------------------------------------------------------------------- 1 | Simple Queries 2 | -------------- 3 | 4 | Here is an example of using ArangoDB's **simply queries**: 5 | 6 | .. testcode:: 7 | 8 | from arango import ArangoClient 9 | 10 | # Initialize the ArangoDB client. 11 | client = ArangoClient() 12 | 13 | # Connect to "test" database as root user. 14 | db = client.db('test', username='root', password='passwd') 15 | 16 | # Get the API wrapper for "students" collection. 17 | students = db.collection('students') 18 | 19 | # Get the IDs of all documents in the collection. 20 | students.ids() 21 | 22 | # Get the keys of all documents in the collection. 23 | students.keys() 24 | 25 | # Get all documents in the collection with skip and limit. 26 | students.all(skip=0, limit=100) 27 | 28 | # Find documents that match the given filters. 29 | students.find({'name': 'Mary'}, skip=0, limit=100) 30 | 31 | # Get documents from the collection by IDs or keys. 32 | students.get_many(['id1', 'id2', 'key1']) 33 | 34 | # Get a random document from the collection. 35 | students.random() 36 | 37 | # Update all documents that match the given filters. 38 | students.update_match({'name': 'Kim'}, {'age': 20}) 39 | 40 | # Replace all documents that match the given filters. 41 | students.replace_match({'name': 'Ben'}, {'age': 20}) 42 | 43 | # Delete all documents that match the given filters. 44 | students.delete_match({'name': 'John'}) 45 | 46 | Here are all simple query (and other utility) methods available: 47 | 48 | * :func:`arango.collection.Collection.all` 49 | * :func:`arango.collection.Collection.find` 50 | * :func:`arango.collection.Collection.find_near` 51 | * :func:`arango.collection.Collection.find_in_range` 52 | * :func:`arango.collection.Collection.find_in_radius` 53 | * :func:`arango.collection.Collection.find_in_box` 54 | * :func:`arango.collection.Collection.find_by_text` 55 | * :func:`arango.collection.Collection.get_many` 56 | * :func:`arango.collection.Collection.ids` 57 | * :func:`arango.collection.Collection.keys` 58 | * :func:`arango.collection.Collection.random` 59 | * :func:`arango.collection.StandardCollection.update_match` 60 | * :func:`arango.collection.StandardCollection.replace_match` 61 | * :func:`arango.collection.StandardCollection.delete_match` 62 | * :func:`arango.collection.StandardCollection.import_bulk` 63 | -------------------------------------------------------------------------------- /docs/specs.rst: -------------------------------------------------------------------------------- 1 | API Specification 2 | ----------------- 3 | 4 | This page contains the specification for all classes and methods available in 5 | python-arango. 6 | 7 | .. _ArangoClient: 8 | 9 | ArangoClient 10 | ============ 11 | 12 | .. autoclass:: arango.client.ArangoClient 13 | :members: 14 | 15 | .. _AsyncDatabase: 16 | 17 | AsyncDatabase 18 | ============= 19 | 20 | .. autoclass:: arango.database.AsyncDatabase 21 | :inherited-members: 22 | :members: 23 | 24 | .. _AsyncJob: 25 | 26 | AsyncJob 27 | ======== 28 | 29 | .. autoclass:: arango.job.AsyncJob 30 | :members: 31 | 32 | .. _AQL: 33 | 34 | AQL 35 | ==== 36 | 37 | .. autoclass:: arango.aql.AQL 38 | :members: 39 | 40 | .. _AQLQueryCache: 41 | 42 | AQLQueryCache 43 | ============= 44 | 45 | .. autoclass:: arango.aql.AQLQueryCache 46 | :members: 47 | 48 | .. _Backup: 49 | 50 | Backup 51 | ====== 52 | 53 | .. autoclass:: arango.backup.Backup 54 | :inherited-members: 55 | :members: 56 | 57 | .. _BatchDatabase: 58 | 59 | BatchDatabase 60 | ============= 61 | 62 | .. autoclass:: arango.database.BatchDatabase 63 | :inherited-members: 64 | :members: 65 | 66 | .. _BatchJob: 67 | 68 | BatchJob 69 | ======== 70 | 71 | .. autoclass:: arango.job.BatchJob 72 | :members: 73 | 74 | .. _Cluster: 75 | 76 | Cluster 77 | ======= 78 | 79 | .. autoclass:: arango.cluster.Cluster 80 | :members: 81 | 82 | .. _Collection: 83 | 84 | Collection 85 | ========== 86 | 87 | .. autoclass:: arango.collection.Collection 88 | :members: 89 | 90 | .. _Cursor: 91 | 92 | Cursor 93 | ====== 94 | 95 | .. autoclass:: arango.cursor.Cursor 96 | :members: 97 | 98 | .. _DefaultHTTPClient: 99 | 100 | DefaultHTTPClient 101 | ================= 102 | 103 | .. autoclass:: arango.http.DefaultHTTPClient 104 | :members: 105 | 106 | DeflateRequestCompression 107 | ========================= 108 | 109 | .. autoclass:: arango.http.DeflateRequestCompression 110 | :members: 111 | 112 | .. _EdgeCollection: 113 | 114 | EdgeCollection 115 | ============== 116 | 117 | .. autoclass:: arango.collection.EdgeCollection 118 | :members: 119 | 120 | .. _Foxx: 121 | 122 | Foxx 123 | ==== 124 | 125 | .. autoclass:: arango.foxx.Foxx 126 | :members: 127 | 128 | .. _Graph: 129 | 130 | Graph 131 | ===== 132 | 133 | .. autoclass:: arango.graph.Graph 134 | :members: 135 | 136 | .. _HTTPClient: 137 | 138 | HTTPClient 139 | ========== 140 | 141 | .. autoclass:: arango.http.HTTPClient 142 | :members: 143 | 144 | .. _OverloadControlDatabase: 145 | 146 | OverloadControlDatabase 147 | ======================= 148 | 149 | .. autoclass:: arango.database.OverloadControlDatabase 150 | :inherited-members: 151 | :members: 152 | 153 | .. _Pregel: 154 | 155 | Pregel 156 | ====== 157 | 158 | .. autoclass:: arango.pregel.Pregel 159 | :members: 160 | 161 | .. _Replication: 162 | 163 | Replication 164 | =========== 165 | 166 | .. autoclass:: arango.replication.Replication 167 | :members: 168 | 169 | .. _Request: 170 | 171 | Request 172 | ======= 173 | 174 | .. autoclass:: arango.request.Request 175 | :members: 176 | 177 | .. _Response: 178 | 179 | Response 180 | ======== 181 | 182 | .. autoclass:: arango.response.Response 183 | :members: 184 | 185 | .. _StandardCollection: 186 | 187 | StandardCollection 188 | ================== 189 | 190 | .. autoclass:: arango.collection.StandardCollection 191 | :inherited-members: 192 | :members: 193 | 194 | .. _StandardDatabase: 195 | 196 | StandardDatabase 197 | ================ 198 | 199 | .. autoclass:: arango.database.StandardDatabase 200 | :inherited-members: 201 | :members: 202 | 203 | .. _TransactionDatabase: 204 | 205 | TransactionDatabase 206 | =================== 207 | 208 | .. autoclass:: arango.database.TransactionDatabase 209 | :inherited-members: 210 | :members: 211 | 212 | .. _VertexCollection: 213 | 214 | VertexCollection 215 | ================ 216 | 217 | .. autoclass:: arango.collection.VertexCollection 218 | :members: 219 | 220 | .. _WriteAheadLog: 221 | 222 | WAL 223 | ==== 224 | 225 | .. autoclass:: arango.wal.WAL 226 | :members: 227 | -------------------------------------------------------------------------------- /docs/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arangodb/python-arango/3769989166098b05849223bdc3eb90d934cb12ba/docs/static/logo.png -------------------------------------------------------------------------------- /docs/task.rst: -------------------------------------------------------------------------------- 1 | Tasks 2 | ----- 3 | 4 | ArangoDB can schedule user-defined Javascript snippets as one-time or periodic 5 | (re-scheduled after each execution) tasks. Tasks are executed in the context of 6 | the database they are defined in. 7 | 8 | **Example:** 9 | 10 | .. testcode:: 11 | 12 | from arango import ArangoClient 13 | 14 | # Initialize the ArangoDB client. 15 | client = ArangoClient() 16 | 17 | # Connect to "test" database as root user. 18 | db = client.db('test', username='root', password='passwd') 19 | 20 | # List all active tasks 21 | db.tasks() 22 | 23 | # Create a new task which simply prints parameters. 24 | db.create_task( 25 | name='test_task', 26 | command=''' 27 | var task = function(params){ 28 | var db = require('@arangodb'); 29 | db.print(params); 30 | } 31 | task(params); 32 | ''', 33 | params={'foo': 'bar'}, 34 | offset=300, 35 | period=10, 36 | task_id='001' 37 | ) 38 | 39 | # Retrieve details on a task by ID. 40 | db.task('001') 41 | 42 | # Delete an existing task by ID. 43 | db.delete_task('001', ignore_missing=True) 44 | 45 | .. note:: 46 | When deleting a database, any tasks that were initialized under its context 47 | remain active. It is therefore advisable to delete any running tasks before 48 | deleting the database. 49 | 50 | Refer to :ref:`StandardDatabase` class for API specification. 51 | -------------------------------------------------------------------------------- /docs/threading.rst: -------------------------------------------------------------------------------- 1 | Multithreading 2 | -------------- 3 | 4 | There are a few things you should consider before using python-arango in a 5 | multithreaded (or multiprocess) architecture. 6 | 7 | Stateful Objects 8 | ================ 9 | 10 | Instances of the following classes are considered *stateful*, and should not be 11 | accessed across multiple threads without locks in place: 12 | 13 | * :ref:`BatchDatabase` (see :doc:`batch`) 14 | * :ref:`BatchJob` (see :doc:`batch`) 15 | * :ref:`Cursor` (see :doc:`cursor`) 16 | 17 | 18 | HTTP Sessions 19 | ============= 20 | 21 | When :ref:`ArangoClient` is initialized, a `requests.Session`_ instance is 22 | created per ArangoDB host connected. HTTP requests to a host are sent using 23 | only its corresponding session. For more information on how to override this 24 | behaviour, see :doc:`http`. 25 | 26 | Note that a `requests.Session`_ object may not always be thread-safe. Always 27 | research your use case! 28 | 29 | .. _requests.Session: http://docs.python-requests.org/en/master/user/advanced/#session-objects 30 | -------------------------------------------------------------------------------- /docs/transaction.rst: -------------------------------------------------------------------------------- 1 | Transactions 2 | ------------ 3 | 4 | In **transactions**, requests to ArangoDB server are committed as a single, 5 | logical unit of work (ACID compliant). 6 | 7 | .. warning:: 8 | 9 | New transaction REST API was added to ArangoDB version 3.5. In order to use 10 | it python-arango's own transaction API had to be overhauled in version 11 | 5.0.0. **The changes are not backward-compatible**: context managers are no 12 | longer offered (you must always commit the transaction youself), method 13 | signatures are different when beginning the transaction, and results are 14 | returned immediately instead of job objects. 15 | 16 | **Example:** 17 | 18 | .. testcode:: 19 | 20 | from arango import ArangoClient 21 | 22 | # Initialize the ArangoDB client. 23 | client = ArangoClient() 24 | 25 | # Connect to "test" database as root user. 26 | db = client.db('test', username='root', password='passwd') 27 | col = db.collection('students') 28 | 29 | # Begin a transaction. Read and write collections must be declared ahead of 30 | # time. This returns an instance of TransactionDatabase, database-level 31 | # API wrapper tailored specifically for executing transactions. 32 | txn_db = db.begin_transaction(read=col.name, write=col.name) 33 | 34 | # The API wrapper is specific to a single transaction with a unique ID. 35 | txn_db.transaction_id 36 | 37 | # Child wrappers are also tailored only for the specific transaction. 38 | txn_aql = txn_db.aql 39 | txn_col = txn_db.collection('students') 40 | 41 | # API execution context is always set to "transaction". 42 | assert txn_db.context == 'transaction' 43 | assert txn_aql.context == 'transaction' 44 | assert txn_col.context == 'transaction' 45 | 46 | # From python-arango version 5+, results are returned immediately instead 47 | # of job objects on API execution. 48 | assert '_rev' in txn_col.insert({'_key': 'Abby'}) 49 | assert '_rev' in txn_col.insert({'_key': 'John'}) 50 | assert '_rev' in txn_col.insert({'_key': 'Mary'}) 51 | 52 | # Check the transaction status. 53 | txn_db.transaction_status() 54 | 55 | # Commit the transaction. 56 | txn_db.commit_transaction() 57 | assert 'Abby' in col 58 | assert 'John' in col 59 | assert 'Mary' in col 60 | assert len(col) == 3 61 | 62 | # Begin another transaction. Note that the wrappers above are specific to 63 | # the last transaction and cannot be reused. New ones must be created. 64 | txn_db = db.begin_transaction(read=col.name, write=col.name) 65 | txn_col = txn_db.collection('students') 66 | assert '_rev' in txn_col.insert({'_key': 'Kate'}) 67 | assert '_rev' in txn_col.insert({'_key': 'Mike'}) 68 | assert '_rev' in txn_col.insert({'_key': 'Lily'}) 69 | assert len(txn_col) == 6 70 | 71 | # Abort the transaction 72 | txn_db.abort_transaction() 73 | assert 'Kate' not in col 74 | assert 'Mike' not in col 75 | assert 'Lily' not in col 76 | assert len(col) == 3 # transaction is aborted so txn_col cannot be used 77 | 78 | # Fetch an existing transaction. Useful if you have received a Transaction ID 79 | # from some other part of your system or an external system. 80 | original_txn = db.begin_transaction(write='students') 81 | txn_col = original_txn.collection('students') 82 | assert '_rev' in txn_col.insert({'_key': 'Chip'}) 83 | txn_db = db.fetch_transaction(original_txn.transaction_id) 84 | txn_col = txn_db.collection('students') 85 | assert '_rev' in txn_col.insert({'_key': 'Alya'}) 86 | txn_db.abort_transaction() 87 | 88 | See :ref:`TransactionDatabase` for API specification. 89 | 90 | Alternatively, you can use 91 | :func:`arango.database.StandardDatabase.execute_transaction` to run raw 92 | Javascript code in a transaction. 93 | 94 | **Example:** 95 | 96 | .. testcode:: 97 | 98 | from arango import ArangoClient 99 | 100 | # Initialize the ArangoDB client. 101 | client = ArangoClient() 102 | 103 | # Connect to "test" database as root user. 104 | db = client.db('test', username='root', password='passwd') 105 | 106 | # Get the API wrapper for "students" collection. 107 | students = db.collection('students') 108 | 109 | # Execute transaction in raw Javascript. 110 | result = db.execute_transaction( 111 | command=''' 112 | function () {{ 113 | var db = require('internal').db; 114 | db.students.save(params.student1); 115 | if (db.students.count() > 1) { 116 | db.students.save(params.student2); 117 | } else { 118 | db.students.save(params.student3); 119 | } 120 | return true; 121 | }} 122 | ''', 123 | params={ 124 | 'student1': {'_key': 'Lucy'}, 125 | 'student2': {'_key': 'Greg'}, 126 | 'student3': {'_key': 'Dona'} 127 | }, 128 | read='students', # Specify the collections read. 129 | write='students' # Specify the collections written. 130 | ) 131 | assert result is True 132 | assert 'Lucy' in students 133 | assert 'Greg' in students 134 | assert 'Dona' not in students 135 | -------------------------------------------------------------------------------- /docs/user.rst: -------------------------------------------------------------------------------- 1 | Users and Permissions 2 | --------------------- 3 | 4 | Python-arango provides operations for managing users and permissions. Most of 5 | these operations can only be performed by admin users via ``_system`` database. 6 | 7 | **Example:** 8 | 9 | .. testcode:: 10 | 11 | from arango import ArangoClient 12 | 13 | # Initialize the ArangoDB client. 14 | client = ArangoClient() 15 | 16 | # Connect to "_system" database as root user. 17 | sys_db = client.db('_system', username='root', password='passwd') 18 | 19 | # List all users. 20 | sys_db.users() 21 | 22 | # Create a new user. 23 | sys_db.create_user( 24 | username='johndoe@gmail.com', 25 | password='first_password', 26 | active=True, 27 | extra={'team': 'backend', 'title': 'engineer'} 28 | ) 29 | 30 | # Check if a user exists. 31 | sys_db.has_user('johndoe@gmail.com') 32 | 33 | # Retrieve details of a user. 34 | sys_db.user('johndoe@gmail.com') 35 | 36 | # Update an existing user. 37 | sys_db.update_user( 38 | username='johndoe@gmail.com', 39 | password='second_password', 40 | active=True, 41 | extra={'team': 'frontend', 'title': 'engineer'} 42 | ) 43 | 44 | # Replace an existing user. 45 | sys_db.replace_user( 46 | username='johndoe@gmail.com', 47 | password='third_password', 48 | active=True, 49 | extra={'team': 'frontend', 'title': 'architect'} 50 | ) 51 | 52 | # Retrieve user permissions for all databases and collections. 53 | sys_db.permissions('johndoe@gmail.com') 54 | 55 | # Retrieve user permission for "test" database. 56 | sys_db.permission( 57 | username='johndoe@gmail.com', 58 | database='test' 59 | ) 60 | 61 | # Retrieve user permission for "students" collection in "test" database. 62 | sys_db.permission( 63 | username='johndoe@gmail.com', 64 | database='test', 65 | collection='students' 66 | ) 67 | 68 | # Update user permission for "test" database. 69 | sys_db.update_permission( 70 | username='johndoe@gmail.com', 71 | permission='rw', 72 | database='test' 73 | ) 74 | 75 | # Update user permission for "students" collection in "test" database. 76 | sys_db.update_permission( 77 | username='johndoe@gmail.com', 78 | permission='ro', 79 | database='test', 80 | collection='students' 81 | ) 82 | 83 | # Reset user permission for "test" database. 84 | sys_db.reset_permission( 85 | username='johndoe@gmail.com', 86 | database='test' 87 | ) 88 | 89 | # Reset user permission for "students" collection in "test" database. 90 | sys_db.reset_permission( 91 | username='johndoe@gmail.com', 92 | database='test', 93 | collection='students' 94 | ) 95 | 96 | See :ref:`StandardDatabase` for API specification. 97 | -------------------------------------------------------------------------------- /docs/view.rst: -------------------------------------------------------------------------------- 1 | Views and ArangoSearch 2 | ---------------------- 3 | 4 | Python-arango supports **view** management. For more information on view 5 | properties, refer to `ArangoDB manual`_. 6 | 7 | .. _ArangoDB manual: https://docs.arangodb.com 8 | 9 | **Example:** 10 | 11 | .. testcode:: 12 | 13 | from arango import ArangoClient 14 | 15 | # Initialize the ArangoDB client. 16 | client = ArangoClient() 17 | 18 | # Connect to "test" database as root user. 19 | db = client.db('test', username='root', password='passwd') 20 | 21 | # Retrieve list of views. 22 | db.views() 23 | 24 | # Create a view. 25 | db.create_view( 26 | name='foo', 27 | view_type='arangosearch', 28 | properties={ 29 | 'cleanupIntervalStep': 0, 30 | 'consolidationIntervalMsec': 0 31 | } 32 | ) 33 | 34 | # Rename a view. 35 | db.rename_view('foo', 'bar') 36 | 37 | # Retrieve view properties. 38 | db.view('bar') 39 | 40 | # Partially update view properties. 41 | db.update_view( 42 | name='bar', 43 | properties={ 44 | 'cleanupIntervalStep': 1000, 45 | 'consolidationIntervalMsec': 200 46 | } 47 | ) 48 | 49 | # Replace view properties. Unspecified ones are reset to default. 50 | db.replace_view( 51 | name='bar', 52 | properties={'cleanupIntervalStep': 2000} 53 | ) 54 | 55 | # Delete a view. 56 | db.delete_view('bar') 57 | 58 | 59 | Python-arango also supports **ArangoSearch** views. 60 | 61 | **Example:** 62 | 63 | .. testcode:: 64 | 65 | from arango import ArangoClient 66 | 67 | # Initialize the ArangoDB client. 68 | client = ArangoClient() 69 | 70 | # Connect to "test" database as root user. 71 | db = client.db('test', username='root', password='passwd') 72 | 73 | # Create an ArangoSearch view. 74 | db.create_arangosearch_view( 75 | name='arangosearch_view', 76 | properties={'cleanupIntervalStep': 0} 77 | ) 78 | 79 | # Partially update an ArangoSearch view. 80 | db.update_arangosearch_view( 81 | name='arangosearch_view', 82 | properties={'cleanupIntervalStep': 1000} 83 | ) 84 | 85 | # Replace an ArangoSearch view. 86 | db.replace_arangosearch_view( 87 | name='arangosearch_view', 88 | properties={'cleanupIntervalStep': 2000} 89 | ) 90 | 91 | # ArangoSearch views can be retrieved or deleted using regular view API 92 | db.view('arangosearch_view') 93 | db.delete_view('arangosearch_view') 94 | 95 | 96 | For more information on the content of view **properties**, see 97 | https://www.arangodb.com/docs/stable/http/views-arangosearch.html 98 | 99 | Refer to :ref:`StandardDatabase` class for API specification. 100 | -------------------------------------------------------------------------------- /docs/wal.rst: -------------------------------------------------------------------------------- 1 | Write-Ahead Log (WAL) 2 | --------------------- 3 | 4 | **Write-Ahead Log (WAL)** is a set of append-only files recording all writes 5 | on ArangoDB server. It is typically used to perform data recovery after a crash 6 | or synchronize slave databases with master databases in replicated environments. 7 | WAL operations can only be performed by admin users via ``_system`` database. 8 | 9 | **Example:** 10 | 11 | .. code-block:: python 12 | 13 | from arango import ArangoClient 14 | 15 | # Initialize the ArangoDB client. 16 | client = ArangoClient() 17 | 18 | # Connect to "_system" database as root user. 19 | sys_db = client.db('_system', username='root', password='passwd') 20 | 21 | # Get the WAL API wrapper. 22 | wal = sys_db.wal 23 | 24 | # Configure WAL properties. 25 | wal.configure( 26 | historic_logs=15, 27 | oversized_ops=False, 28 | log_size=30000000, 29 | reserve_logs=5, 30 | throttle_limit=0, 31 | throttle_wait=16000 32 | ) 33 | 34 | # Retrieve WAL properties. 35 | wal.properties() 36 | 37 | # List WAL transactions. 38 | wal.transactions() 39 | 40 | # Flush WAL with garbage collection. 41 | wal.flush(garbage_collect=True) 42 | 43 | # Get the available ranges of tick values. 44 | wal.tick_ranges() 45 | 46 | # Get the last available tick value. 47 | wal.last_tick() 48 | 49 | # Get recent WAL operations. 50 | wal.tail() 51 | 52 | See :class:`WriteAheadLog` for API specification. 53 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel", "setuptools_scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | normalize = true 7 | 8 | [project] 9 | name = "python-arango" 10 | description = "Python Driver for ArangoDB" 11 | authors = [ {name= "Joohwan Oh", email = "joohwan.oh@outlook.com" }] 12 | maintainers = [ 13 | {name = "Joohwan Oh", email = "joohwan.oh@outlook.com"}, 14 | {name = "Alexandru Petenchea", email = "alex.petenchea@gmail.com"}, 15 | {name = "Anthony Mahanna", email = "anthony.mahanna@arangodb.com"} 16 | ] 17 | keywords = ["arangodb", "python", "driver"] 18 | readme = "README.md" 19 | dynamic = ["version"] 20 | license = { file = "LICENSE" } 21 | requires-python = ">=3.9" 22 | 23 | classifiers = [ 24 | "Intended Audience :: Developers", 25 | "License :: OSI Approved :: MIT License", 26 | "Natural Language :: English", 27 | "Operating System :: OS Independent", 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3.9", 30 | "Programming Language :: Python :: 3.10", 31 | "Programming Language :: Python :: 3.11", 32 | "Programming Language :: Python :: 3.12", 33 | "Topic :: Documentation :: Sphinx", 34 | "Typing :: Typed", 35 | ] 36 | 37 | dependencies = [ 38 | "urllib3>=1.26.0", 39 | "requests", 40 | "requests_toolbelt", 41 | "PyJWT", 42 | "setuptools>=42", 43 | "importlib_metadata>=4.7.1", 44 | "packaging>=23.1", 45 | ] 46 | 47 | [project.optional-dependencies] 48 | dev = [ 49 | "black>=22.3.0", 50 | "flake8>=4.0.1", 51 | "isort>=5.10.1", 52 | "mypy>=0.942", 53 | "mock", 54 | "pre-commit>=2.17.0", 55 | "pytest>=7.1.1", 56 | "pytest-cov>=3.0.0", 57 | "sphinx", 58 | "sphinx_rtd_theme", 59 | "types-requests", 60 | "types-setuptools", 61 | ] 62 | 63 | [tool.setuptools.package-data] 64 | "arango" = ["py.typed"] 65 | 66 | [project.urls] 67 | homepage = "https://github.com/arangodb/python-arango" 68 | 69 | [tool.setuptools] 70 | packages = ["arango"] 71 | 72 | 73 | [tool.pytest.ini_options] 74 | addopts = "-s -vv -p no:warnings" 75 | minversion = "6.0" 76 | testpaths = ["tests"] 77 | 78 | [tool.coverage.run] 79 | omit = [ 80 | "arango/version.py", 81 | "arango/formatter.py", 82 | "setup.py", 83 | ] 84 | 85 | [tool.isort] 86 | profile = "black" 87 | 88 | [tool.mypy] 89 | warn_return_any = true 90 | warn_unused_configs = true 91 | ignore_missing_imports = true 92 | strict = true 93 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203, E741, W503 4 | exclude =.git .idea .*_cache dist htmlcov venv arango/errno.py 5 | per-file-ignores = __init__.py:F401 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /starter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Starts a local ArangoDB server or cluster (community or enterprise). 4 | # Useful for testing the python-arango driver against a local ArangoDB setup. 5 | 6 | # Usage: 7 | # ./starter.sh [single|cluster] [community|enterprise] [version] 8 | # Example: 9 | # ./starter.sh cluster enterprise 3.12.1 10 | 11 | setup="${1:-single}" 12 | license="${2:-community}" 13 | version="${3:-latest}" 14 | 15 | extra_ports="" 16 | if [ "$setup" == "single" ]; then 17 | echo "" 18 | elif [ "$setup" == "cluster" ]; then 19 | extra_ports="-p 8539:8539 -p 8549:8549" 20 | else 21 | echo "Invalid argument. Please provide either 'single' or 'cluster'." 22 | exit 1 23 | fi 24 | 25 | image_name="" 26 | if [ "$license" == "community" ]; then 27 | image_name="arangodb" 28 | elif [ "$license" == "enterprise" ]; then 29 | image_name="enterprise" 30 | else 31 | echo "Invalid argument. Please provide either 'community' or 'enterprise'." 32 | exit 1 33 | fi 34 | 35 | if [ "$version" == "latest" ]; then 36 | conf_file="${setup}-3.12" 37 | elif [[ "$version" == *.*.* ]]; then 38 | conf_file="${setup}-${version%.*}" 39 | else 40 | conf_file="${setup}-${version}" 41 | fi 42 | 43 | docker run -d \ 44 | --name arango \ 45 | -p 8528:8528 \ 46 | -p 8529:8529 \ 47 | $extra_ports \ 48 | -v "$(pwd)/tests/static/":/tests/static \ 49 | -v /tmp:/tmp \ 50 | "arangodb/$image_name:$version" \ 51 | /bin/sh -c "arangodb --configuration=/tests/static/$conf_file.conf" 52 | 53 | wget --quiet --waitretry=1 --tries=120 -O - http://localhost:8528/version | jq 54 | if [ $? -eq 0 ]; then 55 | echo "OK starter ready" 56 | exit 0 57 | else 58 | echo "ERROR starter not ready, giving up" 59 | exit 1 60 | fi 61 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arangodb/python-arango/3769989166098b05849223bdc3eb90d934cb12ba/tests/__init__.py -------------------------------------------------------------------------------- /tests/executors.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from arango.executor import AsyncApiExecutor, BatchApiExecutor, TransactionApiExecutor 4 | from arango.job import BatchJob 5 | 6 | 7 | class TestAsyncApiExecutor(AsyncApiExecutor): 8 | def __init__(self, connection) -> None: 9 | super().__init__(connection=connection, return_result=True) 10 | 11 | def execute(self, request, response_handler): 12 | job = AsyncApiExecutor.execute(self, request, response_handler) 13 | while job.status() != "done": 14 | time.sleep(0.01) 15 | return job.result() 16 | 17 | 18 | class TestBatchExecutor(BatchApiExecutor): 19 | def __init__(self, connection) -> None: 20 | super().__init__(connection=connection, return_result=True) 21 | 22 | def execute(self, request, response_handler): 23 | self._committed = False 24 | self._queue.clear() 25 | 26 | job = BatchJob(response_handler) 27 | self._queue[job.id] = (request, job) 28 | self.commit() 29 | return job.result() 30 | 31 | 32 | class TestTransactionApiExecutor(TransactionApiExecutor): 33 | # noinspection PyMissingConstructor 34 | def __init__(self, connection) -> None: 35 | self._conn = connection 36 | 37 | def execute(self, request, response_handler): 38 | if request.read is request.write is request.exclusive is None: 39 | resp = self._conn.send_request(request) 40 | return response_handler(resp) 41 | 42 | super().__init__( 43 | connection=self._conn, 44 | sync=True, 45 | allow_implicit=False, 46 | lock_timeout=0, 47 | read=request.read, 48 | write=request.write, 49 | exclusive=request.exclusive, 50 | ) 51 | result = super().execute(request, response_handler) 52 | self.commit() 53 | return result 54 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | import time 2 | from collections import deque 3 | from uuid import uuid4 4 | 5 | import jwt 6 | import pytest 7 | 8 | from arango.cursor import Cursor 9 | from arango.exceptions import AsyncExecuteError, TransactionInitError 10 | 11 | 12 | def generate_db_name(): 13 | """Generate and return a random database name. 14 | 15 | :return: Random database name. 16 | :rtype: str 17 | """ 18 | return f"test_database_{uuid4().hex}" 19 | 20 | 21 | def generate_col_name(): 22 | """Generate and return a random collection name. 23 | 24 | :return: Random collection name. 25 | :rtype: str 26 | """ 27 | return f"test_collection_{uuid4().hex}" 28 | 29 | 30 | def generate_graph_name(): 31 | """Generate and return a random graph name. 32 | 33 | :return: Random graph name. 34 | :rtype: str 35 | """ 36 | return f"test_graph_{uuid4().hex}" 37 | 38 | 39 | def generate_doc_key(): 40 | """Generate and return a random document key. 41 | 42 | :return: Random document key. 43 | :rtype: str 44 | """ 45 | return f"test_document_{uuid4().hex}" 46 | 47 | 48 | def generate_task_name(): 49 | """Generate and return a random task name. 50 | 51 | :return: Random task name. 52 | :rtype: str 53 | """ 54 | return f"test_task_{uuid4().hex}" 55 | 56 | 57 | def generate_task_id(): 58 | """Generate and return a random task ID. 59 | 60 | :return: Random task ID 61 | :rtype: str 62 | """ 63 | return f"test_task_id_{uuid4().hex}" 64 | 65 | 66 | def generate_username(): 67 | """Generate and return a random username. 68 | 69 | :return: Random username. 70 | :rtype: str 71 | """ 72 | return f"test_user_{uuid4().hex}" 73 | 74 | 75 | def generate_view_name(): 76 | """Generate and return a random view name. 77 | 78 | :return: Random view name. 79 | :rtype: str 80 | """ 81 | return f"test_view_{uuid4().hex}" 82 | 83 | 84 | def generate_analyzer_name(): 85 | """Generate and return a random analyzer name. 86 | 87 | :return: Random analyzer name. 88 | :rtype: str 89 | """ 90 | return f"test_analyzer_{uuid4().hex}" 91 | 92 | 93 | def generate_string(): 94 | """Generate and return a random unique string. 95 | 96 | :return: Random unique string. 97 | :rtype: str 98 | """ 99 | return uuid4().hex 100 | 101 | 102 | def generate_service_mount(): 103 | """Generate and return a random service name. 104 | 105 | :return: Random service name. 106 | :rtype: str 107 | """ 108 | return f"/test_{uuid4().hex}" 109 | 110 | 111 | def generate_jwt(secret, exp=3600): 112 | """Generate and return a JWT. 113 | 114 | :param secret: JWT secret 115 | :type secret: str 116 | :param exp: Time to expire in seconds. 117 | :type exp: int 118 | :return: JWT 119 | :rtype: str 120 | """ 121 | now = int(time.time()) 122 | return jwt.encode( 123 | payload={ 124 | "iat": now, 125 | "exp": now + exp, 126 | "iss": "arangodb", 127 | "server_id": "client", 128 | }, 129 | key=secret, 130 | ) 131 | 132 | 133 | def clean_doc(obj): 134 | """Return the document(s) with all extra system keys stripped. 135 | 136 | :param obj: document(s) 137 | :type obj: list | dict | arango.cursor.Cursor 138 | :return: Document(s) with the system keys stripped 139 | :rtype: list | dict 140 | """ 141 | if isinstance(obj, (Cursor, list, deque)): 142 | docs = [clean_doc(d) for d in obj] 143 | return sorted(docs, key=lambda doc: doc["_key"]) 144 | 145 | if isinstance(obj, dict): 146 | return { 147 | field: value 148 | for field, value in obj.items() 149 | if field in {"_key", "_from", "_to"} or not field.startswith("_") 150 | } 151 | 152 | 153 | def empty_collection(collection): 154 | """Empty all the documents in the collection. 155 | 156 | :param collection: Collection name 157 | :type collection: arango.collection.StandardCollection | 158 | arango.collection.VertexCollection | arango.collection.EdgeCollection 159 | """ 160 | for doc_id in collection.ids(): 161 | collection.delete(doc_id, sync=True) 162 | 163 | 164 | def extract(key, items): 165 | """Return the sorted values from dicts using the given key. 166 | 167 | :param key: Dictionary key 168 | :type key: str 169 | :param items: Items to filter. 170 | :type items: [dict] 171 | :return: Set of values. 172 | :rtype: [str] 173 | """ 174 | return sorted(item[key] for item in items) 175 | 176 | 177 | def assert_raises(*exc): 178 | """Assert that the given exception is raised. 179 | 180 | :param exc: Expected exception(s). 181 | :type: exc 182 | """ 183 | return pytest.raises(exc + (AsyncExecuteError, TransactionInitError)) 184 | -------------------------------------------------------------------------------- /tests/static/cluster-3.11.conf: -------------------------------------------------------------------------------- 1 | [starter] 2 | mode = cluster 3 | local = true 4 | address = 0.0.0.0 5 | port = 8528 6 | 7 | [auth] 8 | jwt-secret = /tests/static/keyfile 9 | 10 | [args] 11 | all.database.password = passwd 12 | all.database.extended-names = true 13 | all.log.api-enabled = true 14 | all.javascript.allow-admin-execute = true 15 | -------------------------------------------------------------------------------- /tests/static/cluster-3.12.conf: -------------------------------------------------------------------------------- 1 | [starter] 2 | mode = cluster 3 | local = true 4 | address = 0.0.0.0 5 | port = 8528 6 | 7 | [auth] 8 | jwt-secret = /tests/static/keyfile 9 | 10 | [args] 11 | all.database.password = passwd 12 | all.database.extended-names = true 13 | all.log.api-enabled = true 14 | all.javascript.allow-admin-execute = true 15 | all.server.options-api = admin 16 | -------------------------------------------------------------------------------- /tests/static/keyfile: -------------------------------------------------------------------------------- 1 | secret 2 | -------------------------------------------------------------------------------- /tests/static/service.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arangodb/python-arango/3769989166098b05849223bdc3eb90d934cb12ba/tests/static/service.zip -------------------------------------------------------------------------------- /tests/static/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p /tests/static 4 | wget -O /tests/static/service.zip "http://localhost:8000/$PROJECT/tests/static/service.zip" 5 | wget -O /tests/static/keyfile "http://localhost:8000/$PROJECT/tests/static/keyfile" 6 | wget -O /tests/static/arangodb.conf "http://localhost:8000/$PROJECT/tests/static/$ARANGODB_CONF" 7 | arangodb --configuration=/tests/static/arangodb.conf 8 | -------------------------------------------------------------------------------- /tests/static/single-3.11.conf: -------------------------------------------------------------------------------- 1 | [starter] 2 | mode = single 3 | address = 0.0.0.0 4 | port = 8528 5 | 6 | [auth] 7 | jwt-secret = /tests/static/keyfile 8 | 9 | [args] 10 | all.database.password = passwd 11 | all.database.extended-names = true 12 | all.javascript.allow-admin-execute = true 13 | -------------------------------------------------------------------------------- /tests/static/single-3.12.conf: -------------------------------------------------------------------------------- 1 | [starter] 2 | mode = single 3 | address = 0.0.0.0 4 | port = 8528 5 | 6 | [auth] 7 | jwt-secret = /tests/static/keyfile 8 | 9 | [args] 10 | all.database.password = passwd 11 | all.database.extended-names = true 12 | all.javascript.allow-admin-execute = true 13 | all.server.options-api = admin 14 | -------------------------------------------------------------------------------- /tests/test_analyzer.py: -------------------------------------------------------------------------------- 1 | from packaging import version 2 | 3 | from arango.exceptions import ( 4 | AnalyzerCreateError, 5 | AnalyzerDeleteError, 6 | AnalyzerGetError, 7 | AnalyzerListError, 8 | ) 9 | from tests.helpers import assert_raises, generate_analyzer_name 10 | 11 | 12 | def test_analyzer_management(db, bad_db, cluster, enterprise, db_version): 13 | analyzer_name = generate_analyzer_name() 14 | full_analyzer_name = db.name + "::" + analyzer_name 15 | bad_analyzer_name = generate_analyzer_name() 16 | 17 | # Test create identity analyzer 18 | result = db.create_analyzer(analyzer_name, "identity", {}) 19 | assert result["name"] == full_analyzer_name 20 | assert result["type"] == "identity" 21 | assert result["properties"] == {} 22 | assert result["features"] == [] 23 | 24 | # Test create delimiter analyzer 25 | result = db.create_analyzer( 26 | name=generate_analyzer_name(), 27 | analyzer_type="delimiter", 28 | properties={"delimiter": ","}, 29 | ) 30 | assert result["type"] == "delimiter" 31 | assert result["properties"] == {"delimiter": ","} 32 | assert result["features"] == [] 33 | 34 | # Test create duplicate with bad database 35 | with assert_raises(AnalyzerCreateError) as err: 36 | bad_db.create_analyzer(analyzer_name, "identity", {}, []) 37 | assert err.value.error_code in {11, 1228} 38 | 39 | # Test get analyzer 40 | result = db.analyzer(analyzer_name) 41 | assert result["name"] == full_analyzer_name 42 | assert result["type"] == "identity" 43 | assert result["properties"] == {} 44 | assert result["features"] == [] 45 | 46 | # Test get missing analyzer 47 | with assert_raises(AnalyzerGetError) as err: 48 | db.analyzer(bad_analyzer_name) 49 | assert err.value.error_code in {1202} 50 | 51 | # Test list analyzers 52 | result = db.analyzers() 53 | assert full_analyzer_name in [a["name"] for a in result] 54 | 55 | # Test list analyzers with bad database 56 | with assert_raises(AnalyzerListError) as err: 57 | bad_db.analyzers() 58 | assert err.value.error_code in {11, 1228} 59 | 60 | # Test delete analyzer 61 | assert db.delete_analyzer(analyzer_name, force=True) is True 62 | assert full_analyzer_name not in [a["name"] for a in db.analyzers()] 63 | 64 | # Test delete missing analyzer 65 | with assert_raises(AnalyzerDeleteError) as err: 66 | db.delete_analyzer(analyzer_name) 67 | assert err.value.error_code in {1202} 68 | 69 | # Test delete missing analyzer with ignore_missing set to True 70 | assert db.delete_analyzer(analyzer_name, ignore_missing=True) is False 71 | 72 | # Test create geo_s2 analyzer (EE only) 73 | if enterprise: 74 | analyzer_name = generate_analyzer_name() 75 | result = db.create_analyzer(analyzer_name, "geo_s2", {}) 76 | assert result["type"] == "geo_s2" 77 | assert result["features"] == [] 78 | assert result["properties"] == { 79 | "options": {"maxCells": 20, "minLevel": 4, "maxLevel": 23}, 80 | "type": "shape", 81 | "format": "latLngDouble", 82 | } 83 | assert db.delete_analyzer(analyzer_name) 84 | 85 | # Test create delimieter analyzer with multiple delimiters 86 | if db_version >= version.parse("3.12.0"): 87 | result = db.create_analyzer( 88 | name=generate_analyzer_name(), 89 | analyzer_type="multi_delimiter", 90 | properties={"delimiters": [",", "."]}, 91 | ) 92 | 93 | assert result["type"] == "multi_delimiter" 94 | assert result["properties"] == {"delimiters": [",", "."]} 95 | assert result["features"] == [] 96 | 97 | if db_version >= version.parse("3.12.0"): 98 | analyzer_name = generate_analyzer_name() 99 | result = db.create_analyzer(analyzer_name, "wildcard", {"ngramSize": 4}) 100 | assert result["type"] == "wildcard" 101 | assert result["features"] == [] 102 | assert result["properties"] == {"ngramSize": 4} 103 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | from arango.connection import BasicConnection, JwtConnection, JwtSuperuserConnection 2 | from arango.errno import FORBIDDEN, HTTP_UNAUTHORIZED 3 | from arango.exceptions import ( 4 | JWTAuthError, 5 | JWTExpiredError, 6 | JWTSecretListError, 7 | JWTSecretReloadError, 8 | ServerConnectionError, 9 | ServerEncryptionError, 10 | ServerTLSError, 11 | ServerTLSReloadError, 12 | ServerVersionError, 13 | ) 14 | from tests.helpers import assert_raises, generate_jwt, generate_string 15 | 16 | 17 | def test_auth_invalid_method(client, db_name, username, password): 18 | with assert_raises(ValueError) as err: 19 | client.db( 20 | name=db_name, 21 | username=username, 22 | password=password, 23 | verify=True, 24 | auth_method="bad_method", 25 | ) 26 | assert "invalid auth_method" in str(err.value) 27 | 28 | 29 | def test_auth_basic(client, db_name, username, password): 30 | db = client.db( 31 | name=db_name, 32 | username=username, 33 | password=password, 34 | verify=True, 35 | auth_method="basic", 36 | ) 37 | assert isinstance(db.conn, BasicConnection) 38 | assert isinstance(db.version(), str) 39 | assert isinstance(db.properties(), dict) 40 | 41 | 42 | def test_auth_jwt(client, db_name, username, password, secret): 43 | # Test JWT authentication with username and password. 44 | db = client.db( 45 | name=db_name, 46 | username=username, 47 | password=password, 48 | verify=True, 49 | auth_method="jwt", 50 | ) 51 | assert isinstance(db.conn, JwtConnection) 52 | assert isinstance(db.version(), str) 53 | assert isinstance(db.properties(), dict) 54 | 55 | bad_password = generate_string() 56 | with assert_raises(JWTAuthError) as err: 57 | client.db(db_name, username, bad_password, auth_method="jwt") 58 | assert err.value.error_code == HTTP_UNAUTHORIZED 59 | 60 | # Test JWT authentication with user token. 61 | token = generate_jwt(secret) 62 | db = client.db("_system", user_token=token) 63 | assert isinstance(db.conn, JwtConnection) 64 | assert isinstance(db.version(), str) 65 | assert isinstance(db.properties(), dict) 66 | 67 | 68 | # TODO re-examine commented out code 69 | def test_auth_superuser_token(client, db_name, root_password, secret): 70 | token = generate_jwt(secret) 71 | db = client.db("_system", superuser_token=token) 72 | bad_db = client.db("_system", superuser_token="bad_token") 73 | 74 | assert isinstance(db.conn, JwtSuperuserConnection) 75 | assert isinstance(db.version(), str) 76 | assert isinstance(db.properties(), dict) 77 | 78 | # # Test get JWT secrets 79 | # secrets = db.jwt_secrets() 80 | # assert 'active' in secrets 81 | # assert 'passive' in secrets 82 | 83 | # Test get JWT secrets with bad database 84 | with assert_raises(JWTSecretListError) as err: 85 | bad_db.jwt_secrets() 86 | assert err.value.error_code == FORBIDDEN 87 | 88 | # # Test reload JWT secrets 89 | # secrets = db.reload_jwt_secrets() 90 | # assert 'active' in secrets 91 | # assert 'passive' in secrets 92 | 93 | # Test reload JWT secrets with bad database 94 | with assert_raises(JWTSecretReloadError) as err: 95 | bad_db.reload_jwt_secrets() 96 | assert err.value.error_code == FORBIDDEN 97 | 98 | # Test get TLS data 99 | result = db.tls() 100 | assert isinstance(result, dict) 101 | 102 | # Test get TLS data with bad database 103 | with assert_raises(ServerTLSError) as err: 104 | bad_db.tls() 105 | assert err.value.error_code == FORBIDDEN 106 | 107 | # Test reload TLS 108 | result = db.reload_tls() 109 | assert isinstance(result, dict) 110 | 111 | # Test reload TLS with bad database 112 | with assert_raises(ServerTLSReloadError) as err: 113 | bad_db.reload_tls() 114 | assert err.value.error_code == FORBIDDEN 115 | 116 | # # Test get encryption 117 | # result = db.encryption() 118 | # assert isinstance(result, dict) 119 | 120 | # Test reload user-defined encryption keys. 121 | with assert_raises(ServerEncryptionError) as err: 122 | bad_db.encryption() 123 | assert err.value.error_code == FORBIDDEN 124 | 125 | 126 | def test_auth_jwt_expiry(client, db_name, root_password, secret): 127 | # Test automatic token refresh on expired token. 128 | db = client.db("_system", "root", root_password, auth_method="jwt") 129 | valid_token = generate_jwt(secret) 130 | expired_token = generate_jwt(secret, exp=-1000) 131 | db.conn._token = expired_token 132 | db.conn._auth_header = f"bearer {expired_token}" 133 | assert isinstance(db.version(), str) 134 | 135 | # Test expiry error on db instantiation (superuser) 136 | with assert_raises(ServerConnectionError) as err: 137 | client.db("_system", superuser_token=expired_token, verify=True) 138 | 139 | # Test expiry error on db version (superuser) 140 | db = client.db("_system", superuser_token=expired_token) 141 | with assert_raises(ServerVersionError) as err: 142 | db.version() 143 | assert err.value.error_code == FORBIDDEN 144 | 145 | # Test expiry error on set_token (superuser). 146 | db = client.db("_system", superuser_token=valid_token) 147 | with assert_raises(JWTExpiredError) as err: 148 | db.conn.set_token(expired_token) 149 | 150 | # Test expiry error on db instantiation (user) 151 | with assert_raises(JWTExpiredError) as err: 152 | db = client.db("_system", user_token=expired_token) 153 | 154 | # Test expiry error on set_token (user). 155 | db = client.db("_system", user_token=valid_token) 156 | with assert_raises(JWTExpiredError) as err: 157 | db.conn.set_token(expired_token) 158 | -------------------------------------------------------------------------------- /tests/test_backup.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from arango.errno import DATABASE_NOT_FOUND, FILE_NOT_FOUND, FORBIDDEN, HTTP_NOT_FOUND 4 | from arango.exceptions import ( 5 | BackupCreateError, 6 | BackupDeleteError, 7 | BackupDownloadError, 8 | BackupGetError, 9 | BackupRestoreError, 10 | BackupUploadError, 11 | ) 12 | from tests.helpers import assert_raises 13 | 14 | 15 | def test_backup_management(sys_db, bad_db, enterprise, cluster): 16 | if not enterprise: 17 | pytest.skip("Only for ArangoDB enterprise edition") 18 | 19 | # Test create backup "foo". 20 | result = sys_db.backup.create( 21 | label="foo", allow_inconsistent=True, force=False, timeout=1000 22 | ) 23 | assert "backup_id" in result 24 | assert "datetime" in result 25 | 26 | backup_id_foo = result["backup_id"] 27 | assert backup_id_foo.endswith("foo") 28 | 29 | # Test create backup "bar". 30 | result = sys_db.backup.create( 31 | label="bar", allow_inconsistent=True, force=False, timeout=1000 32 | ) 33 | assert "backup_id" in result 34 | assert "datetime" in result 35 | 36 | backup_id_bar = result["backup_id"] 37 | assert backup_id_bar.endswith("bar") 38 | 39 | # Test create backup with bad database. 40 | with assert_raises(BackupCreateError) as err: 41 | bad_db.backup.create() 42 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 43 | 44 | # Test get backup. 45 | result = sys_db.backup.get() 46 | assert len(result["list"]) == 2 47 | 48 | result = sys_db.backup.get(backup_id_foo) 49 | assert len(result["list"]) == 1 50 | assert all(backup_id.endswith("foo") for backup_id in result["list"]) 51 | 52 | result = sys_db.backup.get(backup_id_bar) 53 | assert len(result["list"]) == 1 54 | assert all(backup_id.endswith("bar") for backup_id in result["list"]) 55 | 56 | # Test get backup with bad database. 57 | with assert_raises(BackupGetError) as err: 58 | bad_db.backup.get() 59 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 60 | 61 | # Test upload backup. 62 | backup_id = backup_id_foo if cluster else backup_id_bar 63 | result = sys_db.backup.upload( 64 | backup_id=backup_id, 65 | repository="local://tmp/backups", 66 | config={"local": {"type": "local"}}, 67 | ) 68 | assert "upload_id" in result 69 | 70 | # Test upload backup abort. 71 | assert isinstance( 72 | sys_db.backup.upload(upload_id=result["upload_id"], abort=False), 73 | (str, dict), 74 | ) 75 | 76 | # Test upload backup with bad database. 77 | with assert_raises(BackupUploadError) as err: 78 | bad_db.backup.upload(upload_id=result["upload_id"]) 79 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 80 | 81 | # Test download backup. 82 | result = sys_db.backup.download( 83 | backup_id=backup_id_foo, 84 | repository="local://tmp/backups", 85 | config={"local": {"type": "local"}}, 86 | ) 87 | assert "download_id" in result 88 | 89 | # Test download backup abort. 90 | assert isinstance( 91 | sys_db.backup.download(download_id=result["download_id"], abort=False), 92 | (str, dict), 93 | ) 94 | 95 | # Test download backup with bad database. 96 | with assert_raises(BackupDownloadError) as err: 97 | bad_db.backup.download(download_id=result["download_id"]) 98 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 99 | 100 | # Test restore backup. 101 | result = sys_db.backup.restore(backup_id_foo) 102 | assert isinstance(result, dict) 103 | 104 | # Test restore backup with bad database. 105 | with assert_raises(BackupRestoreError) as err: 106 | bad_db.backup.restore(backup_id_foo) 107 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 108 | 109 | # Test delete backup. 110 | assert sys_db.backup.delete(backup_id_foo) is True 111 | assert sys_db.backup.delete(backup_id_bar) is True 112 | 113 | # Test delete missing backup. 114 | with assert_raises(BackupDeleteError) as err: 115 | sys_db.backup.delete(backup_id_foo) 116 | if cluster: 117 | assert err.value.error_code == HTTP_NOT_FOUND 118 | else: 119 | assert err.value.error_code == FILE_NOT_FOUND 120 | -------------------------------------------------------------------------------- /tests/test_batch.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from arango.database import BatchDatabase 4 | from arango.exceptions import BatchJobResultError, BatchStateError, DocumentInsertError 5 | from arango.job import BatchJob 6 | from tests.helpers import clean_doc, extract 7 | 8 | 9 | def test_batch_wrapper_attributes(db, col, username): 10 | batch_db = db.begin_batch_execution() 11 | assert isinstance(batch_db, BatchDatabase) 12 | assert batch_db.username == username 13 | assert batch_db.context == "batch" 14 | assert batch_db.db_name == db.name 15 | assert batch_db.name == db.name 16 | assert repr(batch_db) == f"" 17 | 18 | batch_col = batch_db.collection(col.name) 19 | assert batch_col.username == username 20 | assert batch_col.context == "batch" 21 | assert batch_col.db_name == db.name 22 | assert batch_col.name == col.name 23 | 24 | batch_aql = batch_db.aql 25 | assert batch_aql.username == username 26 | assert batch_aql.context == "batch" 27 | assert batch_aql.db_name == db.name 28 | 29 | job = batch_aql.execute("INVALID QUERY") 30 | assert isinstance(job, BatchJob) 31 | assert isinstance(job.id, str) 32 | assert repr(job) == f"" 33 | 34 | 35 | def test_batch_execute_without_result(db, col, docs): 36 | with db.begin_batch_execution(return_result=False) as batch_db: 37 | batch_col = batch_db.collection(col.name) 38 | 39 | # Ensure that no jobs are returned 40 | assert batch_col.insert(docs[0]) is None 41 | assert batch_col.delete(docs[0]) is None 42 | assert batch_col.insert(docs[1]) is None 43 | assert batch_col.delete(docs[1]) is None 44 | assert batch_col.insert(docs[2]) is None 45 | assert batch_col.get(docs[2]) is None 46 | assert batch_db.queued_jobs() is None 47 | 48 | # Ensure that the operations went through 49 | assert batch_db.queued_jobs() is None 50 | assert extract("_key", col.all()) == [docs[2]["_key"]] 51 | 52 | 53 | def test_batch_execute_with_result(db, col, docs): 54 | with db.begin_batch_execution(return_result=True) as batch_db: 55 | batch_col = batch_db.collection(col.name) 56 | job1 = batch_col.insert(docs[0]) 57 | job2 = batch_col.insert(docs[1]) 58 | job3 = batch_col.insert(docs[1]) # duplicate 59 | jobs = batch_db.queued_jobs() 60 | assert jobs == [job1, job2, job3] 61 | assert all(job.status() == "pending" for job in jobs) 62 | 63 | assert batch_db.queued_jobs() == [job1, job2, job3] 64 | assert all(job.status() == "done" for job in batch_db.queued_jobs()) 65 | assert extract("_key", col.all()) == extract("_key", docs[:2]) 66 | 67 | # Test successful results 68 | assert job1.result()["_key"] == docs[0]["_key"] 69 | 70 | # Test insert error result 71 | # job2 and job3 are concurrent, either one can fail 72 | with pytest.raises(DocumentInsertError) as err: 73 | job2.result() 74 | job3.result() 75 | assert err.value.error_code == 1210 76 | 77 | 78 | def test_batch_empty_commit(db): 79 | batch_db = db.begin_batch_execution(return_result=False) 80 | assert batch_db.commit() is None 81 | 82 | batch_db = db.begin_batch_execution(return_result=True) 83 | assert batch_db.commit() == [] 84 | 85 | 86 | def test_batch_double_commit(db, col, docs): 87 | batch_db = db.begin_batch_execution() 88 | job = batch_db.collection(col.name).insert(docs[0]) 89 | 90 | # Test first commit 91 | assert batch_db.commit() == [job] 92 | assert job.status() == "done" 93 | assert len(col) == 1 94 | assert clean_doc(col.random()) == docs[0] 95 | 96 | # Test second commit which should fail 97 | with pytest.raises(BatchStateError) as err: 98 | batch_db.commit() 99 | assert "already committed" in str(err.value) 100 | assert len(col) == 1 101 | assert clean_doc(col.random()) == docs[0] 102 | 103 | 104 | def test_batch_action_after_commit(db, col): 105 | with db.begin_batch_execution() as batch_db: 106 | batch_db.collection(col.name).insert({}) 107 | 108 | # Test insert after the batch has been committed 109 | with pytest.raises(BatchStateError) as err: 110 | batch_db.collection(col.name).insert({}) 111 | assert "already committed" in str(err.value) 112 | assert len(col) == 1 113 | 114 | 115 | def test_batch_execute_error(bad_db, col, docs): 116 | batch_db = bad_db.begin_batch_execution(return_result=True) 117 | job = batch_db.collection(col.name).insert_many(docs) 118 | batch_db.commit() 119 | assert len(col) == 0 120 | assert job.status() == "done" 121 | 122 | 123 | def test_batch_job_result_not_ready(db, col, docs): 124 | batch_db = db.begin_batch_execution(return_result=True) 125 | job = batch_db.collection(col.name).insert_many(docs) 126 | 127 | # Test get job result before commit 128 | with pytest.raises(BatchJobResultError) as err: 129 | job.result() 130 | assert str(err.value) == "result not available yet" 131 | 132 | # Test commit to make sure it still works after the errors 133 | assert batch_db.commit() == [job] 134 | assert len(job.result()) == len(docs) 135 | assert extract("_key", col.all()) == extract("_key", docs) 136 | -------------------------------------------------------------------------------- /tests/test_exception.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | 5 | from arango.exceptions import ( 6 | ArangoClientError, 7 | ArangoServerError, 8 | DocumentInsertError, 9 | DocumentParseError, 10 | ) 11 | from arango.request import Request 12 | from arango.response import Response 13 | 14 | 15 | def test_server_error(client, col, docs): 16 | document = docs[0] 17 | with pytest.raises(DocumentInsertError) as err: 18 | col.insert(document, return_new=False) 19 | col.insert(document, return_new=False) # duplicate key error 20 | exc = err.value 21 | 22 | assert isinstance(exc, ArangoServerError) 23 | assert exc.source == "server" 24 | assert exc.message == str(exc) 25 | assert exc.message.startswith("[HTTP 409][ERR 1210] unique constraint") 26 | assert exc.error_code == 1210 27 | assert exc.http_method == "post" 28 | assert exc.http_code == 409 29 | 30 | resp = exc.response 31 | expected_body = { 32 | "code": exc.http_code, 33 | "error": True, 34 | "errorNum": exc.error_code, 35 | "errorMessage": exc.error_message, 36 | } 37 | assert isinstance(resp, Response) 38 | assert resp.is_success is False 39 | assert resp.error_code == exc.error_code 40 | assert resp.body == expected_body 41 | assert resp.error_code == 1210 42 | assert resp.method == "post" 43 | assert resp.status_code == 409 44 | assert resp.status_text == "Conflict" 45 | 46 | assert json.loads(resp.raw_body) == expected_body 47 | assert resp.headers == exc.http_headers 48 | 49 | req = exc.request 50 | assert isinstance(req, Request) 51 | assert req.headers["content-type"] == "application/json" 52 | assert req.method == "post" 53 | assert req.params["silent"] == "0" 54 | assert req.params["returnNew"] == "0" 55 | assert req.data == document 56 | assert req.endpoint.startswith("/_api/document/" + col.name) 57 | 58 | 59 | def test_client_error(col): 60 | with pytest.raises(DocumentParseError) as err: 61 | col.get({"_id": "invalid"}) # malformed document 62 | exc = err.value 63 | 64 | assert isinstance(exc, ArangoClientError) 65 | assert exc.source == "client" 66 | assert exc.error_code is None 67 | assert exc.error_message is None 68 | assert exc.message == str(exc) 69 | assert exc.message.startswith("bad collection name") 70 | assert exc.url is None 71 | assert exc.http_method is None 72 | assert exc.http_code is None 73 | assert exc.http_headers is None 74 | assert exc.response is None 75 | -------------------------------------------------------------------------------- /tests/test_overload.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from arango import errno 4 | from arango.exceptions import OverloadControlExecutorError 5 | 6 | 7 | def flood_with_requests(controlled_db, async_db): 8 | """ 9 | Flood the database with requests. 10 | It is impossible to predict what the last recorded queue time will be. 11 | We can only try and make it as large as possible. However, if the system 12 | is fast enough, it may still be 0. 13 | """ 14 | controlled_db.aql.execute("RETURN SLEEP(0.5)", count=True) 15 | for _ in range(3): 16 | for _ in range(500): 17 | async_db.aql.execute("RETURN SLEEP(0.5)", count=True) 18 | controlled_db.aql.execute("RETURN SLEEP(0.5)", count=True) 19 | if controlled_db.last_queue_time >= 0: 20 | break 21 | 22 | 23 | def test_overload_control(db): 24 | controlled_db = db.begin_controlled_execution(100) 25 | assert controlled_db.max_queue_time == 100 26 | 27 | async_db = db.begin_async_execution(return_result=True) 28 | 29 | flood_with_requests(controlled_db, async_db) 30 | assert controlled_db.last_queue_time >= 0 31 | 32 | # We can only emit a warning here. The test will still pass. 33 | if controlled_db.last_queue_time == 0: 34 | warnings.warn( 35 | f"last_queue_time of {controlled_db} is 0, test may be unreliable" 36 | ) 37 | 38 | controlled_db.adjust_max_queue_time(0.0001) 39 | try: 40 | flood_with_requests(controlled_db, async_db) 41 | assert controlled_db.last_queue_time >= 0 42 | except OverloadControlExecutorError as e: 43 | assert e.http_code == errno.HTTP_PRECONDITION_FAILED 44 | assert e.error_code == errno.QUEUE_TIME_REQUIREMENT_VIOLATED 45 | else: 46 | warnings.warn( 47 | f"last_queue_time of {controlled_db} is {controlled_db.last_queue_time}," 48 | f"test may be unreliable" 49 | ) 50 | -------------------------------------------------------------------------------- /tests/test_permission.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from arango.exceptions import ( 4 | CollectionCreateError, 5 | CollectionListError, 6 | CollectionPropertiesError, 7 | DocumentInsertError, 8 | PermissionGetError, 9 | PermissionListError, 10 | PermissionResetError, 11 | PermissionUpdateError, 12 | ) 13 | from tests.helpers import ( 14 | assert_raises, 15 | extract, 16 | generate_col_name, 17 | generate_db_name, 18 | generate_string, 19 | generate_username, 20 | ) 21 | 22 | 23 | def test_permission_management(client, sys_db, bad_db, cluster): 24 | if cluster: 25 | pytest.skip("Not tested in a cluster setup") 26 | 27 | username = generate_username() 28 | password = generate_string() 29 | db_name = generate_db_name() 30 | col_name_1 = generate_col_name() 31 | col_name_2 = generate_col_name() 32 | 33 | sys_db.create_database( 34 | name=db_name, 35 | users=[{"username": username, "password": password, "active": True}], 36 | ) 37 | db = client.db(db_name, username, password) 38 | assert isinstance(sys_db.permissions(username), dict) 39 | 40 | # Test list permissions with bad database 41 | with assert_raises(PermissionListError) as err: 42 | bad_db.permissions(username) 43 | assert err.value.error_code in {11, 1228} 44 | 45 | # Test get permission with bad database 46 | with assert_raises(PermissionGetError) as err: 47 | bad_db.permission(username, db_name) 48 | assert err.value.error_code in {11, 1228} 49 | 50 | # The user should not have read and write permissions 51 | assert sys_db.permission(username, db_name) == "rw" 52 | assert sys_db.permission(username, db_name, col_name_1) == "rw" 53 | 54 | # Test update permission (database level) with bad database 55 | with assert_raises(PermissionUpdateError): 56 | bad_db.update_permission(username, "ro", db_name) 57 | assert sys_db.permission(username, db_name) == "rw" 58 | 59 | # Test update permission (database level) to read only and verify access 60 | assert sys_db.update_permission(username, "ro", db_name) is True 61 | assert sys_db.permission(username, db_name) == "ro" 62 | with assert_raises(CollectionCreateError) as err: 63 | db.create_collection(col_name_2) 64 | assert err.value.http_code == 403 65 | assert col_name_1 not in extract("name", db.collections()) 66 | assert col_name_2 not in extract("name", db.collections()) 67 | 68 | # Test reset permission (database level) with bad database 69 | with assert_raises(PermissionResetError) as err: 70 | bad_db.reset_permission(username, db_name) 71 | assert err.value.error_code in {11, 1228} 72 | assert sys_db.permission(username, db_name) == "ro" 73 | 74 | # Test reset permission (database level) and verify access 75 | assert sys_db.reset_permission(username, db_name) is True 76 | assert sys_db.permission(username, db_name) == "none" 77 | with assert_raises(CollectionCreateError) as err: 78 | db.create_collection(col_name_1) 79 | assert err.value.http_code == 401 80 | with assert_raises(CollectionListError) as err: 81 | db.collections() 82 | assert err.value.http_code == 401 83 | 84 | # Test update permission (database level) and verify access 85 | assert sys_db.update_permission(username, "rw", db_name) is True 86 | assert sys_db.permission(username, db_name, col_name_2) == "rw" 87 | assert db.create_collection(col_name_1) is not None 88 | assert db.create_collection(col_name_2) is not None 89 | assert col_name_1 in extract("name", db.collections()) 90 | assert col_name_2 in extract("name", db.collections()) 91 | 92 | col_1 = db.collection(col_name_1) 93 | col_2 = db.collection(col_name_2) 94 | 95 | # Verify that user has read and write access to both collections 96 | assert isinstance(col_1.properties(), dict) 97 | assert isinstance(col_1.insert({}), dict) 98 | assert isinstance(col_2.properties(), dict) 99 | assert isinstance(col_2.insert({}), dict) 100 | 101 | # Test update permission (collection level) to read only and verify access 102 | assert sys_db.update_permission(username, "ro", db_name, col_name_1) 103 | assert sys_db.permission(username, db_name, col_name_1) == "ro" 104 | assert isinstance(col_1.properties(), dict) 105 | with assert_raises(DocumentInsertError) as err: 106 | col_1.insert({}) 107 | assert err.value.http_code == 403 108 | assert isinstance(col_2.properties(), dict) 109 | assert isinstance(col_2.insert({}), dict) 110 | 111 | # Test update permission (collection level) to none and verify access 112 | assert sys_db.update_permission(username, "none", db_name, col_name_1) 113 | assert sys_db.permission(username, db_name, col_name_1) == "none" 114 | with assert_raises(CollectionPropertiesError) as err: 115 | col_1.properties() 116 | assert err.value.http_code == 403 117 | with assert_raises(DocumentInsertError) as err: 118 | col_1.insert({}) 119 | assert err.value.http_code == 403 120 | assert isinstance(col_2.properties(), dict) 121 | assert isinstance(col_2.insert({}), dict) 122 | 123 | # Test reset permission (collection level) 124 | assert sys_db.reset_permission(username, db_name, col_name_1) is True 125 | assert sys_db.permission(username, db_name, col_name_1) == "rw" 126 | assert isinstance(col_1.properties(), dict) 127 | assert isinstance(col_1.insert({}), dict) 128 | assert isinstance(col_2.properties(), dict) 129 | assert isinstance(col_2.insert({}), dict) 130 | -------------------------------------------------------------------------------- /tests/test_pregel.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pytest 4 | from packaging import version 5 | 6 | from arango.exceptions import PregelJobCreateError, PregelJobDeleteError 7 | from tests.helpers import assert_raises, generate_string 8 | 9 | 10 | def test_pregel_attributes(db, db_version, username): 11 | if db_version >= version.parse("3.12.0"): 12 | pytest.skip("Pregel is not tested in 3.12.0+") 13 | 14 | assert db.pregel.context in ["default", "async", "batch", "transaction"] 15 | assert db.pregel.username == username 16 | assert db.pregel.db_name == db.name 17 | assert repr(db.pregel) == f"" 18 | 19 | 20 | def test_pregel_management(db, db_version, graph, cluster): 21 | if db_version >= version.parse("3.12.0"): 22 | pytest.skip("Pregel is not tested in 3.12.0+") 23 | 24 | if cluster: 25 | pytest.skip("Not tested in a cluster setup") 26 | 27 | # Test create pregel job 28 | job_id = db.pregel.create_job( 29 | graph.name, 30 | "pagerank", 31 | store=False, 32 | max_gss=100, 33 | thread_count=1, 34 | async_mode=False, 35 | result_field="result", 36 | algorithm_params={"threshold": 0.000001}, 37 | ) 38 | assert isinstance(job_id, int) 39 | 40 | # Test create pregel job with unsupported algorithm 41 | with assert_raises(PregelJobCreateError) as err: 42 | db.pregel.create_job(graph.name, "invalid") 43 | assert err.value.error_code in {4, 10, 1600} 44 | 45 | # Test get existing pregel job 46 | job = db.pregel.job(job_id) 47 | assert isinstance(job["state"], str) 48 | assert isinstance(job["aggregators"], dict) 49 | assert isinstance(job["gss"], int) 50 | assert isinstance(job["received_count"], int) 51 | assert isinstance(job["send_count"], int) 52 | assert "total_runtime" in job 53 | 54 | # Test delete existing pregel job 55 | assert db.pregel.delete_job(job_id) is True 56 | time.sleep(0.2) 57 | job = db.pregel.job(job_id) 58 | assert job["state"] == "canceled" 59 | 60 | # Test delete missing pregel job 61 | with assert_raises(PregelJobDeleteError) as err: 62 | db.pregel.delete_job(generate_string()) 63 | assert err.value.error_code in {4, 10, 1600} 64 | -------------------------------------------------------------------------------- /tests/test_request.py: -------------------------------------------------------------------------------- 1 | from arango.request import Request 2 | 3 | 4 | def test_request_no_data() -> None: 5 | request = Request( 6 | method="post", 7 | endpoint="/_api/test", 8 | params={"bool": True}, 9 | headers={"foo": "bar"}, 10 | ) 11 | assert request.method == "post" 12 | assert request.endpoint == "/_api/test" 13 | assert request.params == {"bool": "1"} 14 | assert request.headers["charset"] == "utf-8" 15 | assert request.headers["content-type"] == "application/json" 16 | assert request.headers["foo"] == "bar" 17 | assert request.data is None 18 | 19 | 20 | def test_request_string_data() -> None: 21 | request = Request( 22 | method="post", 23 | endpoint="/_api/test", 24 | params={"bool": True}, 25 | headers={"foo": "bar"}, 26 | data="test", 27 | ) 28 | assert request.method == "post" 29 | assert request.endpoint == "/_api/test" 30 | assert request.params == {"bool": "1"} 31 | assert request.headers["charset"] == "utf-8" 32 | assert request.headers["content-type"] == "application/json" 33 | assert request.headers["foo"] == "bar" 34 | assert request.data == "test" 35 | 36 | 37 | def test_request_json_data() -> None: 38 | request = Request( 39 | method="post", 40 | endpoint="/_api/test", 41 | params={"bool": True}, 42 | headers={"foo": "bar"}, 43 | data={"baz": "qux"}, 44 | ) 45 | assert request.method == "post" 46 | assert request.endpoint == "/_api/test" 47 | assert request.params == {"bool": "1"} 48 | assert request.headers["charset"] == "utf-8" 49 | assert request.headers["content-type"] == "application/json" 50 | assert request.headers["foo"] == "bar" 51 | assert request.data == {"baz": "qux"} 52 | 53 | 54 | def test_request_transaction_data() -> None: 55 | request = Request( 56 | method="post", 57 | endpoint="/_api/test", 58 | params={"bool": True}, 59 | headers={"foo": "bar"}, 60 | data={"baz": "qux"}, 61 | ) 62 | assert request.method == "post" 63 | assert request.endpoint == "/_api/test" 64 | assert request.params == {"bool": "1"} 65 | assert request.headers["charset"] == "utf-8" 66 | assert request.headers["content-type"] == "application/json" 67 | assert request.headers["foo"] == "bar" 68 | assert request.data == {"baz": "qux"} 69 | -------------------------------------------------------------------------------- /tests/test_resolver.py: -------------------------------------------------------------------------------- 1 | from typing import Set 2 | 3 | import pytest 4 | 5 | from arango.resolver import ( 6 | FallbackHostResolver, 7 | PeriodicHostResolver, 8 | RandomHostResolver, 9 | RoundRobinHostResolver, 10 | SingleHostResolver, 11 | ) 12 | 13 | 14 | def test_bad_resolver(): 15 | with pytest.raises(ValueError): 16 | RandomHostResolver(3, 2) 17 | 18 | 19 | def test_resolver_single_host(): 20 | resolver = SingleHostResolver() 21 | for _ in range(20): 22 | assert resolver.get_host_index() == 0 23 | 24 | 25 | def test_resolver_random_host(): 26 | resolver = RandomHostResolver(10) 27 | for _ in range(20): 28 | assert 0 <= resolver.get_host_index() < 10 29 | 30 | resolver = RandomHostResolver(3) 31 | indexes_to_filter: Set[int] = set() 32 | 33 | index_a = resolver.get_host_index() 34 | indexes_to_filter.add(index_a) 35 | 36 | index_b = resolver.get_host_index(indexes_to_filter) 37 | indexes_to_filter.add(index_b) 38 | assert index_b != index_a 39 | 40 | index_c = resolver.get_host_index(indexes_to_filter) 41 | indexes_to_filter.clear() 42 | indexes_to_filter.add(index_c) 43 | assert index_c not in [index_a, index_b] 44 | 45 | 46 | def test_resolver_round_robin(): 47 | resolver = RoundRobinHostResolver(10) 48 | assert resolver.get_host_index() == 0 49 | assert resolver.get_host_index() == 1 50 | assert resolver.get_host_index() == 2 51 | assert resolver.get_host_index() == 3 52 | assert resolver.get_host_index() == 4 53 | assert resolver.get_host_index() == 5 54 | assert resolver.get_host_index() == 6 55 | assert resolver.get_host_index() == 7 56 | assert resolver.get_host_index() == 8 57 | assert resolver.get_host_index() == 9 58 | assert resolver.get_host_index() == 0 59 | 60 | 61 | def test_resolver_periodic(): 62 | resolver = PeriodicHostResolver(3, requests_period=3) 63 | assert resolver.get_host_index() == 0 64 | assert resolver.get_host_index() == 0 65 | assert resolver.get_host_index() == 1 66 | assert resolver.get_host_index() == 1 67 | assert resolver.get_host_index() == 1 68 | assert resolver.get_host_index() == 2 69 | assert resolver.get_host_index() == 2 70 | assert resolver.get_host_index() == 2 71 | assert resolver.get_host_index() == 0 72 | assert resolver.get_host_index() == 0 73 | assert resolver.get_host_index({0}) == 1 74 | assert resolver.get_host_index() == 1 75 | 76 | 77 | def test_resolver_fallback(): 78 | resolver = FallbackHostResolver(4) 79 | assert resolver.get_host_index() == 0 80 | assert resolver.get_host_index() == 0 81 | assert resolver.get_host_index({0, 1, 3}) == 2 82 | assert resolver.get_host_index({1, 2, 3}) == 0 83 | assert resolver.get_host_index({0}) == 1 84 | assert resolver.get_host_index({0}) == 1 85 | assert resolver.get_host_index() == 1 86 | -------------------------------------------------------------------------------- /tests/test_response.py: -------------------------------------------------------------------------------- 1 | from arango.response import Response 2 | 3 | 4 | def test_response(conn): 5 | response = Response( 6 | method="get", 7 | url="test_url", 8 | headers={"foo": "bar"}, 9 | status_text="baz", 10 | status_code=200, 11 | raw_body="true", 12 | ) 13 | conn.prep_response(response) 14 | 15 | assert response.method == "get" 16 | assert response.url == "test_url" 17 | assert response.headers == {"foo": "bar"} 18 | assert response.status_code == 200 19 | assert response.status_text == "baz" 20 | assert response.raw_body == "true" 21 | assert response.body is True 22 | assert response.error_code is None 23 | assert response.error_message is None 24 | assert response.is_success is True 25 | 26 | test_body = '{"errorNum": 1, "errorMessage": "qux"}' 27 | response = Response( 28 | method="get", 29 | url="test_url", 30 | headers={"foo": "bar"}, 31 | status_text="baz", 32 | status_code=200, 33 | raw_body=test_body, 34 | ) 35 | conn.prep_response(response) 36 | 37 | assert response.method == "get" 38 | assert response.url == "test_url" 39 | assert response.headers == {"foo": "bar"} 40 | assert response.status_code == 200 41 | assert response.status_text == "baz" 42 | assert response.raw_body == test_body 43 | assert response.body == {"errorMessage": "qux", "errorNum": 1} 44 | assert response.error_code == 1 45 | assert response.error_message == "qux" 46 | assert response.is_success is False 47 | -------------------------------------------------------------------------------- /tests/test_task.py: -------------------------------------------------------------------------------- 1 | from arango.exceptions import ( 2 | TaskCreateError, 3 | TaskDeleteError, 4 | TaskGetError, 5 | TaskListError, 6 | ) 7 | from tests.helpers import assert_raises, extract, generate_task_id, generate_task_name 8 | 9 | 10 | def test_task_management(sys_db, db, bad_db): 11 | test_command = 'require("@arangodb").print(params);' 12 | 13 | # Test create task with random ID 14 | task_name = generate_task_name() 15 | new_task = db.create_task( 16 | name=task_name, 17 | command=test_command, 18 | params={"foo": 1, "bar": 2}, 19 | offset=1, 20 | ) 21 | assert new_task["name"] == task_name 22 | assert "print(params)" in new_task["command"] 23 | assert new_task["type"] == "timed" 24 | assert new_task["database"] == db.name 25 | assert isinstance(new_task["created"], float) 26 | assert isinstance(new_task["id"], str) 27 | 28 | # Test get existing task 29 | assert db.task(new_task["id"]) == new_task 30 | 31 | # Test create task with specific ID 32 | task_name = generate_task_name() 33 | task_id = generate_task_id() 34 | new_task = db.create_task( 35 | name=task_name, 36 | command=test_command, 37 | params={"foo": 1, "bar": 2}, 38 | offset=1, 39 | period=10, 40 | task_id=task_id, 41 | ) 42 | assert new_task["name"] == task_name 43 | assert new_task["id"] == task_id 44 | assert "print(params)" in new_task["command"] 45 | assert new_task["type"] == "periodic" 46 | assert new_task["database"] == db.name 47 | assert isinstance(new_task["created"], float) 48 | assert db.task(new_task["id"]) == new_task 49 | 50 | # Test create duplicate task 51 | with assert_raises(TaskCreateError) as err: 52 | db.create_task( 53 | name=task_name, 54 | command=test_command, 55 | params={"foo": 1, "bar": 2}, 56 | task_id=task_id, 57 | ) 58 | assert err.value.error_code == 1851 59 | 60 | # Test list tasks 61 | for task in sys_db.tasks(): 62 | assert task["type"] in {"periodic", "timed"} 63 | assert isinstance(task["id"], str) 64 | assert isinstance(task["name"], str) 65 | assert isinstance(task["created"], float) 66 | assert isinstance(task["command"], str) 67 | 68 | # Test list tasks with bad database 69 | with assert_raises(TaskListError) as err: 70 | bad_db.tasks() 71 | assert err.value.error_code in {11, 1228} 72 | 73 | # Test get missing task 74 | with assert_raises(TaskGetError) as err: 75 | db.task(generate_task_id()) 76 | assert err.value.error_code == 1852 77 | 78 | # Test delete existing task 79 | assert task_id in extract("id", db.tasks()) 80 | assert db.delete_task(task_id) is True 81 | assert task_id not in extract("id", db.tasks()) 82 | with assert_raises(TaskGetError) as err: 83 | db.task(task_id) 84 | assert err.value.error_code == 1852 85 | 86 | # Test delete missing task 87 | with assert_raises(TaskDeleteError) as err: 88 | db.delete_task(generate_task_id(), ignore_missing=False) 89 | assert err.value.error_code == 1852 90 | assert db.delete_task(task_id, ignore_missing=True) is False 91 | -------------------------------------------------------------------------------- /tests/test_user.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from arango.exceptions import ( 4 | DatabasePropertiesError, 5 | UserCreateError, 6 | UserDeleteError, 7 | UserGetError, 8 | UserListError, 9 | UserReplaceError, 10 | UserUpdateError, 11 | ) 12 | from tests.helpers import ( 13 | assert_raises, 14 | extract, 15 | generate_db_name, 16 | generate_string, 17 | generate_username, 18 | ) 19 | 20 | 21 | def test_user_management(sys_db, bad_db): 22 | # Test create user 23 | username = generate_username() 24 | password = generate_string() 25 | assert not sys_db.has_user(username) 26 | 27 | new_user = sys_db.create_user( 28 | username=username, 29 | password=password, 30 | active=True, 31 | extra={"foo": "bar"}, 32 | ) 33 | assert new_user["username"] == username 34 | assert new_user["active"] is True 35 | assert new_user["extra"] == {"foo": "bar"} 36 | assert sys_db.has_user(username) 37 | 38 | # Test create duplicate user 39 | with assert_raises(UserCreateError) as err: 40 | sys_db.create_user(username=username, password=password) 41 | assert err.value.error_code == 1702 42 | 43 | # Test list users 44 | for user in sys_db.users(): 45 | assert isinstance(user["username"], str) 46 | assert isinstance(user["active"], bool) 47 | assert isinstance(user["extra"], dict) 48 | assert sys_db.user(username) == new_user 49 | 50 | # Test list users with bad database 51 | with assert_raises(UserListError) as err: 52 | bad_db.users() 53 | assert err.value.error_code in {11, 1228} 54 | 55 | # Test has user with bad database 56 | with assert_raises(UserListError) as err: 57 | bad_db.has_user(username) 58 | assert err.value.error_code in {11, 1228} 59 | 60 | # Test get user 61 | users = sys_db.users() 62 | for user in users: 63 | assert "active" in user 64 | assert "extra" in user 65 | assert "username" in user 66 | assert username in extract("username", sys_db.users()) 67 | 68 | # Test get missing user 69 | with assert_raises(UserGetError) as err: 70 | sys_db.user(generate_username()) 71 | assert err.value.error_code == 1703 72 | 73 | # Update existing user 74 | new_user = sys_db.update_user( 75 | username=username, 76 | password=password, 77 | active=False, 78 | extra={"bar": "baz"}, 79 | ) 80 | assert new_user["username"] == username 81 | assert new_user["active"] is False 82 | assert new_user["extra"] == {"foo": "bar", "bar": "baz"} 83 | assert sys_db.user(username) == new_user 84 | 85 | # Update missing user 86 | with assert_raises(UserUpdateError) as err: 87 | sys_db.update_user(username=generate_username(), password=generate_string()) 88 | assert err.value.error_code == 1703 89 | 90 | # Replace existing user 91 | new_user = sys_db.replace_user( 92 | username=username, 93 | password=password, 94 | active=False, 95 | extra={"baz": "qux"}, 96 | ) 97 | assert new_user["username"] == username 98 | assert new_user["active"] is False 99 | assert new_user["extra"] == {"baz": "qux"} 100 | assert sys_db.user(username) == new_user 101 | 102 | # Replace missing user 103 | with assert_raises(UserReplaceError) as err: 104 | sys_db.replace_user(username=generate_username(), password=generate_string()) 105 | assert err.value.error_code == 1703 106 | 107 | # Delete an existing user 108 | assert sys_db.delete_user(username) is True 109 | 110 | # Delete a missing user 111 | with assert_raises(UserDeleteError) as err: 112 | sys_db.delete_user(username, ignore_missing=False) 113 | assert err.value.error_code == 1703 114 | assert sys_db.delete_user(username, ignore_missing=True) is False 115 | 116 | 117 | def test_user_change_password(client, sys_db, cluster): 118 | if cluster: 119 | pytest.skip("Not tested in a cluster setup") 120 | 121 | username = generate_username() 122 | password1 = generate_string() 123 | password2 = generate_string() 124 | 125 | sys_db.create_user(username, password1) 126 | sys_db.update_permission(username, "rw", sys_db.name) 127 | 128 | db1 = client.db(sys_db.name, username, password1) 129 | db2 = client.db(sys_db.name, username, password2) 130 | 131 | # Check authentication 132 | assert isinstance(db1.properties(), dict) 133 | with assert_raises(DatabasePropertiesError) as err: 134 | db2.properties() 135 | assert err.value.http_code == 401 136 | 137 | # Update the user password and check again 138 | sys_db.update_user(username, password2) 139 | assert isinstance(db2.properties(), dict) 140 | with assert_raises(DatabasePropertiesError) as err: 141 | db1.properties() 142 | assert err.value.http_code == 401 143 | 144 | # Replace the user password back and check again 145 | sys_db.update_user(username, password1) 146 | assert isinstance(db1.properties(), dict) 147 | with assert_raises(DatabasePropertiesError) as err: 148 | db2.properties() 149 | assert err.value.http_code == 401 150 | 151 | 152 | def test_user_create_with_new_database(client, sys_db, cluster): 153 | if cluster: 154 | pytest.skip("Not tested in a cluster setup") 155 | 156 | db_name = generate_db_name() 157 | 158 | username1 = generate_username() 159 | username2 = generate_username() 160 | username3 = generate_username() 161 | 162 | password1 = generate_string() 163 | password2 = generate_string() 164 | password3 = generate_string() 165 | 166 | result = sys_db.create_database( 167 | name=db_name, 168 | users=[ 169 | {"username": username1, "password": password1, "active": True}, 170 | {"username": username2, "password": password2, "active": True}, 171 | {"username": username3, "password": password3, "active": False}, 172 | ], 173 | ) 174 | assert result is True 175 | 176 | sys_db.update_permission(username1, permission="rw", database=db_name) 177 | sys_db.update_permission(username2, permission="rw", database=db_name) 178 | sys_db.update_permission(username3, permission="rw", database=db_name) 179 | 180 | # Test if the users were created properly 181 | usernames = extract("username", sys_db.users()) 182 | assert all(u in usernames for u in [username1, username2, username3]) 183 | 184 | # Test if the first user has access to the database 185 | db = client.db(db_name, username1, password1) 186 | db.properties() 187 | 188 | # Test if the second user also has access to the database 189 | db = client.db(db_name, username2, password2) 190 | db.properties() 191 | 192 | # Test if the third user has access to the database (should not) 193 | db = client.db(db_name, username3, password3) 194 | with assert_raises(DatabasePropertiesError) as err: 195 | db.properties() 196 | assert err.value.http_code == 401 197 | -------------------------------------------------------------------------------- /tests/test_wal.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from arango.errno import DATABASE_NOT_FOUND, FORBIDDEN, HTTP_UNAUTHORIZED 4 | from arango.exceptions import ( 5 | WALConfigureError, 6 | WALFlushError, 7 | WALLastTickError, 8 | WALPropertiesError, 9 | WALTailError, 10 | WALTickRangesError, 11 | WALTransactionListError, 12 | ) 13 | from tests.helpers import assert_raises 14 | 15 | 16 | def test_wal_misc_methods(sys_db, bad_db): 17 | try: 18 | sys_db.wal.properties() 19 | except WALPropertiesError as wal_err: 20 | if wal_err.http_code == 501: 21 | pytest.skip("WAL not implemented") 22 | 23 | # Test get properties 24 | properties = sys_db.wal.properties() 25 | assert "oversized_ops" in properties 26 | assert "log_size" in properties 27 | assert "historic_logs" in properties 28 | assert "reserve_logs" in properties 29 | assert "throttle_wait" in properties 30 | assert "throttle_limit" in properties 31 | 32 | # Test get properties with bad database 33 | with assert_raises(WALPropertiesError) as err: 34 | bad_db.wal.properties() 35 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 36 | 37 | # Test configure properties 38 | sys_db.wal.configure( 39 | historic_logs=15, 40 | oversized_ops=False, 41 | log_size=30000000, 42 | reserve_logs=5, 43 | throttle_limit=0, 44 | throttle_wait=16000, 45 | ) 46 | properties = sys_db.wal.properties() 47 | assert properties["historic_logs"] == 15 48 | assert properties["oversized_ops"] is False 49 | assert properties["log_size"] == 30000000 50 | assert properties["reserve_logs"] == 5 51 | assert properties["throttle_limit"] == 0 52 | assert properties["throttle_wait"] == 16000 53 | 54 | # Test configure properties with bad database 55 | with assert_raises(WALConfigureError) as err: 56 | bad_db.wal.configure(log_size=2000000) 57 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 58 | 59 | # Test get transactions 60 | result = sys_db.wal.transactions() 61 | assert "count" in result 62 | assert "last_collected" in result 63 | 64 | # Test get transactions with bad database 65 | with assert_raises(WALTransactionListError) as err: 66 | bad_db.wal.transactions() 67 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 68 | 69 | # Test flush 70 | result = sys_db.wal.flush(garbage_collect=False, sync=False) 71 | assert isinstance(result, bool) 72 | 73 | # Test flush with bad database 74 | with assert_raises(WALFlushError) as err: 75 | bad_db.wal.flush(garbage_collect=False, sync=False) 76 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 77 | 78 | 79 | def test_wal_tick_ranges(sys_db, bad_db, cluster): 80 | if cluster: 81 | pytest.skip("Not tested in a cluster setup") 82 | 83 | result = sys_db.wal.tick_ranges() 84 | assert "server" in result 85 | assert "time" in result 86 | assert "tick_min" in result 87 | assert "tick_max" in result 88 | 89 | # Test tick_ranges with bad database 90 | with assert_raises(WALTickRangesError) as err: 91 | bad_db.wal.tick_ranges() 92 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 93 | 94 | 95 | def test_wal_last_tick(sys_db, bad_db, cluster): 96 | if cluster: 97 | pytest.skip("Not tested in a cluster setup") 98 | 99 | result = sys_db.wal.last_tick() 100 | assert "time" in result 101 | assert "tick" in result 102 | assert "server" in result 103 | 104 | # Test last_tick with bad database 105 | with assert_raises(WALLastTickError) as err: 106 | bad_db.wal.last_tick() 107 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 108 | 109 | 110 | def test_wal_tail(sys_db, bad_db, cluster): 111 | if cluster: 112 | pytest.skip("Not tested in a cluster setup") 113 | 114 | result = sys_db.wal.tail( 115 | lower=0, 116 | upper=1000000, 117 | last_scanned=0, 118 | all_databases=True, 119 | chunk_size=1000000, 120 | syncer_id=None, 121 | server_id=None, 122 | client_info="test", 123 | barrier_id=None, 124 | ) 125 | assert "content" in result 126 | assert "last_tick" in result 127 | assert "last_scanned" in result 128 | assert "last_included" in result 129 | assert isinstance(result["check_more"], bool) 130 | assert isinstance(result["from_present"], bool) 131 | 132 | # Test tick_ranges with bad database 133 | with assert_raises(WALTailError) as err: 134 | bad_db.wal.tail() 135 | assert err.value.http_code == HTTP_UNAUTHORIZED 136 | --------------------------------------------------------------------------------