socketflow 0.2.0__tar.gz → 0.2.2__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.
Files changed (28) hide show
  1. {socketflow-0.2.0 → socketflow-0.2.2}/PKG-INFO +1 -1
  2. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/__init__.py +1 -1
  3. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/client_side/client.py +37 -25
  4. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/blueprint.py +16 -0
  5. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/event_loop.py +14 -0
  6. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/transport.py +11 -2
  7. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/server_side/server.py +21 -1
  8. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow.egg-info/PKG-INFO +1 -1
  9. {socketflow-0.2.0 → socketflow-0.2.2}/README.md +0 -0
  10. {socketflow-0.2.0 → socketflow-0.2.2}/setup.cfg +0 -0
  11. {socketflow-0.2.0 → socketflow-0.2.2}/setup.py +0 -0
  12. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/client_side/__init__.py +0 -0
  13. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/__init__.py +0 -0
  14. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/compression.py +0 -0
  15. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/dispatcher.py +0 -0
  16. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/event.py +0 -0
  17. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/exceptions.py +0 -0
  18. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/logs.py +0 -0
  19. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/message_handler.py +0 -0
  20. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/message_manager.py +0 -0
  21. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/metrics.py +0 -0
  22. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/protocol.py +0 -0
  23. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/server_side/__init__.py +0 -0
  24. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow.egg-info/SOURCES.txt +0 -0
  25. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow.egg-info/dependency_links.txt +0 -0
  26. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow.egg-info/not-zip-safe +0 -0
  27. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow.egg-info/requires.txt +0 -0
  28. {socketflow-0.2.0 → socketflow-0.2.2}/socketflow.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socketflow
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: TCP networking for Python, without the boilerplate.
5
5
  Home-page: https://github.com/ayammaximilian/socketflow
6
6
  Author: SocketFlow Team
@@ -34,7 +34,7 @@ from .global_side.protocol import (
34
34
  MAX_PROTOCOL_VERSION,
35
35
  )
36
36
 
37
- __version__ = "0.2.0"
37
+ __version__ = "0.2.2"
38
38
  __all__ = [
39
39
  "TcpServer",
40
40
  "TcpClient",
@@ -169,7 +169,10 @@ class TcpClientProtocol:
169
169
  else "Authentication failed"
170
170
  )
171
171
  self.client._auth_ack_event.set()
172
- if self.client._auth_error is not None or not self.client.require_handshake:
172
+ if (
173
+ self.client._auth_error is not None
174
+ or not self.client.require_handshake
175
+ ):
173
176
  self.client._auth_event.set()
174
177
  continue
175
178
  elif msg_type == "__handshake_ok__":
@@ -182,9 +185,7 @@ class TcpClientProtocol:
182
185
  server_addr = self.client._server_addr
183
186
  if not server_addr:
184
187
  server_addr = (
185
- self.socket.getpeername()
186
- if self.socket
187
- else ("unknown", 0)
188
+ self.socket.getpeername() if self.socket else ("unknown", 0)
188
189
  )
189
190
  wait_response = headers.get("wait_response", False)
190
191
  status_code = headers.get("status_code", 200)
@@ -200,9 +201,7 @@ class TcpClientProtocol:
200
201
  )
201
202
 
202
203
  # Resolve pending wait_response futures
203
- future = (
204
- self.client.pending_responses.pop(data_id) if data_id else None
205
- )
204
+ future = self.client.pending_responses.pop(data_id) if data_id else None
206
205
  if future is not None:
207
206
  if not future.done():
