wirexaapi 1.1.0__tar.gz → 1.1.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wirexaapi
3
- Version: 1.1.0
3
+ Version: 1.1.1
4
4
  Summary: Wirexa API SDK - voice API client (SignalWire/Twilio compatible) and Wirexa DB document database client
5
5
  Author-email: Wirexa Team <dev@wirexa.io>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wirexaapi"
7
- version = "1.1.0"
7
+ version = "1.1.1"
8
8
  description = "Wirexa API SDK - voice API client (SignalWire/Twilio compatible) and Wirexa DB document database client"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -307,43 +307,76 @@ class WirexaClient:
307
307
  with self._lock:
308
308
  self._all.append(c)
309
309
  if c.sock is None:
310
- try:
311
- c.connect()
312
- except socket.timeout as e:
313
- raise ServerSelectionTimeoutError("%s:%d: %s" % (self.address + (e,)))
314
- except OSError as e:
315
- raise ConnectionFailure("%s:%d: %s" % (self.address + (e,)))
310
+ c.connect() # las excepciones las gestiona _op (reintenta)
311
+ c._wdb_used = False
316
312
  c.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
313
+ try:
314
+ c.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
315
+ except OSError:
316
+ pass
317
317
  c.sock.settimeout(self._read_timeout)
318
318
  return c
319
319
 
320
+ # Errores que significan "la conexión keep-alive ya no sirve": hay que
321
+ # reabrir y reintentar. Incluye los de TLS (SSLEOFError/"EOF occurred in
322
+ # violation of protocol"), que es lo que lanza Python cuando nginx cierra
323
+ # una conexión keep-alive inactiva (keepalive_timeout) y se reusa el socket.
324
+ _STALE = (
325
+ http.client.RemoteDisconnected,
326
+ http.client.BadStatusLine,
327
+ http.client.CannotSendRequest,
328
+ http.client.ResponseNotReady,
329
+ ConnectionResetError,
330
+ BrokenPipeError,
331
+ ConnectionAbortedError,
332
+ ssl.SSLError, # incluye ssl.SSLEOFError y ssl.SSLZeroReturnError
333
+ )
334
+
320
335
  def _op(self, op, retry_safe=False, **fields):
321
336
  if self._closed:
322
337
  raise ConnectionFailure("el cliente está cerrado")
323
338
  body = json.dumps(_encode(dict(op=op, **fields)), separators=(",", ":")).encode()
324
- for attempt in (0, 1):
325
- c = self._conn()
326
- reused = getattr(c, "_wdb_used", False)
339
+ attempts = 4
340
+ last = None
341
+ for attempt in range(attempts):
342
+ c = None
343
+ reused = False
344
+ stage = "connect"
327
345
  try:
346
+ c = self._conn()
347
+ reused = getattr(c, "_wdb_used", False)
348
+ stage = "request"
328
349
  c.request("POST", _PATH, body, self._headers)
329
350
  r = c.getresponse()
330
351
  status, raw = r.status, r.read()
331
352
  c._wdb_used = True
332
353
  break
333
- except (http.client.RemoteDisconnected, ConnectionResetError, BrokenPipeError,
334
- http.client.CannotSendRequest, http.client.ResponseNotReady) as e:
335
- # Conexión keep-alive cerrada por el servidor mientras estaba
336
- # ociosa: se reabre y se reintenta una sola vez.
337
- c.close()
338
- c._wdb_used = False
339
- if attempt == 1 or not (reused or retry_safe):
340
- raise ConnectionFailure("%s:%d: %s" % (self.address + (e,)))
341
354
  except socket.timeout as e:
342
- c.close()
355
+ last = e
356
+ self._kill(c)
357
+ # timeout al conectar -> reintentar; al leer -> la petición pudo
358
+ # ejecutarse, así que solo se reintenta si es idempotente.
359
+ if attempt + 1 < attempts and (stage == "connect" or retry_safe):
360
+ continue
361
+ if stage == "connect":
362
+ raise ServerSelectionTimeoutError("%s:%d: %s" % (self.address + (e,)))
343
363
  raise ConnectionFailure("timeout esperando respuesta: %s" % e)
364
+ except self._STALE as e:
365
+ last = e
366
+ self._kill(c)
367
+ # Seguro reintentar cuando: falló al conectar (nunca se envió),
368
+ # el socket reusado estaba muerto (no se procesó), o es lectura.
369
+ if attempt + 1 < attempts and (stage == "connect" or reused or retry_safe):
370
+ continue
371
+ raise ConnectionFailure("%s:%d: %s" % (self.address + (e,)))
344
372
  except OSError as e:
345
- c.close()
373
+ last = e
374
+ self._kill(c)
375
+ if attempt + 1 < attempts and (stage == "connect" or reused or retry_safe):
376
+ continue
346
377
  raise ConnectionFailure("%s:%d: %s" % (self.address + (e,)))
378
+ else: # pragma: no cover
379
+ raise ConnectionFailure("%s:%d: %s" % (self.address + (last,)))
347
380
  try:
348
381
  data = json.loads(raw, object_hook=_decode_hook)
349
382
  except ValueError:
@@ -356,6 +389,18 @@ class WirexaClient:
356
389
  raise OperationFailure(msg, code, data)
357
390
  return data
358
391
 
392
+ def _kill(self, c):
393
+ """Cierra la conexión del hilo para que el próximo _op abra una nueva."""
394
+ if c is None:
395
+ return
396
+ try:
397
+ c.close()
398
+ except Exception:
399
+ pass
400
+ c._wdb_used = False
401
+ if getattr(self._local, "conn", None) is c:
402
+ self._local.conn = None
403
+
359
404
  def __getitem__(self, name):
360
405
  return Database(self, name)
361
406
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wirexaapi
3
- Version: 1.1.0
3
+ Version: 1.1.1
4
4
  Summary: Wirexa API SDK - voice API client (SignalWire/Twilio compatible) and Wirexa DB document database client
5
5
  Author-email: Wirexa Team <dev@wirexa.io>
6
6
  License: MIT
File without changes
File without changes