From c80c28e79c09c225cab1a7302bfc6d388a6cf211 Mon Sep 17 00:00:00 2001 From: chyroc Date: Mon, 6 Jan 2025 10:27:29 +0800 Subject: [PATCH] ws --- .gitignore | 1 + cozepy/__init__.py | 4 +- cozepy/audio/__init__.py | 28 +- .../__init__.py | 8 +- cozepy/audio/voices/__init__.py | 8 +- cozepy/chat/__init__.py | 24 +- cozepy/coze.py | 10 + cozepy/util.py | 10 +- cozepy/websockets/__init__.py | 29 ++ cozepy/websockets/audio/__init__.py | 28 ++ cozepy/websockets/audio/speech/__init__.py | 191 ++++++++++++ .../audio/transcriptions/__init__.py | 167 +++++++++++ cozepy/websockets/chat/__init__.py | 158 ++++++++++ cozepy/websockets/ws.py | 237 +++++++++++++++ examples/chat_stream.py | 5 +- examples/utils/__init__.py | 28 ++ examples/websockets_audio_speech.py | 63 ++++ examples/websockets_audio_transcriptions.py | 83 +++++ examples/websockets_chat.py | 109 +++++++ poetry.lock | 283 +++++++++++++++--- pyproject.toml | 8 + tests/test_audio_translations.py | 6 +- 22 files changed, 1410 insertions(+), 78 deletions(-) rename cozepy/audio/{translations => transcriptions}/__init__.py (90%) create mode 100644 cozepy/websockets/__init__.py create mode 100644 cozepy/websockets/audio/__init__.py create mode 100644 cozepy/websockets/audio/speech/__init__.py create mode 100644 cozepy/websockets/audio/transcriptions/__init__.py create mode 100644 cozepy/websockets/chat/__init__.py create mode 100644 cozepy/websockets/ws.py create mode 100644 examples/utils/__init__.py create mode 100644 examples/websockets_audio_speech.py create mode 100644 examples/websockets_audio_transcriptions.py create mode 100644 examples/websockets_chat.py diff --git a/.gitignore b/.gitignore index de1896b..2dd1059 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ dist/ .env_private scripts/ .cache/ +output.wav diff --git a/cozepy/__init__.py b/cozepy/__init__.py index 70a0ab8..aa121aa 100644 --- a/cozepy/__init__.py +++ b/cozepy/__init__.py @@ -1,6 +1,6 @@ from .audio.rooms import CreateRoomResp from .audio.speech import AudioFormat -from .audio.translations import CreateTranslationResp +from .audio.transcriptions import CreateTranslationResp from .audio.voices import Voice from .auth import ( AsyncDeviceOAuthApp, @@ -110,7 +110,7 @@ # audio.voices "Voice", "AudioFormat", - # audio.translations + # audio.transcriptions "CreateTranslationResp", # auth "AsyncDeviceOAuthApp", diff --git a/cozepy/audio/__init__.py b/cozepy/audio/__init__.py index d919615..58cab7c 100644 --- a/cozepy/audio/__init__.py +++ b/cozepy/audio/__init__.py @@ -7,7 +7,7 @@ if TYPE_CHECKING: from .rooms import AsyncRoomsClient, RoomsClient from .speech import AsyncSpeechClient, SpeechClient - from .translations import AsyncTranslationsClient, TranslationsClient + from .transcriptions import AsyncTranscriptionsClient, TranscriptionsClient from .voices import AsyncVoicesClient, VoicesClient @@ -20,7 +20,7 @@ def __init__(self, base_url: str, auth: Auth, requester: Requester): self._rooms: Optional[RoomsClient] = None self._voices: Optional[VoicesClient] = None self._speech: Optional[SpeechClient] = None - self._translations: Optional[TranslationsClient] = None + self._transcriptions: Optional[TranscriptionsClient] = None @property def rooms(self) -> "RoomsClient": @@ -39,12 +39,14 @@ def speech(self) -> "SpeechClient": return self._speech @property - def translations(self) -> "TranslationsClient": - if self._translations is None: - from .translations import TranslationsClient + def transcriptions(self) -> "TranscriptionsClient": + if self._transcriptions is None: + from .transcriptions import TranscriptionsClient - self._translations = TranslationsClient(base_url=self._base_url, auth=self._auth, requester=self._requester) - return self._translations + self._transcriptions = TranscriptionsClient( + base_url=self._base_url, auth=self._auth, requester=self._requester + ) + return self._transcriptions @property def voices(self) -> "VoicesClient": @@ -64,7 +66,7 @@ def __init__(self, base_url: str, auth: Auth, requester: Requester): self._rooms: Optional[AsyncRoomsClient] = None self._voices: Optional[AsyncVoicesClient] = None self._speech: Optional[AsyncSpeechClient] = None - self._translations: Optional[AsyncTranslationsClient] = None + self._transcriptions: Optional[AsyncTranscriptionsClient] = None @property def rooms(self) -> "AsyncRoomsClient": @@ -91,11 +93,11 @@ def voices(self) -> "AsyncVoicesClient": return self._voices @property - def translations(self) -> "AsyncTranslationsClient": - if self._translations is None: - from .translations import AsyncTranslationsClient + def transcriptions(self) -> "AsyncTranscriptionsClient": + if self._transcriptions is None: + from .transcriptions import AsyncTranscriptionsClient - self._translations = AsyncTranslationsClient( + self._transcriptions = AsyncTranscriptionsClient( base_url=self._base_url, auth=self._auth, requester=self._requester ) - return self._translations + return self._transcriptions diff --git a/cozepy/audio/translations/__init__.py b/cozepy/audio/transcriptions/__init__.py similarity index 90% rename from cozepy/audio/translations/__init__.py rename to cozepy/audio/transcriptions/__init__.py index d4c72a9..7f37181 100644 --- a/cozepy/audio/translations/__init__.py +++ b/cozepy/audio/transcriptions/__init__.py @@ -12,7 +12,7 @@ class CreateTranslationResp(CozeModel): text: str -class TranslationsClient(object): +class TranscriptionsClient(object): def __init__(self, base_url: str, auth: Auth, requester: Requester): self._base_url = remove_url_trailing_slash(base_url) self._auth = auth @@ -30,7 +30,7 @@ def create( :param file: The file to be translated. :return: create translation result """ - url = f"{self._base_url}/v1/audio/translations" + url = f"{self._base_url}/v1/audio/transcriptions" headers: Optional[dict] = kwargs.get("headers") files = {"file": _try_fix_file(file)} return self._requester.request( @@ -38,7 +38,7 @@ def create( ) -class AsyncTranslationsClient(object): +class AsyncTranscriptionsClient(object): """ Room service async client. """ @@ -60,7 +60,7 @@ async def create( :param file: The file to be translated. :return: create translation result """ - url = f"{self._base_url}/v1/audio/translations" + url = f"{self._base_url}/v1/audio/transcriptions" files = {"file": _try_fix_file(file)} headers: Optional[dict] = kwargs.get("headers") return await self._requester.arequest( diff --git a/cozepy/audio/voices/__init__.py b/cozepy/audio/voices/__init__.py index 17787c9..dcfb65b 100644 --- a/cozepy/audio/voices/__init__.py +++ b/cozepy/audio/voices/__init__.py @@ -210,11 +210,7 @@ async def clone( return await self._requester.arequest("post", url, False, Voice, headers=headers, body=body, files=files) async def list( - self, - *, - filter_system_voice: bool = False, - page_num: int = 1, - page_size: int = 100, + self, *, filter_system_voice: bool = False, page_num: int = 1, page_size: int = 100, **kwargs ) -> AsyncNumberPaged[Voice]: """ Get available voices, including system voices + user cloned voices @@ -227,6 +223,7 @@ async def list( :return: list of Voice """ url = f"{self._base_url}/v1/audio/voices" + headers: Optional[dict] = kwargs.get("headers") def request_maker(i_page_num: int, i_page_size: int) -> HTTPRequest: return self._requester.make_request( @@ -237,6 +234,7 @@ def request_maker(i_page_num: int, i_page_size: int) -> HTTPRequest: "page_num": i_page_num, "page_size": i_page_size, }, + headers=headers, cast=_PrivateListVoiceData, is_async=False, stream=False, diff --git a/cozepy/chat/__init__.py b/cozepy/chat/__init__.py index a0a96fe..e22eb23 100644 --- a/cozepy/chat/__init__.py +++ b/cozepy/chat/__init__.py @@ -1,3 +1,4 @@ +import base64 import json import time from enum import Enum @@ -24,6 +25,8 @@ class MessageRole(str, Enum): class MessageType(str, Enum): + UNKNOWN = "" + # User input content. # 用户输入内容。 QUESTION = "question" @@ -52,8 +55,6 @@ class MessageType(str, Enum): # 多 answer 场景下,服务端会返回一个 verbose 包,对应的 content 为 JSON 格式,content.msg_type =generate_answer_finish 代表全部 answer 回复完成。不支持在请求中作为入参。 VERBOSE = "verbose" - UNKNOWN = "" - class MessageContentType(str, Enum): # Text. @@ -187,12 +188,19 @@ def build_assistant_answer(content: str, meta_data: Optional[Dict[str, str]] = N meta_data=meta_data, ) + def get_audio(self) -> Optional[bytes]: + if self.content_type == MessageContentType.AUDIO or self.type == MessageContentType.AUDIO: + return base64.b64decode(self.content) + return None + class ChatStatus(str, Enum): """ The running status of the session """ + UNKNOWN = "" + # The session has been created. CREATED = "created" @@ -214,9 +222,9 @@ class ChatStatus(str, Enum): class ChatError(CozeModel): # The error code. An integer type. 0 indicates success, other values indicate failure. - code: int + code: int = 0 # The error message. A string type. - msg: str + msg: str = "" class ChatRequiredActionType(str, Enum): @@ -266,13 +274,13 @@ class ChatRequiredAction(CozeModel): class ChatUsage(CozeModel): # The total number of Tokens consumed in this chat, including the consumption for both the input # and output parts. - token_count: int + token_count: int = 0 # The total number of Tokens consumed for the output part. - output_count: int + output_count: int = 0 # The total number of Tokens consumed for the input part. - input_count: int + input_count: int = 0 class Chat(CozeModel): @@ -301,7 +309,7 @@ class Chat(CozeModel): # completed: The Bot has finished processing, and the session has ended. # failed: The session has failed. # requires_action: The session is interrupted and requires further processing. - status: ChatStatus + status: ChatStatus = ChatStatus.UNKNOWN # Details of the information needed for execution. required_action: Optional[ChatRequiredAction] = None diff --git a/cozepy/coze.py b/cozepy/coze.py index e6a851f..4535ef6 100644 --- a/cozepy/coze.py +++ b/cozepy/coze.py @@ -15,6 +15,7 @@ from .files import AsyncFilesClient, FilesClient from .knowledge import AsyncKnowledgeClient, KnowledgeClient # deprecated from .templates import AsyncTemplatesClient, TemplatesClient + from .websockets import AsyncWebsocketsClient from .workflows import AsyncWorkflowsClient, WorkflowsClient from .workspaces import AsyncWorkspacesClient, WorkspacesClient @@ -151,6 +152,7 @@ def __init__( self._workspaces: Optional[AsyncWorkspacesClient] = None self._audio: Optional[AsyncAudioClient] = None self._templates: Optional[AsyncTemplatesClient] = None + self._websockets: Optional[AsyncWebsocketsClient] = None @property def bots(self) -> "AsyncBotsClient": @@ -237,3 +239,11 @@ def templates(self) -> "AsyncTemplatesClient": self._templates = AsyncTemplatesClient(self._base_url, self._auth, self._requester) return self._templates + + @property + def websockets(self) -> "AsyncWebsocketsClient": + if not self._websockets: + from .websockets import AsyncWebsocketsClient + + self._websockets = AsyncWebsocketsClient(self._base_url, self._auth, self._requester) + return self._websockets diff --git a/cozepy/util.py b/cozepy/util.py index f7d33c9..e177c67 100644 --- a/cozepy/util.py +++ b/cozepy/util.py @@ -44,6 +44,14 @@ def remove_url_trailing_slash(base_url: str) -> str: return base_url +def http_base_url_to_ws(base_url: str) -> str: + base_url = base_url.replace("https://", "wss://") + + if "api-" in base_url: + return base_url.replace("api-", "ws-") + return base_url.replace("api.", "ws.") + + def remove_none_values(d: dict) -> dict: return {k: v for k, v in d.items() if v is not None} @@ -55,7 +63,7 @@ def write_pcm_to_wav_file( Save PCM binary data to WAV file :param pcm_data: PCM binary data (24kHz, 16-bit, 1 channel, little-endian) - :param output_filename: Output WAV filename + :param filepath: Output WAV filename """ with wave.open(filepath, "wb") as wav_file: diff --git a/cozepy/websockets/__init__.py b/cozepy/websockets/__init__.py new file mode 100644 index 0000000..d433773 --- /dev/null +++ b/cozepy/websockets/__init__.py @@ -0,0 +1,29 @@ +from cozepy import Auth +from cozepy.request import Requester +from cozepy.util import http_base_url_to_ws, remove_url_trailing_slash + +from .audio import AsyncWebsocketsAudioClient +from .chat import AsyncWebsocketsChatClient + + +class AsyncWebsocketsClient(object): + def __init__(self, base_url: str, auth: Auth, requester: Requester): + self._base_url = http_base_url_to_ws(remove_url_trailing_slash(base_url)) + self._auth = auth + self._requester = requester + + @property + def audio(self) -> AsyncWebsocketsAudioClient: + return AsyncWebsocketsAudioClient( + base_url=self._base_url, + auth=self._auth, + requester=self._requester, + ) + + @property + def chat(self) -> AsyncWebsocketsChatClient: + return AsyncWebsocketsChatClient( + base_url=self._base_url, + auth=self._auth, + requester=self._requester, + ) diff --git a/cozepy/websockets/audio/__init__.py b/cozepy/websockets/audio/__init__.py new file mode 100644 index 0000000..3f05e92 --- /dev/null +++ b/cozepy/websockets/audio/__init__.py @@ -0,0 +1,28 @@ +from cozepy.auth import Auth +from cozepy.request import Requester + +from .speech import AsyncWebsocketsAudioSpeechClient +from .transcriptions import AsyncWebsocketsAudioTranscriptionsClient + + +class AsyncWebsocketsAudioClient(object): + def __init__(self, base_url: str, auth: Auth, requester: Requester): + self._base_url = base_url + self._auth = auth + self._requester = requester + + @property + def transcriptions(self) -> "AsyncWebsocketsAudioTranscriptionsClient": + return AsyncWebsocketsAudioTranscriptionsClient( + base_url=self._base_url, + auth=self._auth, + requester=self._requester, + ) + + @property + def speech(self) -> "AsyncWebsocketsAudioSpeechClient": + return AsyncWebsocketsAudioSpeechClient( + base_url=self._base_url, + auth=self._auth, + requester=self._requester, + ) diff --git a/cozepy/websockets/audio/speech/__init__.py b/cozepy/websockets/audio/speech/__init__.py new file mode 100644 index 0000000..f81eba7 --- /dev/null +++ b/cozepy/websockets/audio/speech/__init__.py @@ -0,0 +1,191 @@ +import base64 +from typing import Callable, Dict, Optional + +from cozepy.auth import Auth +from cozepy.log import log_warning +from cozepy.request import Requester +from cozepy.util import remove_url_trailing_slash +from cozepy.websockets.ws import AsyncWebsocketsBaseClient, Event, EventType + + +class InputTextBufferAppendEvent(Event): + delta: str + + @staticmethod + def load(delta: str, event_id: Optional[str] = None) -> "InputTextBufferAppendEvent": + return InputTextBufferAppendEvent.model_validate( + { + "event_id": event_id, + "type": EventType.INPUT_TEXT_BUFFER_APPEND, + "delta": delta, + } + ) + + def dump(self): + return { + "event_id": self.event_id, + "type": self.type.value, + "data": {"delta": self.delta}, + } + + +class InputTextBufferCommitEvent(Event): + @staticmethod + def load(data: None, event_id: Optional[str] = None) -> "InputTextBufferCommitEvent": + return InputTextBufferCommitEvent.model_validate( + { + "event_id": event_id, + "type": EventType.INPUT_TEXT_BUFFER_COMMIT, + "data": None, + } + ) + + def dump(self): + return { + "event_id": self.event_id, + "type": self.type.value, + "data": None, + } + + +class SpeechUpdateEventOpusConfig(object): + bitrate: Optional[int] = None + use_cbr: Optional[bool] = None + frame_size_ms: Optional[float] = None + + def dump(self): + return { + "bitrate": self.bitrate, + "use_cbr": self.use_cbr, + "frame_size_ms": self.frame_size_ms, + } + + +class SpeechUpdateEventPCMConfig(object): + sample_rate: Optional[int] = None + + def dump(self): + return { + "sample_rate": self.sample_rate, + } + + +class SpeechUpdateEventOutputAudio(object): + codec: Optional[str] + pcm_config: Optional[SpeechUpdateEventPCMConfig] + opus_config: Optional[SpeechUpdateEventOpusConfig] + speech_rate: Optional[int] + voice_id: Optional[str] + + def dump(self): + return { + "codec": self.codec, + "pcm_config": self.pcm_config.dump(), + "opus_config": self.opus_config.dump(), + "speech_rate": self.speech_rate, + "voice_id": self.voice_id, + } + + +class SpeechUpdateEvent(Event): + output_audio: SpeechUpdateEventOutputAudio + + @staticmethod + def load( + output_audio: Optional[SpeechUpdateEventOutputAudio] = None, event_id: Optional[str] = None + ) -> "SpeechUpdateEvent": + return SpeechUpdateEvent.model_validate( + { + "event_id": event_id, + "type": EventType.SPEECH_UPDATE, + "data": { + "output_audio": output_audio, + }, + } + ) + + def dump(self): + return { + "event_id": self.event_id, + "type": EventType.SPEECH_UPDATE, + "data": { + "output_audio": self.output_audio.dump(), + }, + } + + +class SpeechAudioCompletedEvent(Event): + @staticmethod + def load(data: None, event_id: Optional[str] = None) -> "SpeechAudioCompletedEvent": + return SpeechAudioCompletedEvent.model_validate( + {"event_id": event_id, "type": EventType.SPEECH_AUDIO_COMPLETED} + ) + + def dump(self): + return { + "event_id": self.event_id, + "type": self.type.value, + } + + +class SpeechAudioDeltaEvent(Event): + delta: bytes + + @staticmethod + def load(delta: bytes, event_id: Optional[str] = None) -> "SpeechAudioDeltaEvent": + return SpeechAudioDeltaEvent.model_validate( + { + "event_id": event_id, + "type": EventType.SPEECH_AUDIO_UPDATE, + "delta": delta, + } + ) + + def dump(self): + return {"event_id": self.event_id, "type": self.type.value, "data": {"delta": self.delta}} + + +class AsyncWebsocketsAudioSpeechCreateClient(AsyncWebsocketsBaseClient): + def __init__(self, base_url: str, auth: Auth, requester: Requester, on_event: Dict[EventType, Callable], **kwargs): + super().__init__( + base_url=base_url, auth=auth, requester=requester, path="v1/audio/speech", on_event=on_event, **kwargs + ) + + async def append(self, text: str) -> None: + await self._input_queue.put(InputTextBufferAppendEvent.load(text)) + + async def commit(self) -> None: + await self._input_queue.put(InputTextBufferCommitEvent.load(None)) + + async def update(self, event: SpeechUpdateEvent) -> None: + await self._input_queue.put(event) + + def _load_event(self, message: Dict) -> Event: + event_id = message.get("event_id") or "" + event_type = message.get("type") or "" + data = message.get("data") or {} + if event_type == EventType.SPEECH_AUDIO_UPDATE.value: + delta = base64.b64decode(data.get("delta")) + return SpeechAudioDeltaEvent.load(delta, event_id) + elif event_type == EventType.SPEECH_AUDIO_COMPLETED.value: + return SpeechAudioCompletedEvent.load(None, event_id) + elif event_type == EventType.INPUT_AUDIO_BUFFER_COMMITTED.value: + pass + else: + log_warning("[%s] unknown event=%s", self._path, event_type) + + +class AsyncWebsocketsAudioSpeechClient: + def __init__(self, base_url: str, auth: Auth, requester: Requester): + self._base_url = remove_url_trailing_slash(base_url) + self._auth = auth + self._requester = requester + + def create(self, *, on_event: Dict[EventType, Callable], **kwargs) -> AsyncWebsocketsAudioSpeechCreateClient: + return AsyncWebsocketsAudioSpeechCreateClient( + base_url=self._base_url, + auth=self._auth, + requester=self._requester, + on_event=on_event, + **kwargs, + ) diff --git a/cozepy/websockets/audio/transcriptions/__init__.py b/cozepy/websockets/audio/transcriptions/__init__.py new file mode 100644 index 0000000..ee04d07 --- /dev/null +++ b/cozepy/websockets/audio/transcriptions/__init__.py @@ -0,0 +1,167 @@ +import base64 +from typing import Any, Callable, Dict, Optional + +from cozepy.auth import Auth +from cozepy.log import log_warning +from cozepy.request import Requester +from cozepy.util import remove_url_trailing_slash +from cozepy.websockets.ws import AsyncWebsocketsBaseClient, Event, EventType +from debug import CozeModel + + +class InputAudioBufferAppendEvent(Event): + chunk: bytes + + @staticmethod + def load(chunk: bytes, event_id: Optional[str] = None) -> "InputAudioBufferAppendEvent": + return InputAudioBufferAppendEvent.model_validate( + { + "event_id": event_id, + "type": EventType.INPUT_AUDIO_BUFFER_APPEND, + "chunk": chunk, + } + ) + + def dump(self): + return { + "event_id": self.event_id, + "type": self.type.value, + "data": {"delta": base64.b64encode(self.chunk).decode("utf-8")}, + } + + +class InputAudioBufferCommitEvent(Event): + @staticmethod + def load(data: Any, event_id: Optional[str] = None) -> "InputAudioBufferCommitEvent": + return InputAudioBufferCommitEvent.model_validate( + { + "event_id": event_id, + "type": EventType.INPUT_AUDIO_BUFFER_COMMIT, + } + ) + + def dump(self): + return { + "event_id": self.event_id, + "type": self.type.value, + } + + +class TranscriptionsUpdateEventInputAudio(CozeModel): + format: Optional[str] + codec: Optional[str] + sample_rate: Optional[int] + channel: Optional[int] + bit_depth: Optional[int] + + def dump(self): + return { + "format": self.format, + "codec": self.codec, + "sample_rate": self.sample_rate, + "channel": self.channel, + "bit_depth": self.bit_depth, + } + + +class TranscriptionsUpdateEvent(Event): + input_audio: TranscriptionsUpdateEventInputAudio + + @staticmethod + def load( + input_audio: TranscriptionsUpdateEventInputAudio, event_id: Optional[str] = None + ) -> "TranscriptionsUpdateEvent": + return TranscriptionsUpdateEvent.model_validate( + {"event_id": event_id, "type": EventType.TRANSCRIPTIONS_UPDATE, "input_audio": input_audio} + ) + + def dump(self): + return { + "event_id": self.event_id, + "type": self.type.value, + "data": {"input_audio": self.input_audio.dump()}, + } + + +class TranscriptionsMessageCompletedEvent(Event): + @staticmethod + def load(data: None, event_id: Optional[str] = None) -> "TranscriptionsMessageCompletedEvent": + return TranscriptionsMessageCompletedEvent.model_validate( + {"event_id": event_id, "type": EventType.TRANSCRIPTIONS_MESSAGE_COMPLETED} + ) + + def dump(self): + return { + "event_id": self.event_id, + "type": self.type.value, + } + + +class TranscriptionsMessageUpdateEvent(Event): + content: str + + @staticmethod + def load(content: str, event_id: Optional[str] = None) -> "TranscriptionsMessageUpdateEvent": + return TranscriptionsMessageUpdateEvent.model_validate( + {"event_id": event_id, "type": EventType.TRANSCRIPTIONS_MESSAGE_UPDATE, "content": content} + ) + + def dump(self): + return { + "event_id": self.event_id, + "type": self.type.value, + "data": {"content": self.content}, + } + + +class AsyncWebsocketsAudioTranscriptionsCreateClient(AsyncWebsocketsBaseClient): + def __init__(self, base_url: str, auth: Auth, requester: Requester, on_event: Dict[EventType, Callable], **kwargs): + super().__init__( + base_url=base_url, + auth=auth, + requester=requester, + path="v1/audio/transcriptions", + on_event=on_event, + wait_events=[EventType.TRANSCRIPTIONS_MESSAGE_COMPLETED], + **kwargs, + ) + + async def update(self, event: TranscriptionsUpdateEventInputAudio) -> None: + await self._input_queue.put(TranscriptionsUpdateEvent.load(event)) + + async def append(self, chunk: bytes) -> None: + await self._input_queue.put(InputAudioBufferAppendEvent.load(chunk)) + + async def commit(self) -> None: + await self._input_queue.put(InputAudioBufferCommitEvent.load(None)) + + def _load_event(self, message: Dict) -> Event: + event_id = message.get("event_id") or "" + event_type = message.get("type") or "" + data = message.get("data") or {} + if event_type == EventType.TRANSCRIPTIONS_MESSAGE_COMPLETED.value: + return TranscriptionsMessageCompletedEvent.load(None, event_id) + elif event_type == EventType.TRANSCRIPTIONS_MESSAGE_UPDATE.value: + return TranscriptionsMessageUpdateEvent.load(data.get("content"), event_id) + elif event_type == EventType.INPUT_AUDIO_BUFFER_COMMITTED.value: + pass + else: + log_warning("[v1/audio/transcriptions] unknown event=%s", event_type) + + +class AsyncWebsocketsAudioTranscriptionsClient: + def __init__(self, base_url: str, auth: Auth, requester: Requester): + self._base_url = remove_url_trailing_slash(base_url) + self._auth = auth + self._requester = requester + + def create( + self, *, on_event: Dict[EventType, Callable], **kwargs + ) -> AsyncWebsocketsAudioTranscriptionsCreateClient: + return AsyncWebsocketsAudioTranscriptionsCreateClient( + base_url=self._base_url, + auth=self._auth, + requester=self._requester, + on_event=on_event, + **kwargs, + ) diff --git a/cozepy/websockets/chat/__init__.py b/cozepy/websockets/chat/__init__.py new file mode 100644 index 0000000..3363d26 --- /dev/null +++ b/cozepy/websockets/chat/__init__.py @@ -0,0 +1,158 @@ +from typing import Callable, Dict, Optional + +from cozepy import Chat, Message +from cozepy.auth import Auth +from cozepy.log import log_warning +from cozepy.request import Requester +from cozepy.util import remove_url_trailing_slash +from cozepy.websockets.audio.transcriptions import InputAudioBufferAppendEvent, InputAudioBufferCommitEvent +from cozepy.websockets.ws import AsyncWebsocketsBaseClient, Event, EventType + + +class ConversationChatCreatedEvent(Event): + data: Chat + + @staticmethod + def load(data: Chat, event_id: Optional[str] = None) -> "ConversationChatCreatedEvent": + return ConversationChatCreatedEvent.model_validate( + { + "event_id": event_id, + "type": EventType.CONVERSATION_CHAT_CREATED, + "data": data, + } + ) + + def dump(self): + return { + "event_id": self.event_id, + "type": self.type.value, + "data": self.data.model_dump(), + } + + +class ConversationMessageDeltaEvent(Event): + data: Message + + @staticmethod + def load(data: Message, event_id: Optional[str] = None) -> "ConversationMessageDeltaEvent": + return ConversationMessageDeltaEvent.model_validate( + { + "event_id": event_id, + "type": EventType.CONVERSATION_MESSAGE_DELTA, + "data": data, + } + ) + + def dump(self): + return { + "event_id": self.event_id, + "type": self.type.value, + "data": self.data.model_dump(), + } + + +class ConversationMessageCompletedEvent(Event): + data: Chat + + @staticmethod + def load(data: Chat, event_id: Optional[str] = None) -> "ConversationMessageCompletedEvent": + return ConversationMessageCompletedEvent.model_validate( + { + "event_id": event_id, + "type": EventType.CONVERSATION_CHAT_COMPLETED, + "data": data, + } + ) + + def dump(self): + return { + "event_id": self.event_id, + "type": self.type.value, + "data": self.data.model_dump(), + } + + +class ConversationAudioDeltaEvent(Event): + data: Message + + @staticmethod + def load(data: Message, event_id: Optional[str] = None) -> "ConversationAudioDeltaEvent": + return ConversationAudioDeltaEvent.model_validate( + { + "event_id": event_id, + "type": EventType.CONVERSATION_AUDIO_DELTA, + "data": data, + } + ) + + def dump(self): + return {"event_id": self.event_id, "type": self.type.value, "data": self.data.model_dump()} + + +class AsyncWebsocketsChatCreateClient(AsyncWebsocketsBaseClient): + def __init__( + self, + base_url: str, + auth: Auth, + requester: Requester, + bot_id: str, + on_event: Dict[EventType, Callable], + **kwargs, + ): + super().__init__( + base_url=base_url, + auth=auth, + requester=requester, + path="v1/chat", + query={ + "bot_id": bot_id, + }, + on_event=on_event, + wait_events=[EventType.CONVERSATION_CHAT_COMPLETED], + **kwargs, + ) + + # async def update(self, event: TranscriptionsUpdateEventInputAudio) -> None: + # await self._input_queue.put(TranscriptionsUpdateEvent.load(event)) + + async def append(self, chunk: bytes) -> None: + await self._input_queue.put(InputAudioBufferAppendEvent.load(chunk)) + + async def commit(self) -> None: + await self._input_queue.put(InputAudioBufferCommitEvent.load(None)) + + def _load_event(self, message: Dict) -> Event: + event_id = message.get("event_id") or "" + event_type = message.get("type") or "" + data = message.get("data") or {} + if event_type == EventType.CONVERSATION_CHAT_CREATED.value: + chat = Chat.model_validate(data) + return ConversationChatCreatedEvent.load(chat, event_id) + if event_type == EventType.CONVERSATION_MESSAGE_DELTA.value: + msg = Message.model_validate(data) + return ConversationMessageDeltaEvent.load(msg, event_id) + elif event_type == EventType.CONVERSATION_CHAT_COMPLETED.value: + chat = Chat.model_validate(data) + return ConversationMessageCompletedEvent.load(chat, event_id) + elif event_type == EventType.CONVERSATION_AUDIO_DELTA.value: + msg = Message.model_validate(data) + return ConversationAudioDeltaEvent.load(msg, event_id) + else: + log_warning("[%s] unknown event=%s", self._path, event_type) + + +class AsyncWebsocketsChatClient: + def __init__(self, base_url: str, auth: Auth, requester: Requester): + self._base_url = remove_url_trailing_slash(base_url) + self._auth = auth + self._requester = requester + + def create(self, *, bot_id: str, on_event: Dict[EventType, Callable], **kwargs) -> AsyncWebsocketsChatCreateClient: + return AsyncWebsocketsChatCreateClient( + base_url=self._base_url, + auth=self._auth, + requester=self._requester, + bot_id=bot_id, + on_event=on_event, + **kwargs, + ) diff --git a/cozepy/websockets/ws.py b/cozepy/websockets/ws.py new file mode 100644 index 0000000..3f33770 --- /dev/null +++ b/cozepy/websockets/ws.py @@ -0,0 +1,237 @@ +import abc +import asyncio +import json +from abc import ABC, abstractmethod +from contextlib import asynccontextmanager +from enum import Enum +from typing import Any, Callable, Dict, List, Optional + +import websockets +from websockets import InvalidStatus +from websockets.asyncio.connection import Connection + +from cozepy import Auth, CozeAPIError +from cozepy.log import log_debug, log_info +from cozepy.model import CozeModel +from cozepy.request import Requester +from cozepy.util import remove_url_trailing_slash +from cozepy.version import coze_client_user_agent, user_agent + + +class EventType(str, Enum): + # common + ERROR = "error" + CLOSED = "closed" + + # v1/audio/speech + # req + INPUT_TEXT_BUFFER_APPEND = "input_text_buffer.append" + INPUT_TEXT_BUFFER_COMMIT = "input_text_buffer.commit" + SPEECH_UPDATE = "speech.update" + # resp + # v1/audio/speech + INPUT_TEXT_BUFFER_COMMITTED = "input_text_buffer.committed" # ignored + SPEECH_AUDIO_UPDATE = "speech.audio.update" + SPEECH_AUDIO_COMPLETED = "speech.audio.completed" + + # v1/audio/transcriptions + # req + INPUT_AUDIO_BUFFER_APPEND = "input_audio_buffer.append" + INPUT_AUDIO_BUFFER_COMMIT = "input_audio_buffer.commit" + TRANSCRIPTIONS_UPDATE = "transcriptions.update" + # resp + INPUT_AUDIO_BUFFER_COMMITTED = "input_audio_buffer.committed" # ignored + TRANSCRIPTIONS_MESSAGE_UPDATE = "transcriptions.message.update" + TRANSCRIPTIONS_MESSAGE_COMPLETED = "transcriptions.message.completed" + + # v1/chat + # req + # INPUT_AUDIO_BUFFER_APPEND = "input_audio_buffer.append" + # INPUT_AUDIO_BUFFER_COMMIT = "input_audio_buffer.commit" + CHAT_UPDATE = "chat.update" + # resp + CONVERSATION_CHAT_CREATED = "conversation.chat.created" + CONVERSATION_MESSAGE_DELTA = "conversation.message.delta" + CONVERSATION_CHAT_REQUIRES_ACTION = "conversation.chat.requires_action" + CONVERSATION_AUDIO_DELTA = "conversation.audio.delta" + CONVERSATION_CHAT_COMPLETED = "conversation.chat.completed" + + +class Event(CozeModel, ABC): + event_id: Optional[str] = None + type: EventType + + @staticmethod + @abstractmethod + def load(data: Any, event_id: Optional[str] = None) -> "Event": + raise NotImplementedError + + @abstractmethod + def dump(self) -> Dict[str, Any]: + raise NotImplementedError + + def dump_json(self) -> str: + return json.dumps(self.dump()) + + +class AsyncWebsocketsBaseClient(abc.ABC): + def __init__( + self, + base_url: str, + auth: Auth, + requester: Requester, + path: str, + query: Dict[str, str] = None, + on_event: Dict[EventType, Callable] = None, + wait_events: Optional[List[EventType]] = None, + **kwargs, + ): + self._base_url = remove_url_trailing_slash(base_url) + self._auth = auth + self._requester = requester + self._path = path + self._ws_url = self._base_url + "/" + path + if query: + self._ws_url += "?" + "&".join([f"{k}={v}" for k, v in query.items()]) + self._on_event = on_event.copy() if on_event else {} + self._headers = kwargs.get("headers") + self._wait_events = wait_events.copy() if wait_events else [] + + self._input_queue: asyncio.Queue[Optional[Event]] = asyncio.Queue() + self._ws: Optional[Connection] = None + self._send_task: Optional[asyncio.Task] = None + self._receive_task: Optional[asyncio.Task] = None + + @asynccontextmanager + async def __call__(self): + try: + await self.connect() + yield self + finally: + await self.close() + + async def connect(self): + headers = { + "Authorization": f"Bearer {self._auth.token}", + "X-Coze-Client-User-Agent": coze_client_user_agent(), + **(self._headers or {}), + } + try: + self._ws = await websockets.connect( + self._ws_url, + user_agent_header=user_agent(), + additional_headers=headers, + ) + log_info("[%s] connected to websocket", self._path) + + self._receive_task = asyncio.create_task(self._receive_loop()) + self._send_task = asyncio.create_task(self._send_loop()) + except InvalidStatus as e: + raise CozeAPIError(None, f"{e}", e.response.headers.get("x-tt-logid")) from e + + async def wait(self, events: Optional[List[EventType]] = None, wait_all=True) -> None: + if events is None: + events = self._wait_events + await self._wait_completed(events, wait_all=wait_all) + + def on(self, event_type: EventType, handler: Callable) -> None: + self._on_event[event_type] = handler + + async def close(self) -> None: + await self._close() + + async def _send_loop(self) -> None: + try: + while True: + event = await self._input_queue.get() + await self._send_event(event) + self._input_queue.task_done() + except Exception as e: + await self._handle_error(e) + + async def _receive_loop(self) -> None: + try: + while True: + if not self._ws: + log_debug("[%s] empty websocket conn, close", self._path) + break + + data = await self._ws.recv() + message = json.loads(data) + event_type = message.get("type") + log_debug("[%s] receive event, type=%s, event=%s", self._path, event_type, data) + + if handler := self._on_event.get(event_type): + if event := self._load_event(message): + await handler(self, event) + except Exception as e: + await self._handle_error(e) + + @abc.abstractmethod + def _load_event(self, message: Dict) -> Event: ... + + async def _wait_completed(self, events: List[EventType], wait_all: bool) -> None: + # 创建一个 Future 对象来控制阻塞 + future = asyncio.Future() + + # 保存原始的事件处理函数 + original_handlers = {} + + # 用于跟踪已完成的事件 + completed_events = set() + + # 定义一个内部处理函数来处理完成事件 + async def _handle_completed(client, event): + event_type = event.type + completed_events.add(event_type) + + if wait_all: + # 所有事件都需要完成 + if completed_events == set(events): + future.set_result(None) + else: + # 任意一个事件完成即可 + future.set_result(None) + + # 为每个指定的事件类型临时注册处理函数 + for event_type in events: + original_handlers[event_type] = self._on_event.get(event_type) + self._on_event[event_type] = _handle_completed + + try: + # 等待直到满足完成条件 + await future + finally: + # 恢复所有原来的处理函数 + for event_type, handler in original_handlers.items(): + if handler: + self._on_event[event_type] = handler + else: + self._on_event.pop(event_type, None) + + async def _handle_error(self, error: Exception) -> None: + if handler := self._on_event.get(EventType.ERROR): + await handler(self, error) + else: + raise error + + async def _close(self) -> None: + log_info("[%s] connect closed", self._path) + if self._send_task: + self._send_task.cancel() + if self._receive_task: + self._receive_task.cancel() + + if self._ws: + await self._ws.close() + self._ws = None + + while not self._input_queue.empty(): + await self._input_queue.get() + + if handler := self._on_event.get(EventType.CLOSED): + await handler(self) + + async def _send_event(self, event: Event) -> None: + log_debug("[%s] send event, type=%s", self._path, event.type.value) + await self._ws.send(event.dump_json(), True) diff --git a/examples/chat_stream.py b/examples/chat_stream.py index 2f50a67..3bc9ff5 100644 --- a/examples/chat_stream.py +++ b/examples/chat_stream.py @@ -6,9 +6,8 @@ import os from cozepy import COZE_COM_BASE_URL +from examples.utils import get_coze_api_token -# Get an access_token through personal access token or oauth. -coze_api_token = os.getenv("COZE_API_TOKEN") # The default access is api.coze.com, but if you need to access api.coze.cn, # please use base_url to configure the api endpoint to access coze_api_base = os.getenv("COZE_API_BASE") or COZE_COM_BASE_URL @@ -16,7 +15,7 @@ from cozepy import Coze, TokenAuth, Message, ChatStatus, MessageContentType, ChatEventType # noqa # Init the Coze client through the access_token. -coze = Coze(auth=TokenAuth(token=coze_api_token), base_url=coze_api_base) +coze = Coze(auth=TokenAuth(token=get_coze_api_token()), base_url=coze_api_base) # Create a bot instance in Coze, copy the last number from the web link as the bot's ID. bot_id = os.getenv("COZE_BOT_ID") or "bot id" diff --git a/examples/utils/__init__.py b/examples/utils/__init__.py new file mode 100644 index 0000000..9aeb7f5 --- /dev/null +++ b/examples/utils/__init__.py @@ -0,0 +1,28 @@ +import os +from typing import Optional + +from cozepy import COZE_CN_BASE_URL, DeviceOAuthApp + + +def get_coze_api_base() -> str: + # The default access is api.coze.com, but if you need to access api.coze.cn, + # please use base_url to configure the api endpoint to access + coze_api_base = os.getenv("COZE_API_BASE") + if coze_api_base: + return coze_api_base + + return COZE_CN_BASE_URL # default + + +def get_coze_api_token(workspace_id: Optional[str] = None) -> str: + # Get an access_token through personal access token or oauth. + coze_api_token = os.getenv("COZE_API_TOKEN") + if coze_api_token: + return coze_api_token + + coze_api_base = get_coze_api_base() + + device_oauth_app = DeviceOAuthApp(client_id="57294420732781205987760324720643.app.coze", base_url=coze_api_base) + device_code = device_oauth_app.get_device_code(workspace_id) + print(f"Please Open: {device_code.verification_url} to get the access token") + return device_oauth_app.get_access_token(device_code=device_code.device_code, poll=True).access_token diff --git a/examples/websockets_audio_speech.py b/examples/websockets_audio_speech.py new file mode 100644 index 0000000..f4b943f --- /dev/null +++ b/examples/websockets_audio_speech.py @@ -0,0 +1,63 @@ +import asyncio +import json +import logging +import os + +from cozepy import AsyncCoze, TokenAuth, setup_logging +from cozepy.util import write_pcm_to_wav_file +from cozepy.websockets.audio.speech import AsyncWebsocketsAudioSpeechCreateClient, EventType, SpeechAudioDeltaEvent +from examples.utils import get_coze_api_base, get_coze_api_token + +coze_log = os.getenv("COZE_LOG") +if coze_log: + setup_logging(logging.getLevelNamesMapping()[coze_log.upper()]) + +kwargs = json.loads(os.getenv("COZE_KWARGS") or "{}") + +d = [] + + +async def handle_speech_audio_update(client: AsyncWebsocketsAudioSpeechCreateClient, event: SpeechAudioDeltaEvent): + d.append(event.delta) + + +async def handle_error(client: AsyncWebsocketsAudioSpeechCreateClient, e: Exception): + print(f"Error occurred: f{e}") + + +async def handle_closed(client: AsyncWebsocketsAudioSpeechCreateClient): + print("Speech connection closed, saving audio data to output.wav") + audio_data = b"".join(d) + write_pcm_to_wav_file(audio_data, "output.wav") + + +async def main(): + coze_api_token = get_coze_api_token() + coze_api_base = get_coze_api_base() + + # Initialize Coze client + coze = AsyncCoze( + auth=TokenAuth(coze_api_token), + base_url=coze_api_base, + ) + + speech = coze.websockets.audio.speech.create( + on_event={ + EventType.SPEECH_AUDIO_UPDATE: handle_speech_audio_update, + EventType.ERROR: handle_error, + EventType.CLOSED: handle_closed, + }, + **kwargs, + ) + + # Text to be converted to speech + text = "你今天好吗? 今天天气不错呀" + + async with speech() as client: + await client.append(text) + await client.commit() + await client.wait() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/websockets_audio_transcriptions.py b/examples/websockets_audio_transcriptions.py new file mode 100644 index 0000000..a5398b1 --- /dev/null +++ b/examples/websockets_audio_transcriptions.py @@ -0,0 +1,83 @@ +import asyncio +import json +import logging +import os + +from cozepy import TokenAuth, setup_logging +from cozepy.audio.speech import AudioFormat +from cozepy.coze import AsyncCoze +from cozepy.websockets.audio.transcriptions import ( + AsyncWebsocketsAudioTranscriptionsCreateClient, + TranscriptionsMessageUpdateEvent, +) +from cozepy.websockets.ws import EventType +from examples.utils import get_coze_api_base, get_coze_api_token + +coze_log = os.getenv("COZE_LOG") +if coze_log: + setup_logging(logging.getLevelNamesMapping()[coze_log.upper()]) +kwargs = json.loads(os.getenv("COZE_KWARGS") or "{}") + + +async def handle_transcriptions_message_update( + client: AsyncWebsocketsAudioTranscriptionsCreateClient, event: TranscriptionsMessageUpdateEvent +): + print("Received:", event.content) + + +async def handle_error(client: AsyncWebsocketsAudioTranscriptionsCreateClient, e: Exception): + print(f"Error occurred: {str(e)}") + + +async def handle_closed(client: AsyncWebsocketsAudioTranscriptionsCreateClient): + """Handle connection closure""" + print("Connection closed") + + +def wrap_coze_speech_to_iterator(coze: AsyncCoze, text: str): + async def iterator(): + voices = await coze.audio.voices.list(**kwargs) + content = await coze.audio.speech.create( + input=text, + voice_id=voices.items[0].voice_id, + response_format=AudioFormat.WAV, + sample_rate=24000, + **kwargs, + ) + for data in content._raw_response.iter_bytes(chunk_size=1024): + yield data + + return iterator + + +async def main(): + coze_api_token = get_coze_api_token() + coze_api_base = get_coze_api_base() + + # Initialize Coze client + coze = AsyncCoze( + auth=TokenAuth(coze_api_token), + base_url=coze_api_base, + ) + # Initialize Audio + speech_stream = wrap_coze_speech_to_iterator(coze, "你今天好吗? 今天天气不错呀") + + transcriptions = coze.websockets.audio.transcriptions.create( + on_event={ + EventType.TRANSCRIPTIONS_MESSAGE_UPDATE: handle_transcriptions_message_update, + EventType.ERROR: handle_error, + EventType.CLOSED: handle_closed, + }, + **kwargs, + ) + + # Create and connect WebSocket client + async with transcriptions() as client: + async for data in speech_stream(): + await client.append(data) + await client.commit() + await client.wait() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/websockets_chat.py b/examples/websockets_chat.py new file mode 100644 index 0000000..01f0c80 --- /dev/null +++ b/examples/websockets_chat.py @@ -0,0 +1,109 @@ +import asyncio +import json +import logging +import os + +from cozepy import TokenAuth, setup_logging +from cozepy.audio.speech import AudioFormat +from cozepy.coze import AsyncCoze +from cozepy.log import log_info +from cozepy.util import write_pcm_to_wav_file +from cozepy.websockets.audio.transcriptions import ( + AsyncWebsocketsAudioTranscriptionsCreateClient, +) +from cozepy.websockets.chat import ( + AsyncWebsocketsChatCreateClient, + ConversationAudioDeltaEvent, + ConversationChatCreatedEvent, + ConversationMessageDeltaEvent, +) +from cozepy.websockets.ws import EventType +from examples.utils import get_coze_api_base, get_coze_api_token + +coze_log = os.getenv("COZE_LOG") +if coze_log: + setup_logging(logging.getLevelNamesMapping()[coze_log.upper()]) + +kwargs = json.loads(os.getenv("COZE_KWARGS") or "{}") +d = [] + + +async def handle_conversation_chat_created( + client: AsyncWebsocketsChatCreateClient, event: ConversationChatCreatedEvent +): + log_info("ChatCreated") + + +async def handle_conversation_message_delta( + client: AsyncWebsocketsChatCreateClient, event: ConversationMessageDeltaEvent +): + print("Received:", event.data.content) + + +async def handle_conversation_audio_delta(client: AsyncWebsocketsChatCreateClient, event: ConversationAudioDeltaEvent): + d.append(event.data.get_audio()) + + +async def handle_error(client: AsyncWebsocketsAudioTranscriptionsCreateClient, e: Exception): + log_info(f"Error occurred: {str(e)}") + + +async def handle_closed(client: AsyncWebsocketsAudioTranscriptionsCreateClient): + print("Chat connection closed, saving audio data to output.wav") + audio_data = b"".join(d) + write_pcm_to_wav_file(audio_data, "output.wav") + + +def wrap_coze_speech_to_iterator(coze: AsyncCoze, text: str): + async def iterator(): + voices = await coze.audio.voices.list(**kwargs) + content = await coze.audio.speech.create( + input=text, + voice_id=voices.items[0].voice_id, + response_format=AudioFormat.WAV, + sample_rate=24000, + **kwargs, + ) + for data in content._raw_response.iter_bytes(chunk_size=1024): + yield data + + return iterator + + +async def main(): + coze_api_token = get_coze_api_token() + coze_api_base = get_coze_api_base() + bot_id = os.getenv("COZE_BOT_ID") + + # Initialize Coze client + coze = AsyncCoze( + auth=TokenAuth(coze_api_token), + base_url=coze_api_base, + ) + # Initialize Audio + speech_stream = wrap_coze_speech_to_iterator(coze, "你今天好吗? 今天天气不错呀") + + chat = coze.websockets.chat.create( + bot_id=bot_id, + on_event={ + EventType.CONVERSATION_CHAT_CREATED: handle_conversation_chat_created, + EventType.CONVERSATION_MESSAGE_DELTA: handle_conversation_message_delta, + EventType.CONVERSATION_AUDIO_DELTA: handle_conversation_audio_delta, + EventType.ERROR: handle_error, + EventType.CLOSED: handle_closed, + }, + **kwargs, + ) + + # Create and connect WebSocket client + async with chat() as client: + # Read and send audio data + async for data in speech_stream(): + await client.append(data) + await client.commit() + log_info("Audio Committed") + await client.wait() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/poetry.lock b/poetry.lock index d4037da..bd8b9cf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -38,13 +38,13 @@ trio = ["trio (<0.22)"] [[package]] name = "anyio" -version = "4.6.2" +version = "4.5.2" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" files = [ - {file = "anyio-4.6.2-py3-none-any.whl", hash = "sha256:6caec6b1391f6f6d7b2ef2258d2902d36753149f67478f7df4be8e54d03a8f54"}, - {file = "anyio-4.6.2.tar.gz", hash = "sha256:f72a7bb3dd0752b3bd8b17a844a019d7fbf6ae218c588f4f9ba1b2f600b12347"}, + {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, + {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, ] [package.dependencies] @@ -74,13 +74,13 @@ cryptography = ">=3.2" [[package]] name = "certifi" -version = "2024.8.30" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] @@ -258,38 +258,38 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "43.0.1" +version = "43.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, - {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, - {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, - {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, - {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, - {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, - {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, - {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, - {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, - {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"}, - {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, + {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, + {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, + {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, + {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, + {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, + {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, + {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, ] [package.dependencies] @@ -302,7 +302,7 @@ nox = ["nox"] pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] @@ -393,13 +393,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "httpcore" -version = "1.0.6" +version = "1.0.7" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f"}, - {file = "httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"}, + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, ] [package.dependencies] @@ -1066,6 +1066,211 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +[[package]] +name = "websocket-client" +version = "1.6.1" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.7" +files = [ + {file = "websocket-client-1.6.1.tar.gz", hash = "sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd"}, + {file = "websocket_client-1.6.1-py3-none-any.whl", hash = "sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d"}, +] + +[package.extras] +docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "websocket-client" +version = "1.8.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "websockets" +version = "13.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee"}, + {file = "websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7"}, + {file = "websockets-13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6"}, + {file = "websockets-13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b"}, + {file = "websockets-13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa"}, + {file = "websockets-13.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700"}, + {file = "websockets-13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c"}, + {file = "websockets-13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0"}, + {file = "websockets-13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f"}, + {file = "websockets-13.1-cp310-cp310-win32.whl", hash = "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe"}, + {file = "websockets-13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a"}, + {file = "websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19"}, + {file = "websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5"}, + {file = "websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd"}, + {file = "websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02"}, + {file = "websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7"}, + {file = "websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096"}, + {file = "websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084"}, + {file = "websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3"}, + {file = "websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9"}, + {file = "websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f"}, + {file = "websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557"}, + {file = "websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc"}, + {file = "websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49"}, + {file = "websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd"}, + {file = "websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0"}, + {file = "websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6"}, + {file = "websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9"}, + {file = "websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68"}, + {file = "websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14"}, + {file = "websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf"}, + {file = "websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c"}, + {file = "websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3"}, + {file = "websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6"}, + {file = "websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708"}, + {file = "websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418"}, + {file = "websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a"}, + {file = "websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f"}, + {file = "websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5"}, + {file = "websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135"}, + {file = "websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2"}, + {file = "websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6"}, + {file = "websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d"}, + {file = "websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2"}, + {file = "websockets-13.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c7934fd0e920e70468e676fe7f1b7261c1efa0d6c037c6722278ca0228ad9d0d"}, + {file = "websockets-13.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:149e622dc48c10ccc3d2760e5f36753db9cacf3ad7bc7bbbfd7d9c819e286f23"}, + {file = "websockets-13.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a569eb1b05d72f9bce2ebd28a1ce2054311b66677fcd46cf36204ad23acead8c"}, + {file = "websockets-13.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95df24ca1e1bd93bbca51d94dd049a984609687cb2fb08a7f2c56ac84e9816ea"}, + {file = "websockets-13.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8dbb1bf0c0a4ae8b40bdc9be7f644e2f3fb4e8a9aca7145bfa510d4a374eeb7"}, + {file = "websockets-13.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:035233b7531fb92a76beefcbf479504db8c72eb3bff41da55aecce3a0f729e54"}, + {file = "websockets-13.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e4450fc83a3df53dec45922b576e91e94f5578d06436871dce3a6be38e40f5db"}, + {file = "websockets-13.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:463e1c6ec853202dd3657f156123d6b4dad0c546ea2e2e38be2b3f7c5b8e7295"}, + {file = "websockets-13.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6d6855bbe70119872c05107e38fbc7f96b1d8cb047d95c2c50869a46c65a8e96"}, + {file = "websockets-13.1-cp38-cp38-win32.whl", hash = "sha256:204e5107f43095012b00f1451374693267adbb832d29966a01ecc4ce1db26faf"}, + {file = "websockets-13.1-cp38-cp38-win_amd64.whl", hash = "sha256:485307243237328c022bc908b90e4457d0daa8b5cf4b3723fd3c4a8012fce4c6"}, + {file = "websockets-13.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9b37c184f8b976f0c0a231a5f3d6efe10807d41ccbe4488df8c74174805eea7d"}, + {file = "websockets-13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:163e7277e1a0bd9fb3c8842a71661ad19c6aa7bb3d6678dc7f89b17fbcc4aeb7"}, + {file = "websockets-13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b889dbd1342820cc210ba44307cf75ae5f2f96226c0038094455a96e64fb07a"}, + {file = "websockets-13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:586a356928692c1fed0eca68b4d1c2cbbd1ca2acf2ac7e7ebd3b9052582deefa"}, + {file = "websockets-13.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7bd6abf1e070a6b72bfeb71049d6ad286852e285f146682bf30d0296f5fbadfa"}, + {file = "websockets-13.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2aad13a200e5934f5a6767492fb07151e1de1d6079c003ab31e1823733ae79"}, + {file = "websockets-13.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:df01aea34b6e9e33572c35cd16bae5a47785e7d5c8cb2b54b2acdb9678315a17"}, + {file = "websockets-13.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e54affdeb21026329fb0744ad187cf812f7d3c2aa702a5edb562b325191fcab6"}, + {file = "websockets-13.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ef8aa8bdbac47f4968a5d66462a2a0935d044bf35c0e5a8af152d58516dbeb5"}, + {file = "websockets-13.1-cp39-cp39-win32.whl", hash = "sha256:deeb929efe52bed518f6eb2ddc00cc496366a14c726005726ad62c2dd9017a3c"}, + {file = "websockets-13.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c65ffa900e7cc958cd088b9a9157a8141c991f8c53d11087e6fb7277a03f81d"}, + {file = "websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238"}, + {file = "websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5"}, + {file = "websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9"}, + {file = "websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6"}, + {file = "websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a"}, + {file = "websockets-13.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23"}, + {file = "websockets-13.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9156c45750b37337f7b0b00e6248991a047be4aa44554c9886fe6bdd605aab3b"}, + {file = "websockets-13.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:80c421e07973a89fbdd93e6f2003c17d20b69010458d3a8e37fb47874bd67d51"}, + {file = "websockets-13.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82d0ba76371769d6a4e56f7e83bb8e81846d17a6190971e38b5de108bde9b0d7"}, + {file = "websockets-13.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9875a0143f07d74dc5e1ded1c4581f0d9f7ab86c78994e2ed9e95050073c94d"}, + {file = "websockets-13.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11e38ad8922c7961447f35c7b17bffa15de4d17c70abd07bfbe12d6faa3e027"}, + {file = "websockets-13.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4059f790b6ae8768471cddb65d3c4fe4792b0ab48e154c9f0a04cefaabcd5978"}, + {file = "websockets-13.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25c35bf84bf7c7369d247f0b8cfa157f989862c49104c5cf85cb5436a641d93e"}, + {file = "websockets-13.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:83f91d8a9bb404b8c2c41a707ac7f7f75b9442a0a876df295de27251a856ad09"}, + {file = "websockets-13.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a43cfdcddd07f4ca2b1afb459824dd3c6d53a51410636a2c7fc97b9a8cf4842"}, + {file = "websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48a2ef1381632a2f0cb4efeff34efa97901c9fbc118e01951ad7cfc10601a9bb"}, + {file = "websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bf774c754c35dbb487360b12c5727adab887f1622b8aed5755880a21c4a20"}, + {file = "websockets-13.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:95858ca14a9f6fa8413d29e0a585b31b278388aa775b8a81fa24830123874678"}, + {file = "websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f"}, + {file = "websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878"}, +] + +[[package]] +name = "websockets" +version = "14.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "websockets-14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a0adf84bc2e7c86e8a202537b4fd50e6f7f0e4a6b6bf64d7ccb96c4cd3330b29"}, + {file = "websockets-14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90b5d9dfbb6d07a84ed3e696012610b6da074d97453bd01e0e30744b472c8179"}, + {file = "websockets-14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2177ee3901075167f01c5e335a6685e71b162a54a89a56001f1c3e9e3d2ad250"}, + {file = "websockets-14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f14a96a0034a27f9d47fd9788913924c89612225878f8078bb9d55f859272b0"}, + {file = "websockets-14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f874ba705deea77bcf64a9da42c1f5fc2466d8f14daf410bc7d4ceae0a9fcb0"}, + {file = "websockets-14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9607b9a442392e690a57909c362811184ea429585a71061cd5d3c2b98065c199"}, + {file = "websockets-14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bea45f19b7ca000380fbd4e02552be86343080120d074b87f25593ce1700ad58"}, + {file = "websockets-14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:219c8187b3ceeadbf2afcf0f25a4918d02da7b944d703b97d12fb01510869078"}, + {file = "websockets-14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad2ab2547761d79926effe63de21479dfaf29834c50f98c4bf5b5480b5838434"}, + {file = "websockets-14.1-cp310-cp310-win32.whl", hash = "sha256:1288369a6a84e81b90da5dbed48610cd7e5d60af62df9851ed1d1d23a9069f10"}, + {file = "websockets-14.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0744623852f1497d825a49a99bfbec9bea4f3f946df6eb9d8a2f0c37a2fec2e"}, + {file = "websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512"}, + {file = "websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac"}, + {file = "websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280"}, + {file = "websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1"}, + {file = "websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3"}, + {file = "websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6"}, + {file = "websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0"}, + {file = "websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89"}, + {file = "websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23"}, + {file = "websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e"}, + {file = "websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09"}, + {file = "websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed"}, + {file = "websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d"}, + {file = "websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707"}, + {file = "websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a"}, + {file = "websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45"}, + {file = "websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58"}, + {file = "websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058"}, + {file = "websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4"}, + {file = "websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05"}, + {file = "websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0"}, + {file = "websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f"}, + {file = "websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9"}, + {file = "websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b"}, + {file = "websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3"}, + {file = "websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59"}, + {file = "websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2"}, + {file = "websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da"}, + {file = "websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9"}, + {file = "websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7"}, + {file = "websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a"}, + {file = "websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6"}, + {file = "websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0"}, + {file = "websockets-14.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:01bb2d4f0a6d04538d3c5dfd27c0643269656c28045a53439cbf1c004f90897a"}, + {file = "websockets-14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:414ffe86f4d6f434a8c3b7913655a1a5383b617f9bf38720e7c0799fac3ab1c6"}, + {file = "websockets-14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fda642151d5affdee8a430bd85496f2e2517be3a2b9d2484d633d5712b15c56"}, + {file = "websockets-14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd7c11968bc3860d5c78577f0dbc535257ccec41750675d58d8dc66aa47fe52c"}, + {file = "websockets-14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a032855dc7db987dff813583d04f4950d14326665d7e714d584560b140ae6b8b"}, + {file = "websockets-14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7e7ea2f782408c32d86b87a0d2c1fd8871b0399dd762364c731d86c86069a78"}, + {file = "websockets-14.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:39450e6215f7d9f6f7bc2a6da21d79374729f5d052333da4d5825af8a97e6735"}, + {file = "websockets-14.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ceada5be22fa5a5a4cdeec74e761c2ee7db287208f54c718f2df4b7e200b8d4a"}, + {file = "websockets-14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3fc753451d471cff90b8f467a1fc0ae64031cf2d81b7b34e1811b7e2691bc4bc"}, + {file = "websockets-14.1-cp39-cp39-win32.whl", hash = "sha256:14839f54786987ccd9d03ed7f334baec0f02272e7ec4f6e9d427ff584aeea8b4"}, + {file = "websockets-14.1-cp39-cp39-win_amd64.whl", hash = "sha256:d9fd19ecc3a4d5ae82ddbfb30962cf6d874ff943e56e0c81f5169be2fda62979"}, + {file = "websockets-14.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5dc25a9dbd1a7f61eca4b7cb04e74ae4b963d658f9e4f9aad9cd00b688692c8"}, + {file = "websockets-14.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:04a97aca96ca2acedf0d1f332c861c5a4486fdcba7bcef35873820f940c4231e"}, + {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df174ece723b228d3e8734a6f2a6febbd413ddec39b3dc592f5a4aa0aff28098"}, + {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:034feb9f4286476f273b9a245fb15f02c34d9586a5bc936aff108c3ba1b21beb"}, + {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c308dabd2b380807ab64b62985eaccf923a78ebc572bd485375b9ca2b7dc7"}, + {file = "websockets-14.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a42d3ecbb2db5080fc578314439b1d79eef71d323dc661aa616fb492436af5d"}, + {file = "websockets-14.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddaa4a390af911da6f680be8be4ff5aaf31c4c834c1a9147bc21cbcbca2d4370"}, + {file = "websockets-14.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a4c805c6034206143fbabd2d259ec5e757f8b29d0a2f0bf3d2fe5d1f60147a4a"}, + {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:205f672a6c2c671a86d33f6d47c9b35781a998728d2c7c2a3e1cf3333fcb62b7"}, + {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef440054124728cc49b01c33469de06755e5a7a4e83ef61934ad95fc327fbb0"}, + {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7591d6f440af7f73c4bd9404f3772bfee064e639d2b6cc8c94076e71b2471c1"}, + {file = "websockets-14.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:25225cc79cfebc95ba1d24cd3ab86aaa35bcd315d12fa4358939bd55e9bd74a5"}, + {file = "websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e"}, + {file = "websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8"}, +] + [[package]] name = "zipp" version = "3.15.0" @@ -1084,4 +1289,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "e01a5b0201b4dbad4fa3de97bbdf45e00e55852fb66636170d8c32dc527a24f0" +content-hash = "9a538c47996a060fe0abd622864b4e417052740cf83499a57c07c8af99c5e06c" diff --git a/pyproject.toml b/pyproject.toml index 1aa09c7..b8049ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,14 @@ httpx = [ ] typing-extensions = "^4.3.0" distro = "^1.9.0" +websocket-client = [ + { version = "^1.8.0", python = ">=3.8" }, + { version = "^1.6.1", python = ">=3.7,<3.8" }, +] +websockets = [ + { version = "^14.1.0", python = ">=3.9" }, + { version = "^13.1.0", python = ">=3.8,<3.9" }, +] [tool.poetry.group.dev.dependencies] pytest = "^7.0.0" diff --git a/tests/test_audio_translations.py b/tests/test_audio_translations.py index 97daff8..ef29605 100644 --- a/tests/test_audio_translations.py +++ b/tests/test_audio_translations.py @@ -19,7 +19,7 @@ def mock_create_translation(respx_mock): }, ) - respx_mock.post("/v1/audio/translations").mock(raw_response) + respx_mock.post("/v1/audio/transcriptions").mock(raw_response) return logid @@ -31,7 +31,7 @@ def test_sync_translation_create(self, respx_mock): mock_logid = mock_create_translation(respx_mock) - res = coze.audio.translations.create(file=("filename", "content")) + res = coze.audio.transcriptions.create(file=("filename", "content")) assert res assert res.response.logid is not None assert res.response.logid == mock_logid @@ -45,6 +45,6 @@ async def test_async_translation_create(self, respx_mock): mock_logid = mock_create_translation(respx_mock) - res = await coze.audio.translations.create(file=("filename", "content")) + res = await coze.audio.transcriptions.create(file=("filename", "content")) assert res assert res.response.logid == mock_logid