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,142 @@
1
+ import re
2
+
3
+
4
+ def search_in_list(
5
+ document: list, pattern: str, target_attr: str, path: str, parent_was_dict: bool, targets: dict, references: dict
6
+ ):
7
+ attribute_types = map(lambda attr: (attr, type(attr)), document)
8
+ for i, at in enumerate(attribute_types):
9
+ a = at[0]
10
+ t = at[1]
11
+ if is_primitive(t):
12
+ result = search_in_primitive(a, pattern)
13
+ if result:
14
+ references[f"{path}[{i}]"] = result
15
+ else:
16
+ action = type_to_action[t]
17
+ action(a, pattern, target_attr, f"{path}[{i}]", False, targets, references)
18
+
19
+
20
+ def search_in_primitive(val, pattern: str):
21
+ match = re.match(pattern, str(val))
22
+ if match:
23
+ return match.group(1)
24
+
25
+
26
+ def is_primitive(t: type):
27
+ return t == str or t == int or t == float or t == bool
28
+
29
+
30
+ def is_list(t: type):
31
+ return t == list
32
+
33
+
34
+ def is_dict(t: type):
35
+ return t == dict
36
+
37
+
38
+ def search_in_dict(
39
+ document: dict, pattern: str, target_attr: str, path: str, parent_was_dict: bool, targets: dict, references: dict
40
+ ):
41
+ """Search for internal references in 'document', referencing to a target attribute.
42
+
43
+ Uses the methods 'search_in_list' and 'search_in_primitive' to continue searching in attributes of these types
44
+
45
+ Args:
46
+ document: the input document which contains internal references
47
+ pattern: (what does a 'reference' look like?) regex which tries to match the reference structure, defaults to
48
+ ^.$<some_id>. Is used to populate the lookup_of_references table
49
+ target_attr: (where is the reference pointing to?) the attribute which contains the id which is being referred
50
+ to is used to populate the lookup_of_targets table
51
+ path: the absolute path at each level in the search
52
+ parent_was_dict: flag to track the type of the parent
53
+ targets: dict to track discovered attributes that define local ids
54
+ references: dict to track discovered references to local ids
55
+
56
+ Returns:
57
+ Two lookup tables: 'references' dict, which mapping from the location of the reference (path), to the
58
+ local id, and 'targets' dict, which is a mapping from the local id to the location of the element which owns
59
+ that id (absolute local path)
60
+ """
61
+
62
+ attribute_types = map(lambda attr: (attr, type(document[attr])), list(document.keys()))
63
+ for a, t in attribute_types:
64
+ val = document[a]
65
+ if is_primitive(t):
66
+ if a == target_attr:
67
+ parent_path = path.rsplit("[", 1)[0]
68
+ final_path = path if parent_was_dict else f"{parent_path}({target_attr}={val})"
69
+ targets[val] = final_path
70
+ result = search_in_primitive(val, pattern)
71
+ if result:
72
+ references[f"{path}.{a}"] = result
73
+ else:
74
+ action = type_to_action[t]
75
+ action(document[a], pattern, target_attr, f"{path}.{a}", True, targets, references)
76
+ return targets, references
77
+
78
+
79
+ def _dig_and_replace(document, path_iterator: iter, replace: str):
80
+ attr = next(path_iterator, None)
81
+ if not attr:
82
+ return True
83
+ attr = int(attr) if is_list(type(document)) else attr
84
+ last_attribute = _dig_and_replace(document[attr], path_iterator, replace)
85
+ if last_attribute:
86
+ document[attr] = replace
87
+
88
+
89
+ def dig_and_replace(targets: dict, references: dict, document):
90
+ """Transform references in 'references' to the absolute (local) path found in 'targets'
91
+
92
+ Traverses the document based on the path found in 'references' lookup table, and updates the entry with the
93
+ corresponding value found in the 'targets' lookup table (the local path to the target attribute)
94
+
95
+ Args:
96
+ references: mapping from the location of the reference (path), to the local id
97
+ targets: mapping from the local id to the location of the element which owns that id
98
+ document: dictionary document which contains references we want to transform/replace
99
+
100
+ Returns:
101
+ The final document with all the local id references resolved into local path references
102
+
103
+ """
104
+ for path, ref in references.items():
105
+ path_items = re.split("[\.\[\]]+", path)[1:] # splits path on the '.', '[' and ']' characters
106
+ path_iterator = iter(path_items)
107
+ _dig_and_replace(document, path_iterator, targets[ref])
108
+ return document
109
+
110
+
111
+ def resolve_local_ids_in_document(
112
+ document: dict,
113
+ pattern: str = r"\^\.\$(\w+)",
114
+ target_attr: str = "_id",
115
+ path: str = "^",
116
+ parent_was_dict: bool = True,
117
+ ):
118
+ """Transform local_id references in 'document' to absolute (local) paths
119
+
120
+ e.g.: customers[0].car.address points to cars[0][0]
121
+
122
+ (local id reference) (absolute path reference)
123
+ "address": ^.$12345 -> "address": ^.cars[0](_id=12345)
124
+
125
+ Args:
126
+ document: the input document which contains internal references
127
+ pattern: (what does a 'reference' look like?) regex which tries to match the reference structure, defaults to
128
+ ^.$<some_id>. Is used to populate the lookup_of_references table
129
+ target_attr: (where is the reference pointing to?) the attribute which contains the id which is being referred
130
+ to is used to populate the lookup_of_targets table
131
+ path: the absolute path at each level in the search
132
+ parent_was_dict: flag to track the type of the parent
133
+
134
+ Returns:
135
+ The final document with all the local id references resolved into local path references
136
+ """
137
+
138
+ targets, references = search_in_dict(document, pattern, target_attr, path, parent_was_dict, {}, {})
139
+ return dig_and_replace(targets, references, document)
140
+
141
+
142
+ type_to_action = {list: search_in_list, dict: search_in_dict}
dm_cli/utils/utils.py ADDED
@@ -0,0 +1,161 @@
1
+ import json
2
+ import os
3
+ import pprint
4
+ from pathlib import Path
5
+ from typing import Callable, Dict, List
6
+
7
+ import typer
8
+ from rich import print
9
+ from rich.console import Console
10
+
11
+ from dm_cli.dmss import ApplicationException, dmss_api
12
+ from dm_cli.dmss_api.exceptions import ApiException, NotFoundException
13
+ from dm_cli.utils.file_structure import get_app_dir_structure, get_json_files_in_dir
14
+
15
+ from ..domain import Dependency, Package
16
+ from ..enums import SIMOS
17
+
18
+ console = Console()
19
+
20
+
21
+ def resolve_dependency(type_ref: str, dependencies: Dict[str, Dependency]) -> str:
22
+ """Takes a type reference and dependencies. Returns the address for
23
+ the Blueprint, prefixed with which protocol should be used to fetch it.
24
+ Expected format is ALIAS:ADDRESS"""
25
+ tag, path = type_ref.split(":", 1)
26
+ path = path.strip(" /")
27
+ dependency = dependencies.get(tag)
28
+ if not dependency:
29
+ raise ApplicationException(f"No dependency with alias '{tag}' was found in the entities dependencies list")
30
+ address = dependency.address.strip(" /")
31
+
32
+ if dependency.protocol == "dmss":
33
+ return f"dmss://{address}/{path}"
34
+ if dependency.protocol == "http":
35
+ return f"http://{address}/{path}"
36
+
37
+ raise ApplicationException(f"Protocol '{dependency.protocol}' is not a valid protocol for resolving dependencies")
38
+
39
+
40
+ def concat_dependencies(
41
+ new_dependencies: List[dict], old_dependencies: Dict[str, Dependency], filename: str
42
+ ) -> Dict[str, Dependency]:
43
+ entity_dependencies = {dependency["alias"]: Dependency(**dependency) for dependency in new_dependencies}
44
+ alias_intersect = entity_dependencies.keys() & old_dependencies.keys()
45
+
46
+ # If there are duplicated aliases, raise error if they are not identical to the existing one
47
+ for duplicated_alias in alias_intersect:
48
+ if entity_dependencies[duplicated_alias] != old_dependencies[duplicated_alias]:
49
+ raise ApplicationException(f"Conflicting dependency alias(es) in file '{filename}'. '{alias_intersect}'")
50
+ old_dependencies.update(entity_dependencies)
51
+ return old_dependencies
52
+
53
+
54
+ def document_already_exists(api_exception: ApiException) -> bool:
55
+ error = json.loads(api_exception.body)
56
+ if error.get("type") == "BadRequestException" and "already exists" in error.get("message"):
57
+ print(f"ERROR: {error.get('message')}")
58
+ return True
59
+ return False
60
+
61
+
62
+ def add_package_to_path(name: str, path: Path):
63
+ package = Package(name, is_root=len(path.parts) == 2)
64
+ dmss_api.document_add(
65
+ str(path.parent),
66
+ json.dumps(package.to_dict()),
67
+ files=[],
68
+ )
69
+
70
+
71
+ def replace_global_addresses(
72
+ document: dict, data_source_id: str, files_to_upload: dict, upload_global_file: Callable
73
+ ) -> dict:
74
+ # This is for contained Files(blob data)? If so they should be embedded in the json document, not uploaded as a blob
75
+ if document["type"] == SIMOS.REFERENCE.value and document["address"] in files_to_upload:
76
+ blob_id = files_to_upload[document["address"]]
77
+ document["address"] = f"${blob_id}"
78
+ # This is for model contained, storage-uncontained documents. They are stored on disk in the "global" folder
79
+ if document["type"] == SIMOS.REFERENCE.value and document["referenceType"] == "storage":
80
+ global_id = upload_global_file(document["address"])
81
+ document["address"] = f"${global_id}"
82
+ for key, value in document.items():
83
+ if key == "_meta_": # meta data can never contain blob data.
84
+ return document
85
+ if isinstance(value, dict) and value:
86
+ document[key] = replace_global_addresses(value, data_source_id, files_to_upload, upload_global_file)
87
+ if isinstance(value, list) and value:
88
+ if len(value) > 0 and isinstance(value[0], dict):
89
+ document[key] = [
90
+ replace_global_addresses(item, data_source_id, files_to_upload, upload_global_file)
91
+ for item in document[key]
92
+ ]
93
+ return document
94
+
95
+
96
+ def destination_is_root(destination: Path) -> bool:
97
+ if len(destination.parts) > 1:
98
+ return False
99
+ return True
100
+
101
+
102
+ def ensure_package_structure(path: Path):
103
+ """Create any missing packages in the provided path (mkdir -R)"""
104
+ try:
105
+ dmss_api.document_get(f"dmss://{path}/")
106
+ except NotFoundException as e:
107
+ error = json.loads(e.body)
108
+ if error["status"] != 404:
109
+ print(f"Target package '{path}' is likely corrupt!")
110
+ pprint.pformat(error)
111
+ raise typer.Exit(code=1)
112
+
113
+ if len(path.parts) > 2: # We're at root package level. Do not check for datasource.
114
+ ensure_package_structure(path.parent)
115
+
116
+ add_package_to_path(path.name, path)
117
+ print(f"Target folder '{path.name}' was missing in '{path.parent}'. Created: ✓")
118
+
119
+
120
+ def get_root_packages_in_data_sources(path: str) -> dict:
121
+ """
122
+ Generate a dict that contains what root packages are included in a data source. Example structure:
123
+ {
124
+ "dataSourceA": [rootPackageA, rootPackageB],
125
+ "dataSourceB": [rootPackageC]
126
+ }
127
+ """
128
+ data_sources_dir, data_dir = get_app_dir_structure(Path(path))
129
+ data_source_definitions: list[str] = get_json_files_in_dir(data_sources_dir)
130
+ root_packages_in_data_sources = {}
131
+ for data_source_definition in data_source_definitions:
132
+ with open(os.path.join(data_sources_dir, data_source_definition)) as file:
133
+ data_source_document = json.load(file)
134
+ # Global folders should be uploaded directly to the data source
135
+ # and should not be included in the root packages of the data source.
136
+ global_folders = data_source_document.get("global_folders", [])
137
+ data_source = data_source_document["name"]
138
+ data_source_path = os.path.join(data_dir, data_source)
139
+ root_packages: list[str] = [
140
+ folder
141
+ for folder in os.listdir(data_source_path)
142
+ if os.path.isdir(os.path.join(data_source_path, folder)) and folder not in global_folders
143
+ ]
144
+ root_packages_in_data_sources[data_source] = root_packages
145
+ return root_packages_in_data_sources
146
+
147
+
148
+ def validate_entities_in_data_sources(data_source_contents: dict):
149
+ """Run validation on entities in data sources.
150
+ data_source_contents is a dict that contains what root packages are included in a data source. Example structure:
151
+ {
152
+ "dataSourceA": [rootPackageA, rootPackageB],
153
+ "dataSourceB": [rootPackageC]
154
+ }
155
+
156
+ """
157
+ for data_source_name in data_source_contents:
158
+ root_packages = data_source_contents[data_source_name]
159
+ for root_package_name in root_packages:
160
+ print("Validating entities in: ", f"{os.path.join(data_source_name, root_package_name)}")
161
+ dmss_api.validate_existing_entity(f"{os.path.join(data_source_name, root_package_name)}")
dm_cli/utils/zip.py ADDED
@@ -0,0 +1,70 @@
1
+ import os
2
+ from pathlib import Path
3
+ from zipfile import ZipFile
4
+
5
+ import emoji
6
+
7
+ from ..dmss import ApplicationException
8
+
9
+
10
+ def zip_all(zip_file: ZipFile, path: str, real_name="", write_folder: bool = True):
11
+ basename = os.path.basename(path)
12
+ if os.path.isdir(path):
13
+ if real_name == "":
14
+ real_name = basename
15
+ if write_folder:
16
+ zip_file.write(path, os.path.join(real_name))
17
+ for root, dirs, files in os.walk(path):
18
+ for d in dirs:
19
+ zip_all(
20
+ zip_file,
21
+ os.path.join(root, d),
22
+ os.path.join(real_name, d),
23
+ write_folder=write_folder,
24
+ )
25
+ for f in files:
26
+ zip_file.write(os.path.join(root, f), os.path.join(real_name, f))
27
+ break
28
+ elif os.path.isfile(path):
29
+ zip_file.write(path, os.path.join(real_name, basename))
30
+ else:
31
+ pass
32
+
33
+
34
+ def unpack_and_save_zipfile(export_location: str, zip_file: ZipFile):
35
+ """Unpack zipfile and save it to export_location. It is assumed that zip file only contains json files and folders.
36
+ If file or folder to export already exists, an exception is raised.
37
+ """
38
+ zip_file_unpacked_path = f"{export_location}/{zip_file.filename.rstrip('.zip')}"
39
+
40
+ zip_has_single_file_and_no_folders = (
41
+ len(zip_file.filelist) == 1 and zip_file.filelist[0].filename.split("/")[0] == ""
42
+ )
43
+ if zip_has_single_file_and_no_folders:
44
+ # If single file in the zip file (and the file is not inside a folder), the unpacked path has .json ending
45
+ # (we assume the zip file always contains json files)
46
+ zip_file_unpacked_path += ".json"
47
+
48
+ if Path(zip_file_unpacked_path).exists():
49
+ print(emoji.emojize(f"\t:error: File or folder '{zip_file_unpacked_path}' already exists. Exiting."))
50
+ raise ApplicationException("Path already exists")
51
+
52
+ zip_file.extractall(path=export_location)
53
+ print(f"Saved unpacked zip file to '{zip_file_unpacked_path}'.")
54
+
55
+
56
+ def save_as_zip_file(export_location: str, filename: str, data: str):
57
+ """Save binary data into a zip file on the local disk.
58
+ If file or folder to export already exists, an exception is raised.
59
+ """
60
+ if not filename.endswith(".zip"):
61
+ raise ApplicationException(message="file ending .zip must be included in filename!")
62
+ saved_zip_file_path = f"{export_location}/{filename}"
63
+
64
+ if Path(saved_zip_file_path).exists():
65
+ print(emoji.emojize(f"\t:error: File or folder '{saved_zip_file_path}' already exists. Exiting."))
66
+ raise ApplicationException("Path already exists")
67
+
68
+ with open(saved_zip_file_path, "wb") as file:
69
+ file.write(data)
70
+ print(f"Wrote zip file to '{saved_zip_file_path}'")