kstlib 3.2.0__tar.gz → 3.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.
- {kstlib-3.2.0/src/kstlib.egg-info → kstlib-3.3.1}/PKG-INFO +1 -1
- kstlib-3.3.1/src/kstlib/_shared/logging_helpers.py +40 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cache/strategies.py +4 -10
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/app.py +3 -3
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/config.py +4 -4
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/rapi/__init__.py +2 -2
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/rapi/call.py +104 -84
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/rapi/list.py +3 -3
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/rapi/show.py +6 -6
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/common.py +12 -4
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/config/sops.py +3 -13
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/db/database.py +2 -8
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/kstlib.conf.yml +4 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/logging/manager.py +11 -2
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/meta.py +1 -1
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/rapi/__init__.py +2 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/rapi/client.py +59 -57
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/rapi/config.py +24 -22
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/rapi/exceptions.py +27 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/transform/chain.py +5 -9
- {kstlib-3.2.0 → kstlib-3.3.1/src/kstlib.egg-info}/PKG-INFO +1 -1
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib.egg-info/SOURCES.txt +1 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/LICENSE.md +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/MANIFEST.in +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/README.md +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/pyproject.toml +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/setup.cfg +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/__main__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/_shared/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/_shared/jinja.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/_shared/redaction.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/alerts/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/alerts/channels/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/alerts/channels/base.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/alerts/channels/email.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/alerts/channels/slack.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/alerts/exceptions.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/alerts/manager.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/alerts/models.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/alerts/throttle.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/auth/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/auth/callback.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/auth/check.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/auth/config.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/auth/errors.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/auth/models.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/auth/providers/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/auth/providers/base.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/auth/providers/oauth2.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/auth/providers/oidc.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/auth/session.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/auth/token.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cache/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cache/decorator.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/auth/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/auth/check.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/auth/common.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/auth/login.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/auth/logout.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/auth/providers.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/auth/status.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/auth/token.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/auth/whoami.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/ops/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/ops/attach.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/ops/common.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/ops/list_sessions.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/ops/logs.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/ops/start.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/ops/status.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/ops/stop.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/secrets/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/secrets/common.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/secrets/decrypt.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/secrets/doctor.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/secrets/encrypt.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/cli/commands/secrets/shred.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/config/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/config/exceptions.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/config/export.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/config/loader.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/db/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/db/aiosqlcipher.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/db/cipher.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/db/exceptions.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/db/pool.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/helpers/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/helpers/exceptions.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/helpers/time_trigger.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/limits.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/logging/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/mail/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/mail/_helpers.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/mail/builder.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/mail/collector.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/mail/exceptions.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/mail/filesystem.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/mail/throttle.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/mail/transport.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/mail/transports/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/mail/transports/gmail.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/mail/transports/resend.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/mail/transports/ses.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/mail/transports/smtp.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/metrics/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/metrics/decorators.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/metrics/exceptions.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/monitoring/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/monitoring/_styles.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/monitoring/cell.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/monitoring/config.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/monitoring/delivery.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/monitoring/exceptions.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/monitoring/image.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/monitoring/kv.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/monitoring/list.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/monitoring/metric.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/monitoring/monitoring.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/monitoring/renderer.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/monitoring/service.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/monitoring/table.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/monitoring/types.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/ops/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/ops/base.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/ops/container.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/ops/exceptions.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/ops/manager.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/ops/models.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/ops/tmux.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/ops/validators.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/pipeline/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/pipeline/base.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/pipeline/exceptions.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/pipeline/models.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/pipeline/runner.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/pipeline/steps/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/pipeline/steps/_base.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/pipeline/steps/_helpers.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/pipeline/steps/callable.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/pipeline/steps/python.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/pipeline/steps/shell.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/pipeline/validators.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/py.typed +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/rapi/credentials.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/resilience/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/resilience/circuit_breaker.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/resilience/exceptions.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/resilience/heartbeat.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/resilience/rate_limiter.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/resilience/shutdown.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/resilience/watchdog.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/secrets/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/secrets/exceptions.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/secrets/models.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/secrets/providers/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/secrets/providers/base.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/secrets/providers/environment.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/secrets/providers/keyring.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/secrets/providers/kms.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/secrets/providers/kwargs.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/secrets/providers/sops.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/secrets/resolver.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/secrets/sensitive.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/secure/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/secure/fs.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/secure/passwords.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/secure/permissions.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/ssl.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/transform/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/transform/config.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/transform/exceptions.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/transform/primitives.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/transform/validators.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/ui/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/ui/exceptions.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/ui/panels.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/ui/spinner.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/ui/tables.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/utils/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/utils/dict.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/utils/formatting.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/utils/http_trace.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/utils/lazy.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/utils/secure_delete.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/utils/serialization.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/utils/text.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/utils/validators.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/websocket/__init__.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/websocket/exceptions.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/websocket/manager.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib/websocket/models.py +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib.egg-info/dependency_links.txt +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib.egg-info/entry_points.txt +0 -0
- {kstlib-3.2.0 → kstlib-3.3.1}/src/kstlib.egg-info/requires.txt +0 -0
- {kstlib-3.2.0 → kstlib-3.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: 3.
|
|
3
|
+
Version: 3.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>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Shared TRACE-level logging helper (private to kstlib internals).
|
|
2
|
+
|
|
3
|
+
Consolidates the ``_log_trace`` helper that was previously duplicated across
|
|
4
|
+
several sub-packages into a single shared emitter. Modules pass their own
|
|
5
|
+
logger so emission stays attributed to the calling module's name.
|
|
6
|
+
|
|
7
|
+
This module must never import ``kstlib.logging`` at top level: see
|
|
8
|
+
:func:`log_trace` for the circular-import rationale.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
__all__ = ["log_trace"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def log_trace(logger: logging.Logger, msg: str, *args: object) -> None:
|
|
22
|
+
"""Emit a TRACE-level (custom level 5) record on the given logger.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
logger: Target standard-library logger to emit the record on.
|
|
26
|
+
msg: Log message, optionally containing ``%``-style placeholders.
|
|
27
|
+
*args: Positional arguments interpolated into ``msg`` lazily by the
|
|
28
|
+
logging framework (only when the record is actually emitted).
|
|
29
|
+
|
|
30
|
+
Note:
|
|
31
|
+
``TRACE_LEVEL`` is imported lazily inside the function to avoid the
|
|
32
|
+
circular import chain
|
|
33
|
+
``kstlib.logging.manager -> kstlib.config -> kstlib.config.sops``.
|
|
34
|
+
Importing ``kstlib.logging`` at module top level would resurrect that
|
|
35
|
+
cycle as soon as a cascade module (e.g. ``kstlib.config.sops``) imports
|
|
36
|
+
this helper.
|
|
37
|
+
"""
|
|
38
|
+
from kstlib.logging import TRACE_LEVEL
|
|
39
|
+
|
|
40
|
+
logger.log(TRACE_LEVEL, msg, *args)
|
|
@@ -29,19 +29,13 @@ from collections.abc import Callable
|
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
from typing import Any, TypeVar, cast
|
|
31
31
|
|
|
32
|
+
from kstlib._shared.logging_helpers import log_trace
|
|
32
33
|
from kstlib.limits import CacheLimits, get_cache_limits
|
|
33
34
|
from kstlib.utils.formatting import format_bytes
|
|
34
35
|
|
|
35
36
|
logger = logging.getLogger(__name__)
|
|
36
37
|
|
|
37
38
|
|
|
38
|
-
def _log_trace(msg: str, *args: object) -> None:
|
|
39
|
-
"""Log at TRACE level (custom level 5, below DEBUG)."""
|
|
40
|
-
from kstlib.logging import TRACE_LEVEL
|
|
41
|
-
|
|
42
|
-
logger.log(TRACE_LEVEL, msg, *args)
|
|
43
|
-
|
|
44
|
-
|
|
45
39
|
_CACHE_FORMAT_VERSION = "kstlib:file-cache:v1"
|
|
46
40
|
_SUPPORTED_SERIALIZERS: set[str] = {"json", "pickle", "auto"}
|
|
47
41
|
_PICKLE_SAFE_BUILTINS: set[str] = {
|
|
@@ -190,7 +184,7 @@ class TTLCacheStrategy(CacheStrategy):
|
|
|
190
184
|
if key not in self._cache:
|
|
191
185
|
# The key is the SHA256 hash from make_key; logging it is safe
|
|
192
186
|
# by construction (never the function args themselves).
|
|
193
|
-
|
|
187
|
+
log_trace(logger, "[CACHE] TTL miss: key=%s", key)
|
|
194
188
|
return None
|
|
195
189
|
|
|
196
190
|
value, expiry = self._cache[key]
|
|
@@ -198,10 +192,10 @@ class TTLCacheStrategy(CacheStrategy):
|
|
|
198
192
|
# Check expiration
|
|
199
193
|
if time.time() > expiry:
|
|
200
194
|
del self._cache[key]
|
|
201
|
-
|
|
195
|
+
log_trace(logger, "[CACHE] TTL miss (expired): key=%s", key)
|
|
202
196
|
return None
|
|
203
197
|
|
|
204
|
-
|
|
198
|
+
log_trace(logger, "[CACHE] TTL hit: key=%s", key)
|
|
205
199
|
return value
|
|
206
200
|
|
|
207
201
|
def set(self, key: str, value: Any) -> None:
|
|
@@ -24,7 +24,7 @@ from kstlib.cli.commands.ops import register_cli as register_ops_cli
|
|
|
24
24
|
from kstlib.cli.commands.rapi import register_cli as register_rapi_cli
|
|
25
25
|
from kstlib.cli.commands.secrets import register_cli as register_secrets_cli
|
|
26
26
|
from kstlib.cli.commands.secrets import shred as secrets_shred
|
|
27
|
-
from kstlib.cli.common import console
|
|
27
|
+
from kstlib.cli.common import console, err_console
|
|
28
28
|
from kstlib.logging import LogManager, get_logger, init_logging
|
|
29
29
|
|
|
30
30
|
app = typer.Typer(add_completion=False, name=meta.__app_name__)
|
|
@@ -223,8 +223,8 @@ def main( # pylint: disable=unused-argument
|
|
|
223
223
|
# Explicit --log-level takes precedence
|
|
224
224
|
level = log_level.upper()
|
|
225
225
|
if level not in LOG_LEVELS:
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
err_console.print(f"[red]Invalid log level: {log_level}[/]")
|
|
227
|
+
err_console.print(f"[dim]Valid levels: {', '.join(LOG_LEVELS)}[/]")
|
|
228
228
|
raise typer.Exit(1)
|
|
229
229
|
elif verbose > 0:
|
|
230
230
|
# -v/-vv/-vvv flags
|
|
@@ -8,7 +8,7 @@ from typing import Annotated
|
|
|
8
8
|
import typer
|
|
9
9
|
from rich.panel import Panel
|
|
10
10
|
|
|
11
|
-
from kstlib.cli.common import console
|
|
11
|
+
from kstlib.cli.common import console, err_console
|
|
12
12
|
from kstlib.config.export import (
|
|
13
13
|
ConfigExportError,
|
|
14
14
|
ConfigExportOptions,
|
|
@@ -59,18 +59,18 @@ def export_command(
|
|
|
59
59
|
try:
|
|
60
60
|
result = export_configuration(options)
|
|
61
61
|
except ConfigExportError as exc:
|
|
62
|
-
|
|
62
|
+
err_console.print(f"[bold red]{exc}[/bold red]")
|
|
63
63
|
raise typer.Exit(code=1) from exc
|
|
64
64
|
|
|
65
65
|
if stdout:
|
|
66
66
|
if result.content is None:
|
|
67
|
-
|
|
67
|
+
err_console.print("[bold red]Export failed: empty content.[/bold red]")
|
|
68
68
|
raise typer.Exit(code=1)
|
|
69
69
|
console.print(result.content)
|
|
70
70
|
return
|
|
71
71
|
|
|
72
72
|
if result.destination is None:
|
|
73
|
-
|
|
73
|
+
err_console.print("[bold red]Export failed: missing destination file.[/bold red]")
|
|
74
74
|
raise typer.Exit(code=1)
|
|
75
75
|
console.print(
|
|
76
76
|
Panel.fit(
|
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
import typer
|
|
8
8
|
from typer.core import TyperGroup
|
|
9
9
|
|
|
10
|
-
from kstlib.cli.common import
|
|
10
|
+
from kstlib.cli.common import err_console
|
|
11
11
|
from kstlib.rapi import load_rapi_config
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
@@ -35,7 +35,7 @@ def _load_config_or_exit() -> RapiConfigManager:
|
|
|
35
35
|
try:
|
|
36
36
|
return load_rapi_config()
|
|
37
37
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
38
|
-
|
|
38
|
+
err_console.print(f"[red]Failed to load rapi config: {exc}[/]")
|
|
39
39
|
raise typer.Exit(code=1) from exc
|
|
40
40
|
|
|
41
41
|
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import re
|
|
6
7
|
from typing import TYPE_CHECKING, Annotated, Any, cast
|
|
7
8
|
|
|
9
|
+
import httpx
|
|
8
10
|
import typer
|
|
9
11
|
from typer.core import TyperCommand
|
|
10
12
|
|
|
@@ -15,7 +17,9 @@ from kstlib.rapi import (
|
|
|
15
17
|
CredentialError,
|
|
16
18
|
EndpointAmbiguousError,
|
|
17
19
|
EndpointNotFoundError,
|
|
20
|
+
PathParameterError,
|
|
18
21
|
RapiClient,
|
|
22
|
+
RapiError,
|
|
19
23
|
RapiResponse,
|
|
20
24
|
RequestError,
|
|
21
25
|
ResponseTooLargeError,
|
|
@@ -530,19 +534,44 @@ def _emit_extracted_value(
|
|
|
530
534
|
print(content)
|
|
531
535
|
|
|
532
536
|
|
|
533
|
-
def
|
|
537
|
+
def _split_extracted_keys(spec: str) -> list[str]:
|
|
538
|
+
"""Split a ``--show-extracted`` spec on commas/whitespace, dropping empties.
|
|
539
|
+
|
|
540
|
+
Examples:
|
|
541
|
+
>>> _split_extracted_keys("v1,v2")
|
|
542
|
+
['v1', 'v2']
|
|
543
|
+
>>> _split_extracted_keys("v1 v2")
|
|
544
|
+
['v1', 'v2']
|
|
545
|
+
>>> _split_extracted_keys("v1, ,v2")
|
|
546
|
+
['v1', 'v2']
|
|
547
|
+
>>> _split_extracted_keys("")
|
|
548
|
+
[]
|
|
549
|
+
|
|
550
|
+
"""
|
|
551
|
+
return [key for key in re.split(r"[,\s]+", spec) if key]
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _resolve_show_extracted(response: RapiResponse, spec: str) -> tuple[Any, str | None]:
|
|
534
555
|
"""Resolve the ``--show-extracted`` value and its empty-policy hint.
|
|
535
556
|
|
|
536
|
-
The
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
557
|
+
The failure semantics follow the output form:
|
|
558
|
+
|
|
559
|
+
- No ``spec`` (bare flag): the whole ``extracted`` mapping is returned as a
|
|
560
|
+
dict (exit 0; a missing ``extract:`` directive still fails).
|
|
561
|
+
- A single key: the raw value is returned and a None value fails (exit 1),
|
|
562
|
+
mirroring ``--pick``. This keeps backward compatibility with v3.2.0
|
|
563
|
+
scripts that read one key.
|
|
564
|
+
- Several keys (comma/space-separated): a JSON object subset is returned
|
|
565
|
+
(exit 0). A declared key whose expression matched nothing appears as
|
|
566
|
+
``null``; only an unknown key fails.
|
|
567
|
+
|
|
568
|
+
An endpoint without an ``extract:`` directive, or an unknown key, is a
|
|
569
|
+
usage failure (exit 1 via the hint). Hints name keys only, never values.
|
|
542
570
|
|
|
543
571
|
Args:
|
|
544
572
|
response: The API response whose ``extracted`` mapping is read.
|
|
545
|
-
|
|
573
|
+
spec: The requested keys (comma/space-separated), or an empty string
|
|
574
|
+
to select all keys.
|
|
546
575
|
|
|
547
576
|
Returns:
|
|
548
577
|
Tuple of (value to emit, empty hint). The hint is consumed by
|
|
@@ -552,12 +581,22 @@ def _resolve_show_extracted(response: RapiResponse, key: str) -> tuple[Any, str
|
|
|
552
581
|
extracted: Mapping[str, Any] = response.extracted
|
|
553
582
|
if not extracted:
|
|
554
583
|
return None, "No extract: directive declared for this endpoint."
|
|
555
|
-
|
|
584
|
+
keys = _split_extracted_keys(spec)
|
|
585
|
+
if not keys:
|
|
556
586
|
return dict(extracted), None
|
|
557
|
-
if
|
|
587
|
+
if len(keys) == 1:
|
|
588
|
+
key = keys[0]
|
|
589
|
+
if key not in extracted:
|
|
590
|
+
available = ", ".join(sorted(extracted))
|
|
591
|
+
return None, f"No extracted key '{key}'. Available: {available}."
|
|
592
|
+
return extracted.get(key), f"Extracted key '{key}' matched nothing."
|
|
593
|
+
missing = [key for key in keys if key not in extracted]
|
|
594
|
+
if missing:
|
|
558
595
|
available = ", ".join(sorted(extracted))
|
|
559
|
-
|
|
560
|
-
|
|
596
|
+
noun = "key" if len(missing) == 1 else "keys"
|
|
597
|
+
names = ", ".join(f"'{key}'" for key in missing)
|
|
598
|
+
return None, f"No extracted {noun} {names}. Available: {available}."
|
|
599
|
+
return {key: extracted[key] for key in keys}, None
|
|
561
600
|
|
|
562
601
|
|
|
563
602
|
def _handle_extraction_output(
|
|
@@ -703,6 +742,44 @@ def _render_response(
|
|
|
703
742
|
_format_output(response, fmt, quiet, out, raw=raw, minify=minify)
|
|
704
743
|
|
|
705
744
|
|
|
745
|
+
def _payload(key: str, value: object) -> dict[str, Any] | None:
|
|
746
|
+
"""Build a single-key error payload, or None when the value is empty."""
|
|
747
|
+
return {key: value} if value else None
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def _build_call_error_result(e: RapiError | httpx.InvalidURL) -> CommandResult:
|
|
751
|
+
"""Map a failed rapi call error to its ERROR CommandResult.
|
|
752
|
+
|
|
753
|
+
Handles every exception caught by the ``call`` command except
|
|
754
|
+
``AuthExpiredError`` (which keeps its dedicated exit-code-4 handling).
|
|
755
|
+
Each branch preserves the exact message and payload of the original
|
|
756
|
+
per-type handler.
|
|
757
|
+
"""
|
|
758
|
+
payload: dict[str, Any] | None = None
|
|
759
|
+
if isinstance(e, EndpointNotFoundError):
|
|
760
|
+
message = f"Endpoint not found: {e.endpoint_ref}"
|
|
761
|
+
payload = _payload("searched_apis", e.searched_apis)
|
|
762
|
+
elif isinstance(e, EndpointAmbiguousError):
|
|
763
|
+
message = f"Ambiguous endpoint: '{e.endpoint_name}' exists in multiple APIs"
|
|
764
|
+
payload = {"matching_apis": e.matching_apis}
|
|
765
|
+
elif isinstance(e, ServerNotFoundError):
|
|
766
|
+
message = f"Server profile not found: '{e.server_name}'. Available: {e.available or '(none configured)'}"
|
|
767
|
+
payload = _payload("available_servers", e.available)
|
|
768
|
+
elif isinstance(e, CredentialError):
|
|
769
|
+
message = f"Credential error: {e}"
|
|
770
|
+
payload = _payload("credential_name", e.credential_name)
|
|
771
|
+
elif isinstance(e, RequestError):
|
|
772
|
+
message = f"Request failed: {e}"
|
|
773
|
+
payload = {"status_code": e.status_code, "retryable": e.retryable}
|
|
774
|
+
elif isinstance(e, ResponseTooLargeError):
|
|
775
|
+
message = f"Response too large: {e.response_size} bytes (max: {e.max_size})"
|
|
776
|
+
elif isinstance(e, PathParameterError):
|
|
777
|
+
message = f"Path parameter error: {e}"
|
|
778
|
+
else:
|
|
779
|
+
message = f"Invalid URL: {e}"
|
|
780
|
+
return CommandResult(status=CommandStatus.ERROR, message=message, payload=payload)
|
|
781
|
+
|
|
782
|
+
|
|
706
783
|
def call(
|
|
707
784
|
endpoint: Annotated[
|
|
708
785
|
str,
|
|
@@ -811,10 +888,12 @@ def call(
|
|
|
811
888
|
str | None,
|
|
812
889
|
typer.Option(
|
|
813
890
|
"--show-extracted",
|
|
814
|
-
metavar="[KEY]",
|
|
891
|
+
metavar="[KEY[,KEY...]]",
|
|
815
892
|
help=(
|
|
816
893
|
"Print values declared by the endpoint extract: directive. "
|
|
817
|
-
"
|
|
894
|
+
"One KEY prints that value; several comma/space-separated keys "
|
|
895
|
+
"print a JSON object subset; without a value, print all keys as JSON. "
|
|
896
|
+
'Quote keys with spaces ("v1 v2") or join with commas (v1,v2) so the shell does not split them.'
|
|
818
897
|
),
|
|
819
898
|
),
|
|
820
899
|
] = None,
|
|
@@ -927,78 +1006,19 @@ def call(
|
|
|
927
1006
|
if not response.ok:
|
|
928
1007
|
raise typer.Exit(code=1)
|
|
929
1008
|
|
|
930
|
-
except EndpointNotFoundError as e:
|
|
931
|
-
exit_with_result(
|
|
932
|
-
CommandResult(
|
|
933
|
-
status=CommandStatus.ERROR,
|
|
934
|
-
message=f"Endpoint not found: {e.endpoint_ref}",
|
|
935
|
-
payload={"searched_apis": e.searched_apis} if e.searched_apis else None,
|
|
936
|
-
),
|
|
937
|
-
quiet=quiet,
|
|
938
|
-
exit_code=1,
|
|
939
|
-
cause=e,
|
|
940
|
-
)
|
|
941
|
-
except EndpointAmbiguousError as e:
|
|
942
|
-
exit_with_result(
|
|
943
|
-
CommandResult(
|
|
944
|
-
status=CommandStatus.ERROR,
|
|
945
|
-
message=f"Ambiguous endpoint: '{e.endpoint_name}' exists in multiple APIs",
|
|
946
|
-
payload={"matching_apis": e.matching_apis},
|
|
947
|
-
),
|
|
948
|
-
quiet=quiet,
|
|
949
|
-
exit_code=1,
|
|
950
|
-
cause=e,
|
|
951
|
-
)
|
|
952
|
-
except ServerNotFoundError as e:
|
|
953
|
-
exit_with_result(
|
|
954
|
-
CommandResult(
|
|
955
|
-
status=CommandStatus.ERROR,
|
|
956
|
-
message=(
|
|
957
|
-
f"Server profile not found: '{e.server_name}'. Available: {e.available or '(none configured)'}"
|
|
958
|
-
),
|
|
959
|
-
payload={"available_servers": e.available} if e.available else None,
|
|
960
|
-
),
|
|
961
|
-
quiet=quiet,
|
|
962
|
-
exit_code=1,
|
|
963
|
-
cause=e,
|
|
964
|
-
)
|
|
965
|
-
except CredentialError as e:
|
|
966
|
-
exit_with_result(
|
|
967
|
-
CommandResult(
|
|
968
|
-
status=CommandStatus.ERROR,
|
|
969
|
-
message=f"Credential error: {e}",
|
|
970
|
-
payload={"credential_name": e.credential_name} if e.credential_name else None,
|
|
971
|
-
),
|
|
972
|
-
quiet=quiet,
|
|
973
|
-
exit_code=1,
|
|
974
|
-
cause=e,
|
|
975
|
-
)
|
|
976
1009
|
except AuthExpiredError as e:
|
|
977
1010
|
_handle_auth_expired_error(e, quiet=quiet)
|
|
978
|
-
except
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
exit_code=1,
|
|
990
|
-
cause=e,
|
|
991
|
-
)
|
|
992
|
-
except ResponseTooLargeError as e:
|
|
993
|
-
exit_with_result(
|
|
994
|
-
CommandResult(
|
|
995
|
-
status=CommandStatus.ERROR,
|
|
996
|
-
message=f"Response too large: {e.response_size} bytes (max: {e.max_size})",
|
|
997
|
-
),
|
|
998
|
-
quiet=quiet,
|
|
999
|
-
exit_code=1,
|
|
1000
|
-
cause=e,
|
|
1001
|
-
)
|
|
1011
|
+
except (
|
|
1012
|
+
EndpointNotFoundError,
|
|
1013
|
+
EndpointAmbiguousError,
|
|
1014
|
+
ServerNotFoundError,
|
|
1015
|
+
CredentialError,
|
|
1016
|
+
RequestError,
|
|
1017
|
+
ResponseTooLargeError,
|
|
1018
|
+
PathParameterError,
|
|
1019
|
+
httpx.InvalidURL,
|
|
1020
|
+
) as e:
|
|
1021
|
+
exit_with_result(_build_call_error_result(e), quiet=quiet, exit_code=1, cause=e)
|
|
1002
1022
|
|
|
1003
1023
|
|
|
1004
1024
|
__all__ = ["call"]
|
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Annotated
|
|
|
7
7
|
import typer
|
|
8
8
|
from rich.table import Table
|
|
9
9
|
|
|
10
|
-
from kstlib.cli.common import console
|
|
10
|
+
from kstlib.cli.common import console, err_console
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from kstlib.rapi.config import ApiConfig, EndpointConfig
|
|
@@ -228,8 +228,8 @@ def list_endpoints(
|
|
|
228
228
|
# Filter by API name if specified
|
|
229
229
|
if api:
|
|
230
230
|
if api not in apis:
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
err_console.print(f"[red]API '{api}' not found.[/]")
|
|
232
|
+
err_console.print(f"[dim]Available APIs: {', '.join(apis.keys())}[/]")
|
|
233
233
|
raise typer.Exit(code=1)
|
|
234
234
|
apis = {api: apis[api]}
|
|
235
235
|
|
|
@@ -11,7 +11,7 @@ from rich.markup import escape
|
|
|
11
11
|
from rich.panel import Panel
|
|
12
12
|
from rich.table import Table
|
|
13
13
|
|
|
14
|
-
from kstlib.cli.common import console
|
|
14
|
+
from kstlib.cli.common import console, err_console
|
|
15
15
|
from kstlib.limits import HARD_MAX_DISPLAY_VALUE_LENGTH, HARD_MAX_ENDPOINT_REF_LENGTH
|
|
16
16
|
from kstlib.rapi import EndpointNotFoundError
|
|
17
17
|
from kstlib.rapi.config import _PATH_PARAM_PATTERN
|
|
@@ -33,12 +33,12 @@ def _truncate(value: str, max_length: int = HARD_MAX_DISPLAY_VALUE_LENGTH) -> st
|
|
|
33
33
|
def _validate_endpoint_ref(endpoint_ref: str) -> None:
|
|
34
34
|
"""Validate endpoint reference for security (deep defense)."""
|
|
35
35
|
if len(endpoint_ref) > HARD_MAX_ENDPOINT_REF_LENGTH:
|
|
36
|
-
|
|
36
|
+
err_console.print(f"[red]Endpoint reference too long: {len(endpoint_ref)} > {HARD_MAX_ENDPOINT_REF_LENGTH}[/]")
|
|
37
37
|
raise typer.Exit(code=1)
|
|
38
38
|
|
|
39
39
|
if not _ENDPOINT_REF_PATTERN.match(endpoint_ref):
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
err_console.print("[red]Endpoint reference contains invalid characters.[/]")
|
|
41
|
+
err_console.print("[dim]Allowed: alphanumeric, underscore, dot, hyphen[/]")
|
|
42
42
|
raise typer.Exit(code=1)
|
|
43
43
|
|
|
44
44
|
|
|
@@ -183,8 +183,8 @@ def show_endpoint(
|
|
|
183
183
|
try:
|
|
184
184
|
api_config, ep_config = config_manager.resolve(endpoint_ref)
|
|
185
185
|
except EndpointNotFoundError as e:
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
err_console.print(f"[red]Endpoint not found: {endpoint_ref}[/]")
|
|
187
|
+
err_console.print(f"[dim]Available APIs: {', '.join(e.searched_apis)}[/]")
|
|
188
188
|
raise typer.Exit(code=1) from e
|
|
189
189
|
|
|
190
190
|
path_params = _PATH_PARAM_PATTERN.findall(ep_config.path)
|
|
@@ -12,6 +12,7 @@ from rich.panel import Panel
|
|
|
12
12
|
from rich.pretty import Pretty
|
|
13
13
|
|
|
14
14
|
console = Console()
|
|
15
|
+
err_console = Console(stderr=True)
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class CommandStatus(str, Enum):
|
|
@@ -39,17 +40,23 @@ STYLE_MAP = {
|
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
def render_result(result: CommandResult) -> None:
|
|
42
|
-
"""Render a command result using Rich components.
|
|
43
|
+
"""Render a command result using Rich components.
|
|
44
|
+
|
|
45
|
+
ERROR results render to stderr (keeping stdout clean for shell capture);
|
|
46
|
+
OK and WARNING results render to stdout.
|
|
47
|
+
"""
|
|
43
48
|
style = STYLE_MAP[result.status]
|
|
44
|
-
|
|
49
|
+
target = err_console if result.status is CommandStatus.ERROR else console
|
|
50
|
+
target.print(Panel(result.message, title=result.status.value.upper(), style=style, border_style=style))
|
|
45
51
|
if result.payload is not None:
|
|
46
|
-
|
|
52
|
+
target.print(Pretty(result.payload))
|
|
47
53
|
|
|
48
54
|
|
|
49
55
|
def emit_result(result: CommandResult, quiet: bool) -> None:
|
|
50
56
|
"""Output a command result honoring the quiet flag."""
|
|
51
57
|
if quiet:
|
|
52
|
-
|
|
58
|
+
target = err_console if result.status is CommandStatus.ERROR else console
|
|
59
|
+
target.print(result.message, style=STYLE_MAP[result.status])
|
|
53
60
|
else:
|
|
54
61
|
render_result(result)
|
|
55
62
|
|
|
@@ -80,6 +87,7 @@ __all__ = [
|
|
|
80
87
|
"CommandStatus",
|
|
81
88
|
"console",
|
|
82
89
|
"emit_result",
|
|
90
|
+
"err_console",
|
|
83
91
|
"exit_error",
|
|
84
92
|
"exit_with_result",
|
|
85
93
|
"render_result",
|
|
@@ -28,6 +28,7 @@ import subprocess
|
|
|
28
28
|
from collections import OrderedDict
|
|
29
29
|
from typing import Any
|
|
30
30
|
|
|
31
|
+
from kstlib._shared.logging_helpers import log_trace
|
|
31
32
|
from kstlib.config.exceptions import ConfigSopsError, ConfigSopsNotAvailableError
|
|
32
33
|
from kstlib.limits import (
|
|
33
34
|
DEFAULT_MAX_SOPS_CACHE_ENTRIES,
|
|
@@ -37,17 +38,6 @@ from kstlib.limits import (
|
|
|
37
38
|
logger = logging.getLogger(__name__)
|
|
38
39
|
|
|
39
40
|
|
|
40
|
-
def _log_trace(msg: str, *args: object) -> None:
|
|
41
|
-
"""Log at TRACE level (custom level 5, below DEBUG).
|
|
42
|
-
|
|
43
|
-
Uses a lazy import to avoid the circular import chain
|
|
44
|
-
``kstlib.logging.manager -> kstlib.config -> kstlib.config.sops``.
|
|
45
|
-
"""
|
|
46
|
-
from kstlib.logging import TRACE_LEVEL
|
|
47
|
-
|
|
48
|
-
logger.log(TRACE_LEVEL, msg, *args)
|
|
49
|
-
|
|
50
|
-
|
|
51
41
|
SOPS_FILE_PATTERNS: tuple[str, ...] = (
|
|
52
42
|
".sops.yml",
|
|
53
43
|
".sops.yaml",
|
|
@@ -217,7 +207,7 @@ class SopsDecryptor:
|
|
|
217
207
|
cached = self._cache.get(resolved)
|
|
218
208
|
if cached and cached[0] == mtime:
|
|
219
209
|
self._cache.move_to_end(resolved)
|
|
220
|
-
|
|
210
|
+
log_trace(logger, "SOPS cache hit for: %s", path.name)
|
|
221
211
|
return cached[1]
|
|
222
212
|
|
|
223
213
|
# Security: reject absolute/relative paths to prevent binary override via config
|
|
@@ -269,7 +259,7 @@ class SopsDecryptor:
|
|
|
269
259
|
else:
|
|
270
260
|
removed = self._cache.pop(path.resolve(), None)
|
|
271
261
|
if removed:
|
|
272
|
-
|
|
262
|
+
log_trace(logger, "SOPS cache entry removed: %s", path.name)
|
|
273
263
|
|
|
274
264
|
@property
|
|
275
265
|
def cache_size(self) -> int:
|
|
@@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Any, cast
|
|
|
17
17
|
|
|
18
18
|
from typing_extensions import Self
|
|
19
19
|
|
|
20
|
+
from kstlib._shared.logging_helpers import log_trace
|
|
20
21
|
from kstlib.db.exceptions import TransactionError
|
|
21
22
|
from kstlib.db.pool import ConnectionPool, PoolStats
|
|
22
23
|
|
|
@@ -35,13 +36,6 @@ log = logging.getLogger(__name__)
|
|
|
35
36
|
_SQL_TRACE_TRUNCATE = 200
|
|
36
37
|
|
|
37
38
|
|
|
38
|
-
def _log_trace(msg: str, *args: object) -> None:
|
|
39
|
-
"""Log at TRACE level (custom level 5, below DEBUG)."""
|
|
40
|
-
from kstlib.logging import TRACE_LEVEL
|
|
41
|
-
|
|
42
|
-
log.log(TRACE_LEVEL, msg, *args)
|
|
43
|
-
|
|
44
|
-
|
|
45
39
|
def _truncate_sql(sql: str, limit: int = _SQL_TRACE_TRUNCATE) -> str:
|
|
46
40
|
"""Return ``sql`` truncated to ``limit`` characters, single-line."""
|
|
47
41
|
flat = " ".join(sql.split())
|
|
@@ -254,7 +248,7 @@ class AsyncDatabase:
|
|
|
254
248
|
# Sanitization invariant : log the truncated SQL only, NEVER the
|
|
255
249
|
# parameters tuple (would leak PII / credentials in WHERE/INSERT
|
|
256
250
|
# values). Truncation prevents giant DDL from drowning the trace.
|
|
257
|
-
|
|
251
|
+
log_trace(log, "[DB] Execute: %s (params=%s)", _truncate_sql(sql), "yes" if parameters else "no")
|
|
258
252
|
pool = self._ensure_pool()
|
|
259
253
|
async with pool.connection() as conn:
|
|
260
254
|
if parameters:
|
|
@@ -175,6 +175,10 @@ logger:
|
|
|
175
175
|
format: "::: PID %(process)d / TID %(thread)d ::: %(message)s"
|
|
176
176
|
show_path: true
|
|
177
177
|
tracebacks_show_locals: true
|
|
178
|
+
# Console routing: stdout (default) | stderr. Route to stderr when the CLI
|
|
179
|
+
# emits data/JSON on stdout, so logs do not corrupt the piped output
|
|
180
|
+
# (Unix data vs diagnostics separation). Invalid values raise ConfigError.
|
|
181
|
+
stream: stdout
|
|
178
182
|
|
|
179
183
|
# File handler settings
|
|
180
184
|
# Two configuration styles are supported:
|
|
@@ -49,7 +49,7 @@ from rich.logging import RichHandler
|
|
|
49
49
|
from rich.theme import Theme
|
|
50
50
|
from rich.traceback import Traceback
|
|
51
51
|
|
|
52
|
-
from kstlib.config import get_config
|
|
52
|
+
from kstlib.config import ConfigError, get_config
|
|
53
53
|
|
|
54
54
|
# =============================================================================
|
|
55
55
|
# HARDCODED LIMITS (Deep Defense)
|
|
@@ -253,6 +253,7 @@ FALLBACK_DEFAULTS = {
|
|
|
253
253
|
"format": "::: PID %(process)d / TID %(thread)d ::: %(message)s",
|
|
254
254
|
"show_path": True,
|
|
255
255
|
"tracebacks_show_locals": False,
|
|
256
|
+
"stream": "stdout",
|
|
256
257
|
},
|
|
257
258
|
"file": {
|
|
258
259
|
"level": "DEBUG",
|
|
@@ -340,7 +341,15 @@ class LogManager(logging.Logger):
|
|
|
340
341
|
# Setup console and theme
|
|
341
342
|
self.width = shutil.get_terminal_size(fallback=(120, 30)).columns
|
|
342
343
|
theme = self._create_theme()
|
|
343
|
-
|
|
344
|
+
# Route console output to stdout (default) or stderr. Routing to stderr
|
|
345
|
+
# keeps stdout clean for CLIs that emit data/JSON there (Unix data vs
|
|
346
|
+
# diagnostics separation). Fail fast on a typo so a misconfigured value
|
|
347
|
+
# never silently falls back to stdout and corrupts piped data.
|
|
348
|
+
stream = self._config.console.get("stream", "stdout")
|
|
349
|
+
if stream not in ("stdout", "stderr"):
|
|
350
|
+
raise ConfigError(f"Invalid console.stream {stream!r}: expected 'stdout' or 'stderr'")
|
|
351
|
+
_internal_log.debug("[INIT] console stream resolved to %r", stream)
|
|
352
|
+
self.console = Console(theme=theme, width=self.width, stderr=(stream == "stderr"))
|
|
344
353
|
|
|
345
354
|
# Setup handlers
|
|
346
355
|
self._setup_handlers()
|
|
@@ -98,6 +98,7 @@ from kstlib.rapi.exceptions import (
|
|
|
98
98
|
EndpointNotFoundError,
|
|
99
99
|
EnvVarError,
|
|
100
100
|
MinifyRequiresRawError,
|
|
101
|
+
PathParameterError,
|
|
101
102
|
RapiError,
|
|
102
103
|
RequestError,
|
|
103
104
|
ResponseTooLargeError,
|
|
@@ -119,6 +120,7 @@ __all__ = [
|
|
|
119
120
|
"HmacConfig",
|
|
120
121
|
"MinifyRequiresRawError",
|
|
121
122
|
"MultipartConfig",
|
|
123
|
+
"PathParameterError",
|
|
122
124
|
"RapiClient",
|
|
123
125
|
"RapiConfigManager",
|
|
124
126
|
"RapiError",
|