Skip to content

Commit 20d6a53

Browse files
authored
Merge pull request #66 from JupiterOne/TD-8043-2.2.0-release
fix delete_relationship() method
2 parents 8d495d6 + 8a31f41 commit 20d6a53

File tree

8 files changed

+148
-67
lines changed

8 files changed

+148
-67
lines changed

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,13 +292,11 @@ j1.update_relationship(
292292
##### Delete a relationship
293293

294294
```python
295-
# Delete by relationship ID
296-
j1.delete_relationship(relationship_id='<id-of-relationship-to-delete>')
297-
298-
# Delete with timestamp
295+
# Delete a relationship (requires relationship ID, source entity ID, and target entity ID)
299296
j1.delete_relationship(
300297
relationship_id='<id-of-relationship-to-delete>',
301-
timestamp=int(time.time()) * 1000
298+
from_entity_id='<id-of-source-entity>',
299+
to_entity_id='<id-of-destination-entity>'
302300
)
303301
```
304302

examples/03_relationship_management.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -175,23 +175,18 @@ def update_relationship_examples(j1, relationship_id, from_entity_id, to_entity_
175175
)
176176
print(f"Updated with custom timestamp\n")
177177

178-
def delete_relationship_examples(j1, relationship_id):
178+
def delete_relationship_examples(j1, relationship_id, from_entity_id, to_entity_id):
179179
"""Demonstrate relationship deletion."""
180180

181181
print("=== Relationship Deletion Examples ===\n")
182182

183-
# 1. Basic deletion
184183
print("1. Deleting a relationship:")
185-
delete_result = j1.delete_relationship(relationship_id=relationship_id)
186-
print(f"Deleted relationship: {delete_result['relationship']['_id']}\n")
187-
188-
# 2. Deletion with timestamp
189-
print("2. Deleting with specific timestamp:")
190-
j1.delete_relationship(
184+
delete_result = j1.delete_relationship(
191185
relationship_id=relationship_id,
192-
timestamp=int(time.time()) * 1000
186+
from_entity_id=from_entity_id,
187+
to_entity_id=to_entity_id
193188
)
194-
print(f"Deleted with timestamp\n")
189+
print(f"Deleted relationship: {delete_result['relationship']['_id']}\n")
195190

196191
def relationship_lifecycle_example(j1, from_entity_id, to_entity_id):
197192
"""Demonstrate complete relationship lifecycle."""
@@ -234,7 +229,11 @@ def relationship_lifecycle_example(j1, from_entity_id, to_entity_id):
234229

235230
# 4. Delete relationship
236231
print("4. Deleting relationship:")
237-
j1.delete_relationship(relationship_id=relationship_id)
232+
j1.delete_relationship(
233+
relationship_id=relationship_id,
234+
from_entity_id=from_entity_id,
235+
to_entity_id=to_entity_id
236+
)
238237
print("Deleted successfully")
239238

240239
# 5. Verify deletion
@@ -281,14 +280,22 @@ def network_relationship_example(j1):
281280
'bandwidth': '100Mbps'
282281
}
283282
)
284-
relationships.append(relationship['relationship']['_id'])
283+
relationships.append({
284+
'id': relationship['relationship']['_id'],
285+
'from': entities[i],
286+
'to': entities[i+1]
287+
})
285288
print(f"Created connection {i}: {relationship['relationship']['_id']}")
286289

287290
print(f"Created {len(entities)} nodes with {len(relationships)} connections")
288291

289292
# Clean up
290-
for relationship_id in relationships:
291-
j1.delete_relationship(relationship_id=relationship_id)
293+
for rel in relationships:
294+
j1.delete_relationship(
295+
relationship_id=rel['id'],
296+
from_entity_id=rel['from'],
297+
to_entity_id=rel['to']
298+
)
292299
for entity_id in entities:
293300
j1.delete_entity(entity_id=entity_id)
294301

@@ -356,7 +363,11 @@ def access_control_relationship_example(j1):
356363
print("Updated access level to write")
357364

358365
# Clean up
359-
j1.delete_relationship(relationship_id=access_relationship['relationship']['_id'])
366+
j1.delete_relationship(
367+
relationship_id=access_relationship['relationship']['_id'],
368+
from_entity_id=user_entity['entity']['_id'],
369+
to_entity_id=resource_entity['entity']['_id']
370+
)
360371
j1.delete_entity(entity_id=user_entity['entity']['_id'])
361372
j1.delete_entity(entity_id=resource_entity['entity']['_id'])
362373

