diff --git a/README.md b/README.md index 3407789..9e124b6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ +![Logo](docs/static/logo.png) + +[![CircleCI](https://dl.circleci.com/status-badge/img/gh/arangodb/python-arango-async/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/arangodb/python-arango-async/tree/main) +[![CodeQL](https://github.com/arangodb/python-arango-async/actions/workflows/codeql.yaml/badge.svg)](https://github.com/arangodb/python-arango-async/actions/workflows/codeql.yaml) +[![Last commit](https://img.shields.io/github/last-commit/arangodb/python-arango-async)](https://github.com/arangodb/python-arango-async/commits/main) + +[![PyPI version badge](https://img.shields.io/pypi/v/python-arango-async?color=3775A9&style=for-the-badge&logo=pypi&logoColor=FFD43B)](https://pypi.org/project/python-arango-async/) +[![Python versions badge](https://img.shields.io/badge/3.9%2B-3776AB?style=for-the-badge&logo=python&logoColor=FFD43B&label=Python)](https://pypi.org/project/python-arango-async/) + +[![License](https://img.shields.io/github/license/arangodb/python-arango?color=9E2165&style=for-the-badge)](https://github.com/arangodb/python-arango/blob/main/LICENSE) +[![Code style: black](https://img.shields.io/static/v1?style=for-the-badge&label=code%20style&message=black&color=black)](https://github.com/psf/black) +[![Downloads](https://img.shields.io/pepy/dt/python-arango-async?style=for-the-badge&color=282661 +)](https://pepy.tech/project/python-arango-async) + # python-arango-async Python driver for [ArangoDB](https://www.arangodb.com), a scalable multi-model @@ -6,9 +20,60 @@ database natively supporting documents, graphs and search. This is the _asyncio_ alternative of the officially supported [python-arango](https://github.com/arangodb/python-arango) driver. -**Note**: This driver is still in development and not yet ready for production use. +**Note: This project is still in active development, features might be added or removed.** ## Requirements -- ArangoDB version 3.10+ +- ArangoDB version 3.11+ - Python version 3.9+ + +## Installation + +```shell +pip install python-arango-async --upgrade +``` + +## Getting Started + +Here is a simple usage example: + +```python +from arangoasync import ArangoClient +from arangoasync.auth import Auth + + +async def main(): + # Initialize the client for ArangoDB. + async with ArangoClient(hosts="http://localhost:8529") as client: + auth = Auth(username="root", password="passwd") + + # Connect to "_system" database as root user. + sys_db = await client.db("_system", auth=auth) + + # Create a new database named "test". + await sys_db.create_database("test") + + # Connect to "test" database as root user. + db = await client.db("test", auth=auth) + + # Create a new collection named "students". + students = await db.create_collection("students") + + # Add a persistent index to the collection. + await students.add_index(type="persistent", fields=["name"], options={"unique": True}) + + # Insert new documents into the collection. + await students.insert({"name": "jane", "age": 39}) + await students.insert({"name": "josh", "age": 18}) + await students.insert({"name": "judy", "age": 21}) + + # Execute an AQL query and iterate through the result cursor. + cursor = await db.aql.execute("FOR doc IN students RETURN doc") + async with cursor: + student_names = [] + async for doc in cursor: + student_names.append(doc["name"]) + +``` + +Please see the [documentation](https://python-arango-async.readthedocs.io/en/latest/) for more details. diff --git a/arangoasync/__init__.py b/arangoasync/__init__.py index 5cc9189..f52e4cc 100644 --- a/arangoasync/__init__.py +++ b/arangoasync/__init__.py @@ -1,5 +1,5 @@ -import logging +import arangoasync.errno as errno # noqa: F401 +from arangoasync.client import ArangoClient # noqa: F401 +from arangoasync.exceptions import * # noqa: F401 F403 from .version import __version__ - -logger = logging.getLogger(__name__) diff --git a/arangoasync/connection.py b/arangoasync/connection.py index 27ab9df..cac1b01 100644 --- a/arangoasync/connection.py +++ b/arangoasync/connection.py @@ -11,9 +11,9 @@ from jwt import ExpiredSignatureError -from arangoasync import errno, logger from arangoasync.auth import Auth, JwtToken from arangoasync.compression import CompressionManager +from arangoasync.errno import HTTP_UNAUTHORIZED from arangoasync.exceptions import ( AuthHeaderError, ClientConnectionAbortedError, @@ -24,6 +24,7 @@ ServerConnectionError, ) from arangoasync.http import HTTPClient +from arangoasync.logger import logger from arangoasync.request import Method, Request from arangoasync.resolver import HostResolver from arangoasync.response import Response @@ -417,7 +418,7 @@ async def send_request(self, request: Request) -> Response: resp = await self.process_request(request) if ( - resp.status_code == errno.HTTP_UNAUTHORIZED + resp.status_code == HTTP_UNAUTHORIZED and self._token is not None and self._token.needs_refresh(self._expire_leeway) ): diff --git a/arangoasync/logger.py b/arangoasync/logger.py new file mode 100644 index 0000000..3af51c2 --- /dev/null +++ b/arangoasync/logger.py @@ -0,0 +1,3 @@ +import logging + +logger = logging.getLogger("arangoasync") diff --git a/docs/aql.rst b/docs/aql.rst new file mode 100644 index 0000000..914c982 --- /dev/null +++ b/docs/aql.rst @@ -0,0 +1,10 @@ +AQL +---- + +**ArangoDB Query Language (AQL)** is used to read and write data. It is similar +to SQL for relational databases, but without the support for data definition +operations such as creating or deleting :doc:`databases `, +:doc:`collections ` or :doc:`indexes `. For more +information, refer to `ArangoDB manual`_. + +.. _ArangoDB manual: https://docs.arangodb.com diff --git a/docs/async.rst b/docs/async.rst new file mode 100644 index 0000000..a47b131 --- /dev/null +++ b/docs/async.rst @@ -0,0 +1,6 @@ +Async API Execution +------------------- + +In **asynchronous API executions**, python-arango-async sends API requests to ArangoDB in +fire-and-forget style. The server processes the requests in the background, and +the results can be retrieved once available via `AsyncJob` objects. diff --git a/docs/collection.rst b/docs/collection.rst new file mode 100644 index 0000000..42487f6 --- /dev/null +++ b/docs/collection.rst @@ -0,0 +1,42 @@ +Collections +----------- + +A **collection** contains :doc:`documents `. It is uniquely identified +by its name which must consist only of hyphen, underscore and alphanumeric +characters. + +Here is an example showing how you can manage standard collections: + +.. code-block:: python + + from arangoasync import ArangoClient + from arangoasync.auth import Auth + + # Initialize the client for ArangoDB. + async with ArangoClient(hosts="http://localhost:8529") as client: + auth = Auth(username="root", password="passwd") + + # Connect to "test" database as root user. + db = await client.db("test", auth=auth) + + # List all collections in the database. + await db.collections() + + # Create a new collection named "students" if it does not exist. + # This returns an API wrapper for "students" collection. + if await db.has_collection("students"): + students = db.collection("students") + else: + students = await db.create_collection("students") + + # Retrieve collection properties. + name = students.name + db_name = students.db_name + properties = await students.properties() + count = await students.count() + + # Perform various operations. + await students.truncate() + + # Delete the collection. + await db.delete_collection("students") diff --git a/docs/database.rst b/docs/database.rst new file mode 100644 index 0000000..f510cb2 --- /dev/null +++ b/docs/database.rst @@ -0,0 +1,61 @@ +Databases +--------- + +ArangoDB server can have an arbitrary number of **databases**. Each database +has its own set of :doc:`collections ` and graphs. +There is a special database named ``_system``, which cannot be dropped and +provides operations for managing users, permissions and other databases. Most +of the operations can only be executed by admin users. See :doc:`user` for more +information. + +**Example:** + +.. code-block:: python + + from arangoasync import ArangoClient + from arangoasync.auth import Auth + + # Initialize the client for ArangoDB. + async with ArangoClient(hosts="http://localhost:8529") as client: + auth = Auth(username="root", password="passwd") + + # Connect to "_system" database as root user. + sys_db = await client.db("_system", auth=auth) + + # List all databases. + await sys_db.databases() + + # Create a new database named "test" if it does not exist. + # Only root user has access to it at time of its creation. + if not await sys_db.has_database("test"): + await sys_db.create_database("test") + + # Delete the database. + await sys_db.delete_database("test") + + # Create a new database named "test" along with a new set of users. + # Only "jane", "john", "jake" and root user have access to it. + if not await sys_db.has_database("test"): + await sys_db.create_database( + name="test", + users=[ + {"username": "jane", "password": "foo", "active": True}, + {"username": "john", "password": "bar", "active": True}, + {"username": "jake", "password": "baz", "active": True}, + ], + ) + + # Connect to the new "test" database as user "jane". + db = await client.db("test", auth=Auth("jane", "foo")) + + # Make sure that user "jane" has read and write permissions. + await sys_db.update_permission(username="jane", permission="rw", database="test") + + # Retrieve various database and server information. + name = db.name + version = await db.version() + status = await db.status() + collections = await db.collections() + + # Delete the database. Note that the new users will remain. + await sys_db.delete_database("test") diff --git a/docs/document.rst b/docs/document.rst new file mode 100644 index 0000000..3398bf9 --- /dev/null +++ b/docs/document.rst @@ -0,0 +1,131 @@ +Documents +--------- + +In python-arango-async, a **document** is an object with the following +properties: + +* Is JSON serializable. +* May be nested to an arbitrary depth. +* May contain lists. +* Contains the ``_key`` field, which identifies the document uniquely within a + specific collection. +* Contains the ``_id`` field (also called the *handle*), which identifies the + document uniquely across all collections within a database. This ID is a + combination of the collection name and the document key using the format + ``{collection}/{key}`` (see example below). +* Contains the ``_rev`` field. ArangoDB supports MVCC (Multiple Version + Concurrency Control) and is capable of storing each document in multiple + revisions. Latest revision of a document is indicated by this field. The + field is populated by ArangoDB and is not required as input unless you want + to validate a document against its current revision. + +For more information on documents and associated terminologies, refer to +`ArangoDB manual`_. Here is an example of a valid document in "students" +collection: + +.. _ArangoDB manual: https://docs.arangodb.com + +.. testcode:: + + { + '_id': 'students/bruce', + '_key': 'bruce', + '_rev': '_Wm3dzEi--_', + 'first_name': 'Bruce', + 'last_name': 'Wayne', + 'address': { + 'street' : '1007 Mountain Dr.', + 'city': 'Gotham', + 'state': 'NJ' + }, + 'is_rich': True, + 'friends': ['robin', 'gordon'] + } + +Standard documents are managed via collection API wrapper: + +.. code-block:: python + + from arangoasync import ArangoClient + from arangoasync.auth import Auth + + # Initialize the client for ArangoDB. + async with ArangoClient(hosts="http://localhost:8529") as client: + auth = Auth(username="root", password="passwd") + + # Connect to "test" database as root user. + db = await client.db("test", auth=auth) + + # Get the API wrapper for "students" collection. + students = db.collection("students") + + # Create some test documents to play around with. + lola = {"_key": "lola", "GPA": 3.5, "first": "Lola", "last": "Martin"} + abby = {"_key": "abby", "GPA": 3.2, "first": "Abby", "last": "Page"} + john = {"_key": "john", "GPA": 3.6, "first": "John", "last": "Kim"} + emma = {"_key": "emma", "GPA": 4.0, "first": "Emma", "last": "Park"} + + # Insert a new document. This returns the document metadata. + metadata = await students.insert(lola) + assert metadata["_id"] == "students/lola" + assert metadata["_key"] == "lola" + + # Insert multiple documents. + await students.insert_many([abby, john, emma]) + + # Check if documents exist in the collection. + assert await students.has("lola") + + # Retrieve the total document count. + count = await students.count() + + # Retrieve one or more matching documents. + async for student in await students.find({"first": "John"}): + assert student["_key"] == "john" + assert student["GPA"] == 3.6 + assert student["last"] == "Kim" + + # Retrieve one or more matching documents, sorted by a field. + async for student in await students.find({"first": "John"}, sort=[{"sort_by": "GPA", "sort_order": "DESC"}]): + assert student["_key"] == "john" + assert student["GPA"] == 3.6 + assert student["last"] == "Kim" + + # Retrieve a document by key. + await students.get("john") + + # Retrieve a document by ID. + await students.get("students/john") + + # Retrieve a document by body with "_id" field. + await students.get({"_id": "students/john"}) + + # Retrieve a document by body with "_key" field. + await students.get({"_key": "john"}) + + # Retrieve multiple documents by ID, key or body. + await students.get_many(["abby", "students/lola", {"_key": "john"}]) + + # Update a single document. + lola["GPA"] = 2.6 + await students.update(lola) + + # Update one or more matching documents. + await students.update_match({"last": "Park"}, {"GPA": 3.0}) + + # Replace a single document. + emma["GPA"] = 3.1 + await students.replace(emma) + + # Replace one or more matching documents. + becky = {"first": "Becky", "last": "Solis", "GPA": "3.3"} + await students.replace_match({"first": "Emma"}, becky) + + # Delete a document by body with "_id" or "_key" field. + await students.delete(emma) + + # Delete multiple documents. Missing ones are ignored. + await students.delete_many([abby, emma]) + + # Delete one or more matching documents. + await students.delete_match({"first": "Emma"}) diff --git a/docs/errno.rst b/docs/errno.rst new file mode 100644 index 0000000..f4ee457 --- /dev/null +++ b/docs/errno.rst @@ -0,0 +1,19 @@ +Error Codes +----------- + +Python-Arango-Async provides ArangoDB error code constants for convenience. + +**Example** + +.. testcode:: + + from arangoasync import errno + + # Some examples + assert errno.NOT_IMPLEMENTED == 9 + assert errno.DOCUMENT_REV_BAD == 1239 + assert errno.DOCUMENT_NOT_FOUND == 1202 + +For more information, refer to `ArangoDB manual`_. + +.. _ArangoDB manual: https://www.arangodb.com/docs/stable/appendix-error-codes.html diff --git a/docs/errors.rst b/docs/errors.rst new file mode 100644 index 0000000..cba6d92 --- /dev/null +++ b/docs/errors.rst @@ -0,0 +1,20 @@ +Error Handling +-------------- + +All python-arango exceptions inherit :class:`arangoasync.exceptions.ArangoError`, +which splits into subclasses :class:`arangoasync.exceptions.ArangoServerError` and +:class:`arangoasync.exceptions.ArangoClientError`. + +Server Errors +============= + +:class:`arangoasync.exceptions.ArangoServerError` exceptions lightly wrap non-2xx +HTTP responses coming from ArangoDB. Each exception object contains the error +message, error code and HTTP request response details. + +Client Errors +============= + +:class:`arangoasync.exceptions.ArangoClientError` exceptions originate from +python-arango client itself. They do not contain error codes nor HTTP request +response details. diff --git a/docs/index.rst b/docs/index.rst index 2419da8..9e71989 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,9 +1,70 @@ +.. image:: /static/logo.png + +| + python-arango-async ------------------- Welcome to the documentation for **python-arango-async**, a Python driver for ArangoDB_. -**The driver is currently work in progress and not yet ready for use.** +**Note: This project is still in active development, features might be added or removed.** + +Requirements +============= + +- ArangoDB version 3.11+ +- Python version 3.9+ + +Installation +============ + +.. code-block:: bash + + ~$ pip install python-arango-async --upgrade + +Contents +======== + +Basics + +.. toctree:: + :maxdepth: 1 + + overview + database + collection + indexes + document + aql + +Specialized Features + +.. toctree:: + :maxdepth: 1 + + transaction + +API Executions + +.. toctree:: + :maxdepth: 1 + + async + +Administration + +.. toctree:: + :maxdepth: 1 + + user + +Miscellaneous + +.. toctree:: + :maxdepth: 1 + + errors + errno Development diff --git a/docs/indexes.rst b/docs/indexes.rst new file mode 100644 index 0000000..e8ae208 --- /dev/null +++ b/docs/indexes.rst @@ -0,0 +1,51 @@ +Indexes +------- + +**Indexes** can be added to collections to speed up document lookups. Every +collection has a primary hash index on ``_key`` field by default. This index +cannot be deleted or modified. Every edge collection has additional indexes +on fields ``_from`` and ``_to``. For more information on indexes, refer to +`ArangoDB manual`_. + +.. _ArangoDB manual: https://docs.arangodb.com + +**Example:** + +.. code-block:: python + + from arangoasync import ArangoClient + from arangoasync.auth import Auth + + # Initialize the client for ArangoDB. + async with ArangoClient(hosts="http://localhost:8529") as client: + auth = Auth(username="root", password="passwd") + + # Connect to "test" database as root user. + db = await client.db("test", auth=auth) + + # Create a new collection named "cities". + cities = await db.create_collection("cities") + + # List the indexes in the collection. + indexes = await cities.indexes() + + # Add a new persistent index on document fields "continent" and "country". + persistent_index = {"type": "persistent", "fields": ["continent", "country"], "unique": True} + persistent_index = await cities.add_index( + type="persistent", + fields=['continent', 'country'], + options={"unique": True} + ) + + # Add new fulltext indexes on fields "continent" and "country". + index = await cities.add_index(type="fulltext", fields=["continent"]) + index = await cities.add_index(type="fulltext", fields=["country"]) + + # Add a new geo-spatial index on field 'coordinates'. + index = await cities.add_index(type="geo", fields=["coordinates"]) + + # Add a new TTL (time-to-live) index on field 'currency'. + index = await cities.add_index(type="ttl", fields=["currency"], options={"expireAfter": 200}) + + # Delete the last index from the collection. + await cities.delete_index(index["id"]) diff --git a/docs/overview.rst b/docs/overview.rst new file mode 100644 index 0000000..ce3f45a --- /dev/null +++ b/docs/overview.rst @@ -0,0 +1,40 @@ +Getting Started +--------------- + +Here is an example showing how **python-arango-async** client can be used: + +.. code-block:: python + + from arangoasync import ArangoClient + from arangoasync.auth import Auth + + # Initialize the client for ArangoDB. + async with ArangoClient(hosts="http://localhost:8529") as client: + auth = Auth(username="root", password="passwd") + + # Connect to "_system" database as root user. + sys_db = await client.db("_system", auth=auth) + + # Create a new database named "test". + await sys_db.create_database("test") + + # Connect to "test" database as root user. + db = await client.db("test", auth=auth) + + # Create a new collection named "students". + students = await db.create_collection("students") + + # Add a persistent index to the collection. + await students.add_index(type="persistent", fields=["name"], options={"unique": True}) + + # Insert new documents into the collection. + await students.insert({"name": "jane", "age": 39}) + await students.insert({"name": "josh", "age": 18}) + await students.insert({"name": "judy", "age": 21}) + + # Execute an AQL query and iterate through the result cursor. + cursor = await db.aql.execute("FOR doc IN students RETURN doc") + async with cursor: + student_names = [] + async for doc in cursor: + student_names.append(doc["name"]) diff --git a/docs/static/logo.png b/docs/static/logo.png new file mode 100644 index 0000000..64845fb Binary files /dev/null and b/docs/static/logo.png differ diff --git a/docs/transaction.rst b/docs/transaction.rst new file mode 100644 index 0000000..225e226 --- /dev/null +++ b/docs/transaction.rst @@ -0,0 +1,5 @@ +Transactions +------------ + +In **transactions**, requests to ArangoDB server are committed as a single, +logical unit of work (ACID compliant). diff --git a/docs/user.rst b/docs/user.rst new file mode 100644 index 0000000..015858c --- /dev/null +++ b/docs/user.rst @@ -0,0 +1,5 @@ +Users and Permissions +--------------------- + +Python-arango provides operations for managing users and permissions. Most of +these operations can only be performed by admin users via ``_system`` database. diff --git a/pyproject.toml b/pyproject.toml index 71cedb1..d5003c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,11 +41,11 @@ dependencies = [ "setuptools>=42", "aiohttp>=3.9", "multidict>=6.0", - "PyJWT>=2.8.0", + "pyjwt>=2.8.0", ] [tool.setuptools.dynamic] -version = { attr = "arangoasync.__version__" } +version = { attr = "arangoasync.version.__version__" } [project.optional-dependencies] dev = [