kobai-sdk 0.3.4rc2__py3-none-any.whl → 0.3.5rc1__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,733 @@
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
+ def special_request(api_url, mobi_config, **kwargs):
11
+ if mobi_config.use_cookies:
12
+ response = requests.get(api_url, cookies={'mobi_web_token':mobi_config.cookies}, **kwargs)
13
+ else:
14
+ response = requests.get(api_url, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password), **kwargs)
15
+ return(response)
16
+
17
+ def special_post(api_url, mobi_config, **kwargs):
18
+ if mobi_config.use_cookies:
19
+ response = requests.post(api_url, cookies={'mobi_web_token':mobi_config.cookies}, **kwargs)
20
+ else:
21
+ response = requests.post(api_url, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password), **kwargs)
22
+ return(response)
23
+
24
+ def special_put(api_url, mobi_config, **kwargs):
25
+ if mobi_config.use_cookies:
26
+ response = requests.put(api_url, cookies={'mobi_web_token':mobi_config.cookies}, **kwargs)
27
+ else:
28
+ response = requests.put(api_url, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password), **kwargs)
29
+ return(response)
30
+
31
+ def special_delete(api_url, mobi_config, **kwargs):
32
+ if mobi_config.use_cookies:
33
+ response = requests.delete(api_url, cookies={'mobi_web_token':mobi_config.cookies}, **kwargs)
34
+ else:
35
+ response = requests.delete(api_url, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password), **kwargs)
36
+ return(response)
37
+
38
+ ##############################
39
+ # Mobi Pull
40
+ ##############################
41
+
42
+ def get_tenant(top_level_ontology_name, mobi_config: MobiSettings):
43
+ #Find Ontology Record
44
+
45
+ ont_record_id = _get_ont_record_by_name(top_level_ontology_name, mobi_config)
46
+ print("Mobi Ontology Record ID:", ont_record_id)
47
+ #Get Deprecated Nodes
48
+
49
+ api_url = mobi_config.mobi_api_url + "/ontologies/" + urllib.parse.quote_plus(ont_record_id) + "/property-ranges"
50
+ #response = requests.get(api_url, verify=False, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
51
+ response = special_request(api_url, mobi_config, verify=False, timeout=5000)
52
+
53
+ prop_ranges = response.json()["propertyToRanges"]
54
+
55
+
56
+ api_url = mobi_config.mobi_api_url + "/ontologies/" + urllib.parse.quote_plus(ont_record_id) + "/ontology-stuff"
57
+ #response = requests.get(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
58
+ response = special_request(api_url, mobi_config, verify=False, timeout=5000)
59
+
60
+ ########################
61
+ # Deprecated Classes
62
+ ########################
63
+
64
+ deprecated = []
65
+
66
+ try:
67
+ resp_data = response.json()["iriList"]["deprecatedIris"]
68
+ except requests.exceptions.JSONDecodeError:
69
+ resp_data = []
70
+
71
+ for iri in resp_data:
72
+ deprecated.append(iri)
73
+
74
+ try:
75
+ resp_data = response.json()["importedIRIs"]
76
+ except requests.exceptions.JSONDecodeError:
77
+ resp_data = []
78
+
79
+ for o in resp_data:
80
+ for iri in o["deprecatedIris"]:
81
+ deprecated.append(iri)
82
+
83
+ ########################
84
+ # Properties
85
+ ########################
86
+
87
+ data_properties = []
88
+ object_properties = []
89
+
90
+ for p in response.json()["iriList"]["dataProperties"]:
91
+ data_properties.append(p)
92
+
93
+ for p in response.json()["iriList"]["objectProperties"]:
94
+ object_properties.append(p)
95
+
96
+ for o in response.json()["importedIRIs"]:
97
+ for p in o["dataProperties"]:
98
+ data_properties.append(p)
99
+
100
+ for o in response.json()["importedIRIs"]:
101
+ for p in o["objectProperties"]:
102
+ object_properties.append(p)
103
+
104
+ all_properties = data_properties + object_properties
105
+
106
+ ########################
107
+ # Classes and Domains
108
+ ########################
109
+
110
+ domains = {}
111
+ concepts = {}
112
+ prop_domains = {}
113
+
114
+ for ont in _get_classes_by_ont(ont_record_id, mobi_config):
115
+ for c in ont["classes"]:
116
+ if c not in deprecated:
117
+ d = _domain_from_uri(c, mobi_config)
118
+ c_lu = _fix_uri(c, "concept", mobi_config)
119
+
120
+ if d not in domains:
121
+ domains[d] = {"name": d, "concepts": [], "color": ""}
122
+
123
+ #Add Leaf Ontology Concepts
124
+ loc = _parent_uri_from_uri(c)
125
+ loc_lu = _fix_uri(loc, "concept", mobi_config)
126
+ if loc_lu not in concepts:
127
+ name = _name_from_uri(loc, mobi_config)
128
+ concepts[loc_lu] = {"label": name, "domainName": d, "name": name, "uri": loc_lu, "properties": [], "relations": [], "inheritedConcepts": []}
129
+
130
+ #Add Class Concepts
131
+ name = _name_from_uri(c, mobi_config)
132
+ concepts[c_lu] = {"label": name, "domainName": d, "name": name, "uri": c_lu, "properties": [], "relations": [], "inheritedConcepts": [loc_lu]}
133
+
134
+ try:
135
+ resp_data = response.json()["classToAssociatedProperties"]
136
+ except requests.exceptions.JSONDecodeError:
137
+ resp_data = {}
138
+
139
+ class_to_props = resp_data
140
+
141
+ props_with_domain = []
142
+ for c in class_to_props:
143
+ for p in class_to_props[c]:
144
+ props_with_domain.append(p)
145
+
146
+ #for p in resp_data:
147
+ for p in all_properties:
148
+ if p not in props_with_domain:
149
+ leaf_ont_concept = _parent_uri_from_uri(p)
150
+ if leaf_ont_concept not in class_to_props:
151
+ class_to_props[leaf_ont_concept] = []
152
+ class_to_props[leaf_ont_concept].append(p)
153
+
154
+ for c in class_to_props:
155
+ if c not in deprecated:
156
+ for p in class_to_props[c]:
157
+ #loc_lu = _fix_uri(c, "concept", mobi_config)
158
+
159
+ if p in prop_ranges:
160
+ range = prop_ranges[p][0]
161
+ 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"]:
162
+ range = "http://www.w3.org/2001/XMLSchema#string"
163
+ else:
164
+ range = "http://www.w3.org/2001/XMLSchema#string"
165
+
166
+ #range_lu = _fix_uri(prop_ranges[p][0], "concept", mobi_config)
167
+
168
+ #if p in prop_domains:
169
+ #for dc in prop_domains[p]:
170
+ #cp = dc + "/" + _label_from_uri(p)
171
+ cp = c + "/" + _label_from_uri(p)
172
+ #dc_lu = _fix_uri(dc, "concept", mobi_config)
173
+ dc_lu = _fix_uri(c, "concept", mobi_config)
174
+
175
+ #deal with case where literal and relation have same name
176
+ relation_labels = []
177
+ for p in object_properties:
178
+ if p in prop_ranges:
179
+ relation_labels.append(_label_from_uri(p))
180
+
181
+ if p in data_properties:
182
+ prop = {"label": _label_from_uri(p), "uri": _fix_uri(cp, "prop", mobi_config), "conceptUri": dc_lu, "propTypeUri": range, "dataClassTags": []}
183
+ if dc_lu in concepts:
184
+ if prop not in concepts[dc_lu]["properties"]:
185
+ if prop["label"] not in relation_labels:
186
+ concepts[dc_lu]["properties"].append(prop)
187
+ if p in object_properties:
188
+ if p in prop_ranges:
189
+ range_lu = _fix_uri(prop_ranges[p][0], "concept", mobi_config)
190
+ prop = {"label": _label_from_uri(p), "uri": _fix_uri(cp, "prop", mobi_config), "conceptUri": dc_lu, "relationTypeUri": range_lu, "dataClassTags": []}
191
+ if dc_lu in concepts:
192
+ if range_lu in concepts:
193
+ if prop not in concepts[dc_lu]["relations"]:
194
+ concepts[dc_lu]["relations"].append(prop)
195
+ else:
196
+ print("PROPERTY RANGE MISSING", p)
197
+
198
+ try:
199
+ resp_data = response.json()["classHierarchy"]["childMap"]
200
+ except requests.exceptions.JSONDecodeError:
201
+ resp_data = {}
202
+
203
+ for c in resp_data:
204
+ c_lu = _fix_uri(c, "concept", mobi_config)
205
+ for pc in resp_data[c]:
206
+ pc_lu = _fix_uri(pc, "concept", mobi_config)
207
+ if c not in deprecated:
208
+ if c_lu in concepts:
209
+ concepts[c_lu]["inheritedConcepts"].append(pc_lu)
210
+ else:
211
+ print("RELATION TARGET MISSING", c_lu)
212
+
213
+ empty_leaf_concepts = []
214
+ for c in concepts:
215
+ if c.split("#")[1][0] == "_":
216
+ if len(concepts[c]["properties"]) == 0 and len(concepts[c]["relations"]) == 0:
217
+ empty_leaf_concepts.append(c)
218
+ for cc in concepts:
219
+ if c in concepts[cc]["inheritedConcepts"]:
220
+ concepts[cc]["inheritedConcepts"].remove(c)
221
+ else:
222
+ print("KEEPING LEAF ONTOLOGY", c)
223
+ for c in empty_leaf_concepts:
224
+ print("REMOVING LEAF ONTOLOGY", c)
225
+ del concepts[c]
226
+
227
+ tenant = {"solutionId": 0, "model": {"name": "AssetModel", "uri": "http://kobai/" + mobi_config.default_tenant_id + "/AssetModel"}, "tenantId": mobi_config.default_tenant_id, "domains": []}
228
+ tenant_encoded = {"solutionId": 0, "model": {"name": "AssetModel", "uri": "http://kobai/" + mobi_config.default_tenant_id + "/AssetModel"}, "tenantId": mobi_config.default_tenant_id, "domains": []}
229
+ _add_empty_tenant_metadata(tenant)
230
+ _add_empty_tenant_metadata(tenant_encoded)
231
+
232
+ di = 0
233
+ for dk, d in domains.items():
234
+ d['id'] = di
235
+ d['color'] = "#" + str(randrange(222222, 888888))
236
+ d_encoded = {}
237
+ d_encoded['id'] = di
238
+ d_encoded['color'] = "#" + str(randrange(222222, 888888))
239
+ d_encoded['name'] = dk
240
+
241
+ for _, c in concepts.items():
242
+ if dk == c['domainName']:
243
+ cprime = {"uri": c['uri'], "label": c['label'], "relations": c['relations'], "properties": c['properties'], "inheritedConcepts": c['inheritedConcepts']}
244
+ d['concepts'].append(cprime)
245
+ encodedConcepts = base64.b64encode(json.dumps(d['concepts']).encode('ascii')).decode('ascii')
246
+ d_encoded['concepts'] = encodedConcepts
247
+ tenant['domains'].append(d)
248
+ tenant_encoded['domains'].append(d_encoded)
249
+ di += 1
250
+
251
+ return tenant, tenant_encoded
252
+
253
+ def _get_classes_by_ont(ont_record_id, mobi_config):
254
+ api_url = mobi_config.mobi_api_url + "/ontologies/" + urllib.parse.quote_plus(ont_record_id) + "/imported-classes"
255
+ #response = requests.get(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
256
+ response = special_request(api_url, mobi_config, verify=False, timeout=5000)
257
+
258
+ data = []
259
+ try:
260
+ resp_data = response.json()
261
+ except requests.exceptions.JSONDecodeError:
262
+ resp_data = []
263
+
264
+ #for ont in response.json():
265
+ for ont in resp_data:
266
+ record = {}
267
+ record["id"] = _trim_trailing_slash(ont["id"])
268
+ record["classes"] = []
269
+ for c in ont["classes"]:
270
+ record["classes"].append(c)
271
+ data.append(record)
272
+
273
+ api_url = mobi_config.mobi_api_url + "/ontologies/" + urllib.parse.quote_plus(ont_record_id) + "/classes"
274
+ #response = requests.get(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
275
+ response = special_request(api_url, mobi_config, verify=False, timeout=5000)
276
+
277
+ try:
278
+ resp_data = response.json()
279
+ except requests.exceptions.JSONDecodeError:
280
+ resp_data = []
281
+
282
+ record = {}
283
+ if len(resp_data) == 0:
284
+ return data
285
+ record["id"] = _parent_uri_from_uri(_trim_trailing_slash(response.json()[0]["@id"]))
286
+ record["classes"] = []
287
+
288
+ for c in resp_data:
289
+ record["classes"].append(c["@id"])
290
+ data.append(record)
291
+
292
+ return data
293
+
294
+ def _get_ont_record_by_name(name, mobi_config):
295
+ api_url = mobi_config.mobi_api_url + "/catalogs/" + urllib.parse.quote_plus(mobi_config.catalog_name) + "/records"
296
+ #response = requests.get(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
297
+ response = special_request(api_url, mobi_config, verify=False, timeout=5000)
298
+
299
+ ont_record_id = ""
300
+ for r in response.json():
301
+ if r["http://purl.org/dc/terms/title"][0]["@value"] == name:
302
+ ont_record_id = r["@id"]
303
+ return ont_record_id
304
+
305
+ def _get_ont_record_by_url(url, mobi_config):
306
+ api_url = mobi_config.mobi_api_url + "/catalogs/" + urllib.parse.quote_plus(mobi_config.catalog_name) + "/records"
307
+ #response = requests.get(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
308
+ response = special_request(api_url, mobi_config, verify=False, timeout=5000)
309
+
310
+ ont_record_id = ""
311
+ for r in response.json():
312
+ for u in r["http://mobi.com/ontologies/ontology-editor#ontologyIRI"]:
313
+ if url == _trim_trailing_slash(u["@id"]):
314
+ ont_record_id = r["@id"]
315
+ return ont_record_id
316
+
317
+ def _add_empty_tenant_metadata(tenant):
318
+ tenant["dataAccessTags"] = []
319
+ tenant["conceptAccessTags"] = []
320
+ tenant["dataSources"] = []
321
+ tenant["dataSets"] = []
322
+ tenant["collections"] = []
323
+ tenant["visualizations"] = []
324
+ tenant["queries"] = []
325
+ tenant["mappingDefs"] = []
326
+ tenant["dataSourceFileKeys"] = []
327
+ tenant["apiQueryProfiles"] = []
328
+ tenant["collectionVizs"] = []
329
+ tenant["collectionVizOrders"] = []
330
+ tenant["queryDataTags"] = []
331
+ tenant["queryCalcs"] = []
332
+ tenant["dataSourceSettings"] = []
333
+ tenant["publishedAPIs"] = []
334
+ tenant["scenarios"] = []
335
+
336
+ ##############################
337
+ # Mobi Replace
338
+ ##############################
339
+
340
+ def replace_tenant_to_mobi(kobai_tenant, top_level_ontology, mobi_config: MobiSettings):
341
+ json_ld = _create_jsonld(kobai_tenant, top_level_ontology)
342
+ _post_model(json_ld, top_level_ontology, mobi_config)
343
+
344
+ def replace_tenant_to_file(kobai_tenant, top_level_ontology):
345
+ return _create_jsonld(kobai_tenant, top_level_ontology)
346
+
347
+ def _create_jsonld(kobai_tenant, top_level_ontology):
348
+ output_json = []
349
+ uri = kobai_tenant["model"]["uri"]
350
+ uri = uri.replace("AssetModel", top_level_ontology)
351
+
352
+ group = {
353
+ "@id": uri,
354
+ "@type": ["http://www.w3.org/2002/07/owl#Ontology"],
355
+ "http://purl.org/dc/terms/description": [{"@value": "This model was exported from Kobai."}],
356
+ "http://purl.org/dc/terms/title": [{"@value": top_level_ontology}]
357
+ }
358
+ output_json.append(group)
359
+
360
+
361
+ for dom in kobai_tenant["domains"]:
362
+ for con in dom["concepts"]:
363
+
364
+ group = {
365
+ "@id": con["uri"].replace("AssetModel", top_level_ontology),
366
+ "@type": ["http://www.w3.org/2002/07/owl#Class"],
367
+ "http://purl.org/dc/terms/title": [{"@value": con["label"]}]
368
+ }
369
+ if len(con["inheritedConcepts"]) > 0:
370
+ group["http://www.w3.org/2000/01/rdf-schema#subClassOf"] = []
371
+ for parent in con["inheritedConcepts"]:
372
+ group["http://www.w3.org/2000/01/rdf-schema#subClassOf"].append(
373
+ {"@id": parent.replace("AssetModel", top_level_ontology)}
374
+ )
375
+ output_json.append(group)
376
+
377
+ for prop in con["properties"]:
378
+ group = {
379
+ "@id": prop["uri"].replace("AssetModel", top_level_ontology),
380
+ "@type": ["http://www.w3.org/2002/07/owl#DatatypeProperty"],
381
+ "http://purl.org/dc/terms/title": [{"@value": prop["label"]}],
382
+ "http://www.w3.org/2000/01/rdf-schema#domain": [{"@id": con["uri"].replace("AssetModel", top_level_ontology)}],
383
+ "http://www.w3.org/2000/01/rdf-schema#range": [{"@id": prop["propTypeUri"]}]
384
+ }
385
+ output_json.append(group)
386
+
387
+ for rel in con["relations"]:
388
+ group = {
389
+ "@id": rel["uri"].replace("AssetModel", top_level_ontology),
390
+ "@type": ["http://www.w3.org/2002/07/owl#ObjectProperty"],
391
+ "http://purl.org/dc/terms/title": [{"@value": rel["label"]}],
392
+ "http://www.w3.org/2000/01/rdf-schema#domain": [{"@id": con["uri"].replace("AssetModel", top_level_ontology)}],
393
+ "http://www.w3.org/2000/01/rdf-schema#range": [{"@id": rel["relationTypeUri"].replace("AssetModel", top_level_ontology)}]
394
+ }
395
+ output_json.append(group)
396
+ return output_json
397
+
398
+ def _post_model(tenant_json, top_level_ontology, mobi_config):
399
+
400
+ mp = MultipartEncoder(fields={
401
+ "title": top_level_ontology,
402
+ "description": "This model was exported from Kobai.",
403
+ "json": json.dumps(tenant_json)
404
+ })
405
+ h = {"Content-type": mp.content_type}
406
+
407
+ api_url = mobi_config.mobi_api_url + "/ontologies"
408
+ #response = requests.post(
409
+ response = special_post(
410
+ api_url,
411
+ mobi_config,
412
+ headers = h,
413
+ data = mp,
414
+ verify=False,
415
+ timeout=5000
416
+ #auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password)
417
+ )
418
+ print("Upload Status", response.status_code)
419
+ if response.status_code != 201:
420
+ print(response.text)
421
+
422
+ ##############################
423
+ # Mobi Update
424
+ ##############################
425
+
426
+ def update_tenant(kobai_tenant, top_level_ontology_name, mobi_config: MobiSettings):
427
+ record_id = _get_ont_record_by_name(top_level_ontology_name, mobi_config)
428
+
429
+ api_url = mobi_config.mobi_api_url + "/catalogs/" + urllib.parse.quote_plus(mobi_config.catalog_name) + "/records/" + urllib.parse.quote_plus(record_id) + "/branches"
430
+ #response = requests.get(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
431
+ response = special_request(api_url, mobi_config, verify=False, timeout=5000)
432
+
433
+ classes = _get_classes_by_ont(record_id, mobi_config)
434
+ ontology_by_class = {}
435
+ for o in classes:
436
+ for c in o["classes"]:
437
+ ontology_by_class[c] = o["id"]
438
+
439
+ _, mobi_tenant = get_tenant(top_level_ontology_name, mobi_config)
440
+
441
+ change_json = _compare_tenants(kobai_tenant, mobi_tenant, classes, mobi_config)
442
+
443
+ #################################
444
+ # Apply changes with Mobi API calls
445
+ #################################
446
+
447
+ for o in change_json:
448
+ if not change_json[o]["changed"]:
449
+ continue
450
+
451
+ ont_record_id = _get_ont_record_by_url(o, mobi_config)
452
+ if ont_record_id == "":
453
+ continue
454
+
455
+ branch_id = _get_or_create_branch_by_record(ont_record_id, "kobai_dev", mobi_config)
456
+ master_branch_id = _get_or_create_branch_by_record(ont_record_id, "MASTER", mobi_config)
457
+
458
+ for change in change_json[o]["class"]:
459
+ _stage_changes([change["mobi"]], ont_record_id, mobi_config)
460
+ _commit_changes("Kobai added class " + change["mobi"]["http://purl.org/dc/terms/title"][0]["@value"], ont_record_id, branch_id, mobi_config)
461
+ for change in change_json[o]["property"]:
462
+ _stage_changes([change["mobi"]], ont_record_id, mobi_config)
463
+ _commit_changes("Kobai added property " + change["mobi"]["http://purl.org/dc/terms/title"][0]["@value"], ont_record_id, branch_id, mobi_config)
464
+
465
+ api_url = mobi_config.mobi_api_url + "/merge-requests"
466
+ pd = {"title": "Kobai Change from kobai-dev to master", "recordId": ont_record_id, "sourceBranchId": branch_id, "targetBranchId": master_branch_id, "assignees": ["admin"], "removeSource": "true"}
467
+ response = requests.post(api_url, verify=mobi_config.verify_ssl, timeout=5000, params=pd, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
468
+ #print(response.status_code)
469
+
470
+ def _compare_tenants(kobai_tenant, mobi_tenant, classes, mobi_config):
471
+ existing_concepts = []
472
+ existing_relations = {}
473
+ existing_properties = {}
474
+
475
+ for dom in mobi_tenant["domains"]:
476
+ conText = base64.b64decode(dom['concepts']).decode('UTF-8')
477
+ cons = json.loads(conText)
478
+ for con in cons:
479
+ existing_concepts.append(con["uri"])
480
+ existing_properties[con["uri"]] = []
481
+ for prop in con["properties"]:
482
+ existing_properties[con["uri"]].append(prop["uri"])
483
+ existing_relations[con["uri"]] = []
484
+ for rel in con["relations"]:
485
+ existing_relations[con["uri"]].append(rel["uri"])
486
+
487
+ new_concepts = []
488
+ new_relations = {}
489
+ new_properties = {}
490
+
491
+ tenantId = kobai_tenant["tenantId"]
492
+ for dom in kobai_tenant["domains"]:
493
+ conText = base64.b64decode(dom['concepts']).decode('UTF-8')
494
+ cons = json.loads(conText)
495
+ for con in cons:
496
+ con_d = con["uri"].replace(tenantId, mobi_config.default_tenant_id)
497
+ if con_d not in existing_concepts:
498
+ print("New Class Detected")
499
+ new_concepts.append(con)
500
+ for prop in con["properties"]:
501
+ prop_d = prop["uri"].replace(tenantId, mobi_config.default_tenant_id)
502
+ if con_d in existing_properties:
503
+ if prop_d not in existing_properties[con_d]:
504
+ print("New Prop Detected")
505
+ if con_d not in new_properties:
506
+ new_properties[con_d] = []
507
+ new_properties[con_d].append(prop)
508
+ print(prop)
509
+ else:
510
+ print("New Property due to New Concept")
511
+ for rel in con["relations"]:
512
+ rel_d = rel["uri"].replace(tenantId, mobi_config.default_tenant_id)
513
+ if con_d in existing_relations:
514
+ if rel_d not in existing_relations[con_d]:
515
+ print("New Rel Detected")
516
+ if con_d not in new_relations:
517
+ new_relations[con_d] = []
518
+ new_relations[con_d].append(rel)
519
+ else:
520
+ print("New Relation due to New Concept")
521
+
522
+ change_json = {}
523
+ for o in classes:
524
+ ont_exist = o["id"]
525
+ change_json[ont_exist] = {}
526
+ change_json[ont_exist]["exists"] = True
527
+ change_json[ont_exist]["changed"] = False
528
+ change_json[ont_exist]["class"] = []
529
+ change_json[ont_exist]["property"] = []
530
+ change_json[ont_exist]["relation"] = []
531
+
532
+ #################################
533
+ # Identify and capture changes associated to Mobi ontology
534
+ #################################
535
+ for c in new_concepts:
536
+ c_d = c["uri"].replace(tenantId, mobi_config.default_tenant_id)
537
+ ont_sig = c_d.replace("http://kobai/" + mobi_config.default_tenant_id + "/AssetModel/", "").replace("#", "/").replace("_", "/")
538
+ ont_sig = "/".join(ont_sig.split("/")[:-1])
539
+ ont = ""
540
+ for o in classes:
541
+ ont_exist = o["id"]
542
+ if ont_sig == _get_ont_sig_from_ont(ont_exist, len(ont_sig.split("/"))):
543
+ change_json[ont_exist]["class"].append({"type": "new", "kobai": c, "mobi": {}})
544
+
545
+ for c in new_properties:
546
+ for p in new_properties[c]:
547
+ ont_sig = _get_ont_sig_from_concept(tenantId, c, mobi_config)
548
+ for o in classes:
549
+ ont_exist = o["id"]
550
+ if ont_sig == _get_ont_sig_from_ont(ont_exist, len(ont_sig.split("/"))):
551
+ change_json[ont_exist]["property"].append({"type": "new", "kobai": p, "mobi": {}})
552
+
553
+ for c in new_relations:
554
+ for r in new_properties[c]:
555
+ ont_sig = _get_ont_sig_from_concept(tenantId, c, mobi_config)
556
+ for o in classes:
557
+ ont_exist = o["id"]
558
+ if ont_sig == _get_ont_sig_from_ont(ont_exist, len(ont_sig.split("/"))):
559
+ change_json[ont_exist]["relation"].append({"type": "new", "kobai": r, "mobi": {}})
560
+
561
+ #################################
562
+ # Generate Mobi json for every change
563
+ #################################
564
+ for ont in change_json:
565
+ changed = False
566
+ for i, change in enumerate(change_json[ont]["class"]):
567
+ c = change["kobai"]
568
+ c_json = {}
569
+ c_json["@id"] = ont + "/" + c["label"].split("_")[-1]
570
+ c_json["@type"] = [ "http://www.w3.org/2002/07/owl#Class" ]
571
+ c_json["http://www.w3.org/2000/01/rdf-schema#label"] = [{"@value": c["label"].split("_")[-1]}]
572
+ c_json["http://purl.org/dc/terms/title"] = [{"@value": c["label"].split("_")[-1]}]
573
+ c_json["http://www.w3.org/2000/01/rdf-schema#subClassOf"] = []
574
+ #c_json["http://www.w3.org/2002/07/owl#deprecated"] = [{"@value": "true", "@type": "http://www.w3.org/2001/XMLSchema#boolean"}]
575
+ for pc in c["inheritedConcepts"]:
576
+ c_json["http://www.w3.org/2000/01/rdf-schema#subClassOf"].append(pc)
577
+ change_json[ont]["class"][i]["mobi"] = c_json
578
+ changed = True
579
+
580
+ for i, change in enumerate(change_json[ont]["property"]):
581
+ p = change["kobai"]
582
+ p_json = {}
583
+ p_json["@id"] = ont + "/" + p["label"]
584
+ p_json["@type"] = [ "http://www.w3.org/2002/07/owl#Class" ]
585
+ p_json["http://www.w3.org/2000/01/rdf-schema#label"] = [{"@value": p["label"]}]
586
+ p_json["http://purl.org/dc/terms/title"] = [{"@value": p["label"]}]
587
+ p_json["http://www.w3.org/2000/01/rdf-schema#domain"] = [{"@id": ont + "/" + _get_concept_name_from_prop_uri(p["uri"])}]
588
+ p_json["http://www.w3.org/2000/01/rdf-schema#range"] = [{"@id": p["propTypeUri"]}]
589
+ change_json[ont]["property"][i]["mobi"] = p_json
590
+ changed = True
591
+
592
+ if changed is True:
593
+ change_json[ont]["changed"] = True
594
+
595
+ return change_json
596
+
597
+ def _stage_changes(changes, ont_record_id, mobi_config):
598
+ 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"
599
+ #response = requests.delete(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
600
+ response = special_delete(api_url, mobi_config, verify=False, timeout=5000)
601
+
602
+ 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"
603
+ m = MultipartEncoder(fields={"additions": json.dumps(changes), "deletions": "[]"})
604
+ h = {"Content-type": m.content_type}
605
+ #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))
606
+ response = special_put(api_url, mobi_config, verify=False, timeout=5000, data=m, headers=h)
607
+
608
+ 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"
609
+ #response = requests.get(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
610
+ response = special_request(api_url, mobi_config, verify=False, timeout=5000)
611
+
612
+ def _commit_changes(message, ont_record_id, branch_id, mobi_config):
613
+ 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"
614
+ pd = {"message": message}
615
+ #response = requests.post(api_url, verify=False, timeout=5000, params=pd, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
616
+ response = special_post(api_url, mobi_config, verify=False, timeout=5000, params=pd)
617
+
618
+ ##############################
619
+ # Mobi Branch
620
+ ##############################
621
+
622
+ #def jprint(data):
623
+ # json_str = json.dumps(data, indent=4)
624
+
625
+ def _get_branches_by_record(id, mobi_config):
626
+ api_url = mobi_config.mobi_api_url + "/catalogs/" + urllib.parse.quote_plus(mobi_config.catalog_name) + "/records/" + urllib.parse.quote_plus(id) + "/branches"
627
+ #response = requests.get(api_url, verify=False, timeout=5000, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
628
+ response = special_request(api_url, mobi_config, verify=False, timeout=5000)
629
+ return response.json()
630
+
631
+ def _get_or_create_branch_by_record(id, name, mobi_config):
632
+ branches = _get_branches_by_record(id, mobi_config)
633
+ for b in branches:
634
+ if b["http://purl.org/dc/terms/title"][0]["@value"] == name:
635
+ return b["@id"]
636
+
637
+ commit = ""
638
+ for b in branches:
639
+ if b["http://purl.org/dc/terms/title"][0]["@value"] == "MASTER":
640
+ commit = b["http://mobi.com/ontologies/catalog#head"][0]["@id"]
641
+
642
+ api_url = mobi_config.mobi_api_url + "/catalogs/" + urllib.parse.quote_plus(mobi_config.catalog_name) + "/records/" + urllib.parse.quote_plus(id) + "/branches"
643
+ pd = {"type": "http://mobi.com/ontologies/catalog#Branch", "title": name, "commitId": commit}
644
+ #requests.post(api_url, verify=False, timeout=5000, params=pd, auth=requests.auth.HTTPBasicAuth(mobi_config.mobi_username, mobi_config.mobi_password))
645
+ special_post(api_url, mobi_config, verify=False, timeout=5000, params=pd)
646
+
647
+ branches = _get_branches_by_record(id, mobi_config)
648
+ for b in branches:
649
+ if b["http://purl.org/dc/terms/title"][0]["@value"] == name:
650
+ return b["@id"]
651
+ return ""
652
+
653
+ ##############################
654
+ # Mobi Parse
655
+ ##############################
656
+ def _get_domain_range(url, mobi_config):
657
+ for d, r in mobi_config.domain_extraction.items():
658
+ if d in url:
659
+ return r
660
+ return {"min": 0, "max": 0}
661
+
662
+ def _parent_uri_from_uri(uri):
663
+ #return "/".join(uri.split("/")[:-1])
664
+ return "/".join(_uri_split(uri)[:-1])
665
+
666
+ def _trim_trailing_slash(uri):
667
+ if uri[-1] == "/":
668
+ return uri[:-1]
669
+ else:
670
+ return uri
671
+
672
+ def _uri_split(uri):
673
+ uri = uri.replace("#", "/")
674
+ return uri.split("/")
675
+
676
+
677
+ ################################
678
+ # Transform from Kobai to Mobi
679
+ ################################
680
+
681
+ def _get_ont_sig_from_concept(tenantId, uri, mobi_config):
682
+ uri = uri.replace(tenantId, mobi_config.default_tenant_id)
683
+ ont_sig = uri.replace("http://kobai/" + mobi_config.default_tenant_id + "/AssetModel/", "").replace("#", "/").replace("_", "/")
684
+ ont_sig = "/".join(ont_sig.split("/")[:-1])
685
+ return ont_sig
686
+
687
+ def _get_ont_sig_from_ont(uri, length):
688
+ return "/".join(uri.split("/")[-length:])
689
+
690
+ def _get_concept_name_from_prop_uri(uri):
691
+ return uri.split("#")[0].split("/")[-1]
692
+ #return uri.split("#")[0].split("_")[-1]
693
+
694
+ ################################
695
+ # Transform from Mobi to Kobai
696
+ ################################
697
+
698
+ def _domain_from_uri(uri, mobi_config):
699
+ #domain = "_".join(uri.split("/")[_get_domain_range(uri, mobi_config)['min']:_get_domain_range(uri, mobi_config)['max']+1])
700
+ domain = "_".join(_uri_split(uri)[_get_domain_range(uri, mobi_config)['min']:_get_domain_range(uri, mobi_config)['max']+1])
701
+ return domain
702
+
703
+ def _name_from_uri(uri, mobi_config):
704
+ #name = "_".join(uri.split("/")[_get_domain_range(uri, mobi_config)['max']+1:])
705
+ name = "_".join(_uri_split(uri)[_get_domain_range(uri, mobi_config)['max']+1:])
706
+ if name == "":
707
+ #name = "_" + "_".join(uri.split("/")[_get_domain_range(uri, mobi_config)['max']:])
708
+ name = "_" + "_".join(_uri_split(uri)[_get_domain_range(uri, mobi_config)['max']:])
709
+ return name
710
+
711
+ def _label_from_uri(uri):
712
+ #return uri.split("/")[-1]
713
+ return _uri_split(uri)[-1]
714
+
715
+ def _fix_uri(uri, type, mobi_config):
716
+ domain = _domain_from_uri(uri, mobi_config)
717
+ name = _name_from_uri(uri, mobi_config)
718
+
719
+ top = "/".join(uri.split("/")[0:_get_domain_range(uri, mobi_config)['min']])
720
+
721
+ if type == "concept":
722
+ uri = domain + "#" + name
723
+ elif type == "prop":
724
+ uri = domain + "/" + "_".join(name.split("_")[0:-1]) + "#" + name.split("_")[-1]
725
+
726
+ for d in mobi_config.domain_extraction:
727
+
728
+ if d in top:
729
+ uri = "http://kobai/" + mobi_config.default_tenant_id + "/AssetModel/" + uri
730
+
731
+ return uri
732
+
733
+
kobai/mobi_config.py ADDED
@@ -0,0 +1,19 @@
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
+ cookies: str = ""
13
+ use_cookies: bool = False
14
+ verify_ssl = False
15
+
16
+ catalog_name: str = "http://mobi.com/catalog-local"
17
+ default_tenant_id: str = "00000000-0000-0000-0000-000000000000"
18
+
19
+ #settings = MobiSettings()
kobai/ms_authenticate.py CHANGED
@@ -20,7 +20,7 @@ def get_scope(client_id: str = None, target_client_id: str = None, scope: str =
20
20
 
21
21
  return f"openid profile offline_access api://{target_client_id}/Kobai.Access"
22
22
 
23
- def device_code(tenant_id: str, client_id: str, target_client_id: str = None, scope: str = None):
23
+ def device_code(tenant_id: str, client_id: str, target_client_id: str = None, scope: str = None, authority: str = None ):
24
24
 
25
25
  """
26
26
  Authenticate using the device code flow and get the access token
@@ -31,7 +31,7 @@ def device_code(tenant_id: str, client_id: str, target_client_id: str = None, sc
31
31
  target_client_id (str): Kobai IDM client ID.
32
32
  scope (str): Scope to be passed
33
33
  """
34
- credential = DeviceCodeCredential(client_id=client_id, tenant_id=tenant_id)
34
+ credential = DeviceCodeCredential(client_id=client_id, tenant_id=tenant_id, authority=authority)
35
35
 
36
36
  try:
37
37
  token = credential.get_token(get_scope(client_id, target_client_id, scope))
kobai/tenant_client.py CHANGED
@@ -8,7 +8,8 @@ from pyspark.sql import SparkSession
8
8
  from langchain_core.language_models.chat_models import BaseChatModel
9
9
  from langchain_core.embeddings import Embeddings
10
10
 
11
- from . import spark_client, databricks_client, ai_query, tenant_api, ai_rag
11
+ from . import spark_client, databricks_client, ai_query, tenant_api, ai_rag, mobi
12
+ from .mobi_config import MobiSettings
12
13
  from .genie import get_genie_descriptions
13
14
 
14
15
  class TenantClient:
@@ -56,6 +57,7 @@ class TenantClient:
56
57
 
57
58
 
58
59
 
60
+
59
61
  ########################################
60
62
  # MS Entra Auth
61
63
  ########################################
@@ -1012,3 +1014,123 @@ class TenantClient:
1012
1014
  '/data-svcs/solution/snapshot/import/upload',
1013
1015
  {'file': EMPTY_TENANT_JSON}
1014
1016
  )
1017
+
1018
+ ########################################
1019
+ # Mobi
1020
+ ########################################
1021
+
1022
+ def pull_mobi_to_tenant(self, ontology_name, mobi_config: MobiSettings):
1023
+
1024
+ """
1025
+ Export an ontology from Mobi and import it into a Kobai tenant, replacing the contents of the tenant.
1026
+
1027
+ Requires that the SDK be authenticated against the target Kobai tenant.
1028
+
1029
+ Parameters:
1030
+ ontology_name (str): The name of the ontology to access in Mobi.
1031
+ mobi_config (MobiSettings): Configuration required to access the Mobi service.
1032
+ """
1033
+
1034
+ tenant_json, tenant_json_enc = mobi.get_tenant(ontology_name, mobi_config)
1035
+ #for d in tenant_json["domains"]:
1036
+ #for c in d["concepts"]:
1037
+ # print(c)
1038
+ self.__set_tenant_import(tenant_json_enc)
1039
+
1040
+ def pull_mobi_to_file(self, ontology_name, mobi_config: MobiSettings, file_name, human_readable=False):
1041
+
1042
+ """
1043
+ Export an ontology from Mobi and save it in a Kobai json import file.
1044
+
1045
+ Requires that the SDK be authenticated against the target Kobai tenant.
1046
+
1047
+ Parameters:
1048
+ ontology_name (str): The name of the ontology to access in Mobi.
1049
+ mobi_config (MobiSettings): Configuration required to access the Mobi service.
1050
+ file_name (str): File name to give the output (no extension)
1051
+ human_readable (bool) OPTIONAL: generate a second, decoded Kobai file.
1052
+ """
1053
+
1054
+ tenant_json, tenant_json_enc = mobi.get_tenant(ontology_name, mobi_config)
1055
+
1056
+ if ".json" in file_name:
1057
+ file_name = file_name.split(".json")[0]
1058
+
1059
+ with open(f"{file_name}.json", "w") as out_file:
1060
+ json.dump(tenant_json_enc, out_file)
1061
+
1062
+ if human_readable:
1063
+ with open(f"{file_name}_decoded.json", "w") as out_file:
1064
+ json.dump(tenant_json, out_file)
1065
+
1066
+ def push_tenant_update_to_mobi(self, ontology_name, mobi_config: MobiSettings):
1067
+
1068
+ """
1069
+ Compare a (modified) Kobai tenant to a Mobi ontology, and generate a Merge Request for the changes.
1070
+
1071
+ Requires that the SDK be authenticated against the target Kobai tenant.
1072
+
1073
+ Parameters:
1074
+ ontology_name (str): The name of the ontology to access in Mobi.
1075
+ mobi_config (MobiSettings): Configuration required to access the Mobi service.
1076
+ """
1077
+
1078
+ tenant_json_enc = self.__get_tenant_export()
1079
+ mobi.update_tenant(tenant_json_enc, ontology_name, mobi_config)
1080
+
1081
+ def push_whole_tenant_to_mobi(self, ontology_name, mobi_config: MobiSettings):
1082
+
1083
+ """
1084
+ Export a tenant from Kobai, and create an ontology in Mobi.
1085
+
1086
+ Requires that the SDK be authenticated against the target Kobai tenant.
1087
+ Requires that an ontology with the same name does not already exist in Mobi.
1088
+
1089
+ Parameters:
1090
+ ontology_name (str): The name of the ontology to create in Mobi.
1091
+ mobi_config (MobiSettings): Configuration required to access the Mobi service.
1092
+ """
1093
+
1094
+ tenant_json = self.get_tenant_config()
1095
+ mobi.replace_tenant_to_mobi(tenant_json, ontology_name, mobi_config)
1096
+
1097
+ def push_whole_tenant_to_jsonld_file(self, ontology_name, file_name):
1098
+
1099
+ """
1100
+ Export a tenant from Kobai, and create an ontology in Mobi.
1101
+
1102
+ Requires that the SDK be authenticated against the target Kobai tenant.
1103
+
1104
+ Parameters:
1105
+ ontology_name (str): The name of the ontology to create in Mobi.
1106
+ file_name (str): File name to give the output (no extension)
1107
+ """
1108
+
1109
+ tenant_json = self.get_tenant_config()
1110
+ tenant_jsonld = mobi.replace_tenant_to_file(tenant_json, ontology_name)
1111
+
1112
+ if ".json" in file_name:
1113
+ file_name = file_name.split(".json")[0]
1114
+
1115
+ with open(f"{file_name}.json", "w") as out_file:
1116
+ json.dump(tenant_jsonld, out_file)
1117
+
1118
+ def get_default_mobi_config(self):
1119
+
1120
+ """
1121
+ Returns a default MobiSettings configuration object.
1122
+
1123
+ Available Fields to Set:
1124
+ domain_extraction: Mapping of ontology url structures to Kobai domain names.
1125
+ mobi_api_url: url for Mobi service. (ex: https://localhost:8443/mobirest)
1126
+ mobi_username: User name for Mobi service.
1127
+ mobi_password: Password for Mobi service.
1128
+ """
1129
+
1130
+ return MobiSettings()
1131
+
1132
+ def __set_tenant_import(self, tenant_json_enc):
1133
+ self.api_client._TenantAPI__run_post_files(
1134
+ '/data-svcs/solution/snapshot/import/upload',
1135
+ {'file': json.dumps(tenant_json_enc)}
1136
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kobai-sdk
3
- Version: 0.3.4rc2
3
+ Version: 0.3.5rc1
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
@@ -222,6 +222,7 @@ Requires-Dist: azure-storage-blob
222
222
  Requires-Dist: langchain-core
223
223
  Requires-Dist: langchain-community
224
224
  Requires-Dist: langchain-classic
225
+ Requires-Dist: delta-spark
225
226
  Provides-Extra: dev
226
227
  Requires-Dist: black; extra == "dev"
227
228
  Requires-Dist: bumpver; extra == "dev"
@@ -4,12 +4,14 @@ kobai/ai_rag.py,sha256=ZBUlpjbQC73yTXNV91uG_Tw-PvtWR5bSAg2NlwJQZwM,14635
4
4
  kobai/databricks_client.py,sha256=fyqqMly2Qm0r1AHWsQjkYeNsDdH0G1JSgTkF9KJ55qA,2118
5
5
  kobai/demo_tenant_client.py,sha256=wlNc-bdI2wotRXo8ppUOalv4hYdBlek_WzJNARZV-AE,9293
6
6
  kobai/genie.py,sha256=-EbEYpu9xj_3zIXaPdwbNJEAmoeM7nb9qK-h1f_STtM,8061
7
- kobai/ms_authenticate.py,sha256=rlmhtvAaSRBlYmvIBy5epMVa4MBGBLPaMwawu1T_xDQ,2252
7
+ kobai/mobi.py,sha256=1pIAdn9qKmg2zm_DnH1qAsgKr-YgH6SJJWXEzSZch38,32787
8
+ kobai/mobi_config.py,sha256=AkrGtwDNMPuwt-udbp_8KsLnLGze4S8-at2UH-zN9YQ,525
9
+ kobai/ms_authenticate.py,sha256=f7t_BaxK8e-SUWoB6JliBEngF2iduF-COcCZq83H5t0,2297
8
10
  kobai/spark_client.py,sha256=opM_F-4Ut5Hq5zZjWMuLvUps9sDULvyPNZHXGL8dW1k,776
9
11
  kobai/tenant_api.py,sha256=Q5yuFd9_V4lo3LWzvYEEO3LpDRWFgQD4TlRPXDTGbiE,4368
10
- kobai/tenant_client.py,sha256=xozM69_e4Z8mD20JpPuVWVU5oOsRVazEjFRbcwV7p6s,38899
11
- kobai_sdk-0.3.4rc2.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
12
- kobai_sdk-0.3.4rc2.dist-info/METADATA,sha256=6Y2KuTxWjYVrCw5VBjgQHK_r5PgjsHajVPuKeFeqi8g,19837
13
- kobai_sdk-0.3.4rc2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- kobai_sdk-0.3.4rc2.dist-info/top_level.txt,sha256=ns1El3BrTTHKvoAgU1XtiSaVIudYeCXbEEUVY8HFDZ4,6
15
- kobai_sdk-0.3.4rc2.dist-info/RECORD,,
12
+ kobai/tenant_client.py,sha256=EeJhjdg1iFKZm0L7iSwbg4FEXbJjh6iWmdGLdKb0yC4,43470
13
+ kobai_sdk-0.3.5rc1.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
14
+ kobai_sdk-0.3.5rc1.dist-info/METADATA,sha256=0QRyijyJKePuaBrZ1vBYO4yjoC6NVxuUy7Uq9rS1CAU,19864
15
+ kobai_sdk-0.3.5rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ kobai_sdk-0.3.5rc1.dist-info/top_level.txt,sha256=ns1El3BrTTHKvoAgU1XtiSaVIudYeCXbEEUVY8HFDZ4,6
17
+ kobai_sdk-0.3.5rc1.dist-info/RECORD,,