@@ -397,7 +408,11 @@ def main():
397408
relationships_to_clean = [basic_rel, props_rel, complex_rel]
398409
for rel in relationships_to_clean:
399410
try:
400-
j1.delete_relationship(relationship_id=rel['relationship']['_id'])
411+
j1.delete_relationship(
412+
relationship_id=rel['relationship']['_id'],
413+
from_entity_id=from_entity_id,
414+
to_entity_id=to_entity_id
415+
)
401416
print(f"Cleaned up relationship: {rel['relationship']['_id']}")
402417
except Exception:
403418
# Relationship may already be deleted or not exist

examples/06_advanced_operations.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,11 @@ def bulk_operations_examples(j1):
112112
for rel_data in relationships_to_create:
113113
try:
114114
relationship = j1.create_relationship(**rel_data)
115-
created_relationships.append(relationship['relationship']['_id'])
115+
created_relationships.append({
116+
'id': relationship['relationship']['_id'],
117+
'from': rel_data['from_entity_id'],
118+
'to': rel_data['to_entity_id']
119+
})
116120
print(f"Created relationship: {relationship['relationship']['_id']}")
117121
except Exception as e:
118122
print(f"Error creating relationship: {e}")
@@ -137,29 +141,35 @@ def bulk_operations_examples(j1):
137141

138142
# 4. Bulk relationship updates
139143
print("4. Bulk relationship updates:")
140-
for rel_id in created_relationships:
144+
for rel in created_relationships:
141145
try:
142146
j1.update_relationship(
143-
relationship_id=rel_id,
147+
relationship_id=rel['id'],
148+
from_entity_id=rel['from'],
149+
to_entity_id=rel['to'],
144150
properties={
145151
"lastUpdated": int(time.time()) * 1000,
146152
"tag.BulkUpdated": "true"
147153
}
148154
)
149-
print(f"Updated relationship: {rel_id}")
155+
print(f"Updated relationship: {rel['id']}")
150156
except Exception as e:
151-
print(f"Error updating relationship {rel_id}: {e}")
157+
print(f"Error updating relationship {rel['id']}: {e}")
152158
print()
153159

154160
# 5. Bulk deletion
155161
print("5. Bulk deletion:")
156162
# Delete relationships first
157-
for rel_id in created_relationships:
163+
for rel in created_relationships:
158164
try:
159-
j1.delete_relationship(relationship_id=rel_id)
160-
print(f"Deleted relationship: {rel_id}")
165+
j1.delete_relationship(
166+
relationship_id=rel['id'],
167+
from_entity_id=rel['from'],
168+
to_entity_id=rel['to']
169+
)
170+
print(f"Deleted relationship: {rel['id']}")
161171
except Exception as e:
162-
print(f"Error deleting relationship {rel_id}: {e}")
172+
print(f"Error deleting relationship {rel['id']}: {e}")
163173

164174
# Then delete entities
165175
for entity_id in created_entities:

examples/examples.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@
8585
print(create_relationship_r)
8686

8787
# delete_relationship
88-
delete_relationship_r = j1.delete_relationship(relationship_id=create_relationship_r['relationship']['_id'])
88+
delete_relationship_r = j1.delete_relationship(
89+
relationship_id=create_relationship_r['relationship']['_id'],
90+
from_entity_id=create_r['entity']['_id'],
91+
to_entity_id=create_r_2['entity']['_id']
92+
)
8993
print("delete_relationship()")
9094
print(delete_relationship_r)
9195

jupiterone/client.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -764,13 +764,37 @@ def update_relationship(self, **kwargs: Any) -> Dict[str, Any]:
764764
response = self._execute_query(query=UPDATE_RELATIONSHIP, variables=variables)
765765
return response["data"]["updateRelationship"]
766766

767-
def delete_relationship(self, relationship_id: Optional[str] = None) -> Dict[str, Any]:
767+
def delete_relationship(
768+
self,
769+
relationship_id: Optional[str] = None,
770+
from_entity_id: Optional[str] = None,
771+
to_entity_id: Optional[str] = None,
772+
) -> Dict[str, Any]:
768773
"""Deletes a relationship between two entities.
769774
770775
args:
771-
relationship_id (str): The ID of the relationship
776+
relationship_id (str): The _id of the relationship to delete
777+
from_entity_id (str): The _id of the source entity
778+
to_entity_id (str): The _id of the target entity
772779
"""
773-
variables = {"relationshipId": relationship_id}
780+
if not relationship_id:
781+
raise JupiterOneClientError("relationship_id is required")
782+
if not isinstance(relationship_id, str) or not relationship_id.strip():
783+
raise JupiterOneClientError("relationship_id must be a non-empty string")
784+
785+
if not from_entity_id:
786+
raise JupiterOneClientError("from_entity_id is required")
787+
self._validate_entity_id(from_entity_id, "from_entity_id")
788+
789+
if not to_entity_id:
790+
raise JupiterOneClientError("to_entity_id is required")
791+
self._validate_entity_id(to_entity_id, "to_entity_id")
792+
793+
variables: Dict[str, Any] = {
794+
"relationshipId": relationship_id,
795+
"fromEntityId": from_entity_id,
796+
"toEntityId": to_entity_id,
797+
}
774798

