typecast-python 0.1.8__tar.gz → 0.2.0__tar.gz

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.
@@ -83,6 +83,10 @@ CMakeFiles/
83
83
  CMakeCache.txt
84
84
  cmake_install.cmake
85
85
  Makefile
86
+ !typecast-go/Makefile
87
+ !typecast-java/Makefile
88
+ !typecast-rust/Makefile
89
+ !typecast-kotlin/Makefile
86
90
  *.cmake
87
91
 
88
92
  # Compiled objects
@@ -254,6 +258,7 @@ DerivedData/
254
258
  .swiftpm/
255
259
  Packages/
256
260
  Package.resolved
261
+ typecast-swift/coverage-summary.txt
257
262
 
258
263
  # CocoaPods
259
264
  Pods/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: typecast-python
3
- Version: 0.1.8
3
+ Version: 0.2.0
4
4
  Summary: Official Typecast Python SDK - Convert text to lifelike speech using AI-powered voices
5
5
  Project-URL: Homepage, https://typecast.ai
6
6
  Project-URL: Documentation, https://typecast.ai/docs/overview
@@ -226,15 +226,15 @@ Requires-Python: >=3.11
226
226
  Requires-Dist: aiohttp>=3.8.0
227
227
  Requires-Dist: pydantic>=2.0.0
228
228
  Requires-Dist: requests>=2.28.0
229
- Requires-Dist: sseclient-py>=1.7.2
230
229
  Requires-Dist: typing-extensions>=4.0.0
231
- Requires-Dist: websockets>=10.0
232
230
  Provides-Extra: dev
231
+ Requires-Dist: aioresponses>=0.7.6; extra == 'dev'
233
232
  Requires-Dist: black>=23.0.0; extra == 'dev'
234
233
  Requires-Dist: flake8>=6.0.0; extra == 'dev'
235
234
  Requires-Dist: isort>=5.0.0; extra == 'dev'
236
235
  Requires-Dist: mypy>=1.0.0; extra == 'dev'
237
236
  Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
237
+ Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
238
238
  Requires-Dist: pytest-mock>=3.14.0; extra == 'dev'
239
239
  Requires-Dist: pytest>=7.0.0; extra == 'dev'
240
240
  Description-Content-Type: text/markdown
@@ -248,6 +248,7 @@ Description-Content-Type: text/markdown
248
248
  Convert text to lifelike speech using AI-powered voices
249
249
 
