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
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# file generated by setuptools_scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '0.0.0'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 0, 0)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .cli import cli
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from decouple import config as environ
|
|
8
|
+
from openpyxl import Workbook
|
|
9
|
+
|
|
10
|
+
from bam_masterdata.cli.entities_to_excel import entities_to_excel
|
|
11
|
+
from bam_masterdata.cli.entities_to_json import entities_to_json
|
|
12
|
+
from bam_masterdata.cli.fill_masterdata import MasterdataCodeGenerator
|
|
13
|
+
from bam_masterdata.logger import logger
|
|
14
|
+
from bam_masterdata.utils import (
|
|
15
|
+
delete_and_create_dir,
|
|
16
|
+
import_module,
|
|
17
|
+
listdir_py_modules,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
DATAMODEL_DIR = os.path.join(".", "bam_masterdata", "datamodel")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.group(help="Entry point to run `bam_masterdata` CLI commands.")
|
|
24
|
+
def cli():
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@cli.command(
|
|
29
|
+
name="fill_masterdata",
|
|
30
|
+
help="Fill the masterdata from the openBIS instance and stores it in the bam_masterdata/datamodel/ modules.",
|
|
31
|
+
)
|
|
32
|
+
@click.option(
|
|
33
|
+
"--url",
|
|
34
|
+
type=str,
|
|
35
|
+
required=False,
|
|
36
|
+
help="""
|
|
37
|
+
(Optional) The URL of the openBIS instance from which to extract the data model. If not defined,
|
|
38
|
+
it is using the value of the `OPENBIS_URL` environment variable.
|
|
39
|
+
""",
|
|
40
|
+
)
|
|
41
|
+
def fill_masterdata(url):
|
|
42
|
+
start_time = time.time()
|
|
43
|
+
|
|
44
|
+
# ! this takes a lot of time loading all the entities in Openbis
|
|
45
|
+
# Use the URL if provided, otherwise fall back to defaults
|
|
46
|
+
if not url:
|
|
47
|
+
url = environ("OPENBIS_URL")
|
|
48
|
+
click.echo(f"Using the openBIS instance: {url}\n")
|
|
49
|
+
generator = MasterdataCodeGenerator(url=url)
|
|
50
|
+
|
|
51
|
+
# Add each module to the `bam_masterdata/datamodel` directory
|
|
52
|
+
for module_name in ["property", "collection", "dataset", "object", "vocabulary"]:
|
|
53
|
+
module_start_time = time.perf_counter() # more precise time measurement
|
|
54
|
+
output_file = Path(os.path.join(DATAMODEL_DIR, f"{module_name}_types.py"))
|
|
55
|
+
|
|
56
|
+
# Get the method from `MasterdataCodeGenerator`
|
|
57
|
+
code = getattr(generator, f"generate_{module_name}_types")()
|
|
58
|
+
code = code.rstrip("\n") + "\n"
|
|
59
|
+
output_file.write_text(code, encoding="utf-8")
|
|
60
|
+
module_elapsed_time = time.perf_counter() - module_start_time
|
|
61
|
+
click.echo(
|
|
62
|
+
f"Generated {module_name} types in {module_elapsed_time:.2f} seconds in {output_file}\n"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
elapsed_time = time.time() - start_time
|
|
66
|
+
click.echo(f"Generated all types in {elapsed_time:.2f} seconds\n\n")
|
|
67
|
+
|
|
68
|
+
# ! for some reason this ruff is not working; apply after using the CLI
|
|
69
|
+
# try:
|
|
70
|
+
# # Run ruff check
|
|
71
|
+
# click.echo("Running `ruff check .`...")
|
|
72
|
+
# subprocess.run(["ruff", "check", "."], check=True)
|
|
73
|
+
|
|
74
|
+
# # Run ruff format
|
|
75
|
+
# click.echo("Running `ruff format .`...")
|
|
76
|
+
# subprocess.run(["ruff", "format", "."], check=True)
|
|
77
|
+
# except subprocess.CalledProcessError as e:
|
|
78
|
+
# click.echo(f"Error during ruff execution: {e}", err=True)
|
|
79
|
+
# else:
|
|
80
|
+
# click.echo("Ruff checks and formatting completed successfully!")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@cli.command(
|
|
84
|
+
name="export_to_json",
|
|
85
|
+
help="Export entities to JSON files to the `./artifacts/` folder.",
|
|
86
|
+
)
|
|
87
|
+
@click.option(
|
|
88
|
+
"--force-delete",
|
|
89
|
+
type=bool,
|
|
90
|
+
required=False,
|
|
91
|
+
default=False,
|
|
92
|
+
help="""
|
|
93
|
+
(Optional) If set to `True`, it will delete the current `./artifacts/` folder and create a new one. Default is `False`.
|
|
94
|
+
""",
|
|
95
|
+
)
|
|
96
|
+
@click.option(
|
|
97
|
+
"--python-path",
|
|
98
|
+
type=str,
|
|
99
|
+
required=False,
|
|
100
|
+
default=DATAMODEL_DIR,
|
|
101
|
+
help="""
|
|
102
|
+
(Optional) The path to the individual Python module or the directory containing the Python modules to process the datamodel.
|
|
103
|
+
Default is `./bam_masterdata/datamodel/`.
|
|
104
|
+
""",
|
|
105
|
+
)
|
|
106
|
+
def export_to_json(force_delete, python_path):
|
|
107
|
+
# Get the directories from the Python modules and the export directory for the static artifacts
|
|
108
|
+
export_dir = os.path.join(".", "artifacts")
|
|
109
|
+
|
|
110
|
+
# Delete and create the export directory
|
|
111
|
+
delete_and_create_dir(
|
|
112
|
+
directory_path=export_dir,
|
|
113
|
+
logger=logger,
|
|
114
|
+
force_delete=force_delete,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Get the Python modules to process the datamodel
|
|
118
|
+
py_modules = listdir_py_modules(directory_path=python_path, logger=logger)
|
|
119
|
+
|
|
120
|
+
# Process each module using the `to_json` method of each entity
|
|
121
|
+
for module_path in py_modules:
|
|
122
|
+
entities_to_json(module_path=module_path, export_dir=export_dir, logger=logger)
|
|
123
|
+
|
|
124
|
+
click.echo(f"All entity artifacts have been generated and saved to {export_dir}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@cli.command(
|
|
128
|
+
name="export_to_excel",
|
|
129
|
+
help="Export entities to an Excel file in the path `./artifacts/masterdata.xlsx`.",
|
|
130
|
+
)
|
|
131
|
+
@click.option(
|
|
132
|
+
"--force-delete",
|
|
133
|
+
type=bool,
|
|
134
|
+
required=False,
|
|
135
|
+
default=False,
|
|
136
|
+
help="""
|
|
137
|
+
(Optional) If set to `True`, it will delete the current `./artifacts/` folder and create a new one. Default is `False`.
|
|
138
|
+
""",
|
|
139
|
+
)
|
|
140
|
+
@click.option(
|
|
141
|
+
"--python-path",
|
|
142
|
+
type=str,
|
|
143
|
+
required=False,
|
|
144
|
+
default=DATAMODEL_DIR,
|
|
145
|
+
help="""
|
|
146
|
+
(Optional) The path to the individual Python module or the directory containing the Python modules to process the datamodel.
|
|
147
|
+
Default is `./bam_masterdata/datamodel/`.
|
|
148
|
+
""",
|
|
149
|
+
)
|
|
150
|
+
def export_to_excel(force_delete, python_path):
|
|
151
|
+
# Get the directories from the Python modules and the export directory for the static artifacts
|
|
152
|
+
export_dir = os.path.join(".", "artifacts")
|
|
153
|
+
|
|
154
|
+
# Delete and create the export directory
|
|
155
|
+
delete_and_create_dir(
|
|
156
|
+
directory_path=export_dir,
|
|
157
|
+
logger=logger,
|
|
158
|
+
force_delete=force_delete,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Get the Python modules to process the datamodel
|
|
162
|
+
py_modules = listdir_py_modules(directory_path=python_path, logger=logger)
|
|
163
|
+
|
|
164
|
+
# Load the definitions module classes
|
|
165
|
+
definitions_module = import_module(
|
|
166
|
+
module_path="./bam_masterdata/metadata/definitions.py"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Process the modules and save the entities to the openBIS masterdata Excel file
|
|
170
|
+
masterdata_file = os.path.join(".", "artifacts", "masterdata.xlsx")
|
|
171
|
+
wb = Workbook()
|
|
172
|
+
for i, module_path in enumerate(py_modules):
|
|
173
|
+
if i == 0:
|
|
174
|
+
ws = wb.active
|
|
175
|
+
else:
|
|
176
|
+
ws = wb.create_sheet()
|
|
177
|
+
ws.title = (
|
|
178
|
+
os.path.basename(module_path)
|
|
179
|
+
.capitalize()
|
|
180
|
+
.replace(".py", "")
|
|
181
|
+
.replace("_", " ")
|
|
182
|
+
)
|
|
183
|
+
entities_to_excel(
|
|
184
|
+
worksheet=ws,
|
|
185
|
+
module_path=module_path,
|
|
186
|
+
definitions_module=definitions_module,
|
|
187
|
+
)
|
|
188
|
+
wb.save(masterdata_file)
|
|
189
|
+
|
|
190
|
+
click.echo(f"All masterdata have been generated and saved to {masterdata_file}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
cli()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from openpyxl.worksheet.worksheet import Worksheet
|
|
6
|
+
|
|
7
|
+
from bam_masterdata.utils import import_module
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def entities_to_excel(
|
|
11
|
+
worksheet: "Worksheet",
|
|
12
|
+
module_path: str,
|
|
13
|
+
definitions_module: Any,
|
|
14
|
+
) -> None:
|
|
15
|
+
"""
|
|
16
|
+
Export entities to the Excel file. The Python modules are imported using the function `import_module`,
|
|
17
|
+
and their contents are inspected (using `inspect`) to find the classes in the datamodel containing
|
|
18
|
+
`defs` and with a `to_json` method defined. Each row is then appended to the `worksheet`.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
worksheet (Worksheet): The worksheet to append the entities.
|
|
22
|
+
module_path (str): Path to the Python module file.
|
|
23
|
+
definitions_module (Any): The module containing the definitions of the entities. This is used
|
|
24
|
+
to match the header definitions of the entities.
|
|
25
|
+
"""
|
|
26
|
+
def_members = inspect.getmembers(definitions_module, inspect.isclass)
|
|
27
|
+
module = import_module(module_path=module_path)
|
|
28
|
+
|
|
29
|
+
# Special case of `PropertyTypeDef` in `property_types.py`
|
|
30
|
+
if "property_types.py" in module_path:
|
|
31
|
+
for name, obj in inspect.getmembers(module):
|
|
32
|
+
if name.startswith("_") or name == "PropertyTypeDef":
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
# Entity title
|
|
36
|
+
worksheet.append([obj.excel_name])
|
|
37
|
+
|
|
38
|
+
# Entity header definitions and values
|
|
39
|
+
worksheet.append(obj.excel_headers)
|
|
40
|
+
row = []
|
|
41
|
+
for f_set in obj.model_fields.keys():
|
|
42
|
+
if f_set == "data_type":
|
|
43
|
+
val = obj.data_type.value
|
|
44
|
+
else:
|
|
45
|
+
val = getattr(obj, f_set)
|
|
46
|
+
row.append(val)
|
|
47
|
+
worksheet.append(row)
|
|
48
|
+
worksheet.append([""]) # empty row after entity definitions
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
# All other datamodel modules
|
|
52
|
+
for _, obj in inspect.getmembers(module, inspect.isclass):
|
|
53
|
+
# Ensure the class has the `to_json` method
|
|
54
|
+
if not hasattr(obj, "defs") or not callable(getattr(obj, "to_json")):
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
obj_instance = obj()
|
|
58
|
+
|
|
59
|
+
# Entity title
|
|
60
|
+
obj_definitions = obj_instance.defs
|
|
61
|
+
worksheet.append([obj_definitions.excel_name])
|
|
62
|
+
|
|
63
|
+
# Entity header definitions and values
|
|
64
|
+
for def_name, def_cls in def_members:
|
|
65
|
+
if def_name == obj_definitions.name:
|
|
66
|
+
break
|
|
67
|
+
worksheet.append(obj_definitions.excel_headers)
|
|
68
|
+
header_values = [
|
|
69
|
+
getattr(obj_definitions, f_set) for f_set in def_cls.model_fields.keys()
|
|
70
|
+
]
|
|
71
|
+
worksheet.append(header_values)
|
|
72
|
+
|
|
73
|
+
# Properties assignment for ObjectType, DatasetType, and CollectionType
|
|
74
|
+
if obj_instance.cls_name in ["ObjectType", "DatasetType", "CollectionType"]:
|
|
75
|
+
if not obj_instance.properties:
|
|
76
|
+
continue
|
|
77
|
+
worksheet.append(obj_instance.properties[0].excel_headers)
|
|
78
|
+
for prop in obj_instance.properties:
|
|
79
|
+
row = []
|
|
80
|
+
for f_set in prop.model_fields.keys():
|
|
81
|
+
if f_set == "data_type":
|
|
82
|
+
val = prop.data_type.value
|
|
83
|
+
else:
|
|
84
|
+
val = getattr(prop, f_set)
|
|
85
|
+
row.append(val)
|
|
86
|
+
worksheet.append(row)
|
|
87
|
+
# Terms assignment for VocabularyType
|
|
88
|
+
elif obj_instance.cls_name == "VocabularyType":
|
|
89
|
+
if not obj_instance.terms:
|
|
90
|
+
continue
|
|
91
|
+
worksheet.append(obj_instance.terms[0].excel_headers)
|
|
92
|
+
for term in obj_instance.terms:
|
|
93
|
+
worksheet.append(
|
|
94
|
+
getattr(term, f_set) for f_set in term.model_fields.keys()
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# ? do the PropertyTypeDef need to be exported to Excel?
|
|
98
|
+
|
|
99
|
+
worksheet.append([""]) # empty row after entity definitions
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from structlog._config import BoundLoggerLazyProxy
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from bam_masterdata.utils import delete_and_create_dir, import_module
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def entities_to_json(
|
|
15
|
+
module_path: str, export_dir: str, logger: "BoundLoggerLazyProxy"
|
|
16
|
+
) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Export entities to JSON files. The Python modules are imported using the function `import_module`,
|
|
19
|
+
and their contents are inspected (using `inspect`) to find the classes in the datamodel containing
|
|
20
|
+
`defs` and with a `to_json` method defined.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
module_path (str): Path to the Python module file.
|
|
24
|
+
export_dir (str): Path to the directory where the JSON files will be saved.
|
|
25
|
+
logger (BoundLoggerLazyProxy): The logger to log messages.
|
|
26
|
+
"""
|
|
27
|
+
module = import_module(module_path=module_path)
|
|
28
|
+
# export to specific subfolders for each type of entity (each module)
|
|
29
|
+
module_export_dir = os.path.join(
|
|
30
|
+
export_dir, os.path.basename(module_path).replace(".py", "")
|
|
31
|
+
)
|
|
32
|
+
delete_and_create_dir(directory_path=module_export_dir, logger=logger)
|
|
33
|
+
|
|
34
|
+
# Special case of `PropertyTypeDef` in `property_types.py`
|
|
35
|
+
if "property_types.py" in module_path:
|
|
36
|
+
for name, obj in inspect.getmembers(module):
|
|
37
|
+
if name.startswith("_") or name == "PropertyTypeDef":
|
|
38
|
+
continue
|
|
39
|
+
try:
|
|
40
|
+
json_data = json.dumps(obj.model_dump(), indent=2)
|
|
41
|
+
output_file = os.path.join(module_export_dir, f"{obj.code}.json")
|
|
42
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
43
|
+
f.write(json_data)
|
|
44
|
+
|
|
45
|
+
click.echo(f"Saved JSON for class {name} to {output_file}")
|
|
46
|
+
except Exception as err:
|
|
47
|
+
click.echo(f"Failed to process class {name} in {module_path}: {err}")
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
# All other datamodel modules
|
|
51
|
+
for name, obj in inspect.getmembers(module, inspect.isclass):
|
|
52
|
+
# Ensure the class has the `to_json` method
|
|
53
|
+
if not hasattr(obj, "defs") or not callable(getattr(obj, "to_json")):
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
# Instantiate the class and call the method
|
|
58
|
+
json_data = obj().to_json(indent=2)
|
|
59
|
+
|
|
60
|
+
# Write JSON data to file
|
|
61
|
+
output_file = os.path.join(module_export_dir, f"{obj.defs.code}.json")
|
|
62
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
63
|
+
f.write(json_data)
|
|
64
|
+
|
|
65
|
+
click.echo(f"Saved JSON for class {name} to {output_file}")
|
|
66
|
+
except Exception as err:
|
|
67
|
+
click.echo(f"Failed to process class {name} in {module_path}: {err}")
|