Skip to content

Commit 826fee9

Browse files
Merge pull request #159 from dashscope/dev/sdk-cli-1
feat(cli): enhance CLI error handling and robustness
2 parents c0d7a0b + f18106b commit 826fee9

25 files changed

Lines changed: 348 additions & 61 deletions

LICENSE.certifi

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
This package contains a modified version of ca-bundle.crt:
2+
3+
ca-bundle.crt -- Bundle of CA Root Certificates
4+
5+
This is a bundle of X.509 certificates of public Certificate Authorities
6+
(CA). These were automatically extracted from Mozilla's root certificates
7+
file (certdata.txt). This file can be found in the mozilla source tree:
8+
https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt
9+
It contains the certificates in PEM format and therefore
10+
can be directly used with curl / libcurl / php_curl, or with
11+
an Apache+mod_ssl webserver for SSL client authentication.
12+
Just configure this file as the SSLCACertificateFile.#
13+
14+
***** BEGIN LICENSE BLOCK *****
15+
This Source Code Form is subject to the terms of the Mozilla Public License,
16+
v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain
17+
one at http://mozilla.org/MPL/2.0/.
18+
19+
***** END LICENSE BLOCK *****
20+
@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $

MANIFEST.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
include dashscope/resources/qwen.tiktoken
2+
include NOTICE
3+
include LICENSE.certifi