250
250
  [![PyPI version](https://img.shields.io/pypi/v/typecast-python.svg?style=flat-square)](https://pypi.org/project/typecast-python/)
251
+ [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg?style=flat-square)](../docs/coverage-policy.md)
251
252
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg?style=flat-square)](LICENSE)
252
253
  [![Python](https://img.shields.io/badge/Python-3.9+-3776ab.svg?style=flat-square&logo=python&logoColor=white)](https://www.python.org/)
253
254
 
@@ -7,6 +7,7 @@
7
7
  Convert text to lifelike speech using AI-powered voices
8
8
 
9
9
  [![PyPI version](https://img.shields.io/pypi/v/typecast-python.svg?style=flat-square)](https://pypi.org/project/typecast-python/)
10
+ [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg?style=flat-square)](../docs/coverage-policy.md)
10
11
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg?style=flat-square)](LICENSE)
11
12
  [![Python](https://img.shields.io/badge/Python-3.9+-3776ab.svg?style=flat-square&logo=python&logoColor=white)](https://www.python.org/)
12
13
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "typecast-python"
7
- version = "0.1.8"
7
+ version = "0.2.0"
8
8
  description = "Official Typecast Python SDK - Convert text to lifelike speech using AI-powered voices"
9
9
  authors = [
10
10
  {name = "Neosapience", email = "help@typecast.ai"}
@@ -32,20 +32,20 @@ dependencies = [
32
32
  "aiohttp>=3.8.0",
33
33
  "requests>=2.28.0",
34
34
  "pydantic>=2.0.0",
35
- "sseclient-py>=1.7.2",
36
- "websockets>=10.0",
37
35
  "typing-extensions>=4.0.0",
38
36
  ]
39
37
 
40
38
  [project.optional-dependencies]
41
39
  dev = [
42
40
  "pytest>=7.0.0",
41
+ "pytest-cov>=7.0.0",
42
+ "pytest-mock>=3.14.0",
43
+ "pytest-asyncio>=0.21.0",
44
+ "aioresponses>=0.7.6",
43
45
  "black>=23.0.0",
44
46
  "flake8>=6.0.0",
45
47
  "mypy>=1.0.0",
46
48
  "isort>=5.0.0",
47
- "pytest-mock>=3.14.0",
48
- "pytest-asyncio>=0.21.0",
49
49
  ]
50
50
 
51
51
  [project.urls]
@@ -90,6 +90,34 @@ dev = [
90
90
  "pytest-cov>=7.0.0",
91
91
  "pytest-mock>=3.14.0",
92
92
  "pytest-asyncio>=0.21.0",
93
+ "aioresponses>=0.7.6",
93
94
  "python-dotenv>=1.1.1",
94
95
  "ruff>=0.14.0",
95
96
  ]
97
+
98
+ [tool.pytest.ini_options]
99
+ asyncio_mode = "auto"
100
+ testpaths = ["tests"]
101
+ filterwarnings = [
102
+ "ignore::DeprecationWarning",
103
+ ]
104
+
105
+ [tool.coverage.run]
106
+ branch = true
107
+ source = ["src/typecast"]
108
+ omit = [
109
+ "*/tests/*",
110
+ "*/examples/*",
111
+ "*/__pycache__/*",
112
+ ]
113
+
114
+ [tool.coverage.report]
115
+ show_missing = true
116
+ skip_covered = false
117
+ fail_under = 100
118
+ exclude_lines = [
119
+ "pragma: no cover",
120
+ "raise NotImplementedError",
121
+ "if TYPE_CHECKING:",
122
+ "\\.\\.\\.",
123
+ ]
@@ -11,14 +11,19 @@ from .exceptions import (
11
11
  UnprocessableEntityError,
12
12
  )
13
13
  from .models import (
14
+ Credits,
14
15
  Error,
15
16
  LanguageCode,
17
+ Limits,
16
18
  Output,
19
+ OutputStream,
20
+ PlanTier,
17
21
  Prompt,
22
+ SubscriptionResponse,
18
23
  TTSRequest,
24
+ TTSRequestStream,
19
25
  TTSResponse,
20
26
  VoicesResponse,
21
- WebSocketMessage,
22
27
  )
23
28
 
24
29
  __all__ = [
@@ -35,12 +40,17 @@ __all__ = [
35
40
  "UnauthorizedError",
36
41
  "UnprocessableEntityError",
37
42
  # Models
43
+ "Credits",
38
44
  "Error",
39
45
  "LanguageCode",
46
+ "Limits",
40
47
  "Output",
48
+ "OutputStream",
49
+ "PlanTier",
41
50
  "Prompt",
51
+ "SubscriptionResponse",
42
52
  "TTSRequest",
53
+ "TTSRequestStream",
43
54
  "TTSResponse",
44
55
  "VoicesResponse",
45
- "WebSocketMessage",
46
56
  ]
@@ -1,4 +1,4 @@
1
- from typing import Optional
1
+ from typing import AsyncIterator, Optional
2
2
 
3
3
  import aiohttp
4
4
 
@@ -13,7 +13,15 @@ from .exceptions import (
13
13
  UnauthorizedError,
14
14
  UnprocessableEntityError,
15
15
  )
16
- from .models import TTSRequest, TTSResponse, VoicesResponse, VoiceV2Response, VoicesV2Filter
16
+ from .models import (
17
+ SubscriptionResponse,
18
+ TTSRequest,
19
+ TTSRequestStream,
20
+ TTSResponse,
21
+ VoicesResponse,
22
+ VoicesV2Filter,
23
+ VoiceV2Response,
24
+ )
17
25
 
18
26
 
19
27
  class AsyncTypecast:
@@ -117,6 +125,49 @@ class AsyncTypecast:
117
125
  format=response.headers.get("Content-Type", "audio/wav").split("/")[-1],
118
126
  )
119
127
 
128
+ async def text_to_speech_stream(
129
+ self, request: TTSRequestStream, chunk_size: int = 8192
130
+ ) -> AsyncIterator[bytes]:
131
+ """Stream synthesized audio from `POST /v1/text-to-speech/stream`.
132
+
133
+ Async generator that yields audio chunks as the server emits them.
134
+ For WAV the first chunk contains the WAV header (declared with size
135
+ 0xFFFFFFFF for streaming) followed by PCM data; subsequent chunks are
136
+ PCM only. For MP3 each chunk contains independently-decodable frames.
137
+
138
+ Args:
139
+ request: Streaming TTS request. Uses `OutputStream`, which omits
140
+ `volume` and `target_lufs` (not supported by the streaming
141
+ endpoint).
142
+ chunk_size: Maximum bytes returned per yielded chunk.
143
+
144
+ Yields:
145
+ Audio chunk bytes in the order produced by the server.
146
+
147
+ Raises:
148
+ TypecastError: If the client session is not initialized.
149
+ BadRequestError, UnauthorizedError, PaymentRequiredError,
150
+ NotFoundError, UnprocessableEntityError, RateLimitError,
151
+ InternalServerError, TypecastError: depending on response status.
152
+ """
153
+ if not isinstance(chunk_size, int) or isinstance(chunk_size, bool) or chunk_size < 1:
154
+ raise ValueError("chunk_size must be a positive integer")
155
+ if not self.session:
156
+ raise TypecastError("Client session not initialized. Use async with.")
157
+ endpoint = "/v1/text-to-speech/stream"
158
+ stream_timeout = aiohttp.ClientTimeout(sock_connect=10, sock_read=300)
159
+ async with self.session.post(
160
+ f"{self.host}{endpoint}",
161
+ json=request.model_dump(exclude_none=True),
162
+ timeout=stream_timeout,
163
+ ) as response:
164
+ if response.status != 200:
165
+ error_text = await response.text()
166
+ self._handle_error(response.status, error_text)
167
+
168
+ async for chunk in response.content.iter_chunked(chunk_size):
169
+ yield chunk
170
+
120
171
  async def voices(self, model: Optional[str] = None) -> list[VoicesResponse]:
121
172
  """Get available voices (V1 API) asynchronously.
122
173
 
@@ -196,12 +247,11 @@ class AsyncTypecast:
196
247
  params = {}
197
248
  if filter:
198
249
  filter_dict = filter.model_dump(exclude_none=True)
199
- # Convert enum values to strings
250
+ # Convert enum values to their underlying str representation.
251
+ # Every VoicesV2Filter field is an Optional[Enum], so getattr
252
+ # falls back only if a future non-enum field is added.
200
253
  for key, value in filter_dict.items():
201
- if hasattr(value, "value"):
202
- params[key] = value.value
203
- else:
204
- params[key] = value
254
+ params[key] = getattr(value, "value", value)
205
255
 
206
256
  async with self.session.get(
207
257
  f"{self.host}{endpoint}", params=params
@@ -213,6 +263,31 @@ class AsyncTypecast:
213
263
  data = await response.json()
214
264
  return [VoiceV2Response.model_validate(item) for item in data]
215
265
 
266
+ async def get_my_subscription(self) -> SubscriptionResponse:
267
+ """Get the authenticated user's current subscription asynchronously.
268
+
269
+ Returns plan tier, credit usage, and concurrency limits. Use this to
270
+ check remaining credits or verify your plan before making TTS calls.
271
+
272
+ Returns:
273
+ SubscriptionResponse with plan, credits, and limits.
274
+
275
+ Raises:
276
+ TypecastError: If the client session is not initialized.
277
+ UnauthorizedError: If the API key is invalid.
278
+ RateLimitError: If the rate limit was exceeded.
279
+ InternalServerError: On server-side failures.
280
+ """
281
+ if not self.session:
282
+ raise TypecastError("Client session not initialized. Use async with.")
283
+ endpoint = "/v1/users/me/subscription"
284
+ async with self.session.get(f"{self.host}{endpoint}") as response:
285
+ if response.status != 200:
286
+ error_text = await response.text()
287
+ self._handle_error(response.status, error_text)
288
+ data = await response.json()
289
+ return SubscriptionResponse.model_validate(data)
290
+
216
291
  async def voice_v2(self, voice_id: str) -> VoiceV2Response:
217
292
  """Get a specific voice by ID with enhanced metadata (V2 API)
218
293
 
@@ -1,4 +1,4 @@
1
- from typing import Optional
1
+ from typing import Iterator, Optional
2
2
 
3
3
  import requests
4
4
 
@@ -13,7 +13,15 @@ from .exceptions import (
13
13
  UnauthorizedError,
14
14
  UnprocessableEntityError,
15
15
  )
16
- from .models import TTSRequest, TTSResponse, VoicesResponse, VoiceV2Response, VoicesV2Filter
16
+ from .models import (
17
+ SubscriptionResponse,
18
+ TTSRequest,
19
+ TTSRequestStream,
20
+ TTSResponse,
21
+ VoicesResponse,
22
+ VoicesV2Filter,
23
+ VoiceV2Response,
24
+ )
17
25
 
18
26
 
19
27
  class Typecast:
@@ -104,6 +112,55 @@ class Typecast:
104
112
  format=response.headers.get("Content-Type", "audio/wav").split("/")[-1],
105
113
  )
106
114
 
115
+ def text_to_speech_stream(
116
+ self, request: TTSRequestStream, chunk_size: int = 8192
117
+ ) -> Iterator[bytes]:
118
+ """Stream synthesized audio from `POST /v1/text-to-speech/stream`.
119
+
120
+ Yields raw audio chunks as the server produces them. For WAV the
121
+ first chunk contains the WAV header (declared with size 0xFFFFFFFF
122
+ for streaming) followed by PCM data; subsequent chunks are PCM only.
123
+ For MP3 each chunk contains independently-decodable MP3 frames.
124
+
125
+ The HTTP response is held open until the iterator is exhausted or
126
+ garbage-collected, so callers should consume the iterator promptly
127
+ (e.g. inside a `for` loop or by writing chunks to disk).
128
+
129
+ Args:
130
+ request: Streaming TTS request. Uses `OutputStream`, which omits
131
+ `volume` and `target_lufs` (not supported by the streaming
132
+ endpoint).
133
+ chunk_size: Maximum bytes returned per yielded chunk.
134
+
135
+ Yields:
136
+ Audio chunk bytes in the order produced by the server.
137
+
138
+ Raises:
139
+ BadRequestError, UnauthorizedError, PaymentRequiredError,
140
+ NotFoundError, UnprocessableEntityError, RateLimitError,
141
+ InternalServerError, TypecastError: depending on response status.
142
+ """
143
+ if not isinstance(chunk_size, int) or isinstance(chunk_size, bool) or chunk_size < 1:
144
+ raise ValueError("chunk_size must be a positive integer")
145
+ endpoint = "/v1/text-to-speech/stream"
146
+ response = self.session.post(
147
+ f"{self.host}{endpoint}",
148
+ json=request.model_dump(exclude_none=True),
149
+ stream=True,
150
+ timeout=(10, 300),
151
+ )
152
+ if response.status_code != 200:
153
+ error_text = response.text
154
+ response.close()
155
+ self._handle_error(response.status_code, error_text)
156
+
157
+ try:
158
+ for chunk in response.iter_content(chunk_size=chunk_size):
159
+ if chunk:
160
+ yield chunk
161
+ finally:
162
+ response.close()
163
+
107
164
  def voices(self, model: Optional[str] = None) -> list[VoicesResponse]:
108
165
  """Get available voices (V1 API).
109
166
 
@@ -173,12 +230,11 @@ class Typecast:
173
230
  params = {}
174
231
  if filter:
175
232
  filter_dict = filter.model_dump(exclude_none=True)
176
- # Convert enum values to strings
233
+ # Convert enum values to their underlying str representation.
234
+ # Every VoicesV2Filter field is an Optional[Enum], so getattr
235
+ # falls back only if a future non-enum field is added.
177
236
  for key, value in filter_dict.items():
178
- if hasattr(value, "value"):
179
- params[key] = value.value
180
- else:
181
- params[key] = value
237
+ params[key] = getattr(value, "value", value)
182
238
 
183
239
  response = self.session.get(f"{self.host}{endpoint}", params=params)
184
240
 
@@ -187,6 +243,26 @@ class Typecast:
187
243
 
188
244
  return [VoiceV2Response.model_validate(item) for item in response.json()]
189
245
 
246
+ def get_my_subscription(self) -> SubscriptionResponse:
247
+ """Get the authenticated user's current subscription.
248
+
249
+ Returns plan tier, credit usage, and concurrency limits. Use this to
250
+ check remaining credits or verify your plan before making TTS calls.
251
+
252
+ Returns:
253
+ SubscriptionResponse with plan, credits, and limits.
254
+
255
+ Raises:
256
+ UnauthorizedError: If the API key is invalid.
257
+ RateLimitError: If the rate limit was exceeded.
258
+ InternalServerError: On server-side failures.
259
+ """
260
+ endpoint = "/v1/users/me/subscription"
261
+ response = self.session.get(f"{self.host}{endpoint}")
262
+ if response.status_code != 200:
263
+ self._handle_error(response.status_code, response.text)
264
+ return SubscriptionResponse.model_validate(response.json())
265
+
190
266
  def voice_v2(self, voice_id: str) -> VoiceV2Response:
191
267
  """Get a specific voice by ID with enhanced metadata (V2 API)
192
268
 
@@ -1,17 +1,19 @@
1
1
  from .error import Error
2
+ from .subscription import Credits, Limits, PlanTier, SubscriptionResponse
2
3
  from .tts import (
3
4
  EmotionPreset,
4
5
  LanguageCode,
5
6
  Output,
7
+ OutputStream,
6
8
  PresetPrompt,
7
9
  Prompt,
8
10
  SmartPrompt,
9
11
  TTSModel,
10
12
  TTSPrompt,
11
13
  TTSRequest,
14
+ TTSRequestStream,
12
15
  TTSResponse,
13
16
  )
14
- from .tts_wss import WebSocketMessage
15
17
  from .voices import (
16
18
  AgeEnum,
17
19
  GenderEnum,
@@ -24,6 +26,7 @@ from .voices import (
24
26
 
25
27
  __all__ = [
26
28
  "TTSRequest",
29
+ "TTSRequestStream",
27
30
  "TTSModel",
28
31
  "TTSPrompt",
29
32
  "Prompt",
@@ -31,6 +34,7 @@ __all__ = [
31
34
  "SmartPrompt",
32
35
  "EmotionPreset",
33
36
  "Output",
37
+ "OutputStream",
34
38
  "TTSResponse",
35
39
  "VoicesResponse",
36
40
  "VoiceV2Response",
@@ -40,6 +44,9 @@ __all__ = [
40
44
  "AgeEnum",
41
45
  "UseCaseEnum",
42
46
  "Error",
43
- "WebSocketMessage",
44
47
  "LanguageCode",
48
+ "PlanTier",
49
+ "Credits",
50
+ "Limits",
51
+ "SubscriptionResponse",
45
52
  ]
@@ -0,0 +1,35 @@
1
+ from enum import Enum
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class PlanTier(str, Enum):
7
+ """Subscription plan tier."""
8
+
9
+ FREE = "free"
10
+ LITE = "lite"
11
+ PLUS = "plus"
12
+ CUSTOM = "custom"
13
+
14
+
15
+ class Credits(BaseModel):
16
+ """Credit usage information."""
17
+
18
+ plan_credits: int = Field(description="Total credits provided by the plan")
19
+ used_credits: int = Field(description="Number of credits used")
20
+
21
+
22
+ class Limits(BaseModel):
23
+ """Usage limit information."""
24
+
25
+ concurrency_limit: int = Field(
26
+ description="Maximum number of concurrent requests allowed"
27
+ )
28
+
29
+
30
+ class SubscriptionResponse(BaseModel):
31
+ """Response from `GET /v1/users/me/subscription`."""
32
+
33
+ plan: PlanTier = Field(description="Current subscription plan tier")
34
+ credits: Credits = Field(description="Credit usage information")
35
+ limits: Limits = Field(description="Usage limit information")
@@ -174,3 +174,41 @@ class TTSResponse(BaseModel):
174
174
  audio_data: bytes
175
175
  duration: float
176
176
  format: str = "wav"
177
+
178
+
179
+ class OutputStream(BaseModel):
180
+ """Audio output settings for streaming mode.
181
+
182
+ Streaming mode does not support `volume` or `target_lufs` because the
183
+ server has to commit each chunk before the full waveform is known.
184
+ Passing either field raises a validation error so misuse fails fast.
185
+ """
186
+
187
+ model_config = ConfigDict(extra="forbid")
188
+
189
+ audio_pitch: Optional[int] = Field(default=0, ge=-12, le=12)
190
+ audio_tempo: Optional[float] = Field(default=1.0, ge=0.5, le=2.0)
191
+ audio_format: Optional[str] = Field(
192
+ default="wav", description="Audio format", examples=["wav", "mp3"]
193
+ )
194
+
195
+
196
+ class TTSRequestStream(BaseModel):
197
+ """Request body for `POST /v1/text-to-speech/stream`.
198
+
199
+ Mirrors `TTSRequest` but uses `OutputStream` (no volume / target_lufs).
200
+ """
201
+
202
+ model_config = ConfigDict(json_schema_extra={"exclude_none": True})
203
+
204
+ voice_id: str = Field(
205
+ description="Voice ID", examples=["tc_62a8975e695ad26f7fb514d1"]
206
+ )
207
+ text: str = Field(description="Text", examples=["Hello. How are you?"])
208
+ model: TTSModel = Field(description="Voice model name", examples=["ssfm-v21"])
209
+ language: Optional[Union[LanguageCode, str]] = Field(
210
+ None, description="Language code (ISO 639-3)", examples=["eng"]
211
+ )
212
+ prompt: Optional[TTSPrompt] = None
213
+ output: Optional[OutputStream] = None
214
+ seed: Optional[int] = None
@@ -1,7 +0,0 @@
1
-
2
- from pydantic import BaseModel
3
-
4
-
5
- class WebSocketMessage(BaseModel):
6
- type: str
7
- payload: dict
@@ -1,35 +0,0 @@
1
- from typing import AsyncIterator, Optional
2
-
3
- import aiohttp
4
-
5
- from . import conf
6
- from .exceptions import TypecastError
7
-
8
-
9
- class TypecastSSE:
10
- SSE_URL = f"{conf.get_host()}/v1/text-to-speech/sse"
11
-
12
- def __init__(self, api_key: str):
13
- self.api_key = conf.get_api_key(api_key)
14
- self.session: Optional[aiohttp.ClientSession] = None
15
-
16
- async def connect(self, endpoint: str) -> AsyncIterator[str]:
17
- if self.session:
18
- await self.session.close()
19
-
20
- self.session = aiohttp.ClientSession(
21
- headers={"X-API-KEY": self.api_key, "Accept": "text/event-stream"}
22
- )
23
-
24
- async with self.session.get(f"{self.SSE_URL}/{endpoint}") as response:
25
- if response.status != 200:
26
- raise TypecastError(f"SSE connection failed: {response.status}")
27
-
28
- async for line in response.content:
29
- decoded_line = line.decode("utf-8").strip()
30
- if decoded_line.startswith("data: "):
31
- yield decoded_line[6:]
32
-
33
- async def close(self):
34
- if self.session:
35
- await self.session.close()
@@ -1,47 +0,0 @@
1
- import asyncio
2
- import json
3
- from typing import Callable, Optional
4
-
5
- import websockets
6
-
7
- from .exceptions import TypecastError
8
- from .models import WebSocketMessage
9
-
10
-
11
- class TypecastWebSocket:
12
- WS_URL = "wss://api.typecast.ai/v1/ws"
13
-
14
- def __init__(self, api_key: str):
15
- self.api_key = api_key
16
- self.ws: Optional[websockets.WebSocketClientProtocol] = None
17
- self.callbacks: dict[str, Callable] = {}
18
-
19
- async def connect(self):
20
- self.ws = await websockets.connect(f"{self.WS_URL}?token={self.api_key}")
21
-
22
- # Start message handler
23
- asyncio.create_task(self._message_handler())
24
-
25
- async def _message_handler(self):
26
- if not self.ws:
27
- return
28
-
29
- async for message in self.ws:
30
- data = json.loads(message)
31
- msg = WebSocketMessage(**data)
32
-
33
- if msg.type in self.callbacks:
34
- await self.callbacks[msg.type](msg.payload)
35
-
36
- def on(self, event_type: str, callback: Callable):
37
- """Register event callback"""
38
- self.callbacks[event_type] = callback
39
-
40
- async def send(self, message: WebSocketMessage):
41
- if not self.ws:
42
- raise TypecastError("WebSocket not connected")
43
- await self.ws.send(message.model_dump_json())
44
-
45
- async def close(self):
46
- if self.ws:
47
- await self.ws.close()
File without changes