mlcflow 1.2.3__tar.gz → 1.2.5__tar.gz
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.
- {mlcflow-1.2.3 → mlcflow-1.2.5}/PKG-INFO +6 -7
- {mlcflow-1.2.3 → mlcflow-1.2.5}/README.md +5 -6
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/action.py +26 -9
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/cache_action.py +92 -25
- mlcflow-1.2.5/mlc/error_codes.py +209 -0
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/index.py +18 -11
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/main.py +22 -7
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/repo_action.py +111 -7
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/script_action.py +44 -4
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlcflow.egg-info/PKG-INFO +6 -7
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlcflow.egg-info/SOURCES.txt +4 -1
- {mlcflow-1.2.3 → mlcflow-1.2.5}/pyproject.toml +1 -1
- mlcflow-1.2.5/tests/test_action_invalid_meta_entries.py +165 -0
- mlcflow-1.2.5/tests/test_cache_mark_tmp.py +73 -0
- mlcflow-1.2.5/tests/test_script_action_apptainer.py +29 -0
- mlcflow-1.2.3/mlc/error_codes.py +0 -77
- {mlcflow-1.2.3 → mlcflow-1.2.5}/LICENSE.md +0 -0
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/__init__.py +0 -0
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/__main__.py +0 -0
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/action_factory.py +0 -0
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/cfg_action.py +0 -0
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/experiment_action.py +0 -0
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/item.py +0 -0
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/logger.py +0 -0
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/meta_schema.py +0 -0
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/repo.py +0 -0
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/utils.py +0 -0
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlcflow.egg-info/dependency_links.txt +0 -0
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlcflow.egg-info/entry_points.txt +0 -0
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlcflow.egg-info/requires.txt +0 -0
- {mlcflow-1.2.3 → mlcflow-1.2.5}/mlcflow.egg-info/top_level.txt +0 -0
- {mlcflow-1.2.3 → mlcflow-1.2.5}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mlcflow
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.5
|
|
4
4
|
Summary: An automation interface tailored for CPU/GPU benchmarking
|
|
5
5
|
Author-email: MLCommons <systems@mlcommons.org>
|
|
6
6
|
License:
|
|
@@ -204,10 +204,10 @@ Dynamic: license-file
|
|
|
204
204
|
[](https://github.com/mlcommons/mlcflow/blob/main/LICENSE.md)
|
|
205
205
|
[](https://pepy.tech/project/mlcflow)
|
|
206
206
|
|
|
207
|
-
[](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml)
|
|
208
|
-
[](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml)
|
|
210
|
-
[](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-bert.yml)
|
|
207
|
+
[](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml)
|
|
208
|
+
[](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml)
|
|
209
|
+
[](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml)
|
|
210
|
+
[](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-bert.yml)
|
|
211
211
|
|
|
212
212
|
|
|
213
213
|
MLCFlow is a versatile CLI and Python interface developed by MLCommons in collaboration with a dedicated team of volunteers (see [Contributors](https://github.com/mlcommons/mlcflow/blob/main/CONTRIBUTORS.md)). It serves as a streamlined replacement for the [CMind](https://github.com/mlcommons/ck/tree/master/cm) tool, designed to drive the automation workflows of MLPerf benchmarks more efficiently. You can use this tool for any of your workflow automation tasks.
|
|
@@ -259,7 +259,7 @@ Each target has its own set of specific actions to tailor automation workflows a
|
|
|
259
259
|
| Target | Action |
|
|
260
260
|
|--------|-----------------|
|
|
261
261
|
| script | run, find/search, rm, mv, cp, add, test, docker-run, show, experiment, doc |
|
|
262
|
-
| cache | find/search, rm, show |
|
|
262
|
+
| cache | find/search, rm, show, list, prune, mark-tmp |
|
|
263
263
|
| repo | pull, search, rm, list, find/search , add |
|
|
264
264
|
|
|
265
265
|
|
|
@@ -356,4 +356,3 @@ classDiagram
|
|
|
356
356
|
Item o-- Repo
|
|
357
357
|
Automation o-- Action
|
|
358
358
|
```
|
|
359
|
-
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
[](https://github.com/mlcommons/mlcflow/blob/main/LICENSE.md)
|
|
4
4
|
[](https://pepy.tech/project/mlcflow)
|
|
5
5
|
|
|
6
|
-
[](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml)
|
|
7
|
-
[](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml)
|
|
9
|
-
[](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-bert.yml)
|
|
6
|
+
[](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml)
|
|
7
|
+
[](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml)
|
|
8
|
+
[](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml)
|
|
9
|
+
[](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-bert.yml)
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
MLCFlow is a versatile CLI and Python interface developed by MLCommons in collaboration with a dedicated team of volunteers (see [Contributors](https://github.com/mlcommons/mlcflow/blob/main/CONTRIBUTORS.md)). It serves as a streamlined replacement for the [CMind](https://github.com/mlcommons/ck/tree/master/cm) tool, designed to drive the automation workflows of MLPerf benchmarks more efficiently. You can use this tool for any of your workflow automation tasks.
|
|
@@ -58,7 +58,7 @@ Each target has its own set of specific actions to tailor automation workflows a
|
|
|
58
58
|
| Target | Action |
|
|
59
59
|
|--------|-----------------|
|
|
60
60
|
| script | run, find/search, rm, mv, cp, add, test, docker-run, show, experiment, doc |
|
|
61
|
-
| cache | find/search, rm, show |
|
|
61
|
+
| cache | find/search, rm, show, list, prune, mark-tmp |
|
|
62
62
|
| repo | pull, search, rm, list, find/search , add |
|
|
63
63
|
|
|
64
64
|
|
|
@@ -155,4 +155,3 @@ classDiagram
|
|
|
155
155
|
Item o-- Repo
|
|
156
156
|
Automation o-- Action
|
|
157
157
|
```
|
|
158
|
-
|
|
@@ -185,6 +185,15 @@ class Action:
|
|
|
185
185
|
self._index = Index(self.repos_path, self.repos)
|
|
186
186
|
return self._index
|
|
187
187
|
|
|
188
|
+
def _item_from_index_entry(self, res, target_name):
|
|
189
|
+
"""Create an Item from an index entry and skip entries with invalid meta."""
|
|
190
|
+
it = Item(res['path'], res['repo'])
|
|
191
|
+
if not isinstance(it.meta, dict):
|
|
192
|
+
logger.warning(
|
|
193
|
+
f"Skipping {target_name} item at {it.path}: missing or invalid meta")
|
|
194
|
+
return None
|
|
195
|
+
return it
|
|
196
|
+
|
|
188
197
|
def __init__(self):
|
|
189
198
|
setup_logging(log_path=os.getcwd(), log_file='.mlc-log.txt')
|
|
190
199
|
self.logger = logger
|
|
@@ -728,8 +737,13 @@ class Action:
|
|
|
728
737
|
# For targets like cache, sometimes user would need to clear the entire cache folder present in the system
|
|
729
738
|
# this helps to fetch entire data pertaining to particular target
|
|
730
739
|
if fetch_all:
|
|
740
|
+
if not target_index:
|
|
741
|
+
return {'return': 0, 'list': result}
|
|
742
|
+
|
|
731
743
|
for res in target_index:
|
|
732
|
-
|
|
744
|
+
it = self._item_from_index_entry(res, target)
|
|
745
|
+
if it:
|
|
746
|
+
result.append(it)
|
|
733
747
|
return {'return': 0, 'list': result}
|
|
734
748
|
|
|
735
749
|
if not uid and not alias and i.get('details'):
|
|
@@ -775,14 +789,16 @@ class Action:
|
|
|
775
789
|
for res in target_index:
|
|
776
790
|
if (res["uid"] == uid or (alias and res["alias"] == alias)) and (
|
|
777
791
|
not item_repo or item_repo == res['repo']):
|
|
778
|
-
it =
|
|
779
|
-
|
|
780
|
-
|
|
792
|
+
it = self._item_from_index_entry(res, target)
|
|
793
|
+
if it:
|
|
794
|
+
result.append(it)
|
|
795
|
+
found = True
|
|
781
796
|
if not found and folder_name:
|
|
782
797
|
for res in target_index:
|
|
783
798
|
if os.path.basename(res["path"]) == folder_name:
|
|
784
|
-
it =
|
|
785
|
-
|
|
799
|
+
it = self._item_from_index_entry(res, target)
|
|
800
|
+
if it:
|
|
801
|
+
result.append(it)
|
|
786
802
|
else:
|
|
787
803
|
tags = i.get("tags")
|
|
788
804
|
if tags:
|
|
@@ -803,11 +819,12 @@ class Action:
|
|
|
803
819
|
n_tags = [p[1:] for p in n_tags_]
|
|
804
820
|
p_tags = list(set(tags_to_match) - set(n_tags_))
|
|
805
821
|
for res in target_index:
|
|
806
|
-
c_tags = res
|
|
822
|
+
c_tags = res.get("tags") or []
|
|
807
823
|
if (exact_tags_match and set(p_tags) == set(c_tags)) or (not exact_tags_match and set(
|
|
808
824
|
p_tags).issubset(set(c_tags)) and set(n_tags).isdisjoint(set(c_tags))):
|
|
809
|
-
it =
|
|
810
|
-
|
|
825
|
+
it = self._item_from_index_entry(res, target)
|
|
826
|
+
if it:
|
|
827
|
+
result.append(it)
|
|
811
828
|
return {'return': 0, 'list': result}
|
|
812
829
|
|
|
813
830
|
find = search
|
|
@@ -5,6 +5,7 @@ import time
|
|
|
5
5
|
from . import utils
|
|
6
6
|
from .logger import logger
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
class CacheAction(Action):
|
|
9
10
|
"""
|
|
10
11
|
####################################################################################################################
|
|
@@ -15,14 +16,16 @@ class CacheAction(Action):
|
|
|
15
16
|
2. show
|
|
16
17
|
3. list
|
|
17
18
|
4. remove(rm)
|
|
19
|
+
5. prune
|
|
20
|
+
6. mark-tmp
|
|
18
21
|
|
|
19
22
|
"""
|
|
20
23
|
|
|
21
24
|
def __init__(self, parent=None):
|
|
22
|
-
#super().__init__(parent)
|
|
25
|
+
# super().__init__(parent)
|
|
23
26
|
self.parent = parent
|
|
24
27
|
self.__dict__.update(vars(parent))
|
|
25
|
-
|
|
28
|
+
|
|
26
29
|
def search(self, i):
|
|
27
30
|
"""
|
|
28
31
|
####################################################################################################################
|
|
@@ -30,11 +33,11 @@ class CacheAction(Action):
|
|
|
30
33
|
Action: Find (Alias: Search)
|
|
31
34
|
####################################################################################################################
|
|
32
35
|
|
|
33
|
-
The `find` (or `search`) action retrieves the path of cache generated by a particular script.
|
|
36
|
+
The `find` (or `search`) action retrieves the path of cache generated by a particular script.
|
|
34
37
|
|
|
35
38
|
Syntax:
|
|
36
39
|
|
|
37
|
-
mlc find cache --tags=<list_of_tag_used_to_run_the_particular_script>
|
|
40
|
+
mlc find cache --tags=<list_of_tag_used_to_run_the_particular_script>
|
|
38
41
|
|
|
39
42
|
Example Command:
|
|
40
43
|
|
|
@@ -42,7 +45,7 @@ class CacheAction(Action):
|
|
|
42
45
|
|
|
43
46
|
"""
|
|
44
47
|
i['target_name'] = "cache"
|
|
45
|
-
#logger.debug(f"Searching for cache with input: {i}")
|
|
48
|
+
# logger.debug(f"Searching for cache with input: {i}")
|
|
46
49
|
r = self.parent.search(i)
|
|
47
50
|
if r['return'] > 0:
|
|
48
51
|
return r
|
|
@@ -83,13 +86,13 @@ class CacheAction(Action):
|
|
|
83
86
|
|
|
84
87
|
Syntax:
|
|
85
88
|
|
|
86
|
-
mlc rm cache --tags=<list_of_tag_used_to_run_the_particular_script>
|
|
89
|
+
mlc rm cache --tags=<list_of_tag_used_to_run_the_particular_script>
|
|
87
90
|
|
|
88
91
|
Options:
|
|
89
92
|
1. `-f`: Force removes caches without confirmation. Without `-f`, the user will be prompted for confirmation before deletion.
|
|
90
93
|
|
|
91
94
|
To remove all generated caches, use:
|
|
92
|
-
|
|
95
|
+
|
|
93
96
|
mlc rm cache
|
|
94
97
|
|
|
95
98
|
Example Command:
|
|
@@ -98,9 +101,68 @@ class CacheAction(Action):
|
|
|
98
101
|
|
|
99
102
|
"""
|
|
100
103
|
i['target_name'] = "cache"
|
|
101
|
-
#logger.debug(f"Removing cache with input: {i}")
|
|
104
|
+
# logger.debug(f"Removing cache with input: {i}")
|
|
102
105
|
return self.parent.rm(i)
|
|
103
106
|
|
|
107
|
+
def mark_tmp(self, i):
|
|
108
|
+
"""
|
|
109
|
+
####################################################################################################################
|
|
110
|
+
Target: Cache
|
|
111
|
+
Action: Mark Tmp (mark-tmp)
|
|
112
|
+
####################################################################################################################
|
|
113
|
+
|
|
114
|
+
The `mark-tmp` action marks one or more cache entries as `tmp` without removing the cache contents.
|
|
115
|
+
|
|
116
|
+
Syntax:
|
|
117
|
+
|
|
118
|
+
mlc mark-tmp cache --tags=<list_of_tags_used_to_run_the_particular_script>
|
|
119
|
+
|
|
120
|
+
Example Command:
|
|
121
|
+
|
|
122
|
+
mlc mark-tmp cache --tags=get,dataset,igbh
|
|
123
|
+
|
|
124
|
+
"""
|
|
125
|
+
i['target_name'] = "cache"
|
|
126
|
+
res = self.search(i)
|
|
127
|
+
if res['return'] > 0:
|
|
128
|
+
return res
|
|
129
|
+
|
|
130
|
+
if len(res['list']) == 0:
|
|
131
|
+
logger.warning(f"No cache found for {i}")
|
|
132
|
+
return {'return': 0, "warnings": [
|
|
133
|
+
{"code": "EMPTY_TARGET", "description": f"No cache found for {i}"}]}
|
|
134
|
+
|
|
135
|
+
updated_count = 0
|
|
136
|
+
|
|
137
|
+
for item in res['list']:
|
|
138
|
+
tags = item.meta.get("tags", [])
|
|
139
|
+
if 'tmp' in tags:
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
tags.append('tmp')
|
|
143
|
+
item.meta["tags"] = tags
|
|
144
|
+
meta_yaml_path = os.path.join(item.path, "meta.yaml")
|
|
145
|
+
meta_json_path = os.path.join(item.path, "meta.json")
|
|
146
|
+
|
|
147
|
+
if os.path.exists(meta_yaml_path):
|
|
148
|
+
save_result = utils.save_yaml(meta_yaml_path, meta=item.meta)
|
|
149
|
+
else:
|
|
150
|
+
save_result = utils.save_json(meta_json_path, meta=item.meta)
|
|
151
|
+
|
|
152
|
+
if save_result['return'] > 0:
|
|
153
|
+
return save_result
|
|
154
|
+
|
|
155
|
+
self.get_index().update(item.meta, "cache", item.path, item.repo)
|
|
156
|
+
updated_count += 1
|
|
157
|
+
|
|
158
|
+
logger.info(f"Marked {updated_count} cache item(s) as tmp")
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
'return': 0,
|
|
162
|
+
'message': f"Marked {updated_count} cache item(s) as tmp",
|
|
163
|
+
'list': res['list']
|
|
164
|
+
}
|
|
165
|
+
|
|
104
166
|
def show(self, run_args):
|
|
105
167
|
"""
|
|
106
168
|
####################################################################################################################
|
|
@@ -109,9 +171,9 @@ class CacheAction(Action):
|
|
|
109
171
|
####################################################################################################################
|
|
110
172
|
|
|
111
173
|
Retrieves the path and metadata of caches generated while running scripts through MLCFlow.
|
|
112
|
-
|
|
174
|
+
|
|
113
175
|
Syntax:
|
|
114
|
-
|
|
176
|
+
|
|
115
177
|
mlc show cache --tags=<list_of_tags_used_while_running_script>
|
|
116
178
|
|
|
117
179
|
Example Command:
|
|
@@ -128,7 +190,11 @@ class CacheAction(Action):
|
|
|
128
190
|
return res
|
|
129
191
|
|
|
130
192
|
logger.info(f"Showing cache with tags: {run_args.get('tags')}")
|
|
131
|
-
cached_meta_keys_to_show = [
|
|
193
|
+
cached_meta_keys_to_show = [
|
|
194
|
+
"uid",
|
|
195
|
+
"tags",
|
|
196
|
+
"dependent_cached_path",
|
|
197
|
+
"associated_script_item"]
|
|
132
198
|
cached_state_keys_to_show = ["new_env", "new_state", "version"]
|
|
133
199
|
for item in res['list']:
|
|
134
200
|
print(f"""Location: {item.path}:
|
|
@@ -137,7 +203,8 @@ Cache Meta:""")
|
|
|
137
203
|
if key in item.meta:
|
|
138
204
|
print(f""" {key}: {item.meta[key]}""")
|
|
139
205
|
print("""Cached State:""")
|
|
140
|
-
cached_state_meta_file = os.path.join(
|
|
206
|
+
cached_state_meta_file = os.path.join(
|
|
207
|
+
item.path, "mlc-cached-state.json")
|
|
141
208
|
if not os.path.exists(cached_state_meta_file):
|
|
142
209
|
continue
|
|
143
210
|
try:
|
|
@@ -149,17 +216,16 @@ Cache Meta:""")
|
|
|
149
216
|
print(f""" {key}:""", end="")
|
|
150
217
|
if meta[key] and isinstance(meta[key], dict):
|
|
151
218
|
print("")
|
|
152
|
-
utils.printd(
|
|
219
|
+
utils.printd(
|
|
220
|
+
meta[key], yaml=False, sort_keys=True, begin_spaces=8)
|
|
153
221
|
else:
|
|
154
222
|
print(f""" {meta[key]}""")
|
|
155
223
|
except json.JSONDecodeError as e:
|
|
156
224
|
logger.error(f"Error decoding JSON: {e}")
|
|
157
225
|
print("......................................................")
|
|
158
226
|
print("")
|
|
159
|
-
|
|
160
|
-
return {'return': 0}
|
|
161
227
|
|
|
162
|
-
|
|
228
|
+
return {'return': 0}
|
|
163
229
|
|
|
164
230
|
def prune(self, args):
|
|
165
231
|
"""
|
|
@@ -176,12 +242,13 @@ Cache Meta:""")
|
|
|
176
242
|
|
|
177
243
|
"""
|
|
178
244
|
self.action_type = "cache"
|
|
179
|
-
|
|
180
|
-
|
|
245
|
+
# to fetch the details of all the caches generated
|
|
246
|
+
run_args = {"fetch_all": True}
|
|
247
|
+
|
|
181
248
|
res = self.search(run_args)
|
|
182
249
|
if res['return'] > 0:
|
|
183
250
|
return res
|
|
184
|
-
|
|
251
|
+
|
|
185
252
|
for item in res['list']:
|
|
186
253
|
expiration_time = item.meta.get('cache_expiration')
|
|
187
254
|
if expiration_time is not None and expiration_time < time.time():
|
|
@@ -192,8 +259,6 @@ Cache Meta:""")
|
|
|
192
259
|
self.rm(ii)
|
|
193
260
|
|
|
194
261
|
return {'return': 0}
|
|
195
|
-
|
|
196
|
-
|
|
197
262
|
|
|
198
263
|
def list(self, args):
|
|
199
264
|
"""
|
|
@@ -210,16 +275,18 @@ Cache Meta:""")
|
|
|
210
275
|
|
|
211
276
|
"""
|
|
212
277
|
self.action_type = "cache"
|
|
213
|
-
|
|
214
|
-
|
|
278
|
+
# to fetch the details of all the caches generated
|
|
279
|
+
run_args = {"fetch_all": True}
|
|
280
|
+
|
|
215
281
|
res = self.search(run_args)
|
|
216
282
|
if res['return'] > 0:
|
|
217
283
|
return res
|
|
218
|
-
|
|
284
|
+
|
|
219
285
|
logger.info(f"Listing all the caches and their paths")
|
|
220
286
|
print("......................................................")
|
|
221
287
|
for item in res['list']:
|
|
222
|
-
print(
|
|
288
|
+
print(
|
|
289
|
+
f"tags: {item.meta['tags'] if item.meta.get('tags') else 'None'}")
|
|
223
290
|
print(f"Location: {item.path}")
|
|
224
291
|
print("......................................................")
|
|
225
292
|
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from enum import Enum, auto
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ErrorCode(Enum):
|
|
6
|
+
"""Enum class for error codes in MLCFlow"""
|
|
7
|
+
# General errors (2000-2007)
|
|
8
|
+
AUTOMATION_SCRIPT_NOT_FOUND = (
|
|
9
|
+
2000, "The specified automation script was not found")
|
|
10
|
+
PATH_DOES_NOT_EXIST = (2001, "Provided path does not exists")
|
|
11
|
+
FILE_NOT_FOUND = (2002, "Required file was not found")
|
|
12
|
+
PERMISSION_DENIED = (2003, "Insufficient permission to execute the script")
|
|
13
|
+
IO_Error = (2004, "File I/O operation failed")
|
|
14
|
+
AUTOMATION_CUSTOM_ERROR = (2005, "Custom error triggered by the script")
|
|
15
|
+
UNSUPPORTED_OS = (
|
|
16
|
+
2006, "The Operating System is not supported by the script")
|
|
17
|
+
MISSING_ENV_VARIABLE = (2007, "Required environment variables are missing")
|
|
18
|
+
|
|
19
|
+
def __init__(self, code, description):
|
|
20
|
+
self.code = code
|
|
21
|
+
self.description = description
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
ERROR_CODES = {error.code for error in ErrorCode}
|
|
25
|
+
ERROR_CODE_PATTERNS = [
|
|
26
|
+
re.compile(r'(?:error|exit|return)\s+code\s*[:=]?\s*(\d+)', re.IGNORECASE),
|
|
27
|
+
re.compile(r'\[errno\s+(\d+)\]', re.IGNORECASE),
|
|
28
|
+
re.compile(r'signal\s+(\d+)', re.IGNORECASE),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WarningCode(Enum):
|
|
33
|
+
"""Enum class for warning codes in MLCFlow"""
|
|
34
|
+
# General warnings (1000-1007)
|
|
35
|
+
IO_WARNING = (1000, "File I/O operation warning")
|
|
36
|
+
AUTOMATION_SCRIPT_NOT_TESTED = (
|
|
37
|
+
1001,
|
|
38
|
+
"the script is not tested on the current operatinig system or is in a development state")
|
|
39
|
+
AUTOMATION_SCRIPT_SKIPPED = (
|
|
40
|
+
1002, "The script has been skipped during execution")
|
|
41
|
+
AUTOMATION_CUSTOM_ERROR = (1003, "Custom warning triggered by the script")
|
|
42
|
+
NON_INTERACTIVE_ENV = (1004, "Non interactive environment detected")
|
|
43
|
+
ELEVATED_PERMISSION_NEEDED = (1005, "Elevated permission needed")
|
|
44
|
+
EMPTY_TARGET = (1006, "The specified target is empty")
|
|
45
|
+
|
|
46
|
+
def __init__(self, code, description):
|
|
47
|
+
self.code = code
|
|
48
|
+
self.description = description
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_error_info(error_code):
|
|
52
|
+
"""Get the error message for a given error code"""
|
|
53
|
+
try:
|
|
54
|
+
return {"error_code": ErrorCode(
|
|
55
|
+
error_code).code, "error_message": ErrorCode(error_code).description}
|
|
56
|
+
except ValueError:
|
|
57
|
+
return f"Unknown error code: {error_code}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_warning_info(warning_code):
|
|
61
|
+
"""Get the warning message for a given warning code"""
|
|
62
|
+
try:
|
|
63
|
+
return {"warning_code": WarningCode(
|
|
64
|
+
warning_code).code, "warning_message": WarningCode(warning_code).description}
|
|
65
|
+
except ValueError:
|
|
66
|
+
return f"Unknown warning code: {warning_code}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_warning_code(code):
|
|
70
|
+
"""Check if a given code is a warning code"""
|
|
71
|
+
try:
|
|
72
|
+
# Check if code is in warning range (2000-2399)
|
|
73
|
+
if 2000 <= code <= 2399:
|
|
74
|
+
WarningCode(code)
|
|
75
|
+
return True
|
|
76
|
+
return False
|
|
77
|
+
except ValueError:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def is_error_code(code):
|
|
82
|
+
"""Check if a given code is an error code"""
|
|
83
|
+
try:
|
|
84
|
+
# Check if code is in error range (1000-1399)
|
|
85
|
+
if 1000 <= code <= 1399:
|
|
86
|
+
ErrorCode(code)
|
|
87
|
+
return True
|
|
88
|
+
return False
|
|
89
|
+
except ValueError:
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_code_type(code):
|
|
94
|
+
"""Get the type of a code (error or warning)"""
|
|
95
|
+
if is_error_code(code):
|
|
96
|
+
return "error"
|
|
97
|
+
elif is_warning_code(code):
|
|
98
|
+
return "warning"
|
|
99
|
+
else:
|
|
100
|
+
return "unknown"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _normalize_code(code):
|
|
104
|
+
"""Convert a code value to int when possible."""
|
|
105
|
+
if code is None:
|
|
106
|
+
return None
|
|
107
|
+
if isinstance(code, int):
|
|
108
|
+
return code
|
|
109
|
+
try:
|
|
110
|
+
return int(str(code).strip())
|
|
111
|
+
except (TypeError, ValueError):
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def detect_error_code(return_code=None, error_message=""):
|
|
116
|
+
"""Detect the most useful error code from a return value or error text."""
|
|
117
|
+
normalized_return_code = _normalize_code(return_code)
|
|
118
|
+
message = error_message or ""
|
|
119
|
+
|
|
120
|
+
for pattern in ERROR_CODE_PATTERNS:
|
|
121
|
+
match = pattern.search(message)
|
|
122
|
+
if match:
|
|
123
|
+
detected = _normalize_code(match.group(1))
|
|
124
|
+
# Prefer the code extracted from the error text when the outer
|
|
125
|
+
# result only carries a generic status such as 1.
|
|
126
|
+
if normalized_return_code in (None, 0, 1):
|
|
127
|
+
return detected
|
|
128
|
+
|
|
129
|
+
return normalized_return_code
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_error_guidance(return_code=None, error_message=""):
|
|
133
|
+
"""Return actionable guidance for known error codes and failure patterns."""
|
|
134
|
+
error_code = detect_error_code(return_code, error_message)
|
|
135
|
+
guidance = {
|
|
136
|
+
"error_code": error_code,
|
|
137
|
+
"error_message": None,
|
|
138
|
+
"suggestions": [],
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if error_code in ERROR_CODES:
|
|
142
|
+
error_info = get_error_info(error_code)
|
|
143
|
+
if isinstance(error_info, dict):
|
|
144
|
+
guidance["error_message"] = error_info["error_message"]
|
|
145
|
+
|
|
146
|
+
message = (error_message or "").lower()
|
|
147
|
+
|
|
148
|
+
if ("no space left on device" in message or
|
|
149
|
+
"disk full" in message or
|
|
150
|
+
"not enough space" in message):
|
|
151
|
+
guidance["error_message"] = guidance["error_message"] or \
|
|
152
|
+
"Likely disk space exhaustion while running the script"
|
|
153
|
+
guidance["suggestions"] = [
|
|
154
|
+
"Free disk space in the work/cache directories and retry the command.",
|
|
155
|
+
"Remove old artifacts or caches if they are no longer needed.",
|
|
156
|
+
]
|
|
157
|
+
# 139 = 128 + SIGSEGV(11), while some tools report the raw signal number
|
|
158
|
+
# 11 or its negative form -11.
|
|
159
|
+
elif ("segmentation fault" in message or error_code in [139, -11, 11]):
|
|
160
|
+
guidance["error_message"] = guidance["error_message"] or \
|
|
161
|
+
"A native program crashed with a segmentation fault"
|
|
162
|
+
guidance["suggestions"] = [
|
|
163
|
+
"Rerun with verbose logs to identify which native command crashed.",
|
|
164
|
+
"Check native dependencies, compiler/runtime compatibility, and input files.",
|
|
165
|
+
]
|
|
166
|
+
# Common downloader/network failure codes used by curl and similar tools:
|
|
167
|
+
# 6/7 resolve/connect failures, 28 timeout, 35 TLS/connect issue,
|
|
168
|
+
# 56 connection reset/read failure, 60 certificate validation failure.
|
|
169
|
+
elif ("network" in message or
|
|
170
|
+
"connection" in message or
|
|
171
|
+
"timed out" in message or
|
|
172
|
+
"temporary failure in name resolution" in message or
|
|
173
|
+
"could not resolve host" in message or
|
|
174
|
+
error_code in [6, 7, 28, 35, 56, 60]):
|
|
175
|
+
guidance["error_message"] = guidance["error_message"] or \
|
|
176
|
+
"Likely network or download failure while running the script"
|
|
177
|
+
guidance["suggestions"] = [
|
|
178
|
+
"Check internet connectivity, proxy/firewall settings, and remote endpoint availability.",
|
|
179
|
+
"Retry the command after verifying the network connection.",
|
|
180
|
+
]
|
|
181
|
+
elif error_code == 126:
|
|
182
|
+
guidance["error_message"] = guidance["error_message"] or \
|
|
183
|
+
"Command found but it could not be executed"
|
|
184
|
+
guidance["suggestions"] = [
|
|
185
|
+
"Check file permissions and whether the target command is executable.",
|
|
186
|
+
]
|
|
187
|
+
elif error_code == 127:
|
|
188
|
+
guidance["error_message"] = guidance["error_message"] or \
|
|
189
|
+
"Command not found during script execution"
|
|
190
|
+
guidance["suggestions"] = [
|
|
191
|
+
"Verify that the required tool is installed and available on PATH.",
|
|
192
|
+
]
|
|
193
|
+
elif error_code == 137:
|
|
194
|
+
guidance["error_message"] = guidance["error_message"] or \
|
|
195
|
+
"Process was terminated, often due to out-of-memory or a kill signal"
|
|
196
|
+
guidance["suggestions"] = [
|
|
197
|
+
"Check system memory limits and retry with fewer parallel jobs if possible.",
|
|
198
|
+
]
|
|
199
|
+
elif error_code == 130:
|
|
200
|
+
guidance["error_message"] = guidance["error_message"] or \
|
|
201
|
+
"The command was interrupted by the user or the environment"
|
|
202
|
+
guidance["suggestions"] = [
|
|
203
|
+
"Retry the command if the interruption was unexpected.",
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
if not guidance["error_message"] and not guidance["suggestions"]:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
return guidance
|
|
@@ -232,8 +232,12 @@ class Index:
|
|
|
232
232
|
|
|
233
233
|
if os.path.isfile(yaml_path):
|
|
234
234
|
config_path = yaml_path
|
|
235
|
+
with open(config_path, "r") as f:
|
|
236
|
+
data = yaml.safe_load(f) or {}
|
|
235
237
|
elif os.path.isfile(json_path):
|
|
236
238
|
config_path = json_path
|
|
239
|
+
with open(config_path, "r") as f:
|
|
240
|
+
data = json.load(f) or {}
|
|
237
241
|
else:
|
|
238
242
|
# No config file found, remove from index if exists
|
|
239
243
|
delete_flag = False
|
|
@@ -257,6 +261,7 @@ class Index:
|
|
|
257
261
|
if delete_flag:
|
|
258
262
|
changed = True
|
|
259
263
|
continue
|
|
264
|
+
|
|
260
265
|
if current_item_keys is not None:
|
|
261
266
|
current_item_keys.add(config_path)
|
|
262
267
|
mtime = self.get_item_mtime(config_path)
|
|
@@ -266,6 +271,19 @@ class Index:
|
|
|
266
271
|
if old_mtime == mtime and not repos_changed:
|
|
267
272
|
continue
|
|
268
273
|
|
|
274
|
+
# Validate script meta against schema during indexing if meta
|
|
275
|
+
# changed or repos changed
|
|
276
|
+
if folder_type == "script":
|
|
277
|
+
# logger.debug(f"-----Meta changed for {automation_path}, validating against schema... -----")
|
|
278
|
+
errors, warnings = validate_meta(data, config_path)
|
|
279
|
+
for e in errors:
|
|
280
|
+
logger.error(f"Meta validation error: {e}")
|
|
281
|
+
for w in warnings:
|
|
282
|
+
logger.debug(f"Meta validation warning: {w}")
|
|
283
|
+
if errors:
|
|
284
|
+
raise ValueError(
|
|
285
|
+
f"Meta validation failed for {config_path}. Fix the above error(s) and try again.")
|
|
286
|
+
|
|
269
287
|
self.modified_times[config_path] = {
|
|
270
288
|
"mtime": mtime,
|
|
271
289
|
"date_time": datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
|
|
@@ -400,17 +418,6 @@ class Index:
|
|
|
400
418
|
tags = data.get("tags", [])
|
|
401
419
|
alias = data.get("alias", None)
|
|
402
420
|
|
|
403
|
-
# Validate script meta against schema during indexing
|
|
404
|
-
if folder_type == "script":
|
|
405
|
-
errors, warnings = validate_meta(data, config_file)
|
|
406
|
-
for e in errors:
|
|
407
|
-
logger.error(f"Meta validation error: {e}")
|
|
408
|
-
for w in warnings:
|
|
409
|
-
logger.debug(f"Meta validation warning: {w}")
|
|
410
|
-
if errors:
|
|
411
|
-
raise ValueError(
|
|
412
|
-
f"Meta validation failed for {config_file}. Fix the above error(s) and try again.")
|
|
413
|
-
|
|
414
421
|
# Remove stale entry for the same meta file path if exists
|
|
415
422
|
self._delete_index_entries(folder_type, "path", folder_path)
|
|
416
423
|
|