NOTICE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
This product depends on certifi (https://github.com/certifi/python-certifi),
2+
which is provided under the Mozilla Public License 2.0.
3+
See bundled LICENSE.certifi for details.

dashscope/audio/http_tts/http_speech_synthesizer.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414

1515

1616
class HttpSpeechSynthesisResult:
17-
"""The result of HTTP speech synthesis."""
17+
"""The result of HTTP speech synthesis.
18+
19+
This class wraps the actual SpeechSynthesisResponse and provides
20+
convenient access to both synthesis-specific data and standard
21+
DashScopeAPIResponse attributes for compatibility with CLI tools.
22+
"""
1823

1924
def __init__(
2025
self,
@@ -62,6 +67,49 @@ def response(self) -> Optional[SpeechSynthesisResponse]:
6267
"""Get the full API response."""
6368
return self._response
6469

70+
# Proxy standard DashScopeAPIResponse attributes for CLI compatibility
71+
@property
72+
def request_id(self) -> str:
73+
"""Get the request ID from the underlying response."""
74+
if self._response:
75+
return self._response.request_id
76+
return ""
77+
78+
@property
79+
def status_code(self) -> int:
80+
"""Get the HTTP status code from the underlying response."""
81+
if self._response:
82+
return self._response.status_code
83+
return 0
84+
85+
@property
86+
def code(self) -> str:
87+
"""Get the error code from the underlying response."""
88+
if self._response:
89+
return self._response.code
90+
return ""
91+
92+
@property
93+
def message(self) -> str:
94+
"""Get the error message from the underlying response."""
95+
if self._response:
96+
return self._response.message
97+
return ""
98+
99+
@property
100+
def output(self):
101+
"""Get the output from the underlying response."""
102+
if self._response:
103+
return self._response.output
104+
return None
105+
106+
@property
107+
def usage(self):
108+
"""Get the usage from the underlying response."""
109+
if self._response:
110+
return self._response.usage
111+
return None
112+
65113

66114
class HttpSpeechSynthesizer(BaseApi):
67115
"""HTTP-based text-to-speech interface for CosyVoice."""

dashscope/cli/__init__.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
from dashscope.common.error import AuthenticationError # noqa: E402
2424
from dashscope.cli import ( # noqa: E402
2525
application,
26-
code_generation,
2726
deployments,
2827
embeddings,
2928
files,
@@ -39,7 +38,6 @@
3938
speech_synthesis,
4039
tokenization,
4140
transcription,
42-
understanding,
4341
video_synthesis,
4442
)
4543

@@ -97,9 +95,7 @@
9795
"embeddings",
9896
"tokenization",
9997
"models",
100-
"understanding",
10198
"application",
102-
"code-generation",
10399
"image-synthesis",
104100
"video-synthesis",
105101
"image-generation",
@@ -276,9 +272,7 @@ def callback(
276272
app.add_typer(embeddings.app)
277273
app.add_typer(tokenization.app)
278274
app.add_typer(models.app)
279-
app.add_typer(understanding.app)
280275
app.add_typer(application.app)
281-
app.add_typer(code_generation.app)
282276
app.add_typer(image_synthesis.app)
283277
app.add_typer(video_synthesis.app)
284278
app.add_typer(image_generation.app)

dashscope/cli/agentic_rl.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,10 @@ def run(
545545
description="[green]✅ Job submitted successfully![/green]",
546546
)
547547

548+
# Validate output is not None before accessing attributes
549+
if result.output is None:
550+
raise OutputError("API returned success but output is empty")
551+
548552
format_output(
549553
{
550554
"job_id": result.output.job_id,
@@ -597,6 +601,10 @@ def get(
597601
f"API returned status {result.status_code}: {result.message}",
598602
)
599603

604+
# Validate output is not None before accessing attributes
605+
if result.output is None:
606+
raise OutputError("API returned success but output is empty")
607+
600608
format_output(
601609
{
602610
"job_id": result.output.job_id,
@@ -669,8 +677,13 @@ def logs(
669677
f"API returned status {result.status_code}: {result.message}",
670678
)
671679

680+
# Validate output is not None before accessing attributes
681+
logs_data = ""
682+
if result.output is not None and isinstance(result.output, dict):
683+
logs_data = result.output.get("logs", "")
684+
672685
format_output(
673-
{"job_id": job_id, "logs": getattr(result.output, "logs", "")},
686+
{"job_id": job_id, "logs": logs_data},
674687
fmt=output_format,
675688
)
676689
except Exception as e:

dashscope/cli/code_generation.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def callback(ctx: typer.Context):
2727
typer.echo(ctx.get_help())
2828

2929

30-
@app.command("create")
30+
@app.command("create", hidden=True)
3131
@handle_sdk_error("Code generation request failed")
3232
def create(
3333
model: str = typer.Option(..., "-m", "--model", help="The model to call"),
@@ -56,7 +56,8 @@ def create(
5656
),
5757
n: int = typer.Option(1, "-n", "--n", help="The number of output results"),
5858
):
59-
"""Call code generation API."""
59+
"""[DEPRECATED] No independent endpoint available.
60+
This command is hidden and will be removed."""
6061
messages = [UserRoleMessageParam(content=content)]
6162
if attachment_meta is not None:
6263
try:

dashscope/cli/common.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,54 @@ def print_failed_message(rsp):
4141
)
4242

4343

44-
def ensure_ok(rsp):
44+
def ensure_ok(rsp, check_business_error: bool = True):
4545
"""Return *rsp.output* when the response is OK; otherwise print the error
4646
and exit with code 1.
4747
4848
This eliminates the repetitive ``if rsp.status_code == OK … else …``
4949
pattern that appears in every command handler.
50+
51+
Enhanced to check both HTTP status and business-level error codes:
52+
- HTTP 200 but InvalidParameter → still treated as failure
53+
- HTTP 4xx/5xx → clear error message
54+
55+
Args:
56+
rsp: The API response object
57+
check_business_error: If True (default), check for business-level
58+
error codes in the output. Set to False for
59+
async task creation where we only care about
60+
HTTP success, not task execution.
5061
"""
51-
if rsp.status_code == HTTPStatus.OK:
52-
return rsp.output
53-
print_failed_message(rsp)
54-
raise typer.Exit(1)
62+
if rsp.status_code != HTTPStatus.OK:
63+
print_failed_message(rsp)
64+
raise typer.Exit(1)
65+
66+
# Check for business-level errors even when HTTP status is 200
67+
output = rsp.output
68+
if output is None:
69+
print_failed_message(rsp)
70+
raise typer.Exit(1)
71+
72+
# Only check business-level errors if explicitly requested
73+
if check_business_error:
74+
# Some APIs return error info in output even with HTTP 200
75+
if isinstance(output, dict):
76+
error_code = output.get("code")
77+
message = output.get("message", "Unknown error")
78+
else:
79+
error_code = getattr(output, "code", None)
80+
message = getattr(output, "message", "Unknown error")
81+
82+
if error_code and error_code != "":
83+
err_console.print(
84+
f"[red]Business Error[/red] request_id: {rsp.request_id}, "
85+
f"status_code: {rsp.status_code}, "
86+
f"code: {error_code}, "
87+
f"message: {message}",
88+
)
89+
raise typer.Exit(1)
90+
91+
return output
5592

5693

5794
def success(message: str):

dashscope/cli/deployments.py

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
"""``deployments`` sub-command group."""
33
import time
4+
from http import HTTPStatus
45
from typing import Optional
56

67
import typer
@@ -12,8 +13,10 @@
1213
console,
1314
err_console,
1415
ensure_ok,
16+
error,
1517
handle_sdk_error,
1618
logger,
19+
print_failed_message,
1720
success,
1821
)
1922

@@ -37,12 +40,34 @@ def callback(ctx: typer.Context):
3740
# ---------------------------------------------------------------------------
3841

3942

40-
def _wait_for_deployment(deployed_model: str):
41-
"""Block until the deployment reaches a non-pending state."""
43+
# ---------------------------------------------------------------------------
44+
# Constants
45+
# ---------------------------------------------------------------------------
46+
DEFAULT_WAIT_TIMEOUT = 3600 # 1 hour default timeout for waiting
47+
48+
49+
def _wait_for_deployment(
50+
deployed_model: str,
51+
timeout: int = DEFAULT_WAIT_TIMEOUT,
52+
):
53+
"""Block until the deployment reaches a non-pending state or times out."""
54+
start_time = time.time()
4255
try:
4356
while True:
57+
# Check timeout
58+
elapsed = time.time() - start_time
59+
if elapsed > timeout:
60+
err_console.print(
61+
"[red]Timeout:[/red] Deployment "
62+
f"{deployed_model} did not complete within "
63+
f"{timeout} seconds. You can check status later via: "
64+
f"[cyan]dashscope deployments get {deployed_model}[/cyan]",
65+
)
66+
raise typer.Exit(1)
67+
4468
rsp = dashscope.Deployments.get(deployed_model)
45-
output = ensure_ok(rsp)
69+
# During polling, only check HTTP success, not business errors
70+
output = ensure_ok(rsp, check_business_error=False)
4671
status = output.status
4772

4873
if status in (
@@ -67,7 +92,12 @@ def _wait_for_deployment(deployed_model: str):
6792

6893
def _print_deployments(output):
6994
"""Pretty-print a list of deployments from *output*."""
70-
if output is None or not output.deployments:
95+
if (
96+
output is None
97+
or not isinstance(output, dict)
98+
or "deployments" not in output
99+
or not output["deployments"]
100+
):
71101
console.print("There is no deployed model!")
72102
return
73103
for dep in output.deployments:
@@ -99,15 +129,46 @@ def create(
99129
"--capacity",
100130
help="The target capacity",
101131
),
132+
plan: Optional[str] = typer.Option(
133+
None,
134+
"--plan",
135+
help="Deployment plan or template ID",
136+
),
137+
template_id: Optional[str] = typer.Option(
138+
None,
139+
"--template-id",
140+
help="Template ID for deployment configuration",
141+
),
102142
):
103143
"""Create a model deployment."""
104-
rsp = dashscope.Deployments.call(
105-
model=model,
106-
capacity=capacity,
107-
suffix=suffix, # type: ignore[arg-type]
108-
)
109-
output = ensure_ok(rsp)
110-
deployed_model = output.deployed_model
144+
kwargs = {
145+
"model": model,
146+
"capacity": capacity,
147+
"suffix": suffix,
148+
}
149+
if plan is not None:
150+
kwargs["plan"] = plan
151+
if template_id is not None:
152+
kwargs["template_id"] = template_id
153+
154+
rsp = dashscope.Deployments.call(**kwargs)
155+
156+
# Enhanced error checking: verify both HTTP status and response content
157+
if rsp.status_code != HTTPStatus.OK:
158+
print_failed_message(rsp)
159+
raise typer.Exit(1)
160+
161+
output = rsp.output
162+
if output is None:
163+
error("Deployment creation returned empty response")
164+
165+
deployed_model = output.get("deployed_model")
166+
if not deployed_model:
167+
error(
168+
"Deployment creation succeeded but missing deployed_model "
169+
f"in response. Response: {output}",
170+
)
171+
111172
success(f"Create model: {deployed_model} deployment")
112173
_wait_for_deployment(deployed_model)
113174

dashscope/cli/files.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def upload(
6363
file_path = os.path.expanduser(file)
6464
if not os.path.exists(file_path):
6565
error(f"File {file_path} does not exist")
66+
return # unreachable, but makes intent clear
6667

6768
rsp = dashscope.Files.upload(
6869
file_path=file_path,
@@ -71,7 +72,13 @@ def upload(
7172
base_address=base_url,
7273
)
7374
output = ensure_ok(rsp)
74-
file_id = output["uploaded_files"][0]["file_id"]
75+
76+
# Validate uploaded_files exists and is not empty
77+
uploaded_files = output.get("uploaded_files", [])
78+
if not uploaded_files:
79+
error("Upload succeeded but no file_id returned in response")
80+
81+
file_id = uploaded_files[0]["file_id"]
7582
success(f"Upload success, file id: {file_id}")
7683

7784

0 commit comments

Comments
 (0)