idmtools 0.0.0.dev0__py3-none-any.whl → 0.0.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.
- idmtools/__init__.py +27 -8
- idmtools/analysis/__init__.py +5 -0
- idmtools/analysis/add_analyzer.py +89 -0
- idmtools/analysis/analyze_manager.py +490 -0
- idmtools/analysis/csv_analyzer.py +103 -0
- idmtools/analysis/download_analyzer.py +96 -0
- idmtools/analysis/map_worker_entry.py +100 -0
- idmtools/analysis/platform_analysis_bootstrap.py +94 -0
- idmtools/analysis/platform_anaylsis.py +291 -0
- idmtools/analysis/tags_analyzer.py +93 -0
- idmtools/assets/__init__.py +9 -0
- idmtools/assets/asset.py +453 -0
- idmtools/assets/asset_collection.py +514 -0
- idmtools/assets/content_handlers.py +19 -0
- idmtools/assets/errors.py +23 -0
- idmtools/assets/file_list.py +191 -0
- idmtools/builders/__init__.py +11 -0
- idmtools/builders/arm_simulation_builder.py +152 -0
- idmtools/builders/csv_simulation_builder.py +76 -0
- idmtools/builders/simulation_builder.py +348 -0
- idmtools/builders/sweep_arm.py +109 -0
- idmtools/builders/yaml_simulation_builder.py +82 -0
- idmtools/config/__init__.py +7 -0
- idmtools/config/idm_config_parser.py +486 -0
- idmtools/core/__init__.py +10 -0
- idmtools/core/cache_enabled.py +114 -0
- idmtools/core/context.py +68 -0
- idmtools/core/docker_task.py +207 -0
- idmtools/core/enums.py +51 -0
- idmtools/core/exceptions.py +91 -0
- idmtools/core/experiment_factory.py +71 -0
- idmtools/core/id_file.py +70 -0
- idmtools/core/interfaces/__init__.py +5 -0
- idmtools/core/interfaces/entity_container.py +64 -0
- idmtools/core/interfaces/iassets_enabled.py +58 -0
- idmtools/core/interfaces/ientity.py +331 -0
- idmtools/core/interfaces/iitem.py +206 -0
- idmtools/core/interfaces/imetadata_operations.py +89 -0
- idmtools/core/interfaces/inamed_entity.py +17 -0
- idmtools/core/interfaces/irunnable_entity.py +159 -0
- idmtools/core/logging.py +387 -0
- idmtools/core/platform_factory.py +316 -0
- idmtools/core/system_information.py +104 -0
- idmtools/core/task_factory.py +145 -0
- idmtools/entities/__init__.py +10 -0
- idmtools/entities/command_line.py +229 -0
- idmtools/entities/command_task.py +155 -0
- idmtools/entities/experiment.py +787 -0
- idmtools/entities/generic_workitem.py +43 -0
- idmtools/entities/ianalyzer.py +163 -0
- idmtools/entities/iplatform.py +1106 -0
- idmtools/entities/iplatform_default.py +39 -0
- idmtools/entities/iplatform_ops/__init__.py +5 -0
- idmtools/entities/iplatform_ops/iplatform_asset_collection_operations.py +148 -0
- idmtools/entities/iplatform_ops/iplatform_experiment_operations.py +415 -0
- idmtools/entities/iplatform_ops/iplatform_simulation_operations.py +315 -0
- idmtools/entities/iplatform_ops/iplatform_suite_operations.py +322 -0
- idmtools/entities/iplatform_ops/iplatform_workflowitem_operations.py +301 -0
- idmtools/entities/iplatform_ops/utils.py +185 -0
- idmtools/entities/itask.py +316 -0
- idmtools/entities/iworkflow_item.py +167 -0
- idmtools/entities/platform_requirements.py +20 -0
- idmtools/entities/relation_type.py +14 -0
- idmtools/entities/simulation.py +255 -0
- idmtools/entities/suite.py +188 -0
- idmtools/entities/task_proxy.py +37 -0
- idmtools/entities/templated_simulation.py +325 -0
- idmtools/frozen/frozen_dict.py +71 -0
- idmtools/frozen/frozen_list.py +66 -0
- idmtools/frozen/frozen_set.py +86 -0
- idmtools/frozen/frozen_tuple.py +18 -0
- idmtools/frozen/frozen_utils.py +179 -0
- idmtools/frozen/ifrozen.py +66 -0
- idmtools/plugins/__init__.py +5 -0
- idmtools/plugins/git_commit.py +117 -0
- idmtools/registry/__init__.py +4 -0
- idmtools/registry/experiment_specification.py +105 -0
- idmtools/registry/functions.py +28 -0
- idmtools/registry/hook_specs.py +132 -0
- idmtools/registry/master_plugin_registry.py +51 -0
- idmtools/registry/platform_specification.py +138 -0
- idmtools/registry/plugin_specification.py +129 -0
- idmtools/registry/task_specification.py +104 -0
- idmtools/registry/utils.py +119 -0
- idmtools/services/__init__.py +5 -0
- idmtools/services/ipersistance_service.py +135 -0
- idmtools/services/platforms.py +13 -0
- idmtools/utils/__init__.py +5 -0
- idmtools/utils/caller.py +24 -0
- idmtools/utils/collections.py +246 -0
- idmtools/utils/command_line.py +45 -0
- idmtools/utils/decorators.py +300 -0
- idmtools/utils/display/__init__.py +22 -0
- idmtools/utils/display/displays.py +181 -0
- idmtools/utils/display/settings.py +25 -0
- idmtools/utils/dropbox_location.py +30 -0
- idmtools/utils/entities.py +127 -0
- idmtools/utils/file.py +72 -0
- idmtools/utils/file_parser.py +151 -0
- idmtools/utils/filter_simulations.py +182 -0
- idmtools/utils/filters/__init__.py +5 -0
- idmtools/utils/filters/asset_filters.py +88 -0
- idmtools/utils/general.py +286 -0
- idmtools/utils/gitrepo.py +336 -0
- idmtools/utils/hashing.py +239 -0
- idmtools/utils/info.py +124 -0
- idmtools/utils/json.py +82 -0
- idmtools/utils/language.py +107 -0
- idmtools/utils/local_os.py +40 -0
- idmtools/utils/time.py +22 -0
- idmtools-0.0.3.dist-info/METADATA +120 -0
- idmtools-0.0.3.dist-info/RECORD +116 -0
- idmtools-0.0.3.dist-info/entry_points.txt +9 -0
- idmtools-0.0.3.dist-info/licenses/LICENSE.TXT +3 -0
- idmtools-0.0.0.dev0.dist-info/METADATA +0 -41
- idmtools-0.0.0.dev0.dist-info/RECORD +0 -5
- {idmtools-0.0.0.dev0.dist-info → idmtools-0.0.3.dist-info}/WHEEL +0 -0
- {idmtools-0.0.0.dev0.dist-info → idmtools-0.0.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example of a tags analyzer to get all the tags from your experiment simulations into one csv file.
|
|
3
|
+
|
|
4
|
+
Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
|
|
5
|
+
"""
|
|
6
|
+
# First, import some necessary system and idmtools packages.
|
|
7
|
+
import os
|
|
8
|
+
from typing import Dict, Any
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from idmtools.entities import IAnalyzer
|
|
12
|
+
|
|
13
|
+
# Create a class for the analyzer
|
|
14
|
+
from idmtools.entities.ianalyzer import ANALYZABLE_ITEM
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TagsAnalyzer(IAnalyzer):
|
|
18
|
+
"""
|
|
19
|
+
Provides an analyzer for CSV output.
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
.. literalinclude:: ../../examples/analyzers/example_analysis_TagsAnalyzer.py
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# Arg option for analyzer init are uid, working_dir, parse (True to leverage the :class:`OutputParser`;
|
|
26
|
+
# False to get the raw data in the :meth:`select_simulation_data`), and filenames
|
|
27
|
+
# In this case, we want uid, working_dir, and parse=True
|
|
28
|
+
def __init__(self, uid=None, working_dir=None, parse=True, output_path="output_tag"):
|
|
29
|
+
"""
|
|
30
|
+
Initialize our Tags Analyzer.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
uid:
|
|
34
|
+
working_dir:
|
|
35
|
+
parse:
|
|
36
|
+
output_path:
|
|
37
|
+
|
|
38
|
+
See Also:
|
|
39
|
+
:class:`~idmtools.entities.ianalyzer.IAnalyzer`.
|
|
40
|
+
"""
|
|
41
|
+
super().__init__(uid, working_dir, parse)
|
|
42
|
+
self.exp_id = None
|
|
43
|
+
self.output_path = output_path
|
|
44
|
+
|
|
45
|
+
def initialize(self):
|
|
46
|
+
"""
|
|
47
|
+
Initialize the item before mapping data. Here we create a directory for the output.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
None
|
|
51
|
+
"""
|
|
52
|
+
self.output_path = os.path.join(self.working_dir, self.output_path)
|
|
53
|
+
|
|
54
|
+
# Create the output path
|
|
55
|
+
if not os.path.exists(self.output_path):
|
|
56
|
+
os.makedirs(self.output_path)
|
|
57
|
+
|
|
58
|
+
# Map is called to get for each simulation a data object (all the metadata of the simulations) and simulation object
|
|
59
|
+
def map(self, data: Dict[str, Any], simulation: ANALYZABLE_ITEM):
|
|
60
|
+
"""
|
|
61
|
+
Map our data for our Workitems/Simulations. In this case, we just extract the tags and build a dataframe from those.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
data: List of files. This should be empty for us.
|
|
65
|
+
simulation: Item to extract
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Data frame with the tags built.
|
|
69
|
+
"""
|
|
70
|
+
df = pd.DataFrame(columns=list(simulation.tags.keys())) # Create a dataframe with the simulation tag keys
|
|
71
|
+
df.loc[str(simulation.uid)] = list(simulation.tags.values()) # Get a list of the sim tag values
|
|
72
|
+
df.index.name = 'SimId' # Label the index keys you create with the names option
|
|
73
|
+
return df
|
|
74
|
+
|
|
75
|
+
# In reduce, we are printing the simulation and result data filtered in map
|
|
76
|
+
def reduce(self, all_data: Dict[ANALYZABLE_ITEM, pd.DataFrame]):
|
|
77
|
+
"""
|
|
78
|
+
Reduce the dictionary of items->Tags dataframe to a single dataframe and write to a csv file.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
all_data: Map of Item->Tags dataframe
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
None
|
|
85
|
+
"""
|
|
86
|
+
results = pd.concat(list(all_data.values()), axis=0) # Combine a list of all the sims tag values
|
|
87
|
+
|
|
88
|
+
# Make a directory labeled the exp id to write the csv results to
|
|
89
|
+
first_sim = list(all_data.keys())[0] # get first Simulation
|
|
90
|
+
exp_id = first_sim.experiment.id # Set the exp id from the first sim data
|
|
91
|
+
output_folder = os.path.join(self.output_path, exp_id)
|
|
92
|
+
os.makedirs(output_folder, exist_ok=True)
|
|
93
|
+
results.to_csv(os.path.join(output_folder, self.__class__.__name__ + '.csv'))
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
idmtools assets package.
|
|
3
|
+
|
|
4
|
+
Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
|
|
5
|
+
"""
|
|
6
|
+
# flake8: noqa F821
|
|
7
|
+
from idmtools.assets.asset import Asset, TAsset, TAssetFilter, TAssetFilterList, TAssetList
|
|
8
|
+
from idmtools.assets.asset_collection import AssetCollection, TAssetCollection
|
|
9
|
+
from idmtools.assets.content_handlers import *
|
idmtools/assets/asset.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""
|
|
2
|
+
idmtools asset class definition.
|
|
3
|
+
|
|
4
|
+
Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import io
|
|
8
|
+
import os
|
|
9
|
+
from dataclasses import dataclass, field, InitVar
|
|
10
|
+
from functools import partial
|
|
11
|
+
from io import BytesIO
|
|
12
|
+
from logging import getLogger, DEBUG
|
|
13
|
+
from pathlib import PurePosixPath
|
|
14
|
+
from typing import TypeVar, Union, List, Callable, Any, Optional, Generator, BinaryIO
|
|
15
|
+
import backoff
|
|
16
|
+
import requests
|
|
17
|
+
from idmtools import IdmConfigParser
|
|
18
|
+
from idmtools.utils.file import file_content_to_generator, content_generator
|
|
19
|
+
from idmtools.utils.hashing import calculate_md5, calculate_md5_stream
|
|
20
|
+
|
|
21
|
+
logger = getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(repr=False)
|
|
25
|
+
class Asset:
|
|
26
|
+
"""
|
|
27
|
+
A class representing an asset. An asset can either be related to a physical asset present on the computer or directly specified by a filename and content.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
#: The absolute path of the asset. Optional if **filename** and **content** are given.
|
|
31
|
+
absolute_path: Optional[str] = field(default=None)
|
|
32
|
+
#: The relative path (compared to the simulation root folder).
|
|
33
|
+
relative_path: Optional[str] = field(default=None)
|
|
34
|
+
#: Name of the file. Optional if **absolute_path** is given.
|
|
35
|
+
filename: Optional[str] = field(default=None)
|
|
36
|
+
#: The content of the file. Optional if **absolute_path** is given.
|
|
37
|
+
content: InitVar[Any] = None
|
|
38
|
+
_content: bytes = field(default=None, init=False)
|
|
39
|
+
_length: Optional[int] = field(default=None, init=False)
|
|
40
|
+
#: Persisted tracks if item has been saved
|
|
41
|
+
persisted: bool = field(default=False)
|
|
42
|
+
#: Handler to api
|
|
43
|
+
handler: Callable = field(default=str, metadata=dict(exclude_from_metadata=True))
|
|
44
|
+
#: Hook to allow downloading from platform
|
|
45
|
+
download_generator_hook: Callable = field(default=None, metadata=dict(exclude_from_metadata=True))
|
|
46
|
+
#: Checksum of asset. Only required for existing assets
|
|
47
|
+
checksum: InitVar[Any] = None
|
|
48
|
+
_checksum: Optional[str] = field(default=None, init=False)
|
|
49
|
+
|
|
50
|
+
def __post_init__(self, content, checksum):
|
|
51
|
+
"""
|
|
52
|
+
After dataclass setup, validate options.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
content: Content to set
|
|
56
|
+
checksum: Checksum to set on object
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
None
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError - If absolute path and content are both provided. You can only provide one.
|
|
63
|
+
If absolute path is set and is a directory. Only file paths are supported.
|
|
64
|
+
If absolute path, filename, checksum, and content is not set.
|
|
65
|
+
FileNotFoundError - If absolute_path doesn't exist
|
|
66
|
+
"""
|
|
67
|
+
# Cache of our asset key
|
|
68
|
+
self._key = None
|
|
69
|
+
self._content = None if isinstance(content, property) else content
|
|
70
|
+
self._checksum = checksum if not isinstance(checksum, property) else None
|
|
71
|
+
self.filename = self.filename or (os.path.basename(self.absolute_path) if self.absolute_path else None)
|
|
72
|
+
# populate absolute path for conditions where user does not supply info
|
|
73
|
+
if not self._checksum and self._content is None and not self.absolute_path and self.filename and not self.persisted:
|
|
74
|
+
# try relative path
|
|
75
|
+
if self.relative_path and os.path.exists(self.short_remote_path()):
|
|
76
|
+
self.absolute_path = os.path.abspath(self.short_remote_path())
|
|
77
|
+
else:
|
|
78
|
+
self.absolute_path = os.path.abspath(self.filename)
|
|
79
|
+
if self.absolute_path and self._content is not None:
|
|
80
|
+
raise ValueError("Absolute Path and Content are mutually exclusive. Please provide only one of the options")
|
|
81
|
+
elif self.absolute_path and not os.path.exists(self.absolute_path):
|
|
82
|
+
raise FileNotFoundError(f"Cannot find specified asset: {self.absolute_path}")
|
|
83
|
+
elif self.absolute_path and os.path.isdir(self.absolute_path) and not self.persisted:
|
|
84
|
+
raise ValueError("Asset cannot be a directory!")
|
|
85
|
+
elif not self.absolute_path and (not self.filename or (self.filename and not self._checksum and self._content is None and not self.persisted)):
|
|
86
|
+
raise ValueError("Impossible to create the asset without either absolute path, filename and content, or filename and checksum!")
|
|
87
|
+
|
|
88
|
+
def __repr__(self):
|
|
89
|
+
"""
|
|
90
|
+
String representation of Asset.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
String representation
|
|
94
|
+
"""
|
|
95
|
+
return f"<Asset: {os.path.join(self.relative_path, self.filename)} from {self.absolute_path}>"
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def checksum(self): # noqa: F811
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
Returns checksum of object.
|
|
102
|
+
|
|
103
|
+
This will return None unless the user has provided checksum or called calculate checksum to avoid computation. If you need to guarantee a
|
|
104
|
+
checksum value, call calculate_checksum beforehand.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Checksum
|
|
108
|
+
"""
|
|
109
|
+
return self._checksum
|
|
110
|
+
|
|
111
|
+
@checksum.setter
|
|
112
|
+
def checksum(self, checksum):
|
|
113
|
+
"""
|
|
114
|
+
Set the checksum property.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
checksum: Checksum set
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
None
|
|
121
|
+
"""
|
|
122
|
+
self._checksum = checksum
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def extension(self) -> str:
|
|
126
|
+
"""
|
|
127
|
+
Returns extension of asset.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Extension
|
|
131
|
+
|
|
132
|
+
Notes:
|
|
133
|
+
This does not preserve the case of the extension in the filename. Extensions will always be returned in lowercase.
|
|
134
|
+
"""
|
|
135
|
+
return os.path.splitext(self.filename)[1].lstrip('.').lower()
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def filename(self): # noqa: F811
|
|
139
|
+
"""
|
|
140
|
+
Filename as asset.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Filename
|
|
144
|
+
"""
|
|
145
|
+
return self._filename or ""
|
|
146
|
+
|
|
147
|
+
@filename.setter
|
|
148
|
+
def filename(self, filename):
|
|
149
|
+
"""
|
|
150
|
+
Set the filename.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
filename: Filename
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
None
|
|
157
|
+
"""
|
|
158
|
+
self._filename = filename if not isinstance(filename, property) and filename else None
|
|
159
|
+
self._key = None
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def relative_path(self): # noqa: F811
|
|
163
|
+
"""
|
|
164
|
+
Get the relative path.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Relative path
|
|
168
|
+
"""
|
|
169
|
+
return self._relative_path or ""
|
|
170
|
+
|
|
171
|
+
@relative_path.setter
|
|
172
|
+
def relative_path(self, relative_path):
|
|
173
|
+
"""
|
|
174
|
+
Sets the relative path of an asset.
|
|
175
|
+
|
|
176
|
+
We filter out strings ending in forward or backward slashes.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
relative_path: Relative path for the item
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
None
|
|
183
|
+
"""
|
|
184
|
+
self._relative_path = relative_path.strip(" \\/") if not isinstance(relative_path, property) and relative_path else None
|
|
185
|
+
self._key = None
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def bytes(self):
|
|
189
|
+
"""
|
|
190
|
+
Bytes is the content as bytes.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
None
|
|
194
|
+
"""
|
|
195
|
+
if isinstance(self.content, bytes):
|
|
196
|
+
return self.content
|
|
197
|
+
return str.encode(self.handler(self.content))
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def length(self):
|
|
201
|
+
"""
|
|
202
|
+
Get length of item.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Length of the content
|
|
206
|
+
"""
|
|
207
|
+
if self._length is None:
|
|
208
|
+
self._length = len(self.content)
|
|
209
|
+
return self._length
|
|
210
|
+
|
|
211
|
+
@length.setter
|
|
212
|
+
def length(self, new_length):
|
|
213
|
+
"""
|
|
214
|
+
Set length of asset.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
new_length: Length to set
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
None
|
|
221
|
+
"""
|
|
222
|
+
self._length = new_length
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def content(self): # noqa: F811
|
|
226
|
+
"""
|
|
227
|
+
Content of the asset.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
The content of the file, either from the content attribute or by opening the absolute path.
|
|
231
|
+
"""
|
|
232
|
+
if self._content is None and self.absolute_path:
|
|
233
|
+
with open(self.absolute_path, "rb") as fp:
|
|
234
|
+
self._content = fp.read()
|
|
235
|
+
|
|
236
|
+
elif self.download_generator_hook:
|
|
237
|
+
if logger.isEnabledFor(DEBUG):
|
|
238
|
+
logger.debug(f"Fetching {self.filename} content from platform")
|
|
239
|
+
self._content = self.download_stream().getvalue()
|
|
240
|
+
|
|
241
|
+
return self._content
|
|
242
|
+
|
|
243
|
+
@content.setter
|
|
244
|
+
def content(self, content):
|
|
245
|
+
"""
|
|
246
|
+
Content property setting.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
content: Content to set
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
None
|
|
253
|
+
"""
|
|
254
|
+
self._content = None if isinstance(content, property) else content
|
|
255
|
+
# Reset checksum to None until requested
|
|
256
|
+
if self._checksum:
|
|
257
|
+
self._checksum = None
|
|
258
|
+
|
|
259
|
+
# region Equality and Hashing
|
|
260
|
+
def __eq__(self, other: 'Asset'):
|
|
261
|
+
"""
|
|
262
|
+
Equality between assets. Assets are the same if the key is the same.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
other: Other assets to compare with
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
True if the keys are the same.
|
|
269
|
+
"""
|
|
270
|
+
return self.__key() == other.__key()
|
|
271
|
+
|
|
272
|
+
def deep_equals(self, other: 'Asset') -> bool:
|
|
273
|
+
"""
|
|
274
|
+
Performs a deep comparison of assets, including contents.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
other: Other asset to compare
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
True if filename, relative path, and contents are equal, otherwise false
|
|
281
|
+
"""
|
|
282
|
+
if self.filename == other.filename and self.relative_path == other.relative_path:
|
|
283
|
+
return self.calculate_checksum() == other.calculate_checksum()
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
def __key(self):
|
|
287
|
+
"""
|
|
288
|
+
Get asset key. Asset key is filename and relative path.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Asset key
|
|
292
|
+
"""
|
|
293
|
+
# We only care to check if filename and relative path is same. Goal here is not identical check but rather that
|
|
294
|
+
# two files don't exist in same remote path
|
|
295
|
+
if self._key is None:
|
|
296
|
+
self._key = self.filename, self.relative_path
|
|
297
|
+
return self._key
|
|
298
|
+
|
|
299
|
+
def __hash__(self):
|
|
300
|
+
"""
|
|
301
|
+
Hash of Asset item.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Asset hash
|
|
305
|
+
"""
|
|
306
|
+
return hash(self.__key())
|
|
307
|
+
# endregion
|
|
308
|
+
|
|
309
|
+
def download_generator(self) -> Generator[bytearray, None, None]:
|
|
310
|
+
"""
|
|
311
|
+
A Download Generator that returns chunks of bytes from the file.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Generator of bytearray
|
|
315
|
+
|
|
316
|
+
Raises:
|
|
317
|
+
ValueError - When there is not a download generator hook defined
|
|
318
|
+
|
|
319
|
+
Notes:
|
|
320
|
+
TODO - Add a custom error with doclink.
|
|
321
|
+
"""
|
|
322
|
+
if not self.download_generator_hook:
|
|
323
|
+
raise ValueError("To be able to download, the Asset needs to be fetched from a platform object")
|
|
324
|
+
else:
|
|
325
|
+
return self.download_generator_hook()
|
|
326
|
+
|
|
327
|
+
def download_stream(self) -> BytesIO:
|
|
328
|
+
"""
|
|
329
|
+
Get a bytes IO stream of the asset.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
BytesIO of the Asset
|
|
333
|
+
"""
|
|
334
|
+
if logger.isEnabledFor(DEBUG):
|
|
335
|
+
logger.debug(f"Download {self.filename} to stream")
|
|
336
|
+
io = BytesIO()
|
|
337
|
+
self.__write_download_generator_to_stream(io)
|
|
338
|
+
return io
|
|
339
|
+
|
|
340
|
+
@backoff.on_exception(backoff.expo, (requests.exceptions.Timeout, requests.exceptions.ConnectionError), max_tries=8)
|
|
341
|
+
def __write_download_generator_to_stream(self, stream: BinaryIO, progress: bool = False):
|
|
342
|
+
"""
|
|
343
|
+
Write the download generator to another stream.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
stream: Stream to download
|
|
347
|
+
progress: Show progress
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
None
|
|
351
|
+
"""
|
|
352
|
+
gen = self.download_generator()
|
|
353
|
+
if progress and not IdmConfigParser.is_progress_bar_disabled():
|
|
354
|
+
from tqdm import tqdm
|
|
355
|
+
gen = tqdm(gen, total=self.length)
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
for chunk in gen:
|
|
359
|
+
if progress:
|
|
360
|
+
gen.update(len(chunk))
|
|
361
|
+
stream.write(chunk)
|
|
362
|
+
finally: # close progress if we have it open
|
|
363
|
+
if progress:
|
|
364
|
+
gen.close()
|
|
365
|
+
|
|
366
|
+
def download_to_path(self, dest: str, force: bool = False):
|
|
367
|
+
"""
|
|
368
|
+
Download an asset to path. This requires loadings the object through the platform.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
dest: Path to write to. If it is a directory, the asset filename will be added to it
|
|
372
|
+
force: Force download even if file exists
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
None
|
|
376
|
+
"""
|
|
377
|
+
if os.path.isdir(dest):
|
|
378
|
+
path = os.path.join(dest, self.short_remote_path())
|
|
379
|
+
path = path.replace("\\", os.path.sep)
|
|
380
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
381
|
+
else:
|
|
382
|
+
path = dest
|
|
383
|
+
|
|
384
|
+
if not os.path.exists(path) or force:
|
|
385
|
+
with open(path, 'wb') as out:
|
|
386
|
+
if logger.isEnabledFor(DEBUG):
|
|
387
|
+
logger.debug(f"Download {self.filename} to {path}")
|
|
388
|
+
self.__write_download_generator_to_stream(out)
|
|
389
|
+
|
|
390
|
+
def calculate_checksum(self) -> str:
|
|
391
|
+
"""
|
|
392
|
+
Calculate checksum on asset. If previous checksum was calculated, that value will be returned.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Checksum string
|
|
396
|
+
"""
|
|
397
|
+
if not self._checksum:
|
|
398
|
+
if self.absolute_path:
|
|
399
|
+
self._checksum = calculate_md5(self.absolute_path)
|
|
400
|
+
elif self.content is not None:
|
|
401
|
+
self._checksum = calculate_md5_stream(io.BytesIO(self.bytes))
|
|
402
|
+
return self._checksum
|
|
403
|
+
|
|
404
|
+
def short_remote_path(self) -> str:
|
|
405
|
+
"""
|
|
406
|
+
Returns the short remote path. This is the join of the relative path and filename.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Remote Path + Filename
|
|
410
|
+
"""
|
|
411
|
+
if self.relative_path:
|
|
412
|
+
path = PurePosixPath(self.relative_path.replace("\\", "/")).joinpath(self.filename)
|
|
413
|
+
else:
|
|
414
|
+
path = PurePosixPath(self.filename)
|
|
415
|
+
return str(path)
|
|
416
|
+
|
|
417
|
+
def save_as(self, dest: str, force: bool = False): # noqa: F811
|
|
418
|
+
"""
|
|
419
|
+
Download asset object to destination file.
|
|
420
|
+
Args:
|
|
421
|
+
dest: the file path
|
|
422
|
+
force: force download
|
|
423
|
+
Returns:
|
|
424
|
+
None
|
|
425
|
+
"""
|
|
426
|
+
if self.absolute_path is not None:
|
|
427
|
+
self.download_generator_hook = partial(file_content_to_generator, self.absolute_path)
|
|
428
|
+
elif self.content:
|
|
429
|
+
self.download_generator_hook = partial(content_generator, self.bytes)
|
|
430
|
+
else:
|
|
431
|
+
raise ValueError("Asset has no content or absolute path")
|
|
432
|
+
|
|
433
|
+
self.download_to_path(dest, force)
|
|
434
|
+
|
|
435
|
+
def save_md5_checksum(self):
|
|
436
|
+
"""
|
|
437
|
+
Save the md5 checksum of the asset to a file in the same directory as the asset.
|
|
438
|
+
Returns:
|
|
439
|
+
None
|
|
440
|
+
"""
|
|
441
|
+
asset = Asset(filename=f"{self.filename}.md5", content=f"{self.filename}:md5:{self.checksum}")
|
|
442
|
+
if asset.checksum is None:
|
|
443
|
+
asset.calculate_checksum()
|
|
444
|
+
asset.save_as(os.path.curdir, force=True)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
TAsset = TypeVar("TAsset", bound=Asset)
|
|
448
|
+
# Assets types
|
|
449
|
+
TAssetList = List[TAsset]
|
|
450
|
+
|
|
451
|
+
# Filters types
|
|
452
|
+
TAssetFilter = Union[Callable[[TAsset], bool], Callable]
|
|
453
|
+
TAssetFilterList = List[TAssetFilter]
|