Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a5adf91
feat(sandbox): add sandbox module
miclle Jun 23, 2026
0638458
feat(sandbox): align runtime APIs with e2b
miclle Jun 23, 2026
1ade2ea
chore(sandbox): remove differences doc from commit
miclle Jun 23, 2026
eb025fc
fix(sandbox): honor background command handles
miclle Jun 23, 2026
3162868
feat(sandbox): align more e2b python APIs
miclle Jun 23, 2026
361e2d3
fix(sandbox): honor git credentials and file timeouts
miclle Jun 23, 2026
e0deaca
fix(sandbox): address PR feedback
miclle Jun 23, 2026
edb32dd
fix(sandbox): harden reviewer feedback paths
miclle Jun 23, 2026
d1f007d
fix(sandbox): tighten credential and compat paths
miclle Jun 23, 2026
d93e914
fix(sandbox): address latest review feedback
miclle Jun 23, 2026
bdba675
fix(sandbox): harden latest review paths
miclle Jun 23, 2026
e3f513d
fix(sandbox): close latest review gaps
miclle Jun 23, 2026
083ffde
fix(sandbox): sign saved injection sandbox creates
miclle Jun 23, 2026
c8ec04d
fix(sandbox): clean up latest review nits
miclle Jun 23, 2026
774d1b5
fix(sandbox): address expanded review feedback
miclle Jun 23, 2026
a1d4377
fix(sandbox): tighten python compatibility feedback
miclle Jun 23, 2026
b9a22f6
fix(sandbox): harden git credential cleanup
miclle Jun 23, 2026
0aeef16
fix(sandbox): handle bytes command output
miclle Jun 23, 2026
7674645
fix(sandbox): support api key env fallbacks
miclle Jun 23, 2026
97d3967
fix(sandbox): harden latest review paths
miclle Jun 23, 2026
8627b2b
fix(sandbox): wrap envd transport errors
miclle Jun 23, 2026
89209eb
fix(sandbox): avoid git credential command leaks
miclle Jun 23, 2026
25e769c
fix(sandbox): tighten client edge cases
miclle Jun 23, 2026
0898f5f
fix(sandbox): close paginated review feedback
miclle Jun 23, 2026
850fe56
fix(sandbox): simplify git config helpers
miclle Jun 23, 2026
2d09a65
fix(sandbox): align command stream timeout
miclle Jun 23, 2026
ecb581c
fix(sandbox): preserve e2b compatibility edges
miclle Jun 23, 2026
64853e5
fix(sandbox): address review compatibility edges
miclle Jun 23, 2026
b50de52
fix(sandbox): clarify option and credential validation
miclle Jun 23, 2026
a1e3a11
fix(sandbox): harden background credentials and bytes input
miclle Jun 23, 2026
18579c1
fix(sandbox): simplify stream decoding and cleanup
miclle Jun 23, 2026
66218bb
fix(sandbox): tighten async and timeout edge cases
miclle Jun 23, 2026
6af02ec
fix(sandbox): close command git and template edge cases
miclle Jun 23, 2026
03049b4
fix(sandbox): cover readiness and credential cleanup edges
miclle Jun 23, 2026
59d2d3e
fix(sandbox): avoid credential files and harden polling
miclle Jun 23, 2026
563d356
fix(sandbox): address review edge cases
miclle Jun 23, 2026
e4aab28
fix(sandbox): preserve protocol errors and git status parsing
miclle Jun 23, 2026
fc22552
fix(sandbox): tighten polling and paginator edges
miclle Jun 23, 2026
fcca4e4
fix(sandbox): harden git credentials and config parsing
miclle Jun 23, 2026
90a4882
fix(sandbox): harden text uploads
miclle Jun 23, 2026
17a638d
fix(sandbox): detect relative legacy git config paths
miclle Jun 23, 2026
5fa7701
ci: run public checks for fork pull requests
miclle Jun 24, 2026
666a206
ci: install pip when conda environment lacks it
miclle Jun 24, 2026
c0ecff2
test: expand sandbox examples
miclle Jun 24, 2026
c198b56
ci: ignore windows mock server cleanup failures
miclle Jun 24, 2026
8cb9e31
fix(sandbox): address review feedback
miclle Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copy this file to .env in the project root before running sandbox examples
# or sandbox integration tests.

# Sandbox API key auth.
QINIU_SANDBOX_API_KEY=

# Optional custom endpoint.
# QINIU_SANDBOX_ENDPOINT is the preferred name; QINIU_SANDBOX_API_URL and
# E2B_API_URL are also accepted by the SDK for compatibility.
QINIU_SANDBOX_ENDPOINT=

# Optional template alias or ID. Defaults to base.
QINIU_SANDBOX_TEMPLATE=base

# Required only for injection-rule and Kodo resource examples/tests.
QINIU_SANDBOX_ACCESS_KEY=
QINIU_SANDBOX_SECRET_KEY=

# Optional Git remote examples.
GIT_REPO_URL=
GIT_USERNAME=
GIT_PASSWORD=

# Optional Git repository resource example.
GITHUB_TOKEN=
QINIU_SANDBOX_GIT_MOUNT_PATH=/workspace/repo

