Skip to content

Commit 2160ea4

Browse files
authored
Fix graph creation flags and consolidate option (#107)
* adding failing test * improve GraphProperties * Fix graph creation flags and add GraphOptions support while deprecating separate arguments
1 parent 9bbb115 commit 2160ea4

6 files changed

Lines changed: 197 additions & 14 deletions

File tree

‎arangoasync/database.py‎

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,11 +1240,18 @@ async def create_graph(
12401240
edge collection), "from" (list of vertex collection names) and "to"
12411241
(list of vertex collection names).
12421242
is_disjoint (bool | None): Whether to create a Disjoint SmartGraph
1243-
instead of a regular SmartGraph (Enterprise Edition only).
1244-
is_smart (bool | None): Define if the created graph should be smart
1245-
(Enterprise Edition only).
1243+
instead of a regular SmartGraph. Only has an effect with
1244+
`smart_graph_attribute` or `is_smart` (Enterprise Edition only).
1245+
Deprecated: use `GraphOptions.is_disjoint` instead.
1246+
is_smart (bool | None): If `True`, create a SmartGraph when
1247+
`options.smart_graph_attribute` is set, or an EnterpriseGraph
1248+
otherwise. Ignored for SatelliteGraphs (Enterprise Edition only).
1249+
Deprecated: use `GraphOptions.is_smart` instead.
12461250
options (GraphOptions | dict | None): Options for creating collections
1247-
within this graph.
1251+
within this graph. Setting `smart_graph_attribute` creates a
1252+
SmartGraph even if `is_smart` is omitted or `False`. Explicit
1253+
`is_smart` and `is_disjoint` arguments override the corresponding
1254+
entries in these options.
12481255
orphan_collections (list | None): An array of additional vertex
12491256
collections. Documents in these collections do not have edges
12501257
within this graph.
@@ -1260,22 +1267,31 @@ async def create_graph(
12601267
References:
12611268
- `create-a-graph <https://docs.arango.ai/arangodb/stable/develop/http-api/graphs/named-graphs/#create-a-graph>`__
12621269
""" # noqa: E501
1270+
if is_smart is not None or is_disjoint is not None:
1271+
warn(
1272+
"The separate is_smart and is_disjoint arguments are deprecated; "
1273+
"pass these flags through GraphOptions or the options "
1274+
"dictionary instead.",
1275+
DeprecationWarning,
1276+
stacklevel=2,
1277+
)
1278+
12631279
params: Params = {}
12641280
if wait_for_sync is not None:
12651281
params["waitForSync"] = wait_for_sync
12661282

12671283
data: Json = {"name": name}
12681284
if edge_definitions is not None:
12691285
data["edgeDefinitions"] = edge_definitions
1270-
if is_disjoint is not None:
1271-
data["isDisjoint"] = is_disjoint
1272-
if is_smart is not None:
1273-
data["isSmart"] = is_smart
12741286
if options is not None:
12751287
if isinstance(options, GraphOptions):
1276-
data["options"] = options.to_dict()
1288+
data["options"] = options.to_dict().copy()
12771289
else:
1278-
data["options"] = options
1290+
data["options"] = options.copy()
1291+
if is_disjoint is not None:
1292+
data.setdefault("options", {})["isDisjoint"] = is_disjoint
1293+
if is_smart is not None:
1294+
data.setdefault("options", {})["isSmart"] = is_smart
12791295
if orphan_collections is not None:
12801296
data["orphanCollections"] = orphan_collections
12811297

‎arangoasync/graph.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,11 @@ async def properties(self) -> Result[GraphProperties]:
8787
"""Get the properties of the graph.
8888
8989
Returns:
90-
GraphProperties: Properties of the graph.
90+
GraphProperties: Properties of the graph, including `is_smart`,
91+
`is_disjoint`, and `smart_graph_attribute`.
9192
9293
Raises:
93-
GraphProperties: If the operation fails.
94+
GraphPropertiesError: If the operation fails.
9495
9596
References:
9697
- `get-a-graph <https://docs.arango.ai/arangodb/stable/develop/http-api/graphs/named-graphs/#get-a-graph>`__

‎arangoasync/typings.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1873,6 +1873,16 @@ def is_smart(self) -> bool:
18731873
"""Check if the graph is a smart graph."""
18741874
return cast(bool, self._data.get("isSmart", False))
18751875

1876+
@property
1877+
def is_disjoint(self) -> bool:
1878+
"""Check if the graph is a Disjoint SmartGraph."""
1879+
return cast(bool, self._data.get("isDisjoint", False))
1880+
1881+
@property
1882+
def smart_graph_attribute(self) -> Optional[str]:
1883+
"""Return the sharding attribute for a SmartGraph, if present."""
1884+
return cast(Optional[str], self._data.get("smartGraphAttribute"))
1885+
18761886
@property
18771887
def is_satellite(self) -> bool:
18781888
"""Check if the graph is a satellite graph."""
@@ -1963,6 +1973,10 @@ class GraphOptions(JsonWrapper):
19631973
Enterprise Edition.
19641974
write_concern (int | None): The write concern for new collections in the
19651975
graph.
1976+
is_smart (bool | None): Create a SmartGraph when `smart_graph_attribute`
1977+
is set, or an EnterpriseGraph otherwise. Ignored for SatelliteGraphs.
1978+
is_disjoint (bool | None): Create a Disjoint SmartGraph. Only has an
1979+
effect with `smart_graph_attribute` or `is_smart`.
19661980
19671981
References:
19681982
- `create-a-graph <https://docs.arango.ai/arangodb/stable/develop/http-api/graphs/named-graphs/#create-a-graph>`__
@@ -1975,6 +1989,8 @@ def __init__(
19751989
satellites: Optional[List[str]] = None,
19761990
smart_graph_attribute: Optional[str] = None,
19771991
write_concern: Optional[int] = None,
1992+
is_smart: Optional[bool] = None,
1993+
is_disjoint: Optional[bool] = None,
19781994
) -> None:
19791995
data: Json = dict()
19801996
if number_of_shards is not None:
@@ -1987,8 +2003,20 @@ def __init__(
19872003
data["smartGraphAttribute"] = smart_graph_attribute
19882004
if write_concern is not None:
19892005
data["writeConcern"] = write_concern
2006+
if is_smart is not None:
2007+
data["isSmart"] = is_smart
2008+
if is_disjoint is not None:
2009+
data["isDisjoint"] = is_disjoint
19902010
super().__init__(data)
19912011

2012+
@property
2013+
def is_smart(self) -> Optional[bool]:
2014+
return cast(Optional[bool], self._data.get("isSmart"))
2015+
2016+
@property
2017+
def is_disjoint(self) -> Optional[bool]:
2018+
return cast(Optional[bool], self._data.get("isDisjoint"))
2019+
19922020
@property
19932021
def number_of_shards(self) -> Optional[int]:
19942022
return cast(int, self._data.get("numberOfShards"))

‎docs/graph.rst‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,45 @@ For more information, refer to `ArangoDB Manual`_.
4242
# Delete the graph.
4343
await db.delete_graph("school")
4444
45+
Graph creation options
46+
======================
47+
48+
Pass ``GraphOptions(is_smart=True)`` as ``options`` to create an EnterpriseGraph
49+
without a user-defined sharding attribute. Set ``GraphOptions.smart_graph_attribute``
50+
to create a SmartGraph; ``is_smart=True`` is then optional. Every vertex must
51+
contain the named attribute.
52+
53+
Set ``is_disjoint=True`` in ``GraphOptions`` with a smart graph attribute to create
54+
a Disjoint SmartGraph, which rejects edges between vertices with different
55+
attribute values.
56+
For example, on an Enterprise cluster:
57+
58+
.. code-block:: python
59+
60+
from arangoasync.typings import GraphOptions
61+
62+
regional = await db.create_graph(
63+
"regional",
64+
edge_definitions=[{
65+
"collection": "routes",
66+
"from": ["places"],
67+
"to": ["places"],
68+
}],
69+
options=GraphOptions(
70+
number_of_shards=3,
71+
smart_graph_attribute="region",
72+
is_disjoint=True,
73+
),
74+
)
75+
properties = await regional.properties()
76+
assert properties.is_disjoint is True
77+
assert properties.smart_graph_attribute == "region"
78+
79+
The separate ``create_graph(is_smart=..., is_disjoint=...)`` arguments are
80+
deprecated. Pass these flags through ``GraphOptions`` or a raw ``options``
81+
dictionary instead. Existing separate arguments still take precedence over
82+
matching entries in ``options``, including explicit ``False`` values.
83+
4584
.. _edge-definitions:
4685

4786
Edge Definitions

‎tests/test_graph.py‎

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import pytest
22

3+
from arangoasync.errno import INVALID_DISJOINT_SMART_EDGE
34
from arangoasync.exceptions import (
45
DocumentDeleteError,
6+
DocumentInsertError,
57
EdgeCollectionListError,
68
EdgeDefinitionDeleteError,
79
EdgeDefinitionListError,
@@ -19,6 +21,80 @@
1921
from tests.helpers import generate_col_name, generate_graph_name
2022

2123

24+
@pytest.mark.asyncio
25+
@pytest.mark.parametrize("creation_api", ["options", "legacy"])
26+
@pytest.mark.parametrize(
27+
"smart_field,disjoint",
28+
[(None, False), ("region", False), ("region", True)],
29+
ids=["enterprise", "smart", "disjoint-smart"],
30+
)
31+
async def test_graph_create_smart_options(
32+
db, cluster, skip_tests, smart_field, disjoint, creation_api
33+
):
34+
if "enterprise" in skip_tests:
35+
pytest.skip("Only for ArangoDB enterprise edition")
36+
if not cluster:
37+
pytest.skip("Only for ArangoDB clusters")
38+
39+
# Create an EnterpriseGraph, SmartGraph, or Disjoint SmartGraph.
40+
graph_name = generate_graph_name()
41+
vertices = generate_col_name()
42+
edges = generate_col_name()
43+
options = GraphOptions(
44+
number_of_shards=3,
45+
smart_graph_attribute=smart_field,
46+
is_smart=True,
47+
is_disjoint=disjoint,
48+
)
49+
kwargs = {}
50+
if creation_api == "legacy":
51+
options = GraphOptions(
52+
number_of_shards=3,
53+
smart_graph_attribute=smart_field,
54+
is_smart=False,
55+
is_disjoint=not disjoint,
56+
)
57+
kwargs = {"is_smart": True, "is_disjoint": disjoint}
58+
original_options = options.to_dict().copy()
59+
graph = await db.create_graph(
60+
graph_name,
61+
edge_definitions=[{"collection": edges, "from": [vertices], "to": [vertices]}],
62+
options=options,
63+
**kwargs,
64+
)
65+
try:
66+
assert options.to_dict() == original_options
67+
# Check that the server honored the graph creation options.
68+
properties = await graph.properties()
69+
assert properties.is_smart is True
70+
assert properties.is_disjoint is disjoint
71+
assert (properties.smart_graph_attribute or "") == (smart_field or "")
72+
assert properties.number_of_shards == 3
73+
74+
# Add two vertices in one region and a third in another.
75+
first = await graph.insert_vertex(vertices, {"region": "eu"})
76+
second = await graph.insert_vertex(vertices, {"region": "eu"})
77+
third = await graph.insert_vertex(vertices, {"region": "us"})
78+
# All three graph types must allow edges within a region.
79+
edge = await graph.insert_edge(
80+
edges, {"_from": first["_id"], "_to": second["_id"]}
81+
)
82+
assert await graph.has_edge(edge["_id"])
83+
84+
# Only the disjoint graph must reject edges between regions.
85+
cross_region_edge = {"_from": first["_id"], "_to": third["_id"]}
86+
if disjoint:
87+
with pytest.raises(DocumentInsertError) as err:
88+
await graph.insert_edge(edges, cross_region_edge)
89+
assert err.value.error_code == INVALID_DISJOINT_SMART_EDGE
90+
else:
91+
edge = await graph.insert_edge(edges, cross_region_edge)
92+
assert await graph.has_edge(edge["_id"])
93+
finally:
94+
# Remove the graph and its collections even if an assertion fails.
95+
await db.delete_graph(graph_name, drop_collections=True)
96+
97+
2298
@pytest.mark.asyncio
2399
async def test_graph_basic(db, bad_db):
24100
graph1_name = generate_graph_name()
@@ -60,8 +136,8 @@ async def test_graph_properties(db, bad_graph, cluster, skip_tests):
60136
# Create a graph
61137
name = generate_graph_name()
62138
is_smart = cluster and "enterprise" not in skip_tests
63-
options = GraphOptions(number_of_shards=3)
64-
graph = await db.create_graph(name, is_smart=is_smart, options=options)
139+
options = GraphOptions(number_of_shards=3, is_smart=is_smart)
140+
graph = await db.create_graph(name, options=options)
65141

66142
with pytest.raises(GraphPropertiesError):
67143
await bad_graph.properties()

‎tests/test_typings.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,17 @@ def test_GraphProperties():
358358
]
359359
assert graph_properties.orphan_collections == ["orphan1", "orphan2"]
360360

361+
for is_disjoint in (False, True):
362+
properties = GraphProperties(
363+
{"isDisjoint": is_disjoint, "smartGraphAttribute": "region"}
364+
)
365+
assert properties.is_disjoint is is_disjoint
366+
assert properties.smart_graph_attribute == "region"
367+
368+
properties = GraphProperties({})
369+
assert properties.is_disjoint is False
370+
assert properties.smart_graph_attribute is None
371+
361372

362373
def test_GraphOptions():
363374
graph_options = GraphOptions(
@@ -374,6 +385,18 @@ def test_GraphOptions():
374385
assert graph_options.smart_graph_attribute == "region"
375386
assert graph_options.write_concern == 1
376387

388+
for flag in (False, True):
389+
options = GraphOptions(is_smart=flag, is_disjoint=flag)
390+
assert options.is_smart is flag
391+
assert options.is_disjoint is flag
392+
assert options.to_dict() == {"isSmart": flag, "isDisjoint": flag}
393+
394+
options = GraphOptions()
395+
assert options.is_smart is None
396+
assert options.is_disjoint is None
397+
assert "isSmart" not in options
398+
assert "isDisjoint" not in options
399+
377400

378401
def test_VertexCollectionOptions():
379402
options = VertexCollectionOptions(

0 commit comments

Comments
 (0)