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.
Files changed (35) hide show
  1. bam_masterdata/__init__.py +0 -0
  2. bam_masterdata/_version.py +16 -0
  3. bam_masterdata/cli/__init__.py +1 -0
  4. bam_masterdata/cli/cli.py +194 -0
  5. bam_masterdata/cli/entities_to_excel.py +99 -0
  6. bam_masterdata/cli/entities_to_json.py +67 -0
  7. bam_masterdata/cli/fill_masterdata.py +426 -0
  8. bam_masterdata/datamodel/__init__.py +0 -0
  9. bam_masterdata/datamodel/collection_types.py +317 -0
  10. bam_masterdata/datamodel/dataset_types.py +1497 -0
  11. bam_masterdata/datamodel/object_types.py +13560 -0
  12. bam_masterdata/datamodel/property_types.py +6515 -0
  13. bam_masterdata/datamodel/vocabulary_types.py +81811 -0
  14. bam_masterdata/logger.py +41 -0
  15. bam_masterdata/metadata/__init__.py +0 -0
  16. bam_masterdata/metadata/definitions.py +442 -0
  17. bam_masterdata/metadata/entities.py +181 -0
  18. bam_masterdata/openbis/__init__.py +1 -0
  19. bam_masterdata/openbis/get_entities.py +181 -0
  20. bam_masterdata/openbis/login.py +18 -0
  21. bam_masterdata/utils/__init__.py +6 -0
  22. bam_masterdata/utils/utils.py +115 -0
  23. bam_masterdata-0.0.0.dist-info/METADATA +176 -0
  24. bam_masterdata-0.0.0.dist-info/RECORD +35 -0
  25. bam_masterdata-0.0.0.dist-info/WHEEL +5 -0
  26. bam_masterdata-0.0.0.dist-info/entry_points.txt +2 -0
  27. bam_masterdata-0.0.0.dist-info/top_level.txt +2 -0
  28. tests/__init__.py +0 -0
  29. tests/cli/test_entities_to_excel.py +82 -0
  30. tests/cli/test_entities_to_json.py +42 -0
  31. tests/conftest.py +146 -0
  32. tests/metadata/test_definitions.py +288 -0
  33. tests/metadata/test_entities.py +54 -0
  34. tests/test_logger.py +37 -0
  35. 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