diff --git a/arangoasync/collection.py b/arangoasync/collection.py index 5b9563b..ae36fa8 100644 --- a/arangoasync/collection.py +++ b/arangoasync/collection.py @@ -1,7 +1,6 @@ -__all__ = ["Collection", "CollectionType", "StandardCollection"] +__all__ = ["Collection", "StandardCollection"] -from enum import Enum from typing import Generic, Optional, Tuple, TypeVar from arangoasync.errno import HTTP_NOT_FOUND, HTTP_PRECONDITION_FAILED @@ -21,13 +20,6 @@ V = TypeVar("V") -class CollectionType(Enum): - """Collection types.""" - - DOCUMENT = 2 - EDGE = 3 - - class Collection(Generic[T, U, V]): """Base class for collection API wrappers. diff --git a/arangoasync/database.py b/arangoasync/database.py index eb7f74e..2628537 100644 --- a/arangoasync/database.py +++ b/arangoasync/database.py @@ -4,9 +4,9 @@ ] -from typing import Optional, Sequence, TypeVar, cast +from typing import List, Optional, Sequence, TypeVar, cast -from arangoasync.collection import CollectionType, StandardCollection +from arangoasync.collection import StandardCollection from arangoasync.connection import Connection from arangoasync.errno import HTTP_FORBIDDEN, HTTP_NOT_FOUND from arangoasync.exceptions import ( @@ -22,8 +22,17 @@ from arangoasync.request import Method, Request from arangoasync.response import Response from arangoasync.serialization import Deserializer, Serializer -from arangoasync.typings import Json, Jsons, Params, Result -from arangoasync.wrapper import KeyOptions, ServerStatusInformation, User +from arangoasync.typings import ( + CollectionInfo, + CollectionType, + Json, + Jsons, + KeyOptions, + Params, + Result, + ServerStatusInformation, + UserInfo, +) T = TypeVar("T") U = TypeVar("U") @@ -69,7 +78,10 @@ async def status(self) -> Result[ServerStatusInformation]: Raises: ServerSatusError: If retrieval fails. - """ + + References: + - `get-server-status-information `__ + """ # noqa: E501 request = Request(method=Method.GET, endpoint="/_admin/status") def response_handler(resp: Response) -> ServerStatusInformation: @@ -79,9 +91,40 @@ def response_handler(resp: Response) -> ServerStatusInformation: return await self._executor.execute(request, response_handler) + async def databases(self) -> Result[List[str]]: + """Return the names of all databases. + + Note: + This method can only be executed in the **_system** database. + + Returns: + list: Database names. + + Raises: + DatabaseListError: If retrieval fails. + + References: + - `list-all-databases `__ + """ # noqa: E501 + request = Request(method=Method.GET, endpoint="/_api/database") + + def response_handler(resp: Response) -> List[str]: + if resp.is_success: + body = self.deserializer.loads(resp.raw_body) + return cast(List[str], body["result"]) + msg: Optional[str] = None + if resp.status_code == HTTP_FORBIDDEN: + msg = "This request can only be executed in the _system database." + raise DatabaseListError(resp, request, msg) + + return await self._executor.execute(request, response_handler) + async def has_database(self, name: str) -> Result[bool]: """Check if a database exists. + Note: + This method can only be executed from within the **_system** database. + Args: name (str): Database name. @@ -89,33 +132,39 @@ async def has_database(self, name: str) -> Result[bool]: bool: `True` if the database exists, `False` otherwise. Raises: - DatabaseListError: If failed to retrieve the list of databases. + DatabaseListError: If retrieval fails. """ request = Request(method=Method.GET, endpoint="/_api/database") def response_handler(resp: Response) -> bool: - if not resp.is_success: - raise DatabaseListError(resp, request) - body = self.deserializer.loads(resp.raw_body) - return name in body["result"] + if resp.is_success: + body = self.deserializer.loads(resp.raw_body) + return name in body["result"] + msg: Optional[str] = None + if resp.status_code == HTTP_FORBIDDEN: + msg = "This request can only be executed in the _system database." + raise DatabaseListError(resp, request, msg) return await self._executor.execute(request, response_handler) async def create_database( self, name: str, - users: Optional[Sequence[Json | User]] = None, + users: Optional[Sequence[Json | UserInfo]] = None, replication_factor: Optional[int | str] = None, write_concern: Optional[int] = None, sharding: Optional[bool] = None, ) -> Result[bool]: """Create a new database. + Note: + This method can only be executed from within the **_system** database. + Args: name (str): Database name. users (list | None): Optional list of users with access to the new database, where each user is of :class:`User - ` type, or a dictionary with fields + ` type, or a dictionary with fields "username", "password" and "active". If not set, the default user **root** will be used to ensure that the new database will be accessible after it is created. @@ -125,12 +174,12 @@ async def create_database( (Enterprise Edition only), and 1, which disables replication. Used for clusters only. write_concern (int | None): Default write concern for collections created - in this database. Determines how many copies of each shard are required - to be in sync on different DB-Servers. If there are less than these many - copies in the cluster a shard will refuse to write. Writes to shards with - enough up-to-date copies will succeed at the same time, however. Value of - this parameter can not be larger than the value of **replication_factor**. - Used for clusters only. + in this database. Determines how many copies of each shard are required + to be in sync on different DB-Servers. If there are less than these many + copies in the cluster a shard will refuse to write. Writes to shards with + enough up-to-date copies will succeed at the same time, however. Value of + this parameter can not be larger than the value of **replication_factor**. + Used for clusters only. sharding (str | None): Sharding method used for new collections in this database. Allowed values are: "", "flexible" and "single". The first two are equivalent. Used for clusters only. @@ -140,7 +189,10 @@ async def create_database( Raises: DatabaseCreateError: If creation fails. - """ + + References: + - `create-a-database `__ + """ # noqa: E501 data: Json = {"name": name} options: Json = {} @@ -173,7 +225,10 @@ async def create_database( def response_handler(resp: Response) -> bool: if resp.is_success: return True - raise DatabaseCreateError(resp, request) + msg: Optional[str] = None + if resp.status_code == HTTP_FORBIDDEN: + msg = "This request can only be executed in the _system database." + raise DatabaseCreateError(resp, request, msg) return await self._executor.execute(request, response_handler) @@ -182,6 +237,9 @@ async def delete_database( ) -> Result[bool]: """Delete a database. + Note: + This method can only be executed from within the **_system** database. + Args: name (str): Database name. ignore_missing (bool): Do not raise an exception on missing database. @@ -192,7 +250,10 @@ async def delete_database( Raises: DatabaseDeleteError: If deletion fails. - """ + + References: + - `drop-a-database `__ + """ # noqa: E501 request = Request(method=Method.DELETE, endpoint=f"/_api/database/{name}") def response_handler(resp: Response) -> bool: @@ -200,13 +261,10 @@ def response_handler(resp: Response) -> bool: return True if resp.status_code == HTTP_NOT_FOUND and ignore_missing: return False + msg: Optional[str] = None if resp.status_code == HTTP_FORBIDDEN: - raise DatabaseDeleteError( - resp, - request, - "This request can only be executed in the _system database.", - ) - raise DatabaseDeleteError(resp, request) + msg = "This request can only be executed in the _system database." + raise DatabaseDeleteError(resp, request, msg) return await self._executor.execute(request, response_handler) @@ -241,6 +299,40 @@ def collection( self._executor, name, serializer, deserializer ) + async def collections( + self, + exclude_system: Optional[bool] = None, + ) -> Result[List[CollectionInfo]]: + """Returns basic information for all collections in the current database, + optionally excluding system collections. + + Returns: + list: Collection names. + + Raises: + CollectionListError: If retrieval fails. + + References: + - `list-all-collections `__ + """ # noqa: E501 + params: Params = {} + if exclude_system is not None: + params["excludeSystem"] = exclude_system + + request = Request( + method=Method.GET, + endpoint="/_api/collection", + params=params, + ) + + def response_handler(resp: Response) -> List[CollectionInfo]: + if not resp.is_success: + raise CollectionListError(resp, request) + body = self.deserializer.loads(resp.raw_body) + return [CollectionInfo(c) for c in body["result"]] + + return await self._executor.execute(request, response_handler) + async def has_collection(self, name: str) -> Result[bool]: """Check if a collection exists in the database. @@ -249,14 +341,18 @@ async def has_collection(self, name: str) -> Result[bool]: Returns: bool: True if the collection exists, False otherwise. + + Raises: + CollectionListError: If retrieval fails. """ - request = Request(method=Method.GET, endpoint="/_api/collection") + request = Request(method=Method.GET, endpoint=f"/_api/collection/{name}") def response_handler(resp: Response) -> bool: - if not resp.is_success: - raise CollectionListError(resp, request) - body = self.deserializer.loads(resp.raw_body) - return any(c["name"] == name for c in body["result"]) + if resp.is_success: + return True + if resp.status_code == HTTP_NOT_FOUND: + return False + raise CollectionListError(resp, request) return await self._executor.execute(request, response_handler) @@ -343,7 +439,10 @@ async def create_collection( Raises: ValueError: If parameters are invalid. CollectionCreateError: If the operation fails. - """ + + References: + - `create-a-collection `__ + """ # noqa: E501 data: Json = {"name": name} if col_type is not None: data["type"] = col_type.value @@ -430,7 +529,10 @@ async def delete_collection( Raises: CollectionDeleteError: If the operation fails. - """ + + References: + - `drop-a-collection `__ + """ # noqa: E501 params: Params = {} if is_system is not None: params["isSystem"] = is_system diff --git a/arangoasync/typings.py b/arangoasync/typings.py index b75defc..653c2de 100644 --- a/arangoasync/typings.py +++ b/arangoasync/typings.py @@ -1,13 +1,17 @@ -__all__ = [ - "Json", - "Jsons", - "RequestHeaders", - "ResponseHeaders", - "Params", - "Result", -] - -from typing import Any, Dict, List, MutableMapping, TypeVar, Union +from enum import Enum +from typing import ( + Any, + Callable, + Dict, + Iterator, + List, + MutableMapping, + Optional, + Tuple, + TypeVar, + Union, + cast, +) from multidict import CIMultiDictProxy, MultiDict @@ -28,5 +32,440 @@ Params = MutableMapping[str, bool | int | str] Params.__doc__ = """Type definition for URL (query) parameters""" +Formatter = Callable[[Json], Json] +Formatter.__doc__ = """Type definition for a JSON formatter""" + T = TypeVar("T") Result = Union[T, AsyncJob[T]] + + +class CollectionType(Enum): + """Collection types.""" + + DOCUMENT = 2 + EDGE = 3 + + @staticmethod + def from_int(value: int) -> "CollectionType": + """Return a collection type from its integer value. + + Args: + value (int): Collection type integer value. + + Returns: + CollectionType: Collection type. + """ + if value == 2: + return CollectionType.DOCUMENT + elif value == 3: + return CollectionType.EDGE + else: + raise ValueError(f"Invalid collection type value: {value}") + + def __str__(self) -> str: + return self.name.lower() + + def __repr__(self) -> str: + return self.name.lower() + + +class CollectionStatus(Enum): + """Collection status.""" + + NEW = 1 + UNLOADED = 2 + LOADED = 3 + UNLOADING = 4 + DELETED = 5 + LOADING = 6 + + @staticmethod + def from_int(value: int) -> "CollectionStatus": + """Return a collection status from its integer value. + + Args: + value (int): Collection status integer value. + + Returns: + CollectionStatus: Collection status. + """ + if value == 1: + return CollectionStatus.NEW + elif value == 2: + return CollectionStatus.UNLOADED + elif value == 3: + return CollectionStatus.LOADED + elif value == 4: + return CollectionStatus.UNLOADING + elif value == 5: + return CollectionStatus.DELETED + elif value == 6: + return CollectionStatus.LOADING + else: + raise ValueError(f"Invalid collection status value: {value}") + + def __str__(self) -> str: + return self.name.lower() + + def __repr__(self) -> str: + return self.name.lower() + + +class JsonWrapper: + """Wrapper over server request/response objects.""" + + def __init__(self, data: Json) -> None: + self._data = data + + def __getitem__(self, key: str) -> Any: + return self._data[key] + + def __setitem__(self, key: str, value: Any) -> None: + self._data[key] = value + + def __delitem__(self, key: str) -> None: + del self._data[key] + + def __iter__(self) -> Iterator[str]: + return iter(self._data) + + def __len__(self) -> int: + return len(self._data) + + def __contains__(self, item: str) -> bool: + return item in self._data + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self._data})" + + def __str__(self) -> str: + return str(self._data) + + def __eq__(self, other: object) -> bool: + return self._data == other + + def get(self, key: str, default: Optional[Any] = None) -> Any: + """Return the value for key if key is in the dictionary, else default.""" + return self._data.get(key, default) + + def items(self) -> Iterator[Tuple[str, Any]]: + """Return an iterator over the dictionary’s key-value pairs.""" + return iter(self._data.items()) + + def to_dict(self) -> Json: + """Return the dictionary.""" + return self._data + + def format(self, formatter: Optional[Formatter] = None) -> Json: + """Apply a formatter to the data. Returns the unmodified data by default.""" + if formatter is not None: + return formatter(self._data) + return self._data + + +class KeyOptions(JsonWrapper): + """Additional options for key generation, used on collections. + + Args: + allow_user_keys (bool): If set to `True`, then you are allowed to supply own + key values in the `_key` attribute of documents. If set to `False`, then + the key generator is solely responsible for generating keys and an error + is raised if you supply own key values in the `_key` attribute of + documents. + generator_type (str): Specifies the type of the key generator. The currently + available generators are "traditional", "autoincrement", "uuid" and + "padded". + increment (int | None): The increment value for the "autoincrement" key + generator. Not allowed for other key generator types. + offset (int | None): The initial offset value for the "autoincrement" key + generator. Not allowed for other key generator types. + data (dict | None): Key options. If this parameter is specified, the + other parameters are ignored. + + Example: + .. code-block:: json + + { + "type": "autoincrement", + "increment": 5, + "allowUserKeys": true + } + + References: + - `create-a-collection `__ + """ # noqa: E501 + + def __init__( + self, + allow_user_keys: bool = True, + generator_type: str = "traditional", + increment: Optional[int] = None, + offset: Optional[int] = None, + data: Optional[Json] = None, + ) -> None: + if data is None: + data = { + "allowUserKeys": allow_user_keys, + "type": generator_type, + } + if increment is not None: + data["increment"] = increment + if offset is not None: + data["offset"] = offset + super().__init__(data) + + def validate(self) -> None: + """Validate key options.""" + if "type" not in self: + raise ValueError('"type" value is required for key options') + if "allowUserKeys" not in self: + raise ValueError('"allowUserKeys" value is required for key options') + + allowed_types = {"autoincrement", "uuid", "padded", "traditional"} + if self["type"] not in allowed_types: + raise ValueError( + f"Invalid key generator type '{self['type']}', " + f"expected one of {allowed_types}" + ) + + if self.get("increment") is not None and self["type"] != "autoincrement": + raise ValueError( + '"increment" value is only allowed for "autoincrement" ' "key generator" + ) + if self.get("offset") is not None and self["type"] != "autoincrement": + raise ValueError( + '"offset" value is only allowed for "autoincrement" ' "key generator" + ) + + +class CollectionInfo(JsonWrapper): + """Collection information. + + Example: + .. code-block:: json + + { + "id" : "151", + "name" : "animals", + "status" : 3, + "type" : 2, + "isSystem" : false, + "globallyUniqueId" : "hDA74058C1843/151" + } + + References: + - `get-the-collection-information `__ + """ # noqa: E501 + + def __init__(self, data: Json) -> None: + super().__init__(data) + + @property + def globally_unique_id(self) -> str: + """ "A unique identifier of the collection (internal property).""" + return cast(str, self._data["globallyUniqueId"]) + + @property + def is_system(self) -> bool: + """Whether the collection is a system collection.""" + return cast(bool, self._data["isSystem"]) + + @property + def name(self) -> str: + """Return the name of the collection.""" + return cast(str, self._data["name"]) + + @property + def status(self) -> CollectionStatus: + """Return the status of the collection.""" + return CollectionStatus.from_int(self._data["status"]) + + @property + def col_type(self) -> CollectionType: + """Return the type of the collection.""" + return CollectionType.from_int(self._data["type"]) + + def format(self, formatter: Optional[Formatter] = None) -> Json: + """Apply a formatter to the data. + + By default, the python-arango compatibility formatter is applied. + """ + if formatter is not None: + return super().format(formatter) + return { + "id": self._data["id"], + "name": self.name, + "system": self.is_system, + "type": str(self.col_type), + "status": str(self.status), + } + + +class UserInfo(JsonWrapper): + """User information. + + Args: + user (str): The name of the user. + password (str | None): The user password as a string. Note that user + password is not returned back by the server. + active (bool): `True` if user is active, `False` otherwise. + extra (dict | None): Additional user information. For internal use only. + Should not be set or modified by end users. + + Example: + .. code-block:: json + + { + "user": "john", + "password": "secret", + "active": true, + "extra": {} + } + + References: + - `create-a-user `__ + """ # noqa: E501 + + def __init__( + self, + user: str, + password: Optional[str] = None, + active: bool = True, + extra: Optional[Json] = None, + ) -> None: + # There is a small inconsistency between _api/user and _api/database. + # The former uses "user" and the latter uses "username" for the username. + data = {"user": user, "username": user, "active": active} + if password is not None: + data["password"] = password + if extra is not None: + data["extra"] = extra + super().__init__(data) + + @property + def user(self) -> str: + return self._data.get("user") # type: ignore[return-value] + + @property + def password(self) -> Optional[str]: + return self._data.get("password") + + @property + def active(self) -> bool: + return self._data.get("active") # type: ignore[return-value] + + @property + def extra(self) -> Optional[Json]: + return self._data.get("extra") + + def to_dict(self) -> Json: + """Return the dictionary.""" + return dict( + user=self.user, + password=self.password, + active=self.active, + extra=self.extra, + ) + + +class ServerStatusInformation(JsonWrapper): + """Status information about the server. + + Example: + .. code-block:: json + + { + "server" : "arango", + "version" : "3.12.2", + "pid" : 244, + "license" : "enterprise", + "mode" : "server", + "operationMode" : "server", + "foxxApi" : true, + "host" : "localhost", + "hostname" : "ebd1509c9185", + "serverInfo" : { + "progress" : { + "phase" : "in wait", + "feature" : "", + "recoveryTick" : 0 + }, + "maintenance" : false, + "role" : "COORDINATOR", + "writeOpsEnabled" : true, + "readOnly" : false, + "persistedId" : "CRDN-329cfc20-071f-4faf-9727-7e48a7aed1e5", + "rebootId" : 1, + "address" : "tcp://localhost:8529", + "serverId" : "CRDN-329cfc20-071f-4faf-9727-7e48a7aed1e5", + "state" : "SERVING" + }, + "coordinator" : { + "foxxmaster" : "CRDN-0ed76822-3e64-47ed-a61b-510f2a696175", + "isFoxxmaster" : false + }, + "agency" : { + "agencyComm" : { + "endpoints" : [ + "tcp://localhost:8551", + "tcp://localhost:8541", + "tcp://localhost:8531" + ] + } + } + } + + References: + - `get-server-status-information `__ + """ # noqa: E501 + + def __init__(self, data: Json) -> None: + super().__init__(data) + + @property + def server(self) -> Optional[str]: + return self._data.get("server") + + @property + def version(self) -> Optional[str]: + return self._data.get("version") + + @property + def pid(self) -> Optional[int]: + return self._data.get("pid") + + @property + def license(self) -> Optional[str]: + return self._data.get("license") + + @property + def mode(self) -> Optional[str]: + return self._data.get("mode") + + @property + def operation_mode(self) -> Optional[str]: + return self._data.get("operationMode") + + @property + def foxx_api(self) -> Optional[bool]: + return self._data.get("foxxApi") + + @property + def host(self) -> Optional[str]: + return self._data.get("host") + + @property + def hostname(self) -> Optional[str]: + return self._data.get("hostname") + + @property + def server_info(self) -> Optional[Json]: + return self._data.get("serverInfo") + + @property + def coordinator(self) -> Optional[Json]: + return self._data.get("coordinator") + + @property + def agency(self) -> Optional[Json]: + return self._data.get("agency") diff --git a/arangoasync/wrapper.py b/arangoasync/wrapper.py deleted file mode 100644 index a8cb0b1..0000000 --- a/arangoasync/wrapper.py +++ /dev/null @@ -1,280 +0,0 @@ -from typing import Any, Iterator, Optional, Tuple - -from arangoasync.typings import Json - - -class JsonWrapper: - """Wrapper over server request/response objects.""" - - def __init__(self, data: Json) -> None: - self._data = data - - def __getitem__(self, key: str) -> Any: - return self._data[key] - - def __setitem__(self, key: str, value: Any) -> None: - self._data[key] = value - - def __delitem__(self, key: str) -> None: - del self._data[key] - - def __iter__(self) -> Iterator[str]: - return iter(self._data) - - def __len__(self) -> int: - return len(self._data) - - def __contains__(self, item: str) -> bool: - return item in self._data - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self._data})" - - def __str__(self) -> str: - return str(self._data) - - def __eq__(self, other: object) -> bool: - return self._data == other - - def get(self, key: str, default: Optional[Any] = None) -> Any: - """Return the value for key if key is in the dictionary, else default.""" - return self._data.get(key, default) - - def items(self) -> Iterator[Tuple[str, Any]]: - """Return an iterator over the dictionary’s key-value pairs.""" - return iter(self._data.items()) - - def to_dict(self) -> Json: - """Return the dictionary.""" - return self._data - - -class KeyOptions(JsonWrapper): - """Additional options for key generation, used on collections. - - https://docs.arangodb.com/stable/develop/http-api/collections/#create-a-collection_body_keyOptions - - Args: - allow_user_keys (bool): If set to `True`, then you are allowed to supply own - key values in the `_key` attribute of documents. If set to `False`, then - the key generator is solely responsible for generating keys and an error - is raised if you supply own key values in the `_key` attribute of - documents. - generator_type (str): Specifies the type of the key generator. The currently - available generators are "traditional", "autoincrement", "uuid" and - "padded". - increment (int | None): The increment value for the "autoincrement" key - generator. Not allowed for other key generator types. - offset (int | None): The initial offset value for the "autoincrement" key - generator. Not allowed for other key generator types. - data (dict | None): Key options. If this parameter is specified, the - other parameters are ignored. - - Example: - .. code-block:: json - - { - "type": "autoincrement", - "increment": 5, - "allowUserKeys": true - } - """ - - def __init__( - self, - allow_user_keys: bool = True, - generator_type: str = "traditional", - increment: Optional[int] = None, - offset: Optional[int] = None, - data: Optional[Json] = None, - ) -> None: - if data is None: - data = { - "allowUserKeys": allow_user_keys, - "type": generator_type, - } - if increment is not None: - data["increment"] = increment - if offset is not None: - data["offset"] = offset - super().__init__(data) - - def validate(self) -> None: - """Validate key options.""" - if "type" not in self: - raise ValueError('"type" value is required for key options') - if "allowUserKeys" not in self: - raise ValueError('"allowUserKeys" value is required for key options') - - allowed_types = {"autoincrement", "uuid", "padded", "traditional"} - if self["type"] not in allowed_types: - raise ValueError( - f"Invalid key generator type '{self['type']}', " - f"expected one of {allowed_types}" - ) - - if self.get("increment") is not None and self["type"] != "autoincrement": - raise ValueError( - '"increment" value is only allowed for "autoincrement" ' "key generator" - ) - if self.get("offset") is not None and self["type"] != "autoincrement": - raise ValueError( - '"offset" value is only allowed for "autoincrement" ' "key generator" - ) - - -class User(JsonWrapper): - """User information. - - https://docs.arangodb.com/stable/develop/http-api/users/#get-a-user - - Args: - username (str): The name of the user. - password (str | None): The user password as a string. Note that user - password is not returned back by the server. - active (bool): `True` if user is active, `False` otherwise. - extra (dict | None): Additional user information. For internal use only. - Should not be set or modified by end users. - - Example: - .. code-block:: json - - { - "username": "john", - "password": "secret", - "active": true, - "extra": {} - } - """ - - def __init__( - self, - username: str, - password: Optional[str] = None, - active: bool = True, - extra: Optional[Json] = None, - ) -> None: - data = {"username": username, "active": active} - if password is not None: - data["password"] = password - if extra is not None: - data["extra"] = extra - super().__init__(data) - - @property - def username(self) -> str: - return self._data.get("username") # type: ignore[return-value] - - @property - def password(self) -> Optional[str]: - return self._data.get("password") - - @property - def active(self) -> bool: - return self._data.get("active") # type: ignore[return-value] - - @property - def extra(self) -> Optional[Json]: - return self._data.get("extra") - - -class ServerStatusInformation(JsonWrapper): - """Status information about the server. - - https://docs.arangodb.com/stable/develop/http-api/administration/#get-server-status-information - - Example: - .. code-block:: json - - { - "server" : "arango", - "version" : "3.12.2", - "pid" : 244, - "license" : "enterprise", - "mode" : "server", - "operationMode" : "server", - "foxxApi" : true, - "host" : "localhost", - "hostname" : "ebd1509c9185", - "serverInfo" : { - "progress" : { - "phase" : "in wait", - "feature" : "", - "recoveryTick" : 0 - }, - "maintenance" : false, - "role" : "COORDINATOR", - "writeOpsEnabled" : true, - "readOnly" : false, - "persistedId" : "CRDN-329cfc20-071f-4faf-9727-7e48a7aed1e5", - "rebootId" : 1, - "address" : "tcp://localhost:8529", - "serverId" : "CRDN-329cfc20-071f-4faf-9727-7e48a7aed1e5", - "state" : "SERVING" - }, - "coordinator" : { - "foxxmaster" : "CRDN-0ed76822-3e64-47ed-a61b-510f2a696175", - "isFoxxmaster" : false - }, - "agency" : { - "agencyComm" : { - "endpoints" : [ - "tcp://localhost:8551", - "tcp://localhost:8541", - "tcp://localhost:8531" - ] - } - } - } - """ - - def __init__(self, data: Json) -> None: - super().__init__(data) - - @property - def server(self) -> Optional[str]: - return self._data.get("server") - - @property - def version(self) -> Optional[str]: - return self._data.get("version") - - @property - def pid(self) -> Optional[int]: - return self._data.get("pid") - - @property - def license(self) -> Optional[str]: - return self._data.get("license") - - @property - def mode(self) -> Optional[str]: - return self._data.get("mode") - - @property - def operation_mode(self) -> Optional[str]: - return self._data.get("operationMode") - - @property - def foxx_api(self) -> Optional[bool]: - return self._data.get("foxxApi") - - @property - def host(self) -> Optional[str]: - return self._data.get("host") - - @property - def hostname(self) -> Optional[str]: - return self._data.get("hostname") - - @property - def server_info(self) -> Optional[Json]: - return self._data.get("serverInfo") - - @property - def coordinator(self) -> Optional[Json]: - return self._data.get("coordinator") - - @property - def agency(self) -> Optional[Json]: - return self._data.get("agency") diff --git a/docs/specs.rst b/docs/specs.rst index 05290ad..13fbdbd 100644 --- a/docs/specs.rst +++ b/docs/specs.rst @@ -37,5 +37,5 @@ python-arango-async. .. automodule:: arangoasync.response :members: -.. automodule:: arangoasync.wrapper +.. automodule:: arangoasync.typings :members: diff --git a/tests/test_database.py b/tests/test_database.py index 319379a..671ef59 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -27,10 +27,19 @@ async def test_create_drop_database(url, sys_db_name, root, password): sys_db = await client.db( sys_db_name, auth_method="basic", auth=auth, verify=True ) + + # Create a new database db_name = generate_db_name() assert await sys_db.create_database(db_name) is True await client.db(db_name, auth_method="basic", auth=auth, verify=True) assert await sys_db.has_database(db_name) is True + + # List available databases + dbs = await sys_db.databases() + assert db_name in dbs + assert "_system" in dbs + + # Drop the newly created database assert await sys_db.delete_database(db_name) is True non_existent_db = generate_db_name() assert await sys_db.has_database(non_existent_db) is False @@ -46,10 +55,16 @@ async def test_create_drop_collection(url, sys_db_name, root, password): # TODO also handle exceptions async with ArangoClient(hosts=url) as client: db = await client.db(sys_db_name, auth_method="basic", auth=auth, verify=True) + + # Create a new collection col_name = generate_col_name() col = await db.create_collection(col_name) assert isinstance(col, StandardCollection) assert await db.has_collection(col_name) + cols = await db.collections() + assert any(c.name == col_name for c in cols) + + # Drop the newly created collection assert await db.delete_collection(col_name) is True assert not await db.has_collection(col_name) non_existent_col = generate_col_name() diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py index 8e14e36..911d446 100644 --- a/tests/test_wrapper.py +++ b/tests/test_wrapper.py @@ -1,6 +1,6 @@ import pytest -from arangoasync.wrapper import JsonWrapper, KeyOptions +from arangoasync.typings import JsonWrapper, KeyOptions def test_basic_wrapper():