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,336 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for getting information and examples from gitrepos.
|
|
3
|
+
|
|
4
|
+
Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
from logging import getLogger
|
|
10
|
+
import requests
|
|
11
|
+
import urllib.request
|
|
12
|
+
from click import secho
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
logger = getLogger(__name__)
|
|
15
|
+
user_logger = getLogger('user')
|
|
16
|
+
|
|
17
|
+
REPO_OWNER = 'institutefordiseasemodeling'
|
|
18
|
+
REPO_NAME = 'idmtools'
|
|
19
|
+
GITHUB_HOME = 'https://github.com'
|
|
20
|
+
GITHUB_API_HOME = 'https://api.github.com'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class GitRepo:
|
|
25
|
+
"""
|
|
26
|
+
GitRepo allows interaction with remote git repos, mainly for examples.
|
|
27
|
+
"""
|
|
28
|
+
repo_owner: str = field(default=None)
|
|
29
|
+
repo_name: str = field(default=None)
|
|
30
|
+
_branch: str = field(default='main', init=False, repr=False)
|
|
31
|
+
_path: str = field(default='', init=False, repr=False)
|
|
32
|
+
_verbose: bool = field(default=False, init=False, repr=False)
|
|
33
|
+
|
|
34
|
+
def __post_init__(self):
|
|
35
|
+
"""
|
|
36
|
+
Initialize GitRepo.
|
|
37
|
+
|
|
38
|
+
If repo_owner or repo_name is None, the defaults REPO_OWNER and REPO_NAME
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
None
|
|
42
|
+
"""
|
|
43
|
+
self.repo_owner = self.repo_owner or REPO_OWNER
|
|
44
|
+
self.repo_name = self.repo_name or REPO_NAME
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def path(self):
|
|
48
|
+
"""
|
|
49
|
+
Path property.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Return path property
|
|
53
|
+
"""
|
|
54
|
+
return self._path
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def branch(self):
|
|
58
|
+
"""
|
|
59
|
+
Branch property.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Return branch property
|
|
63
|
+
"""
|
|
64
|
+
return self._branch
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def verbose(self):
|
|
68
|
+
"""
|
|
69
|
+
Return verbose property.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Return verbose property
|
|
73
|
+
"""
|
|
74
|
+
return self._verbose
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def repo_home_url(self):
|
|
78
|
+
"""
|
|
79
|
+
Construct repo home url.
|
|
80
|
+
|
|
81
|
+
Returns: repo home url
|
|
82
|
+
"""
|
|
83
|
+
return f'{GITHUB_HOME}/{self.repo_owner}/{self.repo_name}'
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def repo_example_url(self):
|
|
87
|
+
"""
|
|
88
|
+
Construct repo example url.
|
|
89
|
+
|
|
90
|
+
Returns: repo example url
|
|
91
|
+
"""
|
|
92
|
+
return f'{self.repo_home_url}/tree/{self._branch}/{self._path}'
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def api_example_url(self):
|
|
96
|
+
"""
|
|
97
|
+
Construct api url of the examples for download.
|
|
98
|
+
|
|
99
|
+
Returns: api url
|
|
100
|
+
"""
|
|
101
|
+
return f'{GITHUB_API_HOME}/repos/{self.repo_owner}/{self.repo_name}/contents/{self._path}?ref={self._branch}'
|
|
102
|
+
|
|
103
|
+
def parse_url(self, url: str, branch: str = None, update: bool = True):
|
|
104
|
+
"""
|
|
105
|
+
Parse url for owner, repo, branch and example path.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
url: example url
|
|
109
|
+
branch: user branch to replace the branch in url
|
|
110
|
+
update: True/False - update repo or not
|
|
111
|
+
|
|
112
|
+
Returns: None
|
|
113
|
+
"""
|
|
114
|
+
default_branch = 'main'
|
|
115
|
+
ex_text = 'Please Verify URL Format: \nhttps://github.com/<owner>/<repo>/(tree|blob)/<branch>/<path>\nor\nhttps://github.com/<owner>/<repo>/'
|
|
116
|
+
|
|
117
|
+
example_url = url.lower().strip().rstrip('/')
|
|
118
|
+
url_chunks = example_url.replace(f'{GITHUB_HOME}/', '').split('/')
|
|
119
|
+
|
|
120
|
+
if len(url_chunks) < 2 or (len(url_chunks) >= 3 and url_chunks[2] not in ['tree', 'blob']):
|
|
121
|
+
raise Exception(f'Your Example URL: {url}\n{ex_text}')
|
|
122
|
+
|
|
123
|
+
repo_owner = url_chunks[0]
|
|
124
|
+
repo_name = url_chunks[1]
|
|
125
|
+
|
|
126
|
+
if len(url_chunks) <= 3:
|
|
127
|
+
_branch = branch if branch else default_branch
|
|
128
|
+
_path = ''
|
|
129
|
+
else:
|
|
130
|
+
_branch = branch if branch else url_chunks[3] if url_chunks[3] else default_branch
|
|
131
|
+
_path = '/'.join(url_chunks[4:])
|
|
132
|
+
|
|
133
|
+
if update:
|
|
134
|
+
self.repo_owner = repo_owner
|
|
135
|
+
self.repo_name = repo_name
|
|
136
|
+
self._branch = _branch
|
|
137
|
+
self._path = _path
|
|
138
|
+
else:
|
|
139
|
+
return {'repo_owner': repo_owner, 'repo_name': repo_name, 'branch': _branch, 'path': _path}
|
|
140
|
+
|
|
141
|
+
def list_public_repos(self, repo_owner: str = None, page: int = 1, raw: bool = False):
|
|
142
|
+
"""
|
|
143
|
+
Utility method to retrieve all public repos.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
repo_owner: the owner of the repo
|
|
147
|
+
page: pagination of results
|
|
148
|
+
raw: bool - return rwo data or simplified list
|
|
149
|
+
|
|
150
|
+
Returns: repo list
|
|
151
|
+
"""
|
|
152
|
+
# build api url
|
|
153
|
+
api_url = f'{GITHUB_API_HOME}/users/{repo_owner if repo_owner else self.repo_owner}/repos'
|
|
154
|
+
|
|
155
|
+
if page:
|
|
156
|
+
api_url = f'{api_url}?page={page}'
|
|
157
|
+
|
|
158
|
+
resp = requests.get(api_url)
|
|
159
|
+
if resp.status_code != 200:
|
|
160
|
+
raise Exception(f'Failed to access: {api_url}')
|
|
161
|
+
|
|
162
|
+
# get repos as json
|
|
163
|
+
repo_list = resp.json()
|
|
164
|
+
|
|
165
|
+
if raw:
|
|
166
|
+
return repo_list
|
|
167
|
+
else:
|
|
168
|
+
return [r['full_name'] for r in repo_list]
|
|
169
|
+
|
|
170
|
+
def list_repo_releases(self, repo_owner: str = None, repo_name: str = None, raw: bool = False):
|
|
171
|
+
"""
|
|
172
|
+
Utility method to retrieve all releases of the repo.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
repo_owner: the owner of the repo
|
|
176
|
+
repo_name: the name of repo
|
|
177
|
+
raw: bool - return raw data or simplified list
|
|
178
|
+
|
|
179
|
+
Returns: the release list of the repo
|
|
180
|
+
"""
|
|
181
|
+
# build api url
|
|
182
|
+
api_url = f'{GITHUB_API_HOME}/repos/{repo_owner if repo_owner else self.repo_owner}/{repo_name if repo_name else self.repo_name}/releases'
|
|
183
|
+
|
|
184
|
+
# make api call
|
|
185
|
+
resp = requests.get(api_url)
|
|
186
|
+
if resp.status_code != 200:
|
|
187
|
+
raise Exception(f'Failed to access: {api_url}')
|
|
188
|
+
|
|
189
|
+
# get repos as json
|
|
190
|
+
repo_list = resp.json()
|
|
191
|
+
|
|
192
|
+
if raw:
|
|
193
|
+
return repo_list
|
|
194
|
+
else:
|
|
195
|
+
return [f"{r['tag_name']} at {r['published_at']}" for r in repo_list]
|
|
196
|
+
|
|
197
|
+
def download(self, path: str = '', output_dir: str = "./", branch: str = 'main') -> int:
|
|
198
|
+
"""
|
|
199
|
+
Download files with example url provided.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
path: local file path to the repo
|
|
203
|
+
output_dir: user local folder to download files to
|
|
204
|
+
branch: specify branch for files download from
|
|
205
|
+
|
|
206
|
+
Returns: total file count downloaded
|
|
207
|
+
"""
|
|
208
|
+
if path.startswith('https://'):
|
|
209
|
+
self.parse_url(path)
|
|
210
|
+
else:
|
|
211
|
+
self._path = path
|
|
212
|
+
self._branch = branch
|
|
213
|
+
|
|
214
|
+
if not os.path.exists(output_dir):
|
|
215
|
+
raise Exception(f"output_dir does not exist: {output_dir}")
|
|
216
|
+
|
|
217
|
+
# First time display download url and local destination info
|
|
218
|
+
if self.verbose:
|
|
219
|
+
user_logger.info(f'Download Examples From: {self.repo_example_url}')
|
|
220
|
+
user_logger.info(f'Local Destination: {os.path.abspath(output_dir)}')
|
|
221
|
+
user_logger.info('Processing...')
|
|
222
|
+
self._verbose = False
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
opener = urllib.request.build_opener()
|
|
226
|
+
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
|
|
227
|
+
urllib.request.install_opener(opener)
|
|
228
|
+
response = urllib.request.urlretrieve(self.api_example_url)
|
|
229
|
+
except KeyboardInterrupt:
|
|
230
|
+
# when CTRL+C is pressed during the execution of this script,
|
|
231
|
+
# bring the cursor to the beginning, erase the current line, and dont make a new line
|
|
232
|
+
user_logger.error("✘ Got interrupted")
|
|
233
|
+
sys.exit()
|
|
234
|
+
except Exception as ex:
|
|
235
|
+
secho(f'Failed to access: {self.api_example_url}', fg="yellow")
|
|
236
|
+
logger.exception(ex)
|
|
237
|
+
exit(1)
|
|
238
|
+
|
|
239
|
+
download_dir = os.path.join(output_dir, self.repo_name)
|
|
240
|
+
|
|
241
|
+
# total files count
|
|
242
|
+
total_files = 0
|
|
243
|
+
with open(response[0], "r") as f:
|
|
244
|
+
data = json.load(f)
|
|
245
|
+
|
|
246
|
+
if isinstance(data, dict) and data["type"] == "file":
|
|
247
|
+
# create folder when necessary
|
|
248
|
+
path = data["path"]
|
|
249
|
+
os.makedirs(os.path.dirname(os.path.join(download_dir, path)), exist_ok=True)
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
# download the file
|
|
253
|
+
opener = urllib.request.build_opener()
|
|
254
|
+
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
|
|
255
|
+
urllib.request.install_opener(opener)
|
|
256
|
+
urllib.request.urlretrieve(data["download_url"], os.path.join(download_dir, path))
|
|
257
|
+
return 1
|
|
258
|
+
except KeyboardInterrupt:
|
|
259
|
+
# when CTRL+C is pressed during the execution of this script,
|
|
260
|
+
# bring the cursor to the beginning, erase the current line, and dont make a new line
|
|
261
|
+
user_logger.error("✘ Got interrupted", )
|
|
262
|
+
sys.exit()
|
|
263
|
+
except Exception as ex:
|
|
264
|
+
secho(f'Failed to access: {self.api_example_url}', fg="yellow")
|
|
265
|
+
user_logger.error(ex)
|
|
266
|
+
exit(1)
|
|
267
|
+
|
|
268
|
+
total_files += len([f for f in data if f['type'] == 'file'])
|
|
269
|
+
|
|
270
|
+
for file in data:
|
|
271
|
+
file_url = file["download_url"]
|
|
272
|
+
path = file["path"]
|
|
273
|
+
|
|
274
|
+
# create folder when necessary
|
|
275
|
+
os.makedirs(os.path.dirname(os.path.join(download_dir, path)), exist_ok=True)
|
|
276
|
+
|
|
277
|
+
if file_url is not None:
|
|
278
|
+
try:
|
|
279
|
+
# download the file
|
|
280
|
+
opener = urllib.request.build_opener()
|
|
281
|
+
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
|
|
282
|
+
urllib.request.install_opener(opener)
|
|
283
|
+
urllib.request.urlretrieve(file_url, os.path.join(download_dir, path))
|
|
284
|
+
except KeyboardInterrupt:
|
|
285
|
+
# when CTRL+C is pressed during the execution of this script,
|
|
286
|
+
# bring the cursor to the beginning, erase the current line, and dont make a new line
|
|
287
|
+
user_logger.error("✘ Got interrupted", )
|
|
288
|
+
sys.exit()
|
|
289
|
+
else:
|
|
290
|
+
total_files += self.download(path, output_dir, branch)
|
|
291
|
+
|
|
292
|
+
return total_files
|
|
293
|
+
|
|
294
|
+
def peep(self, path: str = '', branch: str = 'main'):
|
|
295
|
+
"""
|
|
296
|
+
Download files with example url provided.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
path: local file path to the repo
|
|
300
|
+
branch: specify branch for files download from
|
|
301
|
+
|
|
302
|
+
Returns: None
|
|
303
|
+
"""
|
|
304
|
+
if path.startswith('https://'):
|
|
305
|
+
repo_meta = self.parse_url(path, branch, False)
|
|
306
|
+
else:
|
|
307
|
+
self._path = path
|
|
308
|
+
self._branch = branch
|
|
309
|
+
repo_meta = {'repo_owner': self.repo_owner, 'repo_name': self.repo_name, 'branch': branch or self.branch,
|
|
310
|
+
'path': path or self.path}
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
api_example_url = f"{GITHUB_API_HOME}/repos/{repo_meta['repo_owner']}/{repo_meta['repo_name']}/contents/{repo_meta['path']}?ref={repo_meta['branch']}"
|
|
314
|
+
opener = urllib.request.build_opener()
|
|
315
|
+
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
|
|
316
|
+
urllib.request.install_opener(opener)
|
|
317
|
+
response = urllib.request.urlretrieve(api_example_url)
|
|
318
|
+
except KeyboardInterrupt:
|
|
319
|
+
# when CTRL+C is pressed during the execution of this script,
|
|
320
|
+
# bring the cursor to the beginning, erase the current line, and dont make a new line
|
|
321
|
+
user_logger.error("✘ Got interrupted")
|
|
322
|
+
sys.exit()
|
|
323
|
+
|
|
324
|
+
result = []
|
|
325
|
+
with open(response[0], "r") as f:
|
|
326
|
+
data = json.load(f)
|
|
327
|
+
|
|
328
|
+
if isinstance(data, dict):
|
|
329
|
+
d = {'type': data['type'], 'name': data['path'], 'path': data['path'], 'html_url': data['html_url']}
|
|
330
|
+
result.append(d)
|
|
331
|
+
else:
|
|
332
|
+
for file in data:
|
|
333
|
+
d = {'type': file['type'], 'name': file['name'], 'path': file['path'], 'html_url': file['html_url']}
|
|
334
|
+
result.append(d)
|
|
335
|
+
|
|
336
|
+
return result
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fast hash of Python objects.
|
|
3
|
+
|
|
4
|
+
Copyright 2025, Gates Foundation. All rights reserved.
|
|
5
|
+
"""
|
|
6
|
+
from typing import Union, BinaryIO
|
|
7
|
+
|
|
8
|
+
import decimal
|
|
9
|
+
import hashlib
|
|
10
|
+
import io
|
|
11
|
+
import pickle
|
|
12
|
+
import types
|
|
13
|
+
from dataclasses import fields, MISSING
|
|
14
|
+
from logging import getLogger, Logger
|
|
15
|
+
|
|
16
|
+
logger = getLogger(__name__)
|
|
17
|
+
Pickler = pickle._Pickler
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _ConsistentSet(object):
|
|
21
|
+
"""
|
|
22
|
+
Class used to ensure the hash of sets is preserved whatever the order of its items.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, set_sequence):
|
|
26
|
+
"""
|
|
27
|
+
Force the order of elements in a set to ensure consistent hashing.
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
# Trying first to order the set using sorted
|
|
31
|
+
self._sequence = sorted(set_sequence)
|
|
32
|
+
except (TypeError, decimal.InvalidOperation):
|
|
33
|
+
# If elements are unorderable, sort them using their hash.
|
|
34
|
+
self._sequence = sorted((hash(e) for e in set_sequence))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _MyHash(object):
|
|
38
|
+
"""
|
|
39
|
+
A class used to hash objects that won't normally pickle.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, *args):
|
|
43
|
+
self.args = args
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Hasher(Pickler):
|
|
47
|
+
"""
|
|
48
|
+
A subclass of pickler to do hashing, rather than pickling.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, hash_name='md5'):
|
|
52
|
+
"""
|
|
53
|
+
Initialize our hasher.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
hash_name: Hash type to use. Defaults to md5
|
|
57
|
+
"""
|
|
58
|
+
self.stream = io.BytesIO()
|
|
59
|
+
Pickler.__init__(self, self.stream)
|
|
60
|
+
# Initialise the hash obj
|
|
61
|
+
self._hash = hashlib.new(hash_name)
|
|
62
|
+
|
|
63
|
+
def hash(self, obj, return_digest=True):
|
|
64
|
+
"""
|
|
65
|
+
Hash an object.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
obj: Object to hash
|
|
69
|
+
return_digest: Should the digest be returned?
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
None if return_digest is False, otherwise the hash digest is returned
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
self.dump(obj)
|
|
76
|
+
except pickle.PicklingError as e:
|
|
77
|
+
e.args += ('PicklingError while hashing %r: %r' % (obj, e),)
|
|
78
|
+
raise
|
|
79
|
+
dumps = self.stream.getvalue()
|
|
80
|
+
self._hash.update(dumps)
|
|
81
|
+
if return_digest:
|
|
82
|
+
return self._hash.hexdigest()
|
|
83
|
+
|
|
84
|
+
def save(self, obj):
|
|
85
|
+
"""
|
|
86
|
+
Save an object to hash.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
obj: Obj to save.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
None
|
|
93
|
+
"""
|
|
94
|
+
from idmtools.utils.collections import ExperimentParentIterator
|
|
95
|
+
import abc
|
|
96
|
+
if isinstance(obj, abc.ABCMeta):
|
|
97
|
+
pass
|
|
98
|
+
elif isinstance(obj, ExperimentParentIterator):
|
|
99
|
+
pass
|
|
100
|
+
elif isinstance(obj, Logger):
|
|
101
|
+
pass
|
|
102
|
+
else:
|
|
103
|
+
if isinstance(obj, (types.MethodType, type({}.pop))):
|
|
104
|
+
# the Pickler cannot pickle instance methods; here we decompose
|
|
105
|
+
# them into components that make them uniquely identifiable
|
|
106
|
+
if hasattr(obj, '__func__'):
|
|
107
|
+
func_name = obj.__func__.__name__
|
|
108
|
+
else:
|
|
109
|
+
func_name = obj.__name__
|
|
110
|
+
inst = obj.__self__
|
|
111
|
+
if isinstance(inst, pickle):
|
|
112
|
+
obj = _MyHash(func_name, inst.__name__)
|
|
113
|
+
elif inst is None:
|
|
114
|
+
# type(None) or type(module) do not pickle
|
|
115
|
+
obj = _MyHash(func_name, inst)
|
|
116
|
+
else:
|
|
117
|
+
cls = obj.__self__.__class__
|
|
118
|
+
obj = _MyHash(func_name, inst, cls)
|
|
119
|
+
Pickler.save(self, obj)
|
|
120
|
+
|
|
121
|
+
def memoize(self, obj):
|
|
122
|
+
"""
|
|
123
|
+
Disable memoization for strings so hashing happens on value and not reference.
|
|
124
|
+
"""
|
|
125
|
+
if isinstance(obj, (str, bytes)):
|
|
126
|
+
return
|
|
127
|
+
Pickler.memoize(self, obj)
|
|
128
|
+
|
|
129
|
+
def _batch_setitems(self, items):
|
|
130
|
+
"""
|
|
131
|
+
Force the order of keys in dictionary to ensure consistent hashing.
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
# First try quick way of sorting keys if possible
|
|
135
|
+
Pickler._batch_setitems(self, iter(sorted(items)))
|
|
136
|
+
except TypeError:
|
|
137
|
+
# If keys are unorderable, sort them using their hash
|
|
138
|
+
Pickler._batch_setitems(self, iter(sorted((hash(k), v) for k, v in items)))
|
|
139
|
+
|
|
140
|
+
def save_set(self, set_items):
|
|
141
|
+
"""
|
|
142
|
+
Save set hashing.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
set_items: Set items
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
None
|
|
149
|
+
"""
|
|
150
|
+
# forces order of items in Set to ensure consistent hash
|
|
151
|
+
Pickler.save(self, _ConsistentSet(set_items))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def hash_obj(obj, hash_name='md5'):
|
|
155
|
+
"""
|
|
156
|
+
Quick calculation of a hash to identify uniquely Python objects.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
obj: Object to hash
|
|
160
|
+
hash_name: The hashing algorithm to use. 'md5' is faster; 'sha1' is considered safer.
|
|
161
|
+
"""
|
|
162
|
+
hasher = Hasher(hash_name=hash_name)
|
|
163
|
+
return hasher.hash(obj)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def ignore_fields_in_dataclass_on_pickle(item):
|
|
167
|
+
"""
|
|
168
|
+
Ignore certain fields for pickling on dataclasses.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
item: Item to pickle
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
State of item to pickle
|
|
175
|
+
"""
|
|
176
|
+
state = item.__dict__.copy()
|
|
177
|
+
attrs = set(vars(item).keys())
|
|
178
|
+
|
|
179
|
+
# Retrieve fields default values
|
|
180
|
+
fds = fields(item)
|
|
181
|
+
field_default = {f.name: f.default for f in fds}
|
|
182
|
+
|
|
183
|
+
# Update default with parent's pre-populated values
|
|
184
|
+
if hasattr(item, 'pre_getstate'):
|
|
185
|
+
pre_state = item.pre_getstate()
|
|
186
|
+
pre_state = pre_state or {}
|
|
187
|
+
field_default.update(pre_state)
|
|
188
|
+
|
|
189
|
+
# Don't pickle ignore_pickle fields: set values to default
|
|
190
|
+
for field_name in attrs.intersection(item.pickle_ignore_fields):
|
|
191
|
+
if field_name in state:
|
|
192
|
+
if field_default[field_name] is MISSING:
|
|
193
|
+
state[field_name] = None
|
|
194
|
+
else:
|
|
195
|
+
state[field_name] = field_default[field_name]
|
|
196
|
+
|
|
197
|
+
return state
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def calculate_md5(filename: str, chunk_size: int = 8192) -> str:
|
|
201
|
+
"""
|
|
202
|
+
Calculate MD5.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
filename: Filename to caclulate md5 for
|
|
206
|
+
chunk_size: Chunk size
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
md5 as string
|
|
210
|
+
"""
|
|
211
|
+
with open(filename, "rb") as f:
|
|
212
|
+
return calculate_md5_stream(f, chunk_size)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def calculate_md5_stream(stream: Union[io.BytesIO, BinaryIO], chunk_size: int = 8192, hash_type: str = 'md5', file_hash=None):
|
|
216
|
+
"""
|
|
217
|
+
Calculate md5 on stream.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
chunk_size:
|
|
221
|
+
stream:
|
|
222
|
+
hash_type: Hash function
|
|
223
|
+
file_hash: File hash
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
md5 of stream
|
|
227
|
+
"""
|
|
228
|
+
if file_hash is None:
|
|
229
|
+
if not hasattr(hashlib, hash_type):
|
|
230
|
+
raise ValueError(f"Could not find hash function {hash_type}")
|
|
231
|
+
else:
|
|
232
|
+
file_hash = getattr(hashlib, hash_type)()
|
|
233
|
+
|
|
234
|
+
while True:
|
|
235
|
+
chunk = stream.read(chunk_size)
|
|
236
|
+
if not chunk:
|
|
237
|
+
break
|
|
238
|
+
file_hash.update(chunk)
|
|
239
|
+
return file_hash.hexdigest()
|
idmtools/utils/info.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities to fetch info about local system such as packages installed.
|
|
3
|
+
|
|
4
|
+
Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
|
|
5
|
+
"""
|
|
6
|
+
import sys
|
|
7
|
+
from logging import getLogger
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
logger = getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_doc_base_url() -> str:
|
|
14
|
+
"""
|
|
15
|
+
Get base url for documentation links.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Doc base url
|
|
19
|
+
"""
|
|
20
|
+
return "https://institutefordiseasemodeling.github.io/idmtools/"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_pip_packages_10_to_6():
|
|
24
|
+
"""
|
|
25
|
+
Load packages for versions 1.0 to 6 of pip.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
None
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ImportError: If the pip version is different.
|
|
32
|
+
"""
|
|
33
|
+
from pip.util import get_installed_distributions
|
|
34
|
+
return get_installed_distributions()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_pip_packages_6_to_9():
|
|
38
|
+
"""
|
|
39
|
+
Get packages for pip versions 6 through 9.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
None
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ImportError: If the pip version is different.
|
|
46
|
+
"""
|
|
47
|
+
from pip.utils import get_installed_distributions
|
|
48
|
+
return get_installed_distributions()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_pip_packages_10_to_current():
|
|
52
|
+
"""
|
|
53
|
+
Get packages for pip versions 10 to current.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
None
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ImportError: If the pip version is different.
|
|
60
|
+
"""
|
|
61
|
+
from pip._internal.utils.misc import get_installed_distributions
|
|
62
|
+
return get_installed_distributions()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_packages_from_pip():
|
|
66
|
+
"""
|
|
67
|
+
Attempt to load packages from pip.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
(List[str]): A list of packages installed.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
from importlib.metadata import distributions
|
|
74
|
+
except ImportError:
|
|
75
|
+
from importlib_metadata import distributions # for python 3.7
|
|
76
|
+
return [f'{d.metadata["Name"]} {d.version}' for d in distributions()]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_packages_list() -> List[str]:
|
|
80
|
+
"""
|
|
81
|
+
Return a list of installed packages in the current environment.
|
|
82
|
+
|
|
83
|
+
Currently |IT_s| depends on pip for this functionality and since it is just used for troubleshooting, errors can be ignored.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
(List[str]): A list of packages installed.
|
|
87
|
+
"""
|
|
88
|
+
packages = get_packages_from_pip()
|
|
89
|
+
if packages is None: # fall back to sys modules
|
|
90
|
+
packages = []
|
|
91
|
+
# for name, module in sys.modules:
|
|
92
|
+
modules = list(sys.modules.items())
|
|
93
|
+
for name, mod in modules:
|
|
94
|
+
version = ''
|
|
95
|
+
if hasattr(mod, 'version'):
|
|
96
|
+
version = mod.version
|
|
97
|
+
elif hasattr(mod, '__version__'):
|
|
98
|
+
version = mod.__version__
|
|
99
|
+
packages.append(f'{name}=={version}')
|
|
100
|
+
packages = list(sorted(packages))
|
|
101
|
+
return packages
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_help_version_url(help_path, url_template: str = 'https://docs.idmod.org/projects/idmtools/en/{version}/', version: Optional[str] = None) -> str:
|
|
105
|
+
"""
|
|
106
|
+
Get the help url for a subject based on a version.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
help_path: Path to config(minus base url). For example, configuration.html
|
|
110
|
+
url_template: Template for URL containing version replacement formatter
|
|
111
|
+
version: Optional version. If not provided, the version of idmtools installed will be used. For development versions, the version will always be nightly
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Path to url
|
|
115
|
+
"""
|
|
116
|
+
from idmtools import __version__
|
|
117
|
+
from urllib import parse
|
|
118
|
+
if version is None:
|
|
119
|
+
if "nightly" in __version__:
|
|
120
|
+
version = "latest"
|
|
121
|
+
else:
|
|
122
|
+
version = f'v{__version__[0:5]}'
|
|
123
|
+
|
|
124
|
+
return parse.urljoin(url_template.format(version=version), help_path)
|