Skip to content

Commit 8155b95

Browse files
authored
Tasks API (#62)
* Adding support for /_api/tasks * Adding docs for /_api/tasks
1 parent 8833877 commit 8155b95

File tree

7 files changed

+324
-0
lines changed

7 files changed

+324
-0
lines changed

arangoasync/database.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
PermissionUpdateError,
4141
ServerStatusError,
4242
ServerVersionError,
43+
TaskCreateError,
44+
TaskDeleteError,
45+
TaskGetError,
46+
TaskListError,
4347
TransactionAbortError,
4448
TransactionCommitError,
4549
TransactionExecuteError,
@@ -2193,6 +2197,148 @@ def response_handler(resp: Response) -> Json:
21932197

21942198
return await self._executor.execute(request, response_handler)
21952199

2200+
async def tasks(self) -> Result[Jsons]:
2201+
"""Fetches all existing tasks from the server.
2202+
2203+
Returns:
2204+
list: List of currently active server tasks.
2205+
2206+
Raises:
2207+
TaskListError: If the list cannot be retrieved.
2208+
2209+
References:
2210+
- `list-all-tasks <https://docs.arangodb.com/stable/develop/http-api/tasks/#list-all-tasks>`__
2211+
""" # noqa: E501
2212+
request = Request(method=Method.GET, endpoint="/_api/tasks")
2213+
2214+
def response_handler(resp: Response) -> Jsons:
2215+
if not resp.is_success:
2216+
raise TaskListError(resp, request)
2217+
result: Jsons = self.deserializer.loads_many(resp.raw_body)
2218+
return result
2219+
2220+
return await self._executor.execute(request, response_handler)
2221+
2222+
async def task(self, task_id: str) -> Result[Json]:
2223+
"""Return the details of an active server task.
2224+
2225+
Args:
2226+
task_id (str) -> Server task ID.
2227+
2228+
Returns:
2229+
dict: Details of the server task.
2230+
2231+
Raises:
2232+
TaskGetError: If the task details cannot be retrieved.
2233+
2234+
References:
2235+
- `get-a-task <https://docs.arangodb.com/stable/develop/http-api/tasks/#get-a-task>`__
2236+
""" # noqa: E501
2237+
request = Request(method=Method.GET, endpoint=f"/_api/tasks/{task_id}")
2238+
2239+
def response_handler(resp: Response) -> Json:
2240+
if not resp.is_success:
2241+
raise TaskGetError(resp, request)
2242+
result: Json = self.deserializer.loads(resp.raw_body)
2243+
return result
2244+
2245+
return await self._executor.execute(request, response_handler)
2246+
2247+
async def create_task(
2248+
self,
2249+
command: str,
2250+
task_id: Optional[str] = None,
2251+
name: Optional[str] = None,
2252+
offset: Optional[int] = None,
2253+
params: Optional[Json] = None,
2254+
period: Optional[int] = None,
2255+
) -> Result[Json]:
2256+
"""Create a new task.
2257+
2258+
Args:
2259+
command (str): The JavaScript code to be executed.
2260+
task_id (str | None): Optional task ID. If not provided, the server will
2261+
generate a unique ID.
2262+
name (str | None): The name of the task.
2263+
offset (int | None): The offset in seconds after which the task should
2264+
start executing.
2265+
params (dict | None): Parameters to be passed to the command.
2266+
period (int | None): The number of seconds between the executions.
2267+
2268+
Returns:
2269+
dict: Details of the created task.
2270+
2271+
Raises:
2272+
TaskCreateError: If the task cannot be created.
2273+
2274+
References:
2275+
- `create-a-task <https://docs.arangodb.com/stable/develop/http-api/tasks/#create-a-task>`__
2276+
- `create-a-task-with-id <https://docs.arangodb.com/stable/develop/http-api/tasks/#create-a-task-with-id>`__
2277+
""" # noqa: E501
2278+
data: Json = {"command": command}
2279+
if name is not None:
2280+
data["name"] = name
2281+
if offset is not None:
2282+
data["offset"] = offset
2283+
if params is not None:
2284+
data["params"] = params
2285+
if period is not None:
2286+
data["period"] = period
2287+
2288+
if task_id is None:
2289+
request = Request(
2290+
method=Method.POST,
2291+
endpoint="/_api/tasks",
2292+
data=self.serializer.dumps(data),
2293+
)
2294+
else:
2295+
request = Request(
2296+
method=Method.PUT,
2297+
endpoint=f"/_api/tasks/{task_id}",
2298+
data=self.serializer.dumps(data),
2299+
)
2300+
2301+
def response_handler(resp: Response) -> Json:
2302+
if not resp.is_success:
2303+
raise TaskCreateError(resp, request)
2304+
result: Json = self.deserializer.loads(resp.raw_body)
2305+
return result
2306+
2307+
return await self._executor.execute(request, response_handler)
2308+
2309+
async def delete_task(
2310+
self,
2311+
task_id: str,
2312+
ignore_missing: bool = False,
2313+
) -> Result[bool]:
2314+
"""Delete a server task.
2315+
2316+
Args:
2317+
task_id (str): Task ID.
2318+
ignore_missing (bool): If `True`, do not raise an exception if the
2319+
task does not exist.
2320+
2321+
Returns:
2322+
bool: `True` if the task was deleted successfully, `False` if the
2323+
task was not found and **ignore_missing** was set to `True`.
2324+
2325+
Raises:
2326+
TaskDeleteError: If the operation fails.
2327+
2328+
References:
2329+
- `delete-a-task <https://docs.arangodb.com/stable/develop/http-api/tasks/#delete-a-task>`__
2330+
""" # noqa: E501
2331+
request = Request(method=Method.DELETE, endpoint=f"/_api/tasks/{task_id}")
2332+
2333+
def response_handler(resp: Response) -> bool:
2334+
if resp.is_success:
2335+
return True
2336+
if resp.status_code == HTTP_NOT_FOUND and ignore_missing:
2337+
return False
2338+
raise TaskDeleteError(resp, request)
2339+
2340+
return await self._executor.execute(request, response_handler)
2341+
21962342

21972343
class StandardDatabase(Database):
21982344
"""Standard database API wrapper.

arangoasync/exceptions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,22 @@ class SortValidationError(ArangoClientError):
451451
"""Invalid sort parameters."""
452452

453453

454+
class TaskCreateError(ArangoServerError):
455+
"""Failed to create server task."""
456+
457+
458+
class TaskDeleteError(ArangoServerError):
459+
"""Failed to delete server task."""
460+
461+
462+
class TaskGetError(ArangoServerError):
463+
"""Failed to retrieve server task details."""
464+
465+
466+
class TaskListError(ArangoServerError):
467+
"""Failed to retrieve server tasks."""
468+
469+
454470
class TransactionAbortError(ArangoServerError):
455471
"""Failed to abort transaction."""
456472

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Contents
7373
compression
7474
serialization
7575
backup
76+
task
7677
errors
7778
errno
7879
logging

docs/task.rst

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
.. code-block:: python
11+
12+
from arangoasync import ArangoClient
13+
from arangoasync.auth import Auth
14+
15+
# Initialize the client for ArangoDB.
16+
async with ArangoClient(hosts="http://localhost:8529") as client:
17+
auth = Auth(username="root", password="passwd")
18+
19+
# Connect to "test" database as root user.
20+
db = await client.db("test", auth=auth)
21+
22+
# Create a new task which simply prints parameters.
23+
await db.create_task(
24+
name="test_task",
25+
command="""
26+
var task = function(params){
27+
var db = require('@arangodb');
28+
db.print(params);
29+
}
30+
task(params);
31+
""",
32+
params={"foo": "bar"},
33+
offset=300,
34+
period=10,
35+
task_id="001"
36+
)
37+
38+
# List all active tasks
39+
tasks = await db.tasks()
40+
41+
# Retrieve details of a task by ID.
42+
details = await db.task("001")
43+
44+
# Delete an existing task by ID.
45+
await db.delete_task('001', ignore_missing=True)
46+
47+
48+
.. note::
49+
When deleting a database, any tasks that were initialized under its context
50+
remain active. It is therefore advisable to delete any running tasks before
51+
deleting the database.

tests/conftest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,19 @@ async def teardown():
256256
verify=False,
257257
)
258258

259+
# Remove all tasks
260+
test_tasks = [
261+
task
262+
for task in await sys_db.tasks()
263+
if task["name"].startswith("test_task")
264+
]
265+
await asyncio.gather(
266+
*(
267+
sys_db.delete_task(task["id"], ignore_missing=True)
268+
for task in test_tasks
269+
)
270+
)
271+
259272
# Remove all test users.
260273
tst_users = [
261274
user["user"]

tests/helpers.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,21 @@ def generate_analyzer_name():
6262
str: Random analyzer name.
6363
"""
6464
return f"test_analyzer_{uuid4().hex}"
65+
66+
67+
def generate_task_name():
68+
"""Generate and return a random task name.
69+
70+
Returns:
71+
str: Random task name.
72+
"""
73+
return f"test_task_{uuid4().hex}"
74+
75+
76+
def generate_task_id():
77+
"""Generate and return a random task ID.
78+
79+
Returns:
80+
str: Random task ID
81+
"""
82+
return f"test_task_id_{uuid4().hex}"

tests/test_task.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import pytest
2+
3+
from arangoasync.exceptions import (
4+
TaskCreateError,
5+
TaskDeleteError,
6+
TaskGetError,
7+
TaskListError,
8+
)
9+
from tests.helpers import generate_task_id, generate_task_name
10+
11+
12+
@pytest.mark.asyncio
13+
async def test_task_management(sys_db, bad_db):
14+
# This test intentionally uses the system database because cleaning up tasks is
15+
# easier there.
16+
17+
test_command = 'require("@arangodb").print(params);'
18+
19+
# Test errors
20+
with pytest.raises(TaskCreateError):
21+
await bad_db.create_task(command=test_command)
22+
with pytest.raises(TaskGetError):
23+
await bad_db.task("non_existent_task_id")
24+
with pytest.raises(TaskListError):
25+
await bad_db.tasks()
26+
with pytest.raises(TaskDeleteError):
27+
await bad_db.delete_task("non_existent_task_id")
28+
29+
# Create a task with a random ID
30+
task_name = generate_task_name()
31+
new_task = await sys_db.create_task(
32+
name=task_name,
33+
command=test_command,
34+
params={"foo": 1, "bar": 2},
35+
offset=1,
36+
)
37+
assert new_task["name"] == task_name
38+
task_id = new_task["id"]
39+
assert await sys_db.task(task_id) == new_task
40+
41+
# Delete task
42+
assert await sys_db.delete_task(task_id) is True
43+
44+
# Create a task with a specific ID
45+
task_name = generate_task_name()
46+
task_id = generate_task_id()
47+
new_task = await sys_db.create_task(
48+
name=task_name,
49+
command=test_command,
50+
params={"foo": 1, "bar": 2},
51+
offset=1,
52+
period=10,
53+
task_id=task_id,
54+
)
55+
assert new_task["name"] == task_name
56+
assert new_task["id"] == task_id
57+
58+
# Try to create a duplicate task
59+
with pytest.raises(TaskCreateError):
60+
await sys_db.create_task(
61+
name=task_name,
62+
command=test_command,
63+
params={"foo": 1, "bar": 2},
64+
task_id=task_id,
65+
)
66+
67+
# Test get missing task
68+
with pytest.raises(TaskGetError):
69+
await sys_db.task(generate_task_id())
70+
71+
# Test list tasks
72+
tasks = await sys_db.tasks()
73+
assert len(tasks) == 1
74+
75+
# Delete tasks
76+
assert await sys_db.delete_task(task_id) is True
77+
assert await sys_db.delete_task(task_id, ignore_missing=True) is False
78+
with pytest.raises(TaskDeleteError):
79+
await sys_db.delete_task(task_id)

0 commit comments

Comments
 (0)