Skip to content

Commit d676262

Browse files
committed
Inserting and retrieving vertex documents
1 parent a7dd6b2 commit d676262

File tree

3 files changed

+223
-20
lines changed

3 files changed

+223
-20
lines changed

arangoasync/collection.py

Lines changed: 142 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
]
77

88

9-
from typing import Any, Generic, List, Optional, Sequence, Tuple, TypeVar, cast
9+
from typing import Any, Generic, List, Optional, Sequence, TypeVar, cast
1010

1111
from arangoasync.cursor import Cursor
1212
from arangoasync.errno import (
@@ -75,6 +75,26 @@ def __init__(
7575
self._doc_deserializer = doc_deserializer
7676
self._id_prefix = f"{self._name}/"
7777

78+
@staticmethod
79+
def get_col_name(doc: str | Json) -> str:
80+
"""Extract the collection name from the document.
81+
82+
Args:
83+
doc (str | dict): Document ID or body with "_id" field.
84+
85+
Returns:
86+
str: Collection name.
87+
88+
Raises:
89+
DocumentParseError: If document ID is missing.
90+
"""
91+
try:
92+
doc_id: str = doc["_id"] if isinstance(doc, dict) else doc
93+
except KeyError:
94+
raise DocumentParseError('field "_id" required')
95+
else:
96+
return doc_id.split("/", 1)[0]
97+
7898
def _validate_id(self, doc_id: str) -> str:
7999
"""Check the collection name in the document ID.
80100
@@ -120,25 +140,21 @@ def _ensure_key_from_id(self, body: Json) -> Json:
120140
121141
Returns:
122142
dict: Document body with "_key" field if it has "_id" field.
143+
144+
Raises:
145+
DocumentParseError: If document is malformed.
123146
"""
124147
if "_id" in body and "_key" not in body:
125148
doc_id = self._validate_id(body["_id"])
126149
body = body.copy()
127150
body["_key"] = doc_id[len(self._id_prefix) :]
128151
return body
129152

130-
def _prep_from_doc(
131-
self,
132-
document: str | Json,
133-
rev: Optional[str] = None,
134-
check_rev: bool = False,
135-
) -> Tuple[str, Json]:
136-
"""Prepare document ID, body and request headers before a query.
153+
def _prep_from_doc(self, document: str | Json) -> str:
154+
"""Prepare document ID before a query.
137155
138156
Args:
139157
document (str | dict): Document ID, key or body.
140-
rev (str | None): Document revision.
141-
check_rev (bool): Whether to check the revision.
142158
143159
Returns:
144160
Document ID and request headers.
@@ -149,7 +165,6 @@ def _prep_from_doc(
149165
"""
150166
if isinstance(document, dict):
151167
doc_id = self._extract_id(document)
152-
rev = rev or document.get("_rev")
153168
elif isinstance(document, str):
154169
if "/" in document:
155170
doc_id = self._validate_id(document)
@@ -158,10 +173,7 @@ def _prep_from_doc(
158173
else:
159174
raise TypeError("Document must be str or a dict")
160175

161-
if not check_rev or rev is None:
162-
return doc_id, {}
163-
else:
164-
return doc_id, {"If-Match": rev}
176+
return doc_id
165177

166178
def _build_filter_conditions(self, filters: Optional[Json]) -> str:
167179
"""Build filter conditions for an AQL query.
@@ -597,7 +609,7 @@ async def get(
597609
References:
598610
- `get-a-document <https://docs.arangodb.com/stable/develop/http-api/documents/#get-a-document>`__
599611
""" # noqa: E501
600-
handle, _ = self._prep_from_doc(document)
612+
handle = self._prep_from_doc(document)
601613

602614
headers: RequestHeaders = {}
603615
if allow_dirty_read:
@@ -656,7 +668,7 @@ async def has(
656668
References:
657669
- `get-a-document-header <https://docs.arangodb.com/stable/develop/http-api/documents/#get-a-document-header>`__
658670
""" # noqa: E501
659-
handle, _ = self._prep_from_doc(document)
671+
handle = self._prep_from_doc(document)
660672

661673
headers: RequestHeaders = {}
662674
if allow_dirty_read:
@@ -742,7 +754,6 @@ async def insert(
742754
- `create-a-document <https://docs.arangodb.com/stable/develop/http-api/documents/#create-a-document>`__
743755
""" # noqa: E501
744756
if isinstance(document, dict):
745-
# We assume that the document deserializer works with dictionaries.
746757
document = cast(T, self._ensure_key_from_id(document))
747758

748759
params: Params = {}
@@ -1752,6 +1763,119 @@ def graph(self) -> str:
17521763
"""
17531764
return self._graph
17541765

1766+
async def get(
1767+
self,
1768+
vertex: str | Json,
1769+
if_match: Optional[str] = None,
1770+
if_none_match: Optional[str] = None,
1771+
) -> Result[Optional[Json]]:
1772+
"""Return a document.
1773+
1774+
Args:
1775+
vertex (str | dict): Document ID, key or body.
1776+
Document body must contain the "_id" or "_key" field.
1777+
if_match (str | None): The document is returned, if it has the same
1778+
revision as the given ETag.
1779+
if_none_match (str | None): The document is returned, if it has a
1780+
different revision than the given ETag.
1781+
1782+
Returns:
1783+
Document or `None` if not found.
1784+
1785+
Raises:
1786+
DocumentRevisionError: If the revision is incorrect.
1787+
DocumentGetError: If retrieval fails.
1788+
DocumentParseError: If the document is malformed.
1789+
1790+
References:
1791+
- `get-a-vertex <https://docs.arangodb.com/stable/develop/http-api/graphs/named-graphs/#get-a-vertex>`__
1792+
""" # noqa: E501
1793+
handle = self._prep_from_doc(vertex)
1794+
1795+
headers: RequestHeaders = {}
1796+
if if_match is not None:
1797+
headers["If-Match"] = if_match
1798+
if if_none_match is not None:
1799+
headers["If-None-Match"] = if_none_match
1800+
1801+
request = Request(
1802+
method=Method.GET,
1803+
endpoint=f"/_api/gharial/{self._graph}/vertex/{handle}",
1804+
headers=headers,
1805+
)
1806+
1807+
def response_handler(resp: Response) -> Optional[Json]:
1808+
if resp.is_success:
1809+
data: Json = self.deserializer.loads(resp.raw_body)
1810+
return cast(Json, data["vertex"])
1811+
elif resp.status_code == HTTP_NOT_FOUND:
1812+
if resp.error_code == DOCUMENT_NOT_FOUND:
1813+
return None
1814+
else:
1815+
raise DocumentGetError(resp, request)
1816+
elif resp.status_code == HTTP_PRECONDITION_FAILED:
1817+
raise DocumentRevisionError(resp, request)
1818+
else:
1819+
raise DocumentGetError(resp, request)
1820+
1821+
return await self._executor.execute(request, response_handler)
1822+
1823+
async def insert(
1824+
self,
1825+
vertex: T,
1826+
wait_for_sync: Optional[bool] = None,
1827+
return_new: Optional[bool] = None,
1828+
) -> Result[Json]:
1829+
"""Insert a new vertex document.
1830+
1831+
Args:
1832+
vertex (dict): Document to insert. If it contains the "_key" or "_id"
1833+
field, the value is used as the key of the new document (otherwise
1834+
it is auto-generated). Any "_rev" field is ignored.
1835+
wait_for_sync (bool | None): Wait until document has been synced to disk.
1836+
return_new (bool | None): Additionally return the complete new document
1837+
under the attribute `new` in the result.
1838+
1839+
Returns:
1840+
dict: Document metadata (e.g. document id, key, revision).
1841+
1842+
Raises:
1843+
DocumentInsertError: If insertion fails.
1844+
DocumentParseError: If the document is malformed.
1845+
1846+
References:
1847+
- `create-a-vertex <https://docs.arangodb.com/stable/develop/http-api/graphs/named-graphs/#create-a-vertex>`__
1848+
""" # noqa: E501
1849+
if isinstance(vertex, dict):
1850+
vertex = cast(T, self._ensure_key_from_id(vertex))
1851+
1852+
params: Params = {}
1853+
if wait_for_sync is not None:
1854+
params["waitForSync"] = wait_for_sync
1855+
if return_new is not None:
1856+
params["returnNew"] = return_new
1857+
1858+
request = Request(
1859+
method=Method.POST,
1860+
endpoint=f"/_api/gharial/{self._graph}/vertex/{self.name}",
1861+
params=params,
1862+
data=self._doc_serializer.dumps(vertex),
1863+
)
1864+
1865+
def response_handler(resp: Response) -> Json:
1866+
if resp.is_success:
1867+
data: Json = self._executor.deserialize(resp.raw_body)
1868+
return cast(Json, data["vertex"])
1869+
msg: Optional[str] = None
1870+
if resp.status_code == HTTP_NOT_FOUND:
1871+
msg = (
1872+
"The graph cannot be found or the collection is not "
1873+
"part of the graph."
1874+
)
1875+
raise DocumentInsertError(resp, request, msg)
1876+
1877+
return await self._executor.execute(request, response_handler)
1878+
17551879

17561880
class EdgeCollection(Collection[T, U, V]):
17571881
"""Edge collection API wrapper.

arangoasync/graph.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from typing import Generic, List, Optional, Sequence, TypeVar, cast
55

6-
from arangoasync.collection import EdgeCollection, VertexCollection
6+
from arangoasync.collection import Collection, EdgeCollection, VertexCollection
77
from arangoasync.exceptions import (
88
EdgeCollectionListError,
99
EdgeDefinitionCreateError,
@@ -233,6 +233,73 @@ def response_handler(resp: Response) -> None:
233233

234234
await self._executor.execute(request, response_handler)
235235

236+
async def vertex(
237+
self,
238+
vertex: str | Json,
239+
if_match: Optional[str] = None,
240+
if_none_match: Optional[str] = None,
241+
) -> Result[Optional[Json]]:
242+
"""Return a document.
243+
244+
Args:
245+
vertex (str | dict): Document ID, key or body.
246+
Document body must contain the "_id" or "_key" field.
247+
if_match (str | None): The document is returned, if it has the same
248+
revision as the given ETag.
249+
if_none_match (str | None): The document is returned, if it has a
250+
different revision than the given ETag.
251+
252+
Returns:
253+
Document or `None` if not found.
254+
255+
Raises:
256+
DocumentRevisionError: If the revision is incorrect.
257+
DocumentGetError: If retrieval fails.
258+
DocumentParseError: If the document is malformed.
259+
260+
References:
261+
- `get-a-vertex <https://docs.arangodb.com/stable/develop/http-api/graphs/named-graphs/#get-a-vertex>`__
262+
""" # noqa: E501
263+
return await self.vertex_collection(Collection.get_col_name(vertex)).get(
264+
vertex,
265+
if_match=if_match,
266+
if_none_match=if_none_match,
267+
)
268+
269+
async def insert_vertex(
270+
self,
271+
collection: str,
272+
vertex: T,
273+
wait_for_sync: Optional[bool] = None,
274+
return_new: Optional[bool] = None,
275+
) -> Result[Json]:
276+
"""Insert a new vertex document.
277+
278+
Args:
279+
collection (str): Name of the vertex collection to insert the document into.
280+
vertex (dict): Document to insert. If it contains the "_key" or "_id"
281+
field, the value is used as the key of the new document (otherwise
282+
it is auto-generated). Any "_rev" field is ignored.
283+
wait_for_sync (bool | None): Wait until document has been synced to disk.
284+
return_new (bool | None): Additionally return the complete new document
285+
under the attribute `new` in the result.
286+
287+
Returns:
288+
dict: Document metadata (e.g. document id, key, revision).
289+
290+
Raises:
291+
DocumentInsertError: If insertion fails.
292+
DocumentParseError: If the document is malformed.
293+
294+
References:
295+
- `create-a-vertex <https://docs.arangodb.com/stable/develop/http-api/graphs/named-graphs/#create-a-vertex>`__
296+
""" # noqa: E501
297+
return await self.vertex_collection(collection).insert(
298+
vertex,
299+
wait_for_sync=wait_for_sync,
300+
return_new=return_new,
301+
)
302+
236303
def edge_collection(self, name: str) -> EdgeCollection[T, U, V]:
237304
"""Returns the edge collection API wrapper.
238305

tests/test_graph.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ async def test_graph_properties(db, bad_graph, cluster, enterprise):
103103
assert properties.edge_definitions[0]["to"][0] == vcol2_name
104104

105105

106-
async def test_vertex_collections(db, bad_graph):
106+
async def test_vertex_collections(db, docs, bad_graph):
107107
# Test errors
108108
with pytest.raises(VertexCollectionCreateError):
109109
await bad_graph.create_vertex_collection("bad_col")
@@ -132,6 +132,18 @@ async def test_vertex_collections(db, bad_graph):
132132
await graph.delete_vertex_collection(names[0])
133133
assert await graph.has_vertex_collection(names[0]) is False
134134

135+
# Insert in both collections
136+
v1_meta = await graph.insert_vertex(names[1], docs[0])
137+
v2_meta = await graph.insert_vertex(names[2], docs[1])
138+
139+
# Get the vertex
140+
v1 = await graph.vertex(v1_meta)
141+
assert v1 is not None
142+
v2 = await graph.vertex(v2_meta["_id"])
143+
assert v2 is not None
144+
v3 = await graph.vertex(f"{names[2]}/bad_id")
145+
assert v3 is None
146+
135147

136148
async def test_edge_collections(db, bad_graph):
137149
# Test errors

0 commit comments

Comments
 (0)