orbitalsai 1.1.0__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- orbitalsai/__init__.py +24 -2
- orbitalsai/streaming/__init__.py +117 -0
- orbitalsai/streaming/async_client.py +507 -0
- orbitalsai/streaming/audio/__init__.py +33 -0
- orbitalsai/streaming/audio/buffer.py +171 -0
- orbitalsai/streaming/audio/converter.py +327 -0
- orbitalsai/streaming/audio/formats.py +112 -0
- orbitalsai/streaming/audio/source.py +317 -0
- orbitalsai/streaming/client.py +384 -0
- orbitalsai/streaming/config.py +207 -0
- orbitalsai/streaming/connection.py +298 -0
- orbitalsai/streaming/events.py +360 -0
- orbitalsai/streaming/exceptions.py +179 -0
- orbitalsai/streaming/protocol.py +245 -0
- orbitalsai-1.2.0.dist-info/METADATA +850 -0
- orbitalsai-1.2.0.dist-info/RECORD +24 -0
- {orbitalsai-1.1.0.dist-info → orbitalsai-1.2.0.dist-info}/WHEEL +1 -1
- orbitalsai-1.1.0.dist-info/METADATA +0 -491
- orbitalsai-1.1.0.dist-info/RECORD +0 -11
- {orbitalsai-1.1.0.dist-info → orbitalsai-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {orbitalsai-1.1.0.dist-info → orbitalsai-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OrbitalsAI Streaming Configuration
|
|
3
|
+
|
|
4
|
+
Configuration dataclass for streaming transcription sessions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("orbitalsai.streaming")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Supported languages for streaming transcription
|
|
15
|
+
STREAMING_SUPPORTED_LANGUAGES: List[str] = [
|
|
16
|
+
"english", "hausa", "igbo", "yoruba"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
# Valid sample rate range
|
|
20
|
+
MIN_SAMPLE_RATE = 8000
|
|
21
|
+
MAX_SAMPLE_RATE = 48000
|
|
22
|
+
DEFAULT_SAMPLE_RATE = 16000
|
|
23
|
+
|
|
24
|
+
# Default chunk size (samples at 16kHz = 500ms)
|
|
25
|
+
DEFAULT_CHUNK_SIZE = 8000
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class StreamingConfig:
|
|
30
|
+
"""
|
|
31
|
+
Configuration for streaming transcription sessions.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
sample_rate: Audio sample rate in Hz (8000-48000, default: 16000)
|
|
35
|
+
chunk_size: Number of samples per chunk (default: 8000 = 500ms at 16kHz)
|
|
36
|
+
encoding: Audio encoding format (default: "pcm_s16le")
|
|
37
|
+
language: Target transcription language (default: "english")
|
|
38
|
+
auto_detect_language: Whether to auto-detect language (default: False)
|
|
39
|
+
max_retries: Maximum reconnection attempts (default: 5)
|
|
40
|
+
retry_delay: Initial retry delay in seconds (default: 1.0)
|
|
41
|
+
keepalive_interval: WebSocket ping interval in seconds (default: 15.0)
|
|
42
|
+
connection_timeout: Connection timeout in seconds (default: 30.0)
|
|
43
|
+
interim_results: Whether to receive partial transcripts (default: True)
|
|
44
|
+
auto_flush: Whether to auto-flush on silence (default: True)
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
config = StreamingConfig(
|
|
48
|
+
language="hausa",
|
|
49
|
+
sample_rate=16000,
|
|
50
|
+
interim_results=True
|
|
51
|
+
)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
# Audio settings
|
|
55
|
+
sample_rate: int = DEFAULT_SAMPLE_RATE
|
|
56
|
+
chunk_size: int = DEFAULT_CHUNK_SIZE
|
|
57
|
+
encoding: str = "pcm_s16le"
|
|
58
|
+
|
|
59
|
+
# Language settings
|
|
60
|
+
language: str = "english"
|
|
61
|
+
auto_detect_language: bool = False
|
|
62
|
+
|
|
63
|
+
# Connection settings
|
|
64
|
+
max_retries: int = 5
|
|
65
|
+
retry_delay: float = 1.0
|
|
66
|
+
max_retry_delay: float = 60.0
|
|
67
|
+
keepalive_interval: float = 15.0
|
|
68
|
+
connection_timeout: float = 30.0
|
|
69
|
+
|
|
70
|
+
# Processing settings
|
|
71
|
+
interim_results: bool = True
|
|
72
|
+
auto_flush: bool = True
|
|
73
|
+
|
|
74
|
+
def __post_init__(self) -> None:
|
|
75
|
+
"""Validate configuration after initialization."""
|
|
76
|
+
self.validate()
|
|
77
|
+
|
|
78
|
+
def validate(self) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Validate configuration values.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ValueError: If any configuration value is invalid
|
|
84
|
+
"""
|
|
85
|
+
# Validate sample rate
|
|
86
|
+
if not MIN_SAMPLE_RATE <= self.sample_rate <= MAX_SAMPLE_RATE:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"sample_rate must be between {MIN_SAMPLE_RATE} and {MAX_SAMPLE_RATE}, "
|
|
89
|
+
f"got {self.sample_rate}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Validate chunk size
|
|
93
|
+
if self.chunk_size <= 0:
|
|
94
|
+
raise ValueError(f"chunk_size must be positive, got {self.chunk_size}")
|
|
95
|
+
|
|
96
|
+
# Validate language
|
|
97
|
+
if self.language.lower() not in STREAMING_SUPPORTED_LANGUAGES:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Unsupported language: {self.language}. "
|
|
100
|
+
f"Supported languages: {', '.join(STREAMING_SUPPORTED_LANGUAGES)}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Validate connection settings
|
|
104
|
+
if self.max_retries < 0:
|
|
105
|
+
raise ValueError(f"max_retries must be non-negative, got {self.max_retries}")
|
|
106
|
+
|
|
107
|
+
if self.retry_delay <= 0:
|
|
108
|
+
raise ValueError(f"retry_delay must be positive, got {self.retry_delay}")
|
|
109
|
+
|
|
110
|
+
if self.keepalive_interval <= 0:
|
|
111
|
+
raise ValueError(f"keepalive_interval must be positive, got {self.keepalive_interval}")
|
|
112
|
+
|
|
113
|
+
if self.connection_timeout <= 0:
|
|
114
|
+
raise ValueError(f"connection_timeout must be positive, got {self.connection_timeout}")
|
|
115
|
+
|
|
116
|
+
# Validate encoding
|
|
117
|
+
if self.encoding != "pcm_s16le":
|
|
118
|
+
logger.warning(
|
|
119
|
+
f"Only 'pcm_s16le' encoding is currently supported. "
|
|
120
|
+
f"Got '{self.encoding}', using 'pcm_s16le' instead."
|
|
121
|
+
)
|
|
122
|
+
self.encoding = "pcm_s16le"
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def chunk_duration_ms(self) -> float:
|
|
126
|
+
"""
|
|
127
|
+
Calculate chunk duration in milliseconds.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Duration of one chunk in milliseconds
|
|
131
|
+
"""
|
|
132
|
+
return (self.chunk_size / self.sample_rate) * 1000
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def bytes_per_chunk(self) -> int:
|
|
136
|
+
"""
|
|
137
|
+
Calculate bytes per chunk for PCM16 audio.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Number of bytes per chunk (chunk_size * 2 for int16)
|
|
141
|
+
"""
|
|
142
|
+
return self.chunk_size * 2 # 2 bytes per int16 sample
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def bytes_per_second(self) -> int:
|
|
146
|
+
"""
|
|
147
|
+
Calculate bytes per second for the configured sample rate.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Number of bytes per second of audio
|
|
151
|
+
"""
|
|
152
|
+
return self.sample_rate * 2 # 2 bytes per int16 sample
|
|
153
|
+
|
|
154
|
+
def copy(self, **updates) -> 'StreamingConfig':
|
|
155
|
+
"""
|
|
156
|
+
Create a copy of this config with optional updates.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
**updates: Fields to update in the copy
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
New StreamingConfig with updates applied
|
|
163
|
+
"""
|
|
164
|
+
import copy
|
|
165
|
+
new_config = copy.copy(self)
|
|
166
|
+
for key, value in updates.items():
|
|
167
|
+
if hasattr(new_config, key):
|
|
168
|
+
setattr(new_config, key, value)
|
|
169
|
+
else:
|
|
170
|
+
raise ValueError(f"Unknown config field: {key}")
|
|
171
|
+
new_config.validate()
|
|
172
|
+
return new_config
|
|
173
|
+
|
|
174
|
+
def to_dict(self) -> dict:
|
|
175
|
+
"""
|
|
176
|
+
Convert config to dictionary.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Dictionary representation of config
|
|
180
|
+
"""
|
|
181
|
+
return {
|
|
182
|
+
"sample_rate": self.sample_rate,
|
|
183
|
+
"chunk_size": self.chunk_size,
|
|
184
|
+
"encoding": self.encoding,
|
|
185
|
+
"language": self.language,
|
|
186
|
+
"auto_detect_language": self.auto_detect_language,
|
|
187
|
+
"max_retries": self.max_retries,
|
|
188
|
+
"retry_delay": self.retry_delay,
|
|
189
|
+
"max_retry_delay": self.max_retry_delay,
|
|
190
|
+
"keepalive_interval": self.keepalive_interval,
|
|
191
|
+
"connection_timeout": self.connection_timeout,
|
|
192
|
+
"interim_results": self.interim_results,
|
|
193
|
+
"auto_flush": self.auto_flush,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def from_dict(cls, data: dict) -> 'StreamingConfig':
|
|
198
|
+
"""
|
|
199
|
+
Create config from dictionary.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
data: Dictionary with config values
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
New StreamingConfig instance
|
|
206
|
+
"""
|
|
207
|
+
return cls(**{k: v for k, v in data.items() if hasattr(cls, k)})
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OrbitalsAI Streaming Connection Manager
|
|
3
|
+
|
|
4
|
+
WebSocket connection management with auto-reconnection.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import random
|
|
10
|
+
import time
|
|
11
|
+
from typing import Callable, Optional
|
|
12
|
+
|
|
13
|
+
from .protocol import should_retry, get_close_reason, CLOSE_CODES
|
|
14
|
+
from .exceptions import (
|
|
15
|
+
ConnectionError,
|
|
16
|
+
ReconnectionFailedError,
|
|
17
|
+
AuthenticationError,
|
|
18
|
+
ServiceUnavailableError,
|
|
19
|
+
ServerBusyError,
|
|
20
|
+
InsufficientCreditsError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("orbitalsai.streaming")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def calculate_backoff(
|
|
27
|
+
attempt: int,
|
|
28
|
+
base_delay: float = 1.0,
|
|
29
|
+
max_delay: float = 60.0,
|
|
30
|
+
jitter: bool = True
|
|
31
|
+
) -> float:
|
|
32
|
+
"""
|
|
33
|
+
Calculate exponential backoff delay with optional jitter.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
attempt: Current retry attempt (0-indexed)
|
|
37
|
+
base_delay: Initial delay in seconds
|
|
38
|
+
max_delay: Maximum delay in seconds
|
|
39
|
+
jitter: Whether to add random jitter
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Delay in seconds
|
|
43
|
+
"""
|
|
44
|
+
delay = min(base_delay * (2 ** attempt), max_delay)
|
|
45
|
+
|
|
46
|
+
if jitter:
|
|
47
|
+
# Add 0-10% jitter
|
|
48
|
+
jitter_amount = random.uniform(0, delay * 0.1)
|
|
49
|
+
delay += jitter_amount
|
|
50
|
+
|
|
51
|
+
return delay
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ConnectionState:
|
|
55
|
+
"""Connection state enumeration."""
|
|
56
|
+
DISCONNECTED = "disconnected"
|
|
57
|
+
CONNECTING = "connecting"
|
|
58
|
+
CONNECTED = "connected"
|
|
59
|
+
RECONNECTING = "reconnecting"
|
|
60
|
+
CLOSED = "closed"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ConnectionManager:
|
|
64
|
+
"""
|
|
65
|
+
Manages WebSocket connection with auto-reconnection.
|
|
66
|
+
|
|
67
|
+
Handles:
|
|
68
|
+
- Initial connection establishment
|
|
69
|
+
- Automatic reconnection on connection loss
|
|
70
|
+
- Exponential backoff with jitter
|
|
71
|
+
- Close code handling
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
manager = ConnectionManager(
|
|
75
|
+
max_retries=5,
|
|
76
|
+
base_delay=1.0,
|
|
77
|
+
max_delay=60.0
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
async with manager.connect(ws_url) as ws:
|
|
81
|
+
await ws.send(data)
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
max_retries: int = 5,
|
|
87
|
+
base_delay: float = 1.0,
|
|
88
|
+
max_delay: float = 60.0,
|
|
89
|
+
connection_timeout: float = 30.0,
|
|
90
|
+
on_reconnect: Optional[Callable[[], None]] = None,
|
|
91
|
+
on_reconnect_failed: Optional[Callable[[int], None]] = None,
|
|
92
|
+
):
|
|
93
|
+
"""
|
|
94
|
+
Initialize connection manager.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
max_retries: Maximum number of reconnection attempts
|
|
98
|
+
base_delay: Initial retry delay in seconds
|
|
99
|
+
max_delay: Maximum retry delay in seconds
|
|
100
|
+
connection_timeout: Connection timeout in seconds
|
|
101
|
+
on_reconnect: Callback when reconnection starts
|
|
102
|
+
on_reconnect_failed: Callback when reconnection fails (receives attempt count)
|
|
103
|
+
"""
|
|
104
|
+
self.max_retries = max_retries
|
|
105
|
+
self.base_delay = base_delay
|
|
106
|
+
self.max_delay = max_delay
|
|
107
|
+
self.connection_timeout = connection_timeout
|
|
108
|
+
self.on_reconnect = on_reconnect
|
|
109
|
+
self.on_reconnect_failed = on_reconnect_failed
|
|
110
|
+
|
|
111
|
+
self._state = ConnectionState.DISCONNECTED
|
|
112
|
+
self._retry_count = 0
|
|
113
|
+
self._last_close_code: Optional[int] = None
|
|
114
|
+
self._last_close_reason: Optional[str] = None
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def state(self) -> str:
|
|
118
|
+
"""Get current connection state."""
|
|
119
|
+
return self._state
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def is_connected(self) -> bool:
|
|
123
|
+
"""Check if currently connected."""
|
|
124
|
+
return self._state == ConnectionState.CONNECTED
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def retry_count(self) -> int:
|
|
128
|
+
"""Get current retry count."""
|
|
129
|
+
return self._retry_count
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def last_close_code(self) -> Optional[int]:
|
|
133
|
+
"""Get last WebSocket close code."""
|
|
134
|
+
return self._last_close_code
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def last_close_reason(self) -> Optional[str]:
|
|
138
|
+
"""Get last WebSocket close reason."""
|
|
139
|
+
return self._last_close_reason
|
|
140
|
+
|
|
141
|
+
def reset(self) -> None:
|
|
142
|
+
"""Reset connection state and retry count."""
|
|
143
|
+
self._state = ConnectionState.DISCONNECTED
|
|
144
|
+
self._retry_count = 0
|
|
145
|
+
self._last_close_code = None
|
|
146
|
+
self._last_close_reason = None
|
|
147
|
+
|
|
148
|
+
def record_close(self, code: int, reason: str = "") -> None:
|
|
149
|
+
"""
|
|
150
|
+
Record a connection close event.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
code: WebSocket close code
|
|
154
|
+
reason: Close reason string
|
|
155
|
+
"""
|
|
156
|
+
self._last_close_code = code
|
|
157
|
+
self._last_close_reason = reason or get_close_reason(code)
|
|
158
|
+
self._state = ConnectionState.DISCONNECTED
|
|
159
|
+
|
|
160
|
+
def should_reconnect(self, close_code: Optional[int] = None) -> bool:
|
|
161
|
+
"""
|
|
162
|
+
Determine if reconnection should be attempted.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
close_code: WebSocket close code (uses last recorded if not provided)
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
True if should attempt reconnection
|
|
169
|
+
"""
|
|
170
|
+
code = close_code or self._last_close_code
|
|
171
|
+
|
|
172
|
+
if code is None:
|
|
173
|
+
return True # Unknown close, try to reconnect
|
|
174
|
+
|
|
175
|
+
# Check if we've exhausted retries
|
|
176
|
+
if self._retry_count >= self.max_retries:
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
# Check if this close code allows retry
|
|
180
|
+
return should_retry(code)
|
|
181
|
+
|
|
182
|
+
def get_retry_delay(self) -> float:
|
|
183
|
+
"""
|
|
184
|
+
Get delay before next retry attempt.
|
|
185
|
+
|
|
186
|
+
Uses exponential backoff with jitter.
|
|
187
|
+
May use longer delays for certain close codes.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Delay in seconds
|
|
191
|
+
"""
|
|
192
|
+
base = self.base_delay
|
|
193
|
+
|
|
194
|
+
# Use longer base delay for server busy
|
|
195
|
+
if self._last_close_code == 4003: # Server busy
|
|
196
|
+
base = self.base_delay * 2
|
|
197
|
+
|
|
198
|
+
return calculate_backoff(
|
|
199
|
+
self._retry_count,
|
|
200
|
+
base_delay=base,
|
|
201
|
+
max_delay=self.max_delay,
|
|
202
|
+
jitter=True
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
async def wait_for_retry(self) -> None:
|
|
206
|
+
"""Wait before retry attempt."""
|
|
207
|
+
delay = self.get_retry_delay()
|
|
208
|
+
logger.info(f"Waiting {delay:.1f}s before retry attempt {self._retry_count + 1}")
|
|
209
|
+
await asyncio.sleep(delay)
|
|
210
|
+
|
|
211
|
+
def increment_retry(self) -> int:
|
|
212
|
+
"""
|
|
213
|
+
Increment retry counter.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
New retry count
|
|
217
|
+
"""
|
|
218
|
+
self._retry_count += 1
|
|
219
|
+
self._state = ConnectionState.RECONNECTING
|
|
220
|
+
|
|
221
|
+
if self.on_reconnect:
|
|
222
|
+
try:
|
|
223
|
+
self.on_reconnect()
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.warning(f"on_reconnect callback error: {e}")
|
|
226
|
+
|
|
227
|
+
return self._retry_count
|
|
228
|
+
|
|
229
|
+
def reset_retry_count(self) -> None:
|
|
230
|
+
"""Reset retry counter (call after successful connection)."""
|
|
231
|
+
self._retry_count = 0
|
|
232
|
+
|
|
233
|
+
def mark_connected(self) -> None:
|
|
234
|
+
"""Mark connection as established."""
|
|
235
|
+
self._state = ConnectionState.CONNECTED
|
|
236
|
+
self.reset_retry_count()
|
|
237
|
+
|
|
238
|
+
def mark_connecting(self) -> None:
|
|
239
|
+
"""Mark connection as in progress."""
|
|
240
|
+
self._state = ConnectionState.CONNECTING
|
|
241
|
+
|
|
242
|
+
def mark_closed(self) -> None:
|
|
243
|
+
"""Mark connection as permanently closed."""
|
|
244
|
+
self._state = ConnectionState.CLOSED
|
|
245
|
+
|
|
246
|
+
def raise_for_close_code(self, code: int, reason: str = "") -> None:
|
|
247
|
+
"""
|
|
248
|
+
Raise appropriate exception for close code.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
code: WebSocket close code
|
|
252
|
+
reason: Close reason string
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
Appropriate exception for the close code
|
|
256
|
+
"""
|
|
257
|
+
self.record_close(code, reason)
|
|
258
|
+
|
|
259
|
+
reason = reason or get_close_reason(code)
|
|
260
|
+
|
|
261
|
+
if code == 4001:
|
|
262
|
+
raise AuthenticationError(reason)
|
|
263
|
+
elif code == 4002:
|
|
264
|
+
raise ServiceUnavailableError(reason)
|
|
265
|
+
elif code == 4003:
|
|
266
|
+
raise ServerBusyError(reason)
|
|
267
|
+
elif code in (4004, 4005):
|
|
268
|
+
raise InsufficientCreditsError(reason)
|
|
269
|
+
else:
|
|
270
|
+
raise ConnectionError(reason, code=code)
|
|
271
|
+
|
|
272
|
+
def check_can_reconnect(self) -> None:
|
|
273
|
+
"""
|
|
274
|
+
Check if reconnection is possible.
|
|
275
|
+
|
|
276
|
+
Raises:
|
|
277
|
+
ReconnectionFailedError: If max retries exceeded
|
|
278
|
+
Other exceptions: For non-retryable close codes
|
|
279
|
+
"""
|
|
280
|
+
if not self.should_reconnect():
|
|
281
|
+
if self._retry_count >= self.max_retries:
|
|
282
|
+
if self.on_reconnect_failed:
|
|
283
|
+
try:
|
|
284
|
+
self.on_reconnect_failed(self._retry_count)
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.warning(f"on_reconnect_failed callback error: {e}")
|
|
287
|
+
|
|
288
|
+
raise ReconnectionFailedError(
|
|
289
|
+
f"Failed to reconnect after {self._retry_count} attempts",
|
|
290
|
+
attempts=self._retry_count
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Non-retryable close code
|
|
294
|
+
if self._last_close_code:
|
|
295
|
+
self.raise_for_close_code(
|
|
296
|
+
self._last_close_code,
|
|
297
|
+
self._last_close_reason or ""
|
|
298
|
+
)
|