kstlib 2.2.0__tar.gz → 2.2.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.
Files changed (189) hide show
  1. {kstlib-2.2.0/src/kstlib.egg-info → kstlib-2.2.1}/PKG-INFO +2 -2
  2. {kstlib-2.2.0 → kstlib-2.2.1}/pyproject.toml +1 -1
  3. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/limits.py +39 -0
  4. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/meta.py +1 -1
  5. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/websocket/manager.py +128 -5
  6. {kstlib-2.2.0 → kstlib-2.2.1/src/kstlib.egg-info}/PKG-INFO +2 -2
  7. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib.egg-info/requires.txt +1 -1
  8. {kstlib-2.2.0 → kstlib-2.2.1}/LICENSE.md +0 -0
  9. {kstlib-2.2.0 → kstlib-2.2.1}/MANIFEST.in +0 -0
  10. {kstlib-2.2.0 → kstlib-2.2.1}/README.md +0 -0
  11. {kstlib-2.2.0 → kstlib-2.2.1}/setup.cfg +0 -0
  12. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/__init__.py +0 -0
  13. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/__main__.py +0 -0
  14. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/__init__.py +0 -0
  15. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/channels/__init__.py +0 -0
  16. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/channels/base.py +0 -0
  17. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/channels/email.py +0 -0
  18. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/channels/slack.py +0 -0
  19. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/exceptions.py +0 -0
  20. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/manager.py +0 -0
  21. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/models.py +0 -0
  22. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/alerts/throttle.py +0 -0
  23. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/__init__.py +0 -0
  24. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/callback.py +0 -0
  25. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/check.py +0 -0
  26. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/config.py +0 -0
  27. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/errors.py +0 -0
  28. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/models.py +0 -0
  29. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/providers/__init__.py +0 -0
  30. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/providers/base.py +0 -0
  31. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/providers/oauth2.py +0 -0
  32. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/providers/oidc.py +0 -0
  33. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/session.py +0 -0
  34. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/auth/token.py +0 -0
  35. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cache/__init__.py +0 -0
  36. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cache/decorator.py +0 -0
  37. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cache/strategies.py +0 -0
  38. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/__init__.py +0 -0
  39. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/app.py +0 -0
  40. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/__init__.py +0 -0
  41. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/__init__.py +0 -0
  42. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/check.py +0 -0
  43. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/common.py +0 -0
  44. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/login.py +0 -0
  45. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/logout.py +0 -0
  46. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/providers.py +0 -0
  47. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/status.py +0 -0
  48. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/token.py +0 -0
  49. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/auth/whoami.py +0 -0
  50. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/config.py +0 -0
  51. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/ops/__init__.py +0 -0
  52. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/ops/attach.py +0 -0
  53. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/ops/common.py +0 -0
  54. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/ops/list_sessions.py +0 -0
  55. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/ops/logs.py +0 -0
  56. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/ops/start.py +0 -0
  57. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/ops/status.py +0 -0
  58. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/ops/stop.py +0 -0
  59. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/rapi/__init__.py +0 -0
  60. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/rapi/call.py +0 -0
  61. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/rapi/list.py +0 -0
  62. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/rapi/show.py +0 -0
  63. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/secrets/__init__.py +0 -0
  64. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/secrets/common.py +0 -0
  65. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/secrets/decrypt.py +0 -0
  66. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/secrets/doctor.py +0 -0
  67. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/secrets/encrypt.py +0 -0
  68. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/commands/secrets/shred.py +0 -0
  69. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/cli/common.py +0 -0
  70. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/config/__init__.py +0 -0
  71. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/config/exceptions.py +0 -0
  72. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/config/export.py +0 -0
  73. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/config/loader.py +0 -0
  74. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/config/sops.py +0 -0
  75. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/db/__init__.py +0 -0
  76. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/db/aiosqlcipher.py +0 -0
  77. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/db/cipher.py +0 -0
  78. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/db/database.py +0 -0
  79. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/db/exceptions.py +0 -0
  80. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/db/pool.py +0 -0
  81. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/helpers/__init__.py +0 -0
  82. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/helpers/exceptions.py +0 -0
  83. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/helpers/time_trigger.py +0 -0
  84. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/kstlib.conf.yml +0 -0
  85. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/logging/__init__.py +0 -0
  86. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/logging/manager.py +0 -0
  87. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/__init__.py +0 -0
  88. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/builder.py +0 -0
  89. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/exceptions.py +0 -0
  90. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/filesystem.py +0 -0
  91. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/transport.py +0 -0
  92. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/transports/__init__.py +0 -0
  93. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/transports/gmail.py +0 -0
  94. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/transports/resend.py +0 -0
  95. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/transports/ses.py +0 -0
  96. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/mail/transports/smtp.py +0 -0
  97. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/metrics/__init__.py +0 -0
  98. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/metrics/decorators.py +0 -0
  99. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/metrics/exceptions.py +0 -0
  100. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/__init__.py +0 -0
  101. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/_styles.py +0 -0
  102. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/cell.py +0 -0
  103. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/config.py +0 -0
  104. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/delivery.py +0 -0
  105. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/exceptions.py +0 -0
  106. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/image.py +0 -0
  107. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/kv.py +0 -0
  108. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/list.py +0 -0
  109. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/metric.py +0 -0
  110. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/monitoring.py +0 -0
  111. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/renderer.py +0 -0
  112. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/service.py +0 -0
  113. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/table.py +0 -0
  114. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/monitoring/types.py +0 -0
  115. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ops/__init__.py +0 -0
  116. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ops/base.py +0 -0
  117. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ops/container.py +0 -0
  118. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ops/exceptions.py +0 -0
  119. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ops/manager.py +0 -0
  120. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ops/models.py +0 -0
  121. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ops/tmux.py +0 -0
  122. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ops/validators.py +0 -0
  123. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/__init__.py +0 -0
  124. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/base.py +0 -0
  125. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/exceptions.py +0 -0
  126. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/models.py +0 -0
  127. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/runner.py +0 -0
  128. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/steps/__init__.py +0 -0
  129. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/steps/_base.py +0 -0
  130. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/steps/callable.py +0 -0
  131. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/steps/python.py +0 -0
  132. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/steps/shell.py +0 -0
  133. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/pipeline/validators.py +0 -0
  134. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/py.typed +0 -0
  135. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/rapi/__init__.py +0 -0
  136. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/rapi/client.py +0 -0
  137. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/rapi/config.py +0 -0
  138. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/rapi/credentials.py +0 -0
  139. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/rapi/exceptions.py +0 -0
  140. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/resilience/__init__.py +0 -0
  141. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/resilience/circuit_breaker.py +0 -0
  142. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/resilience/exceptions.py +0 -0
  143. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/resilience/heartbeat.py +0 -0
  144. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/resilience/rate_limiter.py +0 -0
  145. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/resilience/shutdown.py +0 -0
  146. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/resilience/watchdog.py +0 -0
  147. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/__init__.py +0 -0
  148. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/exceptions.py +0 -0
  149. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/models.py +0 -0
  150. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/providers/__init__.py +0 -0
  151. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/providers/base.py +0 -0
  152. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/providers/environment.py +0 -0
  153. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/providers/keyring.py +0 -0
  154. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/providers/kms.py +0 -0
  155. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/providers/kwargs.py +0 -0
  156. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/providers/sops.py +0 -0
  157. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/resolver.py +0 -0
  158. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secrets/sensitive.py +0 -0
  159. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secure/__init__.py +0 -0
  160. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secure/fs.py +0 -0
  161. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/secure/permissions.py +0 -0
  162. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ssl.py +0 -0
  163. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/transform/__init__.py +0 -0
  164. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/transform/chain.py +0 -0
  165. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/transform/config.py +0 -0
  166. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/transform/exceptions.py +0 -0
  167. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/transform/primitives.py +0 -0
  168. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/transform/validators.py +0 -0
  169. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ui/__init__.py +0 -0
  170. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ui/exceptions.py +0 -0
  171. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ui/panels.py +0 -0
  172. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ui/spinner.py +0 -0
  173. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/ui/tables.py +0 -0
  174. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/__init__.py +0 -0
  175. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/dict.py +0 -0
  176. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/formatting.py +0 -0
  177. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/http_trace.py +0 -0
  178. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/lazy.py +0 -0
  179. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/secure_delete.py +0 -0
  180. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/serialization.py +0 -0
  181. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/text.py +0 -0
  182. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/utils/validators.py +0 -0
  183. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/websocket/__init__.py +0 -0
  184. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/websocket/exceptions.py +0 -0
  185. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib/websocket/models.py +0 -0
  186. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib.egg-info/SOURCES.txt +0 -0
  187. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib.egg-info/dependency_links.txt +0 -0
  188. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib.egg-info/entry_points.txt +0 -0
  189. {kstlib-2.2.0 → kstlib-2.2.1}/src/kstlib.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kstlib
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: Config-driven helpers for Python projects (dynamic config, secure secrets, preset logging, and more…)
5
5
  Author-email: Michel TRUONG <michel.truong@gmail.com>
