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.
- development_framework_cli-1.7.3.dist-info/METADATA +141 -0
- development_framework_cli-1.7.3.dist-info/RECORD +71 -0
- development_framework_cli-1.7.3.dist-info/WHEEL +4 -0
- development_framework_cli-1.7.3.dist-info/entry_points.txt +2 -0
- development_framework_cli-1.7.3.dist-info/licenses/LICENSE +21 -0
- dm_cli/__init__.py +1 -0
- dm_cli/cli.py +180 -0
- dm_cli/command_group/__init__.py +0 -0
- dm_cli/command_group/data_source.py +176 -0
- dm_cli/command_group/entities.py +116 -0
- dm_cli/dmss.py +104 -0
- dm_cli/dmss_api/__init__.py +27 -0
- dm_cli/dmss_api/api/__init__.py +3 -0
- dm_cli/dmss_api/api/access_control_api.py +349 -0
- dm_cli/dmss_api/api/attribute_api.py +175 -0
- dm_cli/dmss_api/api/blob_api.py +344 -0
- dm_cli/dmss_api/api/blueprint_api.py +326 -0
- dm_cli/dmss_api/api/datasource_api.py +448 -0
- dm_cli/dmss_api/api/default_api.py +4439 -0
- dm_cli/dmss_api/api/document_api.py +922 -0
- dm_cli/dmss_api/api/entity_api.py +464 -0
- dm_cli/dmss_api/api/export_api.py +313 -0
- dm_cli/dmss_api/api/file_api.py +197 -0
- dm_cli/dmss_api/api/health_check_api.py +160 -0
- dm_cli/dmss_api/api/lookup_table_api.py +323 -0
- dm_cli/dmss_api/api/meta_api.py +185 -0
- dm_cli/dmss_api/api/personal_access_token_api.py +433 -0
- dm_cli/dmss_api/api/reference_api.py +174 -0
- dm_cli/dmss_api/api/search_api.py +183 -0
- dm_cli/dmss_api/api/whoami_api.py +163 -0
- dm_cli/dmss_api/api_client.py +896 -0
- dm_cli/dmss_api/apis/__init__.py +32 -0
- dm_cli/dmss_api/configuration.py +482 -0
- dm_cli/dmss_api/exceptions.py +158 -0
- dm_cli/dmss_api/model/__init__.py +5 -0
- dm_cli/dmss_api/model/access_control_list.py +287 -0
- dm_cli/dmss_api/model/access_level.py +290 -0
- dm_cli/dmss_api/model/acl.py +287 -0
- dm_cli/dmss_api/model/data_source_information.py +282 -0
- dm_cli/dmss_api/model/data_source_request.py +281 -0
- dm_cli/dmss_api/model/dependency.py +294 -0
- dm_cli/dmss_api/model/entity.py +275 -0
- dm_cli/dmss_api/model/error_response.py +278 -0
- dm_cli/dmss_api/model/export_meta_response.py +277 -0
- dm_cli/dmss_api/model/get_blueprint_response.py +280 -0
- dm_cli/dmss_api/model/lookup.py +283 -0
- dm_cli/dmss_api/model/pat_data.py +299 -0
- dm_cli/dmss_api/model/recipe.py +311 -0
- dm_cli/dmss_api/model/recipe_attribute.py +304 -0
- dm_cli/dmss_api/model/reference.py +280 -0
- dm_cli/dmss_api/model/reference_entity.py +288 -0
- dm_cli/dmss_api/model/repository.py +317 -0
- dm_cli/dmss_api/model/repository_type.py +290 -0
- dm_cli/dmss_api/model/storage_attribute.py +295 -0
- dm_cli/dmss_api/model/storage_data_types.py +292 -0
- dm_cli/dmss_api/model/storage_recipe.py +293 -0
- dm_cli/dmss_api/model_utils.py +2057 -0
- dm_cli/dmss_api/models/__init__.py +29 -0
- dm_cli/dmss_api/rest.py +358 -0
- dm_cli/domain.py +166 -0
- dm_cli/enums.py +48 -0
- dm_cli/import_entity.py +128 -0
- dm_cli/import_package.py +173 -0
- dm_cli/package_tree_from_zip.py +120 -0
- dm_cli/state.py +12 -0
- dm_cli/utils/__init__.py +0 -0
- dm_cli/utils/file_structure.py +31 -0
- dm_cli/utils/reference.py +129 -0
- dm_cli/utils/resolve_local_ids.py +142 -0
- dm_cli/utils/utils.py +161 -0
- dm_cli/utils/zip.py +70 -0
dm_cli/import_entity.py
ADDED
|
@@ -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)
|
dm_cli/import_package.py
ADDED
|
@@ -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
dm_cli/utils/__init__.py
ADDED
|
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}")
|