Skip to content

Commit f51d17a

Browse files
authoredOct 29, 2024··
Adding more REST methods (#29)
* Improving REST API coverage * Fixing cluster-only properties * Fixing enterprise-only features
·
0.0.50.0.1
1 parent 0e72920 commit f51d17a

File tree

10 files changed

+969
-32
lines changed

10 files changed

+969
-32
lines changed
 

‎arangoasync/collection.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
HTTP_PRECONDITION_FAILED,
1010
)
1111
from arangoasync.exceptions import (
12+
CollectionPropertiesError,
1213
DocumentGetError,
1314
DocumentInsertError,
1415
DocumentParseError,
@@ -18,7 +19,7 @@
1819
from arangoasync.request import Method, Request
1920
from arangoasync.response import Response
2021
from arangoasync.serialization import Deserializer, Serializer
21-
from arangoasync.typings import Json, Params, Result
22+
from arangoasync.typings import CollectionProperties, Json, Params, Result
2223

2324
T = TypeVar("T")
2425
U = TypeVar("U")
@@ -48,9 +49,6 @@ def __init__(
4849
self._doc_deserializer = doc_deserializer
4950
self._id_prefix = f"{self._name}/"
5051

51-
def __repr__(self) -> str:
52-
return f"<StandardCollection {self.name}>"
53-
5452
def _validate_id(self, doc_id: str) -> str:
5553
"""Check the collection name in the document ID.
5654
@@ -148,6 +146,15 @@ def name(self) -> str:
148146
"""
149147
return self._name
150148

149+
@property
150+
def db_name(self) -> str:
151+
"""Return the name of the current database.
152+
153+
Returns:
154+
str: Database name.
155+
"""
156+
return self._executor.db_name
157+
151158

152159
class StandardCollection(Collection[T, U, V]):
153160
"""Standard collection API wrapper.
@@ -168,6 +175,33 @@ def __init__(
168175
) -> None:
169176
super().__init__(executor, name, doc_serializer, doc_deserializer)
170177

178+
def __repr__(self) -> str:
179+
return f"<StandardCollection {self.name}>"
180+
181+
async def properties(self) -> Result[CollectionProperties]:
182+
"""Return the full properties of the current collection.
183+
184+
Returns:
185+
CollectionProperties: Properties.
186+
187+
Raises:
188+
CollectionPropertiesError: If retrieval fails.
189+
190+
References:
191+
- `get-the-properties-of-a-collection <https://docs.arangodb.com/stable/develop/http-api/collections/#get-the-properties-of-a-collection>`__
192+
""" # noqa: E501
193+
request = Request(
194+
method=Method.GET,
195+
endpoint=f"/_api/collection/{self.name}/properties",
196+
)
197+
198+
def response_handler(resp: Response) -> CollectionProperties:
199+
if not resp.is_success:
200+
raise CollectionPropertiesError(resp, request)
201+
return CollectionProperties(self._executor.deserialize(resp.raw_body))
202+
203+
return await self._executor.execute(request, response_handler)
204+
171205
async def get(
172206
self,
173207
document: str | Json,
@@ -269,6 +303,9 @@ async def insert(
269303
bool | dict: Document metadata (e.g. document id, key, revision) or `True`
270304
if **silent** is set to `True`.
271305
306+
Raises:
307+
DocumentInsertError: If insertion fails.
308+
272309
References:
273310
- `create-a-document <https://docs.arangodb.com/stable/develop/http-api/documents/#create-a-document>`__
274311
""" # noqa: E501

‎arangoasync/database.py

Lines changed: 344 additions & 5 deletions
Large diffs are not rendered by default.

‎arangoasync/exceptions.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ class CollectionListError(ArangoServerError):
8787
"""Failed to retrieve collections."""
8888

8989

90+
class CollectionPropertiesError(ArangoServerError):
91+
"""Failed to retrieve collection properties."""
92+
93+
9094
class ClientConnectionAbortedError(ArangoClientError):
9195
"""The connection was aborted."""
9296

@@ -107,6 +111,10 @@ class DatabaseListError(ArangoServerError):
107111
"""Failed to retrieve databases."""
108112

109113

114+
class DatabasePropertiesError(ArangoServerError):
115+
"""Failed to retrieve database properties."""
116+
117+
110118
class DeserializationError(ArangoClientError):
111119
"""Failed to deserialize the server response."""
112120

@@ -131,6 +139,30 @@ class JWTRefreshError(ArangoClientError):
131139
"""Failed to refresh the JWT token."""
132140

133141

142+
class JWTSecretListError(ArangoServerError):
143+
"""Failed to retrieve information on currently loaded JWT secrets."""
144+
145+
146+
class JWTSecretReloadError(ArangoServerError):
147+
"""Failed to reload JWT secrets."""
148+
149+
150+
class PermissionGetError(ArangoServerError):
151+
"""Failed to retrieve user permission."""
152+
153+
154+
class PermissionListError(ArangoServerError):
155+
"""Failed to list user permissions."""
156+
157+
158+
class PermissionResetError(ArangoServerError):
159+
"""Failed to reset user permission."""
160+
161+
162+
class PermissionUpdateError(ArangoServerError):
163+
"""Failed to update user permission."""
164+
165+
134166
class SerializationError(ArangoClientError):
135167
"""Failed to serialize the request."""
136168

@@ -157,3 +189,11 @@ class UserGetError(ArangoServerError):
157189

158190
class UserListError(ArangoServerError):
159191
"""Failed to retrieve users."""
192+
193+
194+
class UserReplaceError(ArangoServerError):
195+
"""Failed to replace user."""
196+
197+
198+
class UserUpdateError(ArangoServerError):
199+
"""Failed to update user."""

‎arangoasync/executor.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ def connection(self) -> Connection:
2929
def context(self) -> str:
3030
return "default"
3131

32+
@property
33+
def db_name(self) -> str:
34+
return self._conn.db_name
35+
3236
@property
3337
def serializer(self) -> Serializer[Json]:
3438
return self._conn.serializer

‎arangoasync/typings.py

Lines changed: 348 additions & 18 deletions
Large diffs are not rendered by default.

‎tests/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,12 @@ async def doc_col(db):
200200
await db.delete_collection(col_name)
201201

202202

203+
@pytest.fixture
204+
def bad_col(db):
205+
col_name = generate_col_name()
206+
return db.collection(col_name)
207+
208+
203209
@pytest_asyncio.fixture(scope="session", autouse=True)
204210
async def teardown():
205211
yield

‎tests/test_client.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,17 @@ async def test_client_jwt_auth(url, sys_db_name, basic_auth_root):
119119

120120

121121
@pytest.mark.asyncio
122-
async def test_client_jwt_superuser_auth(url, sys_db_name, basic_auth_root, token):
122+
async def test_client_jwt_superuser_auth(
123+
url, sys_db_name, basic_auth_root, token, enterprise
124+
):
123125
# successful authentication
124126
async with ArangoClient(hosts=url) as client:
125-
await client.db(sys_db_name, auth_method="superuser", token=token, verify=True)
127+
db = await client.db(
128+
sys_db_name, auth_method="superuser", token=token, verify=True
129+
)
130+
if enterprise:
131+
await db.jwt_secrets()
132+
await db.reload_jwt_secrets()
126133

127134
# token missing
128135
async with ArangoClient(hosts=url) as client:

‎tests/test_collection.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import pytest
2+
3+
from arangoasync.exceptions import CollectionPropertiesError
4+
5+
6+
def test_collection_attributes(db, doc_col):
7+
assert doc_col.db_name == db.name
8+
assert doc_col.name.startswith("test_collection")
9+
assert repr(doc_col) == f"<StandardCollection {doc_col.name}>"
10+
11+
12+
@pytest.mark.asyncio
13+
async def test_collection_misc_methods(doc_col, bad_col):
14+
# Properties
15+
properties = await doc_col.properties()
16+
assert properties.name == doc_col.name
17+
assert properties.is_system is False
18+
assert len(properties.format()) > 1
19+
with pytest.raises(CollectionPropertiesError):
20+
await bad_col.properties()

‎tests/test_database.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import asyncio
2+
13
import pytest
24

35
from arangoasync.collection import StandardCollection
@@ -8,22 +10,51 @@
810
DatabaseCreateError,
911
DatabaseDeleteError,
1012
DatabaseListError,
13+
DatabasePropertiesError,
14+
JWTSecretListError,
15+
JWTSecretReloadError,
16+
ServerStatusError,
1117
)
1218
from arangoasync.typings import CollectionType, KeyOptions, UserInfo
1319
from tests.helpers import generate_col_name, generate_db_name, generate_username
1420

1521

1622
@pytest.mark.asyncio
17-
async def test_database_misc_methods(sys_db):
23+
async def test_database_misc_methods(sys_db, db, bad_db, cluster):
24+
# Status
1825
status = await sys_db.status()
1926
assert status["server"] == "arango"
27+
with pytest.raises(ServerStatusError):
28+
await bad_db.status()
29+
30+
sys_properties, db_properties = await asyncio.gather(
31+
sys_db.properties(), db.properties()
32+
)
33+
assert sys_properties.is_system is True
34+
assert db_properties.is_system is False
35+
assert sys_properties.name == sys_db.name
36+
assert db_properties.name == db.name
37+
if cluster:
38+
assert db_properties.replication_factor == 3
39+
assert db_properties.write_concern == 2
40+
41+
with pytest.raises(DatabasePropertiesError):
42+
await bad_db.properties()
43+
assert len(db_properties.format()) > 1
44+
45+
# JWT secrets
46+
with pytest.raises(JWTSecretListError):
47+
await bad_db.jwt_secrets()
48+
with pytest.raises(JWTSecretReloadError):
49+
await bad_db.reload_jwt_secrets()
2050

2151

2252
@pytest.mark.asyncio
2353
async def test_create_drop_database(
2454
arango_client,
2555
sys_db,
2656
db,
57+
bad_db,
2758
basic_auth_root,
2859
password,
2960
cluster,
@@ -60,12 +91,19 @@ async def test_create_drop_database(
6091
dbs = await sys_db.databases()
6192
assert db_name in dbs
6293
assert "_system" in dbs
94+
dbs = await sys_db.databases_accessible_to_user()
95+
assert db_name in dbs
96+
assert "_system" in dbs
97+
dbs = await db.databases_accessible_to_user()
98+
assert db.name in dbs
6399

64100
# Cannot list databases without permission
65101
with pytest.raises(DatabaseListError):
66102
await db.databases()
67103
with pytest.raises(DatabaseListError):
68104
await db.has_database(db_name)
105+
with pytest.raises(DatabaseListError):
106+
await bad_db.databases_accessible_to_user()
69107

70108
# Databases can only be dropped from the system database
71109
with pytest.raises(DatabaseDeleteError):

‎tests/test_user.py

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
11
import pytest
22

3-
from arangoasync.exceptions import UserCreateError, UserDeleteError, UserListError
3+
from arangoasync.auth import Auth
4+
from arangoasync.errno import USER_NOT_FOUND
5+
from arangoasync.exceptions import (
6+
CollectionCreateError,
7+
DocumentInsertError,
8+
PermissionResetError,
9+
PermissionUpdateError,
10+
UserCreateError,
11+
UserDeleteError,
12+
UserGetError,
13+
UserListError,
14+
UserReplaceError,
15+
UserUpdateError,
16+
)
417
from arangoasync.typings import UserInfo
5-
from tests.helpers import generate_string, generate_username
18+
from tests.helpers import generate_col_name, generate_string, generate_username
619

720

821
@pytest.mark.asyncio
@@ -44,6 +57,10 @@ async def test_user_management(sys_db, db, bad_db):
4457
assert user.user == username
4558
assert user.active is True
4659

60+
# Get non-existing user
61+
with pytest.raises(UserGetError):
62+
await sys_db.user(generate_username())
63+
4764
# Create already existing user
4865
with pytest.raises(UserCreateError):
4966
await sys_db.create_user(
@@ -55,6 +72,48 @@ async def test_user_management(sys_db, db, bad_db):
5572
)
5673
)
5774

75+
# Update existing user
76+
new_user = await sys_db.update_user(
77+
UserInfo(
78+
user=username,
79+
password=password,
80+
active=False,
81+
extra={"bar": "baz"},
82+
)
83+
)
84+
assert new_user["user"] == username
85+
assert new_user["active"] is False
86+
assert new_user["extra"] == {"foo": "bar", "bar": "baz"}
87+
assert await sys_db.user(username) == new_user
88+
89+
# Update missing user
90+
with pytest.raises(UserUpdateError) as err:
91+
await sys_db.update_user(
92+
UserInfo(user=generate_username(), password=generate_string())
93+
)
94+
assert err.value.error_code == USER_NOT_FOUND
95+
96+
# Replace existing user
97+
new_user = await sys_db.replace_user(
98+
UserInfo(
99+
user=username,
100+
password=password,
101+
active=True,
102+
extra={"baz": "qux"},
103+
)
104+
)
105+
assert new_user["user"] == username
106+
assert new_user["active"] is True
107+
assert new_user["extra"] == {"baz": "qux"}
108+
assert await sys_db.user(username) == new_user
109+
110+
# Replace missing user
111+
with pytest.raises(UserReplaceError) as err:
112+
await sys_db.replace_user(
113+
{"user": generate_username(), "password": generate_string()}
114+
)
115+
assert err.value.error_code == USER_NOT_FOUND
116+
58117
# Delete the newly created user
59118
assert await sys_db.delete_user(username) is True
60119
users = await sys_db.users()
@@ -73,3 +132,60 @@ async def test_user_management(sys_db, db, bad_db):
73132
await bad_db.users()
74133
with pytest.raises(UserListError):
75134
await bad_db.has_user(username)
135+
136+
137+
@pytest.mark.asyncio
138+
async def test_user_change_permissions(sys_db, arango_client, db):
139+
username = generate_username()
140+
password = generate_string()
141+
auth = Auth(username, password)
142+
143+
# Set read-only permissions
144+
await sys_db.create_user(UserInfo(username, password))
145+
146+
# Should not be able to update permissions without permission
147+
with pytest.raises(PermissionUpdateError):
148+
await db.update_permission(username, "ro", db.name)
149+
150+
await sys_db.update_permission(username, "ro", db.name)
151+
152+
# Verify read-only permissions
153+
permission = await sys_db.permission(username, db.name)
154+
assert permission == "ro"
155+
156+
# Should not be able to create a collection
157+
col_name = generate_col_name()
158+
db2 = await arango_client.db(db.name, auth=auth, verify=True)
159+
with pytest.raises(CollectionCreateError):
160+
await db2.create_collection(col_name)
161+
162+
all_permissions = await sys_db.permissions(username)
163+
assert "_system" in all_permissions
164+
assert db.name in all_permissions
165+
all_permissions = await sys_db.permissions(username, full=False)
166+
assert all_permissions[db.name] == "ro"
167+
168+
# Set read-write permissions
169+
await sys_db.update_permission(username, "rw", db.name)
170+
171+
# Should be able to create collection
172+
col = await db2.create_collection(col_name)
173+
await col.insert({"_key": "test"})
174+
175+
# Reset permissions
176+
with pytest.raises(PermissionResetError):
177+
await db.reset_permission(username, db.name)
178+
await sys_db.reset_permission(username, db.name)
179+
with pytest.raises(DocumentInsertError):
180+
await col.insert({"_key": "test"})
181+
182+
# Allow rw access
183+
await sys_db.update_permission(username, "rw", db.name)
184+
await col.insert({"_key": "test2"})
185+
186+
# No access to collection
187+
await sys_db.update_permission(username, "none", db.name, col_name)
188+
with pytest.raises(DocumentInsertError):
189+
await col.insert({"_key": "test"})
190+
191+
await db.delete_collection(col_name)

0 commit comments

Comments
 (0)
Please sign in to comment.