Skip to content

Analyzers #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions arangoasync/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 <https://docs.arangodb.com/stable/develop/http-api/analyzers/#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 <https://docs.arangodb.com/stable/develop/http-api/analyzers/#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 <https://docs.arangodb.com/stable/develop/http-api/analyzers/#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 <https://docs.arangodb.com/stable/develop/http-api/analyzers/#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.

Expand Down
16 changes: 16 additions & 0 deletions arangoasync/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
16 changes: 15 additions & 1 deletion arangoasync/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
39 changes: 39 additions & 0 deletions docs/analyzer.rst
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Contents

transaction
view
analyzer

**API Executions**

Expand Down
9 changes: 9 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
91 changes: 91 additions & 0 deletions tests/test_analyzer.py
Original file line number Diff line number Diff line change
@@ -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}
Loading