kstlib 2.2.1__tar.gz → 2.3.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.1/src/kstlib.egg-info → kstlib-2.3.1}/PKG-INFO +1 -1
  2. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/__init__.py +5 -0
  3. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/config/__init__.py +2 -0
  4. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/config/loader.py +113 -3
  5. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/kstlib.conf.yml +23 -1
  6. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/limits.py +63 -45
  7. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/mail/builder.py +362 -6
  8. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/meta.py +1 -1
  9. {kstlib-2.2.1 → kstlib-2.3.1/src/kstlib.egg-info}/PKG-INFO +1 -1
  10. {kstlib-2.2.1 → kstlib-2.3.1}/LICENSE.md +0 -0
  11. {kstlib-2.2.1 → kstlib-2.3.1}/MANIFEST.in +0 -0
  12. {kstlib-2.2.1 → kstlib-2.3.1}/README.md +0 -0
  13. {kstlib-2.2.1 → kstlib-2.3.1}/pyproject.toml +0 -0
  14. {kstlib-2.2.1 → kstlib-2.3.1}/setup.cfg +0 -0
  15. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/__main__.py +0 -0
  16. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/alerts/__init__.py +0 -0
  17. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/alerts/channels/__init__.py +0 -0
  18. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/alerts/channels/base.py +0 -0
  19. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/alerts/channels/email.py +0 -0
  20. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/alerts/channels/slack.py +0 -0
  21. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/alerts/exceptions.py +0 -0
  22. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/alerts/manager.py +0 -0
  23. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/alerts/models.py +0 -0
  24. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/alerts/throttle.py +0 -0
  25. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/auth/__init__.py +0 -0
  26. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/auth/callback.py +0 -0
  27. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/auth/check.py +0 -0
  28. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/auth/config.py +0 -0
  29. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/auth/errors.py +0 -0
  30. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/auth/models.py +0 -0
  31. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/auth/providers/__init__.py +0 -0
  32. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/auth/providers/base.py +0 -0
  33. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/auth/providers/oauth2.py +0 -0
  34. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/auth/providers/oidc.py +0 -0
  35. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/auth/session.py +0 -0
  36. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/auth/token.py +0 -0
  37. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cache/__init__.py +0 -0
  38. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cache/decorator.py +0 -0
  39. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cache/strategies.py +0 -0
  40. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/__init__.py +0 -0
  41. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/app.py +0 -0
  42. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/__init__.py +0 -0
  43. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/auth/__init__.py +0 -0
  44. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/auth/check.py +0 -0
  45. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/auth/common.py +0 -0
  46. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/auth/login.py +0 -0
  47. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/auth/logout.py +0 -0
  48. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/auth/providers.py +0 -0
  49. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/auth/status.py +0 -0
  50. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/auth/token.py +0 -0
  51. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/auth/whoami.py +0 -0
  52. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/config.py +0 -0
  53. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/ops/__init__.py +0 -0
  54. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/ops/attach.py +0 -0
  55. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/ops/common.py +0 -0
  56. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/ops/list_sessions.py +0 -0
  57. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/ops/logs.py +0 -0
  58. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/ops/start.py +0 -0
  59. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/ops/status.py +0 -0
  60. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/ops/stop.py +0 -0
  61. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/rapi/__init__.py +0 -0
  62. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/rapi/call.py +0 -0
  63. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/rapi/list.py +0 -0
  64. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/rapi/show.py +0 -0
  65. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/secrets/__init__.py +0 -0
  66. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/secrets/common.py +0 -0
  67. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/secrets/decrypt.py +0 -0
  68. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/secrets/doctor.py +0 -0
  69. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/secrets/encrypt.py +0 -0
  70. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/commands/secrets/shred.py +0 -0
  71. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/cli/common.py +0 -0
  72. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/config/exceptions.py +0 -0
  73. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/config/export.py +0 -0
  74. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/config/sops.py +0 -0
  75. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/db/__init__.py +0 -0
  76. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/db/aiosqlcipher.py +0 -0
  77. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/db/cipher.py +0 -0
  78. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/db/database.py +0 -0
  79. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/db/exceptions.py +0 -0
  80. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/db/pool.py +0 -0
  81. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/helpers/__init__.py +0 -0
  82. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/helpers/exceptions.py +0 -0
  83. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/helpers/time_trigger.py +0 -0
  84. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/logging/__init__.py +0 -0
  85. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/logging/manager.py +0 -0
  86. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/mail/__init__.py +0 -0
  87. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/mail/exceptions.py +0 -0
  88. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/mail/filesystem.py +0 -0
  89. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/mail/transport.py +0 -0
  90. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/mail/transports/__init__.py +0 -0
  91. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/mail/transports/gmail.py +0 -0
  92. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/mail/transports/resend.py +0 -0
  93. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/mail/transports/ses.py +0 -0
  94. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/mail/transports/smtp.py +0 -0
  95. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/metrics/__init__.py +0 -0
  96. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/metrics/decorators.py +0 -0
  97. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/metrics/exceptions.py +0 -0
  98. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/monitoring/__init__.py +0 -0
  99. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/monitoring/_styles.py +0 -0
  100. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/monitoring/cell.py +0 -0
  101. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/monitoring/config.py +0 -0
  102. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/monitoring/delivery.py +0 -0
  103. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/monitoring/exceptions.py +0 -0
  104. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/monitoring/image.py +0 -0
  105. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/monitoring/kv.py +0 -0
  106. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/monitoring/list.py +0 -0
  107. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/monitoring/metric.py +0 -0
  108. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/monitoring/monitoring.py +0 -0
  109. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/monitoring/renderer.py +0 -0
  110. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/monitoring/service.py +0 -0
  111. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/monitoring/table.py +0 -0
  112. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/monitoring/types.py +0 -0
  113. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/ops/__init__.py +0 -0
  114. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/ops/base.py +0 -0
  115. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/ops/container.py +0 -0
  116. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/ops/exceptions.py +0 -0
  117. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/ops/manager.py +0 -0
  118. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/ops/models.py +0 -0
  119. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/ops/tmux.py +0 -0
  120. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/ops/validators.py +0 -0
  121. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/pipeline/__init__.py +0 -0
  122. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/pipeline/base.py +0 -0
  123. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/pipeline/exceptions.py +0 -0
  124. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/pipeline/models.py +0 -0
  125. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/pipeline/runner.py +0 -0
  126. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/pipeline/steps/__init__.py +0 -0
  127. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/pipeline/steps/_base.py +0 -0
  128. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/pipeline/steps/callable.py +0 -0
  129. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/pipeline/steps/python.py +0 -0
  130. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/pipeline/steps/shell.py +0 -0
  131. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/pipeline/validators.py +0 -0
  132. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/py.typed +0 -0
  133. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/rapi/__init__.py +0 -0
  134. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/rapi/client.py +0 -0
  135. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/rapi/config.py +0 -0
  136. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/rapi/credentials.py +0 -0
  137. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/rapi/exceptions.py +0 -0
  138. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/resilience/__init__.py +0 -0
  139. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/resilience/circuit_breaker.py +0 -0
  140. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/resilience/exceptions.py +0 -0
  141. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/resilience/heartbeat.py +0 -0
  142. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/resilience/rate_limiter.py +0 -0
  143. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/resilience/shutdown.py +0 -0
  144. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/resilience/watchdog.py +0 -0
  145. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/secrets/__init__.py +0 -0
  146. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/secrets/exceptions.py +0 -0
  147. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/secrets/models.py +0 -0
  148. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/secrets/providers/__init__.py +0 -0
  149. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/secrets/providers/base.py +0 -0
  150. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/secrets/providers/environment.py +0 -0
  151. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/secrets/providers/keyring.py +0 -0
  152. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/secrets/providers/kms.py +0 -0
  153. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/secrets/providers/kwargs.py +0 -0
  154. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/secrets/providers/sops.py +0 -0
  155. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/secrets/resolver.py +0 -0
  156. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/secrets/sensitive.py +0 -0
  157. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/secure/__init__.py +0 -0
  158. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/secure/fs.py +0 -0
  159. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/secure/permissions.py +0 -0
  160. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/ssl.py +0 -0
  161. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/transform/__init__.py +0 -0
  162. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/transform/chain.py +0 -0
  163. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/transform/config.py +0 -0
  164. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/transform/exceptions.py +0 -0
  165. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/transform/primitives.py +0 -0
  166. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/transform/validators.py +0 -0
  167. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/ui/__init__.py +0 -0
  168. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/ui/exceptions.py +0 -0
  169. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/ui/panels.py +0 -0
  170. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/ui/spinner.py +0 -0
  171. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/ui/tables.py +0 -0
  172. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/utils/__init__.py +0 -0
  173. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/utils/dict.py +0 -0
  174. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/utils/formatting.py +0 -0
  175. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/utils/http_trace.py +0 -0
  176. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/utils/lazy.py +0 -0
  177. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/utils/secure_delete.py +0 -0
  178. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/utils/serialization.py +0 -0
  179. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/utils/text.py +0 -0
  180. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/utils/validators.py +0 -0
  181. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/websocket/__init__.py +0 -0
  182. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/websocket/exceptions.py +0 -0
  183. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/websocket/manager.py +0 -0
  184. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib/websocket/models.py +0 -0
  185. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib.egg-info/SOURCES.txt +0 -0
  186. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib.egg-info/dependency_links.txt +0 -0
  187. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib.egg-info/entry_points.txt +0 -0
  188. {kstlib-2.2.1 → kstlib-2.3.1}/src/kstlib.egg-info/requires.txt +0 -0
  189. {kstlib-2.2.1 → kstlib-2.3.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.1
3
+ Version: 2.3.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>
@@ -69,6 +69,7 @@ __all__ = [
69
69
  "ops",
70
70
  "pipeline",
71
71
  "rapi",
72
+ "reload_config",
72
73
  "require_config",
73
74
  "resilience",
74
75
  "secrets",
@@ -117,6 +118,7 @@ _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
117
118
  "load_config": ("kstlib.config.loader", "load_config"),
118
119
  "load_from_env": ("kstlib.config.loader", "load_from_env"),
119
120
  "load_from_file": ("kstlib.config.loader", "load_from_file"),
121
+ "reload_config": ("kstlib.config.loader", "reload_config"),
120
122
  "require_config": ("kstlib.config.loader", "require_config"),
121
123
  # Logging
122
124
  "LogManager": ("kstlib.logging", "LogManager"),
@@ -250,6 +252,9 @@ if TYPE_CHECKING:
250
252
  from kstlib.config.loader import (
251
253
  load_from_file as load_from_file,
252
254
  )
255
+ from kstlib.config.loader import (
256
+ reload_config as reload_config,
257
+ )
253
258
  from kstlib.config.loader import (
254
259
  require_config as require_config,
255
260
  )
@@ -34,6 +34,7 @@ from kstlib.config.loader import (
34
34
  load_config,
35
35
  load_from_env,
36
36
  load_from_file,
37
+ reload_config,
37
38
  require_config,
38
39
  )
39
40
  from kstlib.config.sops import (
@@ -71,6 +72,7 @@ __all__ = [
71
72
  "load_config",
72
73
  "load_from_env",
73
74
  "load_from_file",
75
+ "reload_config",
74
76
  "require_config",
75
77
  "reset_decryptor",
76
78
  ]
@@ -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()
@@ -1064,6 +1132,47 @@ def clear_config() -> None:
1064
1132
  _default_loader.cache = None
1065
1133
 
1066
1134
 
1135
+ def reload_config(filename: str = CONFIG_FILENAME) -> Box:
1136
+ """Force-reload the singleton configuration from disk.
1137
+
1138
+ Equivalent to ``clear_config()`` followed by ``get_config()``, but explicit
1139
+ and discoverable in a single import. Designed for interactive sessions
1140
+ (Jupyter, REPL) where the underlying YAML files have been edited and the
1141
+ cached config needs to be refreshed without restarting the kernel.
1142
+
1143
+ Args:
1144
+ filename: Config filename to search for. Defaults to
1145
+ ``kstlib.conf.yml``.
1146
+
1147
+ Returns:
1148
+ Freshly loaded ``Box`` configuration object. The singleton cache is
1149
+ updated in-place, so subsequent ``get_config()`` calls return the same
1150
+ fresh object.
1151
+
1152
+ Raises:
1153
+ ConfigFileNotFoundError: If no configuration file is found in any
1154
+ search location.
1155
+
1156
+ Note:
1157
+ When to use which:
1158
+
1159
+ - ``reload_config()``: explicit one-shot refresh (Jupyter/REPL).
1160
+ - ``get_config(force_reload=True)``: same effect, but the intent is
1161
+ hidden in a kwarg.
1162
+ - ``clear_config()``: only flushes the cache; the next
1163
+ ``get_config()`` call triggers the actual reload. Useful in tests
1164
+ that want to isolate the cache boundary.
1165
+
1166
+ Examples:
1167
+ >>> from kstlib.config import reload_config
1168
+ >>> cfg = reload_config() # doctest: +SKIP
1169
+ >>> cfg.mail.default # doctest: +SKIP
1170
+ 'corporate'
1171
+
1172
+ """
1173
+ return get_config(filename=filename, force_reload=True)
1174
+
1175
+
1067
1176
  __all__ = [
1068
1177
  "AutoDiscoveryConfig",
1069
1178
  "ConfigLoader",
@@ -1072,5 +1181,6 @@ __all__ = [
1072
1181
  "load_config",
1073
1182
  "load_from_env",
1074
1183
  "load_from_file",
1184
+ "reload_config",
1075
1185
  "require_config",
1076
1186
  ]
@@ -358,6 +358,21 @@ mail:
358
358
  # Leave null to force callers to pass transport= or preset= explicitly.
359
359
  default: null
360
360
 
361
+ # SSL/TLS configuration for mail transports.
362
+ #
363
+ # Values here override the root ``ssl:`` section (bottom of this file)
364
+ # for mail only, and are themselves overridden by ``ssl_verify`` and
365
+ # ``ssl_ca_bundle`` keys set inside an individual preset. Each key
366
+ # cascades independently: you can set ``verify: false`` here and still
367
+ # provide ``ssl_ca_bundle`` at the preset level.
368
+ #
369
+ # Security: setting ``verify: false`` at any level emits a WARNING log
370
+ # at transport build time. Prefer ``ca_bundle: /path/to/private-ca.pem``
371
+ # for internal PKI rather than disabling verification outright.
372
+ ssl:
373
+ verify: true
374
+ ca_bundle: null
375
+
361
376
  # Named transport presets. Each preset declares a "transport" field
362
377
  # (smtp or resend) and backend-specific parameters. Define your own
363
378
  # presets here and reference them via MailBuilder(preset="name").
@@ -368,11 +383,18 @@ mail:
368
383
  # transport: smtp
369
384
  # host: smtp-secure.corp.local
370
385
  # port: 25
371
- # login: svc_user
386
+ # login: svc_mail
372
387
  # password: "secret"
373
388
  # starttls: false
374
389
  # ssl: false
375
390
  # timeout: 30
391
+ # # Optional SSL overrides for this preset (highest priority in the cascade):
392
+ # ssl_verify: false
393
+ # ssl_ca_bundle: /etc/ssl/certs/corp-ca.pem
394
+ # # Optional envelope defaults (sender / reply_to only, never to/cc/bcc):
395
+ # defaults:
396
+ # sender: "Service Notifications <notify@corp.local>"
397
+ # reply_to: "Service Notifications <notify@corp.local>"
376
398
  #
377
399
  # transactional:
378
400
  # transport: resend
@@ -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: