development-framework-cli 1.7.3__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 (71) hide show
  1. development_framework_cli-1.7.3.dist-info/METADATA +141 -0
  2. development_framework_cli-1.7.3.dist-info/RECORD +71 -0
  3. development_framework_cli-1.7.3.dist-info/WHEEL +4 -0
  4. development_framework_cli-1.7.3.dist-info/entry_points.txt +2 -0
  5. development_framework_cli-1.7.3.dist-info/licenses/LICENSE +21 -0
  6. dm_cli/__init__.py +1 -0
  7. dm_cli/cli.py +180 -0
  8. dm_cli/command_group/__init__.py +0 -0
  9. dm_cli/command_group/data_source.py +176 -0
  10. dm_cli/command_group/entities.py +116 -0
  11. dm_cli/dmss.py +104 -0
  12. dm_cli/dmss_api/__init__.py +27 -0
  13. dm_cli/dmss_api/api/__init__.py +3 -0
  14. dm_cli/dmss_api/api/access_control_api.py +349 -0
  15. dm_cli/dmss_api/api/attribute_api.py +175 -0
  16. dm_cli/dmss_api/api/blob_api.py +344 -0
  17. dm_cli/dmss_api/api/blueprint_api.py +326 -0
  18. dm_cli/dmss_api/api/datasource_api.py +448 -0
  19. dm_cli/dmss_api/api/default_api.py +4439 -0
  20. dm_cli/dmss_api/api/document_api.py +922 -0
  21. dm_cli/dmss_api/api/entity_api.py +464 -0
  22. dm_cli/dmss_api/api/export_api.py +313 -0
  23. dm_cli/dmss_api/api/file_api.py +197 -0
  24. dm_cli/dmss_api/api/health_check_api.py +160 -0
  25. dm_cli/dmss_api/api/lookup_table_api.py +323 -0
  26. dm_cli/dmss_api/api/meta_api.py +185 -0
  27. dm_cli/dmss_api/api/personal_access_token_api.py +433 -0
  28. dm_cli/dmss_api/api/reference_api.py +174 -0
  29. dm_cli/dmss_api/api/search_api.py +183 -0
  30. dm_cli/dmss_api/api/whoami_api.py +163 -0
  31. dm_cli/dmss_api/api_client.py +896 -0
  32. dm_cli/dmss_api/apis/__init__.py +32 -0
  33. dm_cli/dmss_api/configuration.py +482 -0
  34. dm_cli/dmss_api/exceptions.py +158 -0
  35. dm_cli/dmss_api/model/__init__.py +5 -0
  36. dm_cli/dmss_api/model/access_control_list.py +287 -0
  37. dm_cli/dmss_api/model/access_level.py +290 -0
  38. dm_cli/dmss_api/model/acl.py +287 -0
  39. dm_cli/dmss_api/model/data_source_information.py +282 -0
  40. dm_cli/dmss_api/model/data_source_request.py +281 -0
  41. dm_cli/dmss_api/model/dependency.py +294 -0
  42. dm_cli/dmss_api/model/entity.py +275 -0
  43. dm_cli/dmss_api/model/error_response.py +278 -0
  44. dm_cli/dmss_api/model/export_meta_response.py +277 -0
  45. dm_cli/dmss_api/model/get_blueprint_response.py +280 -0
  46. dm_cli/dmss_api/model/lookup.py +283 -0
  47. dm_cli/dmss_api/model/pat_data.py +299 -0
  48. dm_cli/dmss_api/model/recipe.py +311 -0
  49. dm_cli/dmss_api/model/recipe_attribute.py +304 -0
  50. dm_cli/dmss_api/model/reference.py +280 -0
  51. dm_cli/dmss_api/model/reference_entity.py +288 -0
  52. dm_cli/dmss_api/model/repository.py +317 -0
  53. dm_cli/dmss_api/model/repository_type.py +290 -0
  54. dm_cli/dmss_api/model/storage_attribute.py +295 -0
  55. dm_cli/dmss_api/model/storage_data_types.py +292 -0
  56. dm_cli/dmss_api/model/storage_recipe.py +293 -0
  57. dm_cli/dmss_api/model_utils.py +2057 -0
  58. dm_cli/dmss_api/models/__init__.py +29 -0
  59. dm_cli/dmss_api/rest.py +358 -0
  60. dm_cli/domain.py +166 -0
  61. dm_cli/enums.py +48 -0
  62. dm_cli/import_entity.py +128 -0
  63. dm_cli/import_package.py +173 -0
  64. dm_cli/package_tree_from_zip.py +120 -0
  65. dm_cli/state.py +12 -0
  66. dm_cli/utils/__init__.py +0 -0
  67. dm_cli/utils/file_structure.py +31 -0
  68. dm_cli/utils/reference.py +129 -0
  69. dm_cli/utils/resolve_local_ids.py +142 -0
  70. dm_cli/utils/utils.py +161 -0
  71. dm_cli/utils/zip.py +70 -0
