diff --git a/arangoasync/database.py b/arangoasync/database.py index 998c6dd..345df74 100644 --- a/arangoasync/database.py +++ b/arangoasync/database.py @@ -14,6 +14,10 @@ from arangoasync.connection import Connection from arangoasync.errno import HTTP_FORBIDDEN, HTTP_NOT_FOUND from arangoasync.exceptions import ( + AnalyzerCreateError, + AnalyzerDeleteError, + AnalyzerGetError, + AnalyzerListError, AsyncJobClearError, AsyncJobListError, CollectionCreateError, @@ -1461,6 +1465,133 @@ def response_handler(resp: Response) -> bool: return await self._executor.execute(request, response_handler) + async def analyzers(self) -> Result[Jsons]: + """List all analyzers in the database. + + Returns: + list: List of analyzers with their properties. + + Raises: + AnalyzerListError: If the operation fails. + + References: + - `list-all-analyzers `__ + """ # noqa: E501 + request = Request(method=Method.GET, endpoint="/_api/analyzer") + + def response_handler(resp: Response) -> Jsons: + if resp.is_success: + result: Jsons = self.deserializer.loads(resp.raw_body)["result"] + return result + raise AnalyzerListError(resp, request) + + return await self._executor.execute(request, response_handler) + + async def analyzer(self, name: str) -> Result[Json]: + """Return analyzer details. + + Args: + name (str): Analyzer name. + + Returns: + dict: Analyzer properties. + + References: + - `get-an-analyzer-definition `__ + """ # noqa: E501 + request = Request(method=Method.GET, endpoint=f"/_api/analyzer/{name}") + + def response_handler(resp: Response) -> Json: + if not resp.is_success: + raise AnalyzerGetError(resp, request) + return Response.format_body(self.deserializer.loads(resp.raw_body)) + + return await self._executor.execute(request, response_handler) + + async def create_analyzer( + self, + name: str, + analyzer_type: str, + properties: Optional[Json] = None, + features: Optional[Sequence[str]] = None, + ) -> Result[Json]: + """Create an analyzer. + + Args: + name (str): Analyzer name. + analyzer_type (str): Type of the analyzer (e.g., "text", "identity"). + properties (dict | None): Properties of the analyzer. + features (list | None): The set of features to set on the Analyzer + generated fields. The default value is an empty array. Possible values: + "frequency", "norm", "position", "offset". + + Returns: + dict: Analyzer properties. + + Raises: + AnalyzerCreateError: If the operation fails. + + References: + - `create-an-analyzer `__ + """ # noqa: E501 + data: Json = {"name": name, "type": analyzer_type} + if properties is not None: + data["properties"] = properties + if features is not None: + data["features"] = features + + request = Request( + method=Method.POST, + endpoint="/_api/analyzer", + data=self.serializer.dumps(data), + ) + + def response_handler(resp: Response) -> Json: + if not resp.is_success: + raise AnalyzerCreateError(resp, request) + return self.deserializer.loads(resp.raw_body) + + return await self._executor.execute(request, response_handler) + + async def delete_analyzer( + self, name: str, force: Optional[bool] = None, ignore_missing: bool = False + ) -> Result[bool]: + """Delete an analyzer. + + Args: + name (str): Analyzer name. + force (bool | None): Remove the analyzer configuration even if in use. + ignore_missing (bool): Do not raise an exception on missing analyzer. + + Returns: + bool: `True` if the analyzer was deleted successfully, `False` if the + analyzer was not found and **ignore_missing** was set to `True`. + + Raises: + AnalyzerDeleteError: If the operation fails. + + References: + - `remove-an-analyzer `__ + """ # noqa: E501 + params: Params = {} + if force is not None: + params["force"] = force + + request = Request( + method=Method.DELETE, + endpoint=f"/_api/analyzer/{name}", + params=params, + ) + + def response_handler(resp: Response) -> bool: + if resp.is_success: + return True + if resp.status_code == HTTP_NOT_FOUND and ignore_missing: + return False + raise AnalyzerDeleteError(resp, request) + + return await self._executor.execute(request, response_handler) + async def has_user(self, username: str) -> Result[bool]: """Check if a user exists. diff --git a/arangoasync/exceptions.py b/arangoasync/exceptions.py index 4e46d06..e052fd4 100644 --- a/arangoasync/exceptions.py +++ b/arangoasync/exceptions.py @@ -135,6 +135,22 @@ class AQLQueryValidateError(ArangoServerError): """Failed to parse and validate query.""" +class AnalyzerCreateError(ArangoServerError): + """Failed to create analyzer.""" + + +class AnalyzerGetError(ArangoServerError): + """Failed to retrieve analyzer details.""" + + +class AnalyzerDeleteError(ArangoServerError): + """Failed to delete analyzer.""" + + +class AnalyzerListError(ArangoServerError): + """Failed to retrieve analyzers.""" + + class AsyncExecuteError(ArangoServerError): """Failed to execute async API request.""" diff --git a/arangoasync/response.py b/arangoasync/response.py index 63b10fb..000def9 100644 --- a/arangoasync/response.py +++ b/arangoasync/response.py @@ -5,7 +5,7 @@ from typing import Optional from arangoasync.request import Method -from arangoasync.typings import ResponseHeaders +from arangoasync.typings import Json, ResponseHeaders class Response: @@ -63,3 +63,17 @@ def __init__( self.error_code: Optional[int] = None self.error_message: Optional[str] = None self.is_success: Optional[bool] = None + + @staticmethod + def format_body(body: Json) -> Json: + """Format the generic response body, stripping the error code and message. + + Args: + body (Json): The response body. + + Returns: + dict: The formatted response body. + """ + body.pop("error", None) + body.pop("code", None) + return body diff --git a/docs/analyzer.rst b/docs/analyzer.rst new file mode 100644 index 0000000..cd92018 --- /dev/null +++ b/docs/analyzer.rst @@ -0,0 +1,39 @@ +Analyzers +--------- + +For more information on analyzers, 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 an analyzer. + await db.create_analyzer( + name='test_analyzer', + analyzer_type='identity', + properties={}, + features=[] + ) + + # Retrieve the created analyzer. + analyzer = await db.analyzer('test_analyzer') + + # Retrieve list of analyzers. + await db.analyzers() + + # Delete an analyzer. + await db.delete_analyzer('test_analyzer', ignore_missing=True) + +Refer to :class:`arangoasync.database.StandardDatabase` class for API specification. diff --git a/docs/index.rst b/docs/index.rst index f30ed6e..375303c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -45,6 +45,7 @@ Contents transaction view + analyzer **API Executions** diff --git a/tests/helpers.py b/tests/helpers.py index b961064..f2f63f7 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -53,3 +53,12 @@ def generate_view_name(): str: Random view name. """ return f"test_view_{uuid4().hex}" + + +def generate_analyzer_name(): + """Generate and return a random analyzer name. + + Returns: + str: Random analyzer name. + """ + return f"test_analyzer_{uuid4().hex}" diff --git a/tests/test_analyzer.py b/tests/test_analyzer.py new file mode 100644 index 0000000..856b6d7 --- /dev/null +++ b/tests/test_analyzer.py @@ -0,0 +1,91 @@ +import pytest +from packaging import version + +from arangoasync.exceptions import ( + AnalyzerCreateError, + AnalyzerDeleteError, + AnalyzerGetError, + AnalyzerListError, +) +from tests.helpers import generate_analyzer_name + + +@pytest.mark.asyncio +async def test_analyzer_management(db, bad_db, enterprise, db_version): + analyzer_name = generate_analyzer_name() + full_analyzer_name = db.name + "::" + analyzer_name + bad_analyzer_name = generate_analyzer_name() + + # Test create identity analyzer + result = await db.create_analyzer(analyzer_name, "identity") + assert result["name"] == full_analyzer_name + assert result["type"] == "identity" + assert result["properties"] == {} + assert result["features"] == [] + + # Test create delimiter analyzer + result = await db.create_analyzer( + name=generate_analyzer_name(), + analyzer_type="delimiter", + properties={"delimiter": ","}, + ) + assert result["type"] == "delimiter" + assert result["properties"] == {"delimiter": ","} + assert result["features"] == [] + + # Test create duplicate with bad database + with pytest.raises(AnalyzerCreateError): + await bad_db.create_analyzer(analyzer_name, "identity") + + # Test get analyzer + result = await db.analyzer(analyzer_name) + assert result["name"] == full_analyzer_name + assert result["type"] == "identity" + assert result["properties"] == {} + assert result["features"] == [] + + # Test get missing analyzer + with pytest.raises(AnalyzerGetError): + await db.analyzer(bad_analyzer_name) + + # Test list analyzers + result = await db.analyzers() + assert full_analyzer_name in [a["name"] for a in result] + + # Test list analyzers with bad database + with pytest.raises(AnalyzerListError): + await bad_db.analyzers() + + # Test delete analyzer + assert await db.delete_analyzer(analyzer_name, force=True) is True + assert full_analyzer_name not in [a["name"] for a in await db.analyzers()] + + # Test delete missing analyzer + with pytest.raises(AnalyzerDeleteError): + await db.delete_analyzer(analyzer_name) + + # Test delete missing analyzer with ignore_missing set to True + assert await db.delete_analyzer(analyzer_name, ignore_missing=True) is False + + # Test create geo_s2 analyzer + if enterprise: + analyzer_name = generate_analyzer_name() + result = await db.create_analyzer(analyzer_name, "geo_s2", properties={}) + assert result["type"] == "geo_s2" + assert await db.delete_analyzer(analyzer_name) + + if db_version >= version.parse("3.12.0"): + # Test delimiter analyzer with multiple delimiters + result = await db.create_analyzer( + name=generate_analyzer_name(), + analyzer_type="multi_delimiter", + properties={"delimiters": [",", "."]}, + ) + assert result["type"] == "multi_delimiter" + assert result["properties"] == {"delimiters": [",", "."]} + + # Test wildcard analyzer + analyzer_name = generate_analyzer_name() + result = await db.create_analyzer(analyzer_name, "wildcard", {"ngramSize": 4}) + assert result["type"] == "wildcard" + assert result["properties"] == {"ngramSize": 4}