robotframework-velo-cli 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,293 @@
1
+ Metadata-Version: 2.4
2
+ Name: robotframework-velo-cli
3
+ Version: 0.1.7
4
+ Summary: Run Robot Framework test suites on the Velo cloud platform
5
+ Author-email: Velo <support@velo.com>
6
+ License: Copyright (c) 2026 Velo. All rights reserved.
7
+
8
+ PROPRIETARY SOFTWARE LICENSE
9
+
10
+ This software and its source code, documentation, and associated files
11
+ (collectively, the "Software") are the exclusive property of Velo and are
12
+ protected by copyright law and international treaties.
13
+
14
+ GRANT OF LICENSE
15
+
16
+ Velo grants you a limited, non-exclusive, non-transferable, non-sub licensable
17
+ license to use the Software solely for your internal business purposes,
18
+ strictly in accordance with any agreement entered into with Velo.
19
+
20
+ RESTRICTIONS
21
+
22
+ You may not, and you may not permit any third party to:
23
+
24
+ 1. Copy, modify, adapt, translate, or create derivative works of the Software;
25
+ 2. Reverse engineer, disassemble, decompile, or otherwise attempt to derive
26
+ the source code of the Software;
27
+ 3. Sell, sublicense, rent, lease, transfer, or otherwise make the Software
28
+ available to any third party;
29
+ 4. Remove or alter any proprietary notices, labels, or marks on the Software;
30
+ 5. Use the Software for any purpose other than as expressly permitted
31
+ under this license.
32
+
33
+ NO WARRANTY
34
+
35
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
36
+ IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS
37
+ FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. IN NO EVENT SHALL VELO BE
38
+ LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE USE OF
39
+ THE SOFTWARE.
40
+
41
+ TERMINATION
42
+
43
+ This license is effective until terminated. It will terminate automatically
44
+ if you fail to comply with any of its terms. Upon termination, you must
45
+ immediately cease all use of the Software and destroy any copies in your
46
+ possession.
47
+
48
+ GOVERNING LAW
49
+
50
+ This license shall be governed by and construed in accordance with applicable
51
+ law. Any disputes arising under this license shall be subject to the exclusive
52
+ jurisdiction of the competent courts.
53
+
54
+ For licensing inquiries, contact: legal@velo.com
55
+
56
+ Project-URL: Homepage, https://velo.com
57
+ Project-URL: Bug Tracker, https://github.com/velo/robotframework-velo-cli/issues
58
+ Keywords: robotframework,robot,testing,automation,velo,sap
59
+ Classifier: Development Status :: 3 - Alpha
60
+ Classifier: Framework :: Robot Framework
61
+ Classifier: Framework :: Robot Framework :: Tool
62
+ Classifier: Intended Audience :: Developers
63
+ Classifier: Intended Audience :: Information Technology
64
+ Classifier: Programming Language :: Python :: 3
65
+ Classifier: Programming Language :: Python :: 3.10
66
+ Classifier: Programming Language :: Python :: 3.11
67
+ Classifier: Programming Language :: Python :: 3.12
68
+ Classifier: Operating System :: OS Independent
69
+ Classifier: Topic :: Software Development :: Testing
70
+ Requires-Python: >=3.10
71
+ Description-Content-Type: text/markdown
72
+ License-File: LICENSE
73
+ Requires-Dist: robotframework>=6.0
74
+ Requires-Dist: requests>=2.31
75
+ Requires-Dist: pathspec>=0.12
76
+ Provides-Extra: dev
77
+ Requires-Dist: pytest>=7; extra == "dev"
78
+ Requires-Dist: pytest-cov; extra == "dev"
79
+ Requires-Dist: ruff; extra == "dev"
80
+ Requires-Dist: mypy; extra == "dev"
81
+ Dynamic: license-file
82
+
83
+ # robotframework-velo-cli
84
+
85
+ Run Robot Framework test suites on the Velo cloud platform — no SAP GUI setup required on your machine.
86
+
87
+ ---
88
+
89
+ ## Installation
90
+
91
+ ```bash
92
+ pip install robotframework-velo-cli
93
+ ```
94
+
95
+ > **Note:** This package intentionally installs a `robot` command that wraps and replaces the standard Robot Framework entry point. `robotframework` is a declared dependency and will be installed automatically. Because `robotframework-velo-cli` is installed after `robotframework`, its `robot` script takes precedence. When `VELO_REMOTE` is not set the wrapper passes all arguments through to Robot Framework unchanged — your existing workflow is unaffected.
96
+
97
+ ---
98
+
99
+ ## Quick start
100
+
101
+ ```bash
102
+ # 1. Set your workspace credentials
103
+ export VELO_API_KEY=<your-api-key>
104
+ export VELO_API_BASE=http://<velo-api-host>:8000 # default: http://localhost:8000
105
+
106
+ # 2. Run your tests remotely — same command you always use
107
+ VELO_REMOTE=1 robot ./tests
108
+
109
+ # 3. With tag filtering — all standard RF flags pass through unchanged
110
+ VELO_REMOTE=1 robot --include smoke --exclude wip ./tests
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Execution modes
116
+
117
+ | Variable | Value | Behaviour |
118
+ |---|---|---|
119
+ | `VELO_REMOTE` | `0` (default) | Package is inert — `robot` runs locally as normal |
120
+ | `VELO_REMOTE` | `1` | Suite is packaged, uploaded, and executed on the Velo platform |
121
+ | `VELO_DEBUG` | `1` | Local debug: native SAP GUI for Windows, or Docker Java elsewhere |
122
+ | `VELO_SAP_CLIENT` | `auto` | SAP backend: `auto`, `java`, or `windows` (see debug mode below) |
123
+
124
+ ---
125
+
126
+ ## Environment variables
127
+
128
+ | Variable | Required | Default | Description |
129
+ |---|---|---|---|
130
+ | `VELO_API_KEY` | Yes (remote) | — | Workspace API key |
131
+ | `VELO_API_BASE` | No | `http://localhost:8000` | Velo API base URL |
132
+ | `VELO_REMOTE` | No | `0` | Set to `1` to enable remote execution |
133
+ | `VELO_DEBUG` | No | `0` | Set to `1` for local debug (Windows native or Docker Java) |
134
+ | `VELO_SAP_CLIENT` | No | `auto` | SAP client: `auto`, `java`, or `windows` |
135
+
136
+ ---
137
+
138
+ ## Remote execution flow
139
+
140
+ When `VELO_REMOTE=1`, the following happens automatically:
141
+
142
+ ```
143
+ 1. Scan working directory and apply .veloignore rules
144
+ 2. Package directory into a .zip archive
145
+ 3. Upload archive to the Velo API
146
+ 4. Trigger a remote run (optionally with --include / --exclude tags)
147
+ 5. Stream Robot Framework log output to your terminal in real time
148
+ 6. On completion: download log.html + report.html to ./results/
149
+ 7. Exit with the standard RF exit code
150
+ ```
151
+
152
+ The terminal experience is identical to a local RF run — log lines appear as tests execute, not buffered.
153
+
154
+ ---
155
+
156
+ ## What gets uploaded
157
+
158
+ The entire current working directory is packaged into a `.zip` archive when you run `robot`. The archive is created from the directory you run the command in, preserving the full folder structure.
159
+
160
+ **Typical archive contents:**
161
+
162
+ ```
163
+ tests/
164
+ smoke/
165
+ login.robot
166
+ regression/
167
+ sales_order.robot
168
+ resources/
169
+ keywords.robot
170
+ variables/
171
+ common.py
172
+ ```
173
+
174
+ Only files are included — empty directories are omitted. The archive is uploaded to the API, extracted into the execution container, and `robot` is run against the entire directory.
175
+
176
+ ---
177
+
178
+ ## .veloignore
179
+
180
+ A `.veloignore` file in the project root controls which files are excluded from the uploaded archive. It uses the same syntax as `.gitignore` (glob patterns, `#` comments, negation with `!`).
181
+
182
+ Place it at the root of your test project:
183
+
184
+ ```
185
+ your-project/
186
+ ├── .veloignore ← here
187
+ ├── tests/
188
+ ├── resources/
189
+ └── ...
190
+ ```
191
+
192
+ ### Default exclusions
193
+
194
+ When **no `.veloignore` file is present**, the following are excluded automatically:
195
+
196
+ ```
197
+ .git
198
+ __pycache__
199
+ *.pyc
200
+ *.pyo
201
+ venv
202
+ .venv
203
+ node_modules
204
+ .env
205
+ .env.*
206
+ results
207
+ *.log
208
+ ```
209
+
210
+ ### Important: defaults are replaced, not merged
211
+
212
+ If a `.veloignore` file exists, **it completely replaces the default list** — the defaults above are no longer applied. Include any defaults you still want in your `.veloignore`.
213
+
214
+ ### Example .veloignore
215
+
216
+ ```gitignore
217
+ # Re-include sensible defaults
218
+ .git
219
+ __pycache__
220
+ *.pyc
221
+ venv
222
+ .venv
223
+ .env
224
+ .env.*
225
+ results
226
+ *.log
227
+
228
+ # Project-specific exclusions
229
+ data/sensitive/
230
+ config/secrets.yaml
231
+ *.csv
232
+ docs/
233
+ ```
234
+
235
+ ### Verifying exclusions
236
+
237
+ After a run, inspect the uploaded archive on the API server:
238
+
239
+ ```bash
240
+ unzip -l <storage_root>/suites/<suite_id>.zip
241
+ ```
242
+
243
+ ---
244
+
245
+ ## Artifacts
246
+
247
+ When the run completes, the following are downloaded to your local `--outputdir` (default: `./results/`):
248
+
249
+ | File | Description |
250
+ |---|---|
251
+ | `log.html` | Full Robot Framework execution log with keyword-level detail |
252
+ | `report.html` | Test suite summary report |
253
+
254
+ The video recording (`recording.mp4`) is stored on the server and accessible via the API at `GET /api/runs/{run_id}/artifacts/video`.
255
+
256
+ ---
257
+
258
+ ## Exit codes
259
+
260
+ The package preserves standard RF exit codes so existing CI scripts and Makefiles work without modification:
261
+
262
+ | Code | Meaning |
263
+ |---|---|
264
+ | `0` | All tests passed |
265
+ | `1` | One or more tests failed |
266
+ | `2` | Invalid RF options or arguments |
267
+ | `3` | Test execution stopped by user |
268
+ | `252` | Help or version info printed |
269
+ | `253` | Platform error (upload failed, API unreachable, run did not start) |
270
+
271
+ ---
272
+
273
+ ## RF flag pass-through
274
+
275
+ All standard `robot` flags are forwarded to the remote execution environment:
276
+
277
+ | Flag | Behaviour |
278
+ |---|---|
279
+ | `--include TAG` / `--exclude TAG` | Passed to remote RF runner |
280
+ | `--variable KEY:VALUE` | Passed to remote RF runner |
281
+ | `--suite SUITE` | Passed to remote RF runner |
282
+ | `--outputdir PATH` | Controls local download destination for results |
283
+ | `--dryrun` | Executes locally — remote is not triggered |
284
+
285
+ ---
286
+
287
+ ## Development install (editable)
288
+
289
+ ```bash
290
+ git clone <repo>
291
+ cd velo
292
+ pip install -e packages/robotframework-velo-cli
293
+ ```
@@ -0,0 +1,15 @@
1
+ robotframework_velo_cli-0.1.7.dist-info/licenses/LICENSE,sha256=Qx-di54idhaOnrKKU-L9H2p-8JwkT_G157WyhxR-SZg,1906
2
+ velo/__init__.py,sha256=FOkEvPiy2cSVFxVUDAnajrFLEOWbkwHRcqn9ElxoGBk,177
3
+ velo/cli.py,sha256=4XkgsHx5zP7VZBUGK42ZkgZsqwVjFwOXLfwmdFYNgls,720
4
+ velo/constants.py,sha256=u9f5PRzF85ZG3dBTSdBj0sKHh9yrThOWqYxCo4S19T4,161
5
+ velo/debug.py,sha256=vofojwNkS4qFGqp5rBAvEyrIxtYV74zhAO0EwdfA4xs,4176
6
+ velo/ignore.py,sha256=FqUAkEMth2uxuvscUZhBn67MgFwysNSntXQrpLKU3BA,1042
7
+ velo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ velo/runner.py,sha256=qe8f8pslPDHEwBz0v501X5HNAAZmgsXTLoqJWNgpC_A,4053
9
+ velo/sap_client.py,sha256=aEOh43sK_aXvoCAHz-ygBn4zRpl9USGbS_Ex-qu4OFc,592
10
+ velo/streamer.py,sha256=3sQG3mQswS9xSSWHKo2PgsBtOWcAZwNm7hPz19s5xIo,2083
11
+ robotframework_velo_cli-0.1.7.dist-info/METADATA,sha256=DsFbPgdwP7yIWIG0sKl0_W0en-XxzCV4dwYibOi4-hg,9298
12
+ robotframework_velo_cli-0.1.7.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
13
+ robotframework_velo_cli-0.1.7.dist-info/entry_points.txt,sha256=RhLfsXbSOVkzZJ82pF5BNCnUc7AaGjnI-Nt8TfF2v84,66
14
+ robotframework_velo_cli-0.1.7.dist-info/top_level.txt,sha256=DvWsWbYteXtPv_CrlE7HAff1eE_Ayg1c3pLECTGAcQs,5
15
+ robotframework_velo_cli-0.1.7.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ robot = velo.cli:main
3
+ velo = velo.cli:velo_main
@@ -0,0 +1,49 @@
1
+ Copyright (c) 2026 Velo. All rights reserved.
2
+
3
+ PROPRIETARY SOFTWARE LICENSE
4
+
5
+ This software and its source code, documentation, and associated files
6
+ (collectively, the "Software") are the exclusive property of Velo and are
7
+ protected by copyright law and international treaties.
8
+
9
+ GRANT OF LICENSE
10
+
11
+ Velo grants you a limited, non-exclusive, non-transferable, non-sub licensable
12
+ license to use the Software solely for your internal business purposes,
13
+ strictly in accordance with any agreement entered into with Velo.
14
+
15
+ RESTRICTIONS
16
+
17
+ You may not, and you may not permit any third party to:
18
+
19
+ 1. Copy, modify, adapt, translate, or create derivative works of the Software;
20
+ 2. Reverse engineer, disassemble, decompile, or otherwise attempt to derive
21
+ the source code of the Software;
22
+ 3. Sell, sublicense, rent, lease, transfer, or otherwise make the Software
23
+ available to any third party;
24
+ 4. Remove or alter any proprietary notices, labels, or marks on the Software;
25
+ 5. Use the Software for any purpose other than as expressly permitted
26
+ under this license.
27
+
28
+ NO WARRANTY
29
+
30
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
31
+ IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS
32
+ FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. IN NO EVENT SHALL VELO BE
33
+ LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE USE OF
34
+ THE SOFTWARE.
35
+
36
+ TERMINATION
37
+
38
+ This license is effective until terminated. It will terminate automatically
39
+ if you fail to comply with any of its terms. Upon termination, you must
40
+ immediately cease all use of the Software and destroy any copies in your
41
+ possession.
42
+
43
+ GOVERNING LAW
44
+
45
+ This license shall be governed by and construed in accordance with applicable
46
+ law. Any disputes arising under this license shall be subject to the exclusive
47
+ jurisdiction of the competent courts.
48
+
49
+ For licensing inquiries, contact: legal@velo.com
velo/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("robotframework-velo-cli")
5
+ except PackageNotFoundError:
6
+ __version__ = "unknown"
velo/cli.py ADDED
@@ -0,0 +1,29 @@
1
+ import os
2
+ import sys
3
+
4
+
5
+ def main() -> None:
6
+ """Shadows the system `robot` command. Gate 2 implementation pending."""
7
+ args = sys.argv[1:]
8
+
9
+ if os.getenv("VELO_REMOTE") == "1":
10
+ from velo.runner import run_remote
11
+
12
+ sys.exit(run_remote(args))
13
+
14
+ elif os.getenv("VELO_DEBUG") == "1":
15
+ from velo.debug import run_debug
16
+
17
+ sys.exit(run_debug(args))
18
+
19
+ else:
20
+ import subprocess
21
+
22
+ result = subprocess.run([sys.executable, "-m", "robot"] + args)
23
+ sys.exit(result.returncode)
24
+
25
+
26
+ def velo_main() -> None:
27
+ """Entry point for `velo` CLI subcommands (e.g. velo agent start). Gate 2+."""
28
+ print("velo agent commands are not available in this build.")
29
+ sys.exit(1)
velo/constants.py ADDED
@@ -0,0 +1,6 @@
1
+ RF_EXIT_ALL_PASS = 0
2
+ RF_EXIT_TEST_FAILURE = 1
3
+ RF_EXIT_INVALID_OPTIONS = 2
4
+ RF_EXIT_STOPPED_BY_USER = 3
5
+ RF_EXIT_HELP_OR_VERSION = 252
6
+ RF_EXIT_PLATFORM_ERROR = 253
velo/debug.py ADDED
@@ -0,0 +1,143 @@
1
+ """VELO_DEBUG=1 — local debug router (native Windows or Docker Java)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from velo.constants import RF_EXIT_PLATFORM_ERROR
12
+ from velo.sap_client import resolve_sap_client
13
+
14
+ DOCKER_DESKTOP_URL = "https://www.docker.com/products/docker-desktop"
15
+ EXECUTION_IMAGE = os.getenv("VELO_EXECUTION_IMAGE", "velo-execution")
16
+
17
+
18
+ def run_debug(rf_args: list[str]) -> int:
19
+ client = resolve_sap_client()
20
+ if client == "windows":
21
+ return _run_native_windows(rf_args)
22
+ return _run_docker_java(rf_args)
23
+
24
+
25
+ def _run_native_windows(rf_args: list[str]) -> int:
26
+ if sys.platform != "win32":
27
+ print(
28
+ "[velo] VELO_DEBUG with SAP GUI for Windows requires a Windows host.",
29
+ file=sys.stderr,
30
+ )
31
+ return RF_EXIT_PLATFORM_ERROR
32
+
33
+ err = _check_windows_scripting()
34
+ if err:
35
+ print(f"[velo] {err}", file=sys.stderr)
36
+ return RF_EXIT_PLATFORM_ERROR
37
+
38
+ env = {**os.environ, "VELO_SAP_CLIENT": "windows"}
39
+ return subprocess.run([sys.executable, "-m", "robot", *rf_args], env=env).returncode
40
+
41
+
42
+ def _check_windows_scripting() -> str | None:
43
+ try:
44
+ import win32com.client # type: ignore[import-untyped]
45
+ except ImportError:
46
+ return (
47
+ "pywin32 is required for Windows SAP GUI debug runs. "
48
+ "Install with: pip install pywin32"
49
+ )
50
+
51
+ try:
52
+ sap_gui = win32com.client.GetObject("SAPGUI")
53
+ engine = sap_gui.GetScriptingEngine
54
+ except Exception:
55
+ return (
56
+ "SAP GUI for Windows is not available. Install SAP GUI, start a session, "
57
+ "and enable scripting in SAP GUI options."
58
+ )
59
+
60
+ if engine is None:
61
+ return (
62
+ "SAP GUI Scripting is disabled. Enable it in SAP GUI settings and verify "
63
+ "server parameter sapgui/user_scripting."
64
+ )
65
+ return None
66
+
67
+
68
+ def _run_docker_java(rf_args: list[str]) -> int:
69
+ if shutil.which("docker") is None:
70
+ print(
71
+ "[velo] Docker is required for Java SAP GUI debug runs on this platform.",
72
+ file=sys.stderr,
73
+ )
74
+ print(f"[velo] Install Docker Desktop: {DOCKER_DESKTOP_URL}", file=sys.stderr)
75
+ return RF_EXIT_PLATFORM_ERROR
76
+
77
+ cwd = Path.cwd().resolve()
78
+ results_dir = _parse_outputdir(rf_args)
79
+ results_dir.mkdir(parents=True, exist_ok=True)
80
+
81
+ docker_cmd = [
82
+ "docker",
83
+ "run",
84
+ "--rm",
85
+ "-v",
86
+ f"{cwd}:/suite:ro",
87
+ "-v",
88
+ f"{results_dir}:/results",
89
+ "-e",
90
+ "VELO_SAP_CLIENT=java",
91
+ "-e",
92
+ "SUITE_DIR=/suite",
93
+ "-e",
94
+ "RESULTS_DIR=/results",
95
+ EXECUTION_IMAGE,
96
+ ]
97
+
98
+ include, exclude = _parse_tags(rf_args)
99
+ if include:
100
+ docker_cmd.extend(["-e", f"ROBOT_INCLUDE={include}"])
101
+ if exclude:
102
+ docker_cmd.extend(["-e", f"ROBOT_EXCLUDE={exclude}"])
103
+
104
+ print(f"[velo] Starting Docker debug run ({EXECUTION_IMAGE})…", flush=True)
105
+ return subprocess.run(docker_cmd).returncode
106
+
107
+
108
+ def _parse_outputdir(rf_args: list[str]) -> Path:
109
+ outputdir = "./results"
110
+ args = list(rf_args)
111
+ idx = 0
112
+ while idx < len(args):
113
+ arg = args[idx]
114
+ if arg in ("--outputdir", "-d") and idx + 1 < len(args):
115
+ outputdir = args[idx + 1]
116
+ idx += 2
117
+ continue
118
+ if arg.startswith("--outputdir="):
119
+ outputdir = arg.split("=", 1)[1]
120
+ idx += 1
121
+ path = Path(outputdir)
122
+ if not path.is_absolute():
123
+ path = Path.cwd() / path
124
+ return path.resolve()
125
+
126
+
127
+ def _parse_tags(rf_args: list[str]) -> tuple[str | None, str | None]:
128
+ include: str | None = None
129
+ exclude: str | None = None
130
+ args = list(rf_args)
131
+ idx = 0
132
+ while idx < len(args):
133
+ arg = args[idx]
134
+ if arg in ("--include", "-i") and idx + 1 < len(args):
135
+ include = args[idx + 1]
136
+ idx += 2
137
+ continue
138
+ if arg in ("--exclude", "-e") and idx + 1 < len(args):
139
+ exclude = args[idx + 1]
140
+ idx += 2
141
+ continue
142
+ idx += 1
143
+ return include, exclude
velo/ignore.py ADDED
@@ -0,0 +1,41 @@
1
+ import zipfile
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ import pathspec
6
+
7
+ DEFAULT_IGNORE = [
8
+ ".git",
9
+ "__pycache__",
10
+ "*.pyc",
11
+ "*.pyo",
12
+ "venv",
13
+ ".venv",
14
+ "node_modules",
15
+ ".env",
16
+ ".env.*",
17
+ "results",
18
+ "*.log",
19
+ ]
20
+
21
+
22
+ def load_ignore_spec(root: Path) -> pathspec.PathSpec[Any]:
23
+ veloignore = root / ".veloignore"
24
+ if veloignore.exists():
25
+ patterns = veloignore.read_text().splitlines()
26
+ else:
27
+ patterns = DEFAULT_IGNORE.copy()
28
+ return pathspec.PathSpec.from_lines("gitwildmatch", patterns)
29
+
30
+
31
+ def build_archive(root: Path, output: Path) -> None:
32
+ """Package root into a zip archive at output, respecting .veloignore rules."""
33
+ spec = load_ignore_spec(root)
34
+ with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as zf:
35
+ for path in sorted(root.rglob("*")):
36
+ if not path.is_file():
37
+ continue
38
+ rel = path.relative_to(root)
39
+ if spec.match_file(str(rel)):
40
+ continue
41
+ zf.write(path, rel)
velo/py.typed ADDED
File without changes
velo/runner.py ADDED
@@ -0,0 +1,124 @@
1
+ import os
2
+ import sys
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ import requests
7
+
8
+ from velo.constants import RF_EXIT_PLATFORM_ERROR
9
+ from velo.ignore import build_archive
10
+ from velo.streamer import stream_logs_to_terminal
11
+
12
+ DEFAULT_API_BASE = "https://velo.aster.pro"
13
+
14
+
15
+ def run_remote(rf_args: list[str]) -> int:
16
+ api_base = os.getenv("VELO_API_BASE", DEFAULT_API_BASE)
17
+ api_key = os.getenv("VELO_API_KEY", "")
18
+ headers = {"Authorization": f"Bearer {api_key}"}
19
+
20
+ root = Path.cwd()
21
+
22
+ # 1. Build archive respecting .veloignore
23
+ with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
24
+ archive_path = Path(tmp.name)
25
+ build_archive(root, archive_path)
26
+ size_kb = archive_path.stat().st_size // 1024
27
+ print(f"[velo] uploading suite ({size_kb} KB)...", flush=True)
28
+
29
+ # 2. Upload suite
30
+ try:
31
+ with open(archive_path, "rb") as f:
32
+ r = requests.post(
33
+ f"{api_base}/cli/api/suites",
34
+ files={"archive": (root.name + ".zip", f, "application/zip")},
35
+ headers=headers,
36
+ )
37
+ r.raise_for_status()
38
+ except Exception as exc:
39
+ print(f"[velo] suite upload failed: {exc}", file=sys.stderr, flush=True)
40
+ return RF_EXIT_PLATFORM_ERROR
41
+ finally:
42
+ archive_path.unlink(missing_ok=True)
43
+
44
+ suite_id = r.json()["suite_id"]
45
+
46
+ # 3. Parse RF flags for include/exclude tags and output dir
47
+ include = _extract_flag(rf_args, "--include")
48
+ exclude = _extract_flag(rf_args, "--exclude")
49
+ output_dir = _extract_flag(rf_args, "--outputdir") or "results"
50
+
51
+ # 4. Trigger run
52
+ try:
53
+ r = requests.post(
54
+ f"{api_base}/cli/api/runs",
55
+ json={"suite_id": suite_id, "include": include, "exclude": exclude},
56
+ headers=headers,
57
+ )
58
+ r.raise_for_status()
59
+ except Exception as exc:
60
+ print(f"[velo] run trigger failed: {exc}", file=sys.stderr, flush=True)
61
+ return RF_EXIT_PLATFORM_ERROR
62
+
63
+ run_id = r.json()["run_id"]
64
+ print(f"[velo] run started: {run_id}", flush=True)
65
+
66
+ # 5. Stream logs to terminal
67
+ stream_logs_to_terminal(run_id, api_base, api_key)
68
+
69
+ # 6. Poll for final status + exit code
70
+ try:
71
+ r = requests.get(f"{api_base}/cli/api/runs/{run_id}", headers=headers)
72
+ r.raise_for_status()
73
+ run = r.json()
74
+ except Exception as exc:
75
+ print(f"[velo] failed to fetch run result: {exc}", file=sys.stderr, flush=True)
76
+ return RF_EXIT_PLATFORM_ERROR
77
+
78
+ # 7. Download artifacts to local output directory
79
+ _download_artifacts(run_id, output_dir, api_base, api_key)
80
+
81
+ exit_code = run.get("exit_code")
82
+ if exit_code is None:
83
+ return RF_EXIT_PLATFORM_ERROR
84
+ return int(exit_code)
85
+
86
+
87
+ def _extract_flag(args: list[str], flag: str) -> str | None:
88
+ """Return the value following `flag` in args, or None if not present."""
89
+ for i, arg in enumerate(args):
90
+ if arg == flag and i + 1 < len(args):
91
+ return args[i + 1]
92
+ if arg.startswith(f"{flag}="):
93
+ return arg.split("=", 1)[1]
94
+ return None
95
+
96
+
97
+ def _download_artifacts(
98
+ run_id: str,
99
+ output_dir: str,
100
+ api_base: str,
101
+ api_key: str,
102
+ ) -> None:
103
+ """Download log.html and report.html into output_dir."""
104
+ dest = Path(output_dir)
105
+ dest.mkdir(parents=True, exist_ok=True)
106
+ headers = {"Authorization": f"Bearer {api_key}"}
107
+
108
+ artifacts = {
109
+ "log.html": f"{api_base}/cli/api/runs/{run_id}/artifacts/log",
110
+ "report.html": f"{api_base}/cli/api/runs/{run_id}/artifacts/report",
111
+ }
112
+
113
+ for filename, url in artifacts.items():
114
+ try:
115
+ r = requests.get(url, headers=headers, timeout=60)
116
+ r.raise_for_status()
117
+ (dest / filename).write_bytes(r.content)
118
+ print(f"[velo] downloaded {filename} → {dest / filename}", flush=True)
119
+ except Exception as exc:
120
+ print(
121
+ f"[velo] warning: could not download {filename}: {exc}",
122
+ file=sys.stderr,
123
+ flush=True,
124
+ )
velo/sap_client.py ADDED
@@ -0,0 +1,18 @@
1
+ """SAP client type resolution for VELO_DEBUG and local runs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+
8
+
9
+ def resolve_sap_client(client: str | None = None) -> str:
10
+ """Return ``java`` or ``windows`` for the active SAP GUI backend."""
11
+ raw = (client or os.getenv("VELO_SAP_CLIENT") or "auto").strip().lower()
12
+ if raw not in ("auto", "java", "windows"):
13
+ raise ValueError(
14
+ f"Invalid VELO_SAP_CLIENT '{raw}'. Use auto, java, or windows."
15
+ )
16
+ if raw == "auto":
17
+ return "windows" if sys.platform == "win32" else "java"
18
+ return raw
velo/streamer.py ADDED
@@ -0,0 +1,63 @@
1
+ import sys
2
+ import time
3
+
4
+ import requests
5
+
6
+
7
+ def stream_logs_to_terminal(run_id: str, api_base: str, api_key: str) -> None:
8
+ """
9
+ Connect to the SSE log stream for a run and print lines to stdout as they arrive.
10
+ Reconnects up to 5 times with exponential backoff on connection errors,
11
+ but stops immediately if the run has already reached a terminal state.
12
+ """
13
+ headers = {
14
+ "Authorization": f"Bearer {api_key}",
15
+ "Accept": "text/event-stream",
16
+ }
17
+ retries = 0
18
+ max_retries = 5
19
+
20
+ while retries <= max_retries:
21
+ try:
22
+ with requests.get(
23
+ f"{api_base}/cli/api/runs/{run_id}/stream",
24
+ headers=headers,
25
+ stream=True,
26
+ timeout=300,
27
+ ) as resp:
28
+ resp.raise_for_status()
29
+ retries = 0
30
+ for raw in resp.iter_lines():
31
+ if raw and raw.startswith(b"data: "):
32
+ print(raw[6:].decode(), flush=True)
33
+ return # stream closed cleanly — run ended
34
+ except Exception:
35
+ retries += 1
36
+ if retries > max_retries:
37
+ print("[velo] lost contact with platform", file=sys.stderr, flush=True)
38
+ return
39
+ if _run_is_terminal(run_id, api_base, api_key):
40
+ return
41
+ wait = 2**retries
42
+ print(
43
+ f"[velo] reconnecting to log stream... ({retries}/{max_retries})",
44
+ file=sys.stderr,
45
+ flush=True,
46
+ )
47
+ time.sleep(wait)
48
+
49
+
50
+ def _run_is_terminal(run_id: str, api_base: str, api_key: str) -> bool:
51
+ """Return True if the run has already completed or failed."""
52
+ try:
53
+ r = requests.get(
54
+ f"{api_base}/cli/api/runs/{run_id}",
55
+ headers={"Authorization": f"Bearer {api_key}"},
56
+ timeout=10,
57
+ )
58
+ if r.status_code == 200:
59
+ status = r.json().get("status", "")
60
+ return status in ("completed", "failed")
61
+ except Exception:
62
+ pass
63
+ return False