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.
Files changed (32) hide show
  1. {mlcflow-1.2.3 → mlcflow-1.2.5}/PKG-INFO +6 -7
  2. {mlcflow-1.2.3 → mlcflow-1.2.5}/README.md +5 -6
  3. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/action.py +26 -9
  4. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/cache_action.py +92 -25
  5. mlcflow-1.2.5/mlc/error_codes.py +209 -0
  6. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/index.py +18 -11
  7. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/main.py +22 -7
  8. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/repo_action.py +111 -7
  9. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/script_action.py +44 -4
  10. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlcflow.egg-info/PKG-INFO +6 -7
  11. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlcflow.egg-info/SOURCES.txt +4 -1
  12. {mlcflow-1.2.3 → mlcflow-1.2.5}/pyproject.toml +1 -1
  13. mlcflow-1.2.5/tests/test_action_invalid_meta_entries.py +165 -0
  14. mlcflow-1.2.5/tests/test_cache_mark_tmp.py +73 -0
  15. mlcflow-1.2.5/tests/test_script_action_apptainer.py +29 -0
  16. mlcflow-1.2.3/mlc/error_codes.py +0 -77
  17. {mlcflow-1.2.3 → mlcflow-1.2.5}/LICENSE.md +0 -0
  18. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/__init__.py +0 -0
  19. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/__main__.py +0 -0
  20. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/action_factory.py +0 -0
  21. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/cfg_action.py +0 -0
  22. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/experiment_action.py +0 -0
  23. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/item.py +0 -0
  24. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/logger.py +0 -0
  25. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/meta_schema.py +0 -0
  26. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/repo.py +0 -0
  27. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlc/utils.py +0 -0
  28. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlcflow.egg-info/dependency_links.txt +0 -0
  29. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlcflow.egg-info/entry_points.txt +0 -0
  30. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlcflow.egg-info/requires.txt +0 -0
  31. {mlcflow-1.2.3 → mlcflow-1.2.5}/mlcflow.egg-info/top_level.txt +0 -0
  32. {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
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
  [![License](https://img.shields.io/badge/License-Apache%202.0-green)](https://github.com/mlcommons/mlcflow/blob/main/LICENSE.md)
205
205
  [![Downloads](https://static.pepy.tech/badge/mlcflow)](https://pepy.tech/project/mlcflow)
206
206
 
207
- [![MLC core actions test](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml/badge.svg)](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml)
208
- [![MLC script automation features test](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml/badge.svg?cache-bust=1)](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml)
209
- [![MLPerf inference resnet50](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml/badge.svg)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml)
210
- [![MLPerf inference bert (deepsparse, tf, onnxruntime, pytorch)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-bert.yml/badge.svg)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-bert.yml)
207
+ [![MLC core actions test](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml/badge.svg?branch=dev)](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml)
208
+ [![MLC script automation features test](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml/badge.svg?branch=dev)](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml)
209
+ [![MLPerf inference resnet50](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml/badge.svg?branch=dev)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml)
210
+ [![MLPerf inference bert (deepsparse, tf, onnxruntime, pytorch)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-bert.yml/badge.svg?branch=dev)](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
  [![License](https://img.shields.io/badge/License-Apache%202.0-green)](https://github.com/mlcommons/mlcflow/blob/main/LICENSE.md)
4
4
  [![Downloads](https://static.pepy.tech/badge/mlcflow)](https://pepy.tech/project/mlcflow)
5
5
 
6
- [![MLC core actions test](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml/badge.svg)](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml)
7
- [![MLC script automation features test](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml/badge.svg?cache-bust=1)](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml)
8
- [![MLPerf inference resnet50](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml/badge.svg)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml)
9
- [![MLPerf inference bert (deepsparse, tf, onnxruntime, pytorch)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-bert.yml/badge.svg)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-bert.yml)
6
+ [![MLC core actions test](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml/badge.svg?branch=dev)](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml)
7
+ [![MLC script automation features test](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml/badge.svg?branch=dev)](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml)
8
+ [![MLPerf inference resnet50](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml/badge.svg?branch=dev)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml)
9
+ [![MLPerf inference bert (deepsparse, tf, onnxruntime, pytorch)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-bert.yml/badge.svg?branch=dev)](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
- result.append(Item(res['path'], res['repo']))
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 = Item(res['path'], res['repo'])
779
- result.append(it)
780
- found = True
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 = Item(res['path'], res['repo'])
785
- result.append(it)
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["tags"]
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 = Item(res['path'], res['repo'])
810
- result.append(it)
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 = ["uid", "tags", "dependent_cached_path", "associated_script_item"]
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(item.path, "mlc-cached-state.json")
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(meta[key], yaml=False, sort_keys=True, begin_spaces=8)
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
- run_args = {"fetch_all": True} # to fetch the details of all the caches generated
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
- run_args = {"fetch_all": True} # to fetch the details of all the caches generated
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(f"tags: {item.meta['tags'] if item.meta.get('tags') else 'None'}")
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