Skip to content
Open
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
59 changes: 42 additions & 17 deletions dashscope/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion dashscope/api_entities/api_request_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
22 changes: 14 additions & 8 deletions dashscope/api_entities/encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,18 +20,25 @@
)
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""
self.encrypted_aes_key_str: str = ""
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()
Expand Down Expand Up @@ -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"),
}

Expand Down
2 changes: 1 addition & 1 deletion dashscope/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# Copyright (c) Alibaba, Inc. and its affiliates.

__version__ = "1.27.7"
__version__ = "1.27.8"
4 changes: 2 additions & 2 deletions tests/legacy/test_encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')}")
Expand All @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_assistants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
119 changes: 119 additions & 0 deletions tests/unit/test_import_deprecation.py
Original file line number Diff line number Diff line change
@@ -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")
2 changes: 1 addition & 1 deletion tests/unit/test_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions tests/unit/test_sdk_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_threads.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading