Skip to content

Commit 75d1989

Browse files
authored
Document insertion and retrieval are working. (#24)
1 parent 3851a61 commit 75d1989

File tree

4 files changed

+148
-6
lines changed

4 files changed

+148
-6
lines changed

arangoasync/collection.py

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
__all__ = ["Collection", "StandardCollection"]
22

33

4-
from typing import Generic, Optional, Tuple, TypeVar
4+
from typing import Generic, Optional, Tuple, TypeVar, cast
55

6-
from arangoasync.errno import HTTP_NOT_FOUND, HTTP_PRECONDITION_FAILED
6+
from arangoasync.errno import (
7+
HTTP_BAD_PARAMETER,
8+
HTTP_NOT_FOUND,
9+
HTTP_PRECONDITION_FAILED,
10+
)
711
from arangoasync.exceptions import (
812
DocumentGetError,
13+
DocumentInsertError,
914
DocumentParseError,
1015
DocumentRevisionError,
1116
)
1217
from arangoasync.executor import ApiExecutor
1318
from arangoasync.request import Method, Request
1419
from arangoasync.response import Response
1520
from arangoasync.serialization import Deserializer, Serializer
16-
from arangoasync.typings import Json, Result
21+
from arangoasync.typings import Json, Params, Result
1722

1823
T = TypeVar("T")
1924
U = TypeVar("U")
@@ -83,6 +88,21 @@ def _extract_id(self, body: Json) -> str:
8388
except KeyError:
8489
raise DocumentParseError('Field "_key" or "_id" required')
8590

91+
def _ensure_key_from_id(self, body: Json) -> Json:
92+
"""Return the body with "_key" field if it has "_id" field.
93+
94+
Args:
95+
body (dict): Document body.
96+
97+
Returns:
98+
dict: Document body with "_key" field if it has "_id" field.
99+
"""
100+
if "_id" in body and "_key" not in body:
101+
doc_id = self._validate_id(body["_id"])
102+
body = body.copy()
103+
body["_key"] = doc_id[len(self._id_prefix) :]
104+
return body
105+
86106
def _prep_from_doc(
87107
self,
88108
document: str | Json,
@@ -172,7 +192,10 @@ async def get(
172192
Raises:
173193
DocumentRevisionError: If the revision is incorrect.
174194
DocumentGetError: If retrieval fails.
175-
"""
195+
196+
References:
197+
- `get-a-document <https://docs.arangodb.com/stable/develop/http-api/documents/#get-a-document>`__
198+
""" # noqa: E501
176199
handle, headers = self._prep_from_doc(document, rev, check_rev)
177200

178201
if allow_dirty_read:
@@ -195,3 +218,102 @@ def response_handler(resp: Response) -> Optional[U]:
195218
raise DocumentGetError(resp, request)
196219

197220
return await self._executor.execute(request, response_handler)
221+
222+
async def insert(
223+
self,
224+
document: T,
225+
wait_for_sync: Optional[bool] = None,
226+
return_new: Optional[bool] = None,
227+
return_old: Optional[bool] = None,
228+
silent: Optional[bool] = None,
229+
overwrite: Optional[bool] = None,
230+
overwrite_mode: Optional[str] = None,
231+
keep_null: Optional[bool] = None,
232+
merge_objects: Optional[bool] = None,
233+
refill_index_caches: Optional[bool] = None,
234+
version_attribute: Optional[str] = None,
235+
) -> Result[bool | Json]:
236+
"""Insert a new document.
237+
238+
Args:
239+
document (dict): Document to insert. If it contains the "_key" or "_id"
240+
field, the value is used as the key of the new document (otherwise
241+
it is auto-generated). Any "_rev" field is ignored.
242+
wait_for_sync (bool | None): Wait until document has been synced to disk.
243+
return_new (bool | None): Additionally return the complete new document
244+
under the attribute `new` in the result.
245+
return_old (bool | None): Additionally return the complete old document
246+
under the attribute `old` in the result. Only available if the
247+
`overwrite` option is used.
248+
silent (bool | None): If set to `True`, no document metadata is returned.
249+
This can be used to save resources.
250+
overwrite (bool | None): If set to `True`, operation does not fail on
251+
duplicate key and existing document is overwritten (replace-insert).
252+
overwrite_mode (str | None): Overwrite mode. Supersedes **overwrite**
253+
option. May be one of "ignore", "replace", "update" or "conflict".
254+
keep_null (bool | None): If set to `True`, fields with value None are
255+
retained in the document. Otherwise, they are removed completely.
256+
Applies only when **overwrite_mode** is set to "update"
257+
(update-insert).
258+
merge_objects (bool | None): If set to True, sub-dictionaries are merged
259+
instead of the new one overwriting the old one. Applies only when
260+
**overwrite_mode** is set to "update" (update-insert).
261+
refill_index_caches (bool | None): Whether to add new entries to
262+
in-memory index caches if document insertions affect the edge index
263+
or cache-enabled persistent indexes.
264+
version_attribute (str | None): Support for simple external versioning to
265+
document operations. Only applicable if **overwrite** is set to true
266+
or **overwriteMode** is set to "update" or "replace".
267+
268+
References:
269+
- `create-a-document <https://docs.arangodb.com/stable/develop/http-api/documents/#create-a-document>`__
270+
""" # noqa: E501
271+
if isinstance(document, dict):
272+
# We assume that the document deserializer works with dictionaries.
273+
document = cast(T, self._ensure_key_from_id(document))
274+
275+
params: Params = {}
276+
if wait_for_sync is not None:
277+
params["waitForSync"] = wait_for_sync
278+
if return_new is not None:
279+
params["returnNew"] = return_new
280+
if return_old is not None:
281+
params["returnOld"] = return_old
282+
if silent is not None:
283+
params["silent"] = silent
284+
if overwrite is not None:
285+
params["overwrite"] = overwrite
286+
if overwrite_mode is not None:
287+
params["overwriteMode"] = overwrite_mode
288+
if keep_null is not None:
289+
params["keepNull"] = keep_null
290+
if merge_objects is not None:
291+
params["mergeObjects"] = merge_objects
292+
if refill_index_caches is not None:
293+
params["refillIndexCaches"] = refill_index_caches
294+
if version_attribute is not None:
295+
params["versionAttribute"] = version_attribute
296+
297+
request = Request(
298+
method=Method.POST,
299+
endpoint=f"/_api/document/{self._name}",
300+
params=params,
301+
data=self._doc_serializer.dumps(document),
302+
)
303+
304+
def response_handler(resp: Response) -> bool | Json:
305+
if resp.is_success:
306+
if silent is True:
307+
return True
308+
return self._executor.deserialize(resp.raw_body)
309+
msg: Optional[str] = None
310+
if resp.status_code == HTTP_BAD_PARAMETER:
311+
msg = (
312+
"Body does not contain a valid JSON representation of "
313+
"one document."
314+
)
315+
elif resp.status_code == HTTP_NOT_FOUND:
316+
msg = "Collection not found."
317+
raise DocumentInsertError(resp, request, msg)
318+
319+
return await self._executor.execute(request, response_handler)

arangoasync/exceptions.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ class ArangoServerError(ArangoError):
5050
def __init__(
5151
self, resp: Response, request: Request, msg: Optional[str] = None
5252
) -> None:
53-
msg = msg or resp.error_message or resp.status_text
53+
if msg is None:
54+
msg = resp.error_message or resp.status_text
55+
else:
56+
msg = f"{msg} ({resp.error_message or resp.status_text})"
5457
self.error_message = resp.error_message
5558
self.error_code = resp.error_code
5659
if self.error_code is not None:
@@ -112,6 +115,10 @@ class DocumentGetError(ArangoServerError):
112115
"""Failed to retrieve document."""
113116

114117

118+
class DocumentInsertError(ArangoServerError):
119+
"""Failed to insert document."""
120+
121+
115122
class DocumentParseError(ArangoClientError):
116123
"""Failed to parse document input."""
117124

arangoasync/executor.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ def serializer(self) -> Serializer[Json]:
3737
def deserializer(self) -> Deserializer[Json, Jsons]:
3838
return self._conn.deserializer
3939

40+
def serialize(self, data: Json) -> str:
41+
return self.serializer.dumps(data)
42+
43+
def deserialize(self, data: bytes) -> Json:
44+
return self.deserializer.loads(data)
45+
4046
async def execute(
4147
self, request: Request, response_handler: Callable[[Response], T]
4248
) -> T:

tests/test_database.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,21 @@ async def test_create_drop_database(url, sys_db_name, root, password):
3131
# Create a new database
3232
db_name = generate_db_name()
3333
assert await sys_db.create_database(db_name) is True
34-
await client.db(db_name, auth_method="basic", auth=auth, verify=True)
34+
new_db = await client.db(db_name, auth_method="basic", auth=auth, verify=True)
3535
assert await sys_db.has_database(db_name) is True
3636

3737
# List available databases
3838
dbs = await sys_db.databases()
3939
assert db_name in dbs
4040
assert "_system" in dbs
4141

42+
# TODO move this to a separate test
43+
col_name = generate_col_name()
44+
col = await new_db.create_collection(col_name)
45+
await col.insert({"_key": "1", "a": 1})
46+
doc = await col.get("1")
47+
assert doc["_key"] == "1"
48+
4249
# Drop the newly created database
4350
assert await sys_db.delete_database(db_name) is True
4451
non_existent_db = generate_db_name()

0 commit comments

Comments
 (0)