Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Changelog
## Unreleased
* Sandbox 注入规则支持 `if_headers` / `if_queries` 条件匹配字段,并兼容 `ifHeaders` / `ifQueries` 入参别名

## 7.18.0
* 新增 Qiniu Sandbox Python SDK 模块,支持生命周期、命令执行、文件系统、Git、PTY、模板、资源挂载和注入规则等能力

Expand Down
12 changes: 8 additions & 4 deletions examples/sandbox_injection_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ def main():
name='python-sdk-example',
injection={
'type': 'http',
'base_url': 'https://api.example.com',
'base_url': 'https://api.example.com/v1/*',
'if_headers': {'X-Qiniu-Sandbox': 'enabled'},
'if_queries': {'tenant': 'demo'},
'headers': {'X-From-Sandbox': 'qiniu-python-sdk'},
},
)
Expand All @@ -25,9 +27,11 @@ def main():
rule_id,
name='python-sdk-example-updated',
injection={
'type': 'http',
'base_url': 'https://api.example.com',
'headers': {'X-Updated-From-Sandbox': 'qiniu-python-sdk'},
'type': 'openai',
'base_url': 'https://api.openai.com/v1/*',
'if_headers': {'X-Qiniu-Sandbox': 'enabled'},
'if_queries': {'model': 'gpt-4o-mini'},
'api_key': 'replace-with-openai-api-key',
},
))
finally:
Expand Down
4 changes: 4 additions & 0 deletions qiniu/services/sandbox/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ def _normalize_injection(injection):
data['api_key'] = data.pop('apiKey')
if 'baseUrl' in data and 'base_url' not in data:
data['base_url'] = data.pop('baseUrl')
if 'ifHeaders' in data and 'if_headers' not in data:
data['if_headers'] = data.pop('ifHeaders')
if 'ifQueries' in data and 'if_queries' not in data:
data['if_queries'] = data.pop('ifQueries')
if 'ruleId' in data and 'ruleID' not in data:
data['ruleID'] = data.pop('ruleId')
if 'rule_id' in data and 'ruleID' not in data:
Expand Down
10 changes: 10 additions & 0 deletions qiniu/services/sandbox/commands.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import base64
import binascii
import re

from qiniu.compat import basestring, bytes as bytes_type, str as text_type

Expand Down Expand Up @@ -93,6 +94,15 @@ def command_result_from_events(events, on_stdout=None, on_stderr=None,
if end:
if end.get('exitCode') is not None:
exit_code = end.get('exitCode')
elif end.get('exit_code') is not None:
exit_code = end.get('exit_code')
else:
status_match = re.match(
r'^exit status (\d+)$',
text_type(end.get('status') or ''),
)
if status_match:
exit_code = int(status_match.group(1))
error = end.get('error') or ''
return CommandResult(pid, exit_code, stdout, stderr, error)

Expand Down
58 changes: 56 additions & 2 deletions tests/cases/test_services/test_sandbox/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,9 @@ def test_util_helpers_encode_unicode_values_safely():
).startswith('v1_')


def test_create_with_kodo_resource_requires_qiniu_credentials():
def test_create_with_kodo_resource_requires_qiniu_credentials(monkeypatch):
monkeypatch.delenv('QINIU_SANDBOX_ACCESS_KEY', raising=False)
monkeypatch.delenv('QINIU_SANDBOX_SECRET_KEY', raising=False)
client = SandboxClient(api_key='api-key', session=RecordingSession())

with pytest.raises(SandboxError) as err:
Expand Down Expand Up @@ -241,7 +243,10 @@ def test_create_with_kodo_resource_uses_qiniu_signature():
}]


def test_create_with_saved_injection_rule_requires_qiniu_credentials():
def test_create_with_saved_injection_rule_requires_qiniu_credentials(
monkeypatch):
monkeypatch.delenv('QINIU_SANDBOX_ACCESS_KEY', raising=False)
monkeypatch.delenv('QINIU_SANDBOX_SECRET_KEY', raising=False)
client = SandboxClient(api_key='api-key', session=RecordingSession())

with pytest.raises(SandboxError) as err:
Expand Down Expand Up @@ -287,6 +292,55 @@ def test_create_with_saved_injection_rule_accepts_snake_case_rule_id():
}]


def test_create_sandbox_normalizes_inline_injection_match_conditions():
session = RecordingSession(
[DummyResponse(201, {'sandboxID': 'sbx123', 'templateID': 'base'})])
client = SandboxClient(api_key='api-key', session=session)

client.create_sandbox(injections=[{
'type': 'openai',
'apiKey': 'openai-key',
'baseUrl': 'https://api.openai.com/v1/*',
'ifHeaders': {'X-Tenant': 'tenant-1'},
'ifQueries': {'model': 'gpt-4o-mini'},
}])

assert body_of(session.requests[0])['injections'] == [{
'type': 'openai',
'api_key': 'openai-key',
'base_url': 'https://api.openai.com/v1/*',
'if_headers': {'X-Tenant': 'tenant-1'},
'if_queries': {'model': 'gpt-4o-mini'},
}]


def test_injection_rule_requests_normalize_match_conditions():
session = RecordingSession([DummyResponse(201, {'ruleID': 'rule-1'})])
client = SandboxClient(access_key='ak', secret_key='sk', session=session)

client.create_injection_rule(
name='conditional-openai',
injection={
'type': 'openai',
'apiKey': 'openai-key',
'baseUrl': 'https://api.openai.com/v1/*',
'ifHeaders': {'X-Tenant': 'tenant-1'},
'ifQueries': {'model': 'gpt-4o-mini'},
},
)

assert body_of(session.requests[0]) == {
'name': 'conditional-openai',
'injection': {
'type': 'openai',
'api_key': 'openai-key',
'base_url': 'https://api.openai.com/v1/*',
'if_headers': {'X-Tenant': 'tenant-1'},
'if_queries': {'model': 'gpt-4o-mini'},
},
}


def test_sandbox_create_signature_matches_e2b_style():
session = RecordingSession([
DummyResponse(201, {
Expand Down
16 changes: 16 additions & 0 deletions tests/cases/test_services/test_sandbox/test_envd.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,22 @@ def test_command_event_decode_keeps_unknown_exit_code():
assert result.exit_code == -1


def test_command_event_decode_handles_status_exit_code():
result = command_result_from_events([{
'event': {'end': {'exited': True, 'status': 'exit status 7'}},
}])

assert result.exit_code == 7


def test_command_event_decode_handles_snake_case_exit_code():
result = command_result_from_events([{
'event': {'end': {'exit_code': 3}},
}])

assert result.exit_code == 3


def test_command_event_decode_handles_bytes_values():
result = command_result_from_events([{
'event': {'data': {
Expand Down
11 changes: 10 additions & 1 deletion tests/cases/test_services/test_sandbox/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,18 @@ def test_git_remote_push_when_credentials_are_configured():
repo_path = '/tmp/qiniu-python-sdk-remote-git'
try:
sandbox = create_integration_sandbox('remote_git')
try:
auth_result = sandbox.git.dangerously_authenticate(
username,
password,
)
except SandboxError as err:
if is_unsupported_runtime_error(err):
pytest.skip('envd does not support process stdin')
raise
assert_command_ok(
'git authenticate',
sandbox.git.dangerously_authenticate(username, password),
auth_result,
)
assert_command_ok(
'git http version',
Expand Down
Loading