wbxsandbox 0.15.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.
- wbxsandbox-0.15.2/LICENSE +21 -0
- wbxsandbox-0.15.2/PKG-INFO +85 -0
- wbxsandbox-0.15.2/README.md +65 -0
- wbxsandbox-0.15.2/pyproject.toml +32 -0
- wbxsandbox-0.15.2/setup.cfg +4 -0
- wbxsandbox-0.15.2/tests/test_sdk.py +128 -0
- wbxsandbox-0.15.2/tests/test_terminal.py +107 -0
- wbxsandbox-0.15.2/wbxsandbox/__init__.py +732 -0
- wbxsandbox-0.15.2/wbxsandbox/__main__.py +3 -0
- wbxsandbox-0.15.2/wbxsandbox/cli.py +35 -0
- wbxsandbox-0.15.2/wbxsandbox/terminal.py +311 -0
- wbxsandbox-0.15.2/wbxsandbox.egg-info/PKG-INFO +85 -0
- wbxsandbox-0.15.2/wbxsandbox.egg-info/SOURCES.txt +14 -0
- wbxsandbox-0.15.2/wbxsandbox.egg-info/dependency_links.txt +1 -0
- wbxsandbox-0.15.2/wbxsandbox.egg-info/entry_points.txt +2 -0
- wbxsandbox-0.15.2/wbxsandbox.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KXCOMM s.r.o.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wbxsandbox
|
|
3
|
+
Version: 0.15.2
|
|
4
|
+
Summary: Python SDK for WBX Sandbox – isolated cloud sandboxes (Firecracker microVMs) for AI agents
|
|
5
|
+
Author: KXCOMM s.r.o.
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://sandbox.wbx.cz
|
|
8
|
+
Project-URL: Documentation, https://sandbox.wbx.cz/docs
|
|
9
|
+
Project-URL: Pricing, https://sandbox.wbx.cz/pricing
|
|
10
|
+
Keywords: sandbox,firecracker,microvm,ai agents,code execution,wbx
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# WBX Sandbox – Python SDK
|
|
22
|
+
|
|
23
|
+
Python SDK for [WBX Sandbox](https://sandbox.wbx.cz): isolated cloud sandboxes for AI agents
|
|
24
|
+
and code execution. Each sandbox is a Firecracker microVM with its own Linux kernel, disk and
|
|
25
|
+
network rules. No dependencies – only the Python standard library (Python 3.9+).
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
pip install wbxsandbox
|
|
30
|
+
|
|
31
|
+
## Configure
|
|
32
|
+
|
|
33
|
+
Create an API key in the portal (API keys) and set it in the environment:
|
|
34
|
+
|
|
35
|
+
export WBXSANDBOX_API_KEY=...
|
|
36
|
+
# optional, defaults to https://sandbox.wbx.cz
|
|
37
|
+
export WBXSANDBOX_URL=https://sandbox.wbx.cz
|
|
38
|
+
|
|
39
|
+
Both can also be passed as `url=` and `api_key=` arguments.
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from wbxsandbox import Sandbox
|
|
45
|
+
|
|
46
|
+
with Sandbox.create(template="base", timeout=300) as sb:
|
|
47
|
+
r = sb.run("python3 -c 'print(1 + 1)'")
|
|
48
|
+
print(r.exit_code, r.stdout) # 0 "2\n"
|
|
49
|
+
sb.files.write("/home/user/data.txt", "hello")
|
|
50
|
+
print(sb.files.read_text("/home/user/data.txt"))
|
|
51
|
+
# the sandbox is destroyed when the block ends
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Background jobs and batches:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from wbxsandbox import run_job
|
|
58
|
+
|
|
59
|
+
job = run_job(command="python3 -c 'print(42)'", template="base", timeout_seconds=600)
|
|
60
|
+
job.wait()
|
|
61
|
+
print(job.summary()["status"])
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Interactive terminal in a running sandbox (installed as the `wbx` command):
|
|
65
|
+
|
|
66
|
+
wbx connect sb_...
|
|
67
|
+
|
|
68
|
+
Documentation: https://sandbox.wbx.cz/docs · Pricing: https://sandbox.wbx.cz/pricing
|
|
69
|
+
|
|
70
|
+
## Versioning
|
|
71
|
+
|
|
72
|
+
The SDK has its own version. Its `major.minor` follows the platform release line
|
|
73
|
+
(SDK 0.15.x works with platform 0.15.x); the patch number is raised only when the SDK
|
|
74
|
+
itself changes. `pyproject.toml` and `wbxsandbox.__version__` always carry the same
|
|
75
|
+
version (checked by `tests/test_sdk.py`).
|
|
76
|
+
|
|
77
|
+
## Tests
|
|
78
|
+
|
|
79
|
+
cd sdk/python && python3 -m unittest discover -s tests
|
|
80
|
+
|
|
81
|
+
The tests do not need network access.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT – see `LICENSE`.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# WBX Sandbox – Python SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for [WBX Sandbox](https://sandbox.wbx.cz): isolated cloud sandboxes for AI agents
|
|
4
|
+
and code execution. Each sandbox is a Firecracker microVM with its own Linux kernel, disk and
|
|
5
|
+
network rules. No dependencies – only the Python standard library (Python 3.9+).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
pip install wbxsandbox
|
|
10
|
+
|
|
11
|
+
## Configure
|
|
12
|
+
|
|
13
|
+
Create an API key in the portal (API keys) and set it in the environment:
|
|
14
|
+
|
|
15
|
+
export WBXSANDBOX_API_KEY=...
|
|
16
|
+
# optional, defaults to https://sandbox.wbx.cz
|
|
17
|
+
export WBXSANDBOX_URL=https://sandbox.wbx.cz
|
|
18
|
+
|
|
19
|
+
Both can also be passed as `url=` and `api_key=` arguments.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from wbxsandbox import Sandbox
|
|
25
|
+
|
|
26
|
+
with Sandbox.create(template="base", timeout=300) as sb:
|
|
27
|
+
r = sb.run("python3 -c 'print(1 + 1)'")
|
|
28
|
+
print(r.exit_code, r.stdout) # 0 "2\n"
|
|
29
|
+
sb.files.write("/home/user/data.txt", "hello")
|
|
30
|
+
print(sb.files.read_text("/home/user/data.txt"))
|
|
31
|
+
# the sandbox is destroyed when the block ends
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Background jobs and batches:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from wbxsandbox import run_job
|
|
38
|
+
|
|
39
|
+
job = run_job(command="python3 -c 'print(42)'", template="base", timeout_seconds=600)
|
|
40
|
+
job.wait()
|
|
41
|
+
print(job.summary()["status"])
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Interactive terminal in a running sandbox (installed as the `wbx` command):
|
|
45
|
+
|
|
46
|
+
wbx connect sb_...
|
|
47
|
+
|
|
48
|
+
Documentation: https://sandbox.wbx.cz/docs · Pricing: https://sandbox.wbx.cz/pricing
|
|
49
|
+
|
|
50
|
+
## Versioning
|
|
51
|
+
|
|
52
|
+
The SDK has its own version. Its `major.minor` follows the platform release line
|
|
53
|
+
(SDK 0.15.x works with platform 0.15.x); the patch number is raised only when the SDK
|
|
54
|
+
itself changes. `pyproject.toml` and `wbxsandbox.__version__` always carry the same
|
|
55
|
+
version (checked by `tests/test_sdk.py`).
|
|
56
|
+
|
|
57
|
+
## Tests
|
|
58
|
+
|
|
59
|
+
cd sdk/python && python3 -m unittest discover -s tests
|
|
60
|
+
|
|
61
|
+
The tests do not need network access.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT – see `LICENSE`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wbxsandbox"
|
|
7
|
+
version = "0.15.2"
|
|
8
|
+
description = "Python SDK for WBX Sandbox – isolated cloud sandboxes (Firecracker microVMs) for AI agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = []
|
|
12
|
+
license = "MIT"
|
|
13
|
+
authors = [{ name = "KXCOMM s.r.o." }]
|
|
14
|
+
keywords = ["sandbox", "firecracker", "microvm", "ai agents", "code execution", "wbx"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
"Topic :: System :: Distributed Computing",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://sandbox.wbx.cz"
|
|
25
|
+
Documentation = "https://sandbox.wbx.cz/docs"
|
|
26
|
+
Pricing = "https://sandbox.wbx.cz/pricing"
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
wbx = "wbxsandbox.cli:main"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools]
|
|
32
|
+
packages = ["wbxsandbox"]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Testy Python SDK bez sítě: verze, podpis webhooku, rozpočet (měna účtu, starší názvy parametrů).
|
|
2
|
+
|
|
3
|
+
Spuštění: cd sdk/python && python3 -m unittest discover -s tests
|
|
4
|
+
"""
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
import unittest
|
|
12
|
+
from unittest import mock
|
|
13
|
+
|
|
14
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
15
|
+
SDK = os.path.dirname(HERE)
|
|
16
|
+
sys.path.insert(0, SDK)
|
|
17
|
+
|
|
18
|
+
import wbxsandbox # noqa: E402
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _read(*parts):
|
|
22
|
+
with open(os.path.join(*parts), encoding="utf-8") as f:
|
|
23
|
+
return f.read()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class VersionTest(unittest.TestCase):
|
|
27
|
+
def test_pyproject_matches_package(self):
|
|
28
|
+
m = re.search(r'^version\s*=\s*"([^"]+)"', _read(SDK, "pyproject.toml"), re.M)
|
|
29
|
+
self.assertIsNotNone(m)
|
|
30
|
+
self.assertEqual(m.group(1), wbxsandbox.__version__)
|
|
31
|
+
|
|
32
|
+
def test_same_major_minor_as_platform(self):
|
|
33
|
+
# SDK má vlastní patch verzi, ale řada major.minor odpovídá platformě. Verze platformy:
|
|
34
|
+
# RELEASE ve vydaném balíku (src/sdk/python i sdk/python), v repozitáři Makefile.
|
|
35
|
+
# Bez metadat platformy test selže – nesmí se tiše přeskočit.
|
|
36
|
+
version = platform_version()
|
|
37
|
+
self.assertIsNotNone(version, "platform version not found (RELEASE in the bundle or Makefile in the repository)")
|
|
38
|
+
self.assertEqual(version.split(".")[:2], wbxsandbox.__version__.split(".")[:2])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def platform_version():
|
|
42
|
+
"""Verze platformy z RELEASE (balík) nebo z Makefile (repozitář); None = nenalezeno."""
|
|
43
|
+
for rel in (os.path.join(SDK, "..", "..", "RELEASE"), os.path.join(SDK, "..", "..", "..", "RELEASE")):
|
|
44
|
+
if os.path.exists(rel):
|
|
45
|
+
m = re.search(r"^version:\s*(\S+)", _read(rel), re.M)
|
|
46
|
+
if m:
|
|
47
|
+
return m.group(1)
|
|
48
|
+
mk = os.path.join(SDK, "..", "..", "Makefile")
|
|
49
|
+
if os.path.exists(mk):
|
|
50
|
+
m = re.search(r"^VERSION\s*:=\s*(\S+)", _read(mk), re.M)
|
|
51
|
+
if m:
|
|
52
|
+
return m.group(1)
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PlatformVersionLookupTest(unittest.TestCase):
|
|
57
|
+
def test_reads_release_then_makefile_and_reports_missing(self):
|
|
58
|
+
import tempfile
|
|
59
|
+
global SDK
|
|
60
|
+
orig = SDK
|
|
61
|
+
try:
|
|
62
|
+
with tempfile.TemporaryDirectory() as d:
|
|
63
|
+
SDK = os.path.join(d, "src", "sdk", "python")
|
|
64
|
+
os.makedirs(SDK)
|
|
65
|
+
self.assertIsNone(platform_version()) # nic → None (test výše pak selže)
|
|
66
|
+
with open(os.path.join(d, "RELEASE"), "w") as f:
|
|
67
|
+
f.write("version: 9.8.7\ncommit: x\n")
|
|
68
|
+
self.assertEqual(platform_version(), "9.8.7") # src/sdk/python → ../../../RELEASE
|
|
69
|
+
with tempfile.TemporaryDirectory() as d:
|
|
70
|
+
SDK = os.path.join(d, "sdk", "python")
|
|
71
|
+
os.makedirs(SDK)
|
|
72
|
+
with open(os.path.join(d, "Makefile"), "w") as f:
|
|
73
|
+
f.write("VERSION := 1.2.3\n")
|
|
74
|
+
self.assertEqual(platform_version(), "1.2.3")
|
|
75
|
+
with open(os.path.join(d, "RELEASE"), "w") as f:
|
|
76
|
+
f.write("version: 1.2.4\n")
|
|
77
|
+
self.assertEqual(platform_version(), "1.2.4") # RELEASE má přednost
|
|
78
|
+
finally:
|
|
79
|
+
SDK = orig
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class WebhookTest(unittest.TestCase):
|
|
83
|
+
def sign(self, secret, t, body):
|
|
84
|
+
return "t=%d,v1=%s" % (t, hmac.new(secret.encode(), b"%d." % t + body, hashlib.sha256).hexdigest())
|
|
85
|
+
|
|
86
|
+
def test_valid_tampered_and_old(self):
|
|
87
|
+
body = b'{"event":"job.finished","id":"job_1"}'
|
|
88
|
+
now = int(time.time())
|
|
89
|
+
sig = self.sign("whsec_x", now, body)
|
|
90
|
+
self.assertTrue(wbxsandbox.verify_webhook("whsec_x", body, sig))
|
|
91
|
+
self.assertFalse(wbxsandbox.verify_webhook("whsec_x", body + b" ", sig))
|
|
92
|
+
self.assertFalse(wbxsandbox.verify_webhook("whsec_y", body, sig))
|
|
93
|
+
self.assertFalse(wbxsandbox.verify_webhook("whsec_x", body, self.sign("whsec_x", now - 3600, body)))
|
|
94
|
+
self.assertFalse(wbxsandbox.verify_webhook("whsec_x", body, "garbage"))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class BudgetTest(unittest.TestCase):
|
|
98
|
+
def call(self, **kw):
|
|
99
|
+
with mock.patch.object(wbxsandbox.Client, "request", return_value={"currency": "CZK"}) as req:
|
|
100
|
+
wbxsandbox.set_budget(url="https://x", api_key="k", **kw)
|
|
101
|
+
return req.call_args
|
|
102
|
+
|
|
103
|
+
def test_limits_in_billing_currency(self):
|
|
104
|
+
args = self.call(monthly="500", daily=0, alert_pct=90)
|
|
105
|
+
self.assertEqual(args.args[:2], ("PUT", "/v1/budget"))
|
|
106
|
+
self.assertEqual(args.kwargs["body"], {"monthly_limit": "500", "daily_limit": "0", "alert_pct": 90})
|
|
107
|
+
|
|
108
|
+
def test_old_parameter_names_still_work(self):
|
|
109
|
+
args = self.call(monthly_eur="12.5", daily_eur="")
|
|
110
|
+
self.assertEqual(args.kwargs["body"], {"monthly_limit": "12.5", "daily_limit": ""})
|
|
111
|
+
# kladně: starší pořadí pozičních argumentů (měsíc, den) zůstává
|
|
112
|
+
with mock.patch.object(wbxsandbox.Client, "request", return_value={}) as req:
|
|
113
|
+
wbxsandbox.set_budget("7", "1", url="https://x", api_key="k")
|
|
114
|
+
self.assertEqual(req.call_args.kwargs["body"], {"monthly_limit": "7", "daily_limit": "1"})
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class PublicURLTest(unittest.TestCase):
|
|
118
|
+
def test_get_url(self):
|
|
119
|
+
c = wbxsandbox.Client(url="https://x", api_key="k")
|
|
120
|
+
sb = wbxsandbox.Sandbox(id="sb_n1_0123456789abcdef", template="base", state="running", _c=c)
|
|
121
|
+
with mock.patch.object(wbxsandbox.Client, "request", return_value={"url": "https://u/"}) as req:
|
|
122
|
+
self.assertEqual(sb.get_url("8080")["url"], "https://u/")
|
|
123
|
+
self.assertEqual(req.call_args.args, ("GET", "/v1/sandboxes/sb_n1_0123456789abcdef/url"))
|
|
124
|
+
self.assertEqual(req.call_args.kwargs["query"], {"port": 8080})
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
unittest.main()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Terminál (wbx connect): adresa WebSocketu a minimální klient WebSocketu proti lokálnímu serveru."""
|
|
2
|
+
import base64
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import socket
|
|
6
|
+
import struct
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
import unittest
|
|
10
|
+
|
|
11
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
12
|
+
sys.path.insert(0, os.path.dirname(HERE))
|
|
13
|
+
|
|
14
|
+
from wbxsandbox import terminal # noqa: E402
|
|
15
|
+
from wbxsandbox.cli import main # noqa: E402
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _server(handler):
|
|
19
|
+
srv = socket.socket()
|
|
20
|
+
srv.bind(("127.0.0.1", 0))
|
|
21
|
+
srv.listen(1)
|
|
22
|
+
|
|
23
|
+
def run():
|
|
24
|
+
c, _ = srv.accept()
|
|
25
|
+
req = b""
|
|
26
|
+
while b"\r\n\r\n" not in req:
|
|
27
|
+
req += c.recv(4096)
|
|
28
|
+
handler(c, req.decode())
|
|
29
|
+
c.close()
|
|
30
|
+
srv.close()
|
|
31
|
+
|
|
32
|
+
threading.Thread(target=run, daemon=True).start()
|
|
33
|
+
return srv.getsockname()[1]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _frame(opcode, payload):
|
|
37
|
+
return bytes([0x80 | opcode, len(payload)]) + payload
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _read_client_frame(c):
|
|
41
|
+
b0, b1 = c.recv(2)
|
|
42
|
+
n = b1 & 0x7F
|
|
43
|
+
if n == 126:
|
|
44
|
+
n = struct.unpack("!H", c.recv(2))[0]
|
|
45
|
+
mask = c.recv(4)
|
|
46
|
+
data = b""
|
|
47
|
+
while len(data) < n:
|
|
48
|
+
data += c.recv(n - len(data))
|
|
49
|
+
return b0 & 0x0F, bytes(b ^ mask[i % 4] for i, b in enumerate(data))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TerminalTest(unittest.TestCase):
|
|
53
|
+
def test_url(self):
|
|
54
|
+
self.assertEqual(terminal.terminal_url("https://sandbox.wbx.cz/", "sb_n01_x", 80, 24),
|
|
55
|
+
"wss://sandbox.wbx.cz/v1/sandboxes/sb_n01_x/terminal?cols=80&rows=24")
|
|
56
|
+
self.assertEqual(terminal.terminal_url("http://10.0.0.1:3000", "sb_n01_x", 1, 2),
|
|
57
|
+
"ws://10.0.0.1:3000/v1/sandboxes/sb_n01_x/terminal?cols=1&rows=2")
|
|
58
|
+
|
|
59
|
+
def test_websocket_roundtrip(self):
|
|
60
|
+
got = {}
|
|
61
|
+
|
|
62
|
+
def handler(c, req):
|
|
63
|
+
got["req"] = req
|
|
64
|
+
key = [l.split(": ", 1)[1] for l in req.split("\r\n") if l.lower().startswith("sec-websocket-key")][0]
|
|
65
|
+
acc = base64.b64encode(hashlib.sha1((key + terminal._GUID).encode()).digest()).decode()
|
|
66
|
+
c.sendall(("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
|
|
67
|
+
f"Sec-WebSocket-Accept: {acc}\r\n\r\n").encode())
|
|
68
|
+
c.sendall(_frame(0x9, b"hi")) # ping → klient odpoví pong
|
|
69
|
+
op, data = _read_client_frame(c)
|
|
70
|
+
got["pong"] = (op, data)
|
|
71
|
+
c.sendall(bytes([0x01, 3]) + b'{"a' + _frame(0x0, b'":1}')) # fragmentovaná zpráva
|
|
72
|
+
op, data = _read_client_frame(c)
|
|
73
|
+
got["sent"] = (op, data)
|
|
74
|
+
c.sendall(_frame(0x8, struct.pack("!H", 1000)))
|
|
75
|
+
_read_client_frame(c)
|
|
76
|
+
|
|
77
|
+
port = _server(handler)
|
|
78
|
+
ws = terminal.WebSocket(f"ws://127.0.0.1:{port}/v1/sandboxes/sb/terminal?cols=1", {"X-API-Key": "k"})
|
|
79
|
+
self.assertEqual(ws.recv_text(), '{"a":1}')
|
|
80
|
+
ws.send_text("x" * 300) # délka přes 126 bajtů
|
|
81
|
+
self.assertIsNone(ws.recv_text())
|
|
82
|
+
ws.close()
|
|
83
|
+
self.assertIn("GET /v1/sandboxes/sb/terminal?cols=1 HTTP/1.1", got["req"])
|
|
84
|
+
self.assertIn("X-API-Key: k", got["req"])
|
|
85
|
+
self.assertEqual(got["pong"], (0xA, b"hi"))
|
|
86
|
+
self.assertEqual(got["sent"], (0x1, b"x" * 300))
|
|
87
|
+
|
|
88
|
+
def test_http_error_is_reported(self):
|
|
89
|
+
def handler(c, req):
|
|
90
|
+
c.sendall(b'HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\n\r\n{"error":"sandbox not found"}')
|
|
91
|
+
|
|
92
|
+
port = _server(handler)
|
|
93
|
+
with self.assertRaises(terminal.WebSocketError) as e:
|
|
94
|
+
terminal.WebSocket(f"ws://127.0.0.1:{port}/x", {})
|
|
95
|
+
self.assertIn("sandbox not found", str(e.exception))
|
|
96
|
+
|
|
97
|
+
def test_cli_requires_key(self):
|
|
98
|
+
env = os.environ.pop("WBXSANDBOX_API_KEY", None)
|
|
99
|
+
try:
|
|
100
|
+
self.assertEqual(main(["connect", "sb_x", "--url", "http://127.0.0.1:1"]), 2)
|
|
101
|
+
finally:
|
|
102
|
+
if env is not None:
|
|
103
|
+
os.environ["WBXSANDBOX_API_KEY"] = env
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
unittest.main()
|