208
207
  future.set_result(
@@ -236,6 +235,12 @@ class TcpClientProtocol:
236
235
  with self._control_lock:
237
236
  self.socket.sendall(data)
238
237
 
238
+ def has_backpressure(self) -> bool:
239
+ """True when the outbound queue to the server is full."""
240
+ if self._writer is None:
241
+ return False
242
+ return self._writer.has_backpressure()
243
+
239
244
  def close(self):
240
245
  self._writer.abort()
241
246
 
@@ -287,7 +292,10 @@ class TcpClientProtocol:
287
292
  try:
288
293
  self.send_data(message_handler.create_ping())
289
294
  except Exception:
290
- return
295
+ # A failed write is evidence the peer is gone. Do not return here,
296
+ # or the silence check below is skipped and a dead connection is
297
+ # never detected. Fall through and let that check decide.
298
+ pass
291
299
 
292
300
  if now - self._last_received_at >= interval * max(
293
301
  self.client.keepalive_max_missed, 1
@@ -362,25 +370,29 @@ class TcpClient:
362
370
  if handshake_timeout is not None and handshake_timeout <= 0:
363
371
  raise ValueError("handshake_timeout must be positive")
364
372
  if (auth_username is None) != (auth_password is None):
365
- raise ValueError("auth_username and auth_password must be provided together")
373
+ raise ValueError(
374
+ "auth_username and auth_password must be provided together"
375
+ )
366
376
  if auth_token is not None and auth_username is not None:
367
- raise ValueError("Use either auth_token or username/password authentication")
377
+ raise ValueError(
378
+ "Use either auth_token or username/password authentication"
379
+ )
368
380
  if min_protocol_version is not None and max_protocol_version is not None:
369
381
  if min_protocol_version > max_protocol_version:
370
382
  raise ValueError(
371
383
  "min_protocol_version cannot exceed max_protocol_version"
372
384
  )
373
385
  if (tls_certfile is None) != (tls_keyfile is None):
374
- raise ValueError(
375
- "tls_certfile and tls_keyfile must be provided together"
376
- )
386
+ raise ValueError("tls_certfile and tls_keyfile must be provided together")
377
387
 
378
388
  self.host = host
379
389
  self.port = port
380
390
  self.metrics = metrics_module.MetricsRegistry()
381
391
  self._log = logs.get_logger("socketflow.client")
382
392
  self.metrics.describe("messages_sent_total", "Messages sent to the server")
383
- self.metrics.describe("messages_received_total", "Messages received from the server")
393
+ self.metrics.describe(
394
+ "messages_received_total", "Messages received from the server"
395
+ )
384
396
  self.metrics.describe("bytes_sent_total", "Bytes written to the server")
385
397
  self.metrics.describe("errors_total", "Errors by context")
386
398
  self.metrics.describe("backpressure_total", "Rejections caused by full queues")
@@ -503,6 +515,7 @@ class TcpClient:
503
515
  return
504
516
  self._negotiated_version = agreed
505
517
  self._server_ready_event.set()
518
+
506
519
  def _on_protocol_error(self, body):
507
520
  """Record a server-side version refusal."""
508
521
  reason = "Server refused the protocol version"
@@ -607,8 +620,6 @@ class TcpClient:
607
620
  self._protocol = None
608
621
  self._connected = False
609
622
 
610
-
611
-
612
623
  def connect(self):
613
624
  """Connect to server"""
614
625
  if self._connected:
@@ -887,13 +898,9 @@ class TcpClient:
887
898
  if not self._connected:
888
899
  raise ExceptionType.NotConnected("Client is not connected")
889
900
  if self.require_handshake and not self._handshake_completed:
890
- raise ExceptionType.HandshakeError(
891
- "Connection handshake is not complete"
892
- )
901
+ raise ExceptionType.HandshakeError("Connection handshake is not complete")
893
902
 
894
- data_id, frame = self._encode_outbound(
895
- data, data_id, path, True, status_code
896
- )
903
+ data_id, frame = self._encode_outbound(data, data_id, path, True, status_code)
897
904
  self._record_send(path, frame)
898
905
 
899
906
  # Register before sending because the peer can reply before send returns.
@@ -965,9 +972,7 @@ class TcpClient:
965
972
  f"No response received within {wait_response_timeout}"
966
973
  )
967
974
  except Exception as e:
968
- raise ExceptionType.ClientError(
969
- f"Error while waiting for response: {e}"
970
- )
975
+ raise ExceptionType.ClientError(f"Error while waiting for response: {e}")
971
976
  finally:
972
977
  request.cancel()
973
978
 
@@ -987,6 +992,13 @@ class TcpClient:
987
992
  def is_connected(self):
988
993
  return self._connected
989
994
 
995
+ def has_backpressure(self) -> bool:
996
+ """True when the outbound queue to the server is full."""
997
+ protocol = self._protocol
998
+ if protocol is None:
999
+ return False
1000
+ return protocol.has_backpressure()
1001
+
990
1002
  @property
991
1003
  def handshake_completed(self):
992
1004
  return self._handshake_completed
@@ -78,6 +78,22 @@ class Blueprint:
78
78
  "Blueprint not registered with client or server"
79
79
  )
80
80
 
