kstlib 2.3.0__tar.gz → 2.4.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.3.0/src/kstlib.egg-info → kstlib-2.4.0}/PKG-INFO +1 -1
  2. {kstlib-2.3.0 → kstlib-2.4.0}/pyproject.toml +4 -1
  3. kstlib-2.4.0/src/kstlib/_shared/__init__.py +6 -0
  4. kstlib-2.4.0/src/kstlib/_shared/jinja.py +91 -0
  5. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/config/loader.py +71 -3
  6. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/limits.py +63 -45
  7. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/mail/__init__.py +2 -0
  8. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/mail/builder.py +235 -70
  9. kstlib-2.4.0/src/kstlib/mail/collector.py +304 -0
  10. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/meta.py +1 -1
  11. {kstlib-2.3.0 → kstlib-2.4.0/src/kstlib.egg-info}/PKG-INFO +1 -1
  12. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib.egg-info/SOURCES.txt +3 -0
  13. {kstlib-2.3.0 → kstlib-2.4.0}/LICENSE.md +0 -0
  14. {kstlib-2.3.0 → kstlib-2.4.0}/MANIFEST.in +0 -0
  15. {kstlib-2.3.0 → kstlib-2.4.0}/README.md +0 -0
  16. {kstlib-2.3.0 → kstlib-2.4.0}/setup.cfg +0 -0
  17. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/__init__.py +0 -0
  18. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/__main__.py +0 -0
  19. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/alerts/__init__.py +0 -0
  20. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/alerts/channels/__init__.py +0 -0
  21. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/alerts/channels/base.py +0 -0
  22. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/alerts/channels/email.py +0 -0
  23. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/alerts/channels/slack.py +0 -0
  24. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/alerts/exceptions.py +0 -0
  25. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/alerts/manager.py +0 -0
  26. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/alerts/models.py +0 -0
  27. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/alerts/throttle.py +0 -0
  28. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/auth/__init__.py +0 -0
  29. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/auth/callback.py +0 -0
  30. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/auth/check.py +0 -0
  31. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/auth/config.py +0 -0
  32. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/auth/errors.py +0 -0
  33. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/auth/models.py +0 -0
  34. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/auth/providers/__init__.py +0 -0
  35. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/auth/providers/base.py +0 -0
  36. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/auth/providers/oauth2.py +0 -0
  37. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/auth/providers/oidc.py +0 -0
  38. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/auth/session.py +0 -0
  39. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/auth/token.py +0 -0
  40. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cache/__init__.py +0 -0
  41. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cache/decorator.py +0 -0
  42. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cache/strategies.py +0 -0
  43. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/__init__.py +0 -0
  44. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/app.py +0 -0
  45. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/__init__.py +0 -0
  46. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/auth/__init__.py +0 -0
  47. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/auth/check.py +0 -0
  48. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/auth/common.py +0 -0
  49. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/auth/login.py +0 -0
  50. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/auth/logout.py +0 -0
  51. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/auth/providers.py +0 -0
  52. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/auth/status.py +0 -0
  53. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/auth/token.py +0 -0
  54. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/auth/whoami.py +0 -0
  55. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/config.py +0 -0
  56. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/ops/__init__.py +0 -0
  57. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/ops/attach.py +0 -0
  58. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/ops/common.py +0 -0
  59. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/ops/list_sessions.py +0 -0
  60. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/ops/logs.py +0 -0
  61. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/ops/start.py +0 -0
  62. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/ops/status.py +0 -0
  63. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/ops/stop.py +0 -0
  64. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/rapi/__init__.py +0 -0
  65. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/rapi/call.py +0 -0
  66. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/rapi/list.py +0 -0
  67. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/rapi/show.py +0 -0
  68. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/secrets/__init__.py +0 -0
  69. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/secrets/common.py +0 -0
  70. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/secrets/decrypt.py +0 -0
  71. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/secrets/doctor.py +0 -0
  72. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/secrets/encrypt.py +0 -0
  73. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/commands/secrets/shred.py +0 -0
  74. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/cli/common.py +0 -0
  75. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/config/__init__.py +0 -0
  76. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/config/exceptions.py +0 -0
  77. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/config/export.py +0 -0
  78. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/config/sops.py +0 -0
  79. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/db/__init__.py +0 -0
  80. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/db/aiosqlcipher.py +0 -0
  81. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/db/cipher.py +0 -0
  82. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/db/database.py +0 -0
  83. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/db/exceptions.py +0 -0
  84. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/db/pool.py +0 -0
  85. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/helpers/__init__.py +0 -0
  86. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/helpers/exceptions.py +0 -0
  87. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/helpers/time_trigger.py +0 -0
  88. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/kstlib.conf.yml +0 -0
  89. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/logging/__init__.py +0 -0
  90. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/logging/manager.py +0 -0
  91. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/mail/exceptions.py +0 -0
  92. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/mail/filesystem.py +0 -0
  93. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/mail/transport.py +0 -0
  94. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/mail/transports/__init__.py +0 -0
  95. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/mail/transports/gmail.py +0 -0
  96. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/mail/transports/resend.py +0 -0
  97. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/mail/transports/ses.py +0 -0
  98. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/mail/transports/smtp.py +0 -0
  99. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/metrics/__init__.py +0 -0
  100. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/metrics/decorators.py +0 -0
  101. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/metrics/exceptions.py +0 -0
  102. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/monitoring/__init__.py +0 -0
  103. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/monitoring/_styles.py +0 -0
  104. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/monitoring/cell.py +0 -0
  105. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/monitoring/config.py +0 -0
  106. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/monitoring/delivery.py +0 -0
  107. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/monitoring/exceptions.py +0 -0
  108. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/monitoring/image.py +0 -0
  109. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/monitoring/kv.py +0 -0
  110. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/monitoring/list.py +0 -0
  111. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/monitoring/metric.py +0 -0
  112. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/monitoring/monitoring.py +0 -0
  113. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/monitoring/renderer.py +0 -0
  114. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/monitoring/service.py +0 -0
  115. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/monitoring/table.py +0 -0
  116. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/monitoring/types.py +0 -0
  117. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/ops/__init__.py +0 -0
  118. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/ops/base.py +0 -0
  119. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/ops/container.py +0 -0
  120. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/ops/exceptions.py +0 -0
  121. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/ops/manager.py +0 -0
  122. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/ops/models.py +0 -0
  123. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/ops/tmux.py +0 -0
  124. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/ops/validators.py +0 -0
  125. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/pipeline/__init__.py +0 -0
  126. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/pipeline/base.py +0 -0
  127. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/pipeline/exceptions.py +0 -0
  128. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/pipeline/models.py +0 -0
  129. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/pipeline/runner.py +0 -0
  130. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/pipeline/steps/__init__.py +0 -0
  131. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/pipeline/steps/_base.py +0 -0
  132. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/pipeline/steps/callable.py +0 -0
  133. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/pipeline/steps/python.py +0 -0
  134. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/pipeline/steps/shell.py +0 -0
  135. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/pipeline/validators.py +0 -0
  136. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/py.typed +0 -0
  137. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/rapi/__init__.py +0 -0
  138. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/rapi/client.py +0 -0
  139. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/rapi/config.py +0 -0
  140. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/rapi/credentials.py +0 -0
  141. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/rapi/exceptions.py +0 -0
  142. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/resilience/__init__.py +0 -0
  143. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/resilience/circuit_breaker.py +0 -0
  144. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/resilience/exceptions.py +0 -0
  145. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/resilience/heartbeat.py +0 -0
  146. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/resilience/rate_limiter.py +0 -0
  147. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/resilience/shutdown.py +0 -0
  148. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/resilience/watchdog.py +0 -0
  149. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/secrets/__init__.py +0 -0
  150. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/secrets/exceptions.py +0 -0
  151. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/secrets/models.py +0 -0
  152. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/secrets/providers/__init__.py +0 -0
  153. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/secrets/providers/base.py +0 -0
  154. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/secrets/providers/environment.py +0 -0
  155. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/secrets/providers/keyring.py +0 -0
  156. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/secrets/providers/kms.py +0 -0
  157. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/secrets/providers/kwargs.py +0 -0
  158. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/secrets/providers/sops.py +0 -0
  159. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/secrets/resolver.py +0 -0
  160. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/secrets/sensitive.py +0 -0
  161. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/secure/__init__.py +0 -0
  162. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/secure/fs.py +0 -0
  163. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/secure/permissions.py +0 -0
  164. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/ssl.py +0 -0
  165. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/transform/__init__.py +0 -0
  166. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/transform/chain.py +0 -0
  167. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/transform/config.py +0 -0
  168. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/transform/exceptions.py +0 -0
  169. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/transform/primitives.py +0 -0
  170. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/transform/validators.py +0 -0
  171. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/ui/__init__.py +0 -0
  172. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/ui/exceptions.py +0 -0
  173. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/ui/panels.py +0 -0
  174. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/ui/spinner.py +0 -0
  175. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/ui/tables.py +0 -0
  176. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/utils/__init__.py +0 -0
  177. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/utils/dict.py +0 -0
  178. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/utils/formatting.py +0 -0
  179. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/utils/http_trace.py +0 -0
  180. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/utils/lazy.py +0 -0
  181. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/utils/secure_delete.py +0 -0
  182. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/utils/serialization.py +0 -0
  183. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/utils/text.py +0 -0
  184. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/utils/validators.py +0 -0
  185. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/websocket/__init__.py +0 -0
  186. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/websocket/exceptions.py +0 -0
  187. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/websocket/manager.py +0 -0
  188. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib/websocket/models.py +0 -0
  189. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib.egg-info/dependency_links.txt +0 -0
  190. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib.egg-info/entry_points.txt +0 -0
  191. {kstlib-2.3.0 → kstlib-2.4.0}/src/kstlib.egg-info/requires.txt +0 -0
  192. {kstlib-2.3.0 → kstlib-2.4.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.3.0
3
+ Version: 2.4.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>
@@ -368,7 +368,10 @@ ignore = [
368
368
  "src/kstlib/logging/__init__.py" = ["S110"]
369
369
  "src/kstlib/logging/manager.py" = ["S110"]
370
370
  # Mail builder - snapshot/notify path legitimately reassigns same-class private state
371
- "src/kstlib/mail/builder.py" = ["SLF001"]
371
+ # PLR0913: Many kwargs - notify decorator API exposes mode/collector/include_* options (parametrised decorator pattern, mirrors metrics/decorators.py)
372
+ "src/kstlib/mail/builder.py" = ["SLF001", "PLR0913"]
373
+ # Shared Jinja2 helper - autoescape default is intentionally False: kstlib mail templates are author-controlled (dev), not user input. Callers opt in to autoescape=True for HTML user-facing content.
374
+ "src/kstlib/_shared/jinja.py" = ["S701"]
372
375
  # Monitoring image - RenderError chain already raised via "from err"; no additional ignores needed here
373
376
  # Monitoring renderer - Markup values are escaped before wrapping; jinja2 Environment defaults to autoescape=True
374
377
  "src/kstlib/monitoring/renderer.py" = ["S701", "S704"]
@@ -0,0 +1,6 @@
1
+ """Shared cross-module helpers (private to kstlib internals).
2
+
3
+ Modules under ``kstlib._shared`` are imported via their full path
4
+ (e.g. ``from kstlib._shared.jinja import render_jinja``) rather than
5
+ re-exported here. They are not part of the public ``kstlib`` API.
6
+ """
@@ -0,0 +1,91 @@
1
+ """Shared Jinja2 rendering helpers for kstlib internal modules.
2
+
3
+ Provides a thin wrapper around :class:`jinja2.Environment` with kstlib's
4
+ default policy:
5
+
6
+ - :class:`jinja2.ChainableUndefined`: missing variables resolve to empty
7
+ string (and dotted access on them keeps returning empty), preserving
8
+ graceful behavior for templates with optional fields.
9
+ - ``keep_trailing_newline=True``: file templates retain their trailing
10
+ newline if any.
11
+ - ``autoescape=False`` by default: kstlib mail templates are authored by
12
+ the application developer, not by end users, so the caller controls
13
+ escaping. Pass ``autoescape=True`` for HTML/XML user-facing content.
14
+
15
+ These helpers live under ``kstlib._shared`` because they are reused by
16
+ several internal subpackages (``mail``, future migrations of
17
+ ``monitoring``). Import via the full path:
18
+
19
+ from kstlib._shared.jinja import render_jinja, render_jinja_file
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import TYPE_CHECKING, Any
25
+
26
+ import jinja2
27
+
28
+ if TYPE_CHECKING:
29
+ from collections.abc import Mapping
30
+ from pathlib import Path
31
+
32
+
33
+ def render_jinja(
34
+ source: str,
35
+ context: Mapping[str, Any],
36
+ *,
37
+ autoescape: bool = False,
38
+ ) -> str:
39
+ """Render a Jinja2 template string with the provided context.
40
+
41
+ Args:
42
+ source: Jinja2 template source as a string.
43
+ context: Mapping of variables exposed to the template.
44
+ autoescape: When True, autoescape HTML/XML special characters.
45
+ Default False; kstlib mail templates are author-controlled.
46
+
47
+ Returns:
48
+ The rendered template as a string.
49
+
50
+ Examples:
51
+ >>> from kstlib._shared.jinja import render_jinja
52
+ >>> render_jinja("Hello {{ name }}", {"name": "Ada"})
53
+ 'Hello Ada'
54
+ >>> render_jinja("{{ missing }}", {})
55
+ ''
56
+
57
+ """
58
+ env = jinja2.Environment(
59
+ autoescape=autoescape,
60
+ undefined=jinja2.ChainableUndefined,
61
+ keep_trailing_newline=True,
62
+ )
63
+ return env.from_string(source).render(**dict(context))
64
+
65
+
66
+ def render_jinja_file(
67
+ path: Path,
68
+ context: Mapping[str, Any],
69
+ *,
70
+ autoescape: bool = False,
71
+ encoding: str = "utf-8",
72
+ ) -> str:
73
+ """Render a Jinja2 template file with the provided context.
74
+
75
+ Reads the file at ``path`` with the requested encoding and renders
76
+ it via :func:`render_jinja`.
77
+
78
+ Args:
79
+ path: Path to the Jinja2 template file.
80
+ context: Mapping of variables exposed to the template.
81
+ autoescape: When True, autoescape HTML/XML special characters.
82
+ encoding: Text encoding used to read the template file.
83
+
84
+ Returns:
85
+ The rendered template as a string.
86
+
87
+ """
88
+ return render_jinja(path.read_text(encoding=encoding), context, autoescape=autoescape)
89
+
90
+
91
+ __all__ = ["render_jinja", "render_jinja_file"]
@@ -318,6 +318,76 @@ def _load_any_config_file(
318
318
  return data
319
319
 
320
320
 
321
+ def _normalize_includes(raw: Any, source_path: pathlib.Path) -> list[str]:
322
+ """Normalize the raw ``include`` YAML value to a clean list of paths.
323
+
324
+ Handles the common footgun where an author leaves ``include:`` with an
325
+ empty body (or comments out every entry) and gets a cryptic
326
+ ``TypeError: 'NoneType' object is not iterable`` at iteration time.
327
+ Normalization rules:
328
+
329
+ 1. Missing key, ``None``, ``[]``, empty string, or whitespace-only
330
+ string produces an empty list silently. Commenting out a key is a
331
+ legitimate editing pattern, not an error worth logging.
332
+ 2. A non-empty string is wrapped as a single-element list after
333
+ ``strip()``.
334
+ 3. A list whose individual items are ``None``, empty, or whitespace-only
335
+ has those entries dropped with a single ``WARNING`` log message
336
+ naming the source file, the drop count, and the original indices.
337
+ 4. Any other top-level type (``int``, ``dict``, ...) or any non-string
338
+ list item raises :exc:`ConfigFormatError`, consistent with the rest
339
+ of this loader.
340
+
341
+ Args:
342
+ raw: Raw value of the ``include`` key popped from the YAML mapping.
343
+ source_path: Path of the YAML file that declared the include, used
344
+ in log and error messages for actionable feedback.
345
+
346
+ Returns:
347
+ Cleaned list of include path strings. Never ``None`` and never
348
+ containing ``None`` or empty entries.
349
+
350
+ Raises:
351
+ ConfigFormatError: If ``raw`` is not ``None``, ``str``, or ``list``,
352
+ or if any list element is not a ``str``.
353
+
354
+ """
355
+ if raw is None:
356
+ return []
357
+ if isinstance(raw, str):
358
+ stripped = raw.strip()
359
+ return [stripped] if stripped else []
360
+ if not isinstance(raw, list):
361
+ raise ConfigFormatError(f"include must be string or list, got {type(raw).__name__} (file: {source_path})")
362
+
363
+ cleaned: list[str] = []
364
+ dropped: list[int] = []
365
+ for idx, item in enumerate(raw):
366
+ if item is None:
367
+ dropped.append(idx)
368
+ continue
369
+ if not isinstance(item, str):
370
+ raise ConfigFormatError(f"include[{idx}] must be string, got {type(item).__name__} (file: {source_path})")
371
+ stripped = item.strip()
372
+ if not stripped:
373
+ dropped.append(idx)
374
+ continue
375
+ cleaned.append(stripped)
376
+
377
+ if dropped:
378
+ # Lazy import: keep loader import graph shallow (see kstlib.limits
379
+ # circular import fix).
380
+ import logging
381
+
382
+ logging.getLogger(__name__).warning(
383
+ "include in %s: dropped %d empty entries at indices %s",
384
+ source_path,
385
+ len(dropped),
386
+ dropped,
387
+ )
388
+ return cleaned
389
+
390
+
321
391
  def _load_with_includes(
322
392
  path: pathlib.Path,
323
393
  loaded_paths: set[pathlib.Path] | None = None,
@@ -365,9 +435,7 @@ def _load_with_includes(
365
435
  loaded_paths.add(path)
366
436
 
367
437
  data = _load_any_config_file(path, encoding, sops_decrypt=sops_decrypt)
368
- includes = data.pop("include", [])
369
- if isinstance(includes, str):
370
- includes = [includes]
438
+ includes = _normalize_includes(data.pop("include", []), path)
371
439
 
372
440
  # Get real extension (handles .sops.yml -> .yml)
373
441
  parent_ext = get_real_extension(path) if is_sops_file(path) else path.suffix.lower()
@@ -16,52 +16,26 @@ Example:
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- from collections.abc import Mapping
20
- from dataclasses import dataclass
21
- from typing import Any
22
-
23
- from kstlib.utils.formatting import format_bytes, parse_size_string
24
-
25
- __all__ = [
26
- "HARD_MAX_CONFIG_FILE_SIZE",
27
- "HARD_MAX_DATETIME_FORMAT_LENGTH",
28
- "HARD_MAX_DISPLAY_VALUE_LENGTH",
29
- "HARD_MAX_ENDPOINT_REF_LENGTH",
30
- "HARD_MAX_EPOCH_TIMESTAMP",
31
- "HARD_MAX_PIPELINE_STEPS",
32
- "HARD_MAX_PIPELINE_TIMEOUT",
33
- "HARD_MAX_RAPI_UPLOAD_SIZE",
34
- "HARD_MAX_STEP_ARGS",
35
- "HARD_MAX_TIMEZONE_LENGTH",
36
- "HARD_MIN_EPOCH_TIMESTAMP",
37
- "HARD_MIN_PIPELINE_TIMEOUT",
38
- "AlertsLimits",
39
- "CacheLimits",
40
- "DatabaseLimits",
41
- "MailLimits",
42
- "PipelineLimits",
43
- "RapiLimits",
44
- "RapiRenderConfig",
45
- "ResilienceLimits",
46
- "SopsLimits",
47
- "WebSocketLimits",
48
- "clamp_with_limits",
49
- "get_alerts_limits",
50
- "get_cache_limits",
51
- "get_db_limits",
52
- "get_mail_limits",
53
- "get_pipeline_limits",
54
- "get_rapi_limits",
55
- "get_rapi_render_config",
56
- "get_resilience_limits",
57
- "get_sops_limits",
58
- "get_websocket_limits",
59
- ]
60
-
61
19
  # =============================================================================
62
- # Hard limits (deep defense - cannot be exceeded via config)
20
+ # Pre-import constants (hard limits + defaults)
21
+ #
22
+ # Defined BEFORE any cross-module import to break a latent circular import.
23
+ # During `kstlib.limits` initialisation, importing `kstlib.utils.formatting`
24
+ # transitively triggers `kstlib.config.__init__`, which re-enters
25
+ # `kstlib.limits` from two points:
26
+ # - `kstlib.config.loader` -> reads HARD_MAX_CONFIG_FILE_SIZE
27
+ # - `kstlib.config.sops` -> reads DEFAULT_MAX_SOPS_CACHE_ENTRIES and
28
+ # HARD_MAX_SOPS_CACHE_ENTRIES
29
+ # Both the HARD_* block below and the DEFAULT_* block that follows must
30
+ # therefore be defined before the cross-module import. If these constants
31
+ # sat at their usual position (after imports), Python would raise
32
+ # ImportError on the cycle re-entry into a still-initialising kstlib.limits.
33
+ #
34
+ # See: fix-circular-import-mail request (2026-04-24).
63
35
  # =============================================================================
64
36
 
37
+ # Hard limits (deep defense - cannot be exceeded via config)
38
+
65
39
  #: Absolute maximum attachment size (25 MiB) - protects against memory exhaustion.
66
40
  HARD_MAX_ATTACHMENT_SIZE = 25 * 1024 * 1024
67
41
 
@@ -240,9 +214,7 @@ HARD_MIN_EPOCH_TIMESTAMP = 0
240
214
  #: Maximum valid epoch timestamp (year 2100) - protects against overflow.
241
215
  HARD_MAX_EPOCH_TIMESTAMP = 4102444800
242
216
 
243
- # =============================================================================
244
217
  # Default limits (used when config is not available)
245
- # =============================================================================
246
218
 
247
219
  DEFAULT_MAX_ATTACHMENT_SIZE = 25 * 1024 * 1024 # 25 MiB
248
220
  DEFAULT_MAX_ATTACHMENTS = 20
@@ -291,6 +263,52 @@ DEFAULT_WS_DISCONNECT_ALERT_INTERVAL = 300.0 # seconds between throttled alerts
291
263
  DEFAULT_PIPELINE_TIMEOUT = 300.0 # seconds (5 minutes)
292
264
  DEFAULT_PIPELINE_ON_ERROR = "fail_fast"
293
265
 
266
+ # =============================================================================
267
+ # Cross-module imports (intentionally after pre-import constants, see note above)
268
+ # =============================================================================
269
+
270
+ from collections.abc import Mapping # noqa: E402
271
+ from dataclasses import dataclass # noqa: E402
272
+ from typing import Any # noqa: E402
273
+
274
+ from kstlib.utils.formatting import format_bytes, parse_size_string # noqa: E402
275
+
276
+ __all__ = [
277
+ "HARD_MAX_CONFIG_FILE_SIZE",
278
+ "HARD_MAX_DATETIME_FORMAT_LENGTH",
279
+ "HARD_MAX_DISPLAY_VALUE_LENGTH",
280
+ "HARD_MAX_ENDPOINT_REF_LENGTH",
281
+ "HARD_MAX_EPOCH_TIMESTAMP",
282
+ "HARD_MAX_PIPELINE_STEPS",
283
+ "HARD_MAX_PIPELINE_TIMEOUT",
284
+ "HARD_MAX_RAPI_UPLOAD_SIZE",
285
+ "HARD_MAX_STEP_ARGS",
286
+ "HARD_MAX_TIMEZONE_LENGTH",
287
+ "HARD_MIN_EPOCH_TIMESTAMP",
288
+ "HARD_MIN_PIPELINE_TIMEOUT",
289
+ "AlertsLimits",
290
+ "CacheLimits",
291
+ "DatabaseLimits",
292
+ "MailLimits",
293
+ "PipelineLimits",
294
+ "RapiLimits",
295
+ "RapiRenderConfig",
296
+ "ResilienceLimits",
297
+ "SopsLimits",
298
+ "WebSocketLimits",
299
+ "clamp_with_limits",
300
+ "get_alerts_limits",
301
+ "get_cache_limits",
302
+ "get_db_limits",
303
+ "get_mail_limits",
304
+ "get_pipeline_limits",
305
+ "get_rapi_limits",
306
+ "get_rapi_render_config",
307
+ "get_resilience_limits",
308
+ "get_sops_limits",
309
+ "get_websocket_limits",
310
+ ]
311
+
294
312
 
295
313
  @dataclass(frozen=True, slots=True)
296
314
  class MailLimits:
@@ -25,6 +25,7 @@ Examples:
25
25
  """
26
26
 
27
27
  from kstlib.mail.builder import MailBuilder, NotifyResult
28
+ from kstlib.mail.collector import NotifyCollector
28
29
  from kstlib.mail.exceptions import MailConfigurationError, MailError, MailTransportError, MailValidationError
29
30
  from kstlib.mail.filesystem import MailFilesystemGuards
30
31
  from kstlib.mail.transport import AsyncMailTransport, AsyncTransportWrapper, MailTransport
@@ -39,5 +40,6 @@ __all__ = [
39
40
  "MailTransport",
40
41
  "MailTransportError",
41
42
  "MailValidationError",
43
+ "NotifyCollector",
42
44
  "NotifyResult",
43
45
  ]