kobai-sdk 0.3.0rc1__py3-none-any.whl → 0.3.0rc2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kobai-sdk might be problematic. Click here for more details.

kobai/mobi.py ADDED
@@ -0,0 +1,682 @@
1
+ import requests
2
+ import urllib.parse
3
+ import json
4
+ import base64
5
+ from random import randrange
6
+ from requests_toolbelt.multipart.encoder import MultipartEncoder
7
+
8
+ from .mobi_config import MobiSettings
9
+
10
+ ##############################
11
+ # Mobi Pull
12
+ ##############################
13
+
14
+ def get_tenant(top_level_ontology_name, mobi_config: MobiSettings):
15
+ #Find Ontology Record
16
+
17
+ ont_record_id = _get_ont_record_by_name(top_level_ontology_name, mobi_config)
18
+ print("Mobi Ontology Record ID:", ont_record_id)
19
+ #Get Deprecated Nodes
20
+
21
+ api_url = mobi_config.mobi_api_url + "/ontologies/" + urllib.parse.quote_plus(ont_record_id) + "/property-ranges"
22
+ response = requests.get(api_url, verify=False, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
23
+
24
+ prop_ranges = response.json()["propertyToRanges"]
25
+
26
+
27
+ api_url = mobi_config.mobi_api_url + "/ontologies/" + urllib.parse.quote_plus(ont_record_id) + "/ontology-stuff"
28
+ response = requests.get(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
29
+
30
+ ########################
31
+ # Deprecated Classes
32
+ ########################
33
+
34
+ deprecated = []
35
+
36
+ try:
37
+ resp_data = response.json()["iriList"]["deprecatedIris"]
38
+ except requests.exceptions.JSONDecodeError:
39
+ resp_data = []
40
+
41
+ for iri in resp_data:
42
+ deprecated.append(iri)
43
+
44
+ try:
45
+ resp_data = response.json()["importedIRIs"]
46
+ except requests.exceptions.JSONDecodeError:
47
+ resp_data = []
48
+
49
+ for o in resp_data:
50
+ for iri in o["deprecatedIris"]:
51
+ deprecated.append(iri)
52
+
53
+ ########################
54
+ # Properties
55
+ ########################
56
+
57
+ data_properties = []
58
+ object_properties = []
59
+
60
+ for p in response.json()["iriList"]["dataProperties"]:
61
+ data_properties.append(p)
62
+
63
+ for p in response.json()["iriList"]["objectProperties"]:
64
+ object_properties.append(p)
65
+
66
+ for o in response.json()["importedIRIs"]:
67
+ for p in o["dataProperties"]:
68
+ data_properties.append(p)
69
+
70
+ for o in response.json()["importedIRIs"]:
71
+ for p in o["objectProperties"]:
72
+ object_properties.append(p)
73
+
74
+ all_properties = data_properties + object_properties
75
+
76
+ ########################
77
+ # Classes and Domains
78
+ ########################
79
+
80
+ domains = {}
81
+ concepts = {}
82
+ prop_domains = {}
83
+
84
+ for ont in _get_classes_by_ont(ont_record_id, mobi_config):
85
+ for c in ont["classes"]:
86
+ if c not in deprecated:
87
+ d = _domain_from_uri(c, mobi_config)
88
+ c_lu = _fix_uri(c, "concept", mobi_config)
89
+
90
+ if d not in domains:
91
+ domains[d] = {"name": d, "concepts": [], "color": ""}
92
+
93
+ #Add Leaf Ontology Concepts
94
+ loc = _parent_uri_from_uri(c)
95
+ loc_lu = _fix_uri(loc, "concept", mobi_config)
96
+ if loc_lu not in concepts:
97
+ name = _name_from_uri(loc, mobi_config)
98
+ concepts[loc_lu] = {"label": name, "domainName": d, "name": name, "uri": loc_lu, "properties": [], "relations": [], "inheritedConcepts": []}
99
+
100
+ #Add Class Concepts
101
+ name = _name_from_uri(c, mobi_config)
102
+ concepts[c_lu] = {"label": name, "domainName": d, "name": name, "uri": c_lu, "properties": [], "relations": [], "inheritedConcepts": [loc_lu]}
103
+
104
+ try:
105
+ resp_data = response.json()["classToAssociatedProperties"]
106
+ except requests.exceptions.JSONDecodeError:
107
+ resp_data = {}
108
+
109
+ class_to_props = resp_data
110
+
111
+ props_with_domain = []
112
+ for c in class_to_props:
113
+ for p in class_to_props[c]:
114
+ props_with_domain.append(p)
115
+
116
+ #for p in resp_data:
117
+ for p in all_properties:
118
+ if p not in props_with_domain:
119
+ leaf_ont_concept = _parent_uri_from_uri(p)
120
+ if leaf_ont_concept not in class_to_props:
121
+ class_to_props[leaf_ont_concept] = []
122
+ class_to_props[leaf_ont_concept].append(p)
123
+
124
+ for c in class_to_props:
125
+ if c not in deprecated:
126
+ for p in class_to_props[c]:
127
+ #loc_lu = _fix_uri(c, "concept", mobi_config)
128
+
129
+ if p in prop_ranges:
130
+ range = prop_ranges[p][0]
131
+ if range not in ["http://www.w3.org/2001/XMLSchema#string", "http://www.w3.org/2001/XMLSchema#number", "http://www.w3.org/2001/XMLSchema#boolean", "http://www.w3.org/2001/XMLSchema#dateTime"]:
132
+ range = "http://www.w3.org/2001/XMLSchema#string"
133
+ else:
134
+ range = "http://www.w3.org/2001/XMLSchema#string"
135
+
136
+ #range_lu = _fix_uri(prop_ranges[p][0], "concept", mobi_config)
137
+
138
+ #if p in prop_domains:
139
+ #for dc in prop_domains[p]:
140
+ #cp = dc + "/" + _label_from_uri(p)
141
+ cp = c + "/" + _label_from_uri(p)
142
+ #dc_lu = _fix_uri(dc, "concept", mobi_config)
143
+ dc_lu = _fix_uri(c, "concept", mobi_config)
144
+ if p in data_properties:
145
+ prop = {"label": _label_from_uri(p), "uri": _fix_uri(cp, "prop", mobi_config), "conceptUri": dc_lu, "propTypeUri": range, "dataClassTags": []}
146
+ if dc_lu in concepts:
147
+ if prop not in concepts[dc_lu]["properties"]:
148
+ concepts[dc_lu]["properties"].append(prop)
149
+ if p in object_properties:
150
+ if p in prop_ranges:
151
+ range_lu = _fix_uri(prop_ranges[p][0], "concept", mobi_config)
152
+ prop = {"label": _label_from_uri(p), "uri": _fix_uri(cp, "prop", mobi_config), "conceptUri": dc_lu, "relationTypeUri": range_lu, "dataClassTags": []}
153
+ if dc_lu in concepts:
154
+ if range_lu in concepts:
155
+ if prop not in concepts[dc_lu]["relations"]:
156
+ concepts[dc_lu]["relations"].append(prop)
157
+ else:
158
+ print("PROPERTY RANGE MISSING", p)
159
+
160
+ try:
161
+ resp_data = response.json()["classHierarchy"]["childMap"]
162
+ except requests.exceptions.JSONDecodeError:
163
+ resp_data = {}
164
+
165
+ for c in resp_data:
166
+ c_lu = _fix_uri(c, "concept", mobi_config)
167
+ for pc in resp_data[c]:
168
+ pc_lu = _fix_uri(pc, "concept", mobi_config)
169
+ if c not in deprecated:
170
+ if c_lu in concepts:
171
+ concepts[c_lu]["inheritedConcepts"].append(pc_lu)
172
+ else:
173
+ print("RELATION TARGET MISSING", c_lu)
174
+
175
+ empty_leaf_concepts = []
176
+ for c in concepts:
177
+ if c.split("#")[1][0] == "_":
178
+ if len(concepts[c]["properties"]) == 0 and len(concepts[c]["relations"]) == 0:
179
+ empty_leaf_concepts.append(c)
180
+ for cc in concepts:
181
+ if c in concepts[cc]["inheritedConcepts"]:
182
+ concepts[cc]["inheritedConcepts"].remove(c)
183
+ else:
184
+ print("KEEPING LEAF ONTOLOGY", c)
185
+ for c in empty_leaf_concepts:
186
+ print("REMOVING LEAF ONTOLOGY", c)
187
+ del concepts[c]
188
+
189
+ tenant = {"solutionId": 0, "model": {"name": "AssetModel", "uri": "http://kobai/" + mobi_config.default_tenant_id + "/AssetModel"}, "tenantId": mobi_config.default_tenant_id, "domains": []}
190
+ tenant_encoded = {"solutionId": 0, "model": {"name": "AssetModel", "uri": "http://kobai/" + mobi_config.default_tenant_id + "/AssetModel"}, "tenantId": mobi_config.default_tenant_id, "domains": []}
191
+ _add_empty_tenant_metadata(tenant)
192
+ _add_empty_tenant_metadata(tenant_encoded)
193
+
194
+ di = 0
195
+ for dk, d in domains.items():
196
+ d['id'] = di
197
+ d['color'] = "#" + str(randrange(222222, 888888))
198
+ d_encoded = {}
199
+ d_encoded['id'] = di
200
+ d_encoded['color'] = "#" + str(randrange(222222, 888888))
201
+ d_encoded['name'] = dk
202
+
203
+ for _, c in concepts.items():
204
+ if dk == c['domainName']:
205
+ cprime = {"uri": c['uri'], "label": c['label'], "relations": c['relations'], "properties": c['properties'], "inheritedConcepts": c['inheritedConcepts']}
206
+ d['concepts'].append(cprime)
207
+ encodedConcepts = base64.b64encode(json.dumps(d['concepts']).encode('ascii')).decode('ascii')
208
+ d_encoded['concepts'] = encodedConcepts
209
+ tenant['domains'].append(d)
210
+ tenant_encoded['domains'].append(d_encoded)
211
+ di += 1
212
+
213
+ return tenant, tenant_encoded
214
+
215
+ def _get_classes_by_ont(ont_record_id, mobi_config):
216
+ api_url = mobi_config.mobi_api_url + "/ontologies/" + urllib.parse.quote_plus(ont_record_id) + "/imported-classes"
217
+ response = requests.get(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
218
+
219
+ data = []
220
+ try:
221
+ resp_data = response.json()
222
+ except requests.exceptions.JSONDecodeError:
223
+ resp_data = []
224
+
225
+ #for ont in response.json():
226
+ for ont in resp_data:
227
+ record = {}
228
+ record["id"] = _trim_trailing_slash(ont["id"])
229
+ record["classes"] = []
230
+ for c in ont["classes"]:
231
+ record["classes"].append(c)
232
+ data.append(record)
233
+
234
+ api_url = mobi_config.mobi_api_url + "/ontologies/" + urllib.parse.quote_plus(ont_record_id) + "/classes"
235
+ response = requests.get(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
236
+
237
+ try:
238
+ resp_data = response.json()
239
+ except requests.exceptions.JSONDecodeError:
240
+ resp_data = []
241
+
242
+ record = {}
243
+ if len(resp_data) == 0:
244
+ return data
245
+ record["id"] = _parent_uri_from_uri(_trim_trailing_slash(response.json()[0]["@id"]))
246
+ record["classes"] = []
247
+
248
+ for c in resp_data:
249
+ record["classes"].append(c["@id"])
250
+ data.append(record)
251
+
252
+ return data
253
+
254
+ def _get_ont_record_by_name(name, mobi_config):
255
+ api_url = mobi_config.mobi_api_url + "/catalogs/" + urllib.parse.quote_plus(mobi_config.catalog_name) + "/records"
256
+ response = requests.get(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
257
+
258
+ ont_record_id = ""
259
+ for r in response.json():
260
+ if r["http://purl.org/dc/terms/title"][0]["@value"] == name:
261
+ ont_record_id = r["@id"]
262
+ return ont_record_id
263
+
264
+ def _get_ont_record_by_url(url, mobi_config):
265
+ api_url = mobi_config.mobi_api_url + "/catalogs/" + urllib.parse.quote_plus(mobi_config.catalog_name) + "/records"
266
+ response = requests.get(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
267
+
268
+ ont_record_id = ""
269
+ for r in response.json():
270
+ for u in r["http://mobi.com/ontologies/ontology-editor#ontologyIRI"]:
271
+ if url == _trim_trailing_slash(u["@id"]):
272
+ ont_record_id = r["@id"]
273
+ return ont_record_id
274
+
275
+ def _add_empty_tenant_metadata(tenant):
276
+ tenant["dataAccessTags"] = []
277
+ tenant["conceptAccessTags"] = []
278
+ tenant["dataSources"] = []
279
+ tenant["dataSets"] = []
280
+ tenant["collections"] = []
281
+ tenant["visualizations"] = []
282
+ tenant["queries"] = []
283
+ tenant["mappingDefs"] = []
284
+ tenant["dataSourceFileKeys"] = []
285
+ tenant["apiQueryProfiles"] = []
286
+ tenant["collectionVizs"] = []
287
+ tenant["collectionVizOrders"] = []
288
+ tenant["queryDataTags"] = []
289
+ tenant["queryCalcs"] = []
290
+ tenant["dataSourceSettings"] = []
291
+ tenant["publishedAPIs"] = []
292
+ tenant["scenarios"] = []
293
+
294
+ ##############################
295
+ # Mobi Replace
296
+ ##############################
297
+
298
+ def replace_tenant_to_mobi(kobai_tenant, top_level_ontology, mobi_config: MobiSettings):
299
+ json_ld = _create_jsonld(kobai_tenant, top_level_ontology)
300
+ _post_model(json_ld, top_level_ontology, mobi_config)
301
+
302
+ def replace_tenant_to_file(kobai_tenant, top_level_ontology):
303
+ return _create_jsonld(kobai_tenant, top_level_ontology)
304
+
305
+ def _create_jsonld(kobai_tenant, top_level_ontology):
306
+ output_json = []
307
+ uri = kobai_tenant["model"]["uri"]
308
+ uri = uri.replace("AssetModel", top_level_ontology)
309
+
310
+ group = {
311
+ "@id": uri,
312
+ "@type": ["http://www.w3.org/2002/07/owl#Ontology"],
313
+ "http://purl.org/dc/terms/description": [{"@value": "This model was exported from Kobai."}],
314
+ "http://purl.org/dc/terms/title": [{"@value": top_level_ontology}]
315
+ }
316
+ output_json.append(group)
317
+
318
+
319
+ for dom in kobai_tenant["domains"]:
320
+ for con in dom["concepts"]:
321
+
322
+ group = {
323
+ "@id": con["uri"].replace("AssetModel", top_level_ontology),
324
+ "@type": ["http://www.w3.org/2002/07/owl#Class"],
325
+ "http://purl.org/dc/terms/title": [{"@value": con["label"]}]
326
+ }
327
+ if len(con["inheritedConcepts"]) > 0:
328
+ group["http://www.w3.org/2000/01/rdf-schema#subClassOf"] = []
329
+ for parent in con["inheritedConcepts"]:
330
+ group["http://www.w3.org/2000/01/rdf-schema#subClassOf"].append(
331
+ {"@id": parent.replace("AssetModel", top_level_ontology)}
332
+ )
333
+ output_json.append(group)
334
+
335
+ for prop in con["properties"]:
336
+ group = {
337
+ "@id": prop["uri"].replace("AssetModel", top_level_ontology),
338
+ "@type": ["http://www.w3.org/2002/07/owl#DatatypeProperty"],
339
+ "http://purl.org/dc/terms/title": [{"@value": prop["label"]}],
340
+ "http://www.w3.org/2000/01/rdf-schema#domain": [{"@id": con["uri"].replace("AssetModel", top_level_ontology)}],
341
+ "http://www.w3.org/2000/01/rdf-schema#range": [{"@id": prop["propTypeUri"]}]
342
+ }
343
+ output_json.append(group)
344
+
345
+ for rel in con["relations"]:
346
+ group = {
347
+ "@id": rel["uri"].replace("AssetModel", top_level_ontology),
348
+ "@type": ["http://www.w3.org/2002/07/owl#ObjectProperty"],
349
+ "http://purl.org/dc/terms/title": [{"@value": rel["label"]}],
350
+ "http://www.w3.org/2000/01/rdf-schema#domain": [{"@id": con["uri"].replace("AssetModel", top_level_ontology)}],
351
+ "http://www.w3.org/2000/01/rdf-schema#range": [{"@id": rel["relationTypeUri"].replace("AssetModel", top_level_ontology)}]
352
+ }
353
+ output_json.append(group)
354
+ return output_json
355
+
356
+ def _post_model(tenant_json, top_level_ontology, mobi_config):
357
+
358
+ mp = MultipartEncoder(fields={
359
+ "title": top_level_ontology,
360
+ "description": "This model was exported from Kobai.",
361
+ "json": json.dumps(tenant_json)
362
+ })
363
+ h = {"Content-type": mp.content_type}
364
+
365
+ api_url = mobi_config.mobi_api_url + "/ontologies"
366
+ response = requests.post(
367
+ api_url,
368
+ headers = h,
369
+ data = mp,
370
+ verify=False,
371
+ timeout=5000,
372
+ auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password)
373
+ )
374
+ print("Upload Status", response.status_code)
375
+ if response.status_code != 201:
376
+ print(response.text)
377
+
378
+ ##############################
379
+ # Mobi Update
380
+ ##############################
381
+
382
+ def update_tenant(kobai_tenant, top_level_ontology_name, mobi_config: MobiSettings):
383
+ record_id = _get_ont_record_by_name(top_level_ontology_name, mobi_config)
384
+
385
+ api_url = mobi_config.mobi_api_url + "/catalogs/" + urllib.parse.quote_plus(mobi_config.catalog_name) + "/records/" + urllib.parse.quote_plus(record_id) + "/branches"
386
+ response = requests.get(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
387
+
388
+ classes = _get_classes_by_ont(record_id, mobi_config)
389
+ ontology_by_class = {}
390
+ for o in classes:
391
+ for c in o["classes"]:
392
+ ontology_by_class[c] = o["id"]
393
+
394
+ _, mobi_tenant = get_tenant(top_level_ontology_name, mobi_config)
395
+
396
+ change_json = _compare_tenants(kobai_tenant, mobi_tenant, classes, mobi_config)
397
+
398
+ #################################
399
+ # Apply changes with Mobi API calls
400
+ #################################
401
+
402
+ for o in change_json:
403
+ if not change_json[o]["changed"]:
404
+ continue
405
+
406
+ ont_record_id = _get_ont_record_by_url(o, mobi_config)
407
+ if ont_record_id == "":
408
+ continue
409
+
410
+ branch_id = _get_or_create_branch_by_record(ont_record_id, "kobai_dev", mobi_config)
411
+ master_branch_id = _get_or_create_branch_by_record(ont_record_id, "MASTER", mobi_config)
412
+
413
+ for change in change_json[o]["class"]:
414
+ _stage_changes([change["mobi"]], ont_record_id, mobi_config)
415
+ _commit_changes("Kobai added class " + change["mobi"]["http://purl.org/dc/terms/title"][0]["@value"], ont_record_id, branch_id, mobi_config)
416
+ for change in change_json[o]["property"]:
417
+ _stage_changes([change["mobi"]], ont_record_id, mobi_config)
418
+ _commit_changes("Kobai added property " + change["mobi"]["http://purl.org/dc/terms/title"][0]["@value"], ont_record_id, branch_id, mobi_config)
419
+
420
+ api_url = mobi_config.mobi_api_url + "/merge-requests"
421
+ pd = {"title": "Kobai Change from kobai-dev to master", "recordId": ont_record_id, "sourceBranchId": branch_id, "targetBranchId": master_branch_id, "assignees": ["admin"], "removeSource": "true"}
422
+ response = requests.post(api_url, verify=False, timeout=5000, params=pd, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
423
+ #print(response.status_code)
424
+
425
+ def _compare_tenants(kobai_tenant, mobi_tenant, classes, mobi_config):
426
+ existing_concepts = []
427
+ existing_relations = {}
428
+ existing_properties = {}
429
+
430
+ for dom in mobi_tenant["domains"]:
431
+ conText = base64.b64decode(dom['concepts']).decode('UTF-8')
432
+ cons = json.loads(conText)
433
+ for con in cons:
434
+ existing_concepts.append(con["uri"])
435
+ existing_properties[con["uri"]] = []
436
+ for prop in con["properties"]:
437
+ existing_properties[con["uri"]].append(prop["uri"])
438
+ existing_relations[con["uri"]] = []
439
+ for rel in con["relations"]:
440
+ existing_relations[con["uri"]].append(rel["uri"])
441
+
442
+ new_concepts = []
443
+ new_relations = {}
444
+ new_properties = {}
445
+
446
+ tenantId = kobai_tenant["tenantId"]
447
+ for dom in kobai_tenant["domains"]:
448
+ conText = base64.b64decode(dom['concepts']).decode('UTF-8')
449
+ cons = json.loads(conText)
450
+ for con in cons:
451
+ con_d = con["uri"].replace(tenantId, mobi_config.default_tenant_id)
452
+ if con_d not in existing_concepts:
453
+ print("New Class Detected")
454
+ new_concepts.append(con)
455
+ for prop in con["properties"]:
456
+ prop_d = prop["uri"].replace(tenantId, mobi_config.default_tenant_id)
457
+ if con_d in existing_properties:
458
+ if prop_d not in existing_properties[con_d]:
459
+ print("New Prop Detected")
460
+ if con_d not in new_properties:
461
+ new_properties[con_d] = []
462
+ new_properties[con_d].append(prop)
463
+ print(prop)
464
+ else:
465
+ print("New Property due to New Concept")
466
+ for rel in con["relations"]:
467
+ rel_d = rel["uri"].replace(tenantId, mobi_config.default_tenant_id)
468
+ if con_d in existing_relations:
469
+ if rel_d not in existing_relations[con_d]:
470
+ print("New Rel Detected")
471
+ if con_d not in new_relations:
472
+ new_relations[con_d] = []
473
+ new_relations[con_d].append(rel)
474
+ else:
475
+ print("New Relation due to New Concept")
476
+
477
+ change_json = {}
478
+ for o in classes:
479
+ ont_exist = o["id"]
480
+ change_json[ont_exist] = {}
481
+ change_json[ont_exist]["exists"] = True
482
+ change_json[ont_exist]["changed"] = False
483
+ change_json[ont_exist]["class"] = []
484
+ change_json[ont_exist]["property"] = []
485
+ change_json[ont_exist]["relation"] = []
486
+
487
+ #################################
488
+ # Identify and capture changes associated to Mobi ontology
489
+ #################################
490
+ for c in new_concepts:
491
+ c_d = c["uri"].replace(tenantId, mobi_config.default_tenant_id)
492
+ ont_sig = c_d.replace("http://kobai/" + mobi_config.default_tenant_id + "/AssetModel/", "").replace("#", "/").replace("_", "/")
493
+ ont_sig = "/".join(ont_sig.split("/")[:-1])
494
+ ont = ""
495
+ for o in classes:
496
+ ont_exist = o["id"]
497
+ if ont_sig == _get_ont_sig_from_ont(ont_exist, len(ont_sig.split("/"))):
498
+ change_json[ont_exist]["class"].append({"type": "new", "kobai": c, "mobi": {}})
499
+
500
+ for c in new_properties:
501
+ for p in new_properties[c]:
502
+ ont_sig = _get_ont_sig_from_concept(tenantId, c, mobi_config)
503
+ for o in classes:
504
+ ont_exist = o["id"]
505
+ if ont_sig == _get_ont_sig_from_ont(ont_exist, len(ont_sig.split("/"))):
506
+ change_json[ont_exist]["property"].append({"type": "new", "kobai": p, "mobi": {}})
507
+
508
+ for c in new_relations:
509
+ for r in new_properties[c]:
510
+ ont_sig = _get_ont_sig_from_concept(tenantId, c, mobi_config)
511
+ for o in classes:
512
+ ont_exist = o["id"]
513
+ if ont_sig == _get_ont_sig_from_ont(ont_exist, len(ont_sig.split("/"))):
514
+ change_json[ont_exist]["relation"].append({"type": "new", "kobai": r, "mobi": {}})
515
+
516
+ #################################
517
+ # Generate Mobi json for every change
518
+ #################################
519
+ for ont in change_json:
520
+ changed = False
521
+ for i, change in enumerate(change_json[ont]["class"]):
522
+ c = change["kobai"]
523
+ c_json = {}
524
+ c_json["@id"] = ont + "/" + c["label"].split("_")[-1]
525
+ c_json["@type"] = [ "http://www.w3.org/2002/07/owl#Class" ]
526
+ c_json["http://www.w3.org/2000/01/rdf-schema#label"] = [{"@value": c["label"].split("_")[-1]}]
527
+ c_json["http://purl.org/dc/terms/title"] = [{"@value": c["label"].split("_")[-1]}]
528
+ c_json["http://www.w3.org/2000/01/rdf-schema#subClassOf"] = []
529
+ #c_json["http://www.w3.org/2002/07/owl#deprecated"] = [{"@value": "true", "@type": "http://www.w3.org/2001/XMLSchema#boolean"}]
530
+ for pc in c["inheritedConcepts"]:
531
+ c_json["http://www.w3.org/2000/01/rdf-schema#subClassOf"].append(pc)
532
+ change_json[ont]["class"][i]["mobi"] = c_json
533
+ changed = True
534
+
535
+ for i, change in enumerate(change_json[ont]["property"]):
536
+ p = change["kobai"]
537
+ p_json = {}
538
+ p_json["@id"] = ont + "/" + p["label"]
539
+ p_json["@type"] = [ "http://www.w3.org/2002/07/owl#Class" ]
540
+ p_json["http://www.w3.org/2000/01/rdf-schema#label"] = [{"@value": p["label"]}]
541
+ p_json["http://purl.org/dc/terms/title"] = [{"@value": p["label"]}]
542
+ p_json["http://www.w3.org/2000/01/rdf-schema#domain"] = [{"@id": ont + "/" + _get_concept_name_from_prop_uri(p["uri"])}]
543
+ p_json["http://www.w3.org/2000/01/rdf-schema#range"] = [{"@id": p["propTypeUri"]}]
544
+ change_json[ont]["property"][i]["mobi"] = p_json
545
+ changed = True
546
+
547
+ if changed is True:
548
+ change_json[ont]["changed"] = True
549
+
550
+ return change_json
551
+
552
+ def _stage_changes(changes, ont_record_id, mobi_config):
553
+ api_url = mobi_config.mobi_api_url + "/catalogs/" + urllib.parse.quote_plus(mobi_config.catalog_name) + "/records/" + urllib.parse.quote_plus(ont_record_id) + "/in-progress-commit"
554
+ response = requests.delete(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
555
+
556
+ api_url = mobi_config.mobi_api_url + "/catalogs/" + urllib.parse.quote_plus(mobi_config.catalog_name) + "/records/" + urllib.parse.quote_plus(ont_record_id) + "/in-progress-commit"
557
+ m = MultipartEncoder(fields={"additions": json.dumps(changes), "deletions": "[]"})
558
+ h = {"Content-type": m.content_type}
559
+ response = requests.put(api_url, verify=False, timeout=5000, data=m, headers=h, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
560
+
561
+ api_url = mobi_config.mobi_api_url + "/catalogs/" + urllib.parse.quote_plus(mobi_config.catalog_name) + "/records/" + urllib.parse.quote_plus(ont_record_id) + "/in-progress-commit"
562
+ response = requests.get(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
563
+
564
+ def _commit_changes(message, ont_record_id, branch_id, mobi_config):
565
+ api_url = mobi_config.mobi_api_url + "/catalogs/" + urllib.parse.quote_plus(mobi_config.catalog_name) + "/records/" + urllib.parse.quote_plus(ont_record_id) + "/branches/" + urllib.parse.quote_plus(branch_id) + "/commits"
566
+ pd = {"message": message}
567
+ response = requests.post(api_url, verify=False, timeout=5000, params=pd, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
568
+
569
+ ##############################
570
+ # Mobi Branch
571
+ ##############################
572
+
573
+ #def jprint(data):
574
+ # json_str = json.dumps(data, indent=4)
575
+
576
+ def _get_branches_by_record(id, mobi_config):
577
+ api_url = mobi_config.mobi_api_url + "/catalogs/" + urllib.parse.quote_plus(mobi_config.catalog_name) + "/records/" + urllib.parse.quote_plus(id) + "/branches"
578
+ response = requests.get(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
579
+ return response.json()
580
+
581
+ def _get_or_create_branch_by_record(id, name, mobi_config):
582
+ branches = _get_branches_by_record(id, mobi_config)
583
+ for b in branches:
584
+ if b["http://purl.org/dc/terms/title"][0]["@value"] == name:
585
+ return b["@id"]
586
+
587
+ commit = ""
588
+ for b in branches:
589
+ if b["http://purl.org/dc/terms/title"][0]["@value"] == "MASTER":
590
+ commit = b["http://mobi.com/ontologies/catalog#head"][0]["@id"]
591
+
592
+ api_url = mobi_config.mobi_api_url + "/catalogs/" + urllib.parse.quote_plus(mobi_config.catalog_name) + "/records/" + urllib.parse.quote_plus(id) + "/branches"
593
+ pd = {"type": "http://mobi.com/ontologies/catalog#Branch", "title": name, "commitId": commit}
594
+ requests.post(api_url, verify=False, timeout=5000, params=pd, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
595
+
596
+ branches = _get_branches_by_record(id, mobi_config)
597
+ for b in branches:
598
+ if b["http://purl.org/dc/terms/title"][0]["@value"] == name:
599
+ return b["@id"]
600
+ return ""
601
+
602
+ ##############################
603
+ # Mobi Parse
604
+ ##############################
605
+ def _get_domain_range(url, mobi_config):
606
+ for d, r in mobi_config.domain_extraction.items():
607
+ if d in url:
608
+ return r
609
+ return {"min": 0, "max": 0}
610
+
611
+ def _parent_uri_from_uri(uri):
612
+ #return "/".join(uri.split("/")[:-1])
613
+ return "/".join(_uri_split(uri)[:-1])
614
+
615
+ def _trim_trailing_slash(uri):
616
+ if uri[-1] == "/":
617
+ return uri[:-1]
618
+ else:
619
+ return uri
620
+
621
+ def _uri_split(uri):
622
+ uri = uri.replace("#", "/")
623
+ return uri.split("/")
624
+
625
+
626
+ ################################
627
+ # Transform from Kobai to Mobi
628
+ ################################
629
+
630
+ def _get_ont_sig_from_concept(tenantId, uri, mobi_config):
631
+ uri = uri.replace(tenantId, mobi_config.default_tenant_id)
632
+ ont_sig = uri.replace("http://kobai/" + mobi_config.default_tenant_id + "/AssetModel/", "").replace("#", "/").replace("_", "/")
633
+ ont_sig = "/".join(ont_sig.split("/")[:-1])
634
+ return ont_sig
635
+
636
+ def _get_ont_sig_from_ont(uri, length):
637
+ return "/".join(uri.split("/")[-length:])
638
+
639
+ def _get_concept_name_from_prop_uri(uri):
640
+ return uri.split("#")[0].split("/")[-1]
641
+ #return uri.split("#")[0].split("_")[-1]
642
+
643
+ ################################
644
+ # Transform from Mobi to Kobai
645
+ ################################
646
+
647
+ def _domain_from_uri(uri, mobi_config):
648
+ #domain = "_".join(uri.split("/")[_get_domain_range(uri, mobi_config)['min']:_get_domain_range(uri, mobi_config)['max']+1])
649
+ domain = "_".join(_uri_split(uri)[_get_domain_range(uri, mobi_config)['min']:_get_domain_range(uri, mobi_config)['max']+1])
650
+ return domain
651
+
652
+ def _name_from_uri(uri, mobi_config):
653
+ #name = "_".join(uri.split("/")[_get_domain_range(uri, mobi_config)['max']+1:])
654
+ name = "_".join(_uri_split(uri)[_get_domain_range(uri, mobi_config)['max']+1:])
655
+ if name == "":
656
+ #name = "_" + "_".join(uri.split("/")[_get_domain_range(uri, mobi_config)['max']:])
657
+ name = "_" + "_".join(_uri_split(uri)[_get_domain_range(uri, mobi_config)['max']:])
658
+ return name
659
+
660
+ def _label_from_uri(uri):
661
+ #return uri.split("/")[-1]
662
+ return _uri_split(uri)[-1]
663
+
664
+ def _fix_uri(uri, type, mobi_config):
665
+ domain = _domain_from_uri(uri, mobi_config)
666
+ name = _name_from_uri(uri, mobi_config)
667
+
668
+ top = "/".join(uri.split("/")[0:_get_domain_range(uri, mobi_config)['min']])
669
+
670
+ if type == "concept":
671
+ uri = domain + "#" + name
672
+ elif type == "prop":
673
+ uri = domain + "/" + "_".join(name.split("_")[0:-1]) + "#" + name.split("_")[-1]
674
+
675
+ for d in mobi_config.domain_extraction:
676
+
677
+ if d in top:
678
+ uri = "http://kobai/" + mobi_config.default_tenant_id + "/AssetModel/" + uri
679
+
680
+ return uri
681
+
682
+
kobai/mobi_config.py ADDED
@@ -0,0 +1,16 @@
1
+ from pydantic_settings import BaseSettings
2
+
3
+ class MobiSettings(BaseSettings):
4
+
5
+ #Application Specific Settings
6
+ domain_extraction: dict = {}
7
+
8
+ #Mobi Server Settings
9
+ mobi_api_url: str = "https://localhost:8443/mobirest"
10
+ mobi_username: str = "admin"
11
+ mobi_password: str = "admin"
12
+
13
+ catalog_name: str = "http://mobi.com/catalog-local"
14
+ default_tenant_id: str = "00000000-0000-0000-0000-000000000000"
15
+
16
+ #settings = MobiSettings()
kobai/tenant_client.py CHANGED
@@ -12,7 +12,8 @@ from langchain_core.language_models.chat_models import BaseChatModel
12
12
  from langchain_core.embeddings import Embeddings
13
13
  from typing import Union
14
14
 
15
- from . import spark_client, databricks_client, ai_query, tenant_api, ai_rag
15
+ from . import spark_client, databricks_client, ai_query, tenant_api, ai_rag, mobi
16
+ from .mobi_config import MobiSettings
16
17
 
17
18
  class TenantClient:
18
19
 
@@ -58,6 +59,122 @@ class TenantClient:
58
59
  self.__api_init_session()
59
60
 
60
61
 
62
+ ########################################
63
+ # Mobi
64
+ ########################################
65
+
66
+ def pull_mobi_to_tenant(self, ontology_name, mobi_config: MobiSettings):
67
+
68
+ """
69
+ Export an ontology from Mobi and import it into a Kobai tenant, replacing the contents of the tenant.
70
+
71
+ Requires that the SDK be authenticated against the target Kobai tenant.
72
+
73
+ Parameters:
74
+ ontology_name (str): The name of the ontology to access in Mobi.
75
+ mobi_config (MobiSettings): Configuration required to access the Mobi service.
76
+ """
77
+
78
+ _, tenant_json_enc = mobi.get_tenant(ontology_name, mobi_config)
79
+ self.__set_tenant_import(tenant_json_enc)
80
+
81
+ def pull_mobi_to_file(self, ontology_name, mobi_config: MobiSettings, file_name, human_readable=False):
82
+
83
+ """
84
+ Export an ontology from Mobi and save it in a Kobai json import file.
85
+
86
+ Requires that the SDK be authenticated against the target Kobai tenant.
87
+
88
+ Parameters:
89
+ ontology_name (str): The name of the ontology to access in Mobi.
90
+ mobi_config (MobiSettings): Configuration required to access the Mobi service.
91
+ file_name (str): File name to give the output (no extension)
92
+ human_readable (bool) OPTIONAL: generate a second, decoded Kobai file.
93
+ """
94
+
95
+ tenant_json, tenant_json_enc = mobi.get_tenant(ontology_name, mobi_config)
96
+
97
+ if ".json" in file_name:
98
+ file_name = file_name.split(".json")[0]
99
+
100
+ with open(f"{file_name}.json", "w") as out_file:
101
+ json.dump(tenant_json_enc, out_file)
102
+
103
+ if human_readable:
104
+ with open(f"{file_name}_decoded.json", "w") as out_file:
105
+ json.dump(tenant_json, out_file)
106
+
107
+ def push_tenant_update_to_mobi(self, ontology_name, mobi_config: MobiSettings):
108
+
109
+ """
110
+ Compare a (modified) Kobai tenant to a Mobi ontology, and generate a Merge Request for the changes.
111
+
112
+ Requires that the SDK be authenticated against the target Kobai tenant.
113
+
114
+ Parameters:
115
+ ontology_name (str): The name of the ontology to access in Mobi.
116
+ mobi_config (MobiSettings): Configuration required to access the Mobi service.
117
+ """
118
+
119
+ tenant_json_enc = self.__get_tenant_export()
120
+ mobi.update_tenant(tenant_json_enc, ontology_name, mobi_config)
121
+
122
+ def push_whole_tenant_to_mobi(self, ontology_name, mobi_config: MobiSettings):
123
+
124
+ """
125
+ Export a tenant from Kobai, and create an ontology in Mobi.
126
+
127
+ Requires that the SDK be authenticated against the target Kobai tenant.
128
+ Requires that an ontology with the same name does not already exist in Mobi.
129
+
130
+ Parameters:
131
+ ontology_name (str): The name of the ontology to create in Mobi.
132
+ mobi_config (MobiSettings): Configuration required to access the Mobi service.
133
+ """
134
+
135
+ tenant_json = self.get_tenant_config()
136
+ mobi.replace_tenant_to_mobi(tenant_json, ontology_name, mobi_config)
137
+
138
+ def push_whole_tenant_to_jsonld_file(self, ontology_name, file_name):
139
+
140
+ """
141
+ Export a tenant from Kobai, and create an ontology in Mobi.
142
+
143
+ Requires that the SDK be authenticated against the target Kobai tenant.
144
+
145
+ Parameters:
146
+ ontology_name (str): The name of the ontology to create in Mobi.
147
+ file_name (str): File name to give the output (no extension)
148
+ """
149
+
150
+ tenant_json = self.get_tenant_config()
151
+ tenant_jsonld = mobi.replace_tenant_to_file(tenant_json, ontology_name)
152
+
153
+ if ".json" in file_name:
154
+ file_name = file_name.split(".json")[0]
155
+
156
+ with open(f"{file_name}.json", "w") as out_file:
157
+ json.dump(tenant_jsonld, out_file)
158
+
159
+ def get_default_mobi_config(self):
160
+
161
+ """
162
+ Returns a default MobiSettings configuration object.
163
+
164
+ Available Fields to Set:
165
+ domain_extraction: Mapping of ontology url structures to Kobai domain names.
166
+ mobi_api_url: url for Mobi service. (ex: https://localhost:8443/mobirest)
167
+ mobi_username: User name for Mobi service.
168
+ mobi_password: Password for Mobi service.
169
+ """
170
+
171
+ return MobiSettings()
172
+
173
+ def __set_tenant_import(self, tenant_json_enc):
174
+ self.api_client._TenantAPI__run_post_files(
175
+ '/data-svcs/solution/snapshot/import/upload',
176
+ {'file': json.dumps(tenant_json_enc)}
177
+ )
61
178
 
62
179
  ########################################
63
180
  # MS Entra Auth
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kobai-sdk
3
- Version: 0.3.0rc1
3
+ Version: 0.3.0rc2
4
4
  Summary: A package that enables interaction with a Kobai tenant.
5
5
  Author-email: Ryan Oattes <ryan@kobai.io>
6
6
  License: Apache License
@@ -3,12 +3,14 @@ kobai/ai_query.py,sha256=xZh_OyakU01gIrnzaW4v_TdfzG51nPu0ntXJw1WEnvw,9424
3
3
  kobai/ai_rag.py,sha256=8B3HM4GoGVrgxJG678NN4vGaDwZRYnQiK5SCGiMIYkM,15186
4
4
  kobai/databricks_client.py,sha256=fyqqMly2Qm0r1AHWsQjkYeNsDdH0G1JSgTkF9KJ55qA,2118
5
5
  kobai/demo_tenant_client.py,sha256=wlNc-bdI2wotRXo8ppUOalv4hYdBlek_WzJNARZV-AE,9293
6
+ kobai/mobi.py,sha256=FKIyVcmDoQetH00Hg9ajOPmO029IGpL1sttgwvB8-Pc,29862
7
+ kobai/mobi_config.py,sha256=BpW1cn7BgF5iTYz_bB0HMpXflfql0PYlQ61Y_Uh1Vns,454
6
8
  kobai/ms_authenticate.py,sha256=rlmhtvAaSRBlYmvIBy5epMVa4MBGBLPaMwawu1T_xDQ,2252
7
9
  kobai/spark_client.py,sha256=opM_F-4Ut5Hq5zZjWMuLvUps9sDULvyPNZHXGL8dW1k,776
8
10
  kobai/tenant_api.py,sha256=Q5yuFd9_V4lo3LWzvYEEO3LpDRWFgQD4TlRPXDTGbiE,4368
9
- kobai/tenant_client.py,sha256=o2bifvmYwxL3_gpRMJaVRXrt9CN8Kfcx7p5r3jR9aGg,39415
10
- kobai_sdk-0.3.0rc1.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
11
- kobai_sdk-0.3.0rc1.dist-info/METADATA,sha256=9yGBoPdo2wWB_wYmAGG00dEY-4xqP7p9Grm1wUWcWfg,19263
12
- kobai_sdk-0.3.0rc1.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
13
- kobai_sdk-0.3.0rc1.dist-info/top_level.txt,sha256=ns1El3BrTTHKvoAgU1XtiSaVIudYeCXbEEUVY8HFDZ4,6
14
- kobai_sdk-0.3.0rc1.dist-info/RECORD,,
11
+ kobai/tenant_client.py,sha256=Zb9XKOM23u311nbnBaB8JQ72nrPVA7LyOmMn526rC7U,43870
12
+ kobai_sdk-0.3.0rc2.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
13
+ kobai_sdk-0.3.0rc2.dist-info/METADATA,sha256=g-g_YgrjB32fVx_gsE9uPpmZXm2B6iBMehsVycgAj7o,19263
14
+ kobai_sdk-0.3.0rc2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ kobai_sdk-0.3.0rc2.dist-info/top_level.txt,sha256=ns1El3BrTTHKvoAgU1XtiSaVIudYeCXbEEUVY8HFDZ4,6
16
+ kobai_sdk-0.3.0rc2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.8.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5