torizon-templates-utils 0.0.1__tar.gz → 0.0.2__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 (18) hide show
  1. {torizon_templates_utils-0.0.1 → torizon_templates_utils-0.0.2}/LICENSE +1 -1
  2. {torizon_templates_utils-0.0.1 → torizon_templates_utils-0.0.2}/PKG-INFO +6 -2
  3. {torizon_templates_utils-0.0.1 → torizon_templates_utils-0.0.2}/pyproject.toml +7 -1
  4. torizon_templates_utils-0.0.2/torizon_templates_utils/animations.py +62 -0
  5. torizon_templates_utils-0.0.2/torizon_templates_utils/args.py +83 -0
  6. {torizon_templates_utils-0.0.1 → torizon_templates_utils-0.0.2}/torizon_templates_utils/colors.py +7 -0
  7. torizon_templates_utils-0.0.2/torizon_templates_utils/debug.py +22 -0
  8. {torizon_templates_utils-0.0.1 → torizon_templates_utils-0.0.2}/torizon_templates_utils/errors.py +18 -2
  9. torizon_templates_utils-0.0.2/torizon_templates_utils/network.py +17 -0
  10. torizon_templates_utils-0.0.2/torizon_templates_utils/tasks.py +921 -0
  11. {torizon_templates_utils-0.0.1 → torizon_templates_utils-0.0.2}/torizon_templates_utils.egg-info/PKG-INFO +6 -2
  12. {torizon_templates_utils-0.0.1 → torizon_templates_utils-0.0.2}/torizon_templates_utils.egg-info/SOURCES.txt +6 -0
  13. torizon_templates_utils-0.0.2/torizon_templates_utils.egg-info/requires.txt +4 -0
  14. {torizon_templates_utils-0.0.1 → torizon_templates_utils-0.0.2}/README.md +0 -0
  15. {torizon_templates_utils-0.0.1 → torizon_templates_utils-0.0.2}/setup.cfg +0 -0
  16. {torizon_templates_utils-0.0.1 → torizon_templates_utils-0.0.2}/torizon_templates_utils/__init__.py +0 -0
  17. {torizon_templates_utils-0.0.1 → torizon_templates_utils-0.0.2}/torizon_templates_utils.egg-info/dependency_links.txt +0 -0
  18. {torizon_templates_utils-0.0.1 → torizon_templates_utils-0.0.2}/torizon_templates_utils.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) .NET Foundation and Contributors
3
+ Copyright (c) Toradex AG 2024
4
4
 
5
5
  All rights reserved.
6
6
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: torizon_templates_utils
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: Package with utilities for Torizon Templates scripts
5
5
  Author-email: Matheus Castello <matheus.castello@toradex.com>
6
6
  Project-URL: Homepage, https://github.com/torizon/vscode-torizon-templates
@@ -11,6 +11,10 @@ Classifier: Operating System :: POSIX :: Linux
11
11
  Requires-Python: >=3.8
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
+ Requires-Dist: torizon-io-api
15
+ Requires-Dist: requests
16
+ Requires-Dist: pyyaml
17
+ Requires-Dist: debugpy
14
18
 
15
19
  # Torizon Templates Utils
16
20
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "torizon_templates_utils"
7
- version = "0.0.1"
7
+ version = "0.0.2"
8
8
  authors = [
9
9
  { name="Matheus Castello", email="matheus.castello@toradex.com" },
10
10
  ]
@@ -16,6 +16,12 @@ classifiers = [
16
16
  "License :: OSI Approved :: MIT License",
17
17
  "Operating System :: POSIX :: Linux",
18
18
  ]
19
+ dependencies = [
20
+ "torizon-io-api",
21
+ "requests",
22
+ "pyyaml",
23
+ "debugpy"
24
+ ]
19
25
 
20
26
  [project.urls]
21
27
  Homepage = "https://github.com/torizon/vscode-torizon-templates"