775799
response = self._execute_query(DELETE_RELATIONSHIP, variables=variables)
776800
return response["data"]["deleteRelationship"]

jupiterone/constants.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,16 @@
117117
}
118118
"""
119119
DELETE_RELATIONSHIP = """
120-
mutation DeleteRelationship($relationshipId: String! $timestamp: Long) {
121-
deleteRelationship (relationshipId: $relationshipId, timestamp: $timestamp) {
120+
mutation DeleteRelationship(
121+
$relationshipId: String!
122+
$fromEntityId: String!
123+
$toEntityId: String!
124+
) {
125+
deleteRelationship(
126+
relationshipId: $relationshipId
127+
fromEntityId: $fromEntityId
128+
toEntityId: $toEntityId
129+
) {
122130
relationship {
123131
_id
124132
}

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setup(
77
name="jupiterone",
8-
version="2.1.0",
8+
version="2.2.0",
99
description="A Python client for the JupiterOne API",
1010
license="MIT License",
1111
author="JupiterOne",

tests/test_delete_relationship.py

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,71 @@
33
import responses
44

55
from jupiterone.client import JupiterOneClient
6-
from jupiterone.constants import CREATE_ENTITY
6+
from jupiterone.errors import JupiterOneClientError
77

88

9-
@responses.activate
10-
def test_tree_query_v1():
9+
MOCK_RESPONSE = {
10+
'data': {
11+
'deleteRelationship': {
12+
'relationship': {
13+
'_id': '1'
14+
},
15+
'edge': {
16+
'id': '1',
17+
'toVertexId': '1',
18+
'fromVertexId': '2',
19+
'relationship': {
20+
'_id': '1'
21+
},
22+
'properties': {}
23+
}
24+
}
25+
}
26+
}
27+
1128

29+
def _make_callback(captured_requests):
30+
"""Return a callback that records request bodies and returns a success response."""
1231
def request_callback(request):
13-
headers = {
14-
'Content-Type': 'application/json'
15-
}
32+
captured_requests.append(json.loads(request.body))
33+
return (200, {'Content-Type': 'application/json'}, json.dumps(MOCK_RESPONSE))
34+
return request_callback
1635

17-
response = {
18-
'data': {
19-
'deleteRelationship': {
20-
'relationship': {
21-
'_id': '1'
22-
},
23-
'edge': {
24-
'id': '1',
25-
'toVertexId': '1',
26-
'fromVertexId': '2',
27-
'relationship': {
28-
'_id': '1'
29-
},
30-
'properties': {}
31-
}
32-
}
33-
}
34-
}
3536

36-
return (200, headers, json.dumps(response))
37-
37+
@responses.activate
38+
def test_delete_relationship_sends_required_variables():
39+
captured = []
3840
responses.add_callback(
3941
responses.POST, 'https://graphql.us.jupiterone.io',
40-
callback=request_callback,
42+
callback=_make_callback(captured),
4143
content_type='application/json',
4244
)
4345

4446
j1 = JupiterOneClient(account='testAccount', token='testToken1234567890')
45-
response = j1.delete_relationship('1')
47+
response = j1.delete_relationship(
48+
relationship_id='1',
49+
from_entity_id='2222222222',
50+
to_entity_id='3333333333'
51+
)
52+
53+
assert len(captured) == 1
54+
variables = captured[0]['variables']
55+
assert variables['relationshipId'] == '1'
56+
assert variables['fromEntityId'] == '2222222222'
57+
assert variables['toEntityId'] == '3333333333'
4658

47-
assert type(response) == dict
48-
assert type(response['relationship']) == dict
4959
assert response['relationship']['_id'] == '1'
5060
assert response['edge']['toVertexId'] == '1'
5161
assert response['edge']['fromVertexId'] == '2'
62+
63+
64+
def test_delete_relationship_raises_without_from_entity_id():
65+
j1 = JupiterOneClient(account='testAccount', token='testToken1234567890')
66+
with pytest.raises(JupiterOneClientError, match="from_entity_id is required"):
67+
j1.delete_relationship(relationship_id='1', to_entity_id='3333333333')
68+
69+
70+
def test_delete_relationship_raises_without_to_entity_id():
71+
j1 = JupiterOneClient(account='testAccount', token='testToken1234567890')
72+
with pytest.raises(JupiterOneClientError, match="to_entity_id is required"):
73+
j1.delete_relationship(relationship_id='1', from_entity_id='2222222222')

0 commit comments

Comments
 (0)