bam-masterdata 0.0.0__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.
- bam_masterdata/__init__.py +0 -0
- bam_masterdata/_version.py +16 -0
- bam_masterdata/cli/__init__.py +1 -0
- bam_masterdata/cli/cli.py +194 -0
- bam_masterdata/cli/entities_to_excel.py +99 -0
- bam_masterdata/cli/entities_to_json.py +67 -0
- bam_masterdata/cli/fill_masterdata.py +426 -0
- bam_masterdata/datamodel/__init__.py +0 -0
- bam_masterdata/datamodel/collection_types.py +317 -0
- bam_masterdata/datamodel/dataset_types.py +1497 -0
- bam_masterdata/datamodel/object_types.py +13560 -0
- bam_masterdata/datamodel/property_types.py +6515 -0
- bam_masterdata/datamodel/vocabulary_types.py +81811 -0
- bam_masterdata/logger.py +41 -0
- bam_masterdata/metadata/__init__.py +0 -0
- bam_masterdata/metadata/definitions.py +442 -0
- bam_masterdata/metadata/entities.py +181 -0
- bam_masterdata/openbis/__init__.py +1 -0
- bam_masterdata/openbis/get_entities.py +181 -0
- bam_masterdata/openbis/login.py +18 -0
- bam_masterdata/utils/__init__.py +6 -0
- bam_masterdata/utils/utils.py +115 -0
- bam_masterdata-0.0.0.dist-info/METADATA +176 -0
- bam_masterdata-0.0.0.dist-info/RECORD +35 -0
- bam_masterdata-0.0.0.dist-info/WHEEL +5 -0
- bam_masterdata-0.0.0.dist-info/entry_points.txt +2 -0
- bam_masterdata-0.0.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/cli/test_entities_to_excel.py +82 -0
- tests/cli/test_entities_to_json.py +42 -0
- tests/conftest.py +146 -0
- tests/metadata/test_definitions.py +288 -0
- tests/metadata/test_entities.py +54 -0
- tests/test_logger.py +37 -0
- tests/utils/test_utils.py +152 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from bam_masterdata.openbis import OpenbisEntities
|
|
6
|
+
from bam_masterdata.utils import code_to_class_name
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MasterdataCodeGenerator:
|
|
10
|
+
"""
|
|
11
|
+
Class to generate Python code for the masterdata datamodel based on the entities existing in an
|
|
12
|
+
openBIS instance.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, url: str = ""):
|
|
16
|
+
start_time = time.time()
|
|
17
|
+
# * This part takes some time due to the loading of all entities from Openbis
|
|
18
|
+
self.properties = OpenbisEntities(url=url).get_property_dict()
|
|
19
|
+
self.collections = OpenbisEntities(url=url).get_collection_dict()
|
|
20
|
+
self.datasets = OpenbisEntities(url=url).get_dataset_dict()
|
|
21
|
+
self.objects = OpenbisEntities(url=url).get_object_dict()
|
|
22
|
+
self.vocabularies = OpenbisEntities(url=url).get_vocabulary_dict()
|
|
23
|
+
elapsed_time = time.time() - start_time
|
|
24
|
+
click.echo(
|
|
25
|
+
f"Loaded OpenBIS entities in `MasterdataCodeGenerator` initialization {elapsed_time:.2f} seconds\n"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def determine_parent_class(
|
|
29
|
+
self, code: str, class_names: dict, default: str, lines: list
|
|
30
|
+
) -> tuple:
|
|
31
|
+
"""
|
|
32
|
+
Determine the parent class information of the entity based on its `code`. It returns
|
|
33
|
+
the `parent_code` and `parent class`, as well as the `class_name` of the entity. The
|
|
34
|
+
class will inherit from `parent_class`.
|
|
35
|
+
|
|
36
|
+
If the parent class does not exist, a note is added to the `lines` list for debugging purposes.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
code (str): The code of the entity.
|
|
40
|
+
class_names (dict): A dictionary with the class names of the entities.
|
|
41
|
+
default (str): The default parent class if the parent class does not exist.
|
|
42
|
+
lines (list): A list of strings to be printed to the Python module.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
tuple: The parent code, parent class, and class name of the entity.
|
|
46
|
+
"""
|
|
47
|
+
parent_code = ""
|
|
48
|
+
if "." in code:
|
|
49
|
+
parent_code = code.rsplit(".", 1)[0]
|
|
50
|
+
parent_class = class_names.get(parent_code, default)
|
|
51
|
+
|
|
52
|
+
# Format class name
|
|
53
|
+
class_name = code_to_class_name(code)
|
|
54
|
+
class_names[code] = class_name
|
|
55
|
+
|
|
56
|
+
# If the parent class does not exist but the `code` shows some inheritance, we add a note for debugging
|
|
57
|
+
if parent_code and parent_class == default:
|
|
58
|
+
lines.append(
|
|
59
|
+
f"# ! The parent class of {class_name} is not defined (missing {parent_class})"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return parent_code, parent_class, class_name
|
|
63
|
+
|
|
64
|
+
def add_properties(
|
|
65
|
+
self, entities: dict, parent_code: str, data: dict, lines: list
|
|
66
|
+
) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Add the properties of the entity to the `lines` list. The properties are added as
|
|
69
|
+
`PropertyTypeAssignment` objects.
|
|
70
|
+
|
|
71
|
+
Note: the assigned properties do not have the information of `code` for the entity when
|
|
72
|
+
data_type is OBJECT or CONTROLLEDVOCABULARY. These are instead defined in `property_types.py`.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
entities (dict): The dictionary of entities (objects, collections, datasets, vocabularies).
|
|
76
|
+
parent_code (code): The code of the parent class.
|
|
77
|
+
data (dict): The data information for the entity as obtained from openBIS.
|
|
78
|
+
lines (list): A list of strings to be printed to the Python module.
|
|
79
|
+
"""
|
|
80
|
+
parent_properties = entities.get(parent_code, {}).get("properties", {}).keys()
|
|
81
|
+
for prop_code, prop_data in data.get("properties", {}).items():
|
|
82
|
+
# Skip "UNKNOWN" properties
|
|
83
|
+
if prop_code == "UNKNOWN":
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
# We check if the property is inherited from the parent class
|
|
87
|
+
if prop_code in parent_properties:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
prop_name = prop_code.lstrip("$").replace(".", "_").lower()
|
|
91
|
+
lines.append(f" {prop_name} = PropertyTypeAssignment(")
|
|
92
|
+
lines.append(f' code="{prop_code}",')
|
|
93
|
+
# ! patching dataType=SAMPLE instead of OBJECT
|
|
94
|
+
data_type = prop_data.get("dataType", "")
|
|
95
|
+
if data_type == "SAMPLE":
|
|
96
|
+
data_type = "OBJECT"
|
|
97
|
+
lines.append(f' data_type="{data_type}",')
|
|
98
|
+
property_label = (prop_data.get("label") or "").replace("\n", "\\n")
|
|
99
|
+
lines.append(f' property_label="{property_label}",')
|
|
100
|
+
description = (
|
|
101
|
+
(prop_data.get("description") or "")
|
|
102
|
+
.replace('"', "`")
|
|
103
|
+
.replace("\n", "\\n")
|
|
104
|
+
.replace("'", "`")
|
|
105
|
+
)
|
|
106
|
+
lines.append(f' description="""{description}""",')
|
|
107
|
+
lines.append(f" mandatory={prop_data.get('mandatory', False)},")
|
|
108
|
+
lines.append(
|
|
109
|
+
f" show_in_edit_views={prop_data.get('show_in_edit_views', False)},"
|
|
110
|
+
)
|
|
111
|
+
section = (
|
|
112
|
+
(prop_data.get("section") or "")
|
|
113
|
+
.replace('"', '\\"')
|
|
114
|
+
.replace("\n", "\\n")
|
|
115
|
+
.replace("'", "\\'")
|
|
116
|
+
)
|
|
117
|
+
lines.append(f' section="{section}",')
|
|
118
|
+
lines.append(" )")
|
|
119
|
+
lines.append("")
|
|
120
|
+
|
|
121
|
+
def generate_property_types(self) -> str:
|
|
122
|
+
"""
|
|
123
|
+
Generate Python code for the property types in the Openbis datamodel. The code is generated
|
|
124
|
+
as a string which is then printed out to the specific Python module in `bam_masterdata/datamodel/property_types.py`.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
str: Python code for the property types.
|
|
128
|
+
"""
|
|
129
|
+
lines = []
|
|
130
|
+
|
|
131
|
+
# Add imports at the top
|
|
132
|
+
lines.append("from bam_masterdata.metadata.definitions import PropertyTypeDef")
|
|
133
|
+
lines.append("")
|
|
134
|
+
|
|
135
|
+
# Process each property type
|
|
136
|
+
for code, data in self.properties.items():
|
|
137
|
+
# Skip the "UNKNOWN" object type
|
|
138
|
+
if code == "UNKNOWN":
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
# Format class name
|
|
142
|
+
class_name = code_to_class_name(code, entity_type="property")
|
|
143
|
+
|
|
144
|
+
# Add class definition
|
|
145
|
+
lines.append(f"{class_name} = PropertyTypeDef(")
|
|
146
|
+
lines.append(f' code="{code}",')
|
|
147
|
+
description = (
|
|
148
|
+
(data.get("description") or "")
|
|
149
|
+
.replace('"', "`")
|
|
150
|
+
.replace("\n", "\\n")
|
|
151
|
+
.replace("'", "`")
|
|
152
|
+
)
|
|
153
|
+
lines.append(f' description="""{description}""",')
|
|
154
|
+
# ! patching dataType=SAMPLE instead of OBJECT
|
|
155
|
+
data_type = data.get("dataType", "")
|
|
156
|
+
if data_type == "SAMPLE":
|
|
157
|
+
data_type = "OBJECT"
|
|
158
|
+
lines.append(f' data_type="{data_type}",')
|
|
159
|
+
object_code = data.get("sampleType", "")
|
|
160
|
+
if object_code:
|
|
161
|
+
lines.append(f' object_code="{object_code}",')
|
|
162
|
+
property_label = (
|
|
163
|
+
(data.get("label") or "").replace('"', '\\"').replace("\n", "\\n")
|
|
164
|
+
)
|
|
165
|
+
vocabulary_code = data.get("vocabulary", "")
|
|
166
|
+
if vocabulary_code:
|
|
167
|
+
lines.append(f' vocabulary_code="{vocabulary_code}",')
|
|
168
|
+
lines.append(f' property_label="{property_label}",')
|
|
169
|
+
lines.append(")")
|
|
170
|
+
lines.append("")
|
|
171
|
+
|
|
172
|
+
# Add newline between classes
|
|
173
|
+
lines.append("")
|
|
174
|
+
|
|
175
|
+
return "\n".join(lines)
|
|
176
|
+
|
|
177
|
+
def generate_collection_types(self) -> str:
|
|
178
|
+
"""
|
|
179
|
+
Generate Python code for the collection types in the Openbis datamodel. The code is generated
|
|
180
|
+
as a string which is then printed out to the specific Python module in `bam_masterdata/datamodel/collection_types.py`.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
str: Python code for the collection types.
|
|
184
|
+
"""
|
|
185
|
+
lines = []
|
|
186
|
+
class_names: dict = {}
|
|
187
|
+
|
|
188
|
+
# Add imports at the top
|
|
189
|
+
lines.append(
|
|
190
|
+
"from bam_masterdata.metadata.definitions import CollectionTypeDef, PropertyTypeAssignment"
|
|
191
|
+
)
|
|
192
|
+
lines.append("from bam_masterdata.metadata.entities import CollectionType")
|
|
193
|
+
lines.append("")
|
|
194
|
+
lines.append("")
|
|
195
|
+
|
|
196
|
+
# Process each collection type
|
|
197
|
+
for code, data in self.collections.items():
|
|
198
|
+
# Skip the "UNKNOWN" object type
|
|
199
|
+
if code == "UNKNOWN":
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
# Determine parent class
|
|
203
|
+
parent_code, parent_class, class_name = self.determine_parent_class(
|
|
204
|
+
code=code,
|
|
205
|
+
class_names=class_names,
|
|
206
|
+
default="CollectionType",
|
|
207
|
+
lines=lines,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Add class definition
|
|
211
|
+
lines.append(f"class {class_name}({parent_class}):")
|
|
212
|
+
lines.append(" defs = CollectionTypeDef(")
|
|
213
|
+
lines.append(f' code="{code}",')
|
|
214
|
+
description = (
|
|
215
|
+
(data.get("description") or "")
|
|
216
|
+
.replace('"', "`")
|
|
217
|
+
.replace("\n", "\\n")
|
|
218
|
+
.replace("'", "`")
|
|
219
|
+
)
|
|
220
|
+
lines.append(f' description="""{description}""",')
|
|
221
|
+
if data.get("validationPlugin") != "":
|
|
222
|
+
lines.append(
|
|
223
|
+
f' validation_script="{data.get("validationPlugin")}",'
|
|
224
|
+
)
|
|
225
|
+
lines.append(" )")
|
|
226
|
+
lines.append("")
|
|
227
|
+
|
|
228
|
+
# Add properties
|
|
229
|
+
self.add_properties(self.collections, parent_code, data, lines)
|
|
230
|
+
# Add newline between classes
|
|
231
|
+
lines.append("")
|
|
232
|
+
|
|
233
|
+
return "\n".join(lines)
|
|
234
|
+
|
|
235
|
+
def generate_dataset_types(self) -> str:
|
|
236
|
+
"""
|
|
237
|
+
Generate Python code for the dataset types in the Openbis datamodel. The code is generated
|
|
238
|
+
as a string which is then printed out to the specific Python module in `bam_masterdata/datamodel/dataset_types.py`.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
str: Python code for the dataset types.
|
|
242
|
+
"""
|
|
243
|
+
lines = []
|
|
244
|
+
class_names: dict = {}
|
|
245
|
+
|
|
246
|
+
# Add imports at the top
|
|
247
|
+
lines.append(
|
|
248
|
+
"from bam_masterdata.metadata.definitions import DatasetTypeDef, PropertyTypeAssignment"
|
|
249
|
+
)
|
|
250
|
+
lines.append("from bam_masterdata.metadata.entities import DatasetType")
|
|
251
|
+
lines.append("")
|
|
252
|
+
lines.append("")
|
|
253
|
+
|
|
254
|
+
# Process each dataset type
|
|
255
|
+
for code, data in self.datasets.items():
|
|
256
|
+
# Skip the "UNKNOWN" object type
|
|
257
|
+
if code == "UNKNOWN":
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
# Determine parent class
|
|
261
|
+
parent_code, parent_class, class_name = self.determine_parent_class(
|
|
262
|
+
code=code, class_names=class_names, default="DatasetType", lines=lines
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Add class definition
|
|
266
|
+
lines.append(f"class {class_name}({parent_class}):")
|
|
267
|
+
lines.append(" defs = DatasetTypeDef(")
|
|
268
|
+
lines.append(f' code="{code}",')
|
|
269
|
+
description = (
|
|
270
|
+
(data.get("description") or "")
|
|
271
|
+
.replace('"', "`")
|
|
272
|
+
.replace("\n", "\\n")
|
|
273
|
+
.replace("'", "`")
|
|
274
|
+
)
|
|
275
|
+
lines.append(f' description="""{description}""",')
|
|
276
|
+
lines.append(" )")
|
|
277
|
+
lines.append("")
|
|
278
|
+
|
|
279
|
+
# Add properties
|
|
280
|
+
self.add_properties(self.datasets, parent_code, data, lines)
|
|
281
|
+
# Add newline between classes
|
|
282
|
+
lines.append("")
|
|
283
|
+
|
|
284
|
+
return "\n".join(lines)
|
|
285
|
+
|
|
286
|
+
def generate_object_types(self) -> str:
|
|
287
|
+
"""
|
|
288
|
+
Generate Python code for the object types in the Openbis datamodel. The code is generated
|
|
289
|
+
as a string which is then printed out to the specific Python module in `bam_masterdata/datamodel/object_types.py`.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
str: Python code for the object types.
|
|
293
|
+
"""
|
|
294
|
+
lines = []
|
|
295
|
+
class_names: dict = {}
|
|
296
|
+
|
|
297
|
+
# Add imports at the top
|
|
298
|
+
lines.append(
|
|
299
|
+
"from bam_masterdata.metadata.definitions import ObjectTypeDef, PropertyTypeAssignment"
|
|
300
|
+
)
|
|
301
|
+
lines.append("from bam_masterdata.metadata.entities import ObjectType")
|
|
302
|
+
lines.append("")
|
|
303
|
+
lines.append("")
|
|
304
|
+
|
|
305
|
+
# Process each object type
|
|
306
|
+
for code, data in self.objects.items():
|
|
307
|
+
# Skip the "UNKNOWN" object type
|
|
308
|
+
if code == "UNKNOWN":
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
# Determine parent class
|
|
312
|
+
parent_code, parent_class, class_name = self.determine_parent_class(
|
|
313
|
+
code=code, class_names=class_names, default="ObjectType", lines=lines
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Add class definition
|
|
317
|
+
lines.append(f"class {class_name}({parent_class}):")
|
|
318
|
+
lines.append(" defs = ObjectTypeDef(")
|
|
319
|
+
lines.append(f' code="{code}",')
|
|
320
|
+
description = (
|
|
321
|
+
(data.get("description") or "")
|
|
322
|
+
.replace('"', "`")
|
|
323
|
+
.replace("\n", "\\n")
|
|
324
|
+
.replace("'", "`")
|
|
325
|
+
)
|
|
326
|
+
lines.append(f' description="""{description}""",')
|
|
327
|
+
lines.append(
|
|
328
|
+
f' generated_code_prefix="{data.get("generatedCodePrefix", "")}",'
|
|
329
|
+
)
|
|
330
|
+
lines.append(" )")
|
|
331
|
+
lines.append("")
|
|
332
|
+
|
|
333
|
+
# Add properties
|
|
334
|
+
self.add_properties(self.objects, parent_code, data, lines)
|
|
335
|
+
# Add newline between classes
|
|
336
|
+
lines.append("")
|
|
337
|
+
|
|
338
|
+
return "\n".join(lines)
|
|
339
|
+
|
|
340
|
+
def generate_vocabulary_types(self) -> str:
|
|
341
|
+
"""
|
|
342
|
+
Generate Python code for the vocabulary types in the Openbis datamodel. The code is generated
|
|
343
|
+
as a string which is then printed out to the specific Python module in `bam_masterdata/datamodel/vocabulary_types.py`.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
str: Python code for the vocabulary types.
|
|
347
|
+
"""
|
|
348
|
+
lines = []
|
|
349
|
+
class_names: dict = {}
|
|
350
|
+
|
|
351
|
+
# Add imports at the top
|
|
352
|
+
lines.append(
|
|
353
|
+
"from bam_masterdata.metadata.definitions import VocabularyTerm, VocabularyTypeDef"
|
|
354
|
+
)
|
|
355
|
+
lines.append("from bam_masterdata.metadata.entities import VocabularyType")
|
|
356
|
+
lines.append("")
|
|
357
|
+
lines.append("")
|
|
358
|
+
|
|
359
|
+
# Process each object type
|
|
360
|
+
for code, data in self.vocabularies.items():
|
|
361
|
+
# Skip the "UNKNOWN" object type
|
|
362
|
+
if code == "UNKNOWN":
|
|
363
|
+
continue
|
|
364
|
+
|
|
365
|
+
# Determine parent class
|
|
366
|
+
parent_code, parent_class, class_name = self.determine_parent_class(
|
|
367
|
+
code=code,
|
|
368
|
+
class_names=class_names,
|
|
369
|
+
default="VocabularyType",
|
|
370
|
+
lines=lines,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Add class definition
|
|
374
|
+
lines.append(f"class {class_name}({parent_class}):")
|
|
375
|
+
lines.append(" defs = VocabularyTypeDef(")
|
|
376
|
+
lines.append(f' code="{code}",')
|
|
377
|
+
description = (
|
|
378
|
+
(data.get("description") or "")
|
|
379
|
+
.replace('"', "`")
|
|
380
|
+
.replace("\n", "\\n")
|
|
381
|
+
.replace("'", "`")
|
|
382
|
+
)
|
|
383
|
+
lines.append(f' description="""{description}""",')
|
|
384
|
+
lines.append(" )")
|
|
385
|
+
lines.append("")
|
|
386
|
+
|
|
387
|
+
# Add terms
|
|
388
|
+
parent_terms = self.objects.get(parent_code, {}).get("terms", {}).keys()
|
|
389
|
+
for term_code, term_data in data.get("terms", {}).items():
|
|
390
|
+
# Skip "UNKNOWN" properties
|
|
391
|
+
if term_code == "UNKNOWN":
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
# We check if the term is inherited from the parent class
|
|
395
|
+
if term_code in parent_terms:
|
|
396
|
+
continue
|
|
397
|
+
|
|
398
|
+
term_name = (
|
|
399
|
+
term_code.lstrip("$").replace(".", "_").replace("-", "_").lower()
|
|
400
|
+
)
|
|
401
|
+
if term_name[0].isdigit():
|
|
402
|
+
term_name = f"_{term_name}"
|
|
403
|
+
if term_name == "l":
|
|
404
|
+
term_name = "L"
|
|
405
|
+
if term_name == "O":
|
|
406
|
+
term_name = "o"
|
|
407
|
+
if term_name == "I":
|
|
408
|
+
term_name = "i"
|
|
409
|
+
lines.append(f" {term_name} = VocabularyTerm(")
|
|
410
|
+
lines.append(f' code="{term_code}",')
|
|
411
|
+
label = (term_data.get("label") or "").replace('"', "")
|
|
412
|
+
lines.append(f' label="{label}",')
|
|
413
|
+
description = (
|
|
414
|
+
(term_data.get("description") or "")
|
|
415
|
+
.replace('"', "`")
|
|
416
|
+
.replace("\n", "\\n")
|
|
417
|
+
.replace("'", "`")
|
|
418
|
+
)
|
|
419
|
+
lines.append(f' description="""{description}""",')
|
|
420
|
+
lines.append(" )")
|
|
421
|
+
lines.append("")
|
|
422
|
+
|
|
423
|
+
# Add newline between classes
|
|
424
|
+
lines.append("")
|
|
425
|
+
|
|
426
|
+
return "\n".join(lines)
|
|
File without changes
|