dashscope 1.8.0__py3-none-any.whl → 1.25.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dashscope/__init__.py +61 -14
- dashscope/aigc/__init__.py +10 -3
- dashscope/aigc/chat_completion.py +282 -0
- dashscope/aigc/code_generation.py +145 -0
- dashscope/aigc/conversation.py +71 -12
- dashscope/aigc/generation.py +288 -16
- dashscope/aigc/image_synthesis.py +473 -31
- dashscope/aigc/multimodal_conversation.py +299 -14
- dashscope/aigc/video_synthesis.py +610 -0
- dashscope/api_entities/aiohttp_request.py +8 -5
- dashscope/api_entities/api_request_data.py +4 -2
- dashscope/api_entities/api_request_factory.py +68 -20
- dashscope/api_entities/base_request.py +20 -3
- dashscope/api_entities/chat_completion_types.py +344 -0
- dashscope/api_entities/dashscope_response.py +243 -15
- dashscope/api_entities/encryption.py +179 -0
- dashscope/api_entities/http_request.py +216 -62
- dashscope/api_entities/websocket_request.py +43 -34
- dashscope/app/__init__.py +5 -0
- dashscope/app/application.py +203 -0
- dashscope/app/application_response.py +246 -0
- dashscope/assistants/__init__.py +16 -0
- dashscope/assistants/assistant_types.py +175 -0
- dashscope/assistants/assistants.py +311 -0
- dashscope/assistants/files.py +197 -0
- dashscope/audio/__init__.py +4 -2
- dashscope/audio/asr/__init__.py +17 -1
- dashscope/audio/asr/asr_phrase_manager.py +203 -0
- dashscope/audio/asr/recognition.py +167 -27
- dashscope/audio/asr/transcription.py +107 -14
- dashscope/audio/asr/translation_recognizer.py +1006 -0
- dashscope/audio/asr/vocabulary.py +177 -0
- dashscope/audio/qwen_asr/__init__.py +7 -0
- dashscope/audio/qwen_asr/qwen_transcription.py +189 -0
- dashscope/audio/qwen_omni/__init__.py +11 -0
- dashscope/audio/qwen_omni/omni_realtime.py +524 -0
- dashscope/audio/qwen_tts/__init__.py +5 -0
- dashscope/audio/qwen_tts/speech_synthesizer.py +77 -0
- dashscope/audio/qwen_tts_realtime/__init__.py +10 -0
- dashscope/audio/qwen_tts_realtime/qwen_tts_realtime.py +355 -0
- dashscope/audio/tts/__init__.py +2 -0
- dashscope/audio/tts/speech_synthesizer.py +5 -0
- dashscope/audio/tts_v2/__init__.py +12 -0
- dashscope/audio/tts_v2/enrollment.py +179 -0
- dashscope/audio/tts_v2/speech_synthesizer.py +886 -0
- dashscope/cli.py +157 -37
- dashscope/client/base_api.py +652 -87
- dashscope/common/api_key.py +2 -0
- dashscope/common/base_type.py +135 -0
- dashscope/common/constants.py +13 -16
- dashscope/common/env.py +2 -0
- dashscope/common/error.py +58 -22
- dashscope/common/logging.py +2 -0
- dashscope/common/message_manager.py +2 -0
- dashscope/common/utils.py +276 -46
- dashscope/customize/__init__.py +0 -0
- dashscope/customize/customize_types.py +192 -0
- dashscope/customize/deployments.py +146 -0
- dashscope/customize/finetunes.py +234 -0
- dashscope/embeddings/__init__.py +5 -1
- dashscope/embeddings/batch_text_embedding.py +208 -0
- dashscope/embeddings/batch_text_embedding_response.py +65 -0
- dashscope/embeddings/multimodal_embedding.py +118 -10
- dashscope/embeddings/text_embedding.py +13 -1
- dashscope/{file.py → files.py} +19 -4
- dashscope/io/input_output.py +2 -0
- dashscope/model.py +11 -2
- dashscope/models.py +43 -0
- dashscope/multimodal/__init__.py +20 -0
- dashscope/multimodal/dialog_state.py +56 -0
- dashscope/multimodal/multimodal_constants.py +28 -0
- dashscope/multimodal/multimodal_dialog.py +648 -0
- dashscope/multimodal/multimodal_request_params.py +313 -0
- dashscope/multimodal/tingwu/__init__.py +10 -0
- dashscope/multimodal/tingwu/tingwu.py +80 -0
- dashscope/multimodal/tingwu/tingwu_realtime.py +579 -0
- dashscope/nlp/__init__.py +0 -0
- dashscope/nlp/understanding.py +64 -0
- dashscope/protocol/websocket.py +3 -0
- dashscope/rerank/__init__.py +0 -0
- dashscope/rerank/text_rerank.py +69 -0
- dashscope/resources/qwen.tiktoken +151643 -0
- dashscope/threads/__init__.py +26 -0
- dashscope/threads/messages/__init__.py +0 -0
- dashscope/threads/messages/files.py +113 -0
- dashscope/threads/messages/messages.py +220 -0
- dashscope/threads/runs/__init__.py +0 -0
- dashscope/threads/runs/runs.py +501 -0
- dashscope/threads/runs/steps.py +112 -0
- dashscope/threads/thread_types.py +665 -0
- dashscope/threads/threads.py +212 -0
- dashscope/tokenizers/__init__.py +7 -0
- dashscope/tokenizers/qwen_tokenizer.py +111 -0
- dashscope/tokenizers/tokenization.py +125 -0
- dashscope/tokenizers/tokenizer.py +45 -0
- dashscope/tokenizers/tokenizer_base.py +32 -0
- dashscope/utils/__init__.py +0 -0
- dashscope/utils/message_utils.py +838 -0
- dashscope/utils/oss_utils.py +243 -0
- dashscope/utils/param_utils.py +29 -0
- dashscope/version.py +3 -1
- {dashscope-1.8.0.dist-info → dashscope-1.25.6.dist-info}/METADATA +53 -50
- dashscope-1.25.6.dist-info/RECORD +112 -0
- {dashscope-1.8.0.dist-info → dashscope-1.25.6.dist-info}/WHEEL +1 -1
- {dashscope-1.8.0.dist-info → dashscope-1.25.6.dist-info}/entry_points.txt +0 -1
- {dashscope-1.8.0.dist-info → dashscope-1.25.6.dist-info/licenses}/LICENSE +2 -4
- dashscope/deployment.py +0 -129
- dashscope/finetune.py +0 -149
- dashscope-1.8.0.dist-info/RECORD +0 -49
- {dashscope-1.8.0.dist-info → dashscope-1.25.6.dist-info}/top_level.txt +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# Copyright (c) Alibaba, Inc. and its affiliates.
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
from dataclasses import dataclass
|
|
3
5
|
from http import HTTPStatus
|
|
@@ -110,13 +112,14 @@ class Role:
|
|
|
110
112
|
SYSTEM = 'system'
|
|
111
113
|
BOT = 'bot'
|
|
112
114
|
ASSISTANT = 'assistant'
|
|
115
|
+
ATTACHMENT = 'attachment'
|
|
113
116
|
|
|
114
117
|
|
|
115
118
|
class Message(DictMixin):
|
|
116
119
|
role: str
|
|
117
120
|
content: Union[str, List]
|
|
118
121
|
|
|
119
|
-
def __init__(self, role: str, content: str, **kwargs):
|
|
122
|
+
def __init__(self, role: str, content: str = None, **kwargs):
|
|
120
123
|
super().__init__(role=role, content=content, **kwargs)
|
|
121
124
|
|
|
122
125
|
@classmethod
|
|
@@ -136,12 +139,37 @@ class Message(DictMixin):
|
|
|
136
139
|
class Choice(DictMixin):
|
|
137
140
|
finish_reason: str
|
|
138
141
|
message: Message
|
|
139
|
-
|
|
142
|
+
|
|
143
|
+
def __init__(self,
|
|
144
|
+
finish_reason: str = None,
|
|
145
|
+
message: Message = None,
|
|
146
|
+
**kwargs):
|
|
140
147
|
msgObject = None
|
|
141
|
-
if message is not None:
|
|
148
|
+
if message is not None and message:
|
|
142
149
|
msgObject = Message(**message)
|
|
143
|
-
super().__init__(finish_reason=finish_reason,
|
|
144
|
-
message=msgObject,
|
|
150
|
+
super().__init__(finish_reason=finish_reason,
|
|
151
|
+
message=msgObject,
|
|
152
|
+
**kwargs)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(init=False)
|
|
156
|
+
class Audio(DictMixin):
|
|
157
|
+
data: str
|
|
158
|
+
url: str
|
|
159
|
+
id: str
|
|
160
|
+
expires_at: int
|
|
161
|
+
|
|
162
|
+
def __init__(self,
|
|
163
|
+
data: str = None,
|
|
164
|
+
url: str = None,
|
|
165
|
+
id: str = None,
|
|
166
|
+
expires_at: int = None,
|
|
167
|
+
**kwargs):
|
|
168
|
+
super().__init__(data=data,
|
|
169
|
+
url=url,
|
|
170
|
+
id=id,
|
|
171
|
+
expires_at=expires_at,
|
|
172
|
+
**kwargs)
|
|
145
173
|
|
|
146
174
|
|
|
147
175
|
@dataclass(init=False)
|
|
@@ -159,7 +187,7 @@ class GenerationOutput(DictMixin):
|
|
|
159
187
|
if choices is not None:
|
|
160
188
|
chs = []
|
|
161
189
|
for choice in choices:
|
|
162
|
-
chs.append(Choice(**choice))
|
|
190
|
+
chs.append(Choice(**choice))
|
|
163
191
|
super().__init__(text=text,
|
|
164
192
|
finish_reason=finish_reason,
|
|
165
193
|
choices=chs,
|
|
@@ -205,23 +233,29 @@ class GenerationResponse(DashScopeAPIResponse):
|
|
|
205
233
|
code=api_response.code,
|
|
206
234
|
message=api_response.message)
|
|
207
235
|
|
|
236
|
+
|
|
208
237
|
@dataclass(init=False)
|
|
209
238
|
class MultiModalConversationOutput(DictMixin):
|
|
210
239
|
choices: List[Choice]
|
|
240
|
+
audio: Audio
|
|
211
241
|
|
|
212
242
|
def __init__(self,
|
|
213
243
|
text: str = None,
|
|
214
244
|
finish_reason: str = None,
|
|
215
245
|
choices: List[Choice] = None,
|
|
246
|
+
audio: Audio = None,
|
|
216
247
|
**kwargs):
|
|
217
248
|
chs = None
|
|
218
249
|
if choices is not None:
|
|
219
250
|
chs = []
|
|
220
251
|
for choice in choices:
|
|
221
|
-
chs.append(Choice(**choice))
|
|
252
|
+
chs.append(Choice(**choice))
|
|
253
|
+
if audio is not None:
|
|
254
|
+
audio = Audio(**audio)
|
|
222
255
|
super().__init__(text=text,
|
|
223
256
|
finish_reason=finish_reason,
|
|
224
257
|
choices=chs,
|
|
258
|
+
audio=audio,
|
|
225
259
|
**kwargs)
|
|
226
260
|
|
|
227
261
|
|
|
@@ -229,14 +263,18 @@ class MultiModalConversationOutput(DictMixin):
|
|
|
229
263
|
class MultiModalConversationUsage(DictMixin):
|
|
230
264
|
input_tokens: int
|
|
231
265
|
output_tokens: int
|
|
266
|
+
characters: int
|
|
267
|
+
|
|
232
268
|
# TODO add image usage info.
|
|
233
269
|
|
|
234
270
|
def __init__(self,
|
|
235
271
|
input_tokens: int = 0,
|
|
236
272
|
output_tokens: int = 0,
|
|
273
|
+
characters: int = 0,
|
|
237
274
|
**kwargs):
|
|
238
275
|
super().__init__(input_tokens=input_tokens,
|
|
239
276
|
output_tokens=output_tokens,
|
|
277
|
+
characters=characters,
|
|
240
278
|
**kwargs)
|
|
241
279
|
|
|
242
280
|
|
|
@@ -260,10 +298,12 @@ class MultiModalConversationResponse(DashScopeAPIResponse):
|
|
|
260
298
|
output=MultiModalConversationOutput(**api_response.output),
|
|
261
299
|
usage=MultiModalConversationUsage(**usage))
|
|
262
300
|
else:
|
|
263
|
-
return MultiModalConversationResponse(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
301
|
+
return MultiModalConversationResponse(
|
|
302
|
+
status_code=api_response.status_code,
|
|
303
|
+
request_id=api_response.request_id,
|
|
304
|
+
code=api_response.code,
|
|
305
|
+
message=api_response.message)
|
|
306
|
+
|
|
267
307
|
|
|
268
308
|
@dataclass(init=False)
|
|
269
309
|
class ConversationResponse(GenerationResponse):
|
|
@@ -366,7 +406,7 @@ class RecognitionResponse(DashScopeAPIResponse):
|
|
|
366
406
|
"""
|
|
367
407
|
result = False
|
|
368
408
|
if sentence is not None and 'end_time' in sentence and sentence[
|
|
369
|
-
|
|
409
|
+
'end_time'] is not None:
|
|
370
410
|
result = True
|
|
371
411
|
return result
|
|
372
412
|
|
|
@@ -433,8 +473,8 @@ class ImageSynthesisOutput(DictMixin):
|
|
|
433
473
|
results: List[ImageSynthesisResult]
|
|
434
474
|
|
|
435
475
|
def __init__(self,
|
|
436
|
-
task_id: str,
|
|
437
|
-
task_status: str,
|
|
476
|
+
task_id: str = None,
|
|
477
|
+
task_status: str = None,
|
|
438
478
|
results: List[ImageSynthesisResult] = [],
|
|
439
479
|
**kwargs):
|
|
440
480
|
res = []
|
|
@@ -448,14 +488,49 @@ class ImageSynthesisOutput(DictMixin):
|
|
|
448
488
|
**kwargs)
|
|
449
489
|
|
|
450
490
|
|
|
491
|
+
@dataclass(init=False)
|
|
492
|
+
class VideoSynthesisOutput(DictMixin):
|
|
493
|
+
task_id: str
|
|
494
|
+
task_status: str
|
|
495
|
+
video_url: str
|
|
496
|
+
|
|
497
|
+
def __init__(self,
|
|
498
|
+
task_id: str,
|
|
499
|
+
task_status: str,
|
|
500
|
+
video_url: str = '',
|
|
501
|
+
**kwargs):
|
|
502
|
+
super().__init__(self,
|
|
503
|
+
task_id=task_id,
|
|
504
|
+
task_status=task_status,
|
|
505
|
+
video_url=video_url,
|
|
506
|
+
**kwargs)
|
|
507
|
+
|
|
508
|
+
|
|
451
509
|
@dataclass(init=False)
|
|
452
510
|
class ImageSynthesisUsage(DictMixin):
|
|
453
511
|
image_count: int
|
|
454
512
|
|
|
455
|
-
def __init__(self, image_count: int, **kwargs):
|
|
513
|
+
def __init__(self, image_count: int = None, **kwargs):
|
|
456
514
|
super().__init__(image_count=image_count, **kwargs)
|
|
457
515
|
|
|
458
516
|
|
|
517
|
+
@dataclass(init=False)
|
|
518
|
+
class VideoSynthesisUsage(DictMixin):
|
|
519
|
+
video_count: int
|
|
520
|
+
video_duration: int
|
|
521
|
+
video_ratio: str
|
|
522
|
+
|
|
523
|
+
def __init__(self,
|
|
524
|
+
video_count: int = 1,
|
|
525
|
+
video_duration: int = 0,
|
|
526
|
+
video_ratio: str = '',
|
|
527
|
+
**kwargs):
|
|
528
|
+
super().__init__(video_count=video_count,
|
|
529
|
+
video_duration=video_duration,
|
|
530
|
+
video_ratio=video_ratio,
|
|
531
|
+
**kwargs)
|
|
532
|
+
|
|
533
|
+
|
|
459
534
|
@dataclass(init=False)
|
|
460
535
|
class ImageSynthesisResponse(DashScopeAPIResponse):
|
|
461
536
|
output: ImageSynthesisOutput
|
|
@@ -483,3 +558,156 @@ class ImageSynthesisResponse(DashScopeAPIResponse):
|
|
|
483
558
|
request_id=api_response.request_id,
|
|
484
559
|
code=api_response.code,
|
|
485
560
|
message=api_response.message)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@dataclass(init=False)
|
|
564
|
+
class VideoSynthesisResponse(DashScopeAPIResponse):
|
|
565
|
+
output: VideoSynthesisOutput
|
|
566
|
+
usage: VideoSynthesisUsage
|
|
567
|
+
|
|
568
|
+
@staticmethod
|
|
569
|
+
def from_api_response(api_response: DashScopeAPIResponse):
|
|
570
|
+
if api_response.status_code == HTTPStatus.OK:
|
|
571
|
+
output = None
|
|
572
|
+
usage = None
|
|
573
|
+
if api_response.output is not None:
|
|
574
|
+
output = VideoSynthesisOutput(**api_response.output)
|
|
575
|
+
if api_response.usage is not None:
|
|
576
|
+
usage = VideoSynthesisUsage(**api_response.usage)
|
|
577
|
+
|
|
578
|
+
return VideoSynthesisResponse(status_code=api_response.status_code,
|
|
579
|
+
request_id=api_response.request_id,
|
|
580
|
+
code=api_response.code,
|
|
581
|
+
message=api_response.message,
|
|
582
|
+
output=output,
|
|
583
|
+
usage=usage)
|
|
584
|
+
|
|
585
|
+
else:
|
|
586
|
+
return VideoSynthesisResponse(status_code=api_response.status_code,
|
|
587
|
+
request_id=api_response.request_id,
|
|
588
|
+
code=api_response.code,
|
|
589
|
+
message=api_response.message)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
@dataclass(init=False)
|
|
593
|
+
class ReRankResult(DictMixin):
|
|
594
|
+
index: int
|
|
595
|
+
relevance_score: float
|
|
596
|
+
document: Dict = None
|
|
597
|
+
|
|
598
|
+
def __init__(self,
|
|
599
|
+
index: int,
|
|
600
|
+
relevance_score: float,
|
|
601
|
+
document: Dict = None,
|
|
602
|
+
**kwargs):
|
|
603
|
+
super().__init__(index=index,
|
|
604
|
+
relevance_score=relevance_score,
|
|
605
|
+
document=document,
|
|
606
|
+
**kwargs)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
@dataclass(init=False)
|
|
610
|
+
class ReRankOutput(DictMixin):
|
|
611
|
+
results: List[ReRankResult]
|
|
612
|
+
|
|
613
|
+
def __init__(self, results: List[ReRankResult] = None, **kwargs):
|
|
614
|
+
ress = None
|
|
615
|
+
if results is not None:
|
|
616
|
+
ress = []
|
|
617
|
+
for res in results:
|
|
618
|
+
ress.append(ReRankResult(**res))
|
|
619
|
+
super().__init__(results=ress, **kwargs)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
@dataclass(init=False)
|
|
623
|
+
class ReRankUsage(DictMixin):
|
|
624
|
+
total_tokens: int
|
|
625
|
+
|
|
626
|
+
def __init__(self, total_tokens=None, **kwargs):
|
|
627
|
+
super().__init__(total_tokens=total_tokens, **kwargs)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
@dataclass(init=False)
|
|
631
|
+
class ReRankResponse(DashScopeAPIResponse):
|
|
632
|
+
output: ReRankOutput
|
|
633
|
+
usage: GenerationUsage
|
|
634
|
+
|
|
635
|
+
@staticmethod
|
|
636
|
+
def from_api_response(api_response: DashScopeAPIResponse):
|
|
637
|
+
if api_response.status_code == HTTPStatus.OK:
|
|
638
|
+
usage = {}
|
|
639
|
+
if api_response.usage:
|
|
640
|
+
usage = api_response.usage
|
|
641
|
+
|
|
642
|
+
return ReRankResponse(status_code=api_response.status_code,
|
|
643
|
+
request_id=api_response.request_id,
|
|
644
|
+
code=api_response.code,
|
|
645
|
+
message=api_response.message,
|
|
646
|
+
output=ReRankOutput(**api_response.output),
|
|
647
|
+
usage=ReRankUsage(**usage))
|
|
648
|
+
else:
|
|
649
|
+
return ReRankResponse(status_code=api_response.status_code,
|
|
650
|
+
request_id=api_response.request_id,
|
|
651
|
+
code=api_response.code,
|
|
652
|
+
message=api_response.message)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
@dataclass(init=False)
|
|
656
|
+
class TextToSpeechAudio(DictMixin):
|
|
657
|
+
expires_at: int
|
|
658
|
+
id: str
|
|
659
|
+
data: str
|
|
660
|
+
url: str
|
|
661
|
+
|
|
662
|
+
def __init__(self,
|
|
663
|
+
expires_at: int,
|
|
664
|
+
id: str,
|
|
665
|
+
data: str = None,
|
|
666
|
+
url: str = None,
|
|
667
|
+
**kwargs):
|
|
668
|
+
super().__init__(expires_at=expires_at,
|
|
669
|
+
id=id,
|
|
670
|
+
data=data,
|
|
671
|
+
url=url,
|
|
672
|
+
**kwargs)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
@dataclass(init=False)
|
|
676
|
+
class TextToSpeechOutput(DictMixin):
|
|
677
|
+
finish_reason: str
|
|
678
|
+
audio: TextToSpeechAudio
|
|
679
|
+
|
|
680
|
+
def __init__(self,
|
|
681
|
+
finish_reason: str = None,
|
|
682
|
+
audio: TextToSpeechAudio = None,
|
|
683
|
+
**kwargs):
|
|
684
|
+
super().__init__(finish_reason=finish_reason,
|
|
685
|
+
audio=audio,
|
|
686
|
+
**kwargs)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
@dataclass(init=False)
|
|
690
|
+
class TextToSpeechResponse(DashScopeAPIResponse):
|
|
691
|
+
output: TextToSpeechOutput
|
|
692
|
+
usage: MultiModalConversationUsage
|
|
693
|
+
|
|
694
|
+
@staticmethod
|
|
695
|
+
def from_api_response(api_response: DashScopeAPIResponse):
|
|
696
|
+
if api_response.status_code == HTTPStatus.OK:
|
|
697
|
+
usage = {}
|
|
698
|
+
if api_response.usage:
|
|
699
|
+
usage = api_response.usage
|
|
700
|
+
|
|
701
|
+
return MultiModalConversationResponse(
|
|
702
|
+
status_code=api_response.status_code,
|
|
703
|
+
request_id=api_response.request_id,
|
|
704
|
+
code=api_response.code,
|
|
705
|
+
message=api_response.message,
|
|
706
|
+
output=TextToSpeechOutput(**api_response.output),
|
|
707
|
+
usage=MultiModalConversationUsage(**usage))
|
|
708
|
+
else:
|
|
709
|
+
return TextToSpeechResponse(
|
|
710
|
+
status_code=api_response.status_code,
|
|
711
|
+
request_id=api_response.request_id,
|
|
712
|
+
code=api_response.code,
|
|
713
|
+
message=api_response.message)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Copyright (c) Alibaba, Inc. and its affiliates.
|
|
2
|
+
import base64
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
10
|
+
from cryptography.hazmat.primitives import serialization, hashes
|
|
11
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
12
|
+
from cryptography.hazmat.backends import default_backend
|
|
13
|
+
|
|
14
|
+
import dashscope
|
|
15
|
+
from dashscope.common.constants import ENCRYPTION_AES_SECRET_KEY_BYTES, ENCRYPTION_AES_IV_LENGTH
|
|
16
|
+
from dashscope.common.logging import logger
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Encryption:
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.pub_key_id: str = ''
|
|
22
|
+
self.pub_key_str: str = ''
|
|
23
|
+
self.aes_key_bytes: bytes = b''
|
|
24
|
+
self.encrypted_aes_key_str: str = ''
|
|
25
|
+
self.iv_bytes: bytes = b''
|
|
26
|
+
self.base64_iv_str: str = ''
|
|
27
|
+
self.valid: bool = False
|
|
28
|
+
|
|
29
|
+
def initialize(self):
|
|
30
|
+
public_keys = self._get_public_keys()
|
|
31
|
+
if not public_keys:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
public_key_str = public_keys.get('public_key')
|
|
35
|
+
public_key_id = public_keys.get('public_key_id')
|
|
36
|
+
if not public_key_str or not public_key_id:
|
|
37
|
+
logger.error("public keys data not valid")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
aes_key_bytes = self._generate_aes_secret_key()
|
|
41
|
+
iv_bytes = self._generate_iv()
|
|
42
|
+
|
|
43
|
+
encrypted_aes_key_str = self._encrypt_aes_key_with_rsa(aes_key_bytes, public_key_str)
|
|
44
|
+
base64_iv_str = base64.b64encode(iv_bytes).decode('utf-8')
|
|
45
|
+
|
|
46
|
+
self.pub_key_id = public_key_id
|
|
47
|
+
self.pub_key_str = public_key_str
|
|
48
|
+
self.aes_key_bytes = aes_key_bytes
|
|
49
|
+
self.encrypted_aes_key_str = encrypted_aes_key_str
|
|
50
|
+
self.iv_bytes = iv_bytes
|
|
51
|
+
self.base64_iv_str = base64_iv_str
|
|
52
|
+
|
|
53
|
+
self.valid = True
|
|
54
|
+
|
|
55
|
+
def encrypt(self, dict_plaintext):
|
|
56
|
+
return self._encrypt_text_with_aes(json.dumps(dict_plaintext, ensure_ascii=False),
|
|
57
|
+
self.aes_key_bytes, self.iv_bytes)
|
|
58
|
+
|
|
59
|
+
def decrypt(self, base64_ciphertext):
|
|
60
|
+
return self._decrypt_text_with_aes(base64_ciphertext, self.aes_key_bytes, self.iv_bytes)
|
|
61
|
+
|
|
62
|
+
def is_valid(self):
|
|
63
|
+
return self.valid
|
|
64
|
+
|
|
65
|
+
def get_pub_key_id(self):
|
|
66
|
+
return self.pub_key_id
|
|
67
|
+
|
|
68
|
+
def get_encrypted_aes_key_str(self):
|
|
69
|
+
return self.encrypted_aes_key_str
|
|
70
|
+
|
|
71
|
+
def get_base64_iv_str(self):
|
|
72
|
+
return self.base64_iv_str
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _get_public_keys():
|
|
76
|
+
url = dashscope.base_http_api_url + '/public-keys/latest'
|
|
77
|
+
headers = {
|
|
78
|
+
"Authorization": f"Bearer {dashscope.api_key}"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
response = requests.get(url, headers=headers)
|
|
82
|
+
if response.status_code != 200:
|
|
83
|
+
logger.error("exceptional public key response: %s" % response)
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
json_resp = response.json()
|
|
87
|
+
response_data = json_resp.get('data')
|
|
88
|
+
|
|
89
|
+
if not response_data:
|
|
90
|
+
logger.error("no valid data in public key response")
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
return response_data
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _generate_aes_secret_key():
|
|
97
|
+
return os.urandom(ENCRYPTION_AES_SECRET_KEY_BYTES)
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def _generate_iv():
|
|
101
|
+
return os.urandom(ENCRYPTION_AES_IV_LENGTH)
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def _encrypt_text_with_aes(plaintext, key, iv):
|
|
105
|
+
"""使用AES-GCM加密数据"""
|
|
106
|
+
|
|
107
|
+
# 创建AES-GCM加密器
|
|
108
|
+
aes_gcm = Cipher(
|
|
109
|
+
algorithms.AES(key),
|
|
110
|
+
modes.GCM(iv, tag=None),
|
|
111
|
+
backend=default_backend()
|
|
112
|
+
).encryptor()
|
|
113
|
+
|
|
114
|
+
# 关联数据设为空(根据需求可调整)
|
|
115
|
+
aes_gcm.authenticate_additional_data(b'')
|
|
116
|
+
|
|
117
|
+
# 加密数据
|
|
118
|
+
ciphertext = aes_gcm.update(plaintext.encode('utf-8')) + aes_gcm.finalize()
|
|
119
|
+
|
|
120
|
+
# 获取认证标签
|
|
121
|
+
tag = aes_gcm.tag
|
|
122
|
+
|
|
123
|
+
# 组合密文和标签
|
|
124
|
+
encrypted_data = ciphertext + tag
|
|
125
|
+
|
|
126
|
+
# 返回Base64编码结果
|
|
127
|
+
return base64.b64encode(encrypted_data).decode('utf-8')
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def _decrypt_text_with_aes(base64_ciphertext, aes_key, iv):
|
|
131
|
+
"""使用AES-GCM解密响应"""
|
|
132
|
+
|
|
133
|
+
# 解码Base64数据
|
|
134
|
+
encrypted_data = base64.b64decode(base64_ciphertext)
|
|
135
|
+
|
|
136
|
+
# 分离密文和标签(标签长度16字节)
|
|
137
|
+
ciphertext = encrypted_data[:-16]
|
|
138
|
+
tag = encrypted_data[-16:]
|
|
139
|
+
|
|
140
|
+
# 创建AES-GCM解密器
|
|
141
|
+
aes_gcm = Cipher(
|
|
142
|
+
algorithms.AES(aes_key),
|
|
143
|
+
modes.GCM(iv, tag),
|
|
144
|
+
backend=default_backend()
|
|
145
|
+
).decryptor()
|
|
146
|
+
|
|
147
|
+
# 验证关联数据(与加密时一致)
|
|
148
|
+
aes_gcm.authenticate_additional_data(b'')
|
|
149
|
+
|
|
150
|
+
# 解密数据
|
|
151
|
+
decrypted_bytes = aes_gcm.update(ciphertext) + aes_gcm.finalize()
|
|
152
|
+
|
|
153
|
+
# 明文
|
|
154
|
+
plaintext = decrypted_bytes.decode('utf-8')
|
|
155
|
+
|
|
156
|
+
return json.loads(plaintext)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _encrypt_aes_key_with_rsa(aes_key, public_key_str):
|
|
160
|
+
"""使用RSA公钥加密AES密钥"""
|
|
161
|
+
|
|
162
|
+
# 解码Base64格式的公钥
|
|
163
|
+
public_key_bytes = base64.b64decode(public_key_str)
|
|
164
|
+
|
|
165
|
+
# 加载公钥
|
|
166
|
+
public_key = serialization.load_der_public_key(
|
|
167
|
+
public_key_bytes,
|
|
168
|
+
backend=default_backend()
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
base64_aes_key = base64.b64encode(aes_key).decode('utf-8')
|
|
172
|
+
|
|
173
|
+
# 使用RSA加密
|
|
174
|
+
encrypted_bytes = public_key.encrypt(
|
|
175
|
+
base64_aes_key.encode('utf-8'),
|
|
176
|
+
padding.PKCS1v15()
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return base64.b64encode(encrypted_bytes).decode('utf-8')
|