From 11a256601b3d00fcc06343e2e966361bff132981 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Sat, 10 Oct 2026 18:36:53 +0800 Subject: [PATCH 1/5] fix(encryption): pass per-call base_url and api_key to Encryption --- dashscope/api_entities/api_request_factory.py | 5 ++++- dashscope/api_entities/encryption.py | 22 ++++++++++++------- tests/legacy/test_encryption.py | 4 ++-- tests/unit/test_sdk_headers.py | 3 ++- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/dashscope/api_entities/api_request_factory.py b/dashscope/api_entities/api_request_factory.py index df1030d4..6511c29c 100644 --- a/dashscope/api_entities/api_request_factory.py +++ b/dashscope/api_entities/api_request_factory.py @@ -162,7 +162,10 @@ def _build_api_request( # pylint: disable=too-many-branches http_url += "?" + urlencode(extra_url_parameters) if enable_encryption is True: - encryption = Encryption() + encryption = Encryption( + base_url=base_address, + api_key=api_key, + ) encryption.initialize() if encryption.is_valid(): logger.debug("encryption enabled") diff --git a/dashscope/api_entities/encryption.py b/dashscope/api_entities/encryption.py index 6fafd996..a40dc2bf 100644 --- a/dashscope/api_entities/encryption.py +++ b/dashscope/api_entities/encryption.py @@ -4,6 +4,7 @@ import base64 import json import os +from typing import Optional import requests from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes @@ -19,11 +20,16 @@ ) from dashscope.common.logging import logger from dashscope.common.env import resolve_base_url +from dashscope.common.api_key import get_default_api_key from dashscope.common.utils import get_sdk_headers class Encryption: - def __init__(self): + def __init__( + self, + base_url: Optional[str] = None, + api_key: Optional[str] = None, + ): self.pub_key_id: str = "" self.pub_key_str: str = "" self.aes_key_bytes: bytes = b"" @@ -31,6 +37,8 @@ def __init__(self): self.iv_bytes: bytes = b"" self.base64_iv_str: str = "" self.valid: bool = False + self._base_url = base_url + self._api_key = api_key def initialize(self): public_keys = self._get_public_keys() @@ -87,14 +95,12 @@ def get_encrypted_aes_key_str(self): def get_base64_iv_str(self): return self.base64_iv_str - @staticmethod - def _get_public_keys(): - url = ( - resolve_base_url(dashscope.base_http_api_url) - + "/public-keys/latest" - ) + def _get_public_keys(self): + base_url = self._base_url or dashscope.base_http_api_url + api_key = self._api_key or get_default_api_key() + url = resolve_base_url(base_url) + "/public-keys/latest" headers = { - "Authorization": f"Bearer {dashscope.api_key}", + "Authorization": f"Bearer {api_key}", **get_sdk_headers(module="utils"), } diff --git a/tests/legacy/test_encryption.py b/tests/legacy/test_encryption.py index 830467c4..5b199612 100644 --- a/tests/legacy/test_encryption.py +++ b/tests/legacy/test_encryption.py @@ -11,7 +11,7 @@ class TestEncryption: @staticmethod @pytest.mark.skip(reason="Legacy integration test, requires real DashScope API") def test_get_public_keys(): - pub_keys = Encryption._get_public_keys() + pub_keys = Encryption()._get_public_keys() print(f"\nrsa:\n{json.dumps(pub_keys, indent=4, ensure_ascii=False)}") print(f"\npublic_key_id: {pub_keys.get('public_key_id')}") print(f"\npublic_key: {pub_keys.get('public_key')}") @@ -37,7 +37,7 @@ def test_encrypt_with_aes(): @staticmethod @pytest.mark.skip(reason="Legacy integration test, requires real DashScope API") def test_encrypt_aes_key_with_rsa(): - public_keys = Encryption._get_public_keys() + public_keys = Encryption()._get_public_keys() public_key = public_keys.get("public_key") aes_key = Encryption._generate_aes_secret_key() diff --git a/tests/unit/test_sdk_headers.py b/tests/unit/test_sdk_headers.py index e23451d6..cbe1181e 100644 --- a/tests/unit/test_sdk_headers.py +++ b/tests/unit/test_sdk_headers.py @@ -661,9 +661,10 @@ def fake_get(url, headers=None, timeout=None): "dashscope.api_entities.encryption.requests.get", fake_get, ) + monkeypatch.setattr("dashscope.api_key", "sk-test") print("\n=== misc ===") try: - Encryption._get_public_keys() # pylint: disable=protected-access + Encryption()._get_public_keys() # pylint: disable=protected-access except _CapturedRequest as e: _check_module(e.headers, "utils", "utils: Encryption public-keys") else: From b0bda1eb035692e7b2c2222259b7f939afce555f Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Tue, 13 Oct 2026 17:56:08 +0800 Subject: [PATCH 2/5] refactor(dashscope): Refactor assistant API import mechanism to support lazy loading --- dashscope/__init__.py | 59 +++++++++---- tests/unit/test_import_deprecation.py | 119 ++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 17 deletions(-) create mode 100644 tests/unit/test_import_deprecation.py diff --git a/dashscope/__init__.py b/dashscope/__init__.py index 944a1576..7cd5f596 100644 --- a/dashscope/__init__.py +++ b/dashscope/__init__.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -# pylint: disable=wrong-import-position +# pylint: disable=wrong-import-position,undefined-all-variable +import importlib import logging import warnings from logging import NullHandler @@ -60,22 +61,6 @@ from dashscope.models import Models from dashscope.nlp.understanding import Understanding from dashscope.rerank import AioTextReRank, TextReRank -from dashscope.assistants import Assistant, AssistantList, Assistants -from dashscope.assistants.assistant_types import AssistantFile, DeleteResponse -from dashscope.threads import ( - MessageFile, - Messages, - Run, - RunList, - Runs, - RunStep, - RunStepList, - Steps, - Thread, - ThreadMessage, - ThreadMessageList, - Threads, -) from dashscope.tokenizers import ( Tokenization, Tokenizer, @@ -127,6 +112,46 @@ def set_region(region: str, workspace_id: str = None): base_websocket_api_url = f"wss://{host}/api-ws/v1/inference" +# The Assistants API (dashscope.assistants / dashscope.threads) is deprecated. +# Resolve its names lazily (PEP 562) so a plain `import dashscope` does not emit +# the DeprecationWarning for code that never touches them; accessing any of these +# names imports dashscope.assistants, which warns once. +_DEPRECATED_LAZY_IMPORTS = { + "Assistant": "dashscope.assistants", + "AssistantList": "dashscope.assistants", + "Assistants": "dashscope.assistants", + "AssistantFile": "dashscope.assistants", + "DeleteResponse": "dashscope.assistants", + "MessageFile": "dashscope.threads", + "Messages": "dashscope.threads", + "Run": "dashscope.threads", + "RunList": "dashscope.threads", + "Runs": "dashscope.threads", + "RunStep": "dashscope.threads", + "RunStepList": "dashscope.threads", + "Steps": "dashscope.threads", + "Thread": "dashscope.threads", + "ThreadMessage": "dashscope.threads", + "ThreadMessageList": "dashscope.threads", + "Threads": "dashscope.threads", +} + + +def __getattr__(name): + module_name = _DEPRECATED_LAZY_IMPORTS.get(name) + if module_name is None: + raise AttributeError( + f"module {__name__!r} has no attribute {name!r}", + ) + value = getattr(importlib.import_module(module_name), name) + globals()[name] = value + return value + + +def __dir__(): + return sorted(set(globals()) | set(_DEPRECATED_LAZY_IMPORTS)) + + __all__ = [ "__version__", "base_compatible_api_url", diff --git a/tests/unit/test_import_deprecation.py b/tests/unit/test_import_deprecation.py new file mode 100644 index 00000000..6ecce74d --- /dev/null +++ b/tests/unit/test_import_deprecation.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Regression tests for issue #207. + +The deprecated Assistants API must not emit a DeprecationWarning on a plain +``import dashscope``; it should warn only when a deprecated name is actually +accessed. These run in subprocesses so each starts from a clean import state. +""" + +import os +import subprocess +import sys +import textwrap +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def _run(snippet, *, error_on_deprecation): + env = dict(os.environ) + existing = env.get("PYTHONPATH") + env["PYTHONPATH"] = str(REPO_ROOT) + ( + os.pathsep + existing if existing else "" + ) + args = [sys.executable] + if error_on_deprecation: + args += ["-W", "error::DeprecationWarning"] + args += ["-c", textwrap.dedent(snippet)] + return subprocess.run( + args, + capture_output=True, + text=True, + cwd=str(REPO_ROOT), + env=env, + check=False, + ) + + +def test_plain_import_emits_no_deprecation_warning(): + result = _run("import dashscope", error_on_deprecation=True) + assert result.returncode == 0, result.stderr + + +def test_non_assistants_usage_emits_no_deprecation_warning(): + result = _run( + "import dashscope; dashscope.Generation; dashscope.TextEmbedding", + error_on_deprecation=True, + ) + assert result.returncode == 0, result.stderr + + +def test_accessing_assistants_emits_deprecation_warning(): + result = _run( + "import dashscope; dashscope.Assistants", + error_on_deprecation=True, + ) + assert result.returncode != 0 + assert "DeprecationWarning" in result.stderr + assert "Assistants API" in result.stderr + + +def test_from_import_assistants_emits_deprecation_warning(): + result = _run( + "from dashscope import Assistants", + error_on_deprecation=True, + ) + assert result.returncode != 0 + assert "DeprecationWarning" in result.stderr + + +def test_accessing_threads_emits_deprecation_warning(): + result = _run( + "import dashscope; dashscope.Threads", + error_on_deprecation=True, + ) + assert result.returncode != 0 + assert "DeprecationWarning" in result.stderr + + +def test_direct_assistants_import_emits_deprecation_warning(): + result = _run("import dashscope.assistants", error_on_deprecation=True) + assert result.returncode != 0 + assert "DeprecationWarning" in result.stderr + + +def test_lazy_names_resolve_to_real_objects(): + import dashscope + from dashscope.assistants import Assistant, Assistants + from dashscope.assistants.assistant_types import ( + AssistantFile, + DeleteResponse, + ) + from dashscope.threads import RunStepList, Threads + + assert dashscope.Assistants is Assistants + assert dashscope.Assistant is Assistant + assert dashscope.AssistantFile is AssistantFile + assert dashscope.DeleteResponse is DeleteResponse + assert dashscope.Threads is Threads + assert dashscope.RunStepList is RunStepList + + +def test_dir_includes_lazy_names(): + import dashscope + + listing = dir(dashscope) + for name in ("Assistants", "Threads", "Runs", "Messages"): + assert name in listing + + +def test_unknown_attribute_raises_attribute_error(): + import dashscope + + try: + dashscope.ThisNameDoesNotExist + except AttributeError: + pass + else: # pragma: no cover + raise AssertionError("expected AttributeError") From 3d304005b9abc83098b2d8da967eb1fdf3307989 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Tue, 13 Oct 2026 18:01:49 +0800 Subject: [PATCH 3/5] fix(tests): add pylint disable for lazy-loaded names --- tests/unit/test_messages.py | 2 +- tests/unit/test_runs.py | 2 +- tests/unit/test_sdk_headers.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_messages.py b/tests/unit/test_messages.py index 5307f3bb..633ccb86 100644 --- a/tests/unit/test_messages.py +++ b/tests/unit/test_messages.py @@ -4,7 +4,7 @@ import json import uuid -from dashscope import Messages +from dashscope import Messages # pylint: disable=no-name-in-module from dashscope.threads.messages.files import Files from tests.unit.mock_request_base import MockServerBase from tests.unit.mock_server import MockServer diff --git a/tests/unit/test_runs.py b/tests/unit/test_runs.py index cfba03d3..fe6830dd 100644 --- a/tests/unit/test_runs.py +++ b/tests/unit/test_runs.py @@ -4,7 +4,7 @@ import json import uuid -from dashscope import Runs, Steps +from dashscope import Runs, Steps # pylint: disable=no-name-in-module from tests.unit.mock_request_base import MockServerBase from tests.unit.mock_server import MockServer diff --git a/tests/unit/test_sdk_headers.py b/tests/unit/test_sdk_headers.py index cbe1181e..7a3042f3 100644 --- a/tests/unit/test_sdk_headers.py +++ b/tests/unit/test_sdk_headers.py @@ -533,7 +533,7 @@ def test_tasks_module(capture_headers): def test_resource_api_modules(capture_headers): # pylint: disable=unused-argument,import-outside-toplevel,redefined-outer-name - from dashscope import ( + from dashscope import ( # pylint: disable=no-name-in-module Assistants, Deployments, Files, From a193ee105effb53c83d46c44198fcaaf7c3089d1 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Tue, 13 Oct 2026 18:05:14 +0800 Subject: [PATCH 4/5] fix(tests): add pylint disable for lazy-loaded names in assistants/threads tests --- tests/unit/test_assistants.py | 2 +- tests/unit/test_threads.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_assistants.py b/tests/unit/test_assistants.py index 1316f370..883d053d 100644 --- a/tests/unit/test_assistants.py +++ b/tests/unit/test_assistants.py @@ -4,7 +4,7 @@ import json import uuid -from dashscope import Assistants +from dashscope import Assistants # pylint: disable=no-name-in-module from tests.unit.mock_request_base import MockServerBase from tests.unit.mock_server import MockServer diff --git a/tests/unit/test_threads.py b/tests/unit/test_threads.py index d0a4f567..bbd780a1 100644 --- a/tests/unit/test_threads.py +++ b/tests/unit/test_threads.py @@ -4,7 +4,7 @@ import json import uuid -from dashscope import Threads +from dashscope import Threads # pylint: disable=no-name-in-module from tests.unit.mock_request_base import MockServerBase from tests.unit.mock_server import MockServer From 2a8a8fe772d81b1a7ba954059425ed68b193a872 Mon Sep 17 00:00:00 2001 From: luk384090-cloud Date: Thu, 15 Oct 2026 11:19:50 +0800 Subject: [PATCH 5/5] chore(version): Update version to 1.27.8 --- dashscope/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashscope/version.py b/dashscope/version.py index 42b1753b..6637807c 100644 --- a/dashscope/version.py +++ b/dashscope/version.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -__version__ = "1.27.7" +__version__ = "1.27.8"