81
+ def has_backpressure(self, client_addr: tuple = None) -> bool:
82
+ """True when the outbound queue is full.
83
+
84
+ On a server blueprint, client_addr selects the client to check.
85
+ """
86
+ if hasattr(self, "_client") and self._client:
87
+ return self._client.has_backpressure()
88
+ elif hasattr(self, "_server") and self._server:
89
+ if not client_addr:
90
+ raise ExceptionType.BlueprintError("client_addr parameter is required")
91
+ return self._server.has_backpressure(client_addr)
92
+ else:
93
+ raise ExceptionType.BlueprintError(
94
+ "Blueprint not registered with client or server"
95
+ )
96
+
81
97
  def send(
82
98
  self,
83
99
  data: Any,
@@ -176,6 +176,12 @@ class EventLoopWriter:
176
176
  return True
177
177
  return self.connection.flush(timeout)
178
178
 
179
+ def has_backpressure(self) -> bool:
180
+ """True when the next send would raise Backpressure."""
181
+ if self.connection is None:
182
+ return False
183
+ return self.connection.has_backpressure()
184
+
179
185
  def abort(self, timeout=1.0):
180
186
  if self.connection is not None:
181
187
  self.connection.close()
@@ -233,6 +239,14 @@ class _SelectorConnection:
233
239
  with self._lock:
234
240
  return self._pending_writes
235
241
 
242
+ def has_backpressure(self) -> bool:
243
+ """True when the next send would raise Backpressure."""
244
+ with self._lock:
245
+ return (
246
+ self._pending_writes >= self._max_pending_writes
247
+ or self._queued_bytes >= self._max_queue_bytes
248
+ )
249
+
236
250
  def flush(self, timeout: float = 5.0) -> bool:
237
251
  """Wait until the send buffer is empty.
238
252
 
@@ -50,7 +50,6 @@ class MemoryBudget:
50
50
  self._used_bytes = max(0, self._used_bytes - amount)
51
51
 
52
52
 
53
-
54
53
  class TimeoutScheduler:
55
54
  """Run request timeouts from a single shared thread.
56
55
 
@@ -217,7 +216,9 @@ class PendingResponseRegistry:
217
216
  key = self._key(data_id, owner)
218
217
  with self._lock:
219
218
  if key in self._responses:
220
- raise ValueError(f"A request with data_id {data_id!r} is already pending")
219
+ raise ValueError(
220
+ f"A request with data_id {data_id!r} is already pending"
221
+ )
221
222
  self._responses[key] = future
222
223
 
223
224
  def pop(self, data_id: str, owner: Any = None) -> Optional[Any]:
@@ -335,6 +336,14 @@ class SocketWriter:
335
336
  return False
336
337
  time.sleep(0.002)
337
338
 
339
+ def has_backpressure(self) -> bool:
340
+ """True when the next send would raise Backpressure."""
341
+ with self._state_lock:
342
+ return (
343
+ self._pending_writes >= self._max_pending_writes
344
+ or self._queued_bytes >= self._max_queue_bytes
345
+ )
346
+
338
347
  def _complete(self, payload: bytes):
339
348
  with self._state_lock:
340
349
  self._queued_bytes -= len(payload)
@@ -436,6 +436,12 @@ class TcpServerProtocol:
436
436
  except Exception:
437
437
  return False
438
438
 
439
+ def has_backpressure(self) -> bool:
440
+ """True when the outbound queue to this client is full."""
441
+ if self._writer is None:
442
+ return False
443
+ return self._writer.has_backpressure()
444
+
439
445
  def close(self):
440
446
  if self._writer is not None:
441
447
  self._writer.abort()
@@ -504,7 +510,10 @@ class TcpServerProtocol:
504
510
  try:
505
511
  self.send(message_handler.create_ping())
506
512
  except Exception:
507
- return
513
+ # A failed write is evidence the peer is gone. Do not return here,
514
+ # or the silence check below is skipped and a dead connection is
515
+ # never detected. Fall through and let that check decide.
516
+ pass
508
517
 
509
518
  if now - self._last_received_at >= interval * max(
510
519
  self.server.keepalive_max_missed, 1
@@ -1429,6 +1438,17 @@ class TcpServer:
1429
1438
  with self._clients_lock:
1430
1439
  return client_addr in self._clients
1431
1440
 
1441
+ def has_backpressure(self, client_addr) -> bool:
1442
+ """True when the outbound queue to one client is full.
1443
+
1444
+ Returns False when the client is not connected.
1445
+ """
1446
+ with self._clients_lock:
1447
+ protocol = self._clients.get(client_addr)
1448
+ if protocol is None:
1449
+ return False
1450
+ return protocol.has_backpressure()
1451
+
1432
1452
  def get_client_identity(self, client_addr: tuple) -> Optional[str]:
1433
1453
  """Return the verified certificate common name for a client, or None."""
1434
1454
  with self._clients_lock:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socketflow
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: TCP networking for Python, without the boilerplate.
5
5
  Home-page: https://github.com/ayammaximilian/socketflow
6
6
  Author: SocketFlow Team
File without changes
File without changes
File without changes