kstlib 0.0.1a0__py3-none-any.whl → 1.0.1__py3-none-any.whl
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/__init__.py +266 -1
- kstlib/__main__.py +16 -0
- kstlib/alerts/__init__.py +110 -0
- kstlib/alerts/channels/__init__.py +36 -0
- kstlib/alerts/channels/base.py +197 -0
- kstlib/alerts/channels/email.py +227 -0
- kstlib/alerts/channels/slack.py +389 -0
- kstlib/alerts/exceptions.py +72 -0
- kstlib/alerts/manager.py +651 -0
- kstlib/alerts/models.py +142 -0
- kstlib/alerts/throttle.py +263 -0
- kstlib/auth/__init__.py +139 -0
- kstlib/auth/callback.py +399 -0
- kstlib/auth/config.py +502 -0
- kstlib/auth/errors.py +127 -0
- kstlib/auth/models.py +316 -0
- kstlib/auth/providers/__init__.py +14 -0
- kstlib/auth/providers/base.py +393 -0
- kstlib/auth/providers/oauth2.py +645 -0
- kstlib/auth/providers/oidc.py +821 -0
- kstlib/auth/session.py +338 -0
- kstlib/auth/token.py +482 -0
- kstlib/cache/__init__.py +50 -0
- kstlib/cache/decorator.py +261 -0
- kstlib/cache/strategies.py +516 -0
- kstlib/cli/__init__.py +8 -0
- kstlib/cli/app.py +195 -0
- kstlib/cli/commands/__init__.py +5 -0
- kstlib/cli/commands/auth/__init__.py +39 -0
- kstlib/cli/commands/auth/common.py +122 -0
- kstlib/cli/commands/auth/login.py +325 -0
- kstlib/cli/commands/auth/logout.py +74 -0
- kstlib/cli/commands/auth/providers.py +57 -0
- kstlib/cli/commands/auth/status.py +291 -0
- kstlib/cli/commands/auth/token.py +199 -0
- kstlib/cli/commands/auth/whoami.py +106 -0
- kstlib/cli/commands/config.py +89 -0
- kstlib/cli/commands/ops/__init__.py +39 -0
- kstlib/cli/commands/ops/attach.py +49 -0
- kstlib/cli/commands/ops/common.py +269 -0
- kstlib/cli/commands/ops/list_sessions.py +252 -0
- kstlib/cli/commands/ops/logs.py +49 -0
- kstlib/cli/commands/ops/start.py +98 -0
- kstlib/cli/commands/ops/status.py +138 -0
- kstlib/cli/commands/ops/stop.py +60 -0
- kstlib/cli/commands/rapi/__init__.py +60 -0
- kstlib/cli/commands/rapi/call.py +341 -0
- kstlib/cli/commands/rapi/list.py +99 -0
- kstlib/cli/commands/rapi/show.py +206 -0
- kstlib/cli/commands/secrets/__init__.py +35 -0
- kstlib/cli/commands/secrets/common.py +425 -0
- kstlib/cli/commands/secrets/decrypt.py +88 -0
- kstlib/cli/commands/secrets/doctor.py +743 -0
- kstlib/cli/commands/secrets/encrypt.py +242 -0
- kstlib/cli/commands/secrets/shred.py +96 -0
- kstlib/cli/common.py +86 -0
- kstlib/config/__init__.py +76 -0
- kstlib/config/exceptions.py +110 -0
- kstlib/config/export.py +225 -0
- kstlib/config/loader.py +963 -0
- kstlib/config/sops.py +287 -0
- kstlib/db/__init__.py +54 -0
- kstlib/db/aiosqlcipher.py +137 -0
- kstlib/db/cipher.py +112 -0
- kstlib/db/database.py +367 -0
- kstlib/db/exceptions.py +25 -0
- kstlib/db/pool.py +302 -0
- kstlib/helpers/__init__.py +35 -0
- kstlib/helpers/exceptions.py +11 -0
- kstlib/helpers/time_trigger.py +396 -0
- kstlib/kstlib.conf.yml +890 -0
- kstlib/limits.py +963 -0
- kstlib/logging/__init__.py +108 -0
- kstlib/logging/manager.py +633 -0
- kstlib/mail/__init__.py +42 -0
- kstlib/mail/builder.py +626 -0
- kstlib/mail/exceptions.py +27 -0
- kstlib/mail/filesystem.py +248 -0
- kstlib/mail/transport.py +224 -0
- kstlib/mail/transports/__init__.py +19 -0
- kstlib/mail/transports/gmail.py +268 -0
- kstlib/mail/transports/resend.py +324 -0
- kstlib/mail/transports/smtp.py +326 -0
- kstlib/meta.py +72 -0
- kstlib/metrics/__init__.py +88 -0
- kstlib/metrics/decorators.py +1090 -0
- kstlib/metrics/exceptions.py +14 -0
- kstlib/monitoring/__init__.py +116 -0
- kstlib/monitoring/_styles.py +163 -0
- kstlib/monitoring/cell.py +57 -0
- kstlib/monitoring/config.py +424 -0
- kstlib/monitoring/delivery.py +579 -0
- kstlib/monitoring/exceptions.py +63 -0
- kstlib/monitoring/image.py +220 -0
- kstlib/monitoring/kv.py +79 -0
- kstlib/monitoring/list.py +69 -0
- kstlib/monitoring/metric.py +88 -0
- kstlib/monitoring/monitoring.py +341 -0
- kstlib/monitoring/renderer.py +139 -0
- kstlib/monitoring/service.py +392 -0
- kstlib/monitoring/table.py +129 -0
- kstlib/monitoring/types.py +56 -0
- kstlib/ops/__init__.py +86 -0
- kstlib/ops/base.py +148 -0
- kstlib/ops/container.py +577 -0
- kstlib/ops/exceptions.py +209 -0
- kstlib/ops/manager.py +407 -0
- kstlib/ops/models.py +176 -0
- kstlib/ops/tmux.py +372 -0
- kstlib/ops/validators.py +287 -0
- kstlib/py.typed +0 -0
- kstlib/rapi/__init__.py +118 -0
- kstlib/rapi/client.py +875 -0
- kstlib/rapi/config.py +861 -0
- kstlib/rapi/credentials.py +887 -0
- kstlib/rapi/exceptions.py +213 -0
- kstlib/resilience/__init__.py +101 -0
- kstlib/resilience/circuit_breaker.py +440 -0
- kstlib/resilience/exceptions.py +95 -0
- kstlib/resilience/heartbeat.py +491 -0
- kstlib/resilience/rate_limiter.py +506 -0
- kstlib/resilience/shutdown.py +417 -0
- kstlib/resilience/watchdog.py +637 -0
- kstlib/secrets/__init__.py +29 -0
- kstlib/secrets/exceptions.py +19 -0
- kstlib/secrets/models.py +62 -0
- kstlib/secrets/providers/__init__.py +79 -0
- kstlib/secrets/providers/base.py +58 -0
- kstlib/secrets/providers/environment.py +66 -0
- kstlib/secrets/providers/keyring.py +107 -0
- kstlib/secrets/providers/kms.py +223 -0
- kstlib/secrets/providers/kwargs.py +101 -0
- kstlib/secrets/providers/sops.py +209 -0
- kstlib/secrets/resolver.py +221 -0
- kstlib/secrets/sensitive.py +130 -0
- kstlib/secure/__init__.py +23 -0
- kstlib/secure/fs.py +194 -0
- kstlib/secure/permissions.py +70 -0
- kstlib/ssl.py +347 -0
- kstlib/ui/__init__.py +23 -0
- kstlib/ui/exceptions.py +26 -0
- kstlib/ui/panels.py +484 -0
- kstlib/ui/spinner.py +864 -0
- kstlib/ui/tables.py +382 -0
- kstlib/utils/__init__.py +48 -0
- kstlib/utils/dict.py +36 -0
- kstlib/utils/formatting.py +338 -0
- kstlib/utils/http_trace.py +237 -0
- kstlib/utils/lazy.py +49 -0
- kstlib/utils/secure_delete.py +205 -0
- kstlib/utils/serialization.py +247 -0
- kstlib/utils/text.py +56 -0
- kstlib/utils/validators.py +124 -0
- kstlib/websocket/__init__.py +97 -0
- kstlib/websocket/exceptions.py +214 -0
- kstlib/websocket/manager.py +1102 -0
- kstlib/websocket/models.py +361 -0
- kstlib-1.0.1.dist-info/METADATA +201 -0
- kstlib-1.0.1.dist-info/RECORD +163 -0
- {kstlib-0.0.1a0.dist-info → kstlib-1.0.1.dist-info}/WHEEL +1 -1
- kstlib-1.0.1.dist-info/entry_points.txt +2 -0
- kstlib-1.0.1.dist-info/licenses/LICENSE.md +9 -0
- kstlib-0.0.1a0.dist-info/METADATA +0 -29
- kstlib-0.0.1a0.dist-info/RECORD +0 -6
- kstlib-0.0.1a0.dist-info/licenses/LICENSE.md +0 -5
- {kstlib-0.0.1a0.dist-info → kstlib-1.0.1.dist-info}/top_level.txt +0 -0
kstlib/alerts/models.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Data models for the alerts module.
|
|
2
|
+
|
|
3
|
+
This module defines the core data structures for alert messages, levels,
|
|
4
|
+
and delivery results.
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
Creating an alert message::
|
|
8
|
+
|
|
9
|
+
from kstlib.alerts.models import AlertLevel, AlertMessage
|
|
10
|
+
|
|
11
|
+
alert = AlertMessage(
|
|
12
|
+
title="Server Down",
|
|
13
|
+
body="Production server api-1 is not responding",
|
|
14
|
+
level=AlertLevel.CRITICAL,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
Checking alert results::
|
|
18
|
+
|
|
19
|
+
from kstlib.alerts.models import AlertResult
|
|
20
|
+
|
|
21
|
+
result = AlertResult(channel="slack", success=True, message_id="12345")
|
|
22
|
+
if result.success:
|
|
23
|
+
print(f"Alert delivered: {result.message_id}")
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from enum import IntEnum
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AlertLevel(IntEnum):
|
|
34
|
+
"""Severity level for alerts.
|
|
35
|
+
|
|
36
|
+
Values are ordered by severity (higher value = more severe).
|
|
37
|
+
Use these to filter which alerts go to which channels.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
INFO: Informational messages (10).
|
|
41
|
+
WARNING: Warning conditions that need attention (20).
|
|
42
|
+
CRITICAL: Critical issues requiring immediate action (30).
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
>>> AlertLevel.CRITICAL > AlertLevel.WARNING
|
|
46
|
+
True
|
|
47
|
+
>>> AlertLevel.INFO.name
|
|
48
|
+
'INFO'
|
|
49
|
+
>>> int(AlertLevel.WARNING)
|
|
50
|
+
20
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
INFO = 10
|
|
54
|
+
WARNING = 20
|
|
55
|
+
CRITICAL = 30
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class AlertMessage:
|
|
60
|
+
"""An alert message to be sent via one or more channels.
|
|
61
|
+
|
|
62
|
+
Attributes:
|
|
63
|
+
title: Short summary of the alert (max 150 chars for Slack).
|
|
64
|
+
body: Detailed message content (max 3000 chars for Slack).
|
|
65
|
+
level: Severity level of the alert.
|
|
66
|
+
timestamp: If True, prefix title with send datetime.
|
|
67
|
+
|
|
68
|
+
Examples:
|
|
69
|
+
>>> msg = AlertMessage(title="Disk Full", body="Server disk at 95%")
|
|
70
|
+
>>> msg.level
|
|
71
|
+
<AlertLevel.INFO: 10>
|
|
72
|
+
>>> msg = AlertMessage(
|
|
73
|
+
... title="DB Connection Failed",
|
|
74
|
+
... body="Cannot connect to primary database",
|
|
75
|
+
... level=AlertLevel.CRITICAL,
|
|
76
|
+
... )
|
|
77
|
+
>>> msg.level.name
|
|
78
|
+
'CRITICAL'
|
|
79
|
+
>>> msg = AlertMessage(
|
|
80
|
+
... title="Alert",
|
|
81
|
+
... body="With timestamp",
|
|
82
|
+
... timestamp=True,
|
|
83
|
+
... )
|
|
84
|
+
>>> ":::" in msg.formatted_title
|
|
85
|
+
True
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
title: str
|
|
89
|
+
body: str
|
|
90
|
+
level: AlertLevel = AlertLevel.INFO
|
|
91
|
+
timestamp: bool = False
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def formatted_title(self) -> str:
|
|
95
|
+
"""Return title with optional timestamp prefix.
|
|
96
|
+
|
|
97
|
+
If timestamp is True, prefixes the title with current datetime
|
|
98
|
+
in format "YYYY-MM-DD HH:MM:SS ::: ".
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Title string, optionally prefixed with timestamp.
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
>>> msg = AlertMessage(title="Test", body="Body", timestamp=False)
|
|
105
|
+
>>> msg.formatted_title
|
|
106
|
+
'Test'
|
|
107
|
+
>>> msg = AlertMessage(title="Test", body="Body", timestamp=True)
|
|
108
|
+
>>> "Test" in msg.formatted_title
|
|
109
|
+
True
|
|
110
|
+
"""
|
|
111
|
+
if self.timestamp:
|
|
112
|
+
now = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S")
|
|
113
|
+
return f"{now} ::: {self.title}"
|
|
114
|
+
return self.title
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True, slots=True)
|
|
118
|
+
class AlertResult:
|
|
119
|
+
"""Result of sending an alert to a channel.
|
|
120
|
+
|
|
121
|
+
Attributes:
|
|
122
|
+
channel: Name of the channel that processed this alert.
|
|
123
|
+
success: Whether the alert was delivered successfully.
|
|
124
|
+
message_id: ID assigned by the channel (if available).
|
|
125
|
+
error: Error message if delivery failed.
|
|
126
|
+
|
|
127
|
+
Examples:
|
|
128
|
+
>>> result = AlertResult(channel="slack", success=True, message_id="msg123")
|
|
129
|
+
>>> result.success
|
|
130
|
+
True
|
|
131
|
+
>>> result = AlertResult(channel="email", success=False, error="SMTP timeout")
|
|
132
|
+
>>> result.error
|
|
133
|
+
'SMTP timeout'
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
channel: str
|
|
137
|
+
success: bool
|
|
138
|
+
message_id: str | None = None
|
|
139
|
+
error: str | None = None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
__all__ = ["AlertLevel", "AlertMessage", "AlertResult"]
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Alert throttling using rate limiting.
|
|
2
|
+
|
|
3
|
+
Provides rate limiting for alerts to prevent flooding channels during
|
|
4
|
+
incident storms. Wraps :class:`kstlib.resilience.rate_limiter.RateLimiter`.
|
|
5
|
+
|
|
6
|
+
Configuration is read from ``kstlib.conf.yml`` under the ``alerts.throttle``
|
|
7
|
+
section, with hard limits enforced for deep defense.
|
|
8
|
+
|
|
9
|
+
Examples:
|
|
10
|
+
Config-driven (recommended)::
|
|
11
|
+
|
|
12
|
+
from kstlib.alerts.throttle import AlertThrottle
|
|
13
|
+
|
|
14
|
+
# Uses alerts.throttle from kstlib.conf.yml
|
|
15
|
+
throttle = AlertThrottle()
|
|
16
|
+
|
|
17
|
+
if throttle.try_acquire():
|
|
18
|
+
await channel.send(alert)
|
|
19
|
+
|
|
20
|
+
Explicit override::
|
|
21
|
+
|
|
22
|
+
# Override config values
|
|
23
|
+
throttle = AlertThrottle(rate=5, per=30.0)
|
|
24
|
+
|
|
25
|
+
With async context::
|
|
26
|
+
|
|
27
|
+
throttle = AlertThrottle()
|
|
28
|
+
|
|
29
|
+
async with throttle:
|
|
30
|
+
await channel.send(alert) # Waits if rate limit hit
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from typing import TYPE_CHECKING
|
|
36
|
+
|
|
37
|
+
from kstlib.alerts.exceptions import AlertThrottledError
|
|
38
|
+
from kstlib.limits import (
|
|
39
|
+
HARD_MAX_THROTTLE_PER,
|
|
40
|
+
HARD_MAX_THROTTLE_RATE,
|
|
41
|
+
HARD_MIN_THROTTLE_PER,
|
|
42
|
+
HARD_MIN_THROTTLE_RATE,
|
|
43
|
+
clamp_with_limits,
|
|
44
|
+
get_alerts_limits,
|
|
45
|
+
)
|
|
46
|
+
from kstlib.resilience.rate_limiter import RateLimiter
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from typing_extensions import Self
|
|
50
|
+
|
|
51
|
+
__all__ = ["AlertThrottle"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AlertThrottle:
|
|
55
|
+
"""Rate limiter for alert delivery.
|
|
56
|
+
|
|
57
|
+
Wraps :class:`kstlib.resilience.rate_limiter.RateLimiter` with
|
|
58
|
+
alert-specific behavior and config-driven defaults.
|
|
59
|
+
|
|
60
|
+
All parameters are optional. If not provided, values are read from
|
|
61
|
+
``kstlib.conf.yml`` under ``alerts.throttle``. Hard limits are enforced
|
|
62
|
+
for deep defense against misconfiguration.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
rate: Maximum alerts per period. If None, uses config value.
|
|
66
|
+
Hard limits: [1, 1000].
|
|
67
|
+
per: Period duration in seconds. If None, uses config value.
|
|
68
|
+
Hard limits: [1.0, 86400.0] (1 day).
|
|
69
|
+
burst: Initial capacity. If None, defaults to rate value.
|
|
70
|
+
Hard limits: [1, rate].
|
|
71
|
+
name: Optional name for identification.
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
Config-driven (recommended)::
|
|
75
|
+
|
|
76
|
+
throttle = AlertThrottle() # Uses kstlib.conf.yml
|
|
77
|
+
|
|
78
|
+
Explicit values::
|
|
79
|
+
|
|
80
|
+
throttle = AlertThrottle(rate=10, per=60.0)
|
|
81
|
+
|
|
82
|
+
Per-hour limiting with burst::
|
|
83
|
+
|
|
84
|
+
throttle = AlertThrottle(rate=100, per=3600.0, burst=20)
|
|
85
|
+
|
|
86
|
+
Non-blocking check::
|
|
87
|
+
|
|
88
|
+
if throttle.try_acquire():
|
|
89
|
+
send_alert()
|
|
90
|
+
else:
|
|
91
|
+
log.warning("Alert throttled")
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
rate: float | None = None,
|
|
97
|
+
per: float | None = None,
|
|
98
|
+
*,
|
|
99
|
+
burst: float | None = None,
|
|
100
|
+
name: str | None = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Initialize AlertThrottle.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
rate: Maximum alerts per period. If None, uses config.
|
|
106
|
+
per: Period duration in seconds. If None, uses config.
|
|
107
|
+
burst: Initial token count. If None, defaults to rate.
|
|
108
|
+
name: Optional name for identification.
|
|
109
|
+
"""
|
|
110
|
+
# Load config defaults
|
|
111
|
+
limits = get_alerts_limits()
|
|
112
|
+
|
|
113
|
+
# Apply kwargs > config > defaults pattern with hard limit clamping
|
|
114
|
+
resolved_rate = clamp_with_limits(
|
|
115
|
+
rate if rate is not None else limits.throttle_rate,
|
|
116
|
+
HARD_MIN_THROTTLE_RATE,
|
|
117
|
+
HARD_MAX_THROTTLE_RATE,
|
|
118
|
+
)
|
|
119
|
+
resolved_per = clamp_with_limits(
|
|
120
|
+
per if per is not None else limits.throttle_per,
|
|
121
|
+
HARD_MIN_THROTTLE_PER,
|
|
122
|
+
HARD_MAX_THROTTLE_PER,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Burst: kwargs > config > rate, clamped to [1, rate]
|
|
126
|
+
if burst is not None:
|
|
127
|
+
resolved_burst = clamp_with_limits(burst, 1, resolved_rate)
|
|
128
|
+
else:
|
|
129
|
+
resolved_burst = clamp_with_limits(limits.throttle_burst, 1, resolved_rate)
|
|
130
|
+
|
|
131
|
+
self._limiter = RateLimiter(
|
|
132
|
+
rate=resolved_rate,
|
|
133
|
+
per=resolved_per,
|
|
134
|
+
burst=resolved_burst,
|
|
135
|
+
name=name,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def rate(self) -> float:
|
|
140
|
+
"""Maximum alerts per period."""
|
|
141
|
+
return self._limiter.rate
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def per(self) -> float:
|
|
145
|
+
"""Period duration in seconds."""
|
|
146
|
+
return self._limiter.per
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def available(self) -> float:
|
|
150
|
+
"""Current available tokens (alerts allowed)."""
|
|
151
|
+
return self._limiter.tokens
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def time_until_available(self) -> float:
|
|
155
|
+
"""Seconds until next alert can be sent."""
|
|
156
|
+
return self._limiter.time_until_token()
|
|
157
|
+
|
|
158
|
+
def try_acquire(self) -> bool:
|
|
159
|
+
"""Try to acquire permission to send an alert.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
True if alert can be sent, False if throttled.
|
|
163
|
+
|
|
164
|
+
Examples:
|
|
165
|
+
>>> throttle = AlertThrottle(rate=2, per=1.0)
|
|
166
|
+
>>> throttle.try_acquire()
|
|
167
|
+
True
|
|
168
|
+
>>> throttle.try_acquire()
|
|
169
|
+
True
|
|
170
|
+
>>> throttle.try_acquire() # Throttled
|
|
171
|
+
False
|
|
172
|
+
"""
|
|
173
|
+
return self._limiter.try_acquire()
|
|
174
|
+
|
|
175
|
+
def acquire(self, *, timeout: float | None = None) -> None:
|
|
176
|
+
"""Acquire permission to send an alert, blocking if needed.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
timeout: Maximum time to wait in seconds.
|
|
180
|
+
|
|
181
|
+
Raises:
|
|
182
|
+
AlertThrottledError: If timeout exceeded.
|
|
183
|
+
|
|
184
|
+
Examples:
|
|
185
|
+
>>> throttle = AlertThrottle(rate=10, per=60.0)
|
|
186
|
+
>>> throttle.acquire() # Blocks if needed
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
self._limiter.acquire(blocking=True, timeout=timeout)
|
|
190
|
+
except Exception:
|
|
191
|
+
raise AlertThrottledError(
|
|
192
|
+
"Alert rate limit exceeded",
|
|
193
|
+
retry_after=self.time_until_available,
|
|
194
|
+
) from None
|
|
195
|
+
|
|
196
|
+
async def acquire_async(self, *, timeout: float | None = None) -> None:
|
|
197
|
+
"""Acquire permission asynchronously, waiting if needed.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
timeout: Maximum time to wait in seconds.
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
AlertThrottledError: If timeout exceeded.
|
|
204
|
+
|
|
205
|
+
Examples:
|
|
206
|
+
>>> import asyncio
|
|
207
|
+
>>> throttle = AlertThrottle(rate=10, per=60.0)
|
|
208
|
+
>>> asyncio.run(throttle.acquire_async())
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
await self._limiter.acquire_async(timeout=timeout)
|
|
212
|
+
except Exception:
|
|
213
|
+
raise AlertThrottledError(
|
|
214
|
+
"Alert rate limit exceeded",
|
|
215
|
+
retry_after=self.time_until_available,
|
|
216
|
+
) from None
|
|
217
|
+
|
|
218
|
+
def reset(self) -> None:
|
|
219
|
+
"""Reset throttle to full capacity.
|
|
220
|
+
|
|
221
|
+
Examples:
|
|
222
|
+
>>> throttle = AlertThrottle(rate=1, per=60.0)
|
|
223
|
+
>>> throttle.try_acquire()
|
|
224
|
+
True
|
|
225
|
+
>>> throttle.try_acquire()
|
|
226
|
+
False
|
|
227
|
+
>>> throttle.reset()
|
|
228
|
+
>>> throttle.try_acquire()
|
|
229
|
+
True
|
|
230
|
+
"""
|
|
231
|
+
self._limiter.reset()
|
|
232
|
+
|
|
233
|
+
def __enter__(self) -> Self:
|
|
234
|
+
"""Enter context manager, acquiring permission."""
|
|
235
|
+
self.acquire()
|
|
236
|
+
return self
|
|
237
|
+
|
|
238
|
+
def __exit__(
|
|
239
|
+
self,
|
|
240
|
+
exc_type: type[BaseException] | None,
|
|
241
|
+
exc_val: BaseException | None,
|
|
242
|
+
exc_tb: object,
|
|
243
|
+
) -> None:
|
|
244
|
+
"""Exit context manager."""
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
async def __aenter__(self) -> Self:
|
|
248
|
+
"""Enter async context manager, acquiring permission."""
|
|
249
|
+
await self.acquire_async()
|
|
250
|
+
return self
|
|
251
|
+
|
|
252
|
+
async def __aexit__(
|
|
253
|
+
self,
|
|
254
|
+
exc_type: type[BaseException] | None,
|
|
255
|
+
exc_val: BaseException | None,
|
|
256
|
+
exc_tb: object,
|
|
257
|
+
) -> None:
|
|
258
|
+
"""Exit async context manager."""
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
def __repr__(self) -> str:
|
|
262
|
+
"""Return string representation."""
|
|
263
|
+
return f"AlertThrottle(rate={self.rate}, per={self.per})"
|
kstlib/auth/__init__.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# pylint: disable=duplicate-code
|
|
2
|
+
"""OAuth2/OIDC authentication module for kstlib.
|
|
3
|
+
|
|
4
|
+
This module provides a config-driven authentication layer supporting:
|
|
5
|
+
- OAuth2 Authorization Code flow
|
|
6
|
+
- OIDC with PKCE extension
|
|
7
|
+
- Automatic token refresh
|
|
8
|
+
- Secure token storage (SOPS encrypted or memory)
|
|
9
|
+
- Preflight validation for IdP configuration
|
|
10
|
+
|
|
11
|
+
Example (explicit configuration):
|
|
12
|
+
>>> from kstlib.auth import AuthSession, OIDCProvider, AuthProviderConfig # doctest: +SKIP
|
|
13
|
+
>>> from kstlib.auth import MemoryTokenStorage # doctest: +SKIP
|
|
14
|
+
>>>
|
|
15
|
+
>>> config = AuthProviderConfig( # doctest: +SKIP
|
|
16
|
+
... client_id="my-app",
|
|
17
|
+
... issuer="https://auth.example.com",
|
|
18
|
+
... scopes=["openid", "profile"],
|
|
19
|
+
... )
|
|
20
|
+
>>> provider = OIDCProvider("example", config, MemoryTokenStorage()) # doctest: +SKIP
|
|
21
|
+
>>> with AuthSession(provider) as session: # doctest: +SKIP
|
|
22
|
+
... resp = session.get("https://api.example.com/users/me")
|
|
23
|
+
|
|
24
|
+
Example (config-driven):
|
|
25
|
+
>>> # Configure in kstlib.conf.yml:
|
|
26
|
+
>>> # auth:
|
|
27
|
+
>>> # providers:
|
|
28
|
+
>>> # corporate:
|
|
29
|
+
>>> # type: oidc
|
|
30
|
+
>>> # issuer: https://idp.corp.local/realms/main
|
|
31
|
+
>>> # client_id: my-app
|
|
32
|
+
>>> # pkce: true
|
|
33
|
+
>>> from kstlib.auth import OIDCProvider, AuthSession
|
|
34
|
+
>>> provider = OIDCProvider.from_config("corporate") # doctest: +SKIP
|
|
35
|
+
>>> with AuthSession(provider) as session: # doctest: +SKIP
|
|
36
|
+
... resp = session.get("https://api.corp.local/users/me")
|
|
37
|
+
|
|
38
|
+
See Also:
|
|
39
|
+
- :mod:`kstlib.auth.config` for config loading helpers
|
|
40
|
+
- :mod:`kstlib.auth.models` for data models
|
|
41
|
+
- :mod:`kstlib.auth.errors` for exception hierarchy
|
|
42
|
+
- :mod:`kstlib.auth.providers` for provider implementations
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
from kstlib.auth.callback import CallbackResult, CallbackServer
|
|
48
|
+
from kstlib.auth.config import (
|
|
49
|
+
build_provider_config,
|
|
50
|
+
get_auth_config,
|
|
51
|
+
get_callback_server_config,
|
|
52
|
+
get_default_provider_name,
|
|
53
|
+
get_provider_config,
|
|
54
|
+
get_token_storage_from_config,
|
|
55
|
+
list_configured_providers,
|
|
56
|
+
)
|
|
57
|
+
from kstlib.auth.errors import (
|
|
58
|
+
AuthError,
|
|
59
|
+
AuthorizationError,
|
|
60
|
+
CallbackServerError,
|
|
61
|
+
ConfigurationError,
|
|
62
|
+
DiscoveryError,
|
|
63
|
+
PreflightError,
|
|
64
|
+
ProviderNotFoundError,
|
|
65
|
+
TokenError,
|
|
66
|
+
TokenExchangeError,
|
|
67
|
+
TokenExpiredError,
|
|
68
|
+
TokenRefreshError,
|
|
69
|
+
TokenStorageError,
|
|
70
|
+
TokenValidationError,
|
|
71
|
+
)
|
|
72
|
+
from kstlib.auth.models import (
|
|
73
|
+
AuthFlow,
|
|
74
|
+
PreflightReport,
|
|
75
|
+
PreflightResult,
|
|
76
|
+
PreflightStatus,
|
|
77
|
+
Token,
|
|
78
|
+
TokenType,
|
|
79
|
+
)
|
|
80
|
+
from kstlib.auth.providers import (
|
|
81
|
+
AbstractAuthProvider,
|
|
82
|
+
AuthProviderConfig,
|
|
83
|
+
OAuth2Provider,
|
|
84
|
+
OIDCProvider,
|
|
85
|
+
)
|
|
86
|
+
from kstlib.auth.session import AuthSession
|
|
87
|
+
from kstlib.auth.token import (
|
|
88
|
+
AbstractTokenStorage,
|
|
89
|
+
MemoryTokenStorage,
|
|
90
|
+
SOPSTokenStorage,
|
|
91
|
+
get_token_storage,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
__all__ = [
|
|
95
|
+
# Providers
|
|
96
|
+
"AbstractAuthProvider",
|
|
97
|
+
# Token storage
|
|
98
|
+
"AbstractTokenStorage",
|
|
99
|
+
# Errors
|
|
100
|
+
"AuthError",
|
|
101
|
+
# Models
|
|
102
|
+
"AuthFlow",
|
|
103
|
+
"AuthProviderConfig",
|
|
104
|
+
# Session
|
|
105
|
+
"AuthSession",
|
|
106
|
+
"AuthorizationError",
|
|
107
|
+
# Callback server
|
|
108
|
+
"CallbackResult",
|
|
109
|
+
"CallbackServer",
|
|
110
|
+
"CallbackServerError",
|
|
111
|
+
"ConfigurationError",
|
|
112
|
+
"DiscoveryError",
|
|
113
|
+
"MemoryTokenStorage",
|
|
114
|
+
"OAuth2Provider",
|
|
115
|
+
"OIDCProvider",
|
|
116
|
+
"PreflightError",
|
|
117
|
+
"PreflightReport",
|
|
118
|
+
"PreflightResult",
|
|
119
|
+
"PreflightStatus",
|
|
120
|
+
"ProviderNotFoundError",
|
|
121
|
+
"SOPSTokenStorage",
|
|
122
|
+
"Token",
|
|
123
|
+
"TokenError",
|
|
124
|
+
"TokenExchangeError",
|
|
125
|
+
"TokenExpiredError",
|
|
126
|
+
"TokenRefreshError",
|
|
127
|
+
"TokenStorageError",
|
|
128
|
+
"TokenType",
|
|
129
|
+
"TokenValidationError",
|
|
130
|
+
# Config helpers
|
|
131
|
+
"build_provider_config",
|
|
132
|
+
"get_auth_config",
|
|
133
|
+
"get_callback_server_config",
|
|
134
|
+
"get_default_provider_name",
|
|
135
|
+
"get_provider_config",
|
|
136
|
+
"get_token_storage",
|
|
137
|
+
"get_token_storage_from_config",
|
|
138
|
+
"list_configured_providers",
|
|
139
|
+
]
|