Skip to content

Commit ca1e0bf

Browse files
Embedded objects (#329)
* Allow embedded objects to be submitted * Fix typos
1 parent cef312c commit ca1e0bf

4 files changed

Lines changed: 110 additions & 90 deletions

File tree

terminusdb_client/client/Client.py

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ def __init__(
193193
self._branch = None
194194
self._ref = None
195195
self._repo = None
196+
self._references = {}
196197

197198
# Default headers
198199
self._default_headers = {"user-agent": user_agent}
@@ -1140,7 +1141,10 @@ def _conv_to_dict(self, obj):
11401141
if hasattr(obj, "_isinstance") and obj._isinstance:
11411142
if hasattr(obj.__class__, "_subdocument"):
11421143
raise ValueError("Subdocument cannot be added directly")
1143-
return obj._obj_to_dict()
1144+
(d, refs) = obj._obj_to_dict()
1145+
# merge all refs
1146+
self._references = {**self._references, **refs}
1147+
return d
11441148
else:
11451149
return obj._to_dict()
11461150
else:
@@ -1157,45 +1161,44 @@ def _ref_extract(self, target_key, search_item):
11571161
for item in value:
11581162
yield from self._ref_extract(target_key, item)
11591163

1164+
def _unseen(self, seen):
1165+
unseen = []
1166+
for key in self._references:
1167+
if key not in seen:
1168+
unseen.append(self._references[key])
1169+
return unseen
1170+
11601171
def _convert_document(self, document, graph_type):
1161-
if isinstance(document, list):
1162-
new_doc = []
1163-
captured = []
1164-
referenced = []
1172+
if not isinstance(document, list):
1173+
document = [document]
11651174

1175+
seen = {}
1176+
objects = []
1177+
while document != []:
11661178
for item in document:
1179+
if hasattr(item, "to_dict") and graph_type != "schema":
1180+
raise InterfaceError(
1181+
"Inserting WOQLSchema object into non-schema graph."
1182+
)
11671183
item_dict = self._conv_to_dict(item)
1168-
new_doc.append(item_dict)
1169-
item_capture = item_dict.get("@capture")
1170-
if item_capture:
1171-
captured.append(item_capture)
1172-
referenced += list(self._ref_extract("@ref", item_dict))
1184+
if hasattr(item, "_capture"):
1185+
seen[item._capture] = item_dict
1186+
else:
1187+
if isinstance(item_dict, list):
1188+
objects += item_dict
1189+
else:
1190+
objects.append(item_dict)
11731191

1174-
referenced = list(set(referenced))
1192+
document = self._unseen(seen)
11751193

1176-
for item in referenced:
1177-
if item not in captured:
1178-
raise ValueError(
1179-
f"{item} is referenced but not captured. Seems you forgot to submit one or more object(s)."
1180-
)
1181-
else:
1182-
if hasattr(document, "to_dict") and graph_type != "schema":
1183-
raise InterfaceError(
1184-
"Inserting WOQLSchema object into non-schema graph."
1185-
)
1186-
new_doc = self._conv_to_dict(document)
1187-
if isinstance(new_doc, dict) and list(self._ref_extract("@ref", new_doc)):
1188-
raise ValueError(
1189-
"There are uncaptured references. Seems you forgot to submit one or more object(s)."
1190-
)
1191-
return new_doc
1194+
return list(seen.values()) + objects
11921195

11931196
def insert_document(
11941197
self,
11951198
document: Union[
11961199
dict,
11971200
List[dict],
1198-
"WOQLSchema", # noqa:F821
1201+
"Schema", # noqa:F821
11991202
"DocumentTemplate", # noqa:F821
12001203
List["DocumentTemplate"], # noqa:F821
12011204
],
@@ -1249,12 +1252,14 @@ def insert_document(
12491252
if last_data_version is not None:
12501253
headers["TerminusDB-Data-Version"] = last_data_version
12511254

1255+
# make sure we track only internal references
1256+
self._references = {}
12521257
new_doc = self._convert_document(document, graph_type)
1258+
all_docs = list(self._references.values())
1259+
self._references = {}
12531260

12541261
if len(new_doc) == 0:
12551262
return
1256-
elif not isinstance(new_doc, list):
1257-
new_doc = [new_doc]
12581263

12591264
if full_replace:
12601265
if new_doc[0].get("@type") != "@context":
@@ -1289,18 +1294,18 @@ def insert_document(
12891294
auth=self._auth(),
12901295
)
12911296
result = json.loads(_finish_response(result))
1292-
if isinstance(document, list):
1293-
for idx, item in enumerate(document):
1297+
if isinstance(all_docs, list):
1298+
for idx, item in enumerate(all_docs):
12941299
if hasattr(item, "_obj_to_dict") and not hasattr(item, "_backend_id"):
1295-
item._backend_id = result[idx][len("terminusdb:///data/") :]
1300+
item._backend_id = result[idx]
12961301
return result
12971302

12981303
def replace_document(
12991304
self,
13001305
document: Union[
13011306
dict,
13021307
List[dict],
1303-
"WOQLSchema", # noqa:F821
1308+
"Schema", # noqa:F821
13041309
"DocumentTemplate", # noqa:F821
13051310
List["DocumentTemplate"], # noqa:F821
13061311
],
@@ -1346,7 +1351,10 @@ def replace_document(
13461351
if last_data_version is not None:
13471352
headers["TerminusDB-Data-Version"] = last_data_version
13481353

1354+
self._references = {}
13491355
new_doc = self._convert_document(document, graph_type)
1356+
all_docs = list(self._references.values())
1357+
self._references = {}
13501358

13511359
json_string = json.dumps(new_doc).encode("utf-8")
13521360
if compress != "never" and len(json_string) > compress:
@@ -1369,8 +1377,8 @@ def replace_document(
13691377
auth=self._auth(),
13701378
)
13711379
result = json.loads(_finish_response(result))
1372-
if isinstance(document, list):
1373-
for idx, item in enumerate(document):
1380+
if isinstance(all_docs, list):
1381+
for idx, item in enumerate(all_docs):
13741382
if hasattr(item, "_obj_to_dict") and not hasattr(item, "_backend_id"):
13751383
item._backend_id = result[idx][len("terminusdb:///data/") :]
13761384
return result
@@ -1380,7 +1388,7 @@ def update_document(
13801388
document: Union[
13811389
dict,
13821390
List[dict],
1383-
"WOQLSchema", # noqa:F821
1391+
"Schema", # noqa:F821
13841392
"DocumentTemplate", # noqa:F821
13851393
List["DocumentTemplate"], # noqa:F821
13861394
],
@@ -1449,7 +1457,7 @@ def delete_document(
14491457
document = [document]
14501458
for doc in document:
14511459
if hasattr(doc, "_obj_to_dict"):
1452-
doc = doc._obj_to_dict()
1460+
(doc, refs) = doc._obj_to_dict()
14531461
if isinstance(doc, dict) and doc.get("@id"):
14541462
doc_id.append(doc.get("@id"))
14551463
elif isinstance(doc, str):
@@ -1996,7 +2004,7 @@ def squash(
19962004
self.reset(commit_id)
19972005
return commit_id
19982006

1999-
def _convert_diff_dcoument(self, document):
2007+
def _convert_diff_document(self, document):
20002008
if isinstance(document, list):
20012009
new_doc = []
20022010
for item in document:
@@ -2012,15 +2020,15 @@ def diff(
20122020
str,
20132021
dict,
20142022
List[dict],
2015-
"WOQLSchema", # noqa:F821
2023+
"Schema", # noqa:F821
20162024
"DocumentTemplate", # noqa:F821
20172025
List["DocumentTemplate"], # noqa:F821
20182026
],
20192027
after: Union[
20202028
str,
20212029
dict,
20222030
List[dict],
2023-
"WOQLSchema", # noqa:F821
2031+
"Schema", # noqa:F821
20242032
"DocumentTemplate", # noqa:F821
20252033
List["DocumentTemplate"], # noqa:F821
20262034
],
@@ -2047,7 +2055,7 @@ def diff(
20472055
if isinstance(item, str):
20482056
request_dict[f"{key}_data_version"] = item
20492057
else:
2050-
request_dict[key] = self._convert_diff_dcoument(item)
2058+
request_dict[key] = self._convert_diff_document(item)
20512059
if document_id is not None:
20522060
if "before_data_version" in request_dict:
20532061
if document_id[: len("terminusdb:///data")] == "terminusdb:///data":
@@ -2084,7 +2092,7 @@ def patch(
20842092
before: Union[
20852093
dict,
20862094
List[dict],
2087-
"WOQLSchema", # noqa:F821
2095+
"Schema", # noqa:F821
20882096
"DocumentTemplate", # noqa:F821
20892097
List["DocumentTemplate"], # noqa:F821
20902098
],
@@ -2109,7 +2117,7 @@ def patch(
21092117
'{ "@id" : "Person/Jane", "@type" : Person", "name" : "Janine"}'"""
21102118

21112119
request_dict = {
2112-
"before": self._convert_diff_dcoument(before),
2120+
"before": self._convert_diff_document(before),
21132121
"patch": patch.content,
21142122
}
21152123

terminusdb_client/schema/schema.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ def _id(self, custom_id):
263263
f"Customized id is not allowed. {str(self.__class__)} is a subdocument or has set id key scheme."
264264
)
265265

266-
def _embeded_rep(self):
266+
def _embedded_rep(self):
267267
"""get representation for embedding as object property"""
268268
if hasattr(self.__class__, "_subdocument"):
269269
return self._obj_to_dict()
@@ -280,23 +280,40 @@ def _obj_to_dict(self, skip_checking=False):
280280
result["@id"] = self._id
281281
elif not hasattr(self, "_subdocument"):
282282
result["@capture"] = self._capture
283-
# elif hasattr(self.__class__, "_key") and hasattr(self.__class__._key, "idgen"):
284-
# result["@id"] = self.__class__._key.idgen(self)
285283

284+
references = {}
286285
for item in self._annotations.keys():
287286
if hasattr(self, item):
288287
the_item = eval(f"self.{item}") # noqa: S307
289288
if the_item is not None:
290289
# object properties
291-
if hasattr(the_item, "_embeded_rep"):
292-
result[item] = the_item._embeded_rep()
290+
if hasattr(the_item, "_embedded_rep"):
291+
ref_obj = the_item._embedded_rep()
292+
if '@ref' in ref_obj:
293+
references[ref_obj['@ref']] = the_item
294+
elif '@id' in ref_obj:
295+
pass
296+
else:
297+
(sub_item, refs) = ref_obj
298+
references = {**references, **refs}
299+
ref_obj = sub_item
300+
result[item] = ref_obj
293301
# handle list and set (set end up passing as list for jsonlize)
294302
elif isinstance(the_item, (list, set)):
295303
new_item = []
296304
for sub_item in the_item:
297305
# inner is object properties
298-
if hasattr(sub_item, "_embeded_rep"):
299-
new_item.append(sub_item._embeded_rep())
306+
if hasattr(sub_item, "_embedded_rep"):
307+
ref_obj = sub_item._embedded_rep()
308+
if '@ref' in ref_obj:
309+
references[ref_obj['@ref']] = sub_item
310+
elif '@id' in ref_obj:
311+
pass
312+
else:
313+
(sub_item, refs) = ref_obj
314+
references = {**references, **refs}
315+
ref_obj = sub_item
316+
new_item.append(ref_obj)
300317
# inner is Enum
301318
elif isinstance(sub_item, Enum):
302319
new_item.append(str(sub_item))
@@ -310,7 +327,7 @@ def _obj_to_dict(self, skip_checking=False):
310327
result[item] = str(the_item)
311328
else:
312329
result[item] = wt.datetime_to_woql(the_item)
313-
return result
330+
return (result, references)
314331

315332

316333
class EnumMetaTemplate(EnumMeta):

terminusdb_client/tests/integration_tests/test_schema.py

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -88,25 +88,11 @@ def test_insert_cheuk(docker_url, test_schema):
8888

8989
client = Client(docker_url, user_agent=test_user_agent)
9090
client.connect(db="test_docapi")
91-
# client.create_database("test_docapi")
92-
# print(cheuk._obj_to_dict())
9391
with pytest.raises(ValueError) as error:
9492
client.insert_document(home)
9593
assert str(error.value) == "Subdocument cannot be added directly"
96-
with pytest.raises(ValueError) as error:
97-
client.insert_document([cheuk])
98-
assert (
99-
str(error.value)
100-
== f"{uk._capture} is referenced but not captured. Seems you forgot to submit one or more object(s)."
101-
)
102-
with pytest.raises(ValueError) as error:
103-
client.insert_document(cheuk)
104-
assert (
105-
str(error.value)
106-
== "There are uncaptured references. Seems you forgot to submit one or more object(s)."
107-
)
10894
assert cheuk._id is None and uk._id is None
109-
client.insert_document([uk, cheuk], commit_msg="Adding cheuk")
95+
client.insert_document([cheuk], commit_msg="Adding cheuk")
11096
assert cheuk._backend_id and cheuk._id
11197
assert uk._backend_id and uk._id
11298
result = client.get_all_documents()
@@ -134,7 +120,7 @@ def test_getting_and_deleting_cheuk(docker_url):
134120
cheuk = new_schema.import_objects(
135121
client.get_documents_by_type("Employee", as_list=True)
136122
)[0]
137-
result = cheuk._obj_to_dict()
123+
result = cheuk._obj_to_dict()[0]
138124
assert result["address_of"]["postal_code"] == "A12 345"
139125
assert result["address_of"]["street"] == "123 Abc Street"
140126
assert result["name"] == "Cheuk"
@@ -183,25 +169,13 @@ def test_insert_cheuk_again(docker_url, test_schema):
183169
cheuk.member_of = Team.information_technology
184170
cheuk._id = "Cheuk is back"
185171

186-
with pytest.raises(ValueError) as error:
187-
client.update_document([uk])
188-
assert (
189-
str(error.value)
190-
== f"{location._capture} is referenced but not captured. Seems you forgot to submit one or more object(s)."
191-
)
192-
with pytest.raises(ValueError) as error:
193-
client.insert_document(uk)
194-
assert (
195-
str(error.value)
196-
== "There are uncaptured references. Seems you forgot to submit one or more object(s)."
197-
)
198-
199172
client.update_document([location, uk, cheuk], commit_msg="Adding cheuk again")
200173
assert location._backend_id and location._id
201174
location.x = -0.7
202175
result = client.replace_document([location], commit_msg="Fixing location")
203176
assert len(result) == 1
204177
result = client.get_all_documents()
178+
205179
for item in result:
206180
if item.get("@type") == "Country":
207181
assert item["name"] == "United Kingdom"

0 commit comments

Comments
 (0)