@@ -0,0 +1,128 @@
1
+ import io
2
+ import json
3
+ from json import JSONDecodeError
4
+ from pathlib import Path
5
+ from zipfile import ZipFile
6
+
7
+ from requests import Response
8
+ from rich import print
9
+ from tenacity import (
10
+ retry,
11
+ retry_if_exception_type,
12
+ retry_if_not_exception_type,
13
+ retry_if_not_result,
14
+ stop_after_attempt,
15
+ wait_random_exponential,
16
+ )
17
+
18
+ from .dmss import ApplicationException, dmss_api
19
+ from .dmss_api.exceptions import ApiException, NotFoundException, ServiceException
20
+ from .import_package import import_package_tree
21
+ from .package_tree_from_zip import package_tree_from_zip
22
+ from .state import state
23
+ from .utils.reference import replace_relative_references
24
+ from .utils.utils import (
25
+ concat_dependencies,
26
+ console,
27
+ destination_is_root,
28
+ ensure_package_structure,
29
+ )
30
+ from .utils.zip import zip_all
31
+
32
+
33
+ @retry(
34
+ wait=wait_random_exponential(multiplier=1, max=60),
35
+ stop=stop_after_attempt(5),
36
+ reraise=True,
37
+ retry=retry_if_exception_type(ServiceException),
38
+ )
39
+ def import_document(source_path: Path, destination: str, document: dict):
40
+ remote_dependencies = dmss_api.export_meta(destination)
41
+ old_dependencies = {dependency["alias"]: dependency for dependency in remote_dependencies.get("dependencies", [])}
42
+
43
+ dependencies = concat_dependencies(
44
+ new_dependencies=document.get("_meta_", {}).get("dependencies", []),
45
+ old_dependencies=old_dependencies,
46
+ filename=source_path.name,
47
+ )
48
+
49
+ # Replace references
50
+ prepared_document = replace_relative_references(document, dependencies, destination)
51
+
52
+ document_json_str = json.dumps(prepared_document)
53
+ dmss_api.document_add(
54
+ destination,
55
+ document_json_str,
56
+ files=[],
57
+ )
58
+
59
+
60
+ def import_single_entity(source_path: Path, destination: str, validate: bool = False):
61
+ ensure_package_structure(Path(destination))
62
+ print(f"Importing ENTITY '{source_path.name}' --> '{destination}'")
63
+
64
+ try: # Load the JSON document
65
+ with open(source_path, "r") as fh:
66
+ if Path(source_path).suffix == ".json":
67
+ content = json.load(fh)
68
+ if validate:
69
+ print(f"Validating {source_path}", end="")
70
+ dmss_api.validate_entity(content)
71
+ print(" [green]✓[/green]")
72
+ import_document(source_path, destination, content)
73
+ else:
74
+ print(f"Unsupported file type {source_path}")
75
+ except JSONDecodeError:
76
+ raise Exception(f"Failed to load the file '{source_path.name}' as a JSON document")
77
+
78
+
79
+ def remove_by_path_ignore_404(target: str):
80
+ try:
81
+ dmss_api.document_remove(target)
82
+ except NotFoundException:
83
+ pass
84
+
85
+
86
+ @retry(
87
+ wait=wait_random_exponential(multiplier=1, max=60),
88
+ stop=stop_after_attempt(5),
89
+ reraise=True,
90
+ retry=retry_if_exception_type(ServiceException),
91
+ )
92
+ def import_folder_entity(
93
+ source_path: Path,
94
+ destination: str,
95
+ raw_package_import: bool = False,
96
+ resolve_local_ids: bool = False,
97
+ ) -> dict:
98
+ destination_path = Path(destination)
99
+
100
+ # Check if target already exists on remote. Then delete or raise exception
101
+ target = f"{destination}/{source_path.name}"
102
+ exists = dmss_api.document_check(target)
103
+ if exists:
104
+ if not state.force:
105
+ raise ValueError(f"Failed to upload to '{target}' - It already exists.")
106
+ console.print(f"'{target}' already exists. Replacing it...", style="dark_orange")
107
+ dmss_api.document_remove(target)
108
+
109
+ dependencies = {}
110
+ is_root = destination_is_root(destination_path)
111
+ if not is_root:
112
+ ensure_package_structure(destination_path)
113
+ remote_dependencies = dmss_api.export_meta(f"{destination}")
114
+ dependencies = {dependency["alias"]: dependency for dependency in remote_dependencies.get("dependencies", [])}
115
+
116
+ memory_file = io.BytesIO()
117
+ with ZipFile(memory_file, mode="w") as zip_file:
118
+ zip_all(
119
+ zip_file,
120
+ source_path,
121
+ write_folder=True,
122
+ )
123
+ memory_file.seek(0)
124
+
125
+ package = package_tree_from_zip(
126
+ destination, memory_file, is_root=is_root, extra_dependencies=dependencies, source_path=source_path
127
+ )
128
+ import_package_tree(package, destination, raw_package_import, resolve_local_ids)
@@ -0,0 +1,173 @@
1
+ import io
2
+ import json
3
+ from json import JSONDecodeError
4
+ from pathlib import Path
5
+ from typing import Dict, List
6
+ from uuid import uuid4
7
+
8
+ from rich.console import Console
9
+ from tenacity import (
10
+ retry,
11
+ retry_if_exception_type,
12
+ retry_if_not_exception_type,
13
+ stop_after_attempt,
14
+ wait_random_exponential,
15
+ )
16
+ from tqdm import tqdm
17
+
18
+ from .dmss import ApplicationException, dmss_api
19
+ from .dmss_api.exceptions import ServiceException
20
+ from .domain import Dependency, File, Package
21
+ from .utils.reference import replace_relative_references
22
+ from .utils.resolve_local_ids import resolve_local_ids_in_document
23
+ from .utils.utils import concat_dependencies, replace_global_addresses
24
+
25
+ console = Console()
26
+
27
+
28
+ def add_object_to_package(path: Path, package: Package, object: io.BytesIO) -> None:
29
+ if len(path.parts) == 1: # End of path means the actual document
30
+ file = File(
31
+ uid=str(uuid4()), # This UID will be the data source ID for this file
32
+ name=object.name,
33
+ content=object,
34
+ path=path,
35
+ )
36
+ package.content.append(file)
37
+ return
38
+ items = [item for item in package.content if not isinstance(item, File)]
39
+ sub_folder = next((p for p in items if p["name"] == path.parts[0]), None)
40
+ if not sub_folder: # If the sub folder has not already been created on parent, create it
41
+ sub_folder = Package(name=path.parts[0], parent=package)
42
+ package.content.append(sub_folder)
43
+
44
+ new_path = str(path).split("/", 1)[1] # Remove first element in path before stepping down
45
+ return add_object_to_package(Path(new_path), sub_folder, object)
46
+
47
+
48
+ def add_file_to_package(path: Path, package: Package, document: dict) -> None:
49
+ if len(path.parts) == 1: # End of path means the actual document
50
+ if path.name.endswith("package.json"):
51
+ # if document is a package.json file, add meta info to package instead of adding it to content list.
52
+ package.meta = document.get("_meta_", {})
53
+ return
54
+ # Create a UUID if the document does not have one
55
+ package.content.append({**document, "_id": document.get("_id", str(uuid4()))})
56
+ return
57
+ items = [item for item in package.content if not isinstance(item, File)]
58
+ sub_folder = next((p for p in items if p["name"] == path.parts[0]), None)
59
+ if not sub_folder: # If the sub folder has not already been created on parent, create it
60
+ sub_folder = Package(name=path.parts[0], parent=package)
61
+ package.content.append(sub_folder)
62
+
63
+ new_path = str(path).split("/", 1)[1] # Remove first element in path before stepping down
64
+ return add_file_to_package(Path(new_path), sub_folder, document)
65
+
66
+
67
+ def add_package_to_package(path: Path, package: Package) -> None:
68
+ if len(path.parts) == 1:
69
+ package.content.append(Package(name=path.parts[0], parent=package))
70
+ return
71
+ items = [item for item in package.content if not isinstance(item, File)]
72
+ sub_folder = next((p for p in items if p["name"] == path.parts[0]), None)
73
+ if not sub_folder: # If the sub folder has not already been created on parent, create it
74
+ sub_folder = Package(name=path.parts[0], parent=package)
75
+ package.content.append(sub_folder)
76
+
77
+ new_path = str(path).split("/", 1)[1] # Remove first element in path before stepping down
78
+ return add_package_to_package(Path(new_path), sub_folder)
79
+
80
+
81
+ def import_package_tree(package: Package, destination: str, raw_package_import: bool, resolve_local_ids: bool) -> None:
82
+ destination_parts = destination.split("/")
83
+ data_source = destination_parts[0]
84
+
85
+ if raw_package_import:
86
+ dmss_api.document_add_simple(data_source, body=package.to_dict())
87
+ else:
88
+ dmss_api.document_add(
89
+ destination,
90
+ json.dumps(package.to_dict()),
91
+ files=[],
92
+ )
93
+
94
+ import_package_content(package, data_source, destination, resolve_local_ids)
95
+
96
+
97
+ @retry(
98
+ wait=wait_random_exponential(multiplier=1, max=60),
99
+ stop=stop_after_attempt(5),
100
+ reraise=True,
101
+ retry=retry_if_exception_type(ServiceException),
102
+ )
103
+ def import_package_content(package: Package, data_source: str, destination: str, resolve_local_ids: bool) -> None:
104
+ files: List[File] = []
105
+ entities: List[dict] = []
106
+ package.traverse_documents(
107
+ lambda document, **kwargs: files.append(document) if isinstance(document, File) else entities.append(document)
108
+ )
109
+ uploaded_file_ids = {}
110
+ if len(files) > 0:
111
+ with tqdm(files, desc=f" Adding files") as bar:
112
+ for file in files:
113
+ dmss_api.file_upload(data_source, json.dumps({"file_id": file.uid}), file.content)
114
+ uploaded_file_ids[f"dmss:/{file.content.destination}/{file.path.stem}"] = file.uid
115
+ bar.update()
116
+
117
+ def upload_global_file(address: str) -> str:
118
+ """Handling uploading of global files."""
119
+ filepath = Path(address)
120
+ if not filepath.is_file():
121
+ raise ApplicationException(
122
+ f"Tried to upload file with address '{address}'. The file was not found", data=package.to_dict()
123
+ )
124
+ if filepath.suffix != ".json":
125
+ # Binary files
126
+ with open(address, "rb") as f:
127
+ file_like = io.BytesIO(f.read())
128
+ file_like.name = filepath.stem
129
+ global_id = str(uuid4())
130
+ dmss_api.blob_upload(data_source, global_id, file_like)
131
+ return global_id
132
+ else:
133
+ try:
134
+ with open(address) as f:
135
+ global_document = json.load(f)
136
+ # Get dependencies from package
137
+ dependencies: Dict[str, Dependency] = {
138
+ dependency["alias"]: Dependency(**dependency)
139
+ for dependency in package.meta.get("dependencies", [])
140
+ }
141
+ # Add dependencies from entity
142
+ dependencies = concat_dependencies(
143
+ global_document.get("_meta_", {}).get("dependencies", []), dependencies, address
144
+ )
145
+ global_document = replace_relative_references(
146
+ global_document,
147
+ dependencies,
148
+ destination,
149
+ file_path=address,
150
+ )
151
+ global_id = dmss_api.document_add_simple(data_source, global_document)
152
+ return global_id
153
+ except JSONDecodeError:
154
+ raise Exception(f"Failed to load the file '{address}' as a JSON document")
155
+
156
+ if len(entities) > 0:
157
+ with tqdm(entities, desc=f" Adding entities") as bar:
158
+ for entity in entities:
159
+ document = replace_global_addresses(entity, destination, uploaded_file_ids, upload_global_file)
160
+ if resolve_local_ids:
161
+ name = f"/{document.get('name')}" if document.get("name") else f" of type {document.get('type')}"
162
+ document = resolve_local_ids_in_document(document)
163
+ print(f"Successfully resolved local IDs in:\t{destination}{name}")
164
+ dmss_api.document_add_simple(data_source, document)
165
+ bar.update()
166
+
167
+ packages: List[Package] = []
168
+ package.traverse_package(lambda package: packages.append(package))
169
+ if len(packages) > 0:
170
+ with tqdm(packages, desc=f" Adding packages") as bar:
171
+ for package in packages:
172
+ dmss_api.document_add_simple(data_source, package.to_dict())
173
+ bar.update()
@@ -0,0 +1,120 @@
1
+ import io
2
+ import json
3
+ from json import JSONDecodeError
4
+ from pathlib import Path
5
+ from typing import Dict, Union
6
+ from zipfile import ZipFile
7
+
8
+ from .domain import Dependency, File, Package
9
+ from .import_package import (
10
+ add_file_to_package,
11
+ add_object_to_package,
12
+ add_package_to_package,
13
+ )
14
+ from .utils.reference import replace_relative_references
15
+ from .utils.utils import concat_dependencies
16
+
17
+
18
+ def package_tree_from_zip(
19
+ destination: str,
20
+ zip_package: io.BytesIO,
21
+ is_root: bool = True,
22
+ extra_dependencies: Union[Dict[str, Dependency], None] = None,
23
+ source_path: Path = None,
24
+ ) -> Package:
25
+ """
26
+ Converts a Zip-folder into a DMSS Package structure.
27
+
28
+ Inserting UUID4's between any references, converting relative paths to absolute paths,
29
+ and dependencyAliases to absolute addresses.
30
+
31
+ @param destination: A string with the documentId for the target. Only a data source is allowed
32
+ @param zip_package: A zip-folder represented as an in-memory io.BytesIO object
33
+ @param source_path: path to the root folder
34
+
35
+ @return: A Package object with sub folders(Package) and documents(dict)
36
+ """
37
+
38
+ with ZipFile(zip_package) as zip_file:
39
+ folder_name = zip_file.filelist[0].filename.split("/", 1)[0]
40
+
41
+ # Find the root packages package.json file
42
+ package_file = next(
43
+ (z for z in zip_file.filelist if z.filename == f"{folder_name}/package.json"),
44
+ None,
45
+ )
46
+
47
+ package_entity = json.loads(zip_file.read(package_file.filename)) if package_file else {}
48
+ if package_file in zip_file.filelist:
49
+ zip_file.filelist.remove(package_file)
50
+ dependencies: Dict[str, Dependency] = {
51
+ dependency["alias"]: Dependency(**dependency)
52
+ for dependency in package_entity.get("_meta_", {}).get("dependencies", [])
53
+ }
54
+ if extra_dependencies:
55
+ dependencies.update(extra_dependencies)
56
+ root_package = Package(
57
+ name=package_entity.get("name", folder_name),
58
+ is_root=is_root,
59
+ meta=package_entity.get("_meta_"),
60
+ )
61
+ # Construct a nested Package object of the package to import
62
+ for file_info in zip_file.filelist:
63
+ filename = file_info.filename.split("/", 1)[1] # Remove RootPackage prefix
64
+ if file_info.is_dir():
65
+ if filename == "": # Skip rootPackage
66
+ continue
67
+ add_package_to_package(Path(filename), root_package)
68
+ continue
69
+ if Path(filename).suffix != ".json":
70
+ file_like = io.BytesIO(zip_file.read(f"{folder_name}/{filename}"))
71
+ file_like.name = Path(filename).name # stem
72
+ file_like.destination = Path(f"/{destination}/{folder_name}/{filename}").parent
73
+ add_object_to_package(Path(filename), root_package, file_like)
74
+ continue
75
+ try:
76
+ json_doc = json.loads(zip_file.read(f"{folder_name}/{filename}"))
77
+ except JSONDecodeError:
78
+ raise Exception(f"Failed to load the file '{filename}' as a JSON document")
79
+
80
+ add_file_to_package(Path(filename), root_package, json_doc)
81
+
82
+ # Add dependencies from entity to the global dependencies list
83
+ dependencies = concat_dependencies(
84
+ json_doc.get("_meta_", {}).get("dependencies", []), dependencies, filename
85
+ )
86
+
87
+ def replace(document, file_path):
88
+ if not isinstance(document, File):
89
+ document = replace_relative_references(
90
+ document,
91
+ dependencies,
92
+ destination,
93
+ file_path=file_path,
94
+ source_path=source_path,
95
+ )
96
+ return document
97
+
98
+ # Now that we have the entire package as a Package tree, traverse it, and replace relative references
99
+ root_package.traverse_documents(
100
+ lambda document, file_path: replace(document, file_path),
101
+ update=True,
102
+ )
103
+ root_package.meta = replace_relative_references(
104
+ root_package.meta,
105
+ dependencies,
106
+ destination,
107
+ file_path=root_package.path(),
108
+ source_path=source_path,
109
+ )
110
+ root_package.traverse_package(
111
+ lambda package: replace_relative_references(
112
+ package.meta,
113
+ dependencies,
114
+ destination,
115
+ file_path=package.path(),
116
+ source_path=source_path,
117
+ )
118
+ )
119
+
120
+ return root_package
dm_cli/state.py ADDED
@@ -0,0 +1,12 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class State:
6
+ force: bool = False
7
+ token: str = "no-token"
8
+ dmss_url: str = "http://localhost:5000"
9
+ debug: bool = False
10
+
11
+
12
+ state = State()
File without changes
@@ -0,0 +1,31 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import List
4
+
5
+ import typer
6
+
7
+ DATA_SOURCE_DEF_FILE_EXT: str = "json"
8
+
9
+
10
+ def get_app_dir_structure(path: Path) -> [Path, Path]:
11
+ app_dir = Path(path)
12
+ if not app_dir.is_dir():
13
+ raise FileNotFoundError(f"The path '{path}' is not a directory.")
14
+
15
+ # Check for presence of expected directories, 'data_sources' and 'data'
16
+ data_sources_dir = path.joinpath("data_sources")
17
+ data_dir = path.joinpath("data")
18
+ if not data_sources_dir.is_dir() or not data_dir.is_dir():
19
+ print(f"The directory '{path.name}' does not have the expected structure. It should contain two directories;")
20
+ print("""
21
+ ├── data
22
+ └── data_sources
23
+ """)
24
+ raise typer.Exit(code=1)
25
+
26
+ return data_sources_dir, data_dir
27
+
28
+
29
+ def get_json_files_in_dir(path: Path) -> List[str]:
30
+ """Get all JSON files found in <path>."""
31
+ return [filename for filename in os.listdir(path) if filename.endswith(f".{DATA_SOURCE_DEF_FILE_EXT}")]
@@ -0,0 +1,129 @@
1
+ from os.path import normpath
2
+ from pathlib import Path
3
+ from typing import Dict, List, Union
4
+
5
+ from ..dmss import ApplicationException
6
+ from ..domain import Dependency
7
+ from ..enums import SIMOS, BuiltinDataTypes, ReferenceTypes
8
+ from .utils import resolve_dependency
9
+
10
+
11
+ def resolve_reference(
12
+ reference: str, dependencies: Dict[str, Dependency], destination: str, file_path: str | None
13
+ ) -> str:
14
+ destination = destination.rstrip("/")
15
+ root_package = file_path.split("/", 1)[0] if file_path else ""
16
+ try:
17
+ if "://" in reference or reference == "_default_" or reference[0] == "^" or reference[0] == "~":
18
+ return reference
19
+ if ":" in reference:
20
+ return resolve_dependency(reference, dependencies)
21
+ if reference[0] == ".":
22
+ normalized_dotted_ref: str = normpath(f"{file_path}/{reference}")
23
+ return f"dmss://{destination}/{normalized_dotted_ref}"
24
+ if reference[0] == "/":
25
+ data_source = destination.split("/")[0]
26
+ return f"dmss://{data_source}{reference}"
27
+ return f"dmss://{destination}/{root_package}/{reference}"
28
+ except ApplicationException as ex:
29
+ ex.debug = f"File: {file_path}, Destination: {destination}, Root: {root_package}"
30
+ raise ex
31
+
32
+
33
+ def replace_relative_references(
34
+ value: dict | list,
35
+ dependencies: Dict[str, Dependency],
36
+ destination: str,
37
+ file_path: Union[str, None] = None,
38
+ source_path: Path = None,
39
+ ) -> Union[str, List[str], dict]:
40
+ """
41
+ Takes a dict or list, and returns the passed value, with
42
+ relative references replaced with absolute ones.
43
+
44
+ For Blob-entities; insert the binary data from the file into the entity.
45
+ It digs down on complex types
46
+
47
+ @param value: Dict or list of an entity
48
+ @param dependencies: A dict containing the dependencies of the document
49
+ @param destination: The name of the data source where the document should be stored
50
+ @param file_path: The path to the directory containing the documents
51
+
52
+ When importing packages, the following additional parameter is required:
53
+ @param zip_file:
54
+ @param source_path: path to the root folder
55
+ """
56
+
57
+ def _resolve_reference(inner_value: str | None):
58
+ if not inner_value:
59
+ return inner_value
60
+ return resolve_reference(
61
+ inner_value,
62
+ dependencies,
63
+ destination,
64
+ file_path,
65
+ )
66
+
67
+ def _replace_relative(inner_value: dict | list):
68
+ return replace_relative_references(inner_value, dependencies, destination, file_path, source_path)
69
+
70
+ if isinstance(value, dict):
71
+ if not value:
72
+ return value
73
+
74
+ if not value.get("type"):
75
+ raise KeyError(f"Object is missing the required 'type' attribute. File: '{file_path}'")
76
+
77
+ value["type"] = _resolve_reference(value["type"])
78
+
79
+ match _resolve_reference(value["type"]):
80
+ case SIMOS.REFERENCE.value:
81
+ if value["referenceType"] == ReferenceTypes.LINK.value:
82
+ value["address"] = _resolve_reference(value["address"])
83
+ else:
84
+ # Handle storage references
85
+ local_file_path = "/".join(str(source_path).split("/")[:-1])
86
+ if value["address"][0] == ".":
87
+ raise ApplicationException(
88
+ f"Relative references by . are not supported", data=value, debug=file_path
89
+ )
90
+ value["address"] = f"{local_file_path}{value['address']}"
91
+ return value
92
+
93
+ case SIMOS.ATTRIBUTE.value:
94
+ if enum_type := value.get("enumType"):
95
+ value["enumType"] = _resolve_reference(enum_type)
96
+ if value["attributeType"] not in [data_type.value for data_type in BuiltinDataTypes]:
97
+ value["attributeType"] = _resolve_reference(value["attributeType"])
98
+ if default := value.get("default"):
99
+ value["default"] = _replace_relative(default)
100
+ return value
101
+
102
+ case SIMOS.BLUEPRINT.value:
103
+ value["extends"] = [_resolve_reference(ext_from) for ext_from in value.get("extends", [])]
104
+ value["attributes"] = [_replace_relative(attr) for attr in value.get("attributes", [])]
105
+ if meta := value.get("_meta_"):
106
+ value["_meta_"] = _replace_relative(meta)
107
+ return value
108
+
109
+ case SIMOS.RECIPE_LINK.value:
110
+ value["_blueprintPath_"] = _resolve_reference(value["_blueprintPath_"])
111
+ if initial_recipe := value.get("initialUiRecipe"):
112
+ value["initialUiRecipe"] = _replace_relative(initial_recipe)
113
+ if uiRecipes := value.get("uiRecipes"):
114
+ value["uiRecipes"] = _replace_relative(uiRecipes)
115
+ return value
116
+
117
+ case _: # The value is a dict, but of unknown type. Need to dig through it recursively
118
+ for key, inner_value in value.items():
119
+ if isinstance(inner_value, dict) or isinstance(inner_value, list):
120
+ value[key] = _replace_relative(inner_value)
121
+ return value
122
+
123
+ if isinstance(value, list):
124
+ if value and (isinstance(value[0], dict) or isinstance(value[0], list)):
125
+ return [_replace_relative(v) for v in value]
126
+ # It's an empty or primitive list. Dig no further
127
+ return value
128
+
129
+ raise ValueError(f"Function can only be called on dicts and lists. Got {value}")