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.
Files changed (166) hide show
  1. kstlib/__init__.py +266 -1
  2. kstlib/__main__.py +16 -0
  3. kstlib/alerts/__init__.py +110 -0
  4. kstlib/alerts/channels/__init__.py +36 -0
  5. kstlib/alerts/channels/base.py +197 -0
  6. kstlib/alerts/channels/email.py +227 -0
  7. kstlib/alerts/channels/slack.py +389 -0
  8. kstlib/alerts/exceptions.py +72 -0
  9. kstlib/alerts/manager.py +651 -0
  10. kstlib/alerts/models.py +142 -0
  11. kstlib/alerts/throttle.py +263 -0
  12. kstlib/auth/__init__.py +139 -0
  13. kstlib/auth/callback.py +399 -0
  14. kstlib/auth/config.py +502 -0
  15. kstlib/auth/errors.py +127 -0
  16. kstlib/auth/models.py +316 -0
  17. kstlib/auth/providers/__init__.py +14 -0
  18. kstlib/auth/providers/base.py +393 -0
  19. kstlib/auth/providers/oauth2.py +645 -0
  20. kstlib/auth/providers/oidc.py +821 -0
  21. kstlib/auth/session.py +338 -0
  22. kstlib/auth/token.py +482 -0
  23. kstlib/cache/__init__.py +50 -0
  24. kstlib/cache/decorator.py +261 -0
  25. kstlib/cache/strategies.py +516 -0
  26. kstlib/cli/__init__.py +8 -0
  27. kstlib/cli/app.py +195 -0
  28. kstlib/cli/commands/__init__.py +5 -0
  29. kstlib/cli/commands/auth/__init__.py +39 -0
  30. kstlib/cli/commands/auth/common.py +122 -0
  31. kstlib/cli/commands/auth/login.py +325 -0
  32. kstlib/cli/commands/auth/logout.py +74 -0
  33. kstlib/cli/commands/auth/providers.py +57 -0
  34. kstlib/cli/commands/auth/status.py +291 -0
  35. kstlib/cli/commands/auth/token.py +199 -0
  36. kstlib/cli/commands/auth/whoami.py +106 -0
  37. kstlib/cli/commands/config.py +89 -0
  38. kstlib/cli/commands/ops/__init__.py +39 -0
  39. kstlib/cli/commands/ops/attach.py +49 -0
  40. kstlib/cli/commands/ops/common.py +269 -0
  41. kstlib/cli/commands/ops/list_sessions.py +252 -0
  42. kstlib/cli/commands/ops/logs.py +49 -0
  43. kstlib/cli/commands/ops/start.py +98 -0
  44. kstlib/cli/commands/ops/status.py +138 -0
  45. kstlib/cli/commands/ops/stop.py +60 -0
  46. kstlib/cli/commands/rapi/__init__.py +60 -0
  47. kstlib/cli/commands/rapi/call.py +341 -0
  48. kstlib/cli/commands/rapi/list.py +99 -0
  49. kstlib/cli/commands/rapi/show.py +206 -0
  50. kstlib/cli/commands/secrets/__init__.py +35 -0
  51. kstlib/cli/commands/secrets/common.py +425 -0
  52. kstlib/cli/commands/secrets/decrypt.py +88 -0
  53. kstlib/cli/commands/secrets/doctor.py +743 -0
  54. kstlib/cli/commands/secrets/encrypt.py +242 -0
  55. kstlib/cli/commands/secrets/shred.py +96 -0
  56. kstlib/cli/common.py +86 -0
  57. kstlib/config/__init__.py +76 -0
  58. kstlib/config/exceptions.py +110 -0
  59. kstlib/config/export.py +225 -0
  60. kstlib/config/loader.py +963 -0
  61. kstlib/config/sops.py +287 -0
  62. kstlib/db/__init__.py +54 -0
  63. kstlib/db/aiosqlcipher.py +137 -0
  64. kstlib/db/cipher.py +112 -0
  65. kstlib/db/database.py +367 -0
  66. kstlib/db/exceptions.py +25 -0
  67. kstlib/db/pool.py +302 -0
  68. kstlib/helpers/__init__.py +35 -0
  69. kstlib/helpers/exceptions.py +11 -0
  70. kstlib/helpers/time_trigger.py +396 -0
  71. kstlib/kstlib.conf.yml +890 -0
  72. kstlib/limits.py +963 -0
  73. kstlib/logging/__init__.py +108 -0
  74. kstlib/logging/manager.py +633 -0
  75. kstlib/mail/__init__.py +42 -0
  76. kstlib/mail/builder.py +626 -0
  77. kstlib/mail/exceptions.py +27 -0
  78. kstlib/mail/filesystem.py +248 -0
  79. kstlib/mail/transport.py +224 -0
  80. kstlib/mail/transports/__init__.py +19 -0
  81. kstlib/mail/transports/gmail.py +268 -0
  82. kstlib/mail/transports/resend.py +324 -0
  83. kstlib/mail/transports/smtp.py +326 -0
  84. kstlib/meta.py +72 -0
  85. kstlib/metrics/__init__.py +88 -0
  86. kstlib/metrics/decorators.py +1090 -0
  87. kstlib/metrics/exceptions.py +14 -0
  88. kstlib/monitoring/__init__.py +116 -0
  89. kstlib/monitoring/_styles.py +163 -0
  90. kstlib/monitoring/cell.py +57 -0
  91. kstlib/monitoring/config.py +424 -0
  92. kstlib/monitoring/delivery.py +579 -0
  93. kstlib/monitoring/exceptions.py +63 -0
  94. kstlib/monitoring/image.py +220 -0
  95. kstlib/monitoring/kv.py +79 -0
  96. kstlib/monitoring/list.py +69 -0
  97. kstlib/monitoring/metric.py +88 -0
  98. kstlib/monitoring/monitoring.py +341 -0
  99. kstlib/monitoring/renderer.py +139 -0
  100. kstlib/monitoring/service.py +392 -0
  101. kstlib/monitoring/table.py +129 -0
  102. kstlib/monitoring/types.py +56 -0
  103. kstlib/ops/__init__.py +86 -0
  104. kstlib/ops/base.py +148 -0
  105. kstlib/ops/container.py +577 -0
  106. kstlib/ops/exceptions.py +209 -0
  107. kstlib/ops/manager.py +407 -0
  108. kstlib/ops/models.py +176 -0
  109. kstlib/ops/tmux.py +372 -0
  110. kstlib/ops/validators.py +287 -0
  111. kstlib/py.typed +0 -0
  112. kstlib/rapi/__init__.py +118 -0
  113. kstlib/rapi/client.py +875 -0
  114. kstlib/rapi/config.py +861 -0
  115. kstlib/rapi/credentials.py +887 -0
  116. kstlib/rapi/exceptions.py +213 -0
  117. kstlib/resilience/__init__.py +101 -0
  118. kstlib/resilience/circuit_breaker.py +440 -0
  119. kstlib/resilience/exceptions.py +95 -0
  120. kstlib/resilience/heartbeat.py +491 -0
  121. kstlib/resilience/rate_limiter.py +506 -0
  122. kstlib/resilience/shutdown.py +417 -0
  123. kstlib/resilience/watchdog.py +637 -0
  124. kstlib/secrets/__init__.py +29 -0
  125. kstlib/secrets/exceptions.py +19 -0
  126. kstlib/secrets/models.py +62 -0
  127. kstlib/secrets/providers/__init__.py +79 -0
  128. kstlib/secrets/providers/base.py +58 -0
  129. kstlib/secrets/providers/environment.py +66 -0
  130. kstlib/secrets/providers/keyring.py +107 -0
  131. kstlib/secrets/providers/kms.py +223 -0
  132. kstlib/secrets/providers/kwargs.py +101 -0
  133. kstlib/secrets/providers/sops.py +209 -0
  134. kstlib/secrets/resolver.py +221 -0
  135. kstlib/secrets/sensitive.py +130 -0
  136. kstlib/secure/__init__.py +23 -0
  137. kstlib/secure/fs.py +194 -0
  138. kstlib/secure/permissions.py +70 -0
  139. kstlib/ssl.py +347 -0
  140. kstlib/ui/__init__.py +23 -0
  141. kstlib/ui/exceptions.py +26 -0
  142. kstlib/ui/panels.py +484 -0
  143. kstlib/ui/spinner.py +864 -0
  144. kstlib/ui/tables.py +382 -0
  145. kstlib/utils/__init__.py +48 -0
  146. kstlib/utils/dict.py +36 -0
  147. kstlib/utils/formatting.py +338 -0
  148. kstlib/utils/http_trace.py +237 -0
  149. kstlib/utils/lazy.py +49 -0
  150. kstlib/utils/secure_delete.py +205 -0
  151. kstlib/utils/serialization.py +247 -0
  152. kstlib/utils/text.py +56 -0
  153. kstlib/utils/validators.py +124 -0
  154. kstlib/websocket/__init__.py +97 -0
  155. kstlib/websocket/exceptions.py +214 -0
  156. kstlib/websocket/manager.py +1102 -0
  157. kstlib/websocket/models.py +361 -0
  158. kstlib-1.0.1.dist-info/METADATA +201 -0
  159. kstlib-1.0.1.dist-info/RECORD +163 -0
  160. {kstlib-0.0.1a0.dist-info → kstlib-1.0.1.dist-info}/WHEEL +1 -1
  161. kstlib-1.0.1.dist-info/entry_points.txt +2 -0
  162. kstlib-1.0.1.dist-info/licenses/LICENSE.md +9 -0
  163. kstlib-0.0.1a0.dist-info/METADATA +0 -29
  164. kstlib-0.0.1a0.dist-info/RECORD +0 -6
  165. kstlib-0.0.1a0.dist-info/licenses/LICENSE.md +0 -5
  166. {kstlib-0.0.1a0.dist-info → kstlib-1.0.1.dist-info}/top_level.txt +0 -0
