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
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}")