kstlib 0.0.1a0__py3-none-any.whl → 1.0.1__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.
- kstlib/__init__.py +266 -1
- kstlib/__main__.py +16 -0
- kstlib/alerts/__init__.py +110 -0
- kstlib/alerts/channels/__init__.py +36 -0
- kstlib/alerts/channels/base.py +197 -0
- kstlib/alerts/channels/email.py +227 -0
- kstlib/alerts/channels/slack.py +389 -0
- kstlib/alerts/exceptions.py +72 -0
- kstlib/alerts/manager.py +651 -0
- kstlib/alerts/models.py +142 -0
- kstlib/alerts/throttle.py +263 -0
- kstlib/auth/__init__.py +139 -0
- kstlib/auth/callback.py +399 -0
- kstlib/auth/config.py +502 -0
- kstlib/auth/errors.py +127 -0
- kstlib/auth/models.py +316 -0
- kstlib/auth/providers/__init__.py +14 -0
- kstlib/auth/providers/base.py +393 -0
- kstlib/auth/providers/oauth2.py +645 -0
- kstlib/auth/providers/oidc.py +821 -0
- kstlib/auth/session.py +338 -0
- kstlib/auth/token.py +482 -0
- kstlib/cache/__init__.py +50 -0
- kstlib/cache/decorator.py +261 -0
- kstlib/cache/strategies.py +516 -0
- kstlib/cli/__init__.py +8 -0
- kstlib/cli/app.py +195 -0
- kstlib/cli/commands/__init__.py +5 -0
- kstlib/cli/commands/auth/__init__.py +39 -0
- kstlib/cli/commands/auth/common.py +122 -0
- kstlib/cli/commands/auth/login.py +325 -0
- kstlib/cli/commands/auth/logout.py +74 -0
- kstlib/cli/commands/auth/providers.py +57 -0
- kstlib/cli/commands/auth/status.py +291 -0
- kstlib/cli/commands/auth/token.py +199 -0
- kstlib/cli/commands/auth/whoami.py +106 -0
- kstlib/cli/commands/config.py +89 -0
- kstlib/cli/commands/ops/__init__.py +39 -0
- kstlib/cli/commands/ops/attach.py +49 -0
- kstlib/cli/commands/ops/common.py +269 -0
- kstlib/cli/commands/ops/list_sessions.py +252 -0
- kstlib/cli/commands/ops/logs.py +49 -0
- kstlib/cli/commands/ops/start.py +98 -0
- kstlib/cli/commands/ops/status.py +138 -0
- kstlib/cli/commands/ops/stop.py +60 -0
- kstlib/cli/commands/rapi/__init__.py +60 -0
- kstlib/cli/commands/rapi/call.py +341 -0
- kstlib/cli/commands/rapi/list.py +99 -0
- kstlib/cli/commands/rapi/show.py +206 -0
- kstlib/cli/commands/secrets/__init__.py +35 -0
- kstlib/cli/commands/secrets/common.py +425 -0
- kstlib/cli/commands/secrets/decrypt.py +88 -0
- kstlib/cli/commands/secrets/doctor.py +743 -0
- kstlib/cli/commands/secrets/encrypt.py +242 -0
- kstlib/cli/commands/secrets/shred.py +96 -0
- kstlib/cli/common.py +86 -0
- kstlib/config/__init__.py +76 -0
- kstlib/config/exceptions.py +110 -0
- kstlib/config/export.py +225 -0
- kstlib/config/loader.py +963 -0
- kstlib/config/sops.py +287 -0
- kstlib/db/__init__.py +54 -0
- kstlib/db/aiosqlcipher.py +137 -0
- kstlib/db/cipher.py +112 -0
- kstlib/db/database.py +367 -0
- kstlib/db/exceptions.py +25 -0
- kstlib/db/pool.py +302 -0
- kstlib/helpers/__init__.py +35 -0
- kstlib/helpers/exceptions.py +11 -0
- kstlib/helpers/time_trigger.py +396 -0
- kstlib/kstlib.conf.yml +890 -0
- kstlib/limits.py +963 -0
- kstlib/logging/__init__.py +108 -0
- kstlib/logging/manager.py +633 -0
- kstlib/mail/__init__.py +42 -0
- kstlib/mail/builder.py +626 -0
- kstlib/mail/exceptions.py +27 -0
- kstlib/mail/filesystem.py +248 -0
- kstlib/mail/transport.py +224 -0
- kstlib/mail/transports/__init__.py +19 -0
- kstlib/mail/transports/gmail.py +268 -0
- kstlib/mail/transports/resend.py +324 -0
- kstlib/mail/transports/smtp.py +326 -0
- kstlib/meta.py +72 -0
- kstlib/metrics/__init__.py +88 -0
- kstlib/metrics/decorators.py +1090 -0
- kstlib/metrics/exceptions.py +14 -0
- kstlib/monitoring/__init__.py +116 -0
- kstlib/monitoring/_styles.py +163 -0
- kstlib/monitoring/cell.py +57 -0
- kstlib/monitoring/config.py +424 -0
- kstlib/monitoring/delivery.py +579 -0
- kstlib/monitoring/exceptions.py +63 -0
- kstlib/monitoring/image.py +220 -0
- kstlib/monitoring/kv.py +79 -0
- kstlib/monitoring/list.py +69 -0
- kstlib/monitoring/metric.py +88 -0
- kstlib/monitoring/monitoring.py +341 -0
- kstlib/monitoring/renderer.py +139 -0
- kstlib/monitoring/service.py +392 -0
- kstlib/monitoring/table.py +129 -0
- kstlib/monitoring/types.py +56 -0
- kstlib/ops/__init__.py +86 -0
- kstlib/ops/base.py +148 -0
- kstlib/ops/container.py +577 -0
- kstlib/ops/exceptions.py +209 -0
- kstlib/ops/manager.py +407 -0
- kstlib/ops/models.py +176 -0
- kstlib/ops/tmux.py +372 -0
- kstlib/ops/validators.py +287 -0
- kstlib/py.typed +0 -0
- kstlib/rapi/__init__.py +118 -0
- kstlib/rapi/client.py +875 -0
- kstlib/rapi/config.py +861 -0
- kstlib/rapi/credentials.py +887 -0
- kstlib/rapi/exceptions.py +213 -0
- kstlib/resilience/__init__.py +101 -0
- kstlib/resilience/circuit_breaker.py +440 -0
- kstlib/resilience/exceptions.py +95 -0
- kstlib/resilience/heartbeat.py +491 -0
- kstlib/resilience/rate_limiter.py +506 -0
- kstlib/resilience/shutdown.py +417 -0
- kstlib/resilience/watchdog.py +637 -0
- kstlib/secrets/__init__.py +29 -0
- kstlib/secrets/exceptions.py +19 -0
- kstlib/secrets/models.py +62 -0
- kstlib/secrets/providers/__init__.py +79 -0
- kstlib/secrets/providers/base.py +58 -0
- kstlib/secrets/providers/environment.py +66 -0
- kstlib/secrets/providers/keyring.py +107 -0
- kstlib/secrets/providers/kms.py +223 -0
- kstlib/secrets/providers/kwargs.py +101 -0
- kstlib/secrets/providers/sops.py +209 -0
- kstlib/secrets/resolver.py +221 -0
- kstlib/secrets/sensitive.py +130 -0
- kstlib/secure/__init__.py +23 -0
- kstlib/secure/fs.py +194 -0
- kstlib/secure/permissions.py +70 -0
- kstlib/ssl.py +347 -0
- kstlib/ui/__init__.py +23 -0
- kstlib/ui/exceptions.py +26 -0
- kstlib/ui/panels.py +484 -0
- kstlib/ui/spinner.py +864 -0
- kstlib/ui/tables.py +382 -0
- kstlib/utils/__init__.py +48 -0
- kstlib/utils/dict.py +36 -0
- kstlib/utils/formatting.py +338 -0
- kstlib/utils/http_trace.py +237 -0
- kstlib/utils/lazy.py +49 -0
- kstlib/utils/secure_delete.py +205 -0
- kstlib/utils/serialization.py +247 -0
- kstlib/utils/text.py +56 -0
- kstlib/utils/validators.py +124 -0
- kstlib/websocket/__init__.py +97 -0
- kstlib/websocket/exceptions.py +214 -0
- kstlib/websocket/manager.py +1102 -0
- kstlib/websocket/models.py +361 -0
- kstlib-1.0.1.dist-info/METADATA +201 -0
- kstlib-1.0.1.dist-info/RECORD +163 -0
- {kstlib-0.0.1a0.dist-info → kstlib-1.0.1.dist-info}/WHEEL +1 -1
- kstlib-1.0.1.dist-info/entry_points.txt +2 -0
- kstlib-1.0.1.dist-info/licenses/LICENSE.md +9 -0
- kstlib-0.0.1a0.dist-info/METADATA +0 -29
- kstlib-0.0.1a0.dist-info/RECORD +0 -6
- kstlib-0.0.1a0.dist-info/licenses/LICENSE.md +0 -5
- {kstlib-0.0.1a0.dist-info → kstlib-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""WebSocket data models and enumerations.
|
|
2
|
+
|
|
3
|
+
This module provides the core data structures for the WebSocket manager:
|
|
4
|
+
|
|
5
|
+
- **ConnectionState**: State machine for connection lifecycle
|
|
6
|
+
- **DisconnectReason**: Categorizes disconnection causes (proactive vs reactive)
|
|
7
|
+
- **ReconnectStrategy**: Available reconnection strategies
|
|
8
|
+
- **WebSocketStats**: Connection and message statistics
|
|
9
|
+
|
|
10
|
+
Examples:
|
|
11
|
+
>>> from kstlib.websocket.models import ConnectionState, DisconnectReason
|
|
12
|
+
>>> state = ConnectionState.CONNECTED
|
|
13
|
+
>>> reason = DisconnectReason.USER_REQUESTED
|
|
14
|
+
>>> reason.is_proactive
|
|
15
|
+
True
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import time
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from enum import Enum, auto
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"ConnectionState",
|
|
26
|
+
"DisconnectReason",
|
|
27
|
+
"ReconnectStrategy",
|
|
28
|
+
"WebSocketStats",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ConnectionState(Enum):
|
|
33
|
+
"""WebSocket connection state machine.
|
|
34
|
+
|
|
35
|
+
State transitions:
|
|
36
|
+
DISCONNECTED -> CONNECTING -> CONNECTED
|
|
37
|
+
CONNECTED -> RECONNECTING -> CONNECTED (on success)
|
|
38
|
+
CONNECTED -> RECONNECTING -> DISCONNECTED (on failure)
|
|
39
|
+
CONNECTED -> CLOSING -> CLOSED
|
|
40
|
+
Any state -> CLOSED (on force_close)
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
DISCONNECTED: Initial state, not connected.
|
|
44
|
+
CONNECTING: Connection attempt in progress.
|
|
45
|
+
CONNECTED: WebSocket connection is active.
|
|
46
|
+
RECONNECTING: Attempting to restore lost connection.
|
|
47
|
+
CLOSING: Graceful shutdown in progress.
|
|
48
|
+
CLOSED: Terminal state, cannot reconnect.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
DISCONNECTED = auto()
|
|
52
|
+
CONNECTING = auto()
|
|
53
|
+
CONNECTED = auto()
|
|
54
|
+
RECONNECTING = auto()
|
|
55
|
+
CLOSING = auto()
|
|
56
|
+
CLOSED = auto()
|
|
57
|
+
|
|
58
|
+
def can_connect(self) -> bool:
|
|
59
|
+
"""Check if a connection attempt is allowed from this state.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
True if connect() can be called from this state.
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
>>> ConnectionState.DISCONNECTED.can_connect()
|
|
66
|
+
True
|
|
67
|
+
>>> ConnectionState.CONNECTED.can_connect()
|
|
68
|
+
False
|
|
69
|
+
"""
|
|
70
|
+
return self in (ConnectionState.DISCONNECTED, ConnectionState.RECONNECTING)
|
|
71
|
+
|
|
72
|
+
def can_send(self) -> bool:
|
|
73
|
+
"""Check if sending messages is allowed from this state.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
True if send() can be called from this state.
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
>>> ConnectionState.CONNECTED.can_send()
|
|
80
|
+
True
|
|
81
|
+
>>> ConnectionState.DISCONNECTED.can_send()
|
|
82
|
+
False
|
|
83
|
+
"""
|
|
84
|
+
return self == ConnectionState.CONNECTED
|
|
85
|
+
|
|
86
|
+
def is_terminal(self) -> bool:
|
|
87
|
+
"""Check if this is a terminal state.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
True if no further state transitions are possible.
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
>>> ConnectionState.CLOSED.is_terminal()
|
|
94
|
+
True
|
|
95
|
+
>>> ConnectionState.DISCONNECTED.is_terminal()
|
|
96
|
+
False
|
|
97
|
+
"""
|
|
98
|
+
return self == ConnectionState.CLOSED
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class DisconnectReason(Enum):
|
|
102
|
+
"""Reason for WebSocket disconnection.
|
|
103
|
+
|
|
104
|
+
Disconnections are categorized as either proactive (user-controlled)
|
|
105
|
+
or reactive (forced by external factors). This distinction is key
|
|
106
|
+
for the proactive connection control feature.
|
|
107
|
+
|
|
108
|
+
Proactive reasons (user-controlled):
|
|
109
|
+
USER_REQUESTED: Manual disconnect via request_disconnect()
|
|
110
|
+
SCHEDULED: Disconnect triggered by schedule_reconnect()
|
|
111
|
+
CALLBACK_TRIGGERED: should_disconnect() callback returned True
|
|
112
|
+
CONNECTION_LIMIT: Preemptive disconnect before platform limit
|
|
113
|
+
|
|
114
|
+
Reactive reasons (forced):
|
|
115
|
+
SERVER_CLOSED: Server initiated the close
|
|
116
|
+
NETWORK_ERROR: Network connectivity issue
|
|
117
|
+
PING_TIMEOUT: No pong response within timeout
|
|
118
|
+
PROTOCOL_ERROR: WebSocket protocol violation
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
# Proactive (user-controlled) disconnections
|
|
122
|
+
USER_REQUESTED = auto()
|
|
123
|
+
SCHEDULED = auto()
|
|
124
|
+
CALLBACK_TRIGGERED = auto()
|
|
125
|
+
CONNECTION_LIMIT = auto()
|
|
126
|
+
|
|
127
|
+
# Reactive (forced) disconnections
|
|
128
|
+
SERVER_CLOSED = auto()
|
|
129
|
+
NETWORK_ERROR = auto()
|
|
130
|
+
PING_TIMEOUT = auto()
|
|
131
|
+
PROTOCOL_ERROR = auto()
|
|
132
|
+
KILLED = auto() # Simulated external kill (e.g., Binance forced disconnect)
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def is_proactive(self) -> bool:
|
|
136
|
+
"""Check if this is a proactive (user-controlled) disconnection.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
True if the disconnection was initiated by the user/application.
|
|
140
|
+
|
|
141
|
+
Examples:
|
|
142
|
+
>>> DisconnectReason.USER_REQUESTED.is_proactive
|
|
143
|
+
True
|
|
144
|
+
>>> DisconnectReason.NETWORK_ERROR.is_proactive
|
|
145
|
+
False
|
|
146
|
+
"""
|
|
147
|
+
return self in (
|
|
148
|
+
DisconnectReason.USER_REQUESTED,
|
|
149
|
+
DisconnectReason.SCHEDULED,
|
|
150
|
+
DisconnectReason.CALLBACK_TRIGGERED,
|
|
151
|
+
DisconnectReason.CONNECTION_LIMIT,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def is_reactive(self) -> bool:
|
|
156
|
+
"""Check if this is a reactive (forced) disconnection.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
True if the disconnection was forced by external factors.
|
|
160
|
+
|
|
161
|
+
Examples:
|
|
162
|
+
>>> DisconnectReason.SERVER_CLOSED.is_reactive
|
|
163
|
+
True
|
|
164
|
+
>>> DisconnectReason.USER_REQUESTED.is_reactive
|
|
165
|
+
False
|
|
166
|
+
"""
|
|
167
|
+
return not self.is_proactive
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ReconnectStrategy(Enum):
|
|
171
|
+
"""Reconnection strategy after disconnection.
|
|
172
|
+
|
|
173
|
+
Attributes:
|
|
174
|
+
IMMEDIATE: Reconnect immediately without delay.
|
|
175
|
+
FIXED_DELAY: Wait a fixed delay before each attempt.
|
|
176
|
+
EXPONENTIAL_BACKOFF: Exponentially increasing delays.
|
|
177
|
+
CALLBACK_CONTROLLED: Reconnection timing controlled by callback.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
IMMEDIATE = auto()
|
|
181
|
+
FIXED_DELAY = auto()
|
|
182
|
+
EXPONENTIAL_BACKOFF = auto()
|
|
183
|
+
CALLBACK_CONTROLLED = auto()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass
|
|
187
|
+
class WebSocketStats:
|
|
188
|
+
"""WebSocket connection and message statistics.
|
|
189
|
+
|
|
190
|
+
Tracks both connection lifecycle events and message throughput.
|
|
191
|
+
The key distinction is between proactive and reactive disconnections,
|
|
192
|
+
which is central to the proactive control feature.
|
|
193
|
+
|
|
194
|
+
Attributes:
|
|
195
|
+
connects: Total successful connection count.
|
|
196
|
+
disconnects: Total disconnection count.
|
|
197
|
+
proactive_disconnects: Disconnections initiated by user/application.
|
|
198
|
+
reactive_disconnects: Disconnections forced by external factors.
|
|
199
|
+
messages_received: Total messages received.
|
|
200
|
+
messages_sent: Total messages sent.
|
|
201
|
+
bytes_received: Total bytes received.
|
|
202
|
+
bytes_sent: Total bytes sent.
|
|
203
|
+
last_connect_time: Unix timestamp of last successful connection.
|
|
204
|
+
last_disconnect_time: Unix timestamp of last disconnection.
|
|
205
|
+
last_message_time: Unix timestamp of last message (sent or received).
|
|
206
|
+
|
|
207
|
+
Examples:
|
|
208
|
+
>>> stats = WebSocketStats()
|
|
209
|
+
>>> stats.record_connect()
|
|
210
|
+
>>> stats.connects
|
|
211
|
+
1
|
|
212
|
+
>>> stats.record_disconnect(proactive=True)
|
|
213
|
+
>>> stats.proactive_disconnects
|
|
214
|
+
1
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
connects: int = 0
|
|
218
|
+
disconnects: int = 0
|
|
219
|
+
proactive_disconnects: int = 0
|
|
220
|
+
reactive_disconnects: int = 0
|
|
221
|
+
messages_received: int = 0
|
|
222
|
+
messages_sent: int = 0
|
|
223
|
+
bytes_received: int = 0
|
|
224
|
+
bytes_sent: int = 0
|
|
225
|
+
last_connect_time: float = 0.0
|
|
226
|
+
last_disconnect_time: float = 0.0
|
|
227
|
+
last_message_time: float = 0.0
|
|
228
|
+
_start_time: float = field(default_factory=time.monotonic)
|
|
229
|
+
|
|
230
|
+
def record_connect(self) -> None:
|
|
231
|
+
"""Record a successful connection.
|
|
232
|
+
|
|
233
|
+
Examples:
|
|
234
|
+
>>> stats = WebSocketStats()
|
|
235
|
+
>>> stats.record_connect()
|
|
236
|
+
>>> stats.connects
|
|
237
|
+
1
|
|
238
|
+
"""
|
|
239
|
+
self.connects += 1
|
|
240
|
+
self.last_connect_time = time.time()
|
|
241
|
+
|
|
242
|
+
def record_disconnect(self, *, proactive: bool = False) -> None:
|
|
243
|
+
"""Record a disconnection.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
proactive: True if this was a user-initiated disconnection.
|
|
247
|
+
|
|
248
|
+
Examples:
|
|
249
|
+
>>> stats = WebSocketStats()
|
|
250
|
+
>>> stats.record_disconnect(proactive=True)
|
|
251
|
+
>>> stats.proactive_disconnects
|
|
252
|
+
1
|
|
253
|
+
>>> stats.record_disconnect(proactive=False)
|
|
254
|
+
>>> stats.reactive_disconnects
|
|
255
|
+
1
|
|
256
|
+
"""
|
|
257
|
+
self.disconnects += 1
|
|
258
|
+
self.last_disconnect_time = time.time()
|
|
259
|
+
if proactive:
|
|
260
|
+
self.proactive_disconnects += 1
|
|
261
|
+
else:
|
|
262
|
+
self.reactive_disconnects += 1
|
|
263
|
+
|
|
264
|
+
def record_message_received(self, size: int = 0) -> None:
|
|
265
|
+
"""Record a received message.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
size: Size of the message in bytes.
|
|
269
|
+
|
|
270
|
+
Examples:
|
|
271
|
+
>>> stats = WebSocketStats()
|
|
272
|
+
>>> stats.record_message_received(100)
|
|
273
|
+
>>> stats.messages_received
|
|
274
|
+
1
|
|
275
|
+
>>> stats.bytes_received
|
|
276
|
+
100
|
|
277
|
+
"""
|
|
278
|
+
self.messages_received += 1
|
|
279
|
+
self.bytes_received += size
|
|
280
|
+
self.last_message_time = time.time()
|
|
281
|
+
|
|
282
|
+
def record_message_sent(self, size: int = 0) -> None:
|
|
283
|
+
"""Record a sent message.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
size: Size of the message in bytes.
|
|
287
|
+
|
|
288
|
+
Examples:
|
|
289
|
+
>>> stats = WebSocketStats()
|
|
290
|
+
>>> stats.record_message_sent(50)
|
|
291
|
+
>>> stats.messages_sent
|
|
292
|
+
1
|
|
293
|
+
>>> stats.bytes_sent
|
|
294
|
+
50
|
|
295
|
+
"""
|
|
296
|
+
self.messages_sent += 1
|
|
297
|
+
self.bytes_sent += size
|
|
298
|
+
self.last_message_time = time.time()
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def uptime(self) -> float:
|
|
302
|
+
"""Time since stats object was created, in seconds.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Elapsed time in seconds.
|
|
306
|
+
|
|
307
|
+
Examples:
|
|
308
|
+
>>> import time
|
|
309
|
+
>>> stats = WebSocketStats()
|
|
310
|
+
>>> time.sleep(0.05)
|
|
311
|
+
>>> stats.uptime > 0
|
|
312
|
+
True
|
|
313
|
+
"""
|
|
314
|
+
return time.monotonic() - self._start_time
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def connection_time(self) -> float:
|
|
318
|
+
"""Time since last connection, in seconds.
|
|
319
|
+
|
|
320
|
+
Returns zero if never connected.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Elapsed time since last connect, or 0 if never connected.
|
|
324
|
+
|
|
325
|
+
Examples:
|
|
326
|
+
>>> stats = WebSocketStats()
|
|
327
|
+
>>> stats.connection_time
|
|
328
|
+
0.0
|
|
329
|
+
>>> stats.record_connect()
|
|
330
|
+
>>> stats.connection_time > 0 or stats.connection_time == 0.0
|
|
331
|
+
True
|
|
332
|
+
"""
|
|
333
|
+
if self.last_connect_time == 0.0:
|
|
334
|
+
return 0.0
|
|
335
|
+
return time.time() - self.last_connect_time
|
|
336
|
+
|
|
337
|
+
def reset(self) -> None:
|
|
338
|
+
"""Reset all statistics to zero.
|
|
339
|
+
|
|
340
|
+
Examples:
|
|
341
|
+
>>> stats = WebSocketStats()
|
|
342
|
+
>>> stats.record_connect()
|
|
343
|
+
>>> stats.record_message_sent(100)
|
|
344
|
+
>>> stats.reset()
|
|
345
|
+
>>> stats.connects
|
|
346
|
+
0
|
|
347
|
+
>>> stats.messages_sent
|
|
348
|
+
0
|
|
349
|
+
"""
|
|
350
|
+
self.connects = 0
|
|
351
|
+
self.disconnects = 0
|
|
352
|
+
self.proactive_disconnects = 0
|
|
353
|
+
self.reactive_disconnects = 0
|
|
354
|
+
self.messages_received = 0
|
|
355
|
+
self.messages_sent = 0
|
|
356
|
+
self.bytes_received = 0
|
|
357
|
+
self.bytes_sent = 0
|
|
358
|
+
self.last_connect_time = 0.0
|
|
359
|
+
self.last_disconnect_time = 0.0
|
|
360
|
+
self.last_message_time = 0.0
|
|
361
|
+
self._start_time = time.monotonic()
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kstlib
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Config-driven helpers for Python projects (dynamic config, secure secrets, preset logging, and more…)
|
|
5
|
+
Author-email: Michel TRUONG <michel.truong@gmail.com>
|
|
6
|
+
Maintainer-email: Michel TRUONG <michel.truong@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/KaminoU/kstlib
|
|
9
|
+
Project-URL: Repository, https://github.com/KaminoU/kstlib
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/KaminoU/kstlib/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/KaminoU/kstlib/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: kstlib
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE.md
|
|
24
|
+
Requires-Dist: pyyaml<7,>=6.0
|
|
25
|
+
Requires-Dist: tomli<3,>=2.3
|
|
26
|
+
Requires-Dist: tomli-w<2,>=1.0
|
|
27
|
+
Requires-Dist: python-box<8,>=7.3
|
|
28
|
+
Requires-Dist: typer<1,>=0.19
|
|
29
|
+
Requires-Dist: click<9,>=8.3
|
|
30
|
+
Requires-Dist: rich<15,>=14.2
|
|
31
|
+
Requires-Dist: structlog<26,>=25.0
|
|
32
|
+
Requires-Dist: aiosqlite<1,>=0.21
|
|
33
|
+
Requires-Dist: aiosmtplib<5,>=4.0
|
|
34
|
+
Requires-Dist: websockets<16,>=15.0
|
|
35
|
+
Requires-Dist: jinja2<4,>=3.1
|
|
36
|
+
Requires-Dist: humanize<5,>=4.11
|
|
37
|
+
Requires-Dist: httpx<1,>=0.28
|
|
38
|
+
Requires-Dist: authlib<2,>=1.5
|
|
39
|
+
Requires-Dist: pendulum<4,>=3.0
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: pytest<9,>=8.4; extra == "dev"
|
|
42
|
+
Requires-Dist: pytest-cov<8,>=7.0; extra == "dev"
|
|
43
|
+
Requires-Dist: pytest-asyncio<2,>=1.2; extra == "dev"
|
|
44
|
+
Requires-Dist: ruff<1,>=0.14; extra == "dev"
|
|
45
|
+
Requires-Dist: mypy<2,>=1.18; extra == "dev"
|
|
46
|
+
Requires-Dist: types-PyYAML<7,>=6.0; extra == "dev"
|
|
47
|
+
Requires-Dist: pre-commit<5,>=4.0; extra == "dev"
|
|
48
|
+
Requires-Dist: boto3<2,>=1.35; extra == "dev"
|
|
49
|
+
Provides-Extra: db-crypto
|
|
50
|
+
Requires-Dist: sqlcipher3<1,>=0.5; extra == "db-crypto"
|
|
51
|
+
Provides-Extra: docs
|
|
52
|
+
Requires-Dist: sphinx<9,>=8.1; extra == "docs"
|
|
53
|
+
Requires-Dist: furo<2026,>=2025.9; extra == "docs"
|
|
54
|
+
Requires-Dist: myst-parser<5,>=4.0; extra == "docs"
|
|
55
|
+
Requires-Dist: sphinx-autodoc-typehints<4,>=3.0; extra == "docs"
|
|
56
|
+
Requires-Dist: sphinx-togglebutton<1,>=0.3; extra == "docs"
|
|
57
|
+
Requires-Dist: sphinx-design<0.7,>=0.6; extra == "docs"
|
|
58
|
+
Provides-Extra: build
|
|
59
|
+
Requires-Dist: build<2,>=1.3; extra == "build"
|
|
60
|
+
Requires-Dist: twine<7,>=6.2; extra == "build"
|
|
61
|
+
Provides-Extra: tox
|
|
62
|
+
Requires-Dist: tox<5,>=4.31; extra == "tox"
|
|
63
|
+
Provides-Extra: infra-tools
|
|
64
|
+
Requires-Dist: awscli-local<1,>=0.22; extra == "infra-tools"
|
|
65
|
+
Provides-Extra: textual
|
|
66
|
+
Requires-Dist: textual<7,>=6.3; extra == "textual"
|
|
67
|
+
Provides-Extra: all
|
|
68
|
+
Requires-Dist: pytest<9,>=8.4; extra == "all"
|
|
69
|
+
Requires-Dist: pytest-cov<8,>=7.0; extra == "all"
|
|
70
|
+
Requires-Dist: pytest-asyncio<2,>=1.2; extra == "all"
|
|
71
|
+
Requires-Dist: ruff<1,>=0.14; extra == "all"
|
|
72
|
+
Requires-Dist: mypy<2,>=1.18; extra == "all"
|
|
73
|
+
Requires-Dist: types-PyYAML<7,>=6.0; extra == "all"
|
|
74
|
+
Requires-Dist: sphinx<9,>=8.1; extra == "all"
|
|
75
|
+
Requires-Dist: furo<2026,>=2025.9; extra == "all"
|
|
76
|
+
Requires-Dist: myst-parser<5,>=4.0; extra == "all"
|
|
77
|
+
Requires-Dist: sphinx-autodoc-typehints<4,>=3.0; extra == "all"
|
|
78
|
+
Requires-Dist: sphinx-togglebutton<1,>=0.3; extra == "all"
|
|
79
|
+
Requires-Dist: sphinx-design<0.7,>=0.6; extra == "all"
|
|
80
|
+
Requires-Dist: build<2,>=1.3; extra == "all"
|
|
81
|
+
Requires-Dist: twine<7,>=6.2; extra == "all"
|
|
82
|
+
Requires-Dist: tox<5,>=4.31; extra == "all"
|
|
83
|
+
Requires-Dist: pre-commit<5,>=4.0; extra == "all"
|
|
84
|
+
Requires-Dist: boto3<2,>=1.35; extra == "all"
|
|
85
|
+
Requires-Dist: awscli-local<1,>=0.22; extra == "all"
|
|
86
|
+
Dynamic: license-file
|
|
87
|
+
|
|
88
|
+
<p align="center">
|
|
89
|
+
<img src="https://raw.githubusercontent.com/KaminoU/kstlib/main/assets/kstlib.svg" alt="Kstlib Logo" width="420">
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
<p align="center">
|
|
93
|
+
<strong>Config-driven Python toolkit for resilient applications</strong>
|
|
94
|
+
</p>
|
|
95
|
+
|
|
96
|
+
<p align="center">
|
|
97
|
+
<a href="https://github.com/KaminoU/kstlib/actions/workflows/ci.yml"><img src="https://github.com/KaminoU/kstlib/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
|
|
98
|
+
<a href="https://kstlib.readthedocs.io/"><img src="https://img.shields.io/badge/docs-RTD-blue" alt="Documentation"></a>
|
|
99
|
+
<a href="https://pypi.org/project/kstlib/"><img src="https://img.shields.io/pypi/v/kstlib?color=blue" alt="PyPI"></a>
|
|
100
|
+
<img src="https://img.shields.io/badge/python-≥3.10-blue" alt="Python">
|
|
101
|
+
<a href="https://github.com/KaminoU/kstlib/blob/main/LICENSE.md"><img src="https://img.shields.io/badge/license-MIT-green" alt="License"></a>
|
|
102
|
+
</p>
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
**kstlib** is a personal Python toolkit built over 7 years of learning and experimentation.
|
|
107
|
+
|
|
108
|
+
It started as a way to explore Python best practices, evolved into utilities for personal automation,
|
|
109
|
+
and now serves as the foundation for study projects in algorithmic trading and market analysis.
|
|
110
|
+
|
|
111
|
+
The focus has always been on building **resilient, secure, and performant** systems.
|
|
112
|
+
|
|
113
|
+
> **Note**: Everything works via Python, but since kstlib is heavily config-driven,
|
|
114
|
+
> the [Examples Gallery](https://kstlib.readthedocs.io/en/latest/examples.html) showcases
|
|
115
|
+
> a YAML-first approach.
|
|
116
|
+
|
|
117
|
+
## Core Modules
|
|
118
|
+
|
|
119
|
+
| Module | Purpose |
|
|
120
|
+
|--------|---------|
|
|
121
|
+
| **config** | Cascading config files, includes, SOPS encryption, Box access |
|
|
122
|
+
| **secrets** | Multi-provider resolver (env, keyring, SOPS, KMS) with guardrails |
|
|
123
|
+
| **logging** | Rich console, rotating files, TRACE level, structlog integration |
|
|
124
|
+
| **auth** | OIDC/OAuth2 with PKCE, token storage, auto-refresh |
|
|
125
|
+
| **mail** | Jinja templates, transports (SMTP, Gmail API, Resend) |
|
|
126
|
+
| **alerts** | Multi-channel (Slack, Email), throttling, severity levels |
|
|
127
|
+
| **websocket** | Resilient connections, auto-reconnect, heartbeat, watchdog |
|
|
128
|
+
| **rapi** | Config-driven REST client with HMAC signing |
|
|
129
|
+
| **monitoring** | Collectors + Jinja rendering + delivery (file, mail) |
|
|
130
|
+
| **resilience** | Circuit breaker, rate limiter, graceful shutdown |
|
|
131
|
+
| **ops** | Session manager (tmux), containers (Docker/Podman) |
|
|
132
|
+
| **helpers** | TimeTrigger, formatting, secure delete, validators |
|
|
133
|
+
|
|
134
|
+
## Quick Start
|
|
135
|
+
|
|
136
|
+
### Installation
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
pip install kstlib
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Basic Usage
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from kstlib.config import load_from_file
|
|
146
|
+
from kstlib import cache
|
|
147
|
+
|
|
148
|
+
config = load_from_file("config.yml")
|
|
149
|
+
|
|
150
|
+
@cache(ttl=300)
|
|
151
|
+
def expensive_computation(x: int) -> int:
|
|
152
|
+
return x ** 2
|
|
153
|
+
|
|
154
|
+
result = expensive_computation(5)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Minimal Configuration
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
app:
|
|
161
|
+
name: "My Application"
|
|
162
|
+
debug: true
|
|
163
|
+
|
|
164
|
+
database:
|
|
165
|
+
host: "localhost"
|
|
166
|
+
port: 5432
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Documentation
|
|
170
|
+
|
|
171
|
+
Full documentation available at **[kstlib.readthedocs.io](https://kstlib.readthedocs.io/)**
|
|
172
|
+
|
|
173
|
+
- [Features Guide](https://kstlib.readthedocs.io/en/latest/features/index.html)
|
|
174
|
+
- [Examples Gallery](https://kstlib.readthedocs.io/en/latest/examples.html)
|
|
175
|
+
- [API Reference](https://kstlib.readthedocs.io/en/latest/api/index.html)
|
|
176
|
+
- [Development Guide](https://kstlib.readthedocs.io/en/latest/development/index.html)
|
|
177
|
+
|
|
178
|
+
## Installation Options
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Standard install
|
|
182
|
+
pip install kstlib
|
|
183
|
+
|
|
184
|
+
# With uv (faster)
|
|
185
|
+
uv pip install kstlib
|
|
186
|
+
|
|
187
|
+
# Development install
|
|
188
|
+
pip install "kstlib[dev]"
|
|
189
|
+
|
|
190
|
+
# All extras
|
|
191
|
+
pip install "kstlib[all]"
|
|
192
|
+
|
|
193
|
+
# From GitHub (latest)
|
|
194
|
+
pip install "git+https://github.com/KaminoU/kstlib.git"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT License - Copyright 2025 Michel TRUONG
|
|
200
|
+
|
|
201
|
+
See [LICENSE](LICENSE.md) for full text.
|