6
6
  Maintainer-email: Michel TRUONG <michel.truong@gmail.com>
@@ -42,7 +42,7 @@ Requires-Dist: websockets<16,>=15.0
42
42
  Requires-Dist: jinja2<4,>=3.1.5
43
43
  Requires-Dist: humanize<5,>=4.11
44
44
  Requires-Dist: httpx<1,>=0.28
45
- Requires-Dist: authlib<2,>=1.6.9
45
+ Requires-Dist: authlib<2,>=1.6.11
46
46
  Requires-Dist: pendulum<4,>=3.0
47
47
  Requires-Dist: cryptography>=46.0.7
48
48
  Requires-Dist: requests>=2.33.0
@@ -56,7 +56,7 @@ dependencies = [
56
56
 
57
57
  # --- HTTP Client & Auth ---
58
58
  "httpx>=0.28,<1", # Modern async HTTP client (OAuth2/OIDC flows)
59
- "authlib>=1.6.9,<2", # OAuth2/OIDC client + JWT signature verification
59
+ "authlib>=1.6.11,<2", # OAuth2/OIDC client + JWT signature verification (GHSA-jj8c-mmj3-mmgv: CSRF in cache-backed OAuth state)
60
60
 
61
61
  # --- Time & Scheduling ---
62
62
  "pendulum>=3.0,<4", # Modern datetime library with timezone support
@@ -200,6 +200,18 @@ HARD_MAX_WS_RECONNECT_CHECK = 60.0
200
200
  HARD_MIN_WS_DISCONNECT_MARGIN = 60.0
201
201
  HARD_MAX_WS_DISCONNECT_MARGIN = 3600.0
202
202
 
203
+ #: WebSocket stable connection time bounds (seconds) - delay before resetting reconnect counter.
204
+ HARD_MIN_WS_STABLE_CONNECTION_TIME = 10.0
205
+ HARD_MAX_WS_STABLE_CONNECTION_TIME = 300.0
206
+
207
+ #: WebSocket server unavailable (code 1013) backoff bounds (seconds).
208
+ HARD_MIN_WS_SERVER_UNAVAILABLE_DELAY = 10.0
209
+ HARD_MAX_WS_SERVER_UNAVAILABLE_DELAY = 300.0
210
+
211
+ #: WebSocket disconnect alert interval bounds (seconds) - throttle window for alerts.
212
+ HARD_MIN_WS_DISCONNECT_ALERT_INTERVAL = 30.0
213
+ HARD_MAX_WS_DISCONNECT_ALERT_INTERVAL = 3600.0
214
+
203
215
  #: Maximum endpoint reference length (api.endpoint format) - protects against DoS.
204
216
  HARD_MAX_ENDPOINT_REF_LENGTH = 256
205
217
 
@@ -272,6 +284,9 @@ DEFAULT_WS_QUEUE_SIZE = 1000 # messages
272
284
  DEFAULT_WS_DISCONNECT_CHECK = 10.0 # seconds
273
285
  DEFAULT_WS_RECONNECT_CHECK = 5.0 # seconds
274
286
  DEFAULT_WS_DISCONNECT_MARGIN = 300.0 # seconds (5 minutes before 24h limit)
287
+ DEFAULT_WS_STABLE_CONNECTION_TIME = 60.0 # seconds before resetting reconnect counter
288
+ DEFAULT_WS_SERVER_UNAVAILABLE_DELAY = 30.0 # seconds to wait on code 1013
289
+ DEFAULT_WS_DISCONNECT_ALERT_INTERVAL = 300.0 # seconds between throttled alerts
275
290
 
276
291
  DEFAULT_PIPELINE_TIMEOUT = 300.0 # seconds (5 minutes)
277
292
  DEFAULT_PIPELINE_ON_ERROR = "fail_fast"
@@ -925,6 +940,9 @@ class WebSocketLimits:
925
940
  disconnect_check_interval: Seconds between should_disconnect checks.
926
941
  reconnect_check_interval: Seconds between should_reconnect checks.
927
942
  disconnect_margin: Seconds before platform limit to disconnect.
943
+ stable_connection_time: Seconds of stable connection before resetting reconnect counter.
944
+ server_unavailable_delay: Seconds to wait on server code 1013 before reconnect.
945
+ disconnect_alert_interval: Seconds between throttled disconnect alerts.
928
946
 
929
947
  """
930
948
 
@@ -938,6 +956,9 @@ class WebSocketLimits:
938
956
  disconnect_check_interval: float
939
957
  reconnect_check_interval: float
940
958
  disconnect_margin: float
959
+ stable_connection_time: float
960
+ server_unavailable_delay: float
961
+ disconnect_alert_interval: float
941
962
 
942
963
 
943
964
  def get_websocket_limits(
@@ -1023,6 +1044,24 @@ def get_websocket_limits(
1023
1044
  HARD_MIN_WS_DISCONNECT_MARGIN,
1024
1045
  HARD_MAX_WS_DISCONNECT_MARGIN,
1025
1046
  ),
1047
+ stable_connection_time=_parse_float_config(
1048
+ _get_nested(config, "websocket", "reconnect", "stable_connection_time"),
1049
+ DEFAULT_WS_STABLE_CONNECTION_TIME,
1050
+ HARD_MIN_WS_STABLE_CONNECTION_TIME,
1051
+ HARD_MAX_WS_STABLE_CONNECTION_TIME,
1052
+ ),
1053
+ server_unavailable_delay=_parse_float_config(
1054
+ _get_nested(config, "websocket", "reconnect", "server_unavailable_delay"),
1055
+ DEFAULT_WS_SERVER_UNAVAILABLE_DELAY,
1056
+ HARD_MIN_WS_SERVER_UNAVAILABLE_DELAY,
1057
+ HARD_MAX_WS_SERVER_UNAVAILABLE_DELAY,
1058
+ ),
1059
+ disconnect_alert_interval=_parse_float_config(
1060
+ _get_nested(config, "websocket", "alert", "disconnect_interval"),
1061
+ DEFAULT_WS_DISCONNECT_ALERT_INTERVAL,
1062
+ HARD_MIN_WS_DISCONNECT_ALERT_INTERVAL,
1063
+ HARD_MAX_WS_DISCONNECT_ALERT_INTERVAL,
1064
+ ),
1026
1065
  )
1027
1066
 
1028
1067
 
@@ -39,7 +39,7 @@ __logo__ = (
39
39
  )
40
40
 
41
41
  __app_name__ = "kstlib"
42
- __version__ = "2.2.0"
42
+ __version__ = "2.2.1"
43
43
  __description__ = (
44
44
  "Config-driven helpers for Python projects (dynamic config, secure secrets, preset logging, and more…)"
45
45
  )
@@ -89,6 +89,9 @@ __all__ = ["WebSocketManager"]
89
89
 
90
90
  log = logging.getLogger(__name__)
91
91
 
92
+ #: WebSocket close code 1013 "Try Again Later" - server temporarily unavailable.
93
+ WS_CODE_TRY_AGAIN_LATER = 1013
94
+
92
95
  # Type aliases for callbacks
93
96
  ShouldDisconnectCallback = Callable[[], bool]
94
97
  ShouldReconnectCallback = Callable[[], bool | float]
@@ -96,6 +99,7 @@ OnConnectCallback = Callable[[], Awaitable[None] | None]
96
99
  OnDisconnectCallback = Callable[[DisconnectReason], Awaitable[None] | None]
97
100
  OnMessageCallback = Callable[[Any], Awaitable[None] | None]
98
101
  OnAlertCallback = Callable[[str, str, Mapping[str, Any]], Awaitable[None] | None]
102
+ DisconnectAlertCallback = Callable[[DisconnectReason, int], Awaitable[None] | None]
99
103
 
100
104
 
101
105
  def _check_websockets_installed() -> None:
@@ -152,6 +156,7 @@ class WebSocketManager:
152
156
  should_reconnect: ShouldReconnectCallback | None = None,
153
157
  on_connect: OnConnectCallback | None = None,
154
158
  on_disconnect: OnDisconnectCallback | None = None,
159
+ on_disconnect_alert: DisconnectAlertCallback | None = None,
155
160
  on_message: OnMessageCallback | None = None,
156
161
  on_alert: OnAlertCallback | None = None,
157
162
  # Connection settings
@@ -164,10 +169,14 @@ class WebSocketManager:
164
169
  max_reconnect_delay: float | None = None,
165
170
  max_reconnect_attempts: int | None = None,
166
171
  auto_reconnect: bool = True,
172
+ stable_connection_time: float | None = None,
173
+ server_unavailable_delay: float | None = None,
167
174
  # Proactive control settings
168
175
  disconnect_check_interval: float | None = None,
169
176
  reconnect_check_interval: float | None = None,
170
177
  disconnect_margin: float | None = None,
178
+ # Alert throttle
179
+ disconnect_alert_interval: float | None = None,
171
180
  # Queue settings
172
181
  queue_size: int | None = None,
173
182
  # Config
@@ -181,6 +190,10 @@ class WebSocketManager:
181
190
  should_reconnect: Callback returning True or delay (seconds) for reconnect.
182
191
  on_connect: Callback invoked after successful connection.
183
192
  on_disconnect: Callback invoked after disconnection with reason.
193
+ on_disconnect_alert: Throttled callback for disconnect alerts.
194
+ Receives (reason, count) where count is the number of
195
+ disconnects since the last alert. Fires at most once per
196
+ ``disconnect_alert_interval`` seconds.
184
197
  on_message: Callback invoked for each received message.
185
198
  on_alert: Callback for alerting (channel, message, context).
186
199
  ping_interval: Seconds between ping frames.
@@ -191,9 +204,17 @@ class WebSocketManager:
191
204
  max_reconnect_delay: Maximum delay for exponential backoff.
192
205
  max_reconnect_attempts: Maximum consecutive reconnection attempts.
193
206
  auto_reconnect: Whether to auto-reconnect on disconnection.
207
+ stable_connection_time: Seconds of stable connection required
208
+ before ``_reconnect_count`` is reset to 0. Protects against
209
+ flapping servers that accept the handshake then close.
210
+ server_unavailable_delay: Seconds to wait before any reconnect
211
+ attempt after receiving close code 1013 (Try Again Later).
194
212
  disconnect_check_interval: Seconds between should_disconnect checks.
195
213
  reconnect_check_interval: Seconds between should_reconnect checks.
196
214
  disconnect_margin: Seconds before platform limit to disconnect.
215
+ disconnect_alert_interval: Minimum seconds between calls to
216
+ ``on_disconnect_alert``. Aggregated count of skipped
217
+ disconnects is passed to the callback.
197
218
  queue_size: Maximum messages in queue (0 = unlimited).
198
219
  config: Optional config mapping for limits resolution.
199
220
 
@@ -229,6 +250,15 @@ class WebSocketManager:
229
250
  )
230
251
  self._disconnect_margin = disconnect_margin if disconnect_margin is not None else limits.disconnect_margin
231
252
  self._queue_size = queue_size if queue_size is not None else limits.queue_size
253
+ self._stable_connection_time = (
254
+ stable_connection_time if stable_connection_time is not None else limits.stable_connection_time
255
+ )
256
+ self._server_unavailable_delay = (
257
+ server_unavailable_delay if server_unavailable_delay is not None else limits.server_unavailable_delay
258
+ )
259
+ self._disconnect_alert_interval = (
260
+ disconnect_alert_interval if disconnect_alert_interval is not None else limits.disconnect_alert_interval
261
+ )
232
262
 
233
263
  # Settings
234
264
  self._reconnect_strategy = reconnect_strategy
@@ -239,6 +269,7 @@ class WebSocketManager:
239
269
  self._should_reconnect = should_reconnect
240
270
  self._on_connect = on_connect
241
271
  self._on_disconnect = on_disconnect
272
+ self._on_disconnect_alert = on_disconnect_alert
242
273
  self._on_message = on_message
243
274
  self._on_alert = on_alert
244
275
 
@@ -249,12 +280,16 @@ class WebSocketManager:
249
280
  self._reconnect_count = 0
250
281
  self._connect_time: float = 0.0
251
282
  self._scheduled_reconnect_delay: float | None = None
283
+ self._force_backoff_delay: float | None = None
284
+ self._disconnect_alert_count: int = 0
285
+ self._last_disconnect_alert_at: float = 0.0
252
286
 
253
287
  # Background tasks
254
288
  self._disconnect_check_task: asyncio.Task[None] | None = None
255
289
  self._reconnect_check_task: asyncio.Task[None] | None = None
256
290
  self._receive_task: asyncio.Task[None] | None = None
257
291
  self._ping_task: asyncio.Task[None] | None = None
292
+ self._stable_connection_task: asyncio.Task[None] | None = None
258
293
 
259
294
  # Events
260
295
  self._connected_event = asyncio.Event()
@@ -398,11 +433,17 @@ class WebSocketManager:
398
433
  ) from last_error
399
434
 
400
435
  async def _finalize_connection(self) -> None:
401
- """Finalize successful connection setup."""
436
+ """Finalize successful connection setup.
437
+
438
+ Note: ``_reconnect_count`` is NOT reset here. A successful handshake
439
+ alone is not proof of a healthy connection - a flapping server can
440
+ accept the TCP/WS handshake then close immediately. The counter is
441
+ reset only after ``_stable_connection_time`` seconds of uptime by
442
+ ``_stable_connection_reset_loop``.
443
+ """
402
444
  self._state = ConnectionState.CONNECTED
403
445
  self._connected_event.set()
404
446
  self._connect_time = time.monotonic()
405
- self._reconnect_count = 0
406
447
  self._stats.record_connect()
407
448
 
408
449
  log.info("WebSocket connected to %s", self._url)
@@ -429,6 +470,31 @@ class WebSocketManager:
429
470
  if self._should_disconnect is not None:
430
471
  self._disconnect_check_task = asyncio.create_task(self._disconnect_check_loop(), name="ws_disconnect_check")
431
472
 
473
+ # Start delayed reconnect-counter reset. Only scheduled if a previous
474
+ # reconnect has occurred. Avoids scheduling a no-op task on first connect.
475
+ if self._reconnect_count > 0:
476
+ self._stable_connection_task = asyncio.create_task(
477
+ self._stable_connection_reset_loop(),
478
+ name="ws_stable_connection_reset",
479
+ )
480
+
481
+ async def _stable_connection_reset_loop(self) -> None:
482
+ """Reset ``_reconnect_count`` after ``_stable_connection_time`` seconds.
483
+
484
+ Scheduled from ``_start_background_tasks``. If the connection drops
485
+ before the delay expires, ``_cancel_background_tasks`` cancels this
486
+ task WITHOUT resetting the counter, so the next reconnect attempt
487
+ keeps the exponential backoff growing.
488
+ """
489
+ await asyncio.sleep(self._stable_connection_time)
490
+ if self._state == ConnectionState.CONNECTED:
491
+ log.debug(
492
+ "Stable connection reached after %.1fs, resetting reconnect counter (was %d)",
493
+ self._stable_connection_time,
494
+ self._reconnect_count,
495
+ )
496
+ self._reconnect_count = 0
497
+
432
498
  def _parse_message(self, message: str | bytes) -> Any:
433
499
  """Parse incoming message, attempting JSON decode for strings."""
434
500
  if isinstance(message, str):
@@ -524,15 +590,27 @@ class WebSocketManager:
524
590
  if was_connected:
525
591
  self._stats.record_disconnect(proactive=reason.is_proactive)
526
592
 
593
+ # Code 1013 "Try Again Later": server explicitly asks clients to back off.
594
+ # Force a pre-reconnect delay regardless of the retry strategy.
595
+ if code == WS_CODE_TRY_AGAIN_LATER:
596
+ self._force_backoff_delay = self._server_unavailable_delay
597
+ log.warning(
598
+ "Server unavailable (1013), waiting %.0fs before reconnect",
599
+ self._server_unavailable_delay,
600
+ )
601
+
527
602
  # Cancel background tasks
528
603
  await self._cancel_background_tasks()
529
604
 
530
- # Invoke on_disconnect callback
605
+ # Invoke on_disconnect callback (per-event)
531
606
  if self._on_disconnect is not None:
532
607
  result = self._on_disconnect(reason)
533
608
  if asyncio.iscoroutine(result):
534
609
  await result
535
610
 
611
+ # Throttled alert callback (aggregated count since last alert)
612
+ await self._maybe_emit_disconnect_alert(reason)
613
+
536
614
  log.info("WebSocket disconnected: %s (code=%d)", reason.name, code)
537
615
 
538
616
  # Handle reconnection
@@ -552,6 +630,33 @@ class WebSocketManager:
552
630
  self._state = ConnectionState.DISCONNECTED
553
631
  self._disconnected_event.set()
554
632
 
633
+ async def _maybe_emit_disconnect_alert(self, reason: DisconnectReason) -> None:
634
+ """Emit ``on_disconnect_alert`` if the throttle window has elapsed.
635
+
636
+ The aggregated count is the number of disconnects observed since
637
+ the previous alert (including this one). After firing, the counter
638
+ is reset to 0 and the timestamp updated.
639
+ """
640
+ if self._on_disconnect_alert is None:
641
+ return
642
+
643
+ self._disconnect_alert_count += 1
644
+
645
+ now = time.monotonic()
646
+ if now - self._last_disconnect_alert_at < self._disconnect_alert_interval:
647
+ return
648
+
649
+ count = self._disconnect_alert_count
650
+ self._disconnect_alert_count = 0
651
+ self._last_disconnect_alert_at = now
652
+
653
+ try:
654
+ result = self._on_disconnect_alert(reason, count)
655
+ if asyncio.iscoroutine(result):
656
+ await result
657
+ except Exception:
658
+ log.exception("Error in on_disconnect_alert callback")
659
+
555
660
  async def _reconnect_check_loop(self) -> None:
556
661
  """Background task to check should_reconnect callback."""
557
662
  while self._state == ConnectionState.RECONNECTING and not self._shutdown_event.is_set():
@@ -582,11 +687,22 @@ class WebSocketManager:
582
687
  log.warning("Error in should_reconnect callback: %s", e)
583
688
 
584
689
  async def _attempt_reconnect(self) -> None:
585
- """Attempt to reconnect with retry logic."""
690
+ """Attempt to reconnect with retry logic.
691
+
692
+ Honors ``_force_backoff_delay`` first if set (e.g. after close
693
+ code 1013), then applies the strategy-based backoff.
694
+ """
586
695
  if self._shutdown_event.is_set():
587
696
  log.debug("Shutdown requested, skipping reconnect attempt")
588
697
  return
589
698
 
699
+ if self._force_backoff_delay is not None:
700
+ delay = self._force_backoff_delay
701
+ self._force_backoff_delay = None
702
+ await asyncio.sleep(delay)
703
+ if self._shutdown_event.is_set():
704
+ return
705
+
590
706
  self._reconnect_count += 1
591
707
 
592
708
  if self._reconnect_count > self._max_reconnect_attempts:
@@ -635,12 +751,18 @@ class WebSocketManager:
635
751
  await asyncio.sleep(delay)
636
752
 
637
753
  async def _cancel_background_tasks(self) -> None:
638
- """Cancel all background tasks."""
754
+ """Cancel all background tasks.
755
+
756
+ Note: cancelling ``_stable_connection_task`` does NOT reset
757
+ ``_reconnect_count``. If the connection was torn down before the
758
+ stable window elapsed, the caller is treated as still flapping.
759
+ """
639
760
  tasks = [
640
761
  self._receive_task,
641
762
  self._disconnect_check_task,
642
763
  self._reconnect_check_task,
643
764
  self._ping_task,
765
+ self._stable_connection_task,
644
766
  ]
645
767
  for task in tasks:
646
768
  if task is not None and not task.done():
@@ -652,6 +774,7 @@ class WebSocketManager:
652
774
  self._disconnect_check_task = None
653
775
  self._reconnect_check_task = None
654
776
  self._ping_task = None
777
+ self._stable_connection_task = None
655
778
 
656
779
  async def _resubscribe(self) -> None:
657
780
  """Re-subscribe to all channels after reconnection."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kstlib
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: Config-driven helpers for Python projects (dynamic config, secure secrets, preset logging, and more…)
5
5
  Author-email: Michel TRUONG <michel.truong@gmail.com>
6
6
  Maintainer-email: Michel TRUONG <michel.truong@gmail.com>
@@ -42,7 +42,7 @@ Requires-Dist: websockets<16,>=15.0
42
42
  Requires-Dist: jinja2<4,>=3.1.5
43
43
  Requires-Dist: humanize<5,>=4.11
44
44
  Requires-Dist: httpx<1,>=0.28
45
- Requires-Dist: authlib<2,>=1.6.9
45
+ Requires-Dist: authlib<2,>=1.6.11
46
46
  Requires-Dist: pendulum<4,>=3.0
47
47
  Requires-Dist: cryptography>=46.0.7
48
48
  Requires-Dist: requests>=2.33.0
@@ -14,7 +14,7 @@ websockets<16,>=15.0
14
14
  jinja2<4,>=3.1.5
15
15
  humanize<5,>=4.11
16
16
  httpx<1,>=0.28
17
- authlib<2,>=1.6.9
17
+ authlib<2,>=1.6.11
18
18
  pendulum<4,>=3.0
19
19
  cryptography>=46.0.7
20
20
  requests>=2.33.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes