kstlib 2.1.4__tar.gz → 2.2.0__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 (192) hide show
  1. {kstlib-2.1.4/src/kstlib.egg-info → kstlib-2.2.0}/PKG-INFO +8 -4
  2. {kstlib-2.1.4 → kstlib-2.2.0}/pyproject.toml +50 -11
  3. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/__init__.py +5 -12
  4. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/__init__.py +1 -0
  5. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/channels/__init__.py +1 -0
  6. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/channels/base.py +8 -0
  7. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/channels/email.py +5 -0
  8. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/channels/slack.py +8 -0
  9. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/exceptions.py +4 -0
  10. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/manager.py +87 -56
  11. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/models.py +5 -0
  12. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/throttle.py +7 -0
  13. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/__init__.py +1 -0
  14. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/callback.py +6 -0
  15. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/check.py +11 -1
  16. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/config.py +12 -0
  17. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/models.py +10 -0
  18. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/providers/base.py +11 -0
  19. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/providers/oauth2.py +16 -3
  20. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/providers/oidc.py +21 -6
  21. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/session.py +4 -0
  22. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/token.py +10 -0
  23. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cache/__init__.py +1 -0
  24. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cache/decorator.py +3 -0
  25. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cache/strategies.py +12 -0
  26. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/app.py +3 -0
  27. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/check.py +15 -0
  28. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/common.py +2 -0
  29. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/login.py +4 -1
  30. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/status.py +9 -0
  31. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/token.py +1 -0
  32. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/ops/attach.py +1 -0
  33. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/ops/common.py +60 -29
  34. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/ops/list_sessions.py +4 -0
  35. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/ops/logs.py +1 -0
  36. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/ops/start.py +2 -1
  37. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/ops/status.py +3 -0
  38. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/ops/stop.py +2 -1
  39. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/rapi/__init__.py +35 -3
  40. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/rapi/call.py +39 -2
  41. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/rapi/list.py +10 -6
  42. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/rapi/show.py +5 -6
  43. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/secrets/common.py +1 -0
  44. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/secrets/doctor.py +12 -0
  45. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/config/exceptions.py +1 -0
  46. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/config/loader.py +66 -6
  47. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/config/sops.py +9 -0
  48. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/db/__init__.py +1 -0
  49. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/db/aiosqlcipher.py +4 -1
  50. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/db/cipher.py +2 -0
  51. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/db/database.py +9 -0
  52. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/db/pool.py +5 -0
  53. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/helpers/__init__.py +1 -0
  54. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/helpers/time_trigger.py +12 -0
  55. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/kstlib.conf.yml +53 -0
  56. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/limits.py +19 -0
  57. kstlib-2.2.0/src/kstlib/logging/__init__.py +169 -0
  58. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/logging/manager.py +134 -2
  59. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/__init__.py +1 -0
  60. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/builder.py +238 -19
  61. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/exceptions.py +3 -1
  62. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/filesystem.py +1 -0
  63. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/transport.py +10 -1
  64. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/transports/gmail.py +8 -0
  65. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/transports/resend.py +10 -0
  66. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/transports/ses.py +6 -0
  67. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/transports/smtp.py +10 -0
  68. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/meta.py +1 -1
  69. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/metrics/__init__.py +1 -0
  70. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/metrics/decorators.py +12 -0
  71. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/metrics/exceptions.py +4 -1
  72. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/__init__.py +1 -0
  73. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/_styles.py +2 -0
  74. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/cell.py +2 -0
  75. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/config.py +9 -0
  76. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/delivery.py +11 -0
  77. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/exceptions.py +2 -0
  78. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/image.py +7 -2
  79. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/kv.py +2 -0
  80. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/list.py +2 -0
  81. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/metric.py +2 -0
  82. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/monitoring.py +8 -1
  83. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/renderer.py +7 -3
  84. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/service.py +11 -0
  85. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/table.py +3 -0
  86. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/types.py +2 -0
  87. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ops/__init__.py +1 -0
  88. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ops/base.py +8 -0
  89. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ops/container.py +20 -2
  90. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ops/exceptions.py +6 -0
  91. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ops/manager.py +12 -0
  92. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ops/models.py +5 -0
  93. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ops/tmux.py +18 -4
  94. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ops/validators.py +7 -0
  95. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/pipeline/__init__.py +1 -0
  96. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/pipeline/base.py +2 -0
  97. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/pipeline/exceptions.py +8 -0
  98. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/pipeline/models.py +14 -0
  99. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/pipeline/runner.py +36 -8
  100. kstlib-2.2.0/src/kstlib/pipeline/steps/_base.py +118 -0
  101. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/pipeline/steps/callable.py +63 -1
  102. kstlib-2.2.0/src/kstlib/pipeline/steps/python.py +71 -0
  103. kstlib-2.2.0/src/kstlib/pipeline/steps/shell.py +74 -0
  104. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/pipeline/validators.py +15 -13
  105. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/rapi/__init__.py +4 -0
  106. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/rapi/client.py +178 -26
  107. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/rapi/config.py +399 -0
  108. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/rapi/credentials.py +71 -14
  109. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/rapi/exceptions.py +60 -1
  110. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/resilience/__init__.py +1 -0
  111. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/resilience/circuit_breaker.py +9 -3
  112. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/resilience/exceptions.py +13 -5
  113. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/resilience/heartbeat.py +11 -0
  114. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/resilience/rate_limiter.py +12 -2
  115. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/resilience/shutdown.py +8 -0
  116. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/resilience/watchdog.py +11 -0
  117. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/models.py +2 -0
  118. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/providers/base.py +1 -0
  119. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/providers/environment.py +2 -0
  120. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/providers/keyring.py +5 -0
  121. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/providers/kms.py +4 -0
  122. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/providers/kwargs.py +6 -0
  123. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/resolver.py +6 -0
  124. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/sensitive.py +1 -0
  125. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secure/fs.py +8 -1
  126. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secure/permissions.py +3 -0
  127. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ssl.py +10 -0
  128. kstlib-2.2.0/src/kstlib/transform/__init__.py +116 -0
  129. kstlib-2.2.0/src/kstlib/transform/chain.py +1095 -0
  130. kstlib-2.2.0/src/kstlib/transform/config.py +912 -0
  131. kstlib-2.2.0/src/kstlib/transform/exceptions.py +184 -0
  132. kstlib-2.2.0/src/kstlib/transform/primitives.py +720 -0
  133. kstlib-2.2.0/src/kstlib/transform/validators.py +382 -0
  134. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ui/exceptions.py +5 -3
  135. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ui/panels.py +4 -0
  136. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ui/spinner.py +7 -0
  137. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ui/tables.py +2 -0
  138. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/dict.py +1 -0
  139. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/formatting.py +9 -0
  140. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/http_trace.py +38 -7
  141. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/lazy.py +1 -0
  142. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/secure_delete.py +1 -0
  143. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/serialization.py +10 -3
  144. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/validators.py +35 -1
  145. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/websocket/__init__.py +1 -0
  146. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/websocket/exceptions.py +13 -0
  147. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/websocket/manager.py +19 -1
  148. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/websocket/models.py +16 -0
  149. {kstlib-2.1.4 → kstlib-2.2.0/src/kstlib.egg-info}/PKG-INFO +8 -4
  150. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib.egg-info/SOURCES.txt +7 -0
  151. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib.egg-info/requires.txt +7 -3
  152. kstlib-2.1.4/src/kstlib/logging/__init__.py +0 -126
  153. kstlib-2.1.4/src/kstlib/pipeline/steps/python.py +0 -136
  154. kstlib-2.1.4/src/kstlib/pipeline/steps/shell.py +0 -142
  155. {kstlib-2.1.4 → kstlib-2.2.0}/LICENSE.md +0 -0
  156. {kstlib-2.1.4 → kstlib-2.2.0}/MANIFEST.in +0 -0
  157. {kstlib-2.1.4 → kstlib-2.2.0}/README.md +0 -0
  158. {kstlib-2.1.4 → kstlib-2.2.0}/setup.cfg +0 -0
  159. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/__main__.py +0 -0
  160. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/errors.py +0 -0
  161. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/providers/__init__.py +0 -0
  162. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/__init__.py +0 -0
  163. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/__init__.py +0 -0
  164. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/__init__.py +0 -0
  165. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/logout.py +0 -0
  166. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/providers.py +0 -0
  167. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/whoami.py +0 -0
  168. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/config.py +0 -0
  169. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/ops/__init__.py +0 -0
  170. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/secrets/__init__.py +0 -0
  171. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/secrets/decrypt.py +0 -0
  172. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/secrets/encrypt.py +0 -0
  173. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/secrets/shred.py +0 -0
  174. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/common.py +0 -0
  175. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/config/__init__.py +0 -0
  176. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/config/export.py +0 -0
  177. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/db/exceptions.py +0 -0
  178. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/helpers/exceptions.py +0 -0
  179. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/transports/__init__.py +0 -0
  180. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/pipeline/steps/__init__.py +0 -0
  181. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/py.typed +0 -0
  182. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/__init__.py +0 -0
  183. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/exceptions.py +0 -0
  184. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/providers/__init__.py +0 -0
  185. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/providers/sops.py +0 -0
  186. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secure/__init__.py +0 -0
  187. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ui/__init__.py +0 -0
  188. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/__init__.py +0 -0
  189. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/text.py +0 -0
  190. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib.egg-info/dependency_links.txt +0 -0
  191. {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib.egg-info/entry_points.txt +0 -0
  192. {kstlib-2.1.4 → kstlib-2.2.0}/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.1.4
3
+ Version: 2.2.0
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>
@@ -30,6 +30,8 @@ Requires-Dist: pyyaml<7,>=6.0
30
30
  Requires-Dist: tomli<3,>=2.3
31
31
  Requires-Dist: tomli-w<2,>=1.0
32
32
  Requires-Dist: python-box<8,>=7.3
33
+ Requires-Dist: platformdirs<5,>=4.0
34
+ Requires-Dist: defusedxml<1,>=0.7
33
35
  Requires-Dist: typer<1,>=0.19
34
36
  Requires-Dist: click<9,>=8.3
35
37
  Requires-Dist: rich<15,>=14.2
@@ -42,16 +44,17 @@ Requires-Dist: humanize<5,>=4.11
42
44
  Requires-Dist: httpx<1,>=0.28
43
45
  Requires-Dist: authlib<2,>=1.6.9
44
46
  Requires-Dist: pendulum<4,>=3.0
45
- Requires-Dist: cryptography>=46.0.6
47
+ Requires-Dist: cryptography>=46.0.7
46
48
  Requires-Dist: requests>=2.33.0
47
49
  Requires-Dist: urllib3>=2.6.3
48
50
  Provides-Extra: dev
49
- Requires-Dist: pytest<9,>=8.4; extra == "dev"
51
+ Requires-Dist: pytest<10,>=9.0.3; extra == "dev"
50
52
  Requires-Dist: pytest-cov<8,>=7.0; extra == "dev"
51
53
  Requires-Dist: pytest-asyncio<2,>=1.2; extra == "dev"
52
54
  Requires-Dist: ruff<1,>=0.14; extra == "dev"
53
55
  Requires-Dist: mypy<2,>=1.18; extra == "dev"
54
56
  Requires-Dist: types-PyYAML<7,>=6.0; extra == "dev"
57
+ Requires-Dist: types-defusedxml<1,>=0.7; extra == "dev"
55
58
  Requires-Dist: pre-commit<5,>=4.0; extra == "dev"
56
59
  Requires-Dist: filelock>=3.20.3; extra == "dev"
57
60
  Requires-Dist: jaraco-context>=6.1.0; extra == "dev"
@@ -81,12 +84,13 @@ Requires-Dist: awscli-local<1,>=0.22; extra == "infra-tools"
81
84
  Provides-Extra: textual
82
85
  Requires-Dist: textual<7,>=6.3; extra == "textual"
83
86
  Provides-Extra: all
84
- Requires-Dist: pytest<9,>=8.4; extra == "all"
87
+ Requires-Dist: pytest<10,>=9.0.3; extra == "all"
85
88
  Requires-Dist: pytest-cov<8,>=7.0; extra == "all"
86
89
  Requires-Dist: pytest-asyncio<2,>=1.2; extra == "all"
87
90
  Requires-Dist: ruff<1,>=0.14; extra == "all"
88
91
  Requires-Dist: mypy<2,>=1.18; extra == "all"
89
92
  Requires-Dist: types-PyYAML<7,>=6.0; extra == "all"
93
+ Requires-Dist: types-defusedxml<1,>=0.7; extra == "all"
90
94
  Requires-Dist: sphinx<9,>=8.1; extra == "all"
91
95
  Requires-Dist: furo<2026,>=2025.9; extra == "all"
92
96
  Requires-Dist: myst-parser<5,>=4.0; extra == "all"
@@ -32,10 +32,12 @@ keywords = [
32
32
  dynamic = ["version"]
33
33
  dependencies = [
34
34
  # --- Configuration & Serialization ---
35
- "pyyaml>=6.0,<7", # YAML config parsing
36
- "tomli>=2.3,<3", # Read-only TOML parser (for pyproject & configs)
37
- "tomli-w>=1.0,<2", # TOML writer for config exports
38
- "python-box>=7.3,<8", # Dot-access dicts (Box, BoxList, ConfigBox)
35
+ "pyyaml>=6.0,<7", # YAML config parsing
36
+ "tomli>=2.3,<3", # Read-only TOML parser (for pyproject & configs)
37
+ "tomli-w>=1.0,<2", # TOML writer for config exports
38
+ "python-box>=7.3,<8", # Dot-access dicts (Box, BoxList, ConfigBox)
39
+ "platformdirs>=4.0,<5", # Cross-platform system config dirs (XDG on Linux, native on Mac/Windows)
40
+ "defusedxml>=0.7,<1", # XXE-safe XML parsing for kstlib.transform (CVE protection)
39
41
 
40
42
  # --- CLI & Output Interface ---
41
43
  "typer>=0.19,<1", # CLI framework (based on Click)
@@ -60,7 +62,7 @@ dependencies = [
60
62
  "pendulum>=3.0,<4", # Modern datetime library with timezone support
61
63
 
62
64
  # --- Security: transitive dep lower bounds (CVE) ---
63
- "cryptography>=46.0.6", # CVE-2026-26007 + CVE-2026-34073 (via authlib)
65
+ "cryptography>=46.0.7", # CVE-2026-26007 + CVE-2026-34073 + CVE-2026-39892
64
66
  "requests>=2.33.0", # CVE-2024-47081 + CVE-2026-25645 (via httpx)
65
67
  "urllib3>=2.6.3", # CVE-2025-50182/50181 + CVE-2025-66418/66471 + CVE-2026-21441 (via requests)
66
68
  ]
@@ -91,7 +93,7 @@ Changelog = "https://github.com/KaminoU/kstlib/blob/main/CHANGELOG.md"
91
93
  [project.optional-dependencies]
92
94
  # Development tools (testing, linting, typing)
93
95
  dev = [
94
- "pytest>=8.4,<9",
96
+ "pytest>=9.0.3,<10", # >=9.0.3 pulls CVE-2025-71176 temp dir fix
95
97
  "pytest-cov>=7.0,<8",
96
98
  "pytest-asyncio>=1.2,<2", # For async tests (roadmap Phase 0)
97
99
 
@@ -99,8 +101,9 @@ dev = [
99
101
  "ruff>=0.14,<1", # Fast linter + formatter (replaces Flake8, isort, Black, Pylint)
100
102
 
101
103
  # Typing checks
102
- "mypy>=1.18,<2", # Static type checker (validates type hints)
103
- "types-PyYAML>=6.0,<7", # Type stubs for PyYAML (used by mypy)
104
+ "mypy>=1.18,<2", # Static type checker (validates type hints)
105
+ "types-PyYAML>=6.0,<7", # Type stubs for PyYAML (used by mypy)
106
+ "types-defusedxml>=0.7,<1", # Type stubs for defusedxml (used by mypy)
104
107
 
105
108
  # Git hooks
106
109
  "pre-commit>=4.0,<5", # Git pre-commit hooks for local CI checks
@@ -152,12 +155,13 @@ textual = ["textual>=6.3,<7"]
152
155
 
153
156
  # Everything for local development
154
157
  all = [
155
- "pytest>=8.4,<9",
158
+ "pytest>=9.0.3,<10", # >=9.0.3 pulls CVE-2025-71176 temp dir fix
156
159
  "pytest-cov>=7.0,<8",
157
160
  "pytest-asyncio>=1.2,<2",
158
161
  "ruff>=0.14,<1",
159
162
  "mypy>=1.18,<2",
160
163
  "types-PyYAML>=6.0,<7",
164
+ "types-defusedxml>=0.7,<1",
161
165
  "sphinx>=8.1,<9",
162
166
  "furo>=2025.9,<2026",
163
167
  "myst-parser>=4.0,<5",
@@ -285,11 +289,14 @@ ignore = [
285
289
  "src/kstlib/cli/commands/secrets/encrypt.py" = ["PLR0913"]
286
290
  "src/kstlib/cli/commands/secrets/shred.py" = ["PLR0913"]
287
291
  # T201: print statement - acceptable for CLI raw output (piping, --raw flags)
288
- # PLR0912/C901/PLR0915: Too many branches/complexity/statements - acceptable for CLI commands with multiple output modes
289
- "src/kstlib/cli/commands/auth/login.py" = ["PLR0912", "C901"]
292
+ # PLR0912/C901/PLR0913/PLR0915: Too many branches/complexity/args/statements - acceptable for CLI commands with multiple output modes
293
+ "src/kstlib/cli/commands/auth/login.py" = ["PLR0912", "PLR0913", "C901"]
290
294
  "src/kstlib/cli/commands/auth/status.py" = ["PLR0912", "C901"]
291
295
  "src/kstlib/cli/commands/auth/token.py" = ["T201", "C901", "PLR0912", "PLR0913", "PLR0915"]
292
296
  "src/kstlib/cli/commands/auth/whoami.py" = ["T201", "PLR0912", "C901"]
297
+ "src/kstlib/cli/commands/ops/common.py" = ["PLR0913"]
298
+ "src/kstlib/cli/commands/ops/start.py" = ["PLR0913"]
299
+ "src/kstlib/cli/commands/ops/stop.py" = ["PLR0913"]
293
300
  "src/kstlib/cli/commands/rapi/call.py" = ["T201", "PLR0913"]
294
301
  # S110: try-except-pass - config loading is intentionally optional (no-op on failure)
295
302
  # PLR0913: Too many arguments - decorator API requires multiple options
@@ -353,6 +360,37 @@ ignore = [
353
360
  "src/kstlib/alerts/channels/slack.py" = ["PLR0913"]
354
361
  # Alert manager - transport factory has many branches for different transport types
355
362
  "src/kstlib/alerts/manager.py" = ["C901"]
363
+ # Auth check - "token_type" is a label string ("id_token"/"access_token"), not a credential
364
+ "src/kstlib/auth/check.py" = ["S107"]
365
+ # aiosqlcipher - optional dependency feature detection via try/except import
366
+ "src/kstlib/db/aiosqlcipher.py" = ["F401"]
367
+ # Logging - auto-init and preset discovery MUST swallow exceptions to avoid breaking host apps
368
+ "src/kstlib/logging/__init__.py" = ["S110"]
369
+ "src/kstlib/logging/manager.py" = ["S110"]
370
+ # Mail builder - snapshot/notify path legitimately reassigns same-class private state
371
+ "src/kstlib/mail/builder.py" = ["SLF001"]
372
+ # Monitoring image - RenderError chain already raised via "from err"; no additional ignores needed here
373
+ # Monitoring renderer - Markup values are escaped before wrapping; jinja2 Environment defaults to autoescape=True
374
+ "src/kstlib/monitoring/renderer.py" = ["S701", "S704"]
375
+ # Monitoring service config has many options
376
+ "src/kstlib/monitoring/monitoring.py" = ["PLR0913"]
377
+ # ops/* - subprocess calls validated via ops/validators.py (binary resolved via shutil.which, list-form args)
378
+ # S606/S108: tmux default socket path /tmp/tmux-<uid> is a Unix convention, not a user-controlled tempfile
379
+ "src/kstlib/ops/container.py" = ["S603"]
380
+ "src/kstlib/ops/tmux.py" = ["S603", "S606", "S108", "ARG002"]
381
+ # Pipeline steps - subprocess calls validated (python via shutil.which, shell is documented opt-in)
382
+ "src/kstlib/pipeline/steps/python.py" = ["S603"]
383
+ "src/kstlib/pipeline/steps/shell.py" = ["S602"]
384
+ # Pipeline validators - many validation options
385
+ "src/kstlib/pipeline/validators.py" = ["PLR0913"]
386
+ # Transform chain config has many options
387
+ "src/kstlib/transform/chain.py" = ["PLR0913"]
388
+ # Transform primitives - PrimitiveConfig used in runtime signatures (not TYPE_CHECKING-only)
389
+ # N817: ElementTree imported as ET is the stdlib-documented convention for xml/defusedxml
390
+ "src/kstlib/transform/primitives.py" = ["TC001", "N817"]
391
+ # utils/http_trace - httpx._types accessed intentionally for accurate event hook typing
392
+ "src/kstlib/utils/http_trace.py" = ["SLF001"]
393
+ # utils/serialization - defusedxml.minidom used for display pretty-printing
356
394
 
357
395
  [tool.ruff.format]
358
396
  # Use double quotes for strings
@@ -499,6 +537,7 @@ addopts = [
499
537
  "--cov=src",
500
538
  "--cov-report=term-missing",
501
539
  "--cov-report=html",
540
+ "--doctest-modules",
502
541
  ]
503
542
 
504
543
  # Markers for test categorization
@@ -187,23 +187,16 @@ def __getattr__(name: str) -> Any:
187
187
  # Handle lazy imports
188
188
  if name in _LAZY_IMPORTS:
189
189
  module_path, attr_name = _LAZY_IMPORTS[name]
190
- try:
191
- module = importlib.import_module(module_path)
192
- attr = getattr(module, attr_name)
193
- _loaded[name] = attr
194
- return attr
195
- except (ImportError, AttributeError):
196
- # ConfigNotLoadedError might not exist in minimal installs
197
- if name == "ConfigNotLoadedError":
198
- _loaded[name] = None
199
- return None
200
- raise
190
+ module = importlib.import_module(module_path)
191
+ attr = getattr(module, attr_name)
192
+ _loaded[name] = attr
193
+ return attr
201
194
 
202
195
  raise AttributeError(f"module 'kstlib' has no attribute {name!r}")
203
196
 
204
197
 
205
198
  # Auto-install rich traceback if KSTLIB_TRACEBACK=1 (opt-in for fast imports)
206
- # Default changed from "1" to "0" for metricsormance - users must opt-in now
199
+ # Default changed from "1" to "0" for performance - users must opt-in now
207
200
  if TYPE_CHECKING:
208
201
  # For static analysis - provide type hints for lazy-loaded symbols
209
202
  # pylint: disable=useless-import-alias
@@ -85,6 +85,7 @@ Examples:
85
85
  config=app_config["alerts"],
86
86
  credential_resolver=resolver,
87
87
  )
88
+
88
89
  """
89
90
 
90
91
  from kstlib.alerts.exceptions import (
@@ -22,6 +22,7 @@ Examples:
22
22
  sender="alerts@example.com",
23
23
  recipients=["oncall@example.com"],
24
24
  )
25
+
25
26
  """
26
27
 
27
28
  from kstlib.alerts.channels.base import AlertChannel, AsyncAlertChannel
@@ -27,6 +27,7 @@ Examples:
27
27
  # Send via HTTP POST
28
28
  pass
29
29
  return AlertResult(channel=self.name, success=True)
30
+
30
31
  """
31
32
 
32
33
  from __future__ import annotations
@@ -57,6 +58,7 @@ class AlertChannel(ABC):
57
58
  def send(self, alert: AlertMessage) -> AlertResult:
58
59
  print(f"[{alert.level.name}] {alert.title}")
59
60
  return AlertResult(channel=self.name, success=True)
61
+
60
62
  """
61
63
 
62
64
  @property
@@ -79,6 +81,7 @@ class AlertChannel(ABC):
79
81
 
80
82
  Raises:
81
83
  AlertDeliveryError: If delivery fails.
84
+
82
85
  """
83
86
 
84
87
 
@@ -100,6 +103,7 @@ class AsyncAlertChannel(ABC):
100
103
  async with httpx.AsyncClient() as client:
101
104
  await client.post(...)
102
105
  return AlertResult(channel=self.name, success=True)
106
+
103
107
  """
104
108
 
105
109
  @property
@@ -122,6 +126,7 @@ class AsyncAlertChannel(ABC):
122
126
 
123
127
  Raises:
124
128
  AlertDeliveryError: If delivery fails.
129
+
125
130
  """
126
131
 
127
132
 
@@ -144,6 +149,7 @@ class AsyncChannelWrapper(AsyncAlertChannel):
144
149
 
145
150
  # Now usable in async context
146
151
  await async_channel.send(alert)
152
+
147
153
  """
148
154
 
149
155
  def __init__(
@@ -157,6 +163,7 @@ class AsyncChannelWrapper(AsyncAlertChannel):
157
163
  Args:
158
164
  channel: The sync channel to wrap.
159
165
  executor: Optional custom thread pool executor.
166
+
160
167
  """
161
168
  self._channel = channel
162
169
  self._executor = executor
@@ -185,6 +192,7 @@ class AsyncChannelWrapper(AsyncAlertChannel):
185
192
 
186
193
  Raises:
187
194
  AlertDeliveryError: If the underlying channel fails.
195
+
188
196
  """
189
197
  loop = asyncio.get_running_loop()
190
198
  return await loop.run_in_executor(
@@ -29,6 +29,7 @@ Examples:
29
29
  recipients=["oncall@example.com", "backup@example.com"],
30
30
  subject_prefix="[PROD ALERT]",
31
31
  )
32
+
32
33
  """
33
34
 
34
35
  from __future__ import annotations
@@ -96,6 +97,7 @@ class EmailChannel(AsyncAlertChannel):
96
97
  recipients=["oncall@company.com"],
97
98
  subject_prefix="[PROD]",
98
99
  )
100
+
99
101
  """
100
102
 
101
103
  def __init__(
@@ -118,6 +120,7 @@ class EmailChannel(AsyncAlertChannel):
118
120
 
119
121
  Raises:
120
122
  AlertConfigurationError: If configuration is invalid.
123
+
121
124
  """
122
125
  if not sender:
123
126
  raise AlertConfigurationError("Email sender is required")
@@ -161,6 +164,7 @@ class EmailChannel(AsyncAlertChannel):
161
164
 
162
165
  Raises:
163
166
  AlertDeliveryError: If email delivery fails.
167
+
164
168
  """
165
169
  message = self._build_message(alert)
166
170
 
@@ -195,6 +199,7 @@ class EmailChannel(AsyncAlertChannel):
195
199
 
196
200
  Returns:
197
201
  EmailMessage ready for transport.
202
+
198
203
  """
199
204
  message = EmailMessage()
200
205
 
@@ -35,6 +35,7 @@ Examples:
35
35
  config={"credentials": "slack_webhook"},
36
36
  credential_resolver=resolver,
37
37
  )
38
+
38
39
  """
39
40
 
40
41
  from __future__ import annotations
@@ -100,6 +101,7 @@ def _mask_webhook_url(url: str) -> str:
100
101
  Examples:
101
102
  >>> _mask_webhook_url("https://hooks.slack.com/services/T123/B456/xyz")
102
103
  'https://hooks.slack.com/services/T***/B***/***'
104
+
103
105
  """
104
106
  if not url or "hooks.slack.com" not in url:
105
107
  return "***"
@@ -123,6 +125,7 @@ def _truncate(text: str, max_length: int) -> str:
123
125
 
124
126
  Returns:
125
127
  Truncated text with '...' if exceeded.
128
+
126
129
  """
127
130
  if len(text) <= max_length:
128
131
  return text
@@ -161,6 +164,7 @@ class SlackChannel(AsyncAlertChannel):
161
164
  icon_emoji=":fire:",
162
165
  timeout=5.0,
163
166
  )
167
+
164
168
  """
165
169
 
166
170
  def __init__(
@@ -190,6 +194,7 @@ class SlackChannel(AsyncAlertChannel):
190
194
 
191
195
  Raises:
192
196
  AlertConfigurationError: If webhook_url is invalid.
197
+
193
198
  """
194
199
  if not webhook_url:
195
200
  raise AlertConfigurationError("Slack webhook URL is required")
@@ -240,6 +245,7 @@ class SlackChannel(AsyncAlertChannel):
240
245
 
241
246
  Raises:
242
247
  AlertDeliveryError: If the webhook request fails.
248
+
243
249
  """
244
250
  try:
245
251
  import httpx
@@ -303,6 +309,7 @@ class SlackChannel(AsyncAlertChannel):
303
309
 
304
310
  Returns:
305
311
  Dict suitable for JSON serialization.
312
+
306
313
  """
307
314
  # Truncate to Slack limits (use formatted_title for timestamp support)
308
315
  title = _truncate(alert.formatted_title, MAX_TITLE_LENGTH)
@@ -353,6 +360,7 @@ class SlackChannel(AsyncAlertChannel):
353
360
 
354
361
  Raises:
355
362
  AlertConfigurationError: If configuration is invalid.
363
+
356
364
  """
357
365
  # Get webhook URL from credentials
358
366
  cred_name = config.get("credentials")
@@ -26,6 +26,7 @@ class AlertDeliveryError(AlertError):
26
26
  'slack'
27
27
  >>> err.retryable
28
28
  True
29
+
29
30
  """
30
31
 
31
32
  def __init__(self, message: str, *, channel: str, retryable: bool = False) -> None:
@@ -35,6 +36,7 @@ class AlertDeliveryError(AlertError):
35
36
  message: Error description.
36
37
  channel: Name of the channel that failed.
37
38
  retryable: Whether the delivery could succeed on retry.
39
+
38
40
  """
39
41
  super().__init__(message)
40
42
  self.channel = channel
@@ -51,6 +53,7 @@ class AlertThrottledError(AlertError):
51
53
  >>> err = AlertThrottledError("Rate limit exceeded", retry_after=30.0)
52
54
  >>> err.retry_after
53
55
  30.0
56
+
54
57
  """
55
58
 
56
59
  def __init__(self, message: str, *, retry_after: float) -> None:
@@ -59,6 +62,7 @@ class AlertThrottledError(AlertError):
59
62
  Args:
60
63
  message: Error description.
61
64
  retry_after: Seconds until the rate limit resets.
65
+
62
66
  """
63
67
  super().__init__(message)
64
68
  self.retry_after = retry_after
@@ -29,6 +29,7 @@ Examples:
29
29
  .add_channel(slack_channel, min_level=AlertLevel.INFO)
30
30
  .add_channel(email_channel, min_level=AlertLevel.CRITICAL)
31
31
  )
32
+
32
33
  """
33
34
 
34
35
  from __future__ import annotations
@@ -77,6 +78,7 @@ class AlertManagerStats:
77
78
  total_failed: Total alerts that failed delivery.
78
79
  total_throttled: Total alerts dropped due to throttling.
79
80
  by_channel: Per-channel statistics.
81
+
80
82
  """
81
83
 
82
84
  total_sent: int = 0
@@ -147,6 +149,7 @@ class AlertManager:
147
149
  config=config["alerts"],
148
150
  credential_resolver=resolver,
149
151
  )
152
+
150
153
  """
151
154
 
152
155
  def __init__(self) -> None:
@@ -191,6 +194,7 @@ class AlertManager:
191
194
  AlertManager(channels=1)
192
195
  >>> manager.add_channel(email_channel, min_level=AlertLevel.CRITICAL) # doctest: +SKIP
193
196
  AlertManager(channels=2)
197
+
194
198
  """
195
199
  # Wrap sync channels for async usage
196
200
  if isinstance(channel, AlertChannel):
@@ -249,6 +253,7 @@ class AlertManager:
249
253
 
250
254
  >>> alerts = [alert1, alert2, alert3] # doctest: +SKIP
251
255
  >>> results = await manager.send(alerts, channel="watchdog") # doctest: +SKIP
256
+
252
257
  """
253
258
  if not self._channels:
254
259
  log.warning("No channels configured, alert not sent")
@@ -289,6 +294,7 @@ class AlertManager:
289
294
 
290
295
  Returns:
291
296
  List of results for this alert.
297
+
292
298
  """
293
299
  # Determine matching entries
294
300
  if target_entries is not None:
@@ -325,6 +331,7 @@ class AlertManager:
325
331
 
326
332
  Returns:
327
333
  List with matching entry, or empty list if not found.
334
+
328
335
  """
329
336
  for entry in self._channels:
330
337
  # Match by key (e.g., "hb")
@@ -351,6 +358,7 @@ class AlertManager:
351
358
 
352
359
  Returns:
353
360
  AlertResult with delivery status.
361
+
354
362
  """
355
363
  channel_name = entry.channel.name
356
364
 
@@ -433,6 +441,7 @@ class AlertManager:
433
441
 
434
442
  Raises:
435
443
  AlertConfigurationError: If configuration is invalid.
444
+
436
445
  """
437
446
  from kstlib.alerts.channels import SlackChannel
438
447
  from kstlib.alerts.throttle import AlertThrottle
@@ -524,6 +533,7 @@ def _parse_level(level_str: str) -> AlertLevel:
524
533
 
525
534
  Raises:
526
535
  AlertConfigurationError: If level is invalid.
536
+
527
537
  """
528
538
  level_map = {
529
539
  "info": AlertLevel.INFO,
@@ -537,6 +547,76 @@ def _parse_level(level_str: str) -> AlertLevel:
537
547
  return level
538
548
 
539
549
 
550
+ def _create_smtp_transport(
551
+ transport_config: Mapping[str, Any],
552
+ ) -> MailTransport:
553
+ """Build an SMTP mail transport from raw config."""
554
+ from kstlib.mail.transports import SMTPCredentials, SMTPSecurity, SMTPTransport
555
+
556
+ credentials = None
557
+ username = transport_config.get("username")
558
+ if username:
559
+ credentials = SMTPCredentials(
560
+ username=username,
561
+ password=transport_config.get("password"),
562
+ )
563
+
564
+ security = SMTPSecurity(
565
+ use_starttls=transport_config.get("use_tls", True),
566
+ )
567
+
568
+ return SMTPTransport(
569
+ host=transport_config.get("host", "localhost"),
570
+ port=int(transport_config.get("port", 587)),
571
+ credentials=credentials,
572
+ security=security,
573
+ )
574
+
575
+
576
+ def _create_resend_transport(
577
+ transport_config: Mapping[str, Any],
578
+ name: str,
579
+ credential_resolver: CredentialResolver | None,
580
+ ) -> AsyncMailTransport:
581
+ """Build a Resend mail transport from raw config."""
582
+ from kstlib.mail.transports import ResendTransport
583
+
584
+ api_key = transport_config.get("api_key")
585
+ if not api_key and credential_resolver:
586
+ cred_name = transport_config.get("credentials")
587
+ if cred_name:
588
+ record = credential_resolver.resolve(cred_name)
589
+ api_key = record.value
590
+
591
+ if not api_key:
592
+ raise AlertConfigurationError(f"Resend transport for '{name}' requires 'api_key' or 'credentials'")
593
+
594
+ return ResendTransport(api_key=api_key)
595
+
596
+
597
+ def _create_ses_transport(
598
+ transport_config: Mapping[str, Any],
599
+ credential_resolver: CredentialResolver | None,
600
+ ) -> AsyncMailTransport:
601
+ """Build an AWS SES mail transport from raw config."""
602
+ from kstlib.mail.transports import SesTransport
603
+
604
+ aws_access_key_id = transport_config.get("aws_access_key_id")
605
+ aws_secret_access_key = transport_config.get("aws_secret_access_key")
606
+
607
+ if credential_resolver:
608
+ cred_name = transport_config.get("credentials")
609
+ if cred_name:
610
+ record = credential_resolver.resolve(cred_name)
611
+ aws_access_key_id = aws_access_key_id or record.value
612
+
613
+ return SesTransport(
614
+ region=transport_config.get("region", "eu-west-3"),
615
+ aws_access_key_id=aws_access_key_id,
616
+ aws_secret_access_key=aws_secret_access_key,
617
+ )
618
+
619
+
540
620
  def _create_email_transport(
541
621
  transport_config: Mapping[str, Any],
542
622
  name: str,
@@ -544,6 +624,8 @@ def _create_email_transport(
544
624
  ) -> MailTransport | AsyncMailTransport:
545
625
  """Create a mail transport from configuration.
546
626
 
627
+ Delegates to a per-type factory to keep the dispatcher small.
628
+
547
629
  Args:
548
630
  transport_config: Transport configuration dict.
549
631
  name: Channel name for error messages.
@@ -554,72 +636,20 @@ def _create_email_transport(
554
636
 
555
637
  Raises:
556
638
  AlertConfigurationError: If configuration is invalid.
639
+
557
640
  """
558
641
  transport_type = transport_config.get("type", "smtp").lower()
559
-
560
642
  if transport_type == "smtp":
561
- from kstlib.mail.transports import SMTPCredentials, SMTPSecurity, SMTPTransport
562
-
563
- credentials = None
564
- username = transport_config.get("username")
565
- if username:
566
- credentials = SMTPCredentials(
567
- username=username,
568
- password=transport_config.get("password"),
569
- )
570
-
571
- security = SMTPSecurity(
572
- use_starttls=transport_config.get("use_tls", True),
573
- )
574
-
575
- return SMTPTransport(
576
- host=transport_config.get("host", "localhost"),
577
- port=int(transport_config.get("port", 587)),
578
- credentials=credentials,
579
- security=security,
580
- )
581
-
643
+ return _create_smtp_transport(transport_config)
582
644
  if transport_type == "gmail":
583
- # Gmail requires OAuth2 Token from kstlib.auth module
584
- # Use GmailTransport directly with a Token object in code
585
645
  raise AlertConfigurationError(
586
646
  f"Gmail transport for '{name}' requires programmatic configuration. "
587
647
  "Use GmailTransport(token=...) directly instead of config."
588
648
  )
589
-
590
649
  if transport_type == "resend":
591
- from kstlib.mail.transports import ResendTransport
592
-
593
- api_key = transport_config.get("api_key")
594
- if not api_key and credential_resolver:
595
- cred_name = transport_config.get("credentials")
596
- if cred_name:
597
- record = credential_resolver.resolve(cred_name)
598
- api_key = record.value
599
-
600
- if not api_key:
601
- raise AlertConfigurationError(f"Resend transport for '{name}' requires 'api_key' or 'credentials'")
602
-
603
- return ResendTransport(api_key=api_key)
604
-
650
+ return _create_resend_transport(transport_config, name, credential_resolver)
605
651
  if transport_type == "ses":
606
- from kstlib.mail.transports import SesTransport
607
-
608
- aws_access_key_id = transport_config.get("aws_access_key_id")
609
- aws_secret_access_key = transport_config.get("aws_secret_access_key")
610
-
611
- if credential_resolver:
612
- cred_name = transport_config.get("credentials")
613
- if cred_name:
614
- record = credential_resolver.resolve(cred_name)
615
- aws_access_key_id = aws_access_key_id or record.value
616
-
617
- return SesTransport(
618
- region=transport_config.get("region", "eu-west-3"),
619
- aws_access_key_id=aws_access_key_id,
620
- aws_secret_access_key=aws_secret_access_key,
621
- )
622
-
652
+ return _create_ses_transport(transport_config, credential_resolver)
623
653
  raise AlertConfigurationError(f"Unknown transport type '{transport_type}' for email channel '{name}'")
624
654
 
625
655
 
@@ -640,6 +670,7 @@ def _create_email_channel(
640
670
 
641
671
  Raises:
642
672
  AlertConfigurationError: If configuration is invalid.
673
+
643
674
  """
644
675
  from kstlib.alerts.channels import EmailChannel
645
676