├── .gitignore ├── .travis.yml ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── VERSION ├── aioarangodb ├── __init__.py ├── api.py ├── aql.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 ├── replication.py ├── request.py ├── resolver.py ├── response.py ├── tests │ ├── __init__.py │ ├── arangodocker.py │ ├── conftest.py │ ├── executors.py │ ├── fixtures.py │ ├── helpers.py │ ├── static │ │ ├── keyfile │ │ └── service.zip │ ├── test_analyzer.py │ ├── test_aql.py │ ├── test_async.py │ ├── test_auth.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_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 ├── utils.py ├── version.py └── wal.py ├── docs ├── Makefile ├── admin.rst ├── analyzer.rst ├── aql.rst ├── async.rst ├── batch.rst ├── cluster.rst ├── collection.rst ├── conf.py ├── contributing.rst ├── cursor.rst ├── database.rst ├── document.rst ├── errno.rst ├── errors.rst ├── foxx.rst ├── graph.rst ├── index.rst ├── indexes.rst ├── make.bat ├── overview.rst ├── pregel.rst ├── replication.rst ├── serializer.rst ├── specs.rst ├── static │ └── logo.png ├── task.rst ├── threading.rst ├── transaction.rst ├── user.rst ├── view.rst └── wal.rst ├── requirements.txt ├── setup.cfg ├── setup.py └── test-requirements.txt /.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 | # VSCode 113 | .vscode/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | matrix: 4 | include: 5 | - python: 2.7 6 | - python: 3.5 7 | - python: 3.6 8 | - python: 3.7 9 | dist: xenial 10 | sudo: true 11 | services: 12 | - docker 13 | before_install: 14 | - docker run --name arango -d -p 8529:8529 -e ARANGO_ROOT_PASSWORD=passwd arangodb/arangodb:3.6.1 15 | - docker cp tests/static/service.zip arango:/tmp/service.zip 16 | install: 17 | - pip install flake8 mock 18 | - pip install pytest==3.5.1 19 | - pip install pytest-cov==2.5.1 20 | - pip install python-coveralls==2.9.1 21 | - pip install sphinx sphinx_rtd_theme 22 | - pip install . 23 | script: 24 | - python -m flake8 25 | - python -m sphinx -b doctest docs docs/_build 26 | - python -m sphinx -b html -W docs docs/_build 27 | - py.test --complete -s -v --cov=arango 28 | after_success: 29 | - coveralls -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | 0.1.3 (unreleased) 2 | ------------------ 3 | 4 | - Nothing changed yet. 5 | 6 | 7 | 0.1.2 (2020-06-12) 8 | ------------------ 9 | 10 | - Splitting fixture so it can be reused on subpackages 11 | [bloodbare] 12 | 13 | 14 | 0.1.1 (2020-06-12) 15 | ------------------ 16 | 17 | - Allow to get hostname on pytest 18 | [bloodbare] 19 | 20 | 21 | 0.1.0 (2020-06-11) 22 | ------------------ 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Joohwan Oh 4 | Copyright (c) 2020 Ramon Navarro Bosch 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | Email: ramon.nb@gmail.com -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt *.rst VERSION 2 | recursive-include *.txt *.py 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | 5 | .. image:: https://travis-ci.org/bloodbare/aioarangodb.svg?branch=master 6 | :target: https://travis-ci.org/bloodbare/aioarangodb 7 | :alt: Travis Build Status 8 | 9 | .. image:: https://readthedocs.org/projects/aioarangodb/badge/?version=master 10 | :target: http://aioarangodb.readthedocs.io/en/master/?badge=master 11 | :alt: Documentation Status 12 | 13 | .. image:: https://badge.fury.io/py/aioarangodb.svg 14 | :target: https://badge.fury.io/py/aioarangodb 15 | :alt: Package Version 16 | 17 | .. image:: https://img.shields.io/badge/python-3.5%2C%203.6%2C%203.7-blue.svg 18 | :target: https://github.com/bloodbare/aioarangodb 19 | :alt: Python Versions 20 | 21 | .. image:: https://coveralls.io/repos/github/bloodbare/aioarangodb/badge.svg?branch=master 22 | :target: https://coveralls.io/github/bloodbare/aioarangodb?branch=master 23 | :alt: Test Coverage 24 | 25 | .. image:: https://img.shields.io/github/issues/bloodbare/aioarangodb.svg 26 | :target: https://github.com/bloodbare/aioarangodb/issues 27 | :alt: Issues Open 28 | 29 | .. image:: https://img.shields.io/badge/license-MIT-blue.svg 30 | :target: https://raw.githubusercontent.com/bloodbare/aioarangodb/master/LICENSE 31 | :alt: MIT License 32 | 33 | | 34 | 35 | Welcome to the GitHub page for **aioarangodb**, a Python driver for ArangoDB_ Asyncio only. 36 | 37 | Announcements 38 | ============= 39 | 40 | - This project is a fork of https://github.com/joowani/python-arango with only asyncio python>=3.5 support. Many thanks for the great job. 41 | 42 | Features 43 | ======== 44 | 45 | - Pythonic interface 46 | - Lightweight 47 | - High API coverage 48 | 49 | Compatibility 50 | ============= 51 | 52 | - Python versions 3.5, 3.6 and 3.7 are supported 53 | - aioArangoDB supports ArangoDB 3.5+ 54 | 55 | Installation 56 | ============ 57 | 58 | To install a stable version from PyPi_: 59 | 60 | .. code-block:: bash 61 | 62 | ~$ pip install aioarangodb 63 | 64 | 65 | To install the latest version directly from GitHub_: 66 | 67 | .. code-block:: bash 68 | 69 | ~$ pip install -e git+git@github.com:bloodbare/aioarangodb.git@master#egg=aioarangodb 70 | 71 | You may need to use ``sudo`` depending on your environment. 72 | 73 | Getting Started 74 | =============== 75 | 76 | Here is a simple usage example: 77 | 78 | .. code-block:: python 79 | 80 | from aioarangodb import ArangoClient 81 | 82 | # Initialize the client for ArangoDB. 83 | client = ArangoClient(hosts='http://localhost:8529') 84 | 85 | # Connect to "_system" database as root user. 86 | sys_db = await client.db('_system', username='root', password='passwd') 87 | 88 | # Create a new database named "test". 89 | await await sys_db.create_database('test') 90 | 91 | # Connect to "test" database as root user. 92 | db = await client.db('test', username='root', password='passwd') 93 | 94 | # Create a new collection named "students". 95 | students = await db.create_collection('students') 96 | 97 | # Add a hash index to the collection. 98 | await students.add_hash_index(fields=['name'], unique=True) 99 | 100 | # Insert new documents into the collection. 101 | await students.insert({'name': 'jane', 'age': 39}) 102 | await students.insert({'name': 'josh', 'age': 18}) 103 | await students.insert({'name': 'judy', 'age': 21}) 104 | 105 | # Execute an AQL query and iterate through the result cursor. 106 | cursor = await db.aql.execute('FOR doc IN students RETURN doc') 107 | student_names = [document['name'] async for document in cursor] 108 | 109 | 110 | Here is another example with graphs: 111 | 112 | .. code-block:: python 113 | 114 | from aioarangodb import ArangoClient 115 | 116 | # Initialize the client for ArangoDB. 117 | client = ArangoClient(hosts='http://localhost:8529') 118 | 119 | # Connect to "test" database as root user. 120 | db = await client.db('test', username='root', password='passwd') 121 | 122 | # Create a new graph named "school". 123 | graph = await db.create_graph('school') 124 | 125 | # Create vertex collections for the graph. 126 | students = await graph.create_vertex_collection('students') 127 | lectures = await graph.create_vertex_collection('lectures') 128 | 129 | # Create an edge definition (relation) for the graph. 130 | register = await graph.create_edge_definition( 131 | edge_collection='register', 132 | from_vertex_collections=['students'], 133 | to_vertex_collections=['lectures'] 134 | ) 135 | 136 | # Insert vertex documents into "students" (from) vertex collection. 137 | await students.insert({'_key': '01', 'full_name': 'Anna Smith'}) 138 | await students.insert({'_key': '02', 'full_name': 'Jake Clark'}) 139 | await students.insert({'_key': '03', 'full_name': 'Lisa Jones'}) 140 | 141 | # Insert vertex documents into "lectures" (to) vertex collection. 142 | await lectures.insert({'_key': 'MAT101', 'title': 'Calculus'}) 143 | await lectures.insert({'_key': 'STA101', 'title': 'Statistics'}) 144 | await lectures.insert({'_key': 'CSC101', 'title': 'Algorithms'}) 145 | 146 | # Insert edge documents into "register" edge collection. 147 | await register.insert({'_from': 'students/01', '_to': 'lectures/MAT101'}) 148 | await register.insert({'_from': 'students/01', '_to': 'lectures/STA101'}) 149 | await register.insert({'_from': 'students/01', '_to': 'lectures/CSC101'}) 150 | await register.insert({'_from': 'students/02', '_to': 'lectures/MAT101'}) 151 | await register.insert({'_from': 'students/02', '_to': 'lectures/STA101'}) 152 | await register.insert({'_from': 'students/03', '_to': 'lectures/CSC101'}) 153 | 154 | # Traverse the graph in outbound direction, breadth-first. 155 | result = await graph.traverse( 156 | start_vertex='students/01', 157 | direction='outbound', 158 | strategy='breadthfirst' 159 | ) 160 | 161 | Check out the documentation_ for more information. 162 | 163 | Contributing 164 | ============ 165 | 166 | Please take a look at this page_ before submitting a pull request. Thanks! 167 | 168 | .. _ArangoDB: https://www.arangodb.com 169 | .. _releases: https://github.com/bloodbare/aioarangodb/releases 170 | .. _PyPi: https://pypi.python.org/pypi/aioarangodb 171 | .. _GitHub: https://github.com/bloodbare/aioarangodb 172 | .. _documentation: 173 | http://aioarangodb.readthedocs.io/en/master/index.html 174 | .. _page: 175 | http://aioarangodb.readthedocs.io/en/master/contributing.html -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.3.dev0 2 | -------------------------------------------------------------------------------- /aioarangodb/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import ArangoClient # noqa: F401 2 | from .exceptions import * # noqa: F401 F403 3 | from .http import * # noqa: F401 F403 4 | from . import errno # noqa: F401 5 | -------------------------------------------------------------------------------- /aioarangodb/api.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | __all__ = ['APIWrapper'] 4 | 5 | 6 | class APIWrapper(object): 7 | """Base class for API wrappers. 8 | 9 | :param connection: HTTP connection. 10 | :type connection: arango.connection.Connection 11 | :param executor: API executor. 12 | :type executor: arango.executor.Executor 13 | """ 14 | 15 | def __init__(self, connection, executor): 16 | self._conn = connection 17 | self._executor = executor 18 | 19 | @property 20 | def conn(self): 21 | """Return HTTP connection object. 22 | 23 | :return: HTTP connection. 24 | :rtype: arango.connection.Connection 25 | """ 26 | return self._conn 27 | 28 | @property 29 | def db_name(self): 30 | """Return the name of the current database. 31 | 32 | :return: Database name. 33 | :rtype: str | unicode 34 | """ 35 | return self._conn.db_name 36 | 37 | @property 38 | def username(self): 39 | """Return the username. 40 | 41 | :returns: Username. 42 | :rtype: str | unicode 43 | """ 44 | return self._conn.username 45 | 46 | @property 47 | def context(self): 48 | """Return the API execution context. 49 | 50 | :return: API execution context. Possible values are "default", "async", 51 | "batch" and "transaction". 52 | :rtype: str | unicode 53 | """ 54 | return self._executor.context 55 | 56 | async def _execute(self, request, response_handler): 57 | """Execute an API per execution context. 58 | 59 | :param request: HTTP request. 60 | :type request: arango.request.Request 61 | :param response_handler: HTTP response handler. 62 | :type response_handler: callable 63 | :return: API execution result. 64 | :rtype: str | unicode | bool | int | list | dict 65 | """ 66 | return await self._executor.execute(request, response_handler) 67 | -------------------------------------------------------------------------------- /aioarangodb/client.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import json 4 | 5 | from six import string_types 6 | 7 | __all__ = ['ArangoClient'] 8 | 9 | from .connection import ( 10 | BasicConnection, 11 | JWTConnection, 12 | JWTSuperuserConnection 13 | ) 14 | from .database import StandardDatabase 15 | from .exceptions import ServerConnectionError 16 | from .http import DefaultHTTPClient 17 | from .resolver import ( 18 | SingleHostResolver, 19 | RandomHostResolver, 20 | RoundRobinHostResolver 21 | ) 22 | from .version import __version__ 23 | 24 | 25 | class ArangoClient(object): 26 | """ArangoDB client. 27 | 28 | :param hosts: Host URL or list of URLs (coordinators in a cluster). 29 | :type hosts: [str | unicode] 30 | :param host_resolver: Host resolver. This parameter used for clusters (when 31 | multiple host URLs are provided). Accepted values are "roundrobin" and 32 | "random". Any other value defaults to round robin. 33 | :type host_resolver: str | unicode 34 | :param http_client: User-defined HTTP client. 35 | :type http_client: arango.http.HTTPClient 36 | :param serializer: User-defined JSON serializer. Must be a callable 37 | which takes a JSON data type object as its only argument and return 38 | the serialized string. If not given, ``json.dumps`` is used by default. 39 | :type serializer: callable 40 | :param deserializer: User-defined JSON de-serializer. Must be a callable 41 | which takes a JSON serialized string as its only argument and return 42 | the de-serialized object. If not given, ``json.loads`` is used by 43 | default. 44 | :type deserializer: callable 45 | """ 46 | 47 | def __init__(self, 48 | hosts='http://127.0.0.1:8529', 49 | host_resolver='roundrobin', 50 | http_client=None, 51 | serializer=json.dumps, 52 | deserializer=json.loads): 53 | if isinstance(hosts, string_types): 54 | self._hosts = [host.strip('/') for host in hosts.split(',')] 55 | else: 56 | self._hosts = [host.strip('/') for host in hosts] 57 | 58 | host_count = len(self._hosts) 59 | if host_count == 1: 60 | self._host_resolver = SingleHostResolver() 61 | elif host_resolver == 'random': 62 | self._host_resolver = RandomHostResolver(host_count) 63 | else: 64 | self._host_resolver = RoundRobinHostResolver(host_count) 65 | 66 | self._http = http_client or DefaultHTTPClient() 67 | self._serializer = serializer 68 | self._deserializer = deserializer 69 | self._sessions = [self._http.create_session(h) for h in self._hosts] 70 | 71 | def __repr__(self): 72 | return ''.format(','.join(self._hosts)) 73 | 74 | @property 75 | def hosts(self): 76 | """Return the list of ArangoDB host URLs. 77 | 78 | :return: List of ArangoDB host URLs. 79 | :rtype: [str | unicode] 80 | """ 81 | return self._hosts 82 | 83 | @property 84 | def version(self): 85 | """Return the client version. 86 | 87 | :return: Client version. 88 | :rtype: str | unicode 89 | """ 90 | return __version__ 91 | 92 | async def close(self): 93 | for session in self._sessions: 94 | await session.close() 95 | 96 | async def db( 97 | self, 98 | name='_system', 99 | username='root', 100 | password='', 101 | verify=False, 102 | auth_method='basic', 103 | superuser_token=None): 104 | """Connect to an ArangoDB database and return the database API wrapper. 105 | 106 | :param name: Database name. 107 | :type name: str | unicode 108 | :param username: Username for basic authentication. 109 | :type username: str | unicode 110 | :param password: Password for basic authentication. 111 | :type password: str | unicode 112 | :param verify: Verify the connection by sending a test request. 113 | :type verify: bool 114 | :param auth_method: HTTP authentication method. Accepted values are 115 | "basic" (default) and "jwt". If set to "jwt", the token is 116 | refreshed automatically using ArangoDB username and password. This 117 | assumes that the clocks of the server and client are synchronized. 118 | :type auth_method: str | unicode 119 | :param superuser_token: User generated token for superuser access. 120 | If set, parameters **username**, **password** and **auth_method** 121 | are ignored. This token is not refreshed automatically. 122 | :type superuser_token: str | unicode 123 | :return: Standard database API wrapper. 124 | :rtype: arango.database.StandardDatabase 125 | :raise arango.exceptions.ServerConnectionError: If **verify** was set 126 | to True and the connection fails. 127 | """ 128 | if superuser_token is not None: 129 | connection = JWTSuperuserConnection( 130 | hosts=self._hosts, 131 | host_resolver=self._host_resolver, 132 | sessions=self._sessions, 133 | db_name=name, 134 | http_client=self._http, 135 | serializer=self._serializer, 136 | deserializer=self._deserializer, 137 | superuser_token=superuser_token 138 | ) 139 | elif auth_method == 'basic': 140 | connection = BasicConnection( 141 | hosts=self._hosts, 142 | host_resolver=self._host_resolver, 143 | sessions=self._sessions, 144 | db_name=name, 145 | username=username, 146 | password=password, 147 | http_client=self._http, 148 | serializer=self._serializer, 149 | deserializer=self._deserializer, 150 | ) 151 | elif auth_method == 'jwt': 152 | connection = JWTConnection( 153 | hosts=self._hosts, 154 | host_resolver=self._host_resolver, 155 | sessions=self._sessions, 156 | db_name=name, 157 | username=username, 158 | password=password, 159 | http_client=self._http, 160 | serializer=self._serializer, 161 | deserializer=self._deserializer, 162 | ) 163 | await connection.refresh_token() 164 | else: 165 | raise ValueError('invalid auth_method: {}'.format(auth_method)) 166 | 167 | if verify: 168 | try: 169 | await connection.ping() 170 | except ServerConnectionError as err: 171 | raise err 172 | except Exception as err: 173 | raise ServerConnectionError('bad connection: {}'.format(err)) 174 | 175 | return StandardDatabase(connection) 176 | -------------------------------------------------------------------------------- /aioarangodb/cluster.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | __all__ = ['Cluster'] 4 | 5 | from .api import APIWrapper 6 | from .exceptions import ( 7 | ClusterEndpointsError, 8 | ClusterHealthError, 9 | ClusterMaintenanceModeError, 10 | ClusterServerEngineError, 11 | ClusterServerIDError, 12 | ClusterServerRoleError, 13 | ClusterServerStatisticsError, 14 | ClusterServerVersionError, 15 | ) 16 | from .request import Request 17 | 18 | 19 | class Cluster(APIWrapper): # pragma: no cover 20 | 21 | def __init__(self, connection, executor): 22 | super(Cluster, self).__init__(connection, executor) 23 | 24 | async def server_id(self): 25 | """Return the server ID. 26 | 27 | :return: Server ID. 28 | :rtype: str | unicode 29 | :raise arango.exceptions.ClusterServerIDError: If retrieval fails. 30 | """ 31 | request = Request( 32 | method='get', 33 | endpoint='/_admin/server/id' 34 | ) 35 | 36 | def response_handler(resp): 37 | if resp.is_success: 38 | return resp.body['id'] 39 | raise ClusterServerIDError(resp, request) 40 | 41 | return await self._execute(request, response_handler) 42 | 43 | async def server_role(self): 44 | """Return the server role. 45 | 46 | :return: Server role. Possible values are "SINGLE" (server which is 47 | not in a cluster), "COORDINATOR" (cluster coordinator), "PRIMARY", 48 | "SECONDARY", "AGENT" (Agency server in a cluster) or "UNDEFINED". 49 | :rtype: str | unicode 50 | :raise arango.exceptions.ClusterServerRoleError: If retrieval fails. 51 | """ 52 | request = Request( 53 | method='get', 54 | endpoint='/_admin/server/role' 55 | ) 56 | 57 | def response_handler(resp): 58 | if resp.is_success: 59 | return resp.body['role'] 60 | raise ClusterServerRoleError(resp, request) 61 | 62 | return await self._execute(request, response_handler) 63 | 64 | async def server_version(self, server_id): 65 | """Return the version of the given server. 66 | 67 | :param server_id: Server ID. 68 | :type server_id: str | unicode 69 | :return: Version of the given server. 70 | :rtype: dict 71 | :raise arango.exceptions.ClusterServerVersionError: If retrieval fails. 72 | """ 73 | request = Request( 74 | method='get', 75 | endpoint='/_admin/cluster/nodeVersion', 76 | params={'ServerID': server_id} 77 | ) 78 | 79 | def response_handler(resp): 80 | if resp.is_success: 81 | return resp.body 82 | raise ClusterServerVersionError(resp, request) 83 | 84 | return await self._execute(request, response_handler) 85 | 86 | async def server_engine(self, server_id): 87 | """Return the engine details for the given server. 88 | 89 | :param server_id: Server ID. 90 | :type server_id: str | unicode 91 | :return: Engine details of the given server. 92 | :rtype: dict 93 | :raise arango.exceptions.ClusterServerEngineError: If retrieval fails. 94 | """ 95 | request = Request( 96 | method='get', 97 | endpoint='/_admin/cluster/nodeEngine', 98 | params={'ServerID': server_id} 99 | ) 100 | 101 | def response_handler(resp): 102 | if resp.is_success: 103 | return resp.body 104 | raise ClusterServerEngineError(resp, request) 105 | 106 | return await self._execute(request, response_handler) 107 | 108 | async def server_statistics(self, server_id): 109 | """Return the statistics for the given server. 110 | 111 | :param server_id: Server ID. 112 | :type server_id: str | unicode 113 | :return: Statistics for the given server. 114 | :rtype: dict 115 | :raise arango.exceptions.ClusterServerStatisticsError: If retrieval 116 | fails. 117 | """ 118 | request = Request( 119 | method='get', 120 | endpoint='/_admin/cluster/nodeStatistics', 121 | params={'ServerID': server_id} 122 | ) 123 | 124 | def response_handler(resp): 125 | if resp.is_success: 126 | return resp.body 127 | raise ClusterServerStatisticsError(resp, request) 128 | 129 | return await self._execute(request, response_handler) 130 | 131 | async def health(self): 132 | """Return the cluster health. 133 | 134 | :return: Cluster health. 135 | :rtype: dict 136 | :raise arango.exceptions.ClusterHealthError: If retrieval fails. 137 | """ 138 | request = Request( 139 | method='get', 140 | endpoint='/_admin/cluster/health', 141 | ) 142 | 143 | def response_handler(resp): 144 | if resp.is_success: 145 | resp.body.pop('error') 146 | resp.body.pop('code') 147 | return resp.body 148 | raise ClusterHealthError(resp, request) 149 | 150 | return await self._execute(request, response_handler) 151 | 152 | async def toggle_maintenance_mode(self, mode): 153 | """Enable or disable the cluster supervision (agency) maintenance mode. 154 | 155 | :param mode: Maintenance mode. Allowed values are "on" and "off". 156 | :type mode: str | unicode 157 | :return: Result of the operation. 158 | :rtype: dict 159 | :raise arango.exceptions.ClusterMaintenanceModeError: If toggle fails. 160 | """ 161 | request = Request( 162 | method='put', 163 | endpoint='/_admin/cluster/maintenance', 164 | data='"{}"'.format(mode) 165 | ) 166 | 167 | def response_handler(resp): 168 | if resp.is_success: 169 | return resp.body 170 | raise ClusterMaintenanceModeError(resp, request) 171 | 172 | return await self._execute(request, response_handler) 173 | 174 | async def endpoints(self): 175 | """Return coordinate endpoints. This method is for clusters only. 176 | 177 | :return: List of endpoints. 178 | :rtype: [str | unicode] 179 | :raise arango.exceptions.ServerEndpointsError: If retrieval fails. 180 | """ 181 | request = Request( 182 | method='get', 183 | endpoint='/_api/cluster/endpoints' 184 | ) 185 | 186 | def response_handler(resp): 187 | if not resp.is_success: 188 | raise ClusterEndpointsError(resp, request) 189 | return [item['endpoint'] for item in resp.body['endpoints']] 190 | 191 | return await self._execute(request, response_handler) 192 | -------------------------------------------------------------------------------- /aioarangodb/http.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | __all__ = ['HTTPClient', 'DefaultHTTPClient'] 4 | 5 | from abc import ABCMeta, abstractmethod 6 | 7 | import aiohttp 8 | 9 | from .response import Response 10 | 11 | 12 | class HTTPClient(object): # pragma: no cover 13 | """Abstract base class for HTTP clients.""" 14 | 15 | __metaclass__ = ABCMeta 16 | 17 | @abstractmethod 18 | def create_session(self, host): 19 | """Return a new requests session given the host URL. 20 | 21 | This method must be overridden by the user. 22 | 23 | :param host: ArangoDB host URL. 24 | :type host: str | unicode 25 | :returns: Requests session object. 26 | :rtype: requests.Session 27 | """ 28 | raise NotImplementedError 29 | 30 | @abstractmethod 31 | async def send_request( 32 | self, 33 | session, 34 | method, 35 | url, 36 | headers=None, 37 | params=None, 38 | data=None, 39 | auth=None): 40 | """Send an HTTP request. 41 | 42 | This method must be overridden by the user. 43 | 44 | :param session: Requests session object. 45 | :type session: requests.Session 46 | :param method: HTTP method in lowercase (e.g. "post"). 47 | :type method: str | unicode 48 | :param url: Request URL. 49 | :type url: str | unicode 50 | :param headers: Request headers. 51 | :type headers: dict 52 | :param params: URL (query) parameters. 53 | :type params: dict 54 | :param data: Request payload. 55 | :type data: str | unicode | bool | int | list | dict 56 | :param auth: Username and password. 57 | :type auth: tuple 58 | :returns: HTTP response. 59 | :rtype: arango.response.Response 60 | """ 61 | raise NotImplementedError 62 | 63 | 64 | class DefaultHTTPClient(HTTPClient): 65 | """Default HTTP client implementation.""" 66 | 67 | def create_session(self, host): 68 | """Create and return a new session/connection. 69 | 70 | :param host: ArangoDB host URL. 71 | :type host: str | unicode 72 | :returns: requests session object 73 | :rtype: requests.Session 74 | """ 75 | return aiohttp.ClientSession() 76 | 77 | async def send_request( 78 | self, 79 | session, 80 | method, 81 | url, 82 | params=None, 83 | data=None, 84 | headers=None, 85 | auth=None): 86 | """Send an HTTP request. 87 | 88 | :param session: Requests session object. 89 | :type session: requests.Session 90 | :param method: HTTP method in lowercase (e.g. "post"). 91 | :type method: str | unicode 92 | :param url: Request URL. 93 | :type url: str | unicode 94 | :param headers: Request headers. 95 | :type headers: dict 96 | :param params: URL (query) parameters. 97 | :type params: dict 98 | :param data: Request payload. 99 | :type data: str | unicode | bool | int | list | dict 100 | :param auth: Username and password. 101 | :type auth: tuple 102 | :returns: HTTP response. 103 | :rtype: arango.response.Response 104 | """ 105 | request = getattr(session, method) 106 | if auth is not None: 107 | auth = aiohttp.BasicAuth(auth[0], auth[1]) 108 | response = await request( 109 | url=url, 110 | params=params, 111 | data=data, 112 | headers=headers, 113 | auth=auth 114 | ) 115 | return Response( 116 | method=method, 117 | url=url, 118 | headers=response.headers, 119 | status_code=response.status, 120 | status_text=response.reason, 121 | raw_body=await response.text(), 122 | ) 123 | -------------------------------------------------------------------------------- /aioarangodb/pregel.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | __all__ = ['Pregel'] 4 | 5 | from .api import APIWrapper 6 | from .exceptions import ( 7 | PregelJobGetError, 8 | PregelJobCreateError, 9 | PregelJobDeleteError 10 | ) 11 | from .request import Request 12 | 13 | 14 | class Pregel(APIWrapper): 15 | """Pregel API wrapper. 16 | 17 | :param connection: HTTP connection. 18 | :type connection: arango.connection.Connection 19 | :param executor: API executor. 20 | :type executor: arango.executor.Executor 21 | """ 22 | 23 | def __init__(self, connection, executor): 24 | super(Pregel, self).__init__(connection, executor) 25 | 26 | def __repr__(self): 27 | return ''.format(self._conn.db_name) 28 | 29 | async def job(self, job_id): 30 | """Return the details of a Pregel job. 31 | 32 | :param job_id: Pregel job ID. 33 | :type job_id: int 34 | :return: Details of the Pregel job. 35 | :rtype: dict 36 | :raise arango.exceptions.PregelJobGetError: If retrieval fails. 37 | """ 38 | request = Request( 39 | method='get', 40 | endpoint='/_api/control_pregel/{}'.format(job_id) 41 | ) 42 | 43 | def response_handler(resp): 44 | if not resp.is_success: 45 | raise PregelJobGetError(resp, request) 46 | if 'receivedCount' in resp.body: 47 | resp.body['received_count'] = resp.body.pop('receivedCount') 48 | if 'sendCount' in resp.body: 49 | resp.body['send_count'] = resp.body.pop('sendCount') 50 | if 'totalRuntime' in resp.body: 51 | resp.body['total_runtime'] = resp.body.pop('totalRuntime') 52 | if 'edgeCount' in resp.body: # pragma: no cover 53 | resp.body['edge_count'] = resp.body.pop('edgeCount') 54 | if 'vertexCount' in resp.body: # pragma: no cover 55 | resp.body['vertex_count'] = resp.body.pop('vertexCount') 56 | return resp.body 57 | 58 | return await self._execute(request, response_handler) 59 | 60 | async def create_job( 61 | self, 62 | graph, 63 | algorithm, 64 | store=True, 65 | max_gss=None, 66 | thread_count=None, 67 | async_mode=None, 68 | result_field=None, 69 | algorithm_params=None): 70 | """Start a new Pregel job. 71 | 72 | :param graph: Graph name. 73 | :type graph: str | unicode 74 | :param algorithm: Algorithm (e.g. "pagerank"). 75 | :type algorithm: str | unicode 76 | :param store: If set to True, Pregel engine writes results back to the 77 | database. If set to False, results can be queried via AQL. 78 | :type store: bool 79 | :param max_gss: Max number of global iterations for the algorithm. 80 | :type max_gss: int 81 | :param thread_count: Number of parallel threads to use per worker. 82 | This does not influence the number of threads used to load or store 83 | data from the database (it depends on the number of shards). 84 | :type thread_count: int 85 | :param async_mode: If set to True, algorithms which support async mode 86 | run without synchronized global iterations. This might lead to 87 | performance increase if there are load imbalances. 88 | :type async_mode: bool 89 | :param result_field: If specified, most algorithms will write their 90 | results into this field. 91 | :type result_field: str | unicode 92 | :param algorithm_params: Additional algorithm parameters. 93 | :type algorithm_params: dict 94 | :return: Pregel job ID. 95 | :rtype: int 96 | :raise arango.exceptions.PregelJobCreateError: If create fails. 97 | """ 98 | data = {'algorithm': algorithm, 'graphName': graph} 99 | 100 | if algorithm_params is None: 101 | algorithm_params = {} 102 | 103 | if store is not None: 104 | algorithm_params['store'] = store 105 | if max_gss is not None: 106 | algorithm_params['maxGSS'] = max_gss 107 | if thread_count is not None: 108 | algorithm_params['parallelism'] = thread_count 109 | if async_mode is not None: 110 | algorithm_params['async'] = async_mode 111 | if result_field is not None: 112 | algorithm_params['resultField'] = result_field 113 | if algorithm_params: 114 | data['params'] = algorithm_params 115 | 116 | request = Request( 117 | method='post', 118 | endpoint='/_api/control_pregel', 119 | data=data 120 | ) 121 | 122 | def response_handler(resp): 123 | if resp.is_success: 124 | return resp.body 125 | raise PregelJobCreateError(resp, request) 126 | 127 | return await self._execute(request, response_handler) 128 | 129 | async def delete_job(self, job_id): 130 | """Delete a Pregel job. 131 | 132 | :param job_id: Pregel job ID. 133 | :type job_id: int 134 | :return: True if Pregel job was deleted successfully. 135 | :rtype: bool 136 | :raise arango.exceptions.PregelJobDeleteError: If delete fails. 137 | """ 138 | request = Request( 139 | method='delete', 140 | endpoint='/_api/control_pregel/{}'.format(job_id) 141 | ) 142 | 143 | def response_handler(resp): 144 | if resp.is_success: 145 | return True 146 | raise PregelJobDeleteError(resp, request) 147 | 148 | return await self._execute(request, response_handler) 149 | -------------------------------------------------------------------------------- /aioarangodb/request.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | __all__ = ['Request'] 4 | 5 | 6 | class Request(object): 7 | """HTTP request. 8 | 9 | :param method: HTTP method in lowercase (e.g. "post"). 10 | :type method: str | unicode 11 | :param endpoint: API endpoint. 12 | :type endpoint: str | unicode 13 | :param headers: Request headers. 14 | :type headers: dict 15 | :param params: URL parameters. 16 | :type params: dict 17 | :param data: Request payload. 18 | :type data: str | unicode | bool | int | list | dict | 19 | aiohttp.MultipartWriter 20 | :param read: Names of collections read during transaction. 21 | :type read: str | unicode | [str | unicode] 22 | :param write: Name(s) of collections written to during transaction with 23 | shared access. 24 | :type write: str | unicode | [str | unicode] 25 | :param exclusive: Name(s) of collections written to during transaction 26 | with exclusive access. 27 | :type exclusive: str | unicode | [str | unicode] 28 | :param deserialize: Whether the response body can be deserialized. 29 | :type deserialize: bool 30 | :ivar method: HTTP method in lowercase (e.g. "post"). 31 | :vartype method: str | unicode 32 | :ivar endpoint: API endpoint. 33 | :vartype endpoint: str | unicode 34 | :ivar headers: Request headers. 35 | :vartype headers: dict 36 | :ivar params: URL (query) parameters. 37 | :vartype params: dict 38 | :ivar data: Request payload. 39 | :vartype data: str | unicode | bool | int | list | dict 40 | :ivar read: Names of collections read during transaction. 41 | :vartype read: str | unicode | [str | unicode] | None 42 | :ivar write: Name(s) of collections written to during transaction with 43 | shared access. 44 | :vartype write: str | unicode | [str | unicode] | None 45 | :ivar exclusive: Name(s) of collections written to during transaction 46 | with exclusive access. 47 | :vartype exclusive: str | unicode | [str | unicode] | None 48 | :ivar deserialize: Whether the response body can be deserialized. 49 | :vartype deserialize: bool 50 | """ 51 | 52 | __slots__ = ( 53 | 'method', 54 | 'endpoint', 55 | 'headers', 56 | 'params', 57 | 'data', 58 | 'read', 59 | 'write', 60 | 'exclusive', 61 | 'deserialize', 62 | 'files' 63 | ) 64 | 65 | def __init__(self, 66 | method, 67 | endpoint, 68 | headers=None, 69 | params=None, 70 | data=None, 71 | read=None, 72 | write=None, 73 | exclusive=None, 74 | deserialize=True): 75 | self.method = method 76 | self.endpoint = endpoint 77 | self.headers = { 78 | 'content-type': 'application/json', 79 | 'charset': 'utf-8' 80 | } 81 | if headers is not None: 82 | for field in headers: 83 | self.headers[field.lower()] = headers[field] 84 | 85 | # Sanitize URL params. 86 | if params is not None: 87 | for key, val in params.items(): 88 | if isinstance(val, bool): 89 | params[key] = int(val) 90 | 91 | self.params = params 92 | self.data = data 93 | self.read = read 94 | self.write = write 95 | self.exclusive = exclusive 96 | self.deserialize = deserialize 97 | -------------------------------------------------------------------------------- /aioarangodb/resolver.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | __all__ = [ 4 | 'SingleHostResolver', 5 | 'RandomHostResolver', 6 | 'RoundRobinHostResolver', 7 | ] 8 | 9 | import random 10 | from abc import ABCMeta, abstractmethod 11 | 12 | 13 | class HostResolver(object): # pragma: no cover 14 | """Abstract base class for host resolvers.""" 15 | 16 | __metaclass__ = ABCMeta 17 | 18 | @abstractmethod 19 | def get_host_index(self): 20 | raise NotImplementedError 21 | 22 | 23 | class SingleHostResolver(HostResolver): 24 | """Single host resolver.""" 25 | 26 | def get_host_index(self): 27 | return 0 28 | 29 | 30 | class RandomHostResolver(HostResolver): 31 | """Random host resolver.""" 32 | 33 | def __init__(self, host_count): 34 | self._max = host_count - 1 35 | 36 | def get_host_index(self): 37 | return random.randint(0, self._max) 38 | 39 | 40 | class RoundRobinHostResolver(HostResolver): 41 | """Round-robin host resolver.""" 42 | 43 | def __init__(self, host_count): 44 | self._index = -1 45 | self._count = host_count 46 | 47 | def get_host_index(self): 48 | self._index = (self._index + 1) % self._count 49 | return self._index 50 | -------------------------------------------------------------------------------- /aioarangodb/response.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | __all__ = ['Response'] 4 | 5 | 6 | class Response(object): 7 | """HTTP response. 8 | 9 | :param method: HTTP method in lowercase (e.g. "post"). 10 | :type method: str | unicode 11 | :param url: API URL. 12 | :type url: str | unicode 13 | :param headers: Response headers. 14 | :type headers: requests.structures.CaseInsensitiveDict | dict 15 | :param status_code: Response status code. 16 | :type status_code: int 17 | :param status_text: Response status text. 18 | :type status_text: str | unicode 19 | :param raw_body: Raw response body. 20 | :type raw_body: str | unicode 21 | 22 | :ivar method: HTTP method in lowercase (e.g. "post"). 23 | :vartype method: str | unicode 24 | :ivar url: API URL. 25 | :vartype url: str | unicode 26 | :ivar headers: Response headers. 27 | :vartype headers: requests.structures.CaseInsensitiveDict | dict 28 | :ivar status_code: Response status code. 29 | :vartype status_code: int 30 | :ivar status_text: Response status text. 31 | :vartype status_text: str | unicode 32 | :ivar raw_body: Raw response body. 33 | :vartype raw_body: str | unicode 34 | :ivar body: JSON-deserialized response body. 35 | :vartype body: str | unicode | bool | int | list | dict 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 | unicode 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__(self, 58 | method, 59 | url, 60 | headers, 61 | status_code, 62 | status_text, 63 | raw_body): 64 | self.method = method.lower() 65 | self.url = url 66 | self.headers = headers 67 | self.status_code = status_code 68 | self.status_text = status_text 69 | self.raw_body = raw_body 70 | 71 | # Populated later 72 | self.body = None 73 | self.error_code = None 74 | self.error_message = None 75 | self.is_success = None 76 | -------------------------------------------------------------------------------- /aioarangodb/tests/__init__.py: -------------------------------------------------------------------------------- 1 | global_data = dict() 2 | -------------------------------------------------------------------------------- /aioarangodb/tests/arangodocker.py: -------------------------------------------------------------------------------- 1 | from pytest_docker_fixtures.containers._base import BaseImage 2 | from pytest_docker_fixtures.images import settings 3 | import requests 4 | 5 | 6 | settings['arango'] = { 7 | 'image': 'arangodb', 8 | 'version': '3.6.4', 9 | 'env': { 10 | 'ARANGO_ROOT_PASSWORD': 'secret', 11 | 'ARANGO_NO_AUTH': '1' 12 | } 13 | } 14 | 15 | class ArangoDB(BaseImage): 16 | name = 'arango' 17 | port = 8529 18 | 19 | def check(self): 20 | url = f'http://{self.host}:{self.get_port()}/' 21 | try: 22 | resp = requests.get(url) 23 | if resp.status_code == 200: 24 | return True 25 | except Exception: 26 | pass 27 | return False 28 | 29 | 30 | arango_image = ArangoDB() -------------------------------------------------------------------------------- /aioarangodb/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from aioarangodb.tests import global_data 2 | from aioarangodb.tests.helpers import generate_db_name 3 | pytest_plugins = ['aioarangodb.tests.fixtures'] 4 | 5 | 6 | def pytest_addoption(parser): 7 | parser.addoption('--passwd', action='store', default='secret') 8 | parser.addoption('--complete', action='store_true') 9 | parser.addoption('--cluster', action='store_true') 10 | parser.addoption('--replication', action='store_true') 11 | parser.addoption('--secret', action='store', default='secret') 12 | parser.addoption('--tst_db_name', action='store', default=generate_db_name()) 13 | 14 | def pytest_configure(config): 15 | global_data['secret'] = config.getoption('secret') 16 | global_data['tst_db_name'] = config.getoption('tst_db_name') 17 | global_data['replication'] = config.getoption('replication') 18 | global_data['cluster'] = config.getoption('cluster') 19 | global_data['complete'] = config.getoption('complete') 20 | global_data['passwd'] = config.getoption('passwd') 21 | global_data['username'] = 'root' 22 | -------------------------------------------------------------------------------- /aioarangodb/tests/executors.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from aioarangodb.executor import ( 4 | AsyncExecutor, 5 | BatchExecutor, 6 | TransactionExecutor, 7 | ) 8 | from aioarangodb.job import BatchJob 9 | 10 | 11 | class TestAsyncExecutor(AsyncExecutor): 12 | 13 | def __init__(self, connection): 14 | super(TestAsyncExecutor, self).__init__( 15 | connection=connection, 16 | return_result=True 17 | ) 18 | 19 | async def execute(self, request, response_handler): 20 | job = await AsyncExecutor.execute(self, request, response_handler) 21 | while job.status() != 'done': 22 | time.sleep(.01) 23 | return job.result() 24 | 25 | 26 | class TestBatchExecutor(BatchExecutor): 27 | 28 | def __init__(self, connection): 29 | super(TestBatchExecutor, self).__init__( 30 | connection=connection, 31 | return_result=True 32 | ) 33 | 34 | async def execute(self, request, response_handler): 35 | self._committed = False 36 | self._queue.clear() 37 | 38 | job = BatchJob(response_handler) 39 | self._queue[job.id] = (request, job) 40 | await self.commit() 41 | return await job.result() 42 | 43 | 44 | class TestTransactionExecutor(TransactionExecutor): 45 | 46 | # noinspection PyMissingConstructor 47 | def __init__(self, connection): 48 | self._conn = connection 49 | 50 | async def execute(self, request, response_handler): 51 | if request.read is request.write is request.exclusive is None: 52 | resp = self._conn.send_request(request) 53 | return response_handler(resp) 54 | 55 | super(TestTransactionExecutor, self).__init__( 56 | connection=self._conn, 57 | sync=True, 58 | allow_implicit=False, 59 | lock_timeout=0, 60 | read=request.read, 61 | write=request.write, 62 | exclusive=request.exclusive 63 | ) 64 | result = await TransactionExecutor.execute(self, request, response_handler) 65 | await self.commit() 66 | return result 67 | -------------------------------------------------------------------------------- /aioarangodb/tests/helpers.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from calendar import timegm 4 | from datetime import datetime 5 | from collections import deque 6 | from uuid import uuid4 7 | 8 | import jwt 9 | import pytest 10 | 11 | from aioarangodb.cursor import Cursor 12 | from aioarangodb.exceptions import ( 13 | AsyncExecuteError, 14 | BatchExecuteError, 15 | TransactionInitError 16 | ) 17 | 18 | 19 | def generate_db_name(): 20 | """Generate and return a random database name. 21 | 22 | :return: Random database name. 23 | :rtype: str | unicode 24 | """ 25 | return 'test_database_{}'.format(uuid4().hex) 26 | 27 | 28 | def generate_col_name(): 29 | """Generate and return a random collection name. 30 | 31 | :return: Random collection name. 32 | :rtype: str | unicode 33 | """ 34 | return 'test_collection_{}'.format(uuid4().hex) 35 | 36 | 37 | def generate_graph_name(): 38 | """Generate and return a random graph name. 39 | 40 | :return: Random graph name. 41 | :rtype: str | unicode 42 | """ 43 | return 'test_graph_{}'.format(uuid4().hex) 44 | 45 | 46 | def generate_doc_key(): 47 | """Generate and return a random document key. 48 | 49 | :return: Random document key. 50 | :rtype: str | unicode 51 | """ 52 | return 'test_document_{}'.format(uuid4().hex) 53 | 54 | 55 | def generate_task_name(): 56 | """Generate and return a random task name. 57 | 58 | :return: Random task name. 59 | :rtype: str | unicode 60 | """ 61 | return 'test_task_{}'.format(uuid4().hex) 62 | 63 | 64 | def generate_task_id(): 65 | """Generate and return a random task ID. 66 | 67 | :return: Random task ID 68 | :rtype: str | unicode 69 | """ 70 | return 'test_task_id_{}'.format(uuid4().hex) 71 | 72 | 73 | def generate_username(): 74 | """Generate and return a random username. 75 | 76 | :return: Random username. 77 | :rtype: str | unicode 78 | """ 79 | return 'test_user_{}'.format(uuid4().hex) 80 | 81 | 82 | def generate_view_name(): 83 | """Generate and return a random view name. 84 | 85 | :return: Random view name. 86 | :rtype: str | unicode 87 | """ 88 | return 'test_view_{}'.format(uuid4().hex) 89 | 90 | 91 | def generate_analyzer_name(): 92 | """Generate and return a random analyzer name. 93 | 94 | :return: Random analyzer name. 95 | :rtype: str | unicode 96 | """ 97 | return 'test_analyzer_{}'.format(uuid4().hex) 98 | 99 | 100 | def generate_string(): 101 | """Generate and return a random unique string. 102 | 103 | :return: Random unique string. 104 | :rtype: str | unicode 105 | """ 106 | return uuid4().hex 107 | 108 | 109 | def generate_service_mount(): 110 | """Generate and return a random service name. 111 | 112 | :return: Random service name. 113 | :rtype: str | unicode 114 | """ 115 | return '/test_{}'.format(uuid4().hex) 116 | 117 | 118 | def generate_jwt(secret, exp=3600): 119 | """Generate and return a JWT. 120 | 121 | :param secret: JWT secret 122 | :type secret: str | unicode 123 | :param exp: Time to expire in seconds. 124 | :type exp: int 125 | :return: JWT 126 | :rtype: str | unicode 127 | """ 128 | now = timegm(datetime.utcnow().utctimetuple()) 129 | return jwt.encode( 130 | payload={ 131 | 'iat': now, 132 | 'exp': now + exp, 133 | 'iss': 'arangodb', 134 | 'server_id': 'client' 135 | }, 136 | key=secret, 137 | ).decode('utf-8') 138 | 139 | 140 | async def clean_doc(obj): 141 | """Return the document(s) with all extra system keys stripped. 142 | 143 | :param obj: document(s) 144 | :type obj: list | dict | arango.cursor.Cursor 145 | :return: Document(s) with the system keys stripped 146 | :rtype: list | dict 147 | """ 148 | if isinstance(obj, (Cursor)): 149 | docs = [await clean_doc(d) async for d in obj] 150 | return sorted(docs, key=lambda doc: doc['_key']) 151 | 152 | if isinstance(obj, (list, deque)): 153 | docs = [await clean_doc(d) for d in obj] 154 | return sorted(docs, key=lambda doc: doc['_key']) 155 | 156 | if isinstance(obj, dict): 157 | return { 158 | field: value for field, value in obj.items() 159 | if field in {'_key', '_from', '_to'} or not field.startswith('_') 160 | } 161 | 162 | 163 | async def empty_collection(collection): 164 | """Empty all the documents in the collection. 165 | 166 | :param collection: Collection name 167 | :type collection: arango.collection.StandardCollection 168 | """ 169 | async for doc_id in await collection.ids(): 170 | await collection.delete(doc_id, sync=True) 171 | 172 | 173 | async def extract(key, items): 174 | """Return the sorted values from dicts using the given key. 175 | 176 | :param key: Dictionary key 177 | :type key: str | unicode 178 | :param items: Items to filter. 179 | :type items: [dict] 180 | :return: Set of values. 181 | :rtype: [str | unicode] 182 | """ 183 | if isinstance(items, (Cursor)): 184 | return sorted([item[key] async for item in items]) 185 | else: 186 | return sorted(item[key] for item in items) 187 | 188 | 189 | def assert_raises(*exc): 190 | """Assert that the given exception is raised. 191 | 192 | :param exc: Expected exception(s). 193 | :type: exc 194 | """ 195 | # noinspection PyTypeChecker 196 | return pytest.raises( 197 | exc + (AsyncExecuteError, BatchExecuteError, TransactionInitError) 198 | ) 199 | -------------------------------------------------------------------------------- /aioarangodb/tests/static/keyfile: -------------------------------------------------------------------------------- 1 | secret -------------------------------------------------------------------------------- /aioarangodb/tests/static/service.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getflaps/aioarangodb/b7e4c888e9bb3a93c7efbb3b0321de28ee5b5ce9/aioarangodb/tests/static/service.zip -------------------------------------------------------------------------------- /aioarangodb/tests/test_analyzer.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | import pytest 3 | 4 | from aioarangodb.exceptions import ( 5 | AnalyzerCreateError, 6 | AnalyzerDeleteError, 7 | AnalyzerGetError, 8 | AnalyzerListError 9 | ) 10 | from aioarangodb.tests.helpers import assert_raises, generate_analyzer_name 11 | 12 | 13 | @pytest.mark.asyncio 14 | async def test_analyzer_management(db, bad_db, cluster): 15 | analyzer_name = generate_analyzer_name() 16 | full_analyzer_name = db.name + '::' + analyzer_name 17 | bad_analyzer_name = generate_analyzer_name() 18 | 19 | # Test create analyzer 20 | result = await db.create_analyzer(analyzer_name, 'identity', {}) 21 | assert result['name'] == full_analyzer_name 22 | assert result['type'] == 'identity' 23 | assert result['properties'] == {} 24 | assert result['features'] == [] 25 | 26 | # Test create duplicate with bad database 27 | with assert_raises(AnalyzerCreateError) as err: 28 | await bad_db.create_analyzer(analyzer_name, 'identity', {}, []) 29 | assert err.value.error_code in {11, 1228} 30 | 31 | # Test get analyzer 32 | result = await db.analyzer(analyzer_name) 33 | assert result['name'] == full_analyzer_name 34 | assert result['type'] == 'identity' 35 | assert result['properties'] == {} 36 | assert result['features'] == [] 37 | 38 | # Test get missing analyzer 39 | with assert_raises(AnalyzerGetError) as err: 40 | await db.analyzer(bad_analyzer_name) 41 | assert err.value.error_code in {1202} 42 | 43 | # Test list analyzers 44 | result = await db.analyzers() 45 | assert full_analyzer_name in [a['name'] for a in result] 46 | 47 | # Test list analyzers with bad database 48 | with assert_raises(AnalyzerListError) as err: 49 | await bad_db.analyzers() 50 | assert err.value.error_code in {11, 1228} 51 | 52 | # Test delete analyzer 53 | assert await db.delete_analyzer(analyzer_name, force=True) is True 54 | assert full_analyzer_name not in [a['name'] for a in await db.analyzers()] 55 | 56 | # Test delete missing analyzer 57 | with assert_raises(AnalyzerDeleteError) as err: 58 | await db.delete_analyzer(analyzer_name) 59 | assert err.value.error_code in {1202} 60 | 61 | # Test delete missing analyzer with ignore_missing set to True 62 | assert await db.delete_analyzer(analyzer_name, ignore_missing=True) is False 63 | -------------------------------------------------------------------------------- /aioarangodb/tests/test_auth.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | import pytest 3 | from six import string_types 4 | from aioarangodb.connection import ( 5 | BasicConnection, 6 | JWTConnection, 7 | JWTSuperuserConnection 8 | ) 9 | from aioarangodb.exceptions import ( 10 | JWTAuthError, 11 | # JWTSecretListError, 12 | # JWTSecretReloadError, 13 | ServerTLSError, 14 | ServerTLSReloadError, 15 | ServerVersionError 16 | ) 17 | from aioarangodb.errno import FORBIDDEN, HTTP_UNAUTHORIZED 18 | from aioarangodb.tests.helpers import assert_raises, generate_string, generate_jwt 19 | pytestmark = pytest.mark.asyncio 20 | 21 | 22 | async def test_auth_invalid_method(client, sys_db, db_name, username, password): 23 | with assert_raises(ValueError) as err: 24 | await client.db( 25 | name=db_name, 26 | username=username, 27 | password=password, 28 | verify=True, 29 | auth_method='bad_method' 30 | ) 31 | assert 'invalid auth_method' in str(err.value) 32 | 33 | 34 | async def test_auth_basic(client, sys_db, db_name, username, password): 35 | db = await client.db( 36 | name=db_name, 37 | username=username, 38 | password=password, 39 | verify=True, 40 | auth_method='basic' 41 | ) 42 | assert isinstance(db.conn, BasicConnection) 43 | assert isinstance(await db.version(), string_types) 44 | assert isinstance(await db.properties(), dict) 45 | 46 | 47 | async def test_auth_jwt(client, sys_db, db_name, username, password): 48 | db = await client.db( 49 | name=db_name, 50 | username=username, 51 | password=password, 52 | verify=True, 53 | auth_method='jwt' 54 | ) 55 | assert isinstance(db.conn, JWTConnection) 56 | assert isinstance(await db.version(), string_types) 57 | assert isinstance(await db.properties(), dict) 58 | 59 | bad_password = generate_string() 60 | with assert_raises(JWTAuthError) as err: 61 | await client.db(db_name, username, bad_password, auth_method='jwt') 62 | assert err.value.error_code == HTTP_UNAUTHORIZED 63 | 64 | 65 | async def test_auth_superuser_token(client, sys_db, db_name, root_password, secret): 66 | token = generate_jwt(secret) 67 | db = await client.db('_system', superuser_token=token) 68 | bad_db = await client.db('_system', superuser_token='bad_token') 69 | 70 | assert isinstance(db.conn, JWTSuperuserConnection) 71 | assert isinstance(await db.version(), string_types) 72 | assert isinstance(await db.properties(), dict) 73 | 74 | # # Test get JWT secrets 75 | # secrets = db.jwt_secrets() 76 | # assert 'active' in secrets 77 | # assert 'passive' in secrets 78 | # 79 | # # Test get JWT secrets with bad database 80 | # with assert_raises(JWTSecretListError) as err: 81 | # bad_db.jwt_secrets() 82 | # assert err.value.error_code == FORBIDDEN 83 | # 84 | # # Test reload JWT secrets 85 | # secrets = db.reload_jwt_secrets() 86 | # assert 'active' in secrets 87 | # assert 'passive' in secrets 88 | # 89 | # # Test reload JWT secrets with bad database 90 | # with assert_raises(JWTSecretReloadError) as err: 91 | # bad_db.reload_jwt_secrets() 92 | # assert err.value.error_code == FORBIDDEN 93 | 94 | # Test get TLS data 95 | result = await db.tls() 96 | assert isinstance(result, dict) 97 | 98 | # Test get TLS data with bad database 99 | with assert_raises(ServerTLSError) as err: 100 | await bad_db.tls() 101 | assert err.value.error_code == FORBIDDEN 102 | 103 | # Test reload TLS 104 | result = await db.reload_tls() 105 | assert isinstance(result, dict) 106 | 107 | # Test reload TLS with bad database 108 | with assert_raises(ServerTLSReloadError) as err: 109 | await bad_db.reload_tls() 110 | assert err.value.error_code == FORBIDDEN 111 | 112 | 113 | async def test_auth_jwt_expiry(client, sys_db, db_name, root_password, secret): 114 | # Test automatic token refresh on expired token. 115 | db = await client.db('_system', 'root', root_password, auth_method='jwt') 116 | expired_token = generate_jwt(secret, exp=0) 117 | db.conn._token = expired_token 118 | db.conn._auth_header = 'bearer {}'.format(expired_token) 119 | assert isinstance(await db.version(), string_types) 120 | 121 | # Test correct error on token expiry. 122 | db = await client.db('_system', superuser_token=expired_token) 123 | with assert_raises(ServerVersionError) as err: 124 | await db.version() 125 | assert err.value.error_code == FORBIDDEN 126 | -------------------------------------------------------------------------------- /aioarangodb/tests/test_batch.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import json 4 | 5 | import mock 6 | import pytest 7 | from six import string_types 8 | 9 | from aioarangodb.database import BatchDatabase 10 | from aioarangodb.exceptions import ( 11 | DocumentInsertError, 12 | BatchExecuteError, 13 | BatchJobResultError, 14 | BatchStateError 15 | ) 16 | from aioarangodb.job import BatchJob 17 | from aioarangodb.tests.helpers import extract, clean_doc 18 | pytestmark = pytest.mark.asyncio 19 | 20 | async def test_batch_wrapper_attributes(db, col, username): 21 | batch_db = db.begin_batch_execution() 22 | assert isinstance(batch_db, BatchDatabase) 23 | assert batch_db.username == username 24 | assert batch_db.context == 'batch' 25 | assert batch_db.db_name == db.name 26 | assert batch_db.name == db.name 27 | assert repr(batch_db) == ''.format(db.name) 28 | 29 | batch_col = batch_db.collection(col.name) 30 | assert batch_col.username == username 31 | assert batch_col.context == 'batch' 32 | assert batch_col.db_name == db.name 33 | assert batch_col.name == col.name 34 | 35 | batch_aql = batch_db.aql 36 | assert batch_aql.username == username 37 | assert batch_aql.context == 'batch' 38 | assert batch_aql.db_name == db.name 39 | 40 | job = await batch_aql.execute('INVALID QUERY') 41 | assert isinstance(job, BatchJob) 42 | assert isinstance(job.id, string_types) 43 | assert repr(job) == ''.format(job.id) 44 | 45 | 46 | async def test_batch_execute_without_result(db, col, docs): 47 | async with db.begin_batch_execution(return_result=False) as batch_db: 48 | batch_col = batch_db.collection(col.name) 49 | # Ensure that no jobs are returned 50 | assert await batch_col.insert(docs[0]) is None 51 | assert await batch_col.delete(docs[0]) is None 52 | assert await batch_col.insert(docs[1]) is None 53 | assert await batch_col.delete(docs[1]) is None 54 | assert await batch_col.insert(docs[2]) is None 55 | assert await batch_col.get(docs[2]) is None 56 | assert batch_db.queued_jobs() is None 57 | 58 | # Ensure that the operations went through 59 | assert batch_db.queued_jobs() is None 60 | assert await extract('_key', await col.all()) == [docs[2]['_key']] 61 | 62 | 63 | async def test_batch_execute_with_result(db, col, docs): 64 | async with db.begin_batch_execution(return_result=True) as batch_db: 65 | batch_col = batch_db.collection(col.name) 66 | job1 = await batch_col.insert(docs[0]) 67 | job2 = await batch_col.insert(docs[1]) 68 | job3 = await batch_col.insert(docs[1]) # duplicate 69 | jobs = batch_db.queued_jobs() 70 | assert jobs == [job1, job2, job3] 71 | assert all(job.status() == 'pending' for job in jobs) 72 | 73 | assert batch_db.queued_jobs() == [job1, job2, job3] 74 | assert all(job.status() == 'done' for job in batch_db.queued_jobs()) 75 | assert await extract('_key', await col.all()) == await extract('_key', docs[:2]) 76 | 77 | # Test successful results 78 | assert job1.result()['_key'] == docs[0]['_key'] 79 | assert job2.result()['_key'] == docs[1]['_key'] 80 | 81 | # Test insert error result 82 | with pytest.raises(DocumentInsertError) as err: 83 | await job3.result() 84 | assert err.value.error_code == 1210 85 | 86 | 87 | async def test_batch_empty_commit(db): 88 | batch_db = db.begin_batch_execution(return_result=False) 89 | assert await batch_db.commit() is None 90 | 91 | batch_db = db.begin_batch_execution(return_result=True) 92 | assert await batch_db.commit() == [] 93 | 94 | 95 | async def test_batch_double_commit(db, col, docs): 96 | batch_db = db.begin_batch_execution() 97 | job = await batch_db.collection(col.name).insert(docs[0]) 98 | 99 | # Test first commit 100 | assert await batch_db.commit() == [job] 101 | assert job.status() == 'done' 102 | assert await col.count() == 1 103 | assert await clean_doc(await col.random()) == docs[0] 104 | 105 | # Test second commit which should fail 106 | with pytest.raises(BatchStateError) as err: 107 | await batch_db.commit() 108 | assert 'already committed' in str(err.value) 109 | assert await col.count() == 1 110 | assert await clean_doc(await col.random()) == docs[0] 111 | 112 | 113 | async def test_batch_action_after_commit(db, col): 114 | async with db.begin_batch_execution() as batch_db: 115 | await batch_db.collection(col.name).insert({}) 116 | 117 | # Test insert after the batch has been committed 118 | with pytest.raises(BatchStateError) as err: 119 | await batch_db.collection(col.name).insert({}) 120 | assert 'already committed' in str(err.value) 121 | assert await col.count() == 1 122 | 123 | 124 | async def test_batch_execute_error(bad_db, col, docs): 125 | batch_db = bad_db.begin_batch_execution(return_result=True) 126 | job = await batch_db.collection(col.name).insert_many(docs) 127 | 128 | # Test batch execute with bad database 129 | with pytest.raises(BatchExecuteError) as err: 130 | await batch_db.commit() 131 | assert err.value.error_code in {11, 1228} 132 | assert await col.count() == 0 133 | assert job.status() == 'pending' 134 | 135 | 136 | async def test_batch_job_result_not_ready(db, col, docs): 137 | batch_db = db.begin_batch_execution(return_result=True) 138 | job = await batch_db.collection(col.name).insert_many(docs) 139 | 140 | # Test get job result before commit 141 | with pytest.raises(BatchJobResultError) as err: 142 | await job.result() 143 | assert str(err.value) == 'result not available yet' 144 | 145 | # Test commit to make sure it still works after the errors 146 | assert await batch_db.commit() == [job] 147 | assert len(job.result()) == len(docs) 148 | assert await extract('_key', await col.all()) == await extract('_key', docs) 149 | 150 | 151 | class AsyncMock(mock.MagicMock): 152 | async def __call__(self, *args, **kwargs): 153 | return super(AsyncMock, self).__call__(*args, **kwargs) 154 | 155 | async def test_batch_bad_state(db, col, docs): 156 | batch_db = db.begin_batch_execution() 157 | batch_col = batch_db.collection(col.name) 158 | await batch_col.insert(docs[0]) 159 | await batch_col.insert(docs[1]) 160 | await batch_col.insert(docs[2]) 161 | 162 | # Monkey patch the connection object 163 | mock_resp = mock.MagicMock() 164 | mock_resp.is_success = True 165 | mock_resp.raw_body = '' 166 | mock_send_request = AsyncMock() 167 | mock_send_request.return_value = mock_resp 168 | mock_connection = mock.MagicMock() 169 | mock_connection.send_request = mock_send_request 170 | mock_connection.serialize = json.dumps 171 | mock_connection.deserialize = json.loads 172 | batch_db._executor._conn = mock_connection 173 | 174 | # Test commit with invalid batch state 175 | with pytest.raises(BatchStateError) as err: 176 | await batch_db.commit() 177 | assert 'expecting 3 parts in batch response but got 0' in str(err.value) 178 | -------------------------------------------------------------------------------- /aioarangodb/tests/test_client.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import json 4 | 5 | import pytest 6 | 7 | from aioarangodb.client import ArangoClient 8 | from aioarangodb.database import StandardDatabase 9 | from aioarangodb.exceptions import ServerConnectionError 10 | from aioarangodb.http import DefaultHTTPClient 11 | from aioarangodb.resolver import ( 12 | SingleHostResolver, 13 | RandomHostResolver, 14 | RoundRobinHostResolver 15 | ) 16 | from aioarangodb.version import __version__ 17 | from aioarangodb.tests.arangodocker import arango_image 18 | from aioarangodb.tests.helpers import ( 19 | generate_db_name, 20 | generate_username, 21 | generate_string 22 | ) 23 | pytestmark = pytest.mark.asyncio 24 | 25 | 26 | async def test_client_attributes(client): 27 | http_client = DefaultHTTPClient() 28 | 29 | client = ArangoClient( 30 | hosts=f'http://127.0.0.1:{arango_image.get_port()}', 31 | http_client=http_client 32 | ) 33 | assert client.version == __version__ 34 | assert client.hosts == [f'http://127.0.0.1:{arango_image.get_port()}'] 35 | 36 | assert repr(client) == f'' 37 | assert isinstance(client._host_resolver, SingleHostResolver) 38 | 39 | client_repr = f'' 40 | client_hosts = [f'http://127.0.0.1:{arango_image.get_port()}', f'http://localhost:{arango_image.get_port()}'] 41 | 42 | await client.close() 43 | client = ArangoClient( 44 | hosts=f'http://127.0.0.1:{arango_image.get_port()},http://localhost' 45 | f':{arango_image.get_port()}', 46 | http_client=http_client, 47 | serializer=json.dumps, 48 | deserializer=json.loads, 49 | ) 50 | assert client.version == __version__ 51 | assert client.hosts == client_hosts 52 | assert repr(client) == client_repr 53 | assert isinstance(client._host_resolver, RoundRobinHostResolver) 54 | 55 | await client.close() 56 | client = ArangoClient( 57 | hosts=client_hosts, 58 | host_resolver='random', 59 | http_client=http_client, 60 | serializer=json.dumps, 61 | deserializer=json.loads, 62 | ) 63 | assert client.version == __version__ 64 | assert client.hosts == client_hosts 65 | assert repr(client) == client_repr 66 | assert isinstance(client._host_resolver, RandomHostResolver) 67 | await client.close() 68 | 69 | 70 | async def test_client_good_connection(db, username, password): 71 | client = ArangoClient(hosts=f'http://127.0.0.1:{arango_image.get_port()}') 72 | 73 | # Test connection with verify flag on and off 74 | for verify in (True, False): 75 | db = await client.db(db.name, username, password, verify=verify) 76 | assert isinstance(db, StandardDatabase) 77 | assert db.name == db.name 78 | assert db.username == username 79 | assert db.context == 'default' 80 | await client.close() 81 | 82 | 83 | 84 | async def test_client_bad_connection(db, username, password, cluster): 85 | client = ArangoClient(hosts=f'http://127.0.0.1:{arango_image.get_port()}') 86 | 87 | bad_db_name = generate_db_name() 88 | bad_username = generate_username() 89 | bad_password = generate_string() 90 | 91 | if not cluster: 92 | # Test connection with bad username password 93 | with pytest.raises(ServerConnectionError): 94 | await client.db(db.name, bad_username, bad_password, verify=True) 95 | 96 | # Test connection with missing database 97 | with pytest.raises(ServerConnectionError): 98 | await client.db(bad_db_name, bad_username, bad_password, verify=True) 99 | await client.close() 100 | 101 | # Test connection with invalid host URL 102 | client = ArangoClient(hosts='http://127.0.0.1:8500') 103 | with pytest.raises(ServerConnectionError) as err: 104 | await client.db(db.name, username, password, verify=True) 105 | assert 'bad connection' in str(err.value) 106 | await client.close() 107 | 108 | 109 | async def test_client_custom_http_client(db, username, password): 110 | 111 | # Define custom HTTP client which increments the counter on any API call. 112 | class MyHTTPClient(DefaultHTTPClient): 113 | 114 | def __init__(self): 115 | super(MyHTTPClient, self).__init__() 116 | self.counter = 0 117 | 118 | async def send_request( 119 | self, 120 | session, 121 | method, 122 | url, 123 | headers=None, 124 | params=None, 125 | data=None, 126 | auth=None): 127 | self.counter += 1 128 | return await super(MyHTTPClient, self).send_request( 129 | session, method, url, headers, params, data, auth 130 | ) 131 | 132 | http_client = MyHTTPClient() 133 | client = ArangoClient( 134 | hosts=f'http://127.0.0.1:{arango_image.get_port()}', 135 | http_client=http_client 136 | ) 137 | # Set verify to True to send a test API call on initialization. 138 | await client.db(db.name, username, password, verify=True) 139 | assert http_client.counter == 1 140 | await client.close() 141 | -------------------------------------------------------------------------------- /aioarangodb/tests/test_cluster.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import pytest 4 | 5 | from aioarangodb.errno import ( 6 | DATABASE_NOT_FOUND, 7 | FORBIDDEN 8 | ) 9 | from aioarangodb.exceptions import ( 10 | ClusterEndpointsError, 11 | ClusterHealthError, 12 | ClusterMaintenanceModeError, 13 | ClusterServerEngineError, 14 | ClusterServerIDError, 15 | ClusterServerRoleError, 16 | ClusterServerStatisticsError, 17 | ClusterServerVersionError, 18 | ) 19 | from aioarangodb.tests.helpers import assert_raises 20 | pytestmark = pytest.mark.asyncio 21 | 22 | 23 | async def test_cluster_server_id(sys_db, bad_db, cluster): 24 | if not cluster: 25 | pytest.skip('Only tested in a cluster setup') 26 | 27 | result = await sys_db.cluster.server_id() 28 | assert isinstance(result, str) 29 | 30 | with assert_raises(ClusterServerIDError) as err: 31 | await bad_db.cluster.server_id() 32 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 33 | 34 | 35 | async def test_cluster_server_role(sys_db, bad_db, cluster): 36 | if not cluster: 37 | pytest.skip('Only tested in a cluster setup') 38 | 39 | result = await sys_db.cluster.server_role() 40 | assert isinstance(result, str) 41 | 42 | with assert_raises(ClusterServerRoleError) as err: 43 | await bad_db.cluster.server_role() 44 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 45 | 46 | 47 | async def test_cluster_health(sys_db, bad_db, cluster): 48 | if not cluster: 49 | pytest.skip('Only tested in a cluster setup') 50 | 51 | result = await sys_db.cluster.health() 52 | assert 'Health' in result 53 | assert 'ClusterId' in result 54 | 55 | with assert_raises(ClusterHealthError) as err: 56 | await bad_db.cluster.health() 57 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 58 | 59 | 60 | async def test_cluster_server_version(sys_db, bad_db, cluster): 61 | if not cluster: 62 | pytest.skip('Only tested in a cluster setup') 63 | 64 | server_id = await sys_db.cluster.server_id() 65 | result = await sys_db.cluster.server_version(server_id) 66 | assert 'server' in result 67 | assert 'version' in result 68 | 69 | with assert_raises(ClusterServerVersionError) as err: 70 | await bad_db.cluster.server_version(server_id) 71 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 72 | 73 | 74 | async def test_cluster_server_engine(sys_db, bad_db, cluster): 75 | if not cluster: 76 | pytest.skip('Only tested in a cluster setup') 77 | 78 | server_id = await sys_db.cluster.server_id() 79 | result = await sys_db.cluster.server_engine(server_id) 80 | assert 'name' in result 81 | assert 'supports' in result 82 | 83 | with assert_raises(ClusterServerEngineError) as err: 84 | bad_db.cluster.server_engine(server_id) 85 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 86 | 87 | 88 | async def test_cluster_server_statistics(sys_db, bad_db, cluster): 89 | if not cluster: 90 | pytest.skip('Only tested in a cluster setup') 91 | 92 | server_id = await sys_db.cluster.server_id() 93 | result = await sys_db.cluster.server_statistics(server_id) 94 | assert 'time' in result 95 | assert 'system' in result 96 | assert 'enabled' in result 97 | 98 | with assert_raises(ClusterServerStatisticsError) as err: 99 | await bad_db.cluster.server_statistics(server_id) 100 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 101 | 102 | 103 | async def test_cluster_toggle_maintenance_mode(sys_db, bad_db, cluster): 104 | if not cluster: 105 | pytest.skip('Only tested in a cluster setup') 106 | 107 | result = await sys_db.cluster.toggle_maintenance_mode('on') 108 | assert 'error' in result 109 | assert 'warning' in result 110 | 111 | result = await sys_db.cluster.toggle_maintenance_mode('off') 112 | assert 'error' in result 113 | assert 'warning' in result 114 | 115 | with assert_raises(ClusterMaintenanceModeError) as err: 116 | await bad_db.cluster.toggle_maintenance_mode('on') 117 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 118 | 119 | 120 | async def test_cluster_endpoints(db, bad_db, cluster): 121 | if not cluster: 122 | pytest.skip('Only tested in a cluster setup') 123 | 124 | # Test get server endpoints 125 | assert len(await db.cluster.endpoints()) > 0 126 | 127 | # Test get server endpoints with bad database 128 | with assert_raises(ClusterEndpointsError) as err: 129 | await bad_db.cluster.endpoints() 130 | assert err.value.error_code in {11, 1228} 131 | -------------------------------------------------------------------------------- /aioarangodb/tests/test_exception.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import json 4 | 5 | import pytest 6 | # from requests.structures import CaseInsensitiveDict 7 | 8 | from aioarangodb.exceptions import ( 9 | ArangoServerError, 10 | DocumentInsertError, 11 | DocumentParseError, 12 | ArangoClientError 13 | ) 14 | from aioarangodb.request import Request 15 | from aioarangodb.response import Response 16 | pytestmark = pytest.mark.asyncio 17 | 18 | async def test_server_error(client, col, docs): 19 | document = docs[0] 20 | with pytest.raises(DocumentInsertError) as err: 21 | await col.insert(document, return_new=False) 22 | await col.insert(document, return_new=False) # duplicate key error 23 | exc = err.value 24 | 25 | assert isinstance(exc, ArangoServerError) 26 | assert exc.source == 'server' 27 | assert exc.message == str(exc) 28 | assert exc.message.startswith('[HTTP 409][ERR 1210] unique constraint') 29 | assert exc.error_code == 1210 30 | assert exc.http_method == 'post' 31 | assert exc.http_code == 409 32 | # assert isinstance(exc.http_headers, CaseInsensitiveDict) 33 | 34 | resp = exc.response 35 | expected_body = { 36 | 'code': exc.http_code, 37 | 'error': True, 38 | 'errorNum': exc.error_code, 39 | 'errorMessage': exc.error_message 40 | } 41 | assert isinstance(resp, Response) 42 | assert resp.is_success is False 43 | assert resp.error_code == exc.error_code 44 | assert resp.body == expected_body 45 | assert resp.error_code == 1210 46 | assert resp.method == 'post' 47 | assert resp.status_code == 409 48 | assert resp.status_text == 'Conflict' 49 | 50 | assert json.loads(resp.raw_body) == expected_body 51 | assert resp.headers == exc.http_headers 52 | 53 | req = exc.request 54 | assert isinstance(req, Request) 55 | assert req.headers['content-type'] == 'application/json' 56 | assert req.method == 'post' 57 | assert req.params['silent'] == 0 58 | assert req.params['returnNew'] == 0 59 | assert req.data == document 60 | assert req.endpoint.startswith('/_api/document/' + col.name) 61 | 62 | 63 | async def test_client_error(col): 64 | with pytest.raises(DocumentParseError) as err: 65 | await col.get({'_id': 'invalid'}) # malformed document 66 | exc = err.value 67 | 68 | assert isinstance(exc, ArangoClientError) 69 | assert exc.source == 'client' 70 | assert exc.error_code is None 71 | assert exc.error_message is None 72 | assert exc.message == str(exc) 73 | assert exc.message.startswith('bad collection name') 74 | assert exc.url is None 75 | assert exc.http_method is None 76 | assert exc.http_code is None 77 | assert exc.http_headers is None 78 | assert exc.response is None 79 | -------------------------------------------------------------------------------- /aioarangodb/tests/test_index.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import pytest 4 | from aioarangodb.exceptions import ( 5 | IndexListError, 6 | IndexCreateError, 7 | IndexDeleteError, 8 | IndexLoadError 9 | ) 10 | from aioarangodb.tests.helpers import assert_raises, extract 11 | pytestmark = pytest.mark.asyncio 12 | 13 | 14 | async def test_list_indexes(icol, bad_col): 15 | indexes = await icol.indexes() 16 | assert isinstance(indexes, list) 17 | assert len(indexes) > 0 18 | assert 'id' in indexes[0] 19 | assert 'type' in indexes[0] 20 | assert 'fields' in indexes[0] 21 | assert 'selectivity' in indexes[0] 22 | assert 'sparse' in indexes[0] 23 | assert 'unique' in indexes[0] 24 | 25 | with assert_raises(IndexListError) as err: 26 | await bad_col.indexes() 27 | assert err.value.error_code in {11, 1228} 28 | 29 | 30 | async def test_add_hash_index(icol): 31 | icol = icol 32 | 33 | fields = ['attr1', 'attr2'] 34 | result = await icol.add_hash_index( 35 | fields=fields, 36 | unique=True, 37 | sparse=True, 38 | deduplicate=True, 39 | name='hash_index', 40 | in_background=False 41 | ) 42 | 43 | expected_index = { 44 | 'sparse': True, 45 | 'type': 'hash', 46 | 'fields': ['attr1', 'attr2'], 47 | 'unique': True, 48 | 'deduplicate': True, 49 | 'name': 'hash_index' 50 | } 51 | for key, value in expected_index.items(): 52 | assert result[key] == value 53 | 54 | assert result['id'] in await extract('id', await icol.indexes()) 55 | 56 | # Clean up the index 57 | await icol.delete_index(result['id']) 58 | 59 | 60 | async def test_add_skiplist_index(icol): 61 | fields = ['attr1', 'attr2'] 62 | result = await icol.add_skiplist_index( 63 | fields=fields, 64 | unique=True, 65 | sparse=True, 66 | deduplicate=True, 67 | name='skiplist_index', 68 | in_background=False 69 | ) 70 | 71 | expected_index = { 72 | 'sparse': True, 73 | 'type': 'skiplist', 74 | 'fields': ['attr1', 'attr2'], 75 | 'unique': True, 76 | 'deduplicate': True, 77 | 'name': 'skiplist_index' 78 | } 79 | for key, value in expected_index.items(): 80 | assert result[key] == value 81 | 82 | assert result['id'] in await extract('id', await icol.indexes()) 83 | 84 | # Clean up the index 85 | await icol.delete_index(result['id']) 86 | 87 | 88 | async def test_add_geo_index(icol): 89 | # Test add geo index with one attribute 90 | result = await icol.add_geo_index( 91 | fields=['attr1'], 92 | ordered=False, 93 | name='geo_index', 94 | in_background=True 95 | ) 96 | 97 | expected_index = { 98 | 'sparse': True, 99 | 'type': 'geo', 100 | 'fields': ['attr1'], 101 | 'unique': False, 102 | 'geo_json': False, 103 | 'name': 'geo_index' 104 | } 105 | for key, value in expected_index.items(): 106 | assert result[key] == value 107 | 108 | assert result['id'] in await extract('id', await icol.indexes()) 109 | 110 | # Test add geo index with two attributes 111 | result = await icol.add_geo_index( 112 | fields=['attr1', 'attr2'], 113 | ordered=False, 114 | ) 115 | expected_index = { 116 | 'sparse': True, 117 | 'type': 'geo', 118 | 'fields': ['attr1', 'attr2'], 119 | 'unique': False, 120 | } 121 | for key, value in expected_index.items(): 122 | assert result[key] == value 123 | 124 | assert result['id'] in await extract('id', await icol.indexes()) 125 | 126 | # Test add geo index with more than two attributes (should fail) 127 | with assert_raises(IndexCreateError) as err: 128 | await icol.add_geo_index(fields=['attr1', 'attr2', 'attr3']) 129 | assert err.value.error_code == 10 130 | 131 | # Clean up the index 132 | await icol.delete_index(result['id']) 133 | 134 | 135 | async def test_add_fulltext_index(icol): 136 | # Test add fulltext index with one attributes 137 | result = await icol.add_fulltext_index( 138 | fields=['attr1'], 139 | min_length=10, 140 | name='fulltext_index', 141 | in_background=True 142 | ) 143 | expected_index = { 144 | 'sparse': True, 145 | 'type': 'fulltext', 146 | 'fields': ['attr1'], 147 | 'min_length': 10, 148 | 'unique': False, 149 | 'name': 'fulltext_index' 150 | } 151 | for key, value in expected_index.items(): 152 | assert result[key] == value 153 | 154 | assert result['id'] in await extract('id', await icol.indexes()) 155 | 156 | # Test add fulltext index with two attributes (should fail) 157 | with assert_raises(IndexCreateError) as err: 158 | await icol.add_fulltext_index(fields=['attr1', 'attr2']) 159 | assert err.value.error_code == 10 160 | 161 | # Clean up the index 162 | await icol.delete_index(result['id']) 163 | 164 | 165 | async def test_add_persistent_index(icol): 166 | # Test add persistent index with two attributes 167 | result = await icol.add_persistent_index( 168 | fields=['attr1', 'attr2'], 169 | unique=True, 170 | sparse=True, 171 | name='persistent_index', 172 | in_background=True 173 | ) 174 | expected_index = { 175 | 'sparse': True, 176 | 'type': 'persistent', 177 | 'fields': ['attr1', 'attr2'], 178 | 'unique': True, 179 | 'name': 'persistent_index' 180 | } 181 | for key, value in expected_index.items(): 182 | assert result[key] == value 183 | 184 | assert result['id'] in await extract('id', await icol.indexes()) 185 | 186 | # Clean up the index 187 | await icol.delete_index(result['id']) 188 | 189 | 190 | async def test_add_ttl_index(icol): 191 | # Test add persistent index with two attributes 192 | result = await icol.add_ttl_index( 193 | fields=['attr1'], 194 | expiry_time=1000, 195 | name='ttl_index', 196 | in_background=True 197 | ) 198 | expected_index = { 199 | 'type': 'ttl', 200 | 'fields': ['attr1'], 201 | 'expiry_time': 1000, 202 | 'name': 'ttl_index' 203 | } 204 | for key, value in expected_index.items(): 205 | assert result[key] == value 206 | 207 | assert result['id'] in await extract('id', await icol.indexes()) 208 | 209 | # Clean up the index 210 | await icol.delete_index(result['id']) 211 | 212 | 213 | async def test_delete_index(icol, bad_col): 214 | old_indexes = set(await extract('id', await icol.indexes())) 215 | icol.add_hash_index(['attr3', 'attr4'], unique=True) 216 | icol.add_skiplist_index(['attr3', 'attr4'], unique=True) 217 | icol.add_fulltext_index(fields=['attr3'], min_length=10) 218 | 219 | new_indexes = set(await extract('id', await icol.indexes())) 220 | assert new_indexes.issuperset(old_indexes) 221 | 222 | indexes_to_delete = new_indexes - old_indexes 223 | for index_id in indexes_to_delete: 224 | assert await icol.delete_index(index_id) is True 225 | 226 | new_indexes = set(await extract('id', await icol.indexes())) 227 | assert new_indexes == old_indexes 228 | 229 | # Test delete missing indexes 230 | for index_id in indexes_to_delete: 231 | assert await icol.delete_index(index_id, ignore_missing=True) is False 232 | for index_id in indexes_to_delete: 233 | with assert_raises(IndexDeleteError) as err: 234 | await icol.delete_index(index_id, ignore_missing=False) 235 | assert err.value.error_code == 1212 236 | 237 | # Test delete indexes with bad collection 238 | for index_id in indexes_to_delete: 239 | with assert_raises(IndexDeleteError) as err: 240 | await bad_col.delete_index(index_id, ignore_missing=False) 241 | assert err.value.error_code in {11, 1228} 242 | 243 | 244 | async def test_load_indexes(icol, bad_col): 245 | # Test load indexes 246 | assert await icol.load_indexes() is True 247 | 248 | # Test load indexes with bad collection 249 | with assert_raises(IndexLoadError) as err: 250 | await bad_col.load_indexes() 251 | assert err.value.error_code in {11, 1228} 252 | -------------------------------------------------------------------------------- /aioarangodb/tests/test_permission.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import pytest 4 | 5 | from aioarangodb.exceptions import ( 6 | CollectionCreateError, 7 | CollectionListError, 8 | CollectionPropertiesError, 9 | DocumentInsertError, 10 | PermissionResetError, 11 | PermissionGetError, 12 | PermissionUpdateError, 13 | PermissionListError 14 | ) 15 | from aioarangodb.tests.helpers import ( 16 | assert_raises, 17 | extract, 18 | generate_col_name, 19 | generate_db_name, 20 | generate_string, 21 | generate_username 22 | ) 23 | pytestmark = pytest.mark.asyncio 24 | 25 | 26 | async def test_permission_management(client, sys_db, bad_db, cluster): 27 | if cluster: 28 | pytest.skip('Not tested in a cluster setup') 29 | 30 | username = generate_username() 31 | password = generate_string() 32 | db_name = generate_db_name() 33 | col_name_1 = generate_col_name() 34 | col_name_2 = generate_col_name() 35 | 36 | await sys_db.create_database( 37 | name=db_name, 38 | users=[{ 39 | 'username': username, 40 | 'password': password, 41 | 'active': True 42 | }] 43 | ) 44 | db = await client.db(db_name, username, password) 45 | assert isinstance(await sys_db.permissions(username), dict) 46 | 47 | # Test list permissions with bad database 48 | with assert_raises(PermissionListError) as err: 49 | await bad_db.permissions(username) 50 | assert err.value.error_code in {11, 1228} 51 | 52 | # Test get permission with bad database 53 | with assert_raises(PermissionGetError) as err: 54 | await bad_db.permission(username, db_name) 55 | assert err.value.error_code in {11, 1228} 56 | 57 | # The user should not have read and write permissions 58 | assert await sys_db.permission(username, db_name) == 'rw' 59 | assert await sys_db.permission(username, db_name, col_name_1) == 'rw' 60 | 61 | # Test update permission (database level) with bad database 62 | with assert_raises(PermissionUpdateError): 63 | await bad_db.update_permission(username, 'ro', db_name) 64 | assert await sys_db.permission(username, db_name) == 'rw' 65 | 66 | # Test update permission (database level) to read only and verify access 67 | assert await sys_db.update_permission(username, 'ro', db_name) is True 68 | assert await sys_db.permission(username, db_name) == 'ro' 69 | with assert_raises(CollectionCreateError) as err: 70 | await db.create_collection(col_name_2) 71 | assert err.value.http_code == 403 72 | assert col_name_1 not in await extract('name', await db.collections()) 73 | assert col_name_2 not in await extract('name', await db.collections()) 74 | 75 | # Test reset permission (database level) with bad database 76 | with assert_raises(PermissionResetError) as err: 77 | await bad_db.reset_permission(username, db_name) 78 | assert err.value.error_code in {11, 1228} 79 | assert await sys_db.permission(username, db_name) == 'ro' 80 | 81 | # Test reset permission (database level) and verify access 82 | assert await sys_db.reset_permission(username, db_name) is True 83 | assert await sys_db.permission(username, db_name) == 'none' 84 | with assert_raises(CollectionCreateError) as err: 85 | await db.create_collection(col_name_1) 86 | assert err.value.http_code == 401 87 | with assert_raises(CollectionListError) as err: 88 | await db.collections() 89 | assert err.value.http_code == 401 90 | 91 | # Test update permission (database level) and verify access 92 | assert await sys_db.update_permission(username, 'rw', db_name) is True 93 | assert await sys_db.permission(username, db_name, col_name_2) == 'rw' 94 | assert await db.create_collection(col_name_1) is not None 95 | assert await db.create_collection(col_name_2) is not None 96 | assert col_name_1 in await extract('name', await db.collections()) 97 | assert col_name_2 in await extract('name', await db.collections()) 98 | 99 | col_1 = await db.collection(col_name_1) 100 | col_2 = await db.collection(col_name_2) 101 | 102 | # Verify that user has read and write access to both collections 103 | assert isinstance(await col_1.properties(), dict) 104 | assert isinstance(await col_1.insert({}), dict) 105 | assert isinstance(await col_2.properties(), dict) 106 | assert isinstance(await col_2.insert({}), dict) 107 | 108 | # Test update permission (collection level) to read only and verify access 109 | assert await sys_db.update_permission(username, 'ro', db_name, col_name_1) 110 | assert await sys_db.permission(username, db_name, col_name_1) == 'ro' 111 | assert isinstance(await col_1.properties(), dict) 112 | with assert_raises(DocumentInsertError) as err: 113 | await col_1.insert({}) 114 | assert err.value.http_code == 403 115 | assert isinstance(await col_2.properties(), dict) 116 | assert isinstance(await col_2.insert({}), dict) 117 | 118 | # Test update permission (collection level) to none and verify access 119 | assert await sys_db.update_permission(username, 'none', db_name, col_name_1) 120 | assert await sys_db.permission(username, db_name, col_name_1) == 'none' 121 | with assert_raises(CollectionPropertiesError) as err: 122 | await col_1.properties() 123 | assert err.value.http_code == 403 124 | with assert_raises(DocumentInsertError) as err: 125 | await col_1.insert({}) 126 | assert err.value.http_code == 403 127 | assert isinstance(await col_2.properties(), dict) 128 | assert isinstance(await col_2.insert({}), dict) 129 | 130 | # Test reset permission (collection level) 131 | assert await sys_db.reset_permission(username, db_name, col_name_1) is True 132 | assert await sys_db.permission(username, db_name, col_name_1) == 'rw' 133 | assert isinstance(await col_1.properties(), dict) 134 | assert isinstance(await col_1.insert({}), dict) 135 | assert isinstance(await col_2.properties(), dict) 136 | assert isinstance(await col_2.insert({}), dict) 137 | -------------------------------------------------------------------------------- /aioarangodb/tests/test_pregel.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import asyncio 4 | 5 | import pytest 6 | from six import string_types 7 | 8 | from aioarangodb.exceptions import ( 9 | PregelJobCreateError, 10 | PregelJobGetError, 11 | PregelJobDeleteError 12 | ) 13 | from aioarangodb.tests.helpers import ( 14 | assert_raises, 15 | generate_string 16 | ) 17 | pytestmark = pytest.mark.asyncio 18 | 19 | 20 | async def test_pregel_attributes(db, username): 21 | assert db.pregel.context in ['default', 'async', 'batch', 'transaction'] 22 | assert db.pregel.username == username 23 | assert db.pregel.db_name == db.name 24 | assert repr(db.pregel) == ''.format(db.name) 25 | 26 | 27 | async def test_pregel_management(db, graph, cluster): 28 | if cluster: 29 | pytest.skip('Not tested in a cluster setup') 30 | 31 | # Test create pregel job 32 | job_id = await db.pregel.create_job( 33 | graph.name, 34 | 'pagerank', 35 | store=False, 36 | max_gss=100, 37 | thread_count=1, 38 | async_mode=False, 39 | result_field='result', 40 | algorithm_params={'threshold': 0.000001} 41 | ) 42 | assert isinstance(job_id, int) 43 | 44 | # Test create pregel job with unsupported algorithm 45 | with assert_raises(PregelJobCreateError) as err: 46 | await db.pregel.create_job(graph.name, 'invalid') 47 | assert err.value.error_code in {4, 10, 1600} 48 | 49 | # Test get existing pregel job 50 | job = await db.pregel.job(job_id) 51 | assert isinstance(job['state'], string_types) 52 | assert isinstance(job['aggregators'], dict) 53 | assert isinstance(job['gss'], int) 54 | assert isinstance(job['received_count'], int) 55 | assert isinstance(job['send_count'], int) 56 | assert 'total_runtime' in job 57 | 58 | # Test delete existing pregel job 59 | assert await db.pregel.delete_job(job_id) is True 60 | await asyncio.sleep(0.2) 61 | with assert_raises(PregelJobGetError) as err: 62 | await db.pregel.job(job_id) 63 | assert err.value.error_code in {4, 10, 1600} 64 | 65 | # Test delete missing pregel job 66 | with assert_raises(PregelJobDeleteError) as err: 67 | await db.pregel.delete_job(generate_string()) 68 | assert err.value.error_code in {4, 10, 1600} 69 | -------------------------------------------------------------------------------- /aioarangodb/tests/test_request.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import pytest 4 | from aioarangodb.request import Request 5 | pytestmark = pytest.mark.asyncio 6 | 7 | 8 | async def test_request_no_data(): 9 | request = Request( 10 | method='post', 11 | endpoint='/_api/test', 12 | params={'bool': True}, 13 | headers={'foo': 'bar'} 14 | ) 15 | assert request.method == 'post' 16 | assert request.endpoint == '/_api/test' 17 | assert request.params == {'bool': 1} 18 | assert request.headers == { 19 | 'charset': 'utf-8', 20 | 'content-type': 'application/json', 21 | 'foo': 'bar', 22 | } 23 | assert request.data is None 24 | 25 | 26 | async def test_request_string_data(): 27 | request = Request( 28 | method='post', 29 | endpoint='/_api/test', 30 | params={'bool': True}, 31 | headers={'foo': 'bar'}, 32 | data='test' 33 | ) 34 | assert request.method == 'post' 35 | assert request.endpoint == '/_api/test' 36 | assert request.params == {'bool': 1} 37 | assert request.headers == { 38 | 'charset': 'utf-8', 39 | 'content-type': 'application/json', 40 | 'foo': 'bar', 41 | } 42 | assert request.data == 'test' 43 | 44 | 45 | async def test_request_json_data(): 46 | request = Request( 47 | method='post', 48 | endpoint='/_api/test', 49 | params={'bool': True}, 50 | headers={'foo': 'bar'}, 51 | data={'baz': 'qux'} 52 | ) 53 | assert request.method == 'post' 54 | assert request.endpoint == '/_api/test' 55 | assert request.params == {'bool': 1} 56 | assert request.headers == { 57 | 'charset': 'utf-8', 58 | 'content-type': 'application/json', 59 | 'foo': 'bar', 60 | } 61 | assert request.data == {'baz': 'qux'} 62 | 63 | 64 | async def test_request_transaction_data(): 65 | request = Request( 66 | method='post', 67 | endpoint='/_api/test', 68 | params={'bool': True}, 69 | headers={'foo': 'bar'}, 70 | data={'baz': 'qux'}, 71 | ) 72 | assert request.method == 'post' 73 | assert request.endpoint == '/_api/test' 74 | assert request.params == {'bool': 1} 75 | assert request.headers == { 76 | 'charset': 'utf-8', 77 | 'content-type': 'application/json', 78 | 'foo': 'bar', 79 | } 80 | assert request.data == {'baz': 'qux'} 81 | -------------------------------------------------------------------------------- /aioarangodb/tests/test_resolver.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import pytest 4 | from aioarangodb.resolver import ( 5 | SingleHostResolver, 6 | RandomHostResolver, 7 | RoundRobinHostResolver 8 | ) 9 | pytestmark = pytest.mark.asyncio 10 | 11 | 12 | async def test_resolver_single_host(): 13 | resolver = SingleHostResolver() 14 | for _ in range(20): 15 | assert resolver.get_host_index() == 0 16 | 17 | 18 | async def test_resolver_random_host(): 19 | resolver = RandomHostResolver(10) 20 | for _ in range(20): 21 | assert 0 <= resolver.get_host_index() < 10 22 | 23 | 24 | async def test_resolver_round_robin(): 25 | resolver = RoundRobinHostResolver(10) 26 | assert resolver.get_host_index() == 0 27 | assert resolver.get_host_index() == 1 28 | assert resolver.get_host_index() == 2 29 | assert resolver.get_host_index() == 3 30 | assert resolver.get_host_index() == 4 31 | assert resolver.get_host_index() == 5 32 | assert resolver.get_host_index() == 6 33 | assert resolver.get_host_index() == 7 34 | assert resolver.get_host_index() == 8 35 | assert resolver.get_host_index() == 9 36 | assert resolver.get_host_index() == 0 37 | -------------------------------------------------------------------------------- /aioarangodb/tests/test_response.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import pytest 4 | from aioarangodb.response import Response 5 | pytestmark = pytest.mark.asyncio 6 | 7 | 8 | async def test_response(conn): 9 | response = Response( 10 | method='get', 11 | url='test_url', 12 | headers={'foo': 'bar'}, 13 | status_text='baz', 14 | status_code=200, 15 | raw_body='true', 16 | ) 17 | conn.prep_response(response) 18 | 19 | assert response.method == 'get' 20 | assert response.url == 'test_url' 21 | assert response.headers == {'foo': 'bar'} 22 | assert response.status_code == 200 23 | assert response.status_text == 'baz' 24 | assert response.raw_body == 'true' 25 | assert response.body is True 26 | assert response.error_code is None 27 | assert response.error_message is None 28 | assert response.is_success is True 29 | 30 | test_body = '{"errorNum": 1, "errorMessage": "qux"}' 31 | response = Response( 32 | method='get', 33 | url='test_url', 34 | headers={'foo': 'bar'}, 35 | status_text='baz', 36 | status_code=200, 37 | raw_body=test_body, 38 | ) 39 | conn.prep_response(response) 40 | 41 | assert response.method == 'get' 42 | assert response.url == 'test_url' 43 | assert response.headers == {'foo': 'bar'} 44 | assert response.status_code == 200 45 | assert response.status_text == 'baz' 46 | assert response.raw_body == test_body 47 | assert response.body == {'errorMessage': 'qux', 'errorNum': 1} 48 | assert response.error_code == 1 49 | assert response.error_message == 'qux' 50 | assert response.is_success is False 51 | -------------------------------------------------------------------------------- /aioarangodb/tests/test_task.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from six import string_types 4 | import pytest 5 | 6 | from aioarangodb.exceptions import ( 7 | TaskCreateError, 8 | TaskDeleteError, 9 | TaskGetError, 10 | TaskListError 11 | ) 12 | from aioarangodb.tests.helpers import ( 13 | assert_raises, 14 | extract, 15 | generate_task_id, 16 | generate_task_name, 17 | ) 18 | pytestmark = pytest.mark.asyncio 19 | 20 | 21 | async def test_task_management(sys_db, db, bad_db): 22 | test_command = 'require("@arangodb").print(params);' 23 | 24 | # Test create task with random ID 25 | task_name = generate_task_name() 26 | new_task = await db.create_task( 27 | name=task_name, 28 | command=test_command, 29 | params={'foo': 1, 'bar': 2}, 30 | offset=1, 31 | ) 32 | assert new_task['name'] == task_name 33 | assert 'print(params)' in new_task['command'] 34 | assert new_task['type'] == 'timed' 35 | assert new_task['database'] == db.name 36 | assert isinstance(new_task['created'], float) 37 | assert isinstance(new_task['id'], string_types) 38 | 39 | # Test get existing task 40 | assert await db.task(new_task['id']) == new_task 41 | 42 | # Test create task with specific ID 43 | task_name = generate_task_name() 44 | task_id = generate_task_id() 45 | new_task = await db.create_task( 46 | name=task_name, 47 | command=test_command, 48 | params={'foo': 1, 'bar': 2}, 49 | offset=1, 50 | period=10, 51 | task_id=task_id 52 | ) 53 | assert new_task['name'] == task_name 54 | assert new_task['id'] == task_id 55 | assert 'print(params)' in new_task['command'] 56 | assert new_task['type'] == 'periodic' 57 | assert new_task['database'] == db.name 58 | assert isinstance(new_task['created'], float) 59 | assert await db.task(new_task['id']) == new_task 60 | 61 | # Test create duplicate task 62 | with assert_raises(TaskCreateError) as err: 63 | await db.create_task( 64 | name=task_name, 65 | command=test_command, 66 | params={'foo': 1, 'bar': 2}, 67 | task_id=task_id 68 | ) 69 | assert err.value.error_code == 1851 70 | 71 | # Test list tasks 72 | for task in await sys_db.tasks(): 73 | assert task['type'] in {'periodic', 'timed'} 74 | assert isinstance(task['id'], string_types) 75 | assert isinstance(task['name'], string_types) 76 | assert isinstance(task['created'], float) 77 | assert isinstance(task['command'], string_types) 78 | 79 | # Test list tasks with bad database 80 | with assert_raises(TaskListError) as err: 81 | await bad_db.tasks() 82 | assert err.value.error_code in {11, 1228} 83 | 84 | # Test get missing task 85 | with assert_raises(TaskGetError) as err: 86 | await db.task(generate_task_id()) 87 | assert err.value.error_code == 1852 88 | 89 | # Test delete existing task 90 | assert task_id in await extract('id', await db.tasks()) 91 | assert await db.delete_task(task_id) is True 92 | assert task_id not in await extract('id', await db.tasks()) 93 | with assert_raises(TaskGetError) as err: 94 | await db.task(task_id) 95 | assert err.value.error_code == 1852 96 | 97 | # Test delete missing task 98 | with assert_raises(TaskDeleteError) as err: 99 | await db.delete_task(generate_task_id(), ignore_missing=False) 100 | assert err.value.error_code == 1852 101 | assert await db.delete_task(task_id, ignore_missing=True) is False 102 | -------------------------------------------------------------------------------- /aioarangodb/tests/test_transaction.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import pytest 4 | 5 | from aioarangodb.database import TransactionDatabase 6 | from aioarangodb.exceptions import ( 7 | TransactionExecuteError, 8 | TransactionInitError, 9 | TransactionStatusError, 10 | TransactionCommitError, 11 | TransactionAbortError 12 | ) 13 | from aioarangodb.tests.helpers import extract 14 | pytestmark = pytest.mark.asyncio 15 | 16 | 17 | async def test_transaction_execute_raw(db, col, docs): 18 | # Test execute raw transaction 19 | doc = docs[0] 20 | key = doc['_key'] 21 | result = await db.execute_transaction( 22 | command=''' 23 | function (params) {{ 24 | var db = require('internal').db; 25 | db.{col}.save({{'_key': params.key, 'val': 1}}); 26 | return true; 27 | }} 28 | '''.format(col=col.name), 29 | params={'key': key}, 30 | write=[col.name], 31 | read=[col.name], 32 | sync=False, 33 | timeout=1000, 34 | max_size=100000, 35 | allow_implicit=True, 36 | intermediate_commit_count=10, 37 | intermediate_commit_size=10000 38 | ) 39 | assert result is True 40 | assert await col.has(doc) and (await col.get(key))['val'] == 1 41 | 42 | # Test execute invalid transaction 43 | with pytest.raises(TransactionExecuteError) as err: 44 | await db.execute_transaction(command='INVALID COMMAND') 45 | assert err.value.error_code == 10 46 | 47 | 48 | async def test_transaction_init(db, bad_db, col, username): 49 | txn_db = await db.begin_transaction() 50 | 51 | assert isinstance(txn_db, TransactionDatabase) 52 | assert txn_db.username == username 53 | assert txn_db.context == 'transaction' 54 | assert txn_db.db_name == db.name 55 | assert txn_db.name == db.name 56 | assert txn_db.transaction_id is not None 57 | assert repr(txn_db) == ''.format(db.name) 58 | 59 | txn_col = txn_db.collection(col.name) 60 | assert txn_col.username == username 61 | assert txn_col.context == 'transaction' 62 | assert txn_col.db_name == db.name 63 | 64 | txn_aql = txn_db.aql 65 | assert txn_aql.username == username 66 | assert txn_aql.context == 'transaction' 67 | assert txn_aql.db_name == db.name 68 | 69 | with pytest.raises(TransactionInitError) as err: 70 | await bad_db.begin_transaction() 71 | assert err.value.error_code in {11, 1228} 72 | 73 | 74 | async def test_transaction_status(db, col, docs): 75 | txn_db = await db.begin_transaction(read=col.name) 76 | assert await txn_db.transaction_status() == 'running' 77 | 78 | await txn_db.commit_transaction() 79 | assert await txn_db.transaction_status() == 'committed' 80 | 81 | txn_db = await db.begin_transaction(read=col.name) 82 | assert await txn_db.transaction_status() == 'running' 83 | 84 | await txn_db.abort_transaction() 85 | assert await txn_db.transaction_status() == 'aborted' 86 | 87 | # Test transaction_status with an illegal transaction ID 88 | txn_db._executor._id = 'illegal' 89 | with pytest.raises(TransactionStatusError) as err: 90 | await txn_db.transaction_status() 91 | assert err.value.error_code in {10, 1655} 92 | 93 | 94 | async def test_transaction_commit(db, col, docs): 95 | txn_db = await db.begin_transaction( 96 | read=col.name, 97 | write=col.name, 98 | exclusive=[], 99 | sync=True, 100 | allow_implicit=False, 101 | lock_timeout=1000, 102 | max_size=10000 103 | ) 104 | txn_col = txn_db.collection(col.name) 105 | 106 | assert '_rev' in await txn_col.insert(docs[0]) 107 | assert '_rev' in await txn_col.delete(docs[0]) 108 | assert '_rev' in await txn_col.insert(docs[1]) 109 | assert '_rev' in await txn_col.delete(docs[1]) 110 | assert '_rev' in await txn_col.insert(docs[2]) 111 | await txn_db.commit_transaction() 112 | 113 | assert await extract('_key', await col.all()) == [docs[2]['_key']] 114 | assert await txn_db.transaction_status() == 'committed' 115 | 116 | # Test commit_transaction with an illegal transaction ID 117 | txn_db._executor._id = 'illegal' 118 | with pytest.raises(TransactionCommitError) as err: 119 | await txn_db.commit_transaction() 120 | assert err.value.error_code in {10, 1655} 121 | 122 | 123 | async def test_transaction_abort(db, col, docs): 124 | txn_db = await db.begin_transaction(write=col.name) 125 | txn_col = txn_db.collection(col.name) 126 | 127 | assert '_rev' in await txn_col.insert(docs[0]) 128 | assert '_rev' in await txn_col.delete(docs[0]) 129 | assert '_rev' in await txn_col.insert(docs[1]) 130 | assert '_rev' in await txn_col.delete(docs[1]) 131 | assert '_rev' in await txn_col.insert(docs[2]) 132 | await txn_db.abort_transaction() 133 | 134 | assert await extract('_key', await col.all()) == [] 135 | assert await txn_db.transaction_status() == 'aborted' 136 | 137 | txn_db._executor._id = 'illegal' 138 | with pytest.raises(TransactionAbortError) as err: 139 | await txn_db.abort_transaction() 140 | assert err.value.error_code in {10, 1655} 141 | 142 | 143 | async def test_transaction_graph(db, graph, fvcol, fvdocs): 144 | txn_db = await db.begin_transaction(write=fvcol.name) 145 | vcol = txn_db.graph(graph.name).vertex_collection(fvcol.name) 146 | 147 | await vcol.insert(fvdocs[0]) 148 | assert await vcol.count() == 1 149 | 150 | await vcol.delete(fvdocs[0]) 151 | assert await vcol.count() == 0 152 | 153 | await txn_db.commit() 154 | -------------------------------------------------------------------------------- /aioarangodb/tests/test_user.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import pytest 4 | from six import string_types 5 | 6 | from aioarangodb.exceptions import ( 7 | DatabasePropertiesError, 8 | UserCreateError, 9 | UserDeleteError, 10 | UserGetError, 11 | UserListError, 12 | UserReplaceError, 13 | UserUpdateError, 14 | ) 15 | from aioarangodb.tests.helpers import ( 16 | assert_raises, 17 | extract, 18 | generate_db_name, 19 | generate_username, 20 | generate_string, 21 | ) 22 | pytestmark = pytest.mark.asyncio 23 | 24 | 25 | async def test_user_management(sys_db, bad_db): 26 | # Test create user 27 | username = generate_username() 28 | password = generate_string() 29 | assert not await sys_db.has_user(username) 30 | 31 | new_user = await sys_db.create_user( 32 | username=username, 33 | password=password, 34 | active=True, 35 | extra={'foo': 'bar'}, 36 | ) 37 | assert new_user['username'] == username 38 | assert new_user['active'] is True 39 | assert new_user['extra'] == {'foo': 'bar'} 40 | assert await sys_db.has_user(username) 41 | 42 | # Test create duplicate user 43 | with assert_raises(UserCreateError) as err: 44 | await sys_db.create_user( 45 | username=username, 46 | password=password 47 | ) 48 | assert err.value.error_code == 1702 49 | 50 | # Test list users 51 | for user in await sys_db.users(): 52 | assert isinstance(user['username'], string_types) 53 | assert isinstance(user['active'], bool) 54 | assert isinstance(user['extra'], dict) 55 | assert await sys_db.user(username) == new_user 56 | 57 | # Test list users with bad database 58 | with assert_raises(UserListError) as err: 59 | await bad_db.users() 60 | assert err.value.error_code in {11, 1228} 61 | 62 | # Test get user 63 | users = await sys_db.users() 64 | for user in users: 65 | assert 'active' in user 66 | assert 'extra' in user 67 | assert 'username' in user 68 | assert username in await extract('username', await sys_db.users()) 69 | 70 | # Test get missing user 71 | with assert_raises(UserGetError) as err: 72 | await sys_db.user(generate_username()) 73 | assert err.value.error_code == 1703 74 | 75 | # Update existing user 76 | new_user = await sys_db.update_user( 77 | username=username, 78 | password=password, 79 | active=False, 80 | extra={'bar': 'baz'}, 81 | ) 82 | assert new_user['username'] == username 83 | assert new_user['active'] is False 84 | assert new_user['extra'] == {'bar': 'baz'} 85 | assert await sys_db.user(username) == new_user 86 | 87 | # Update missing user 88 | with assert_raises(UserUpdateError) as err: 89 | await sys_db.update_user( 90 | username=generate_username(), 91 | password=generate_string() 92 | ) 93 | assert err.value.error_code == 1703 94 | 95 | # Replace existing user 96 | new_user = await sys_db.replace_user( 97 | username=username, 98 | password=password, 99 | active=False, 100 | extra={'baz': 'qux'}, 101 | ) 102 | assert new_user['username'] == username 103 | assert new_user['active'] is False 104 | assert new_user['extra'] == {'baz': 'qux'} 105 | assert await sys_db.user(username) == new_user 106 | 107 | # Replace missing user 108 | with assert_raises(UserReplaceError) as err: 109 | await sys_db.replace_user( 110 | username=generate_username(), 111 | password=generate_string() 112 | ) 113 | assert err.value.error_code == 1703 114 | 115 | # Delete an existing user 116 | assert await sys_db.delete_user(username) is True 117 | 118 | # Delete a missing user 119 | with assert_raises(UserDeleteError) as err: 120 | await sys_db.delete_user(username, ignore_missing=False) 121 | assert err.value.error_code == 1703 122 | assert await sys_db.delete_user(username, ignore_missing=True) is False 123 | 124 | 125 | async def test_user_change_password(client, sys_db, cluster): 126 | if cluster: 127 | pytest.skip('Not tested in a cluster setup') 128 | 129 | username = generate_username() 130 | password1 = generate_string() 131 | password2 = generate_string() 132 | 133 | await sys_db.create_user(username, password1) 134 | await sys_db.update_permission(username, 'rw', sys_db.name) 135 | 136 | db1 = await client.db(sys_db.name, username, password1) 137 | db2 = await client.db(sys_db.name, username, password2) 138 | 139 | # Check authentication 140 | assert isinstance(await db1.properties(), dict) 141 | with assert_raises(DatabasePropertiesError) as err: 142 | await db2.properties() 143 | assert err.value.http_code == 401 144 | 145 | # Update the user password and check again 146 | await sys_db.update_user(username, password2) 147 | assert isinstance(await db2.properties(), dict) 148 | with assert_raises(DatabasePropertiesError) as err: 149 | await db1.properties() 150 | assert err.value.http_code == 401 151 | 152 | # Replace the user password back and check again 153 | await sys_db.update_user(username, password1) 154 | assert isinstance(await db1.properties(), dict) 155 | with assert_raises(DatabasePropertiesError) as err: 156 | await db2.properties() 157 | assert err.value.http_code == 401 158 | 159 | 160 | async def test_user_create_with_new_database(client, sys_db, cluster): 161 | if cluster: 162 | pytest.skip('Not tested in a cluster setup') 163 | 164 | db_name = generate_db_name() 165 | 166 | username1 = generate_username() 167 | username2 = generate_username() 168 | username3 = generate_username() 169 | 170 | password1 = generate_string() 171 | password2 = generate_string() 172 | password3 = generate_string() 173 | 174 | result = await sys_db.create_database( 175 | name=db_name, 176 | users=[ 177 | {'username': username1, 'password': password1, 'active': True}, 178 | {'username': username2, 'password': password2, 'active': True}, 179 | {'username': username3, 'password': password3, 'active': False}, 180 | ] 181 | ) 182 | assert result is True 183 | 184 | await sys_db.update_permission(username1, permission='rw', database=db_name) 185 | await sys_db.update_permission(username2, permission='rw', database=db_name) 186 | await sys_db.update_permission(username3, permission='rw', database=db_name) 187 | 188 | # Test if the users were created properly 189 | usernames = await extract('username', await sys_db.users()) 190 | assert all(u in usernames for u in [username1, username2, username3]) 191 | 192 | # Test if the first user has access to the database 193 | db = await client.db(db_name, username1, password1) 194 | await db.properties() 195 | 196 | # Test if the second user also has access to the database 197 | db = await client.db(db_name, username2, password2) 198 | await db.properties() 199 | 200 | # Test if the third user has access to the database (should not) 201 | db = await client.db(db_name, username3, password3) 202 | with assert_raises(DatabasePropertiesError) as err: 203 | await db.properties() 204 | assert err.value.http_code == 401 205 | -------------------------------------------------------------------------------- /aioarangodb/tests/test_view.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | import pytest 3 | 4 | from aioarangodb.exceptions import ( 5 | ViewCreateError, 6 | ViewDeleteError, 7 | ViewGetError, 8 | ViewListError, 9 | ViewRenameError, 10 | ViewReplaceError, 11 | ViewUpdateError 12 | ) 13 | from aioarangodb.tests.helpers import assert_raises, generate_view_name 14 | pytestmark = pytest.mark.asyncio 15 | 16 | 17 | async def test_view_management(db, bad_db, cluster): 18 | # Test create view 19 | view_name = generate_view_name() 20 | bad_view_name = generate_view_name() 21 | view_type = 'arangosearch' 22 | 23 | result = await db.create_view( 24 | view_name, 25 | view_type, 26 | {'consolidationIntervalMsec': 50000} 27 | ) 28 | assert 'id' in result 29 | assert result['name'] == view_name 30 | assert result['type'] == view_type 31 | assert result['consolidation_interval_msec'] == 50000 32 | view_id = result['id'] 33 | 34 | # Test create duplicate view 35 | with assert_raises(ViewCreateError) as err: 36 | await db.create_view( 37 | view_name, 38 | view_type, 39 | {'consolidationIntervalMsec': 50000} 40 | ) 41 | assert err.value.error_code == 1207 42 | 43 | # Test list views 44 | result = await db.views() 45 | assert len(result) == 1 46 | view = result[0] 47 | assert view['id'] == view_id 48 | assert view['name'] == view_name 49 | assert view['type'] == view_type 50 | 51 | # Test list views with bad database 52 | with assert_raises(ViewListError) as err: 53 | await bad_db.views() 54 | assert err.value.error_code in {11, 1228} 55 | 56 | # Test get view 57 | view = await db.view(view_name) 58 | assert view['id'] == view_id 59 | assert view['name'] == view_name 60 | assert view['type'] == view_type 61 | assert view['consolidation_interval_msec'] == 50000 62 | 63 | # Test get missing view 64 | with assert_raises(ViewGetError) as err: 65 | await db.view(bad_view_name) 66 | assert err.value.error_code == 1203 67 | 68 | # Test update view 69 | view = await db.update_view(view_name, {'consolidationIntervalMsec': 70000}) 70 | assert view['id'] == view_id 71 | assert view['name'] == view_name 72 | assert view['type'] == view_type 73 | assert view['consolidation_interval_msec'] == 70000 74 | 75 | # Test update view with bad database 76 | with assert_raises(ViewUpdateError) as err: 77 | await bad_db.update_view(view_name, {'consolidationIntervalMsec': 80000}) 78 | assert err.value.error_code in {11, 1228} 79 | 80 | # Test replace view 81 | view = await db.replace_view(view_name, {'consolidationIntervalMsec': 40000}) 82 | assert view['id'] == view_id 83 | assert view['name'] == view_name 84 | assert view['type'] == view_type 85 | assert view['consolidation_interval_msec'] == 40000 86 | 87 | # Test replace view with bad database 88 | with assert_raises(ViewReplaceError) as err: 89 | await bad_db.replace_view(view_name, {'consolidationIntervalMsec': 7000}) 90 | assert err.value.error_code in {11, 1228} 91 | 92 | if cluster: 93 | new_view_name = view_name 94 | else: 95 | # Test rename view 96 | new_view_name = generate_view_name() 97 | assert await db.rename_view(view_name, new_view_name) is True 98 | result = await db.views() 99 | assert len(result) == 1 100 | view = result[0] 101 | assert view['id'] == view_id 102 | assert view['name'] == new_view_name 103 | 104 | # Test rename missing view 105 | with assert_raises(ViewRenameError) as err: 106 | await db.rename_view(bad_view_name, view_name) 107 | assert err.value.error_code == 1203 108 | 109 | # Test delete view 110 | assert await db.delete_view(new_view_name) is True 111 | assert len(await db.views()) == 0 112 | 113 | # Test delete missing view 114 | with assert_raises(ViewDeleteError) as err: 115 | await db.delete_view(new_view_name) 116 | assert err.value.error_code == 1203 117 | 118 | # Test delete missing view with ignore_missing set to True 119 | assert await db.delete_view(view_name, ignore_missing=True) is False 120 | 121 | 122 | async def test_arangosearch_view_management(db, bad_db, cluster): 123 | # Test create arangosearch view 124 | view_name = generate_view_name() 125 | result = await db.create_arangosearch_view( 126 | view_name, 127 | {'consolidationIntervalMsec': 50000} 128 | ) 129 | assert 'id' in result 130 | assert result['name'] == view_name 131 | assert result['type'].lower() == 'arangosearch' 132 | assert result['consolidation_interval_msec'] == 50000 133 | view_id = result['id'] 134 | 135 | # Test create duplicate arangosearch view 136 | with assert_raises(ViewCreateError) as err: 137 | await db.create_arangosearch_view( 138 | view_name, 139 | {'consolidationIntervalMsec': 50000} 140 | ) 141 | assert err.value.error_code == 1207 142 | 143 | result = await db.views() 144 | if not cluster: 145 | assert len(result) == 1 146 | view = result[0] 147 | assert view['id'] == view_id 148 | assert view['name'] == view_name 149 | assert view['type'] == 'arangosearch' 150 | 151 | # Test update arangosearch view 152 | view = await db.update_arangosearch_view( 153 | view_name, 154 | {'consolidationIntervalMsec': 70000} 155 | ) 156 | assert view['id'] == view_id 157 | assert view['name'] == view_name 158 | assert view['type'].lower() == 'arangosearch' 159 | assert view['consolidation_interval_msec'] == 70000 160 | 161 | # Test update arangosearch view with bad database 162 | with assert_raises(ViewUpdateError) as err: 163 | await bad_db.update_arangosearch_view( 164 | view_name, 165 | {'consolidationIntervalMsec': 70000} 166 | ) 167 | assert err.value.error_code in {11, 1228} 168 | 169 | # Test replace arangosearch view 170 | view = await db.replace_arangosearch_view( 171 | view_name, 172 | {'consolidationIntervalMsec': 40000} 173 | ) 174 | assert view['id'] == view_id 175 | assert view['name'] == view_name 176 | assert view['type'] == 'arangosearch' 177 | assert view['consolidation_interval_msec'] == 40000 178 | 179 | # Test replace arangosearch with bad database 180 | with assert_raises(ViewReplaceError) as err: 181 | await bad_db.replace_arangosearch_view( 182 | view_name, 183 | {'consolidationIntervalMsec': 70000} 184 | ) 185 | assert err.value.error_code in {11, 1228} 186 | 187 | # Test delete arangosearch view 188 | assert await db.delete_view(view_name, ignore_missing=False) is True 189 | -------------------------------------------------------------------------------- /aioarangodb/tests/test_wal.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import pytest 4 | 5 | from aioarangodb.errno import ( 6 | FORBIDDEN, 7 | HTTP_UNAUTHORIZED, 8 | DATABASE_NOT_FOUND 9 | ) 10 | from aioarangodb.exceptions import ( 11 | WALConfigureError, 12 | WALFlushError, 13 | WALPropertiesError, 14 | WALTransactionListError, 15 | WALTickRangesError, 16 | WALLastTickError, 17 | WALTailError 18 | ) 19 | from aioarangodb.tests.helpers import assert_raises 20 | pytestmark = pytest.mark.asyncio 21 | 22 | 23 | async def test_wal_misc_methods(sys_db, bad_db): 24 | try: 25 | await sys_db.wal.properties() 26 | except WALPropertiesError as err: 27 | if err.http_code == 501: 28 | pytest.skip('WAL not implemented') 29 | 30 | # Test get properties 31 | properties = await sys_db.wal.properties() 32 | assert 'oversized_ops' in properties 33 | assert 'log_size' in properties 34 | assert 'historic_logs' in properties 35 | assert 'reserve_logs' in properties 36 | assert 'throttle_wait' in properties 37 | assert 'throttle_limit' in properties 38 | 39 | # Test get properties with bad database 40 | with assert_raises(WALPropertiesError) as err: 41 | await bad_db.wal.properties() 42 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 43 | 44 | # Test configure properties 45 | await sys_db.wal.configure( 46 | historic_logs=15, 47 | oversized_ops=False, 48 | log_size=30000000, 49 | reserve_logs=5, 50 | throttle_limit=0, 51 | throttle_wait=16000 52 | ) 53 | properties = await sys_db.wal.properties() 54 | assert properties['historic_logs'] == 15 55 | assert properties['oversized_ops'] is False 56 | assert properties['log_size'] == 30000000 57 | assert properties['reserve_logs'] == 5 58 | assert properties['throttle_limit'] == 0 59 | assert properties['throttle_wait'] == 16000 60 | 61 | # Test configure properties with bad database 62 | with assert_raises(WALConfigureError) as err: 63 | await bad_db.wal.configure(log_size=2000000) 64 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 65 | 66 | # Test get transactions 67 | result = await sys_db.wal.transactions() 68 | assert 'count' in result 69 | assert 'last_collected' in result 70 | 71 | # Test get transactions with bad database 72 | with assert_raises(WALTransactionListError) as err: 73 | await bad_db.wal.transactions() 74 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 75 | 76 | # Test flush 77 | result = await sys_db.wal.flush(garbage_collect=False, sync=False) 78 | assert isinstance(result, bool) 79 | 80 | # Test flush with bad database 81 | with assert_raises(WALFlushError) as err: 82 | await bad_db.wal.flush(garbage_collect=False, sync=False) 83 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 84 | 85 | 86 | async def test_wal_tick_ranges(sys_db, bad_db, cluster): 87 | if cluster: 88 | pytest.skip('Not tested in a cluster setup') 89 | 90 | result = await sys_db.wal.tick_ranges() 91 | assert 'server' in result 92 | assert 'time' in result 93 | assert 'tick_min' in result 94 | assert 'tick_max' in result 95 | 96 | # Test tick_ranges with bad database 97 | with assert_raises(WALTickRangesError) as err: 98 | await bad_db.wal.tick_ranges() 99 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 100 | 101 | 102 | async def test_wal_last_tick(sys_db, bad_db, cluster): 103 | if cluster: 104 | pytest.skip('Not tested in a cluster setup') 105 | 106 | result = await sys_db.wal.last_tick() 107 | assert 'time' in result 108 | assert 'tick' in result 109 | assert 'server' in result 110 | 111 | # Test last_tick with bad database 112 | with assert_raises(WALLastTickError) as err: 113 | await bad_db.wal.last_tick() 114 | assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} 115 | 116 | 117 | async def test_wal_tail(sys_db, bad_db, cluster): 118 | if cluster: 119 | pytest.skip('Not tested in a cluster setup') 120 | 121 | result = await sys_db.wal.tail( 122 | lower=0, 123 | upper=1000000, 124 | last_scanned=0, 125 | all_databases=True, 126 | chunk_size=1000000, 127 | syncer_id=None, 128 | server_id=None, 129 | client_info='test', 130 | barrier_id=None 131 | ) 132 | assert 'content' in result 133 | assert 'last_tick' in result 134 | assert 'last_scanned' in result 135 | assert 'last_included' in result 136 | assert isinstance(result['check_more'], bool) 137 | assert isinstance(result['from_present'], bool) 138 | 139 | # Test tick_ranges with bad database 140 | with assert_raises(WALTailError) as err: 141 | await bad_db.wal.tail() 142 | assert err.value.http_code == HTTP_UNAUTHORIZED 143 | -------------------------------------------------------------------------------- /aioarangodb/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import logging 4 | from contextlib import contextmanager 5 | 6 | from six import string_types 7 | 8 | from .exceptions import DocumentParseError 9 | 10 | 11 | @contextmanager 12 | def suppress_warning(logger_name): 13 | """Suppress logger messages. 14 | 15 | :param logger_name: Full name of the logger. 16 | :type logger_name: str | unicode 17 | """ 18 | logger = logging.getLogger(logger_name) 19 | original_log_level = logger.getEffectiveLevel() 20 | logger.setLevel(logging.CRITICAL) 21 | yield 22 | logger.setLevel(original_log_level) 23 | 24 | 25 | def get_col_name(doc): 26 | """Return the collection name from input. 27 | 28 | :param doc: Document ID or body with "_id" field. 29 | :type doc: str | unicode | dict 30 | :return: Collection name. 31 | :rtype: [str | unicode] 32 | :raise arango.exceptions.DocumentParseError: If document ID is missing. 33 | """ 34 | try: 35 | doc_id = doc['_id'] if isinstance(doc, dict) else doc 36 | except KeyError: 37 | raise DocumentParseError('field "_id" required') 38 | return doc_id.split('/', 1)[0] 39 | 40 | 41 | def get_doc_id(doc): 42 | """Return the document ID from input. 43 | 44 | :param doc: Document ID or body with "_id" field. 45 | :type doc: str | unicode | dict 46 | :return: Document ID. 47 | :rtype: str | unicode 48 | :raise arango.exceptions.DocumentParseError: If document ID is missing. 49 | """ 50 | try: 51 | return doc['_id'] if isinstance(doc, dict) else doc 52 | except KeyError: 53 | raise DocumentParseError('field "_id" required') 54 | 55 | 56 | def is_none_or_int(obj): 57 | """Check if obj is None or an integer. 58 | 59 | :param obj: Object to check. 60 | :type obj: object 61 | :return: True if object is None or an integer. 62 | :rtype: bool 63 | """ 64 | return obj is None or (isinstance(obj, int) and obj >= 0) 65 | 66 | 67 | def is_none_or_str(obj): 68 | """Check if obj is None or a string. 69 | 70 | :param obj: Object to check. 71 | :type obj: object 72 | :return: True if object is None or a string. 73 | :rtype: bool 74 | """ 75 | return obj is None or isinstance(obj, string_types) 76 | -------------------------------------------------------------------------------- /aioarangodb/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.0' 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = aioarangodb 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) -------------------------------------------------------------------------------- /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 aioarangodb import ArangoClient 13 | 14 | # Initialize the ArangoDB client. 15 | client = ArangoClient() 16 | 17 | # Connect to "_system" database as root user. 18 | sys_db = await client.db('_system', username='root', password='passwd') 19 | 20 | # Retrieve the server version. 21 | await sys_db.version() 22 | 23 | # Retrieve the server details. 24 | await sys_db.details() 25 | 26 | # Retrieve the target DB version. 27 | await sys_db.required_db_version() 28 | 29 | # Retrieve the database engine. 30 | await sys_db.engine() 31 | 32 | # Retrieve the server time. 33 | await sys_db.time() 34 | 35 | # Retrieve the server role in a cluster. 36 | await sys_db.role() 37 | 38 | # Retrieve the server statistics. 39 | await sys_db.statistics() 40 | 41 | # Read the server log. 42 | await sys_db.read_log(level="debug") 43 | 44 | # Retrieve the log levels. 45 | await sys_db.log_levels() 46 | 47 | # Set the log . 48 | await sys_db.set_log_levels( 49 | agency='DEBUG', 50 | collector='INFO', 51 | threads='WARNING' 52 | ) 53 | 54 | # Echo the last request. 55 | await sys_db.echo() 56 | 57 | # Reload the routing collection. 58 | await sys_db.reload_routing() 59 | 60 | # Retrieve server metrics. 61 | await sys_db.metrics() 62 | 63 | See :ref:`StandardDatabase` for API specification. -------------------------------------------------------------------------------- /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 aioarangodb import ArangoClient 14 | 15 | # Initialize the ArangoDB client. 16 | client = ArangoClient() 17 | 18 | # Connect to "test" database as root user. 19 | db = await client.db('test', username='root', password='passwd') 20 | 21 | # Retrieve list of analyzers. 22 | await db.analyzers() 23 | 24 | # Create an analyzer. 25 | await db.create_analyzer( 26 | name='test_analyzer', 27 | analyzer_type='identity', 28 | properties={}, 29 | features=[] 30 | ) 31 | 32 | # Delete an analyzer. 33 | await db.delete_analyzer('test_analyzer', ignore_missing=True) 34 | 35 | Refer to :ref:`StandardDatabase` class for API specification. -------------------------------------------------------------------------------- /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 aioarangodb import ArangoClient, AQLQueryKillError 23 | 24 | # Initialize the ArangoDB client. 25 | client = ArangoClient() 26 | 27 | # Connect to "test" database as root user. 28 | db = await client.db('test', username='root', password='passwd') 29 | 30 | # Insert some test documents into "students" collection. 31 | await 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 | await aql.explain('FOR doc IN students RETURN doc') 42 | 43 | # Validate the query without executing it. 44 | await aql.validate('FOR doc IN students RETURN doc') 45 | 46 | # Execute the query 47 | cursor = await 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'] async for doc in cursor] 53 | 54 | # List currently running queries. 55 | await aql.queries() 56 | 57 | # List any slow queries. 58 | await aql.slow_queries() 59 | 60 | # Clear slow AQL queries if any. 61 | await aql.clear_slow_queries() 62 | 63 | # Retrieve AQL query tracking properties. 64 | await aql.tracking() 65 | 66 | # Configure AQL query tracking properties. 67 | await 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 | await aql.kill('some_query_id') 76 | except AQLQueryKillError as err: 77 | assert err.http_code == 404 78 | assert err.error_code == 1591 79 | assert 'cannot kill query' in err.message 80 | 81 | See :ref:`AQL` for API specification. 82 | 83 | 84 | AQL User Functions 85 | ================== 86 | 87 | **AQL User Functions** are custom functions you define in Javascript to extend 88 | AQL functionality. They are somewhat similar to SQL procedures. 89 | 90 | **Example:** 91 | 92 | .. testcode:: 93 | 94 | from aioarangodb import ArangoClient 95 | 96 | # Initialize the ArangoDB client. 97 | client = ArangoClient() 98 | 99 | # Connect to "test" database as root user. 100 | db = client.db('test', username='root', password='passwd') 101 | 102 | # Get the AQL API wrapper. 103 | aql = db.aql 104 | 105 | # Create a new AQL user function. 106 | aql.create_function( 107 | # Grouping by name prefix is supported. 108 | name='functions::temperature::converter', 109 | code='function (celsius) { return celsius * 1.8 + 32; }' 110 | ) 111 | # List AQL user functions. 112 | aql.functions() 113 | 114 | # Delete an existing AQL user function. 115 | aql.delete_function('functions::temperature::converter') 116 | 117 | See :ref:`AQL` for API specification. 118 | 119 | 120 | AQL Query Cache 121 | =============== 122 | 123 | **AQL Query Cache** is used to minimize redundant calculation of the same query 124 | results. It is useful when read queries are issued frequently and write queries 125 | are not. 126 | 127 | **Example:** 128 | 129 | .. testcode:: 130 | 131 | from aioarangodb import ArangoClient 132 | 133 | # Initialize the ArangoDB client. 134 | client = ArangoClient() 135 | 136 | # Connect to "test" database as root user. 137 | db = client.db('test', username='root', password='passwd') 138 | 139 | # Get the AQL API wrapper. 140 | aql = db.aql 141 | 142 | # Retrieve AQL query cache properties. 143 | aql.cache.properties() 144 | 145 | # Configure AQL query cache properties 146 | aql.cache.configure(mode='demand', max_results=10000) 147 | 148 | # Clear results in AQL query cache. 149 | aql.cache.clear() 150 | 151 | See :ref:`AQLQueryCache` for API specification. -------------------------------------------------------------------------------- /docs/async.rst: -------------------------------------------------------------------------------- 1 | Asynchronous Execution 2 | ---------------------- 3 | 4 | In **asynchronous execution**, aioarangodb 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 | 11 | .. testcode:: 12 | 13 | import time 14 | 15 | from aioarangodb import ( 16 | ArangoClient, 17 | AQLQueryExecuteError, 18 | AsyncJobCancelError, 19 | AsyncJobClearError 20 | ) 21 | 22 | # Initialize the ArangoDB client. 23 | client = ArangoClient() 24 | 25 | # Connect to "test" database as root user. 26 | db = await client.db('test', username='root', password='passwd') 27 | 28 | # Begin async execution. This returns an instance of AsyncDatabase, a 29 | # database-level API wrapper tailored specifically for async execution. 30 | async_db = db.begin_async_execution(return_result=True) 31 | 32 | # Child wrappers are also tailored for async execution. 33 | async_aql = async_db.aql 34 | async_col = async_db.collection('students') 35 | 36 | # API execution context is always set to "async". 37 | assert async_db.context == 'async' 38 | assert async_aql.context == 'async' 39 | assert async_col.context == 'async' 40 | 41 | # On API execution, AsyncJob objects are returned instead of results. 42 | job1 = await async_col.insert({'_key': 'Neal'}) 43 | job2 = await async_col.insert({'_key': 'Lily'}) 44 | job3 = await async_aql.execute('RETURN 100000') 45 | job4 = await async_aql.execute('INVALID QUERY') # Fails due to syntax error. 46 | 47 | # Retrieve the status of each async job. 48 | for job in [job1, job2, job3, job4]: 49 | # Job status can be "pending", "done" or "cancelled". 50 | assert await job.status() in {'pending', 'done', 'cancelled'} 51 | 52 | # Let's wait until the jobs are finished. 53 | while await job.status() != 'done': 54 | time.sleep(0.1) 55 | 56 | # Retrieve the results of successful jobs. 57 | metadata = job1.result() 58 | assert metadata['_id'] == 'students/Neal' 59 | 60 | metadata = job2.result() 61 | assert metadata['_id'] == 'students/Lily' 62 | 63 | cursor = job3.result() 64 | assert await cursor.next() == 100000 65 | 66 | # If a job fails, the exception is propagated up during result retrieval. 67 | try: 68 | result = job4.result() 69 | except AQLQueryExecuteError as err: 70 | assert err.http_code == 400 71 | assert err.error_code == 1501 72 | assert 'syntax error' in err.message 73 | 74 | # Cancel a job. Only pending jobs still in queue may be cancelled. 75 | # Since job3 is done, there is nothing to cancel and an exception is raised. 76 | try: 77 | await job3.cancel() 78 | except AsyncJobCancelError as err: 79 | assert err.message.endswith('job {} not found'.format(job3.id)) 80 | 81 | # Clear the result of a job from ArangoDB server to free up resources. 82 | # Result of job4 was removed from the server automatically upon retrieval, 83 | # so attempt to clear it raises an exception. 84 | try: 85 | await job4.clear() 86 | except AsyncJobClearError as err: 87 | assert err.message.endswith('job {} not found'.format(job4.id)) 88 | 89 | # List the IDs of the first 100 async jobs completed. 90 | await db.async_jobs(status='done', count=100) 91 | 92 | # List the IDs of the first 100 async jobs still pending. 93 | await db.async_jobs(status='pending', count=100) 94 | 95 | # Clear all async jobs still sitting on the server. 96 | await db.clear_async_jobs() 97 | 98 | .. note:: 99 | Be mindful of server-side memory capacity when issuing a large number of 100 | async requests in small time interval. 101 | 102 | See :ref:`AsyncDatabase` and :ref:`AsyncJob` for API specification. 103 | -------------------------------------------------------------------------------- /docs/batch.rst: -------------------------------------------------------------------------------- 1 | Batch Execution 2 | --------------- 3 | 4 | In **batch execution**, requests to ArangoDB server are stored in client-side 5 | in-memory queue, and committed together in a single HTTP call. After the commit, 6 | results can be retrieved later from :ref:`BatchJob` objects. 7 | 8 | **Example:** 9 | 10 | .. code-block:: python 11 | 12 | from aioarangodb import ArangoClient, AQLQueryExecuteError 13 | 14 | # Initialize the ArangoDB client. 15 | client = ArangoClient() 16 | 17 | # Connect to "test" database as root user. 18 | db = await client.db('test', username='root', password='passwd') 19 | 20 | # Get the API wrapper for "students" collection. 21 | students = db.collection('students') 22 | 23 | # Begin batch execution via context manager. This returns an instance of 24 | # BatchDatabase, a database-level API wrapper tailored specifically for 25 | # batch execution. The batch is automatically committed when exiting the 26 | # context. The BatchDatabase wrapper cannot be reused after commit. 27 | with db.begin_batch_execution(return_result=True) as batch_db: 28 | 29 | # Child wrappers are also tailored for batch execution. 30 | batch_aql = batch_db.aql 31 | batch_col = batch_db.collection('students') 32 | 33 | # API execution context is always set to "batch". 34 | assert batch_db.context == 'batch' 35 | assert batch_aql.context == 'batch' 36 | assert batch_col.context == 'batch' 37 | 38 | # BatchJob objects are returned instead of results. 39 | job1 = await batch_col.insert({'_key': 'Kris'}) 40 | job2 = await batch_col.insert({'_key': 'Rita'}) 41 | job3 = await batch_aql.execute('RETURN 100000') 42 | job4 = await batch_aql.execute('INVALID QUERY') # Fails due to syntax error. 43 | 44 | # Upon exiting context, batch is automatically committed. 45 | assert 'Kris' in students 46 | assert 'Rita' in students 47 | 48 | # Retrieve the status of each batch job. 49 | for job in batch_db.queued_jobs(): 50 | # Status is set to either "pending" (transaction is not committed yet 51 | # and result is not available) or "done" (transaction is committed and 52 | # result is available). 53 | assert await job.status() in {'pending', 'done'} 54 | 55 | # Retrieve the results of successful jobs. 56 | metadata = await job1.result() 57 | assert metadata['_id'] == 'students/Kris' 58 | 59 | metadata = await job2.result() 60 | assert metadata['_id'] == 'students/Rita' 61 | 62 | cursor = await 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 = await 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 | # Batch execution can be initiated without using a context manager. 74 | # If return_result parameter is set to False, no jobs are returned. 75 | batch_db = db.begin_batch_execution(return_result=False) 76 | await batch_db.collection('students').insert({'_key': 'Jake'}) 77 | await batch_db.collection('students').insert({'_key': 'Jill'}) 78 | 79 | # The commit must be called explicitly. 80 | await batch_db.commit() 81 | assert 'Jake' in students 82 | assert 'Jill' in students 83 | 84 | .. note:: 85 | * Be mindful of client-side memory capacity when issuing a large number of 86 | requests in single batch execution. 87 | * :ref:`BatchDatabase` and :ref:`BatchJob` instances are stateful objects, 88 | and should not be shared across multiple threads. 89 | * :ref:`BatchDatabase` instance cannot be reused after commit. 90 | 91 | See :ref:`BatchDatabase` and :ref:`BatchJob` for API specification. 92 | -------------------------------------------------------------------------------- /docs/cluster.rst: -------------------------------------------------------------------------------- 1 | Clusters 2 | -------- 3 | 4 | aioarangodb 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 aioarangodb 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 `aiohttp.ClientSession`_ 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 | Load-Balancing Strategies 35 | ========================= 36 | 37 | There are two load-balancing strategies available: "roundrobin" and "random" 38 | (defaults to "roundrobin" if unspecified). 39 | 40 | **Example:** 41 | 42 | .. testcode:: 43 | 44 | from aioarangodb import ArangoClient 45 | 46 | hosts = ['http://host1:8529', 'http://host2:8529'] 47 | 48 | # Round-robin 49 | client = ArangoClient(hosts=hosts, host_resolver='roundrobin') 50 | 51 | # Random 52 | client = ArangoClient(hosts=hosts, host_resolver='random') 53 | 54 | Administration 55 | ============== 56 | 57 | Below is an example on how to manage clusters using aioarangodb. 58 | 59 | .. code-block:: python 60 | 61 | from aioarangodb import ArangoClient 62 | 63 | # Initialize the ArangoDB client. 64 | client = ArangoClient() 65 | 66 | # Connect to "_system" database as root user. 67 | sys_db = await client.db('_system', username='root', password='passwd') 68 | 69 | # Get the Cluster API wrapper. 70 | cluster = sys_db.cluster 71 | 72 | # Get this server's ID. 73 | await cluster.server_id() 74 | 75 | # Get this server's role. 76 | await cluster.server_role() 77 | 78 | # Get the cluster health. 79 | await cluster.health() 80 | 81 | # Get statistics for a specific server. 82 | server_id = await cluster.server_id() 83 | await cluster.statistics(server_id) 84 | 85 | # Toggle maintenance mode (allowed values are "on" and "off"). 86 | await cluster.toggle_maintenance_mode('on') 87 | await cluster.toggle_maintenance_mode('off') 88 | 89 | See :ref:`ArangoClient` and :ref:`Cluster` for API specification. 90 | -------------------------------------------------------------------------------- /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 aioarangodb: 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 aioarangodb 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 | await 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 = await db.create_collection('students') 35 | 36 | # Retrieve collection properties. 37 | students.name 38 | students.db_name 39 | await students.properties() 40 | await students.revision() 41 | await students.statistics() 42 | await students.checksum() 43 | await students.count() 44 | 45 | # Perform various operations. 46 | await students.load() 47 | await students.unload() 48 | await students.truncate() 49 | await students.configure(journal_size=3000000) 50 | 51 | # Delete the collection. 52 | await db.delete_collection('students') 53 | 54 | See :ref:`StandardDatabase` and :ref:`StandardCollection` for API specification. 55 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # aioarangodb documentation build configuration file, created by 5 | # sphinx-quickstart on Thu Apr 19 03:40:33 2018. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | 21 | _version = {} 22 | with open("../aioarangodb/version.py") as fp: 23 | exec(fp.read(), _version) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.doctest', 37 | 'sphinx.ext.coverage', 38 | 'sphinx.ext.viewcode', 39 | 'sphinx.ext.githubpages', 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # The suffix(es) of source filenames. 46 | # You can specify multiple suffix as a list of string: 47 | # 48 | # source_suffix = ['.rst', '.md'] 49 | source_suffix = '.rst' 50 | 51 | # The master toctree document. 52 | master_doc = 'index' 53 | 54 | # General information about the project. 55 | project = 'aioarangodb' 56 | copyright = '2016, Joohwan Oh, Ramon Navarro' 57 | author = 'Joohwan Oh & Ramon Navarro' 58 | 59 | # The version info for the project you're documenting, acts as replacement for 60 | # |version| and |release|, also used in various other places throughout the 61 | # built documents. 62 | # 63 | # The short X.Y version. 64 | version = _version['__version__'] 65 | # The full version, including alpha/beta/rc tags. 66 | release = _version['__version__'] 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | # 71 | # This is also used if you do content translation via gettext catalogs. 72 | # Usually you set "language" from the command line for these cases. 73 | language = None 74 | 75 | # List of patterns, relative to source directory, that match files and 76 | # directories to ignore when looking for source files. 77 | # This patterns also effect to html_static_path and html_extra_path 78 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 79 | 80 | # The name of the Pygments (syntax highlighting) style to use. 81 | pygments_style = 'sphinx' 82 | 83 | # If true, `todo` and `todoList` produce output, else they produce nothing. 84 | todo_include_todos = True 85 | 86 | 87 | # -- Options for HTML output ---------------------------------------------- 88 | 89 | # The theme to use for HTML and HTML Help pages. See the documentation for 90 | # a list of builtin themes. 91 | # 92 | html_theme = 'sphinx_rtd_theme' 93 | 94 | # Theme options are theme-specific and customize the look and feel of a theme 95 | # further. For a list of options available for each theme, see the 96 | # documentation. 97 | # 98 | # html_theme_options = {} 99 | 100 | # Add any paths that contain custom static files (such as style sheets) here, 101 | # relative to this directory. They are copied after the builtin static files, 102 | # so a file named "default.css" will overwrite the builtin "default.css". 103 | html_static_path = ['static'] 104 | 105 | # Custom sidebar templates, must be a dictionary that maps document names 106 | # to template names. 107 | # 108 | # This is required for the alabaster theme 109 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 110 | html_sidebars = { 111 | '**': [ 112 | 'about.html', 113 | 'navigation.html', 114 | 'relations.html', # needs 'show_related': True theme option to display 115 | 'searchbox.html', 116 | 'donate.html', 117 | ] 118 | } 119 | 120 | 121 | # -- Options for HTMLHelp output ------------------------------------------ 122 | 123 | # Output file base name for HTML help builder. 124 | htmlhelp_basename = 'aioarangodbdoc' 125 | 126 | 127 | # -- Options for LaTeX output --------------------------------------------- 128 | 129 | latex_elements = { 130 | # The paper size ('letterpaper' or 'a4paper'). 131 | # 132 | # 'papersize': 'letterpaper', 133 | 134 | # The font size ('10pt', '11pt' or '12pt'). 135 | # 136 | # 'pointsize': '10pt', 137 | 138 | # Additional stuff for the LaTeX preamble. 139 | # 140 | # 'preamble': '', 141 | 142 | # Latex figure (float) alignment 143 | # 144 | # 'figure_align': 'htbp', 145 | } 146 | 147 | # Grouping the document tree into LaTeX files. List of tuples 148 | # (source start file, target name, title, 149 | # author, documentclass [howto, manual, or own class]). 150 | latex_documents = [ 151 | (master_doc, 'aioarangodb.tex', 'aioarangodb Documentation', 152 | 'Joohwan Oh & Ramon Navarro', 'manual'), 153 | ] 154 | 155 | 156 | # -- Options for manual page output --------------------------------------- 157 | 158 | # One entry per manual page. List of tuples 159 | # (source start file, name, description, authors, manual section). 160 | man_pages = [ 161 | (master_doc, 'aioarangodb', 'aioarangodb Documentation', 162 | [author], 1) 163 | ] 164 | 165 | 166 | # -- Options for Texinfo output ------------------------------------------- 167 | 168 | # Grouping the document tree into Texinfo files. List of tuples 169 | # (source start file, target name, title, author, 170 | # dir menu entry, description, category) 171 | texinfo_documents = [ 172 | (master_doc, 'aioarangodb', 'aioarangodb Documentation', 173 | author, 'aioarangodb', 'One line description of project.', 174 | 'Miscellaneous'), 175 | ] 176 | 177 | autodoc_member_order = 'bysource' 178 | 179 | doctest_global_setup = """ 180 | from aioarangodb import ArangoClient 181 | 182 | # Initialize the ArangoDB client. 183 | client = ArangoClient() 184 | 185 | # Connect to "_system" database as root user. 186 | sys_db = await client.db('_system', username='root', password='passwd') 187 | 188 | # Create "test" database if it does not exist. 189 | if not await sys_db.has_database('test'): 190 | await sys_db.create_database('test') 191 | 192 | # Ensure that user "johndoe@gmail.com" does not exist. 193 | if await sys_db.has_user('johndoe@gmail.com'): 194 | await sys_db.delete_user('johndoe@gmail.com') 195 | 196 | # Connect to "test" database as root user. 197 | db = await client.db('test', username='root', password='passwd') 198 | 199 | # Create "students" collection if it does not exist. 200 | if await db.has_collection('students'): 201 | await db.collection('students').truncate() 202 | else: 203 | await db.create_collection('students') 204 | 205 | # Ensure that "cities" collection does not exist. 206 | if await db.has_collection('cities'): 207 | await db.delete_collection('cities') 208 | 209 | # Create "school" graph if it does not exist. 210 | if await db.has_graph("school"): 211 | school = db.graph('school') 212 | else: 213 | await school = db.create_graph('school') 214 | 215 | # Create "teachers" vertex collection if it does not exist. 216 | if await school.has_vertex_collection('teachers'): 217 | school.vertex_collection('teachers').truncate() 218 | else: 219 | await school.create_vertex_collection('teachers') 220 | 221 | # Create "lectures" vertex collection if it does not exist. 222 | if await school.has_vertex_collection('lectures'): 223 | school.vertex_collection('lectures').truncate() 224 | else: 225 | await school.create_vertex_collection('lectures') 226 | 227 | # Create "teach" edge definition if it does not exist. 228 | if await school.has_edge_definition('teach'): 229 | school.edge_collection('teach').truncate() 230 | else: 231 | await school.create_edge_definition( 232 | edge_collection='teach', 233 | from_vertex_collections=['teachers'], 234 | to_vertex_collections=['lectures'] 235 | ) 236 | """ 237 | -------------------------------------------------------------------------------- /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 dev_ 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 on `Travis CI`_. 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/bloodbare/aioarangodb.git 44 | ~$ cd aioarangodb 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 | **aioarangodb**. 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/bloodbare/aioarangodb.git 61 | ~$ cd aioarangodb 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/bloodbare/aioarangodb.git 70 | ~$ cd aioarangodb 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/bloodbare/aioarangodb.git 86 | ~$ cd aioarangodb/docs 87 | ~$ sphinx-build . build # Open build/index.html in a browser 88 | 89 | As always, thank you for your contribution! 90 | 91 | .. _dev: https://github.com/bloodbare/aioarangodb/tree/dev 92 | .. _GitHub: https://github.com/bloodbare/aioarangodb 93 | .. _PEP8: https://www.python.org/dev/peps/pep-0008/ 94 | .. _coverage: https://coveralls.io/github/bloodbare/aioarangodb 95 | .. _this: http://coverage.readthedocs.io/en/latest/excluding.html 96 | .. _Travis CI: https://travis-ci.org/bloodbare/aioarangodb 97 | .. _Sphinx: https://github.com/sphinx-doc/sphinx 98 | .. _flake8: http://flake8.pycqa.org 99 | .. _here: http://flake8.pycqa.org/en/latest/user/violations.html#in-line-ignoring-errors 100 | .. _pytest: https://github.com/pytest-dev/pytest 101 | .. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText 102 | -------------------------------------------------------------------------------- /docs/cursor.rst: -------------------------------------------------------------------------------- 1 | Cursors 2 | ------- 3 | 4 | Many operations provided by aioarangodb (e.g. executing :doc:`aql` queries) 5 | return result **cursors** to batch the network communication between ArangoDB 6 | server and aioarangodb 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 aioarangodb import ArangoClient 15 | 16 | # Initialize the ArangoDB client. 17 | client = ArangoClient() 18 | 19 | # Connect to "test" database as root user. 20 | db = await client.db('test', username='root', password='passwd') 21 | 22 | # Set up some test data to query against. 23 | await 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 = await 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 | await cursor.batch() 44 | 45 | # Check if the current batch is empty. 46 | await cursor.empty() 47 | 48 | # Get the total count of the result set. 49 | await cursor.count() 50 | 51 | # Flag indicating if there are more to be fetched from server. 52 | await cursor.has_more() 53 | 54 | # Flag indicating if the results are cached. 55 | await cursor.cached() 56 | 57 | # Get the cursor statistics. 58 | await cursor.statistics() 59 | 60 | # Get the performance profile. 61 | await cursor.profile() 62 | 63 | # Get any warnings produced from the query. 64 | await cursor.warnings() 65 | 66 | # Return the next item from the cursor. If current batch is depleted, the 67 | # next batch if fetched from the server automatically. 68 | await 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 | await cursor.fetch() 76 | 77 | # Delete the cursor from the server. 78 | await 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`), aioarangodb 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 aioarangodb 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 | -------------------------------------------------------------------------------- /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 aioarangodb 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 | await 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 await sys_db.has_database('test'): 30 | await sys_db.create_database('test') 31 | 32 | # Delete the database. 33 | await 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 await sys_db.has_database('test'): 38 | await 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 = await client.db('test', username='jane', password='foo') 49 | 50 | # Make sure that user "jane" has read and write permissions. 51 | await 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 | await db.version() 57 | await db.status() 58 | await db.details() 59 | await db.collections() 60 | await db.graphs() 61 | await db.engine() 62 | 63 | # Delete the database. Note that the new users will remain. 64 | await sys_db.delete_database('test') 65 | 66 | See :ref:`ArangoClient` and :ref:`StandardDatabase` for API specification. 67 | -------------------------------------------------------------------------------- /docs/document.rst: -------------------------------------------------------------------------------- 1 | Documents 2 | --------- 3 | 4 | In aioarangodb, a **document** is a Python dictionary with the following 5 | properties: 6 | 7 | * Is JSON serializable. 8 | * May be nested to an arbitrary depth. 9 | * May contain lists. 10 | * Contains the ``_key`` field, which identifies the document uniquely within a 11 | specific collection. 12 | * Contains the ``_id`` field (also called the *handle*), which identifies the 13 | document uniquely across all collections within a database. This ID is a 14 | combination of the collection name and the document key using the format 15 | ``{collection}/{key}`` (see example below). 16 | * Contains the ``_rev`` field. ArangoDB supports MVCC (Multiple Version 17 | Concurrency Control) and is capable of storing each document in multiple 18 | revisions. Latest revision of a document is indicated by this field. The 19 | field is populated by ArangoDB and is not required as input unless you want 20 | to validate a document against its current revision. 21 | 22 | For more information on documents and associated terminologies, refer to 23 | `ArangoDB manual`_. Here is an example of a valid document in "students" 24 | collection: 25 | 26 | .. _ArangoDB manual: https://docs.arangodb.com 27 | 28 | .. testcode:: 29 | 30 | { 31 | '_id': 'students/bruce', 32 | '_key': 'bruce', 33 | '_rev': '_Wm3dzEi--_', 34 | 'first_name': 'Bruce', 35 | 'last_name': 'Wayne', 36 | 'address': { 37 | 'street' : '1007 Mountain Dr.', 38 | 'city': 'Gotham', 39 | 'state': 'NJ' 40 | }, 41 | 'is_rich': True, 42 | 'friends': ['robin', 'gordon'] 43 | } 44 | 45 | .. _edge-documents: 46 | 47 | **Edge documents (edges)** are similar to standard documents but with two 48 | additional required fields ``_from`` and ``_to``. Values of these fields must 49 | be the handles of "from" and "to" vertex documents linked by the edge document 50 | in question (see :doc:`graph` for details). Edge documents are contained in 51 | :ref:`edge collections `. Here is an example of a valid edge 52 | document in "friends" edge collection: 53 | 54 | .. testcode:: 55 | 56 | { 57 | '_id': 'friends/001', 58 | '_key': '001', 59 | '_rev': '_Wm3d4le--_', 60 | '_from': 'students/john', 61 | '_to': 'students/jane', 62 | 'closeness': 9.5 63 | } 64 | 65 | Standard documents are managed via collection API wrapper: 66 | 67 | .. testcode:: 68 | 69 | from aioarangodb import ArangoClient 70 | 71 | # Initialize the ArangoDB client. 72 | client = ArangoClient() 73 | 74 | # Connect to "test" database as root user. 75 | db = await client.db('test', username='root', password='passwd') 76 | 77 | # Get the API wrapper for "students" collection. 78 | students = db.collection('students') 79 | 80 | # Create some test documents to play around with. 81 | lola = {'_key': 'lola', 'GPA': 3.5, 'first': 'Lola', 'last': 'Martin'} 82 | abby = {'_key': 'abby', 'GPA': 3.2, 'first': 'Abby', 'last': 'Page'} 83 | john = {'_key': 'john', 'GPA': 3.6, 'first': 'John', 'last': 'Kim'} 84 | emma = {'_key': 'emma', 'GPA': 4.0, 'first': 'Emma', 'last': 'Park'} 85 | 86 | # Insert a new document. This returns the document metadata. 87 | metadata = await students.insert(lola) 88 | assert metadata['_id'] == 'students/lola' 89 | assert metadata['_key'] == 'lola' 90 | 91 | # Check if documents exist in the collection in multiple ways. 92 | assert await students.has('lola') and 'john' not in students 93 | 94 | # Retrieve the total document count in multiple ways. 95 | assert await students.count() == 1 96 | 97 | # Insert multiple documents in bulk. 98 | await students.import_bulk([abby, john, emma]) 99 | 100 | # Retrieve one or more matching documents. 101 | for student in await students.find({'first': 'John'}): 102 | assert student['_key'] == 'john' 103 | assert student['GPA'] == 3.6 104 | assert student['last'] == 'Kim' 105 | 106 | # Retrieve a document by key. 107 | await students.get('john') 108 | 109 | # Retrieve a document by ID. 110 | await students.get('students/john') 111 | 112 | # Retrieve a document by body with "_id" field. 113 | await students.get({'_id': 'students/john'}) 114 | 115 | # Retrieve a document by body with "_key" field. 116 | await students.get({'_key': 'john'}) 117 | 118 | # Retrieve multiple documents by ID, key or body. 119 | await students.get_many(['abby', 'students/lola', {'_key': 'john'}]) 120 | 121 | # Update a single document. 122 | lola['GPA'] = 2.6 123 | await students.update(lola) 124 | 125 | # Update one or more matching documents. 126 | await students.update_match({'last': 'Park'}, {'GPA': 3.0}) 127 | 128 | # Replace a single document. 129 | emma['GPA'] = 3.1 130 | await students.replace(emma) 131 | 132 | # Replace one or more matching documents. 133 | becky = {'first': 'Becky', 'last': 'Solis', 'GPA': '3.3'} 134 | await students.replace_match({'first': 'Emma'}, becky) 135 | 136 | # Delete a document by key. 137 | await students.delete('john') 138 | 139 | # Delete a document by ID. 140 | await students.delete('students/lola') 141 | 142 | # Delete a document by body with "_id" or "_key" field. 143 | await students.delete(emma) 144 | 145 | # Delete multiple documents. Missing ones are ignored. 146 | await students.delete_many([abby, 'john', 'students/lola']) 147 | 148 | # Iterate through all documents and update individually. 149 | async for student in students: 150 | student['GPA'] = 4.0 151 | student['happy'] = True 152 | students.update(student) 153 | 154 | You can manage documents via database API wrappers also, but only simple 155 | operations (i.e. get, insert, update, replace, delete) are supported and you 156 | must provide document IDs instead of keys: 157 | 158 | .. testcode:: 159 | 160 | from aioarangodb import ArangoClient 161 | 162 | # Initialize the ArangoDB client. 163 | client = ArangoClient() 164 | 165 | # Connect to "test" database as root user. 166 | db = await client.db('test', username='root', password='passwd') 167 | 168 | # Create some test documents to play around with. 169 | # The documents must have the "_id" field instead. 170 | lola = {'_id': 'students/lola', 'GPA': 3.5} 171 | abby = {'_id': 'students/abby', 'GPA': 3.2} 172 | john = {'_id': 'students/john', 'GPA': 3.6} 173 | emma = {'_id': 'students/emma', 'GPA': 4.0} 174 | 175 | # Insert a new document. 176 | metadata = await db.insert_document('students', lola) 177 | assert metadata['_id'] == 'students/lola' 178 | assert metadata['_key'] == 'lola' 179 | 180 | # Check if a document exists. 181 | assert await db.has_document(lola) is True 182 | 183 | # Get a document (by ID or body with "_id" field). 184 | await db.document('students/lola') 185 | await db.document(abby) 186 | 187 | # Update a document. 188 | lola['GPA'] = 3.6 189 | await db.update_document(lola) 190 | 191 | # Replace a document. 192 | lola['GPA'] = 3.4 193 | await db.replace_document(lola) 194 | 195 | # Delete a document (by ID or body with "_id" field). 196 | await db.delete_document('students/lola') 197 | 198 | See :ref:`StandardDatabase` and :ref:`StandardCollection` for API specification. 199 | 200 | When managing documents, using collection API wrappers over database API 201 | wrappers is recommended as more operations are available and less sanity 202 | checking is performed under the hood. 203 | -------------------------------------------------------------------------------- /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 aioarangodb 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 aioarangodb exceptions inherit :class:`aioarangodb.exceptions.ArangoError`, 5 | which splits into subclasses :class:`aioarangodb.exceptions.ArangoServerError` and 6 | :class:`aioarangodb.exceptions.ArangoClientError`. 7 | 8 | Server Errors 9 | ============= 10 | 11 | :class:`aioarangodb.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 aioarangodb import ArangoClient, ArangoServerError, DocumentInsertError 20 | 21 | # Initialize the ArangoDB client. 22 | client = ArangoClient() 23 | 24 | # Connect to "test" database as root user. 25 | db = await 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 | await students.insert({'_key': 'John'}) 32 | await 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:`aioarangodb.exceptions.ArangoClientError` exceptions originate from 73 | aioarangodb client itself. They do not contain error codes nor HTTP request 74 | response details. 75 | 76 | **Example:** 77 | 78 | .. testcode:: 79 | 80 | from aioarangodb import ArangoClient, ArangoClientError, DocumentParseError 81 | 82 | # Initialize the ArangoDB client. 83 | client = ArangoClient() 84 | 85 | # Connect to "test" database as root user. 86 | db = await 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 | await 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 aioarangodb. 114 | 115 | .. automodule:: aioarangodb.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 aioarangodb 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 | await foxx.create_service( 33 | mount=service_mount, 34 | source='/tmp/service.zip', 35 | config={}, 36 | dependencies={}, 37 | development=True, 38 | setup=True, 39 | legacy=True 40 | ) 41 | 42 | # Update (upgrade) a service. 43 | service = await db.foxx.update_service( 44 | mount=service_mount, 45 | source='/tmp/service.zip', 46 | config={}, 47 | dependencies={}, 48 | teardown=True, 49 | setup=True, 50 | legacy=False 51 | ) 52 | 53 | # Replace (overwrite) a service. 54 | service = await db.foxx.replace_service( 55 | mount=service_mount, 56 | source='/tmp/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 | await foxx.service(service_mount) 67 | 68 | # Manage service configuration. 69 | await foxx.config(service_mount) 70 | await foxx.update_config(service_mount, config={}) 71 | await foxx.replace_config(service_mount, config={}) 72 | 73 | # Manage service dependencies. 74 | await foxx.dependencies(service_mount) 75 | await foxx.update_dependencies(service_mount, dependencies={}) 76 | await foxx.replace_dependencies(service_mount, dependencies={}) 77 | 78 | # Toggle development mode for a service. 79 | await foxx.enable_development(service_mount) 80 | await foxx.disable_development(service_mount) 81 | 82 | # Other miscellaneous functions. 83 | await foxx.readme(service_mount) 84 | await foxx.swagger(service_mount) 85 | await foxx.download(service_mount) 86 | await foxx.commit(service_mount) 87 | await foxx.scripts(service_mount) 88 | await foxx.run_script(service_mount, 'setup', []) 89 | await foxx.run_tests(service_mount, reporter='xunit', output_format='xml') 90 | 91 | # Delete a service. 92 | await 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 aioarangodb import ArangoClient 99 | 100 | # Initialize the ArangoDB client. 101 | client = ArangoClient() 102 | 103 | # Connect to "_system" database as root user. 104 | db = await 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 | await 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 | await 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 | await 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 | await foxx.delete_service(service_mount) 143 | 144 | See :ref:`Foxx` for API specification. 145 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | Welcome to the documentation for **aioarangodb**, a Python driver for ArangoDB_ with AsyncIO. 3 | 4 | 5 | Features 6 | ======== 7 | 8 | - Pythonic interface 9 | - Lightweight 10 | - High API coverage 11 | 12 | Compatibility 13 | ============= 14 | 15 | - Python versions 3.5, 3.6 and 3.7 are supported 16 | - aioArangoDB supports ArangoDB 3.5+ 17 | 18 | Installation 19 | ============ 20 | 21 | To install a stable version from PyPi_: 22 | 23 | .. code-block:: bash 24 | 25 | ~$ pip install aioarangodb 26 | 27 | 28 | To install the latest version directly from GitHub_: 29 | 30 | .. code-block:: bash 31 | 32 | ~$ pip install -e git+git@github.com:bloodbare/aioarangodb.git@master#egg=aioarangodb 33 | 34 | 35 | You may need to use ``sudo`` depending on your environment. 36 | 37 | .. _ArangoDB: https://www.arangodb.com 38 | .. _PyPi: https://pypi.python.org/pypi/aioarangodb 39 | .. _GitHub: https://github.com/bloodbare/aioarangodb 40 | 41 | 42 | Contents 43 | ======== 44 | 45 | .. toctree:: 46 | :maxdepth: 1 47 | 48 | overview 49 | database 50 | collection 51 | document 52 | indexes 53 | graph 54 | aql 55 | cursor 56 | async 57 | batch 58 | transaction 59 | admin 60 | user 61 | task 62 | wal 63 | pregel 64 | foxx 65 | view 66 | analyzer 67 | threading 68 | errors 69 | replication 70 | cluster 71 | serializer 72 | errno 73 | contributing 74 | specs 75 | -------------------------------------------------------------------------------- /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 aioarangodb import ArangoClient 17 | 18 | # Initialize the ArangoDB client. 19 | client = ArangoClient() 20 | 21 | # Connect to "test" database as root user. 22 | db = await client.db('test', username='root', password='passwd') 23 | 24 | # Create a new collection named "cities". 25 | cities = await db.create_collection('cities') 26 | 27 | # List the indexes in the collection. 28 | await cities.indexes() 29 | 30 | # Add a new hash index on document fields "continent" and "country". 31 | index = await cities.add_hash_index(fields=['continent', 'country'], unique=True) 32 | 33 | # Add new fulltext indexes on fields "continent" and "country". 34 | index = await cities.add_fulltext_index(fields=['continent']) 35 | index = await cities.add_fulltext_index(fields=['country']) 36 | 37 | # Add a new skiplist index on field 'population'. 38 | index = await cities.add_skiplist_index(fields=['population'], sparse=False) 39 | 40 | # Add a new geo-spatial index on field 'coordinates'. 41 | index = await cities.add_geo_index(fields=['coordinates']) 42 | 43 | # Add a new persistent index on field 'currency'. 44 | index = await cities.add_persistent_index(fields=['currency'], sparse=True) 45 | 46 | # Add a new TTL (time-to-live) index on field 'currency'. 47 | index = await cities.add_ttl_index(fields=['ttl'], expiry_time=200) 48 | 49 | # Indexes may be added with a name that can be referred to in AQL queries. 50 | index = await cities.add_hash_index(fields=['country'], name='my_hash_index') 51 | 52 | # Delete the last index from the collection. 53 | await cities.delete_index(index['id']) 54 | 55 | See :ref:`StandardCollection` for API specification. 56 | -------------------------------------------------------------------------------- /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=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=aioarangodb 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | --------------- 3 | 4 | Here is an example showing how **aioarangodb** client can be used: 5 | 6 | .. testcode:: 7 | 8 | from aioarangodb 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 = await client.db('_system', username='root', password='passwd') 16 | 17 | # Create a new database named "test" if it does not exist. 18 | if not await sys_db.has_database('test'): 19 | await 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 = await 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 = await db.create_collection('students') 31 | 32 | # Add a hash index to the collection. 33 | await students.add_hash_index(fields=['name'], unique=False) 34 | 35 | # Truncate the collection. 36 | await students.truncate() 37 | 38 | # Insert new documents into the collection. 39 | await students.insert({'name': 'jane', 'age': 19}) 40 | await students.insert({'name': 'josh', 'age': 18}) 41 | await students.insert({'name': 'jake', 'age': 21}) 42 | 43 | # Execute an AQL query. This returns a result cursor. 44 | cursor = await db.aql.execute('FOR doc IN students RETURN doc') 45 | 46 | # Iterate through the cursor to retrieve the documents. 47 | student_names = [document['name'] async for document in cursor] 48 | -------------------------------------------------------------------------------- /docs/pregel.rst: -------------------------------------------------------------------------------- 1 | Pregel 2 | ------ 3 | 4 | Python-arango provides support for **Pregel**, ArangoDB module for distributed 5 | iterative graph processing. For more information, refer to `ArangoDB manual`_. 6 | 7 | .. _ArangoDB manual: https://docs.arangodb.com 8 | 9 | **Example:** 10 | 11 | .. testcode:: 12 | 13 | from aioarangodb import ArangoClient 14 | 15 | # Initialize the ArangoDB client. 16 | client = ArangoClient() 17 | 18 | # Connect to "test" database as root user. 19 | db = await client.db('test', username='root', password='passwd') 20 | 21 | # Get the Pregel API wrapper. 22 | pregel = db.pregel 23 | 24 | # Start a new Pregel job in "school" graph. 25 | job_id = await db.pregel.create_job( 26 | graph='school', 27 | algorithm='pagerank', 28 | store=False, 29 | max_gss=100, 30 | thread_count=1, 31 | async_mode=False, 32 | result_field='result', 33 | algorithm_params={'threshold': 0.000001} 34 | ) 35 | 36 | # Retrieve details of a Pregel job by ID. 37 | job = await pregel.job(job_id) 38 | 39 | # Delete a Pregel job by ID. 40 | await pregel.delete_job(job_id) 41 | 42 | See :ref:`Pregel` for API specification. 43 | -------------------------------------------------------------------------------- /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 aioarangodb import ArangoClient 16 | 17 | # Initialize the ArangoDB client. 18 | client = ArangoClient() 19 | 20 | # Connect to "test" database as root user. 21 | db = await 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 = await replication.create_dump_batch(ttl=1000) 28 | 29 | # Extend an existing dump batch. 30 | await replication.extend_dump_batch(batch['id'], ttl=1000) 31 | 32 | # Get an overview of collections and indexes. 33 | await 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 | await replication.cluster_inventory(include_system=True) 41 | 42 | # Get the events data for given collection. 43 | await 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 | await replication.delete_dump_batch(batch['id']) 56 | 57 | # Get the logger state. 58 | await replication.logger_state() 59 | 60 | # Get the logger first tick value. 61 | await replication.logger_first_tick() 62 | 63 | # Get the replication applier configuration. 64 | await replication.applier_config() 65 | 66 | # Update the replication applier configuration. 67 | result = await 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 | await replication.applier_state() 93 | 94 | # Start the replication applier. 95 | await replication.start_applier() 96 | 97 | # Stop the replication applier. 98 | await replication.stop_applier() 99 | 100 | # Get the server ID. 101 | await replication.server_id() 102 | 103 | # Synchronize data from a remote (master) endpoint 104 | await 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/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 aioarangodb 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. -------------------------------------------------------------------------------- /docs/specs.rst: -------------------------------------------------------------------------------- 1 | API Specification 2 | ----------------- 3 | 4 | This page contains the specification for all classes and methods available in 5 | aioarangodb. 6 | 7 | .. _ArangoClient: 8 | 9 | ArangoClient 10 | ============ 11 | 12 | .. autoclass:: aioarangodb.client.ArangoClient 13 | :members: 14 | 15 | .. _AsyncDatabase: 16 | 17 | AsyncDatabase 18 | ============= 19 | 20 | .. autoclass:: aioarangodb.database.AsyncDatabase 21 | :inherited-members: 22 | :members: 23 | 24 | .. _AsyncJob: 25 | 26 | AsyncJob 27 | ======== 28 | 29 | .. autoclass:: aioarangodb.job.AsyncJob 30 | :members: 31 | 32 | .. _AQL: 33 | 34 | AQL 35 | ==== 36 | 37 | .. autoclass:: aioarangodb.aql.AQL 38 | :members: 39 | 40 | .. _AQLQueryCache: 41 | 42 | AQLQueryCache 43 | ============= 44 | 45 | .. autoclass:: aioarangodb.aql.AQLQueryCache 46 | :members: 47 | 48 | .. _BatchDatabase: 49 | 50 | BatchDatabase 51 | ============= 52 | 53 | .. autoclass:: aioarangodb.database.BatchDatabase 54 | :inherited-members: 55 | :members: 56 | 57 | .. _BatchJob: 58 | 59 | BatchJob 60 | ======== 61 | 62 | .. autoclass:: aioarangodb.job.BatchJob 63 | :members: 64 | 65 | .. _Cluster: 66 | 67 | Cluster 68 | ======= 69 | 70 | .. autoclass:: aioarangodb.cluster.Cluster 71 | :members: 72 | 73 | .. _Cursor: 74 | 75 | Cursor 76 | ====== 77 | 78 | .. autoclass:: aioarangodb.cursor.Cursor 79 | :members: 80 | 81 | .. _DefaultHTTPClient: 82 | 83 | DefaultHTTPClient 84 | ================= 85 | 86 | .. autoclass:: aioarangodb.http.DefaultHTTPClient 87 | :members: 88 | 89 | .. _StandardCollection: 90 | 91 | StandardCollection 92 | ================== 93 | 94 | .. autoclass:: aioarangodb.collection.StandardCollection 95 | :inherited-members: 96 | :members: 97 | 98 | .. _StandardDatabase: 99 | 100 | StandardDatabase 101 | ================ 102 | 103 | .. autoclass:: aioarangodb.database.StandardDatabase 104 | :inherited-members: 105 | :members: 106 | 107 | .. _EdgeCollection: 108 | 109 | EdgeCollection 110 | ============== 111 | 112 | .. autoclass:: aioarangodb.collection.EdgeCollection 113 | :members: 114 | 115 | .. _Foxx: 116 | 117 | Foxx 118 | ==== 119 | 120 | .. autoclass:: aioarangodb.foxx.Foxx 121 | :members: 122 | 123 | .. _Graph: 124 | 125 | Graph 126 | ===== 127 | 128 | .. autoclass:: aioarangodb.graph.Graph 129 | :members: 130 | 131 | .. _HTTPClient: 132 | 133 | HTTPClient 134 | ========== 135 | 136 | .. autoclass:: aioarangodb.http.HTTPClient 137 | :members: 138 | 139 | .. _Pregel: 140 | 141 | Pregel 142 | ====== 143 | 144 | .. autoclass:: aioarangodb.pregel.Pregel 145 | :members: 146 | 147 | .. _Request: 148 | 149 | Request 150 | ======= 151 | 152 | .. autoclass:: aioarangodb.request.Request 153 | :members: 154 | 155 | .. _Response: 156 | 157 | Response 158 | ======== 159 | 160 | .. autoclass:: aioarangodb.response.Response 161 | :members: 162 | 163 | .. _Replication: 164 | 165 | Replication 166 | =========== 167 | 168 | .. autoclass:: aioarangodb.replication.Replication 169 | :members: 170 | 171 | .. _TransactionDatabase: 172 | 173 | TransactionDatabase 174 | =================== 175 | 176 | .. autoclass:: aioarangodb.database.TransactionDatabase 177 | :inherited-members: 178 | :members: 179 | 180 | .. _VertexCollection: 181 | 182 | VertexCollection 183 | ================ 184 | 185 | .. autoclass:: aioarangodb.collection.VertexCollection 186 | :members: 187 | 188 | .. _WriteAheadLog: 189 | 190 | WAL 191 | ==== 192 | 193 | .. autoclass:: aioarangodb.wal.WAL 194 | :members: 195 | -------------------------------------------------------------------------------- /docs/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getflaps/aioarangodb/b7e4c888e9bb3a93c7efbb3b0321de28ee5b5ce9/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 aioarangodb import ArangoClient 13 | 14 | # Initialize the ArangoDB client. 15 | client = ArangoClient() 16 | 17 | # Connect to "test" database as root user. 18 | db = await client.db('test', username='root', password='passwd') 19 | 20 | # List all active tasks 21 | await db.tasks() 22 | 23 | # Create a new task which simply prints parameters. 24 | await 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 | await db.task('001') 41 | 42 | # Delete an existing task by ID. 43 | await 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 aioarangodb 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 `aiohttp.ClientSession`_ 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 | -------------------------------------------------------------------------------- /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 | **Example:** 8 | 9 | .. testcode:: 10 | 11 | from aioarangodb import ArangoClient 12 | 13 | # Initialize the ArangoDB client. 14 | client = ArangoClient() 15 | 16 | # Connect to "test" database as root user. 17 | db = await client.db('test', username='root', password='passwd') 18 | col = db.collection('students') 19 | 20 | # Begin a transaction. Read and write collections must be declared ahead of 21 | # time. This returns an instance of TransactionDatabase, database-level 22 | # API wrapper tailored specifically for executing transactions. 23 | txn_db = await db.begin_transaction(read=col.name, write=col.name) 24 | 25 | # The API wrapper is specific to a single transaction with a unique ID. 26 | txn_db.transaction_id 27 | 28 | # Child wrappers are also tailored only for the specific transaction. 29 | txn_aql = txn_db.aql 30 | txn_col = txn_db.collection('students') 31 | 32 | # API execution context is always set to "transaction". 33 | assert txn_db.context == 'transaction' 34 | assert txn_aql.context == 'transaction' 35 | assert txn_col.context == 'transaction' 36 | 37 | assert '_rev' in await txn_col.insert({'_key': 'Abby'}) 38 | assert '_rev' in await txn_col.insert({'_key': 'John'}) 39 | assert '_rev' in await txn_col.insert({'_key': 'Mary'}) 40 | 41 | # Check the transaction status. 42 | await txn_db.transaction_status() 43 | 44 | # Commit the transaction. 45 | await txn_db.commit_transaction() 46 | assert await col.has('Abby') 47 | assert await col.has('John') 48 | assert await col.has('Mary') 49 | assert await col.count() == 3 50 | 51 | # Begin another transaction. Note that the wrappers above are specific to 52 | # the last transaction and cannot be reused. New ones must be created. 53 | txn_db = db.begin_transaction(read=col.name, write=col.name) 54 | txn_col = txn_db.collection('students') 55 | assert '_rev' in await txn_col.insert({'_key': 'Kate'}) 56 | assert '_rev' in await txn_col.insert({'_key': 'Mike'}) 57 | assert '_rev' in await txn_col.insert({'_key': 'Lily'}) 58 | assert await txn_col.count() == 6 59 | 60 | # Abort the transaction 61 | await txn_db.abort_transaction() 62 | assert not await col.has('Kate') 63 | assert not await col.has('Mike') 64 | assert not await col.has('Lily') 65 | assert await col.count() == 3 # transaction is aborted so txn_col cannot be used 66 | 67 | See :ref:`TransactionDatabase` for API specification. 68 | 69 | Alternatively, you can use 70 | :func:`arango.database.StandardDatabase.execute_transaction` to run raw 71 | Javascript code in a transaction. 72 | 73 | **Example:** 74 | 75 | .. testcode:: 76 | 77 | from aioarangodb import ArangoClient 78 | 79 | # Initialize the ArangoDB client. 80 | client = ArangoClient() 81 | 82 | # Connect to "test" database as root user. 83 | db = await client.db('test', username='root', password='passwd') 84 | 85 | # Get the API wrapper for "students" collection. 86 | students = db.collection('students') 87 | 88 | # Execute transaction in raw Javascript. 89 | result = await db.execute_transaction( 90 | command=''' 91 | function () {{ 92 | var db = require('internal').db; 93 | db.students.save(params.student1); 94 | if (db.students.count() > 1) { 95 | db.students.save(params.student2); 96 | } else { 97 | db.students.save(params.student3); 98 | } 99 | return true; 100 | }} 101 | ''', 102 | params={ 103 | 'student1': {'_key': 'Lucy'}, 104 | 'student2': {'_key': 'Greg'}, 105 | 'student3': {'_key': 'Dona'} 106 | }, 107 | read='students', # Specify the collections read. 108 | write='students' # Specify the collections written. 109 | ) 110 | assert result is True 111 | assert await students.has('Lucy') 112 | assert await students.has('Greg') 113 | assert await students.has('Dona') -------------------------------------------------------------------------------- /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 aioarangodb import ArangoClient 12 | 13 | # Initialize the ArangoDB client. 14 | client = ArangoClient() 15 | 16 | # Connect to "_system" database as root user. 17 | sys_db = await client.db('_system', username='root', password='passwd') 18 | 19 | # List all users. 20 | await sys_db.users() 21 | 22 | # Create a new user. 23 | await 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 | await sys_db.has_user('johndoe@gmail.com') 32 | 33 | # Retrieve details of a user. 34 | await sys_db.user('johndoe@gmail.com') 35 | 36 | # Update an existing user. 37 | await 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 | await 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 | await sys_db.permissions('johndoe@gmail.com') 54 | 55 | # Retrieve user permission for "test" database. 56 | await sys_db.permission( 57 | username='johndoe@gmail.com', 58 | database='test' 59 | ) 60 | 61 | # Retrieve user permission for "students" collection in "test" database. 62 | await sys_db.permission( 63 | username='johndoe@gmail.com', 64 | database='test', 65 | collection='students' 66 | ) 67 | 68 | # Update user permission for "test" database. 69 | await 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 | await 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 | await 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 | await sys_db.reset_permission( 91 | username='johndoe@gmail.com', 92 | database='test', 93 | collection='students' 94 | ) 95 | 96 | See :ref:`StandardDatabase` for API specification. -------------------------------------------------------------------------------- /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 aioarangodb import ArangoClient 14 | 15 | # Initialize the ArangoDB client. 16 | client = ArangoClient() 17 | 18 | # Connect to "test" database as root user. 19 | db = await client.db('test', username='root', password='passwd') 20 | 21 | # Retrieve list of views. 22 | await db.views() 23 | 24 | # Create a view. 25 | await 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 | await db.rename_view('foo', 'bar') 36 | 37 | # Retrieve view properties. 38 | await db.view('bar') 39 | 40 | # Partially update view properties. 41 | await 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 | await db.replace_view( 51 | name='bar', 52 | properties={'cleanupIntervalStep': 2000} 53 | ) 54 | 55 | # Delete a view. 56 | await db.delete_view('bar') 57 | 58 | 59 | Python-arango also supports **ArangoSearch** views. 60 | 61 | **Example:** 62 | 63 | .. testcode:: 64 | 65 | from aioarangodb import ArangoClient 66 | 67 | # Initialize the ArangoDB client. 68 | client = ArangoClient() 69 | 70 | # Connect to "test" database as root user. 71 | db = await client.db('test', username='root', password='passwd') 72 | 73 | # Create an ArangoSearch view. 74 | await db.create_arangosearch_view( 75 | name='arangosearch_view', 76 | properties={'cleanupIntervalStep': 0} 77 | ) 78 | 79 | # Partially update an ArangoSearch view. 80 | await db.update_arangosearch_view( 81 | name='arangosearch_view', 82 | properties={'cleanupIntervalStep': 1000} 83 | ) 84 | 85 | # Replace an ArangoSearch view. 86 | await 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 | await db.view('arangosearch_view') 93 | await 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 aioarangodb import ArangoClient 14 | 15 | # Initialize the ArangoDB client. 16 | client = ArangoClient() 17 | 18 | # Connect to "_system" database as root user. 19 | sys_db = await 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 | await 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 | await wal.properties() 36 | 37 | # List WAL transactions. 38 | await wal.transactions() 39 | 40 | # Flush WAL with garbage collection. 41 | await wal.flush(garbage_collect=True) 42 | 43 | # Get the available ranges of tick values. 44 | await wal.tick_ranges() 45 | 46 | # Get the last available tick value. 47 | await wal.last_tick() 48 | 49 | # Get recent WAL operations. 50 | await wal.tail() 51 | 52 | See :class:`WriteAheadLog` for API specification. 53 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | pyjwt 3 | six -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | ignore = D1 I100 D100 D101 E501 6 | exclude = .git,__pycache__,old,build,dist,.venv,.git,.tox,dist,docs,*lib/python*,*egg,builds 7 | max-complexity = 14 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | import re 4 | 5 | 6 | def load_reqs(filename): 7 | with open(filename) as reqs_file: 8 | return [ 9 | re.sub('==', '>=', line) for line in reqs_file.readlines() 10 | if not re.match('\s*#', line) 11 | ] 12 | 13 | 14 | version = open('VERSION').read().rstrip('\n') 15 | requirements = load_reqs('requirements.txt') 16 | test_requirements = load_reqs('test-requirements.txt') 17 | 18 | try: 19 | README = open('README.rst').read() + '\n\n' + open('CHANGELOG.rst').read() 20 | except IOError: 21 | README = None 22 | 23 | setup( 24 | name='aioarangodb', 25 | version=version, 26 | description='AsyncIO ArangoDB driver', 27 | long_description=README, 28 | classifiers=[ 29 | 'Development Status :: 2 - Pre-Alpha', 30 | 'Intended Audience :: Developers', 31 | 'Programming Language :: Python :: 3.5', 32 | 'Programming Language :: Python :: 3.6', 33 | 'Programming Language :: Python :: 3.7', 34 | 'Programming Language :: Python :: 3.8', 35 | ], 36 | url='http://github.com/bloodbare/aioarangodb', 37 | author='Ramon Navarro', 38 | author_email='ramon.nb@gmail.com', 39 | license='MIT', 40 | packages=find_packages(), 41 | zip_safe=False, 42 | install_requires=requirements, 43 | test_suite='tests', 44 | tests_require=test_requirements 45 | ) 46 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest-cov 2 | python-coveralls 3 | mock 4 | pytest 5 | pytest-asyncio 6 | pytest_docker_fixtures 7 | requests 8 | --------------------------------------------------------------------------------