@@ -0,0 +1,62 @@
1
+ import sys
2
+ import time
3
+ import threading
4
+ from itertools import cycle
5
+
6
+ def run_command_with_wait_animation(call, *args):
7
+ anima_frames = ["🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "🕛"]
8
+
9
+ def animate():
10
+ for frame in cycle(anima_frames):
11
+ if not running[0]:
12
+ break
13
+ sys.stdout.write(f"\r{frame} :: RUNNING PLEASE WAIT :: {frame}")
14
+ sys.stdout.flush()
15
+ time.sleep(0.1)
16
+
17
+ # Clear the line
18
+ sys.stdout.write("\r ")
19
+
20
+ if running[1]:
21
+ sys.stdout.write("\r❌ :: TASK FAILED :: ❌\n")
22
+ else:
23
+ sys.stdout.write("\r✅ :: TASK COMPLETED :: ✅\n")
24
+
25
+ def target():
26
+ nonlocal output
27
+ try:
28
+ output = call(*args)
29
+ except Exception as e:
30
+ output = e
31
+ running[1] = True
32
+ finally:
33
+ running[0] = False
34
+
35
+ # [0] is if it's running [1] if it has failed
36
+ running = [True, False]
37
+ output = None
38
+
39
+ animation_thread = threading.Thread(target=animate)
40
+ command_thread = threading.Thread(target=target)
41
+
42
+ animation_thread.start()
43
+ command_thread.start()
44
+
45
+ command_thread.join()
46
+ animation_thread.join()
47
+
48
+ if isinstance(output, Exception):
49
+ raise output
50
+
51
+ return output
52
+
53
+
54
+ # # Example usage
55
+ # def example_script(duration):
56
+ # time.sleep(duration)
57
+ # return "Task finished"
58
+
59
+ # if __name__ == "__main__":
60
+ # print("LET'S RUN A SCRIPT THAT TAKES 5 SECONDS TO FINISH")
61
+ # result = run_command_in_background_with_wait_animation(example_script, 5)
62
+ # print(result)
@@ -0,0 +1,83 @@
1
+
2
+ import sys
3
+ from typing import TypeVar, Type
4
+ from torizon_templates_utils.errors import Error, Error_Out
5
+
6
+ T = TypeVar('T')
7
+
8
+ def get_arg_not_empty(index: int) -> str:
9
+ """
10
+ Get an argument from the command line.
11
+ If the argument is an empty string, an error is raised.
12
+ """
13
+ _arg = sys.argv[index]
14
+
15
+ if _arg == "":
16
+ Error_Out(
17
+ "Error: Argument cannot be empty",
18
+ Error.EUSER
19
+ )
20
+
21
+ return _arg
22
+
23
+
24
+ def get_optional_arg(index: int, default: T) -> T | bool | str:
25
+ """
26
+ Get an optional argument from the command line.
27
+ If the argument is not provided, the default value is returned.
28
+ """
29
+ if len(sys.argv) > index:
30
+ # sys.argv return string
31
+ # we need to return T
32
+ # FIXME: this only convert string to bool for now
33
+ if type(default) is bool:
34
+ if sys.argv[index] == "True" or sys.argv[index] == "true" or sys.argv[index] == "1":
35
+ return True
36
+ else:
37
+ return False
38
+
39
+ return sys.argv[index]
40
+
41
+ return default
42
+
43
+
44
+ def get_arg_iterative(
45
+ index: int, prompt: str, default_type: Type, default: T | None = None, iterative: bool = False
46
+ ) -> T | None | bool | str:
47
+ """
48
+ Get an argument from the command line.
49
+ If the argument is not provided, an error is raised.
50
+ """
51
+ if len(sys.argv) > index:
52
+ if default_type is bool:
53
+ if sys.argv[index] == "True":
54
+ return True
55
+ else:
56
+ return False
57
+
58
+ return sys.argv[index]
59
+ elif default != None:
60
+ return default
61
+ elif iterative:
62
+ _input = input(prompt)
63
+ if _input == "":
64
+ Error_Out(
65
+ "Error: Argument cannot be empty",
66
+ Error.EUSER
67
+ )
68
+ else:
69
+ if default_type is bool:
70
+ if _input == "True":
71
+ return True
72
+ else:
73
+ return False
74
+
75
+ return _input
76
+
77
+ else:
78
+ Error_Out(
79
+ f"Error: Argument for prompt [{prompt}] not provided",
80
+ Error.EUSER
81
+ )
82
+
83
+ return default
@@ -17,12 +17,19 @@ class BgColor(enum.IntEnum):
17
17
  NONE = 0
18
18
  BLACK = 40
19
19
  RED = 41
20
+ BRIGTH_RED = 101
20
21
  GREEN = 42
22
+ BRIGTH_GREEN = 102
21
23
  YELLOW = 43
24
+ BRIGTH_YELLOW = 103
22
25
  BLUE = 44
26
+ BRIGTH_BLUE = 104
23
27
  MAGENTA = 45
28
+ BRIGTH_MAGENTA = 105
24
29
  CYAN = 46
30
+ BRIGTH_CYAN = 106
25
31
  WHITE = 47
32
+ BRIGTH_WHITE = 107
26
33
 
27
34
  # override print to have color and background color
28
35
  def print(
@@ -0,0 +1,22 @@
1
+ # pylint: disable=missing-function-docstring
2
+ # pylint: disable=missing-module-docstring
3
+ import debugpy # type: ignore[import-untyped]
4
+
5
+ DEBUG_INITIALIZED = False
6
+
7
+ def vscode_prepare(port: int = 5679) -> None:
8
+ global DEBUG_INITIALIZED
9
+
10
+ if DEBUG_INITIALIZED:
11
+ return
12
+
13
+ print("__debugpy__")
14
+ debugpy.listen(("0.0.0.0", port))
15
+ print("__debugpy__ go")
16
+ debugpy.wait_for_client()
17
+ print(f"__debugpy__ is connected [{debugpy.is_client_connected()}]")
18
+
19
+ DEBUG_INITIALIZED = True
20
+
21
+ def breakpoint() -> None:
22
+ debugpy.breakpoint()
@@ -13,18 +13,33 @@ class _error_struct:
13
13
 
14
14
 
15
15
  class Error(Enum):
16
+ ENOCONF = _error_struct(
17
+ 1, "Not configured"
18
+ )
19
+ EINVAL = _error_struct(
20
+ 22, "Invalid argument"
21
+ )
16
22
  ENOPKG = _error_struct(
17
23
  65, "Package not installed"
18
24
  )
19
25
  EUSER = _error_struct(
20
26
  69, "User fault"
21
27
  )
22
- ENOCONF = _error_struct(
23
- 1, "Not configured"
28
+ EABORT = _error_struct(
29
+ 170, "Abort"
24
30
  )
25
31
  ENOFOUND = _error_struct(
26
32
  404, "Not found"
27
33
  )
34
+ EFAIL = _error_struct(
35
+ 500, "Failed"
36
+ )
37
+ EUNKNOWN = _error_struct(
38
+ 666, "Unknown error"
39
+ )
40
+ ETOMCRUISE = _error_struct(
41
+ 999, "Impossible condition"
42
+ )
28
43
 
29
44
 
30
45
  def Error_Out(msg: str, error: Error) -> None:
@@ -32,6 +47,7 @@ def Error_Out(msg: str, error: Error) -> None:
32
47
  print(f"Error cause: {error.value.message}\n", color=Color.RED)
33
48
  sys.exit(error.value.code)
34
49
 
50
+
35
51
  def last_return_code() -> int:
36
52
  # we are ignoring the type here because this will get the current
37
53
  # xonsh shell instance
@@ -0,0 +1,17 @@
1
+
2
+ import os
3
+ import re
4
+ import subprocess
5
+
6
+
7
+ def get_host_ip():
8
+ if 'WSL_DISTRO_NAME' in os.environ:
9
+ command = ["/mnt/c/Windows/System32/Wbem/WMIC.exe", "NICCONFIG", "WHERE", "IPEnabled=true", "GET", "IPAddress"]
10
+ result = subprocess.run(command, capture_output=True, text=True)
11
+ ip_address = re.search(r'((1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\.){3}(1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])', result.stdout)
12
+ if ip_address:
13
+ return ip_address.group(0)
14
+ else:
15
+ command = ["hostname", "-I"]
16
+ result = subprocess.run(command, capture_output=True, text=True)
17
+ return result.stdout.split()[0]
@@ -0,0 +1,921 @@
1
+
2
+ import os
3
+ import re
4
+ import yaml # type: ignore[import-untyped]
5
+ import json
6
+ import inspect
7
+ import mimetypes
8
+ import subprocess
9
+ from pathlib import Path
10
+ from typing import List, Dict, Type, TypeVar, Union, Tuple, Optional, Literal
11
+ from torizon_templates_utils.colors import print, Color
12
+
13
+ T = TypeVar('T')
14
+
15
+ def replace_tasks_input():
16
+ for file in Path('.').rglob('*.json'):
17
+ print(file)
18
+ mime_type, _ = mimetypes.guess_type(file)
19
+
20
+ if mime_type is None or mime_type.startswith("application/octet-stream"):
21
+ if "id_rsa" not in str(file):
22
+ with open(file, 'r') as f:
23
+ content = f.read()
24
+
25
+ content = content.replace("input:dockerLogin", "command:docker_login")
26
+ content = content.replace("input:dockerImageRegistry", "command:docker_registry")
27
+ content = content.replace("input:dockerPsswd", "command:docker_password")
28
+
29
+ with open(file, 'w') as f:
30
+ f.write(content)
31
+
32
+
33
+ def _cast_from_json(json_data, cls: Type[T]) -> T:
34
+ # check on json_data if there is some key with . like "name.prop"
35
+ # if so, we need change the key to something like "name_prop"
36
+ keys = list(json_data.keys())
37
+ for key in keys:
38
+ if '.' in key:
39
+ new_key = key.replace('.', '_')
40
+ json_data[new_key] = json_data.pop(key)
41
+
42
+ expected_args = inspect.signature(cls.__init__).parameters
43
+ filtered_data = {k: v for k, v in json_data.items() if k in expected_args}
44
+
45
+ # check if the cls type has the any attribute
46
+ # the any attribute is a Dict[str, str]
47
+ # and it store the non expected args
48
+ if 'any' in expected_args:
49
+ non_expected_args = {k: v for k, v in json_data.items() if k not in expected_args}
50
+ filtered_data['any'] = non_expected_args
51
+
52
+ return cls(**filtered_data)
53
+
54
+
55
+ # For Settings interface we are mapping only the Torizon specific settings
56
+ class TorizonSettings:
57
+ """
58
+ TorizonSettings is a interface to map specific VS Code settings defined
59
+ by the Torizon extension.
60
+ """
61
+ def __init__(
62
+ self,
63
+ torizon_psswd: Optional[str] = None,
64
+ torizon_login: Optional[str] = None,
65
+ torizon_ip: Optional[str] = None,
66
+ torizon_ssh_port: Optional[str] = None,
67
+ host_ip: Optional[str] = None,
68
+ torizon_workspace: Optional[str] = None,
69
+ torizon_debug_ssh_port: Optional[str] = None,
70
+ torizon_debug_port1: Optional[str] = None,
71
+ torizon_debug_port2: Optional[str] = None,
72
+ torizon_debug_port3: Optional[str] = None,
73
+ torizon_gpu: Optional[str] = None,
74
+ torizon_arch: Optional[str] = None,
75
+ wait_sync: Optional[str] = None,
76
+ torizon_run_as: Optional[str] = None,
77
+ torizon_app_root: Optional[str] = None,
78
+ docker_tag: Optional[str] = None,
79
+ tcb_packageName: Optional[str] = None,
80
+ tcb_version: Optional[str] = None,
81
+ torizon_gpuPrefixRC: Optional[str] = None,
82
+ any: Optional[Dict[str, str]] = None
83
+ ):
84
+
85
+ self.torizon_psswd = torizon_psswd
86
+ self.torizon_login = torizon_login
87
+ self.torizon_ip = torizon_ip
88
+ self.torizon_ssh_port = torizon_ssh_port
89
+ self.host_ip = host_ip
90
+ self.torizon_workspace = torizon_workspace
91
+ self.torizon_debug_ssh_port = torizon_debug_ssh_port
92
+ self.torizon_debug_port1 = torizon_debug_port1
93
+ self.torizon_debug_port2 = torizon_debug_port2
94
+ self.torizon_debug_port3 = torizon_debug_port3
95
+ self.torizon_gpu = torizon_gpu
96
+ self.torizon_arch = torizon_arch
97
+ self.wait_sync = wait_sync
98
+ self.torizon_run_as = torizon_run_as
99
+ self.torizon_app_root = torizon_app_root
100
+ self.docker_tag = docker_tag
101
+ self.tcb_packageName = tcb_packageName
102
+ self.tcb_version = tcb_version
103
+ self.torizon_gpuPrefixRC = torizon_gpuPrefixRC
104
+ self.any = any
105
+
106
+
107
+ # These are from:
108
+ # https://code.visualstudio.com/docs/editor/tasks-appendix
109
+
110
+ class ShellConfiguration:
111
+ def __init__(self, executable: str, args: Optional[List[str]]):
112
+ self.executable = executable
113
+ self.args = args
114
+
115
+ class CommandOptions:
116
+ def __init__(
117
+ self,
118
+ cwd: Optional[str] = None,
119
+ env: Optional[Dict[str, str]] = None,
120
+ shell: Optional[ShellConfiguration] = None
121
+ ):
122
+
123
+ self.cwd = cwd
124
+ self.env = env
125
+ self.shell = shell
126
+
127
+ # we are getting this data from json
128
+ # so we need to cast the classes dependencies
129
+ if shell:
130
+ self.shell = _cast_from_json(shell, ShellConfiguration)
131
+
132
+
133
+ class PresentationOptions:
134
+ def __init__(
135
+ self,
136
+ reveal: Optional[Literal['never', 'silent', 'always']] = None,
137
+ echo: Optional[bool] = None,
138
+ focus: Optional[bool] = None,
139
+ panel: Optional[Literal['shared', 'dedicated', 'new']] = None,
140
+ showReuseMessage: Optional[bool] = None,
141
+ clear: Optional[bool] = None,
142
+ group: Optional[str] = None
143
+ ):
144
+
145
+ self.reveal = reveal
146
+ self.echo = echo
147
+ self.focus = focus
148
+ self.panel = panel
149
+ self.showReuseMessage = showReuseMessage
150
+ self.clear = clear
151
+ self.group = group
152
+
153
+
154
+ class ProblemPattern:
155
+ def __init__(
156
+ self,
157
+ regexp: str,
158
+ kind: Optional[Literal['file', 'location']] = None,
159
+ file: Union[int, float] = 0,
160
+ location: Optional[Union[int, float]] = None,
161
+ line: Optional[Union[int, float]] =None,
162
+ column: Optional[Union[int, float]] = None,
163
+ endLine: Optional[Union[int, float]] = None,
164
+ endColumn: Optional[Union[int, float]] = None,
165
+ severity: Optional[Union[int, float]] = None,
166
+ code: Optional[Union[int, float]] = None,
167
+ message: Union[int, float] = 0,
168
+ loop: Optional[bool] = False
169
+ ):
170
+
171
+ self.regexp = regexp
172
+ self.kind = kind
173
+ self.file = file
174
+ self.location = location
175
+ self.line = line
176
+ self.column = column
177
+ self.endLine = endLine
178
+ self.endColumn = endColumn
179
+ self.severity = severity
180
+ self.code = code
181
+ self.message = message
182
+ self.loop = loop
183
+
184
+
185
+ class BackgroundMatcher:
186
+ def __init__(
187
+ self,
188
+ activeOnStart: Optional[bool] = False,
189
+ beginsPattern: Optional[str] = None,
190
+ endsPattern: Optional[str] = None
191
+ ):
192
+
193
+ self.activeOnStart = activeOnStart
194
+ self.beginsPattern = beginsPattern
195
+ self.endsPattern = endsPattern
196
+
197
+
198
+ class ProblemMatcher:
199
+ def __init__(
200
+ self,
201
+ base: Optional[str] = None,
202
+ owner: Optional[str] = 'external',
203
+ source: Optional[str] = None,
204
+ severity: Optional[Literal['error', 'warning', 'info']] = 'error',
205
+ fileLocation: Optional[str | List[str] | List[
206
+ Union[
207
+ Literal['search'],
208
+ Dict[str, Optional[List[str]]]
209
+ ]
210
+ ]] = None,
211
+ pattern: Optional[str | ProblemPattern | List[ProblemPattern]] = None,
212
+ background: Optional[BackgroundMatcher] = None
213
+ ):
214
+
215
+ self.base = base
216
+ self.owner = owner
217
+ self.source = source
218
+ self.severity = severity
219
+ self.fileLocation = fileLocation
220
+ self.pattern = pattern
221
+ self.background = background
222
+
223
+ # we are getting this data from json
224
+ # so we need to cast the classes dependencies
225
+ if pattern:
226
+ self.pattern = _cast_from_json(pattern, ProblemPattern)
227
+
228
+ if background:
229
+ self.background = _cast_from_json(background, BackgroundMatcher)
230
+
231
+
232
+ class RunOptions:
233
+ def __init__(
234
+ self,
235
+ reevaluateOnRerun: Optional[bool] = True,
236
+ runOn: Optional[Literal['default', 'folderOpen']] = 'default'
237
+ ):
238
+
239
+ self.reevaluateOnRerun = reevaluateOnRerun
240
+ self.runOn = runOn
241
+
242
+
243
+ class IconOptions:
244
+ def __init__(
245
+ self,
246
+ id: str,
247
+ color: Optional[str]
248
+ ):
249
+
250
+ self.id = id
251
+ self.color = color
252
+
253
+
254
+ class InputOptions:
255
+ def __init__(
256
+ self,
257
+ id: str,
258
+ description: str,
259
+ default: Optional[str] = None,
260
+ type: Optional[Literal['promptString', 'pickString']] = 'promptString',
261
+ options: Optional[List[str]] = None
262
+ ):
263
+
264
+ self.id = id
265
+ self.description = description
266
+ self.default = default
267
+ self.type = type
268
+ self.options = options
269
+
270
+
271
+ class TaskDescription:
272
+ def __init__(
273
+ self,
274
+ label: str,
275
+ type: Literal['shell', 'process'],
276
+ command: str,
277
+ hide: Optional[bool] = None,
278
+ isBackground: Optional[bool] = None,
279
+ args: Optional[List[str]] = None,
280
+ options: Optional[CommandOptions] = None,
281
+ group: Optional[Literal['build', 'test']] = None,
282
+ presentation: Optional[PresentationOptions] = None,
283
+ problemMatcher: Optional[str | ProblemMatcher | List[str] | List[ProblemMatcher]] = None,
284
+ runOptions: Optional[RunOptions] = None,
285
+ dependsOrder: Optional[Literal['sequence', 'parallel']] = None,
286
+ dependsOn: Optional[List[str]] = None,
287
+ icon: Optional[IconOptions] = None
288
+ ):
289
+
290
+ self.label = label
291
+ self.type = type
292
+ self.command = command
293
+ self.hide: bool = (hide if hide is not None else False)
294
+ self.isBackground = isBackground
295
+ self.options = options
296
+ self.args = args
297
+ self.group = group
298
+ self.presentation = presentation
299
+ self.problemMatcher = problemMatcher
300
+ self.runOptions = runOptions
301
+ self.dependsOrder = dependsOrder
302
+ self.dependsOn = dependsOn
303
+ self.icon = icon
304
+
305
+ # we are getting this data from json
306
+ # so we need to cast the classes dependencies
307
+ if options:
308
+ self.options = _cast_from_json(options, CommandOptions)
309
+
310
+ if presentation:
311
+ self.presentation = _cast_from_json(presentation, PresentationOptions)
312
+
313
+ if runOptions:
314
+ self.runOptions = _cast_from_json(runOptions, RunOptions)
315
+
316
+ if icon:
317
+ self.icon = _cast_from_json(icon, IconOptions)
318
+
319
+ def to_dict(self):
320
+ return {
321
+ 'label': self.label,
322
+ 'type': self.type,
323
+ 'command': self.command,
324
+ 'isBackground': self.isBackground,
325
+ 'args': self.args,
326
+ 'options': self.options.__dict__ if self.options else None,
327
+ 'group': self.group,
328
+ 'presentation': self.presentation.__dict__ if self.presentation else None,
329
+ 'problemMatcher': self.problemMatcher,
330
+ 'runOptions': self.runOptions.__dict__ if self.runOptions else None,
331
+ 'dependsOrder': self.dependsOrder,
332
+ 'dependsOn': self.dependsOn,
333
+ 'icon': self.icon.__dict__ if self.icon else None
334
+ }
335
+
336
+
337
+ class BaseTaskConfiguration:
338
+ def __init__(
339
+ self,
340
+ type: str,
341
+ command: str,
342
+ isBackground: Optional[bool] = None,
343
+ options: Optional[CommandOptions] = None,
344
+ args: Optional[str] = None,
345
+ presentation: Optional[PresentationOptions] = None,
346
+ problemMatcher: Optional[str | ProblemMatcher | List[str] | List[ProblemMatcher]] = None,
347
+ tasks: Optional[List[TaskDescription]] = None
348
+ ):
349
+
350
+ self.type = type
351
+ self.command = command
352
+ self.isBackground = isBackground
353
+ self.options = options
354
+ self.args = args
355
+ self.presentation = presentation
356
+ self.problemMatcher = problemMatcher
357
+ self.tasks = tasks
358
+
359
+
360
+ class TaskConfiguration:
361
+ """
362
+ TorizonConfiguration is a interface to map tasks.json file
363
+ """
364
+
365
+ def __init__(
366
+ self,
367
+ version: Literal['2.0.0'] = '2.0.0',
368
+ tasks: Optional[List[TaskDescription]] = None,
369
+ inputs: Optional[List[InputOptions]] = None,
370
+ windows: Optional[BaseTaskConfiguration] = None,
371
+ osx: Optional[BaseTaskConfiguration] = None,
372
+ linux: Optional[BaseTaskConfiguration] = None
373
+ ):
374
+
375
+ self.version = version
376
+ self.tasks = tasks
377
+ self.inputs = inputs
378
+ self.windows = windows
379
+ self.osx = osx
380
+ self.linux = linux
381
+
382
+ # as this could be from json dict, we need to cast the tasks
383
+ if tasks:
384
+ self.tasks = [_cast_from_json(task, TaskDescription) for task in tasks]
385
+
386
+ if inputs:
387
+ self.inputs = [_cast_from_json(_input, InputOptions) for _input in inputs]
388
+
389
+ # TODO:
390
+ # for now we are not casting the other configurations
391
+ # as them are not used in the templates
392
+
393
+
394
+ def get_tasks_json(file_path: str) -> TaskConfiguration:
395
+ with open(f"{file_path}/.vscode/tasks.json", 'r') as file:
396
+ return _cast_from_json(json.load(file), TaskConfiguration)
397
+
398
+
399
+ def get_settings_json(
400
+ file_path: str,
401
+ custom_file: str | None = None
402
+ ) -> TorizonSettings:
403
+ _file = custom_file if custom_file else "settings.json"
404
+
405
+ with open(f"{file_path}/.vscode/{_file}", 'r') as file:
406
+ _tor_settings = _cast_from_json(json.load(file), TorizonSettings)
407
+
408
+
409
+
410
+ return _tor_settings
411
+
412
+
413
+ class TaskRunner:
414
+ """
415
+ TaskRunner is a class to run tasks from tasks.json file
416
+ """
417
+
418
+ def __init__(
419
+ self,
420
+ tasks: List[TaskDescription],
421
+ inputs: List[InputOptions],
422
+ settings: TorizonSettings,
423
+ debug: bool = False
424
+ ):
425
+
426
+ self.__tasks = tasks
427
+ self.__inputs = inputs
428
+ self.__settings = settings
429
+ self.__debug = debug
430
+ self.__gitlab_ci = False
431
+ self.__override_env = True
432
+ self.__cli_inputs: Dict[str, str] = {}
433
+ self.__can_receive_interactive_input = False
434
+
435
+ # check if we have stdin
436
+ if os.isatty(0) and ("TASKS_DISABLE_INTERACTIVE_INPUT" not in os.environ):
437
+ self.__can_receive_interactive_input = True
438
+
439
+ # environment configs
440
+ if "DOCKER_PSSWD" in os.environ:
441
+ os.environ["config:docker_password"] = os.environ["DOCKER_PSSWD"]
442
+
443
+ if "GITLAB_CI" in os.environ:
444
+ self.__gitlab_ci = True
445
+
446
+ if "TASKS_OVERRIDE_ENV" in os.environ:
447
+ self.__override_env = False
448
+
449
+ if "TASKS_DEBUG" in os.environ:
450
+ self.__debug = True
451
+
452
+ self.__settings_to_env()
453
+
454
+
455
+ def __settings_to_env(self):
456
+ # for keys in settings, we are adding to env
457
+ for key, value in self.__settings.__dict__.items():
458
+ if value is not None:
459
+ os.environ[f"config:{key}"] = f"{value}"
460
+
461
+ # also for non Torizon ones
462
+ for key, value in self.__settings.any.items():
463
+ if isinstance(value, str) or \
464
+ isinstance(value, int) or \
465
+ isinstance(value, float):
466
+
467
+ os.environ[f"config:{key}"] = str(value)
468
+
469
+
470
+ def list_labels(self, show_hidden=False, no_index: bool = False):
471
+ i = 0
472
+
473
+ for task in self.__tasks:
474
+ if no_index:
475
+ if show_hidden or not task.hide:
476
+ print(task.label)
477
+ else:
478
+ if show_hidden or not task.hide:
479
+ print(f"{i}. \t{task.label}")
480
+
481
+ i += 1
482
+
483
+
484
+ def desc_input(self, id: str):
485
+ for _input in self.__inputs:
486
+ if _input.id == id:
487
+ print(json.dumps(_input.__dict__, indent=4))
488
+ return
489
+
490
+ raise ReferenceError(f"Input with id [{id}] not found")
491
+
492
+
493
+ def desc_task(self, label: int | str):
494
+ task = None
495
+
496
+ if isinstance(label, int):
497
+ task = self.__tasks[label]
498
+ else:
499
+ for _task in self.__tasks:
500
+ if _task.label == label:
501
+ task = _task
502
+ break
503
+
504
+ if task is not None:
505
+ task_txt = json.dumps(task.to_dict(), indent=4)
506
+ print(task_txt)
507
+ else:
508
+ raise ReferenceError(f"Task with index [{label}] not found")
509
+
510
+
511
+ def __replace_env_var(self, var: str, env: str):
512
+ if f"${{{var}}}" in env:
513
+ return env.replace(f"${{{var}}}", os.environ[var])
514
+
515
+
516
+ def __check_workspace_folder(self, env: List[str]) -> List[str]:
517
+ ret: List[str] = []
518
+
519
+ for value in env:
520
+ if "workspaceFolder" in value:
521
+ value = self.__replace_env_var("workspaceFolder", value)
522
+ ret.append(value)
523
+
524
+ return ret
525
+
526
+
527
+ def __check_torizon_inputs(self, env: List[str]) -> List[str]:
528
+ ret: List[str] = []
529
+
530
+ for value in env:
531
+ if "${command:torizon_" in value:
532
+ value = value.replace("${command:torizon_", "${config:torizon_")
533
+ ret.append(value)
534
+
535
+ return ret
536
+
537
+
538
+ def __check_docker_inputs(self, env: List[str]) -> List[str]:
539
+ ret: List[str] = []
540
+
541
+ for value in env:
542
+ if "${command:docker_" in value:
543
+ value = value.replace("${command:docker_", "${config:docker_")
544
+ ret.append(value)
545
+
546
+ return ret
547
+
548
+
549
+ def __check_tcb_inputs(self, env: List[str]) -> List[str]:
550
+ ret: List[str] = []
551
+
552
+ for value in env:
553
+ if "${command:tcb" in value:
554
+ if "tcb.getNextPackageVersion" in value:
555
+ # call the xonsh script
556
+ _p_ret = subprocess.run(
557
+ [
558
+ "xonsh",
559
+ "./conf/torizon-io.xsh",
560
+ "package", "latest", "version",
561
+ os.environ["config:tcb_packageName"]
562
+ ],
563
+ capture_output=True,
564
+ text=True
565
+ )
566
+
567
+ if _p_ret.returncode != 0:
568
+ raise RuntimeError(f"Error running torizon-io.xsh: {_p_ret.stderr}")
569
+
570
+ _next = int(_p_ret.stdout.strip()) +1
571
+
572
+ if self.__debug:
573
+ print(f"Next package version: {_next}")
574
+
575
+ ret.append(
576
+ value.replace("${{command:tcb.getNextPackageVersion}}", f"{_next}")
577
+ )
578
+
579
+ elif "tcb.outputTEZIFolder" in value:
580
+ # load the tcbuild.yaml
581
+ with open("tcbuild.yaml", 'r') as file:
582
+ _tcbuild = yaml.load(file, Loader=yaml.FullLoader)
583
+
584
+ _tezi_folder = None
585
+ try:
586
+ _tezi_folder = _tcbuild["output"]["easy-installer"]["local"]
587
+ except KeyError:
588
+ raise RuntimeError("Error replacing variable tcb.outputTEZIFolder, make sure the tcbuild.yaml has the output.easy-installer.local property")
589
+
590
+ value = value.replace("${{command:tcb.outputTEZIFolder}}", _tezi_folder)
591
+
592
+ # for all the items we need to replace ${command:tcb. with ${config:tcb.
593
+ _pattern = r"(?<=\$\{command:tcb\.).*?(?=\s*})"
594
+ _matches = re.findall(_pattern, value)
595
+
596
+ for match in _matches:
597
+ value = value.replace(f"${{command:tcb.{match}}}", f"${{config:tcb.{match}}}")
598
+
599
+ ret.append(value)
600
+
601
+ return ret
602
+
603
+
604
+ def __contains_special_chars(self, str: str) -> bool:
605
+ _pattern = r"[^a-zA-Z0-9\.\-_|>\/=]"
606
+ return re.search(_pattern, str) is not None
607
+
608
+
609
+ def __scape_args(self, args: List[str]) -> List[str]:
610
+ ret: List[str] = []
611
+
612
+ for arg in args:
613
+ if "\"" in arg:
614
+ arg = arg.replace("\"", "\\\"")
615
+
616
+ ret.append(arg)
617
+
618
+ return ret
619
+
620
+
621
+ def __check_config(self, args: List[str]) -> List[str]:
622
+ """
623
+ This method will make the config replacement in the args
624
+ """
625
+ ret: List[str] = []
626
+
627
+ for arg in args:
628
+ if "${config:" in arg:
629
+ _pattern = r"(?<=\$\{config:).*?(?=\s*})"
630
+ _matches = re.findall(_pattern, arg)
631
+
632
+ for match in _matches:
633
+ if "." in match:
634
+ _match = match.replace(".", "_")
635
+ else:
636
+ _match = match
637
+
638
+ # first check if the config exists
639
+ if f"config:{_match}" not in os.environ:
640
+ raise ReferenceError(f"Config with id [{match}] not found. Check your settings.json")
641
+
642
+ # edge case for docker_registry
643
+ if _match == "docker_registry" and os.environ[f"config:{_match}"] == "":
644
+ os.environ[f"config:{_match}"] = "registry-1.docker.io"
645
+
646
+ arg = arg.replace(f"${{config:{match}}}", os.environ[f"config:{_match}"])
647
+
648
+ ret.append(arg)
649
+
650
+ return ret
651
+
652
+
653
+ def __check_vscode_env(self, args: List[str]) -> List[str]:
654
+ """
655
+ handle the VS Code ${env:VAR} replacement
656
+ """
657
+ ret: List[str] = []
658
+
659
+ for arg in args:
660
+ if "${env:" in arg:
661
+ _pattern = r"(?<=\$\{env:).*?(?=\s*})"
662
+ _matches = re.findall(_pattern, arg)
663
+
664
+ for match in _matches:
665
+ if match not in os.environ:
666
+ raise ReferenceError(f"Environment variable with id [{match}] not found")
667
+
668
+ arg = arg.replace(f"${{env:{match}}}", os.environ[match])
669
+
670
+ ret.append(arg)
671
+
672
+ return ret
673
+
674
+
675
+ def __check_long_args(self, args: List[str]) -> List[str]:
676
+ ret: List[str] = []
677
+
678
+ for arg in args:
679
+ if " " in arg:
680
+ arg = f"'{arg}'"
681
+
682
+ ret.append(arg)
683
+
684
+ return ret
685
+
686
+
687
+ def __quoting_special_chars(self, args: List[str]) -> List[str]:
688
+ ret: List[str] = []
689
+
690
+ for arg in args:
691
+ _has_special_chars = self.__contains_special_chars(arg)
692
+ _hash_space = " " in arg
693
+
694
+ if _has_special_chars and not _hash_space:
695
+ arg = f"'{arg}'"
696
+
697
+ ret.append(arg)
698
+
699
+ return ret
700
+
701
+
702
+ def __check_input(self, args: List[str]) -> List[str]:
703
+ ret: List[str] = []
704
+
705
+ for arg in args:
706
+ if "${input:" in arg:
707
+ _pattern = r"(?<=\$\{input:).*?(?=\s*})"
708
+ _matches = re.findall(_pattern, arg)
709
+
710
+ for match in _matches:
711
+ _input = None
712
+ _input_value = "None"
713
+
714
+ for inp in self.__inputs:
715
+ if inp.id == match:
716
+ _input = inp
717
+ break
718
+
719
+ if _input is None:
720
+ raise ReferenceError(f"Input with id [{match}] not found")
721
+
722
+ # first check if the input was set by cli
723
+ if match in self.__cli_inputs:
724
+ _input_value = self.__cli_inputs[match]
725
+ elif _input.default:
726
+ _input_value = _input.default
727
+ else:
728
+ if not self.__can_receive_interactive_input:
729
+ raise RuntimeError("CLI inputs not set and interactive input is disabled")
730
+
731
+ if _input.type == "promptString":
732
+ _input_value = input(f"{_input.description}: ")
733
+ elif _input.type == "pickString":
734
+ for _inp in self.__inputs:
735
+ if _inp.id == match:
736
+ # print options
737
+ assert _inp.options is not None, "pickString option has a valid id but options is empty. Check your tasks.json"
738
+ print(f"Options for [{match}]:")
739
+ _i = 0
740
+ _indexed_options = {}
741
+
742
+ for _opt in _inp.options:
743
+ _indexed_options[str(_i)] = _opt
744
+ print(f"{_i}. {_opt}")
745
+ _i += 1
746
+
747
+ _input_value = input(f"{_input.description} (option index): ")
748
+
749
+ # check if the input is in the options
750
+ if _input_value not in _indexed_options:
751
+ raise ValueError(f"Input value for [{match}] is not in the possible options")
752
+ else:
753
+ _input_value = _indexed_options[_input_value]
754
+
755
+ if _input_value is None:
756
+ raise ValueError(f"Input value for [{match}] could not be None")
757
+
758
+ arg = arg.replace(f"${{input:{match}}}", _input_value)
759
+
760
+ ret.append(arg)
761
+
762
+ return ret
763
+
764
+
765
+ def __parse_envs(self, env: str, task: TaskDescription) -> str | None :
766
+ """
767
+ It's christmas time 🎅
768
+ """
769
+ if task.options:
770
+ value = task.options.env
771
+
772
+ # get the env from the task
773
+ if value:
774
+ _env_value = value.get(env)
775
+
776
+ if _env_value:
777
+ expvalue = [_env_value]
778
+ expvalue = self.__check_workspace_folder(expvalue)
779
+ expvalue = self.__check_torizon_inputs(expvalue)
780
+ expvalue = self.__check_docker_inputs(expvalue)
781
+ expvalue = self.__check_tcb_inputs(expvalue)
782
+ expvalue = self.__check_input(expvalue)
783
+ expvalue = self.__check_config(expvalue)
784
+ exp_value_str = " ".join(expvalue)
785
+
786
+ if self.__debug:
787
+ print(f"Env: {env}={_env_value}")
788
+ print(f"Parsed Env: {env}={exp_value_str}")
789
+
790
+ return exp_value_str
791
+
792
+ return None
793
+
794
+
795
+ def __replace_docker_host(self, arg: str) -> str:
796
+ if "DOCKER_HOST" in arg:
797
+ arg = arg.replace("DOCKER_HOST=", "DOCKER_HOST=tcp://docker:2375")
798
+
799
+ return arg
800
+
801
+
802
+ def set_cli_inputs(self, cli_inputs: Dict[str, str]) -> None:
803
+ """
804
+ Set the cli inputs to be used in the tasks.
805
+ """
806
+ for key, value in cli_inputs.items():
807
+ # validate if the key is in the inputs
808
+ _input = None
809
+ _input = next((inp for inp in self.__inputs if inp.id == key), None)
810
+
811
+ if _input is None:
812
+ raise ReferenceError(f"Input with id [{key}] not found")
813
+
814
+ self.__cli_inputs[key] = value
815
+
816
+
817
+ def run_task(self, label: str) -> None:
818
+ # query the task
819
+ _task = None
820
+ _task = next((task for task in self.__tasks if task.label == label), None)
821
+
822
+ if _task is None:
823
+ raise ReferenceError(f"Task with label [{label}] not found")
824
+
825
+ # prepare the command
826
+ _cmd = _task.command
827
+
828
+ # the cmd itself can use the mechanism to replace stuff
829
+ _cmd = self.__check_workspace_folder([_cmd])[0]
830
+ _cmd = self.__check_torizon_inputs([_cmd])[0]
831
+ _cmd = self.__check_docker_inputs([_cmd])[0]
832
+ _cmd = self.__check_tcb_inputs([_cmd])[0]
833
+ _cmd = self.__check_input([_cmd])[0]
834
+ _cmd = self.__check_vscode_env([_cmd])[0]
835
+ _cmd = self.__check_config([_cmd])[0]
836
+
837
+ _args = []
838
+ if _task.args is not None:
839
+ _args = _task.args
840
+
841
+ _env: Dict[str, str] | None = {}
842
+ _cwd = None
843
+ _last_cwd = os.getcwd()
844
+ if _task.options is not None:
845
+ _env = _task.options.env
846
+ _cwd = _task.options.cwd
847
+
848
+ _depends = []
849
+ if _task.dependsOn is not None:
850
+ _depends = _task.dependsOn
851
+
852
+ # first we need to run the dependencies
853
+ for dep in _depends:
854
+ self.run_task(dep)
855
+
856
+ print(f"> Executing task: {label} <", color=Color.GREEN)
857
+
858
+ _is_background = ""
859
+ if _task.isBackground:
860
+ _is_background = " &"
861
+
862
+ _shell = _task.type == "shell"
863
+
864
+ # FIXME: The scape args was in the powershell implementation
865
+ # but when used on Python it generates weird behavior
866
+ # _args = self.__scape_args(_args)
867
+ _args = self.__check_workspace_folder(_args)
868
+ _args = self.__check_torizon_inputs(_args)
869
+ _args = self.__check_docker_inputs(_args)
870
+ _args = self.__check_tcb_inputs(_args)
871
+ _args = self.__check_input(_args)
872
+ _args = self.__check_vscode_env(_args)
873
+ _args = self.__check_config(_args)
874
+ _args = self.__check_long_args(_args)
875
+ _args = self.__quoting_special_chars(_args)
876
+
877
+ # if in gitlab ci env we need to replace the DOCKER_HOST
878
+ if self.__gitlab_ci:
879
+ _cmd = self.__replace_docker_host(_cmd)
880
+
881
+ # inject env
882
+ if _env is not None:
883
+ for env, value in _env.items():
884
+ if self.__override_env:
885
+ __env = self.__parse_envs(env, _task)
886
+ if __env:
887
+ os.environ[env] = __env
888
+ else:
889
+ if env not in os.environ:
890
+ __env = self.__parse_envs(env, _task)
891
+ if __env:
892
+ os.environ[env] = __env
893
+
894
+ # we need to change the cwd if it's set
895
+ if _cwd is not None:
896
+ os.chdir(_cwd)
897
+
898
+ # execute the task
899
+ _cmd_join = f"{_cmd} {' '.join(_args)}{_is_background}"
900
+
901
+ if self.__debug:
902
+ print(f"Command: {_task.command}", color=Color.YELLOW)
903
+ print(f"Args: {_task.args}", color=Color.YELLOW)
904
+ print(f"Parsed Args: {_args}", color=Color.YELLOW)
905
+ print(f"Parsed Command: {_cmd_join}", color=Color.YELLOW)
906
+
907
+ _ret = subprocess.run(
908
+ [_cmd, *_args] if not _shell else _cmd_join,
909
+ stdout=None,
910
+ stderr=None,
911
+ env=os.environ,
912
+ shell=_shell
913
+ )
914
+
915
+ # go back to the last cwd
916
+ os.chdir(_last_cwd)
917
+
918
+ if _ret.returncode != 0:
919
+ print(f"> TASK [{label}] exited with error code [{_ret.returncode}] <", color=Color.RED)
920
+ raise RuntimeError(f"Error running task: {label}")
921
+
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: torizon_templates_utils
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: Package with utilities for Torizon Templates scripts
5
5
  Author-email: Matheus Castello <matheus.castello@toradex.com>
6
6
  Project-URL: Homepage, https://github.com/torizon/vscode-torizon-templates
@@ -11,6 +11,10 @@ Classifier: Operating System :: POSIX :: Linux
11
11
  Requires-Python: >=3.8
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
+ Requires-Dist: torizon-io-api
15
+ Requires-Dist: requests
16
+ Requires-Dist: pyyaml
17
+ Requires-Dist: debugpy
14
18
 
15
19
  # Torizon Templates Utils
16
20
 
@@ -2,9 +2,15 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  torizon_templates_utils/__init__.py
5
+ torizon_templates_utils/animations.py
6
+ torizon_templates_utils/args.py
5
7
  torizon_templates_utils/colors.py
8
+ torizon_templates_utils/debug.py
6
9
  torizon_templates_utils/errors.py
10
+ torizon_templates_utils/network.py
11
+ torizon_templates_utils/tasks.py
7
12
  torizon_templates_utils.egg-info/PKG-INFO
8
13
  torizon_templates_utils.egg-info/SOURCES.txt
9
14
  torizon_templates_utils.egg-info/dependency_links.txt
15
+ torizon_templates_utils.egg-info/requires.txt
10
16
  torizon_templates_utils.egg-info/top_level.txt
@@ -0,0 +1,4 @@
1
+ torizon-io-api
2
+ requests
3
+ pyyaml
4
+ debugpy