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,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)