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.
- {kstlib-2.1.4/src/kstlib.egg-info → kstlib-2.2.0}/PKG-INFO +8 -4
- {kstlib-2.1.4 → kstlib-2.2.0}/pyproject.toml +50 -11
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/__init__.py +5 -12
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/__init__.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/channels/__init__.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/channels/base.py +8 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/channels/email.py +5 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/channels/slack.py +8 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/exceptions.py +4 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/manager.py +87 -56
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/models.py +5 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/alerts/throttle.py +7 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/__init__.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/callback.py +6 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/check.py +11 -1
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/config.py +12 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/models.py +10 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/providers/base.py +11 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/providers/oauth2.py +16 -3
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/providers/oidc.py +21 -6
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/session.py +4 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/token.py +10 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cache/__init__.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cache/decorator.py +3 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cache/strategies.py +12 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/app.py +3 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/check.py +15 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/common.py +2 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/login.py +4 -1
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/status.py +9 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/token.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/ops/attach.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/ops/common.py +60 -29
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/ops/list_sessions.py +4 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/ops/logs.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/ops/start.py +2 -1
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/ops/status.py +3 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/ops/stop.py +2 -1
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/rapi/__init__.py +35 -3
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/rapi/call.py +39 -2
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/rapi/list.py +10 -6
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/rapi/show.py +5 -6
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/secrets/common.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/secrets/doctor.py +12 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/config/exceptions.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/config/loader.py +66 -6
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/config/sops.py +9 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/db/__init__.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/db/aiosqlcipher.py +4 -1
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/db/cipher.py +2 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/db/database.py +9 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/db/pool.py +5 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/helpers/__init__.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/helpers/time_trigger.py +12 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/kstlib.conf.yml +53 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/limits.py +19 -0
- kstlib-2.2.0/src/kstlib/logging/__init__.py +169 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/logging/manager.py +134 -2
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/__init__.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/builder.py +238 -19
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/exceptions.py +3 -1
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/filesystem.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/transport.py +10 -1
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/transports/gmail.py +8 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/transports/resend.py +10 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/transports/ses.py +6 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/transports/smtp.py +10 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/meta.py +1 -1
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/metrics/__init__.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/metrics/decorators.py +12 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/metrics/exceptions.py +4 -1
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/__init__.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/_styles.py +2 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/cell.py +2 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/config.py +9 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/delivery.py +11 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/exceptions.py +2 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/image.py +7 -2
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/kv.py +2 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/list.py +2 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/metric.py +2 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/monitoring.py +8 -1
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/renderer.py +7 -3
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/service.py +11 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/table.py +3 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/monitoring/types.py +2 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ops/__init__.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ops/base.py +8 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ops/container.py +20 -2
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ops/exceptions.py +6 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ops/manager.py +12 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ops/models.py +5 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ops/tmux.py +18 -4
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ops/validators.py +7 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/pipeline/__init__.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/pipeline/base.py +2 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/pipeline/exceptions.py +8 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/pipeline/models.py +14 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/pipeline/runner.py +36 -8
- kstlib-2.2.0/src/kstlib/pipeline/steps/_base.py +118 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/pipeline/steps/callable.py +63 -1
- kstlib-2.2.0/src/kstlib/pipeline/steps/python.py +71 -0
- kstlib-2.2.0/src/kstlib/pipeline/steps/shell.py +74 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/pipeline/validators.py +15 -13
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/rapi/__init__.py +4 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/rapi/client.py +178 -26
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/rapi/config.py +399 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/rapi/credentials.py +71 -14
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/rapi/exceptions.py +60 -1
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/resilience/__init__.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/resilience/circuit_breaker.py +9 -3
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/resilience/exceptions.py +13 -5
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/resilience/heartbeat.py +11 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/resilience/rate_limiter.py +12 -2
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/resilience/shutdown.py +8 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/resilience/watchdog.py +11 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/models.py +2 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/providers/base.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/providers/environment.py +2 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/providers/keyring.py +5 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/providers/kms.py +4 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/providers/kwargs.py +6 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/resolver.py +6 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/sensitive.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secure/fs.py +8 -1
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secure/permissions.py +3 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ssl.py +10 -0
- kstlib-2.2.0/src/kstlib/transform/__init__.py +116 -0
- kstlib-2.2.0/src/kstlib/transform/chain.py +1095 -0
- kstlib-2.2.0/src/kstlib/transform/config.py +912 -0
- kstlib-2.2.0/src/kstlib/transform/exceptions.py +184 -0
- kstlib-2.2.0/src/kstlib/transform/primitives.py +720 -0
- kstlib-2.2.0/src/kstlib/transform/validators.py +382 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ui/exceptions.py +5 -3
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ui/panels.py +4 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ui/spinner.py +7 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ui/tables.py +2 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/dict.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/formatting.py +9 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/http_trace.py +38 -7
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/lazy.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/secure_delete.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/serialization.py +10 -3
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/validators.py +35 -1
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/websocket/__init__.py +1 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/websocket/exceptions.py +13 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/websocket/manager.py +19 -1
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/websocket/models.py +16 -0
- {kstlib-2.1.4 → kstlib-2.2.0/src/kstlib.egg-info}/PKG-INFO +8 -4
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib.egg-info/SOURCES.txt +7 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib.egg-info/requires.txt +7 -3
- kstlib-2.1.4/src/kstlib/logging/__init__.py +0 -126
- kstlib-2.1.4/src/kstlib/pipeline/steps/python.py +0 -136
- kstlib-2.1.4/src/kstlib/pipeline/steps/shell.py +0 -142
- {kstlib-2.1.4 → kstlib-2.2.0}/LICENSE.md +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/MANIFEST.in +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/README.md +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/setup.cfg +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/__main__.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/errors.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/auth/providers/__init__.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/__init__.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/__init__.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/__init__.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/logout.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/providers.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/auth/whoami.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/config.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/ops/__init__.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/secrets/__init__.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/secrets/decrypt.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/secrets/encrypt.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/commands/secrets/shred.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/cli/common.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/config/__init__.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/config/export.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/db/exceptions.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/helpers/exceptions.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/mail/transports/__init__.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/pipeline/steps/__init__.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/py.typed +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/__init__.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/exceptions.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/providers/__init__.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secrets/providers/sops.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/secure/__init__.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/ui/__init__.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/__init__.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib/utils/text.py +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib.egg-info/dependency_links.txt +0 -0
- {kstlib-2.1.4 → kstlib-2.2.0}/src/kstlib.egg-info/entry_points.txt +0 -0
- {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.
|
|
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.
|
|
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
|
|
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
|
|
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",
|
|
36
|
-
"tomli>=2.3,<3",
|
|
37
|
-
"tomli-w>=1.0,<2",
|
|
38
|
-
"python-box>=7.3,<8",
|
|
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.
|
|
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>=
|
|
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",
|
|
103
|
-
"types-PyYAML>=6.0,<7",
|
|
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>=
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|