# Optional Kodo resource example.
QINIU_SANDBOX_KODO_BUCKET=
QINIU_SANDBOX_KODO_MOUNT_PATH=/workspace/kodo
QINIU_SANDBOX_KODO_PREFIX=

# Optional request injection examples.
QINIU_SANDBOX_HTTP_INJECTION_TOKEN=real_token
QINIU_SANDBOX_OPENAI_API_KEY=
83 changes: 82 additions & 1 deletion .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
on: [push]
name: Run Test Cases
on:
- push
- pull_request

permissions:
contents: read

jobs:
test:
strategy:
Expand Down Expand Up @@ -29,6 +35,9 @@ jobs:
run: |
MAJOR=$(echo "$PYTHON_VERSION" | cut -d'.' -f1)
MINOR=$(echo "$PYTHON_VERSION" | cut -d'.' -f2)
if ! python -m pip --version >/dev/null 2>&1; then
conda install -y pip
fi
# reinstall pip by some python(<3.7) not compatible
if ! [[ $MAJOR -ge 3 && $MINOR -ge 7 ]]; then
cd /tmp
Expand All @@ -49,7 +58,31 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install -I -e ".[dev]"
- name: Run public cases
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
shell: bash -el {0}
env:
MOCK_SERVER_ADDRESS: "http://127.0.0.1:9000"
run: |
flake8 --show-source --max-line-length=160 ./qiniu
python -m pytest \
tests/cases/test_auth.py \
tests/cases/test_http/test_endpoint.py \
tests/cases/test_http/test_endpoints_retry_policy.py \
tests/cases/test_http/test_middleware.py \
tests/cases/test_http/test_qiniu_conf.py \
tests/cases/test_http/test_region.py \
tests/cases/test_http/test_regions_retry_policy.py \
tests/cases/test_http/test_resp.py \
tests/cases/test_http/test_single_flight.py \
tests/cases/test_retry \
tests/cases/test_utils.py \
tests/cases/test_services/test_sandbox/test_client.py \
tests/cases/test_services/test_sandbox/test_config.py \
tests/cases/test_services/test_sandbox/test_envd.py \
tests/cases/test_services/test_sandbox/test_example_config.py
- name: Run cases
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
shell: bash -el {0}
env:
QINIU_ACCESS_KEY: ${{ secrets.QINIU_ACCESS_KEY }}
Expand All @@ -75,6 +108,7 @@ jobs:
run: |
cat py-mock-server.log
- name: Upload results to Codecov
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand Down Expand Up @@ -106,6 +140,10 @@ jobs:
PYTHON_VERSION: ${{ matrix.python_version }}
PIP_BOOTSTRAP_SCRIPT_PREFIX: https://bootstrap.pypa.io/pip
run: |
$null = python -m pip --version
if ($LASTEXITCODE -ne 0) {
conda install -y pip
}
# reinstall pip by some python(<3.7) not compatible
$pyversion = [Version]"$ENV:PYTHON_VERSION"
if ($pyversion -lt [Version]"3.7") {
Expand All @@ -117,7 +155,47 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install -I -e ".[dev]"
- name: Run public cases
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
env:
MOCK_SERVER_ADDRESS: "http://127.0.0.1:9000"
PYTHONPATH: "$PYTHONPATH:."
run: |
Write-Host "======== Setup Mock Server ========="
conda create -y -n mock-server python=3.10
conda activate mock-server
python --version
$processOptions = @{
FilePath="python"
ArgumentList="tests\mock_server\main.py", "--port", "9000"
PassThru=$true
RedirectStandardOutput="py-mock-server.log"
}
$mocksrvp = Start-Process @processOptions
$mocksrvp.Id | Out-File -FilePath "mock-server.pid"
conda deactivate
Sleep 3
Write-Host "======== Running Public Test ========="
python --version
flake8 --show-source --max-line-length=160 ./qiniu
python -m pytest `
tests/cases/test_auth.py `
tests/cases/test_http/test_endpoint.py `
tests/cases/test_http/test_endpoints_retry_policy.py `
tests/cases/test_http/test_middleware.py `
tests/cases/test_http/test_qiniu_conf.py `
tests/cases/test_http/test_region.py `
tests/cases/test_http/test_regions_retry_policy.py `
tests/cases/test_http/test_resp.py `
tests/cases/test_http/test_single_flight.py `
tests/cases/test_retry `
tests/cases/test_utils.py `
tests/cases/test_services/test_sandbox/test_client.py `
tests/cases/test_services/test_sandbox/test_config.py `
tests/cases/test_services/test_sandbox/test_envd.py `
tests/cases/test_services/test_sandbox/test_example_config.py
- name: Run cases
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
env:
QINIU_ACCESS_KEY: ${{ secrets.QINIU_ACCESS_KEY }}
QINIU_SECRET_KEY: ${{ secrets.QINIU_SECRET_KEY }}
Expand Down Expand Up @@ -148,6 +226,7 @@ jobs:
python -m pytest ./test_qiniu.py tests --cov qiniu --cov-report=xml
- name: Post Setup mock server
if: ${{ always() }}
continue-on-error: true
run: |
Try {
$mocksrvpid = Get-Content -Path "mock-server.pid"
Expand All @@ -158,9 +237,11 @@ jobs:
}
- name: Print mock server log
if: ${{ failure() }}
continue-on-error: true
run: |
Get-Content -Path "py-mock-server.log" | Write-Host
- name: Upload results to Codecov
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.DS_Store
*.swp
*.pyc
.env

*.py[cod]

Expand Down
62 changes: 62 additions & 0 deletions examples/sandbox_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
from __future__ import print_function

from qiniu.services.sandbox import SandboxError

from sandbox_common import cleanup_sandbox, create_sandbox, run_example


def main():
sandbox = create_sandbox(metadata={'example': 'sandbox_commands'})
try:
stdout = []
result = sandbox.commands.run(
'printf command-callback',
on_stdout=stdout.append,
)
print('run:', result.stdout)
print('callback:', ''.join(stdout))

handle = sandbox.commands.start(
'read line; echo "stdin:$line"',
stdin=True,
tag='qiniu-python-sdk-commands',
timeout=30,
)
print('started pid:', handle.pid)
print('processes:', [
item.pid for item in sandbox.commands.list()
if item.pid == handle.pid or item.tag == 'qiniu-python-sdk-commands'
])
try:
sandbox.commands.send_stdin(handle.pid, 'hello\n')
sandbox.commands.close_stdin(handle.pid)
print('stdin result:', handle.wait().stdout.strip())
except SandboxError as err:
print('stdin skipped:', err)
try:
sandbox.commands.kill(handle.pid)
except SandboxError:
pass

sleeper = sandbox.commands.run(
'sleep 30',
background=True,
tag='qiniu-python-sdk-kill',
)
print('background pid:', sleeper.pid)
try:
connected = sandbox.commands.connect(sleeper.pid)
print('connect running pid:', connected.pid)
except SandboxError as err:
print('connect running process skipped:', err)
try:
print('kill background:', sandbox.commands.kill(sleeper.pid))
except SandboxError as err:
print('kill background skipped:', err)
finally:
cleanup_sandbox(sandbox)


if __name__ == '__main__':
run_example(main)
60 changes: 60 additions & 0 deletions examples/sandbox_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
import os

from qiniu.services.sandbox import Sandbox
from qiniu.services.sandbox.config import (
env,
load_dotenv_if_present,
required_env,
sandbox_client,
sandbox_template,
)


__all__ = [
'cleanup_sandbox',
'create_sandbox',
'env',
'load_example_env',
'required_env',
'run_example',
'sandbox_client',
'sandbox_template',
]


def load_example_env():
root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
load_dotenv_if_present(
os.path.join(root, '.env'),
os.path.join(os.getcwd(), '.env'),
)


def create_sandbox(**options):
client = options.pop('client', None) or sandbox_client()
template = options.pop('template', sandbox_template())
options.setdefault('timeout', 300)
sandbox = Sandbox.create(template, client=client, **options)
print('Sandbox created:', sandbox.sandbox_id)
return sandbox


def cleanup_sandbox(sandbox):
if sandbox is None:
return
try:
sandbox.kill()
print('Sandbox killed:', sandbox.sandbox_id)
except Exception as err:
print('Failed to kill sandbox:', sandbox.sandbox_id, err)


def run_example(fn):
load_example_env()
try:
fn()
except Exception as err:
print(err)
raise
37 changes: 37 additions & 0 deletions examples/sandbox_connect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
from __future__ import print_function

from qiniu.services.sandbox import Sandbox
from sandbox_common import (
cleanup_sandbox,
create_sandbox,
run_example,
sandbox_client,
)


def main():
client = sandbox_client()
sandbox = create_sandbox(
client=client,
timeout=300,
metadata={'example': 'sandbox_connect'},
)
try:
items = Sandbox.list(client=client, limit=10).next_items()
print('list:', [item.sandbox_id for item in items])

connected = Sandbox.connect(
sandbox.sandbox_id,
client=client,
timeout=300,
)
print('connected:', connected.sandbox_id)
print('envd:', connected.envd_url())
print('uptime:', connected.commands.run('uptime').stdout)
finally:
cleanup_sandbox(sandbox)


if __name__ == '__main__':
run_example(main)
24 changes: 24 additions & 0 deletions examples/sandbox_create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import print_function

from sandbox_common import cleanup_sandbox, create_sandbox, run_example


def main():
sandbox = create_sandbox(
metadata={'example': 'sandbox_create'},
envs={'HELLO': 'qiniu'},
)
try:
print('sandbox:', sandbox.sandbox_id)
result = sandbox.commands.run('printf "$HELLO"')
print('stdout:', result.stdout)

sandbox.files.write('/tmp/qiniu.txt', 'hello from qiniu sandbox')
print('file:', sandbox.files.read_text('/tmp/qiniu.txt'))
finally:
cleanup_sandbox(sandbox)


if __name__ == '__main__':
run_example(main)
Loading
Loading