kstlib/auth/models.py ADDED
@@ -0,0 +1,316 @@
1
+ """Data models for the authentication module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime, timedelta, timezone
7
+ from enum import Enum
8
+ from typing import Any
9
+
10
+
11
+ class AuthFlow(str, Enum):
12
+ """OAuth2/OIDC authentication flows supported by the module.
13
+
14
+ Attributes:
15
+ AUTHORIZATION_CODE: Standard OAuth2 Authorization Code flow.
16
+ AUTHORIZATION_CODE_PKCE: Authorization Code with PKCE extension (recommended).
17
+ CLIENT_CREDENTIALS: Machine-to-machine authentication (no user interaction).
18
+ DEVICE_CODE: For devices with limited input capabilities.
19
+ REFRESH_TOKEN: Token refresh flow (internal use).
20
+ """
21
+
22
+ AUTHORIZATION_CODE = "authorization_code"
23
+ AUTHORIZATION_CODE_PKCE = "authorization_code_pkce"
24
+ CLIENT_CREDENTIALS = "client_credentials"
25
+ DEVICE_CODE = "device_code"
26
+ REFRESH_TOKEN = "refresh_token"
27
+
28
+
29
+ class TokenType(str, Enum):
30
+ """Token type as returned by the authorization server."""
31
+
32
+ BEARER = "Bearer"
33
+ MAC = "MAC"
34
+ DPOP = "DPoP"
35
+
36
+
37
+ class PreflightStatus(str, Enum):
38
+ """Status of a preflight validation step."""
39
+
40
+ SUCCESS = "success"
41
+ FAILURE = "failure"
42
+ WARNING = "warning"
43
+ SKIPPED = "skipped"
44
+
45
+
46
+ @dataclass(slots=True)
47
+ class Token: # pylint: disable=too-many-instance-attributes
48
+ """Represents an OAuth2/OIDC token set.
49
+
50
+ Attributes:
51
+ access_token: The access token issued by the authorization server.
52
+ token_type: Token type (usually "Bearer").
53
+ expires_at: Absolute expiration time (UTC). None if unknown.
54
+ refresh_token: Optional refresh token for obtaining new access tokens.
55
+ scope: List of granted scopes.
56
+ id_token: OIDC ID token (JWT) containing user claims. None for pure OAuth2.
57
+ issued_at: When the token was issued (UTC).
58
+ metadata: Additional provider-specific data.
59
+
60
+ Example:
61
+ >>> from datetime import datetime, timezone
62
+ >>> token = Token(
63
+ ... access_token="eyJhbGc...",
64
+ ... expires_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
65
+ ... refresh_token="dGhpcyBpcyBh...",
66
+ ... scope=["openid", "profile"],
67
+ ... )
68
+ >>> token.is_expired
69
+ True
70
+ >>> token.is_refreshable
71
+ True
72
+ """
73
+
74
+ access_token: str
75
+ token_type: TokenType | str = TokenType.BEARER
76
+ expires_at: datetime | None = None
77
+ refresh_token: str | None = None
78
+ scope: list[str] = field(default_factory=list)
79
+ id_token: str | None = None
80
+ issued_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
81
+ metadata: dict[str, Any] = field(default_factory=dict)
82
+
83
+ @property
84
+ def is_expired(self) -> bool:
85
+ """Check if the access token has expired.
86
+
87
+ Returns:
88
+ True if expired or expiration is unknown and token is old (>1h).
89
+ """
90
+ if self.expires_at is None:
91
+ # Conservative: assume expired after 1 hour if no expiry info
92
+ return datetime.now(timezone.utc) > self.issued_at + timedelta(hours=1)
93
+ return datetime.now(timezone.utc) >= self.expires_at
94
+
95
+ @property
96
+ def is_refreshable(self) -> bool:
97
+ """Check if the token can be refreshed.
98
+
99
+ Returns:
100
+ True if a refresh_token is available.
101
+ """
102
+ return self.refresh_token is not None
103
+
104
+ @property
105
+ def expires_in(self) -> int | None:
106
+ """Seconds until expiration. None if unknown, negative if expired."""
107
+ if self.expires_at is None:
108
+ return None
109
+ delta = self.expires_at - datetime.now(timezone.utc)
110
+ return int(delta.total_seconds())
111
+
112
+ @property
113
+ def should_refresh(self) -> bool:
114
+ """Check if the token should be proactively refreshed.
115
+
116
+ Returns:
117
+ True if token expires within 60 seconds or is already expired.
118
+ """
119
+ if self.expires_at is None:
120
+ return self.is_expired
121
+ # Refresh 60 seconds before actual expiry
122
+ buffer = timedelta(seconds=60)
123
+ return datetime.now(timezone.utc) >= (self.expires_at - buffer)
124
+
125
+ @classmethod
126
+ def from_response(cls, data: dict[str, Any]) -> Token:
127
+ """Create a Token from an OAuth2 token response.
128
+
129
+ Args:
130
+ data: Raw token response from the authorization server.
131
+
132
+ Returns:
133
+ Token instance populated from the response.
134
+
135
+ Example:
136
+ >>> response = {
137
+ ... "access_token": "eyJhbGc...",
138
+ ... "token_type": "Bearer",
139
+ ... "expires_in": 3600,
140
+ ... "refresh_token": "dGhpcyBpcyBh...",
141
+ ... "scope": "openid profile",
142
+ ... "id_token": "eyJhbGc...",
143
+ ... }
144
+ >>> token = Token.from_response(response)
145
+ >>> token.scope
146
+ ['openid', 'profile']
147
+ """
148
+ now = datetime.now(timezone.utc)
149
+
150
+ # Parse expires_at from expires_in
151
+ expires_at = None
152
+ if "expires_in" in data:
153
+ expires_at = now + timedelta(seconds=int(data["expires_in"]))
154
+ elif "expires_at" in data:
155
+ # Some servers return absolute timestamp
156
+ expires_at = datetime.fromtimestamp(data["expires_at"], tz=timezone.utc)
157
+
158
+ # Parse scope (can be string or list)
159
+ scope_raw = data.get("scope", [])
160
+ scope = (scope_raw.split() if scope_raw else []) if isinstance(scope_raw, str) else list(scope_raw)
161
+
162
+ # Extract known fields, rest goes to metadata
163
+ known_fields = {
164
+ "access_token",
165
+ "token_type",
166
+ "expires_in",
167
+ "expires_at",
168
+ "refresh_token",
169
+ "scope",
170
+ "id_token",
171
+ }
172
+ metadata = {k: v for k, v in data.items() if k not in known_fields}
173
+
174
+ return cls(
175
+ access_token=data["access_token"],
176
+ token_type=data.get("token_type", TokenType.BEARER),
177
+ expires_at=expires_at,
178
+ refresh_token=data.get("refresh_token"),
179
+ scope=scope,
180
+ id_token=data.get("id_token"),
181
+ issued_at=now,
182
+ metadata=metadata,
183
+ )
184
+
185
+ def to_dict(self) -> dict[str, Any]:
186
+ """Serialize token to dictionary for storage.
187
+
188
+ Returns:
189
+ Dictionary representation suitable for JSON serialization.
190
+ """
191
+ return {
192
+ "access_token": self.access_token,
193
+ "token_type": str(self.token_type.value if isinstance(self.token_type, TokenType) else self.token_type),
194
+ "expires_at": self.expires_at.isoformat() if self.expires_at else None,
195
+ "refresh_token": self.refresh_token,
196
+ "scope": self.scope,
197
+ "id_token": self.id_token,
198
+ "issued_at": self.issued_at.isoformat(),
199
+ "metadata": self.metadata,
200
+ }
201
+
202
+ @classmethod
203
+ def from_dict(cls, data: dict[str, Any]) -> Token:
204
+ """Deserialize token from dictionary (storage retrieval).
205
+
206
+ Args:
207
+ data: Dictionary from to_dict() or storage.
208
+
209
+ Returns:
210
+ Token instance.
211
+ """
212
+ expires_at = None
213
+ if data.get("expires_at"):
214
+ expires_at = datetime.fromisoformat(data["expires_at"])
215
+
216
+ issued_at = datetime.now(timezone.utc)
217
+ if data.get("issued_at"):
218
+ issued_at = datetime.fromisoformat(data["issued_at"])
219
+
220
+ return cls(
221
+ access_token=data["access_token"],
222
+ token_type=data.get("token_type", TokenType.BEARER),
223
+ expires_at=expires_at,
224
+ refresh_token=data.get("refresh_token"),
225
+ scope=data.get("scope", []),
226
+ id_token=data.get("id_token"),
227
+ issued_at=issued_at,
228
+ metadata=data.get("metadata", {}),
229
+ )
230
+
231
+
232
+ @dataclass(slots=True)
233
+ class PreflightResult:
234
+ """Result of a single preflight validation step.
235
+
236
+ Attributes:
237
+ step: Name/identifier of the validation step.
238
+ status: Outcome of the step (success, failure, warning, skipped).
239
+ message: Human-readable description of the result.
240
+ details: Optional additional information (URLs checked, errors, etc.).
241
+ duration_ms: Time taken for this step in milliseconds.
242
+
243
+ Example:
244
+ >>> result = PreflightResult(
245
+ ... step="discovery",
246
+ ... status=PreflightStatus.SUCCESS,
247
+ ... message="Discovery document fetched successfully",
248
+ ... details={"issuer": "https://idp.example.com", "endpoints": 5},
249
+ ... duration_ms=234,
250
+ ... )
251
+ >>> result.success
252
+ True
253
+ """
254
+
255
+ step: str
256
+ status: PreflightStatus
257
+ message: str
258
+ details: dict[str, Any] = field(default_factory=dict)
259
+ duration_ms: int | None = None
260
+
261
+ @property
262
+ def success(self) -> bool:
263
+ """Check if step passed (success or warning)."""
264
+ return self.status in (PreflightStatus.SUCCESS, PreflightStatus.WARNING)
265
+
266
+ @property
267
+ def failed(self) -> bool:
268
+ """Check if step failed."""
269
+ return self.status == PreflightStatus.FAILURE
270
+
271
+
272
+ @dataclass(slots=True)
273
+ class PreflightReport:
274
+ """Aggregated results from a complete preflight check.
275
+
276
+ Attributes:
277
+ provider_name: Name of the provider being validated.
278
+ results: List of individual step results.
279
+ started_at: When the preflight started.
280
+ completed_at: When the preflight finished.
281
+ """
282
+
283
+ provider_name: str
284
+ results: list[PreflightResult] = field(default_factory=list)
285
+ started_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
286
+ completed_at: datetime | None = None
287
+
288
+ @property
289
+ def success(self) -> bool:
290
+ """Check if all steps passed (no failures)."""
291
+ return all(not r.failed for r in self.results)
292
+
293
+ @property
294
+ def total_duration_ms(self) -> int:
295
+ """Total time for all steps in milliseconds."""
296
+ return sum(r.duration_ms or 0 for r in self.results)
297
+
298
+ @property
299
+ def failed_steps(self) -> list[PreflightResult]:
300
+ """List of failed steps."""
301
+ return [r for r in self.results if r.failed]
302
+
303
+ @property
304
+ def warnings(self) -> list[PreflightResult]:
305
+ """List of steps with warnings."""
306
+ return [r for r in self.results if r.status == PreflightStatus.WARNING]
307
+
308
+
309
+ __all__ = [
310
+ "AuthFlow",
311
+ "PreflightReport",
312
+ "PreflightResult",
313
+ "PreflightStatus",
314
+ "Token",
315
+ "TokenType",
316
+ ]
@@ -0,0 +1,14 @@
1
+ """Authentication providers for OAuth2/OIDC."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from kstlib.auth.providers.base import AbstractAuthProvider, AuthProviderConfig
6
+ from kstlib.auth.providers.oauth2 import OAuth2Provider
7
+ from kstlib.auth.providers.oidc import OIDCProvider
8
+
9
+ __all__ = [
10
+ "AbstractAuthProvider",
11
+ "AuthProviderConfig",
12
+ "OAuth2Provider",
13
+ "OIDCProvider",
14
+ ]