idmtools 0.0.0.dev0__py3-none-any.whl → 0.0.2__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 (118) hide show
  1. idmtools/__init__.py +27 -8
  2. idmtools/analysis/__init__.py +5 -0
  3. idmtools/analysis/add_analyzer.py +89 -0
  4. idmtools/analysis/analyze_manager.py +490 -0
  5. idmtools/analysis/csv_analyzer.py +103 -0
  6. idmtools/analysis/download_analyzer.py +96 -0
  7. idmtools/analysis/map_worker_entry.py +100 -0
  8. idmtools/analysis/platform_analysis_bootstrap.py +94 -0
  9. idmtools/analysis/platform_anaylsis.py +291 -0
  10. idmtools/analysis/tags_analyzer.py +93 -0
  11. idmtools/assets/__init__.py +9 -0
  12. idmtools/assets/asset.py +453 -0
  13. idmtools/assets/asset_collection.py +514 -0
  14. idmtools/assets/content_handlers.py +19 -0
  15. idmtools/assets/errors.py +23 -0
  16. idmtools/assets/file_list.py +191 -0
  17. idmtools/builders/__init__.py +11 -0
  18. idmtools/builders/arm_simulation_builder.py +152 -0
  19. idmtools/builders/csv_simulation_builder.py +76 -0
  20. idmtools/builders/simulation_builder.py +348 -0
  21. idmtools/builders/sweep_arm.py +109 -0
  22. idmtools/builders/yaml_simulation_builder.py +82 -0
  23. idmtools/config/__init__.py +7 -0
  24. idmtools/config/idm_config_parser.py +486 -0
  25. idmtools/core/__init__.py +10 -0
  26. idmtools/core/cache_enabled.py +114 -0
  27. idmtools/core/context.py +68 -0
  28. idmtools/core/docker_task.py +207 -0
  29. idmtools/core/enums.py +51 -0
  30. idmtools/core/exceptions.py +91 -0
  31. idmtools/core/experiment_factory.py +71 -0
  32. idmtools/core/id_file.py +70 -0
  33. idmtools/core/interfaces/__init__.py +5 -0
  34. idmtools/core/interfaces/entity_container.py +64 -0
  35. idmtools/core/interfaces/iassets_enabled.py +58 -0
  36. idmtools/core/interfaces/ientity.py +331 -0
  37. idmtools/core/interfaces/iitem.py +206 -0
  38. idmtools/core/interfaces/imetadata_operations.py +89 -0
  39. idmtools/core/interfaces/inamed_entity.py +17 -0
  40. idmtools/core/interfaces/irunnable_entity.py +159 -0
  41. idmtools/core/logging.py +387 -0
  42. idmtools/core/platform_factory.py +316 -0
  43. idmtools/core/system_information.py +104 -0
  44. idmtools/core/task_factory.py +145 -0
  45. idmtools/entities/__init__.py +10 -0
  46. idmtools/entities/command_line.py +229 -0
  47. idmtools/entities/command_task.py +155 -0
  48. idmtools/entities/experiment.py +787 -0
  49. idmtools/entities/generic_workitem.py +43 -0
  50. idmtools/entities/ianalyzer.py +163 -0
  51. idmtools/entities/iplatform.py +1106 -0
  52. idmtools/entities/iplatform_default.py +39 -0
  53. idmtools/entities/iplatform_ops/__init__.py +5 -0
  54. idmtools/entities/iplatform_ops/iplatform_asset_collection_operations.py +148 -0
  55. idmtools/entities/iplatform_ops/iplatform_experiment_operations.py +415 -0
  56. idmtools/entities/iplatform_ops/iplatform_simulation_operations.py +315 -0
  57. idmtools/entities/iplatform_ops/iplatform_suite_operations.py +322 -0
  58. idmtools/entities/iplatform_ops/iplatform_workflowitem_operations.py +301 -0
  59. idmtools/entities/iplatform_ops/utils.py +185 -0
  60. idmtools/entities/itask.py +316 -0
  61. idmtools/entities/iworkflow_item.py +167 -0
  62. idmtools/entities/platform_requirements.py +20 -0
  63. idmtools/entities/relation_type.py +14 -0
  64. idmtools/entities/simulation.py +255 -0
  65. idmtools/entities/suite.py +188 -0
  66. idmtools/entities/task_proxy.py +37 -0
  67. idmtools/entities/templated_simulation.py +325 -0
  68. idmtools/frozen/frozen_dict.py +71 -0
  69. idmtools/frozen/frozen_list.py +66 -0
  70. idmtools/frozen/frozen_set.py +86 -0
  71. idmtools/frozen/frozen_tuple.py +18 -0
  72. idmtools/frozen/frozen_utils.py +179 -0
  73. idmtools/frozen/ifrozen.py +66 -0
  74. idmtools/plugins/__init__.py +5 -0
  75. idmtools/plugins/git_commit.py +117 -0
  76. idmtools/registry/__init__.py +4 -0
  77. idmtools/registry/experiment_specification.py +105 -0
  78. idmtools/registry/functions.py +28 -0
  79. idmtools/registry/hook_specs.py +132 -0
  80. idmtools/registry/master_plugin_registry.py +51 -0
  81. idmtools/registry/platform_specification.py +138 -0
  82. idmtools/registry/plugin_specification.py +129 -0
  83. idmtools/registry/task_specification.py +104 -0
  84. idmtools/registry/utils.py +119 -0
  85. idmtools/services/__init__.py +5 -0
  86. idmtools/services/ipersistance_service.py +135 -0
  87. idmtools/services/platforms.py +13 -0
  88. idmtools/utils/__init__.py +5 -0
  89. idmtools/utils/caller.py +24 -0
  90. idmtools/utils/collections.py +246 -0
  91. idmtools/utils/command_line.py +45 -0
  92. idmtools/utils/decorators.py +300 -0
  93. idmtools/utils/display/__init__.py +22 -0
  94. idmtools/utils/display/displays.py +181 -0
  95. idmtools/utils/display/settings.py +25 -0
  96. idmtools/utils/dropbox_location.py +30 -0
  97. idmtools/utils/entities.py +127 -0
  98. idmtools/utils/file.py +72 -0
  99. idmtools/utils/file_parser.py +151 -0
  100. idmtools/utils/filter_simulations.py +182 -0
  101. idmtools/utils/filters/__init__.py +5 -0
  102. idmtools/utils/filters/asset_filters.py +88 -0
  103. idmtools/utils/general.py +286 -0
  104. idmtools/utils/gitrepo.py +336 -0
  105. idmtools/utils/hashing.py +239 -0
  106. idmtools/utils/info.py +124 -0
  107. idmtools/utils/json.py +82 -0
  108. idmtools/utils/language.py +107 -0
  109. idmtools/utils/local_os.py +40 -0
  110. idmtools/utils/time.py +22 -0
  111. idmtools-0.0.2.dist-info/METADATA +120 -0
  112. idmtools-0.0.2.dist-info/RECORD +116 -0
  113. idmtools-0.0.2.dist-info/entry_points.txt +9 -0
  114. idmtools-0.0.2.dist-info/licenses/LICENSE.TXT +3 -0
  115. idmtools-0.0.0.dev0.dist-info/METADATA +0 -41
  116. idmtools-0.0.0.dev0.dist-info/RECORD +0 -5
  117. {idmtools-0.0.0.dev0.dist-info → idmtools-0.0.2.dist-info}/WHEEL +0 -0
  118. {idmtools-0.0.0.dev0.dist-info → idmtools-0.0.2.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 *
@@ -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]