kstlib 0.0.1a0__py3-none-any.whl → 1.0.0__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.0.dist-info/METADATA +201 -0
- kstlib-1.0.0.dist-info/RECORD +163 -0
- {kstlib-0.0.1a0.dist-info → kstlib-1.0.0.dist-info}/WHEEL +1 -1
- kstlib-1.0.0.dist-info/entry_points.txt +2 -0
- kstlib-1.0.0.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.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Show user info from OIDC userinfo endpoint."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=too-many-locals,too-many-branches
|
|
4
|
+
# Justification: OIDC userinfo renderer with 3 output modes (raw/quiet/verbose) and
|
|
5
|
+
# claim mapping for 12 standard OIDC claims. Variables are mostly display labels and
|
|
6
|
+
# formatted values - extracting helpers would fragment the linear rendering logic.
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from kstlib.cli.common import console, exit_error
|
|
15
|
+
from kstlib.utils.formatting import format_timestamp
|
|
16
|
+
from kstlib.utils.serialization import to_json
|
|
17
|
+
|
|
18
|
+
from .common import PROVIDER_ARGUMENT, QUIET_OPTION, get_provider, resolve_provider_name
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def whoami(
|
|
22
|
+
provider: str | None = PROVIDER_ARGUMENT,
|
|
23
|
+
quiet: bool = QUIET_OPTION,
|
|
24
|
+
raw: bool = typer.Option(
|
|
25
|
+
False,
|
|
26
|
+
"--raw",
|
|
27
|
+
help="Output raw JSON response.",
|
|
28
|
+
),
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Show user info from the OIDC userinfo endpoint.
|
|
31
|
+
|
|
32
|
+
Only works with OIDC providers that expose a userinfo endpoint.
|
|
33
|
+
"""
|
|
34
|
+
provider_name = resolve_provider_name(provider)
|
|
35
|
+
auth_provider = get_provider(provider_name)
|
|
36
|
+
|
|
37
|
+
# Check if authenticated
|
|
38
|
+
token = auth_provider.get_token(auto_refresh=True)
|
|
39
|
+
if token is None:
|
|
40
|
+
exit_error(f"Not authenticated with {provider_name}.\nRun 'kstlib auth login {provider_name}' first.")
|
|
41
|
+
|
|
42
|
+
# Check if provider supports userinfo (OIDC)
|
|
43
|
+
if not hasattr(auth_provider, "get_userinfo"):
|
|
44
|
+
exit_error(
|
|
45
|
+
f"Provider '{provider_name}' does not support userinfo.\nUserinfo is only available for OIDC providers."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
userinfo = auth_provider.get_userinfo()
|
|
50
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
51
|
+
exit_error(f"Failed to fetch userinfo: {e}")
|
|
52
|
+
|
|
53
|
+
if raw:
|
|
54
|
+
print(to_json(userinfo))
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
if quiet:
|
|
58
|
+
# Show just the essential info
|
|
59
|
+
name = userinfo.get("name") or userinfo.get("preferred_username") or userinfo.get("sub", "unknown")
|
|
60
|
+
email = userinfo.get("email", "")
|
|
61
|
+
if email:
|
|
62
|
+
console.print(f"{name} <{email}>")
|
|
63
|
+
else:
|
|
64
|
+
console.print(name)
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# Verbose output
|
|
68
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
69
|
+
table.add_column("Field", style="dim")
|
|
70
|
+
table.add_column("Value")
|
|
71
|
+
|
|
72
|
+
# Standard OIDC claims in preferred order
|
|
73
|
+
claim_labels = {
|
|
74
|
+
"sub": "Subject",
|
|
75
|
+
"name": "Name",
|
|
76
|
+
"preferred_username": "Username",
|
|
77
|
+
"email": "Email",
|
|
78
|
+
"email_verified": "Email Verified",
|
|
79
|
+
"given_name": "Given Name",
|
|
80
|
+
"family_name": "Family Name",
|
|
81
|
+
"nickname": "Nickname",
|
|
82
|
+
"picture": "Picture",
|
|
83
|
+
"locale": "Locale",
|
|
84
|
+
"zoneinfo": "Timezone",
|
|
85
|
+
"updated_at": "Updated At",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Show known claims first
|
|
89
|
+
for claim, label in claim_labels.items():
|
|
90
|
+
if claim in userinfo:
|
|
91
|
+
value = userinfo[claim]
|
|
92
|
+
if isinstance(value, bool):
|
|
93
|
+
value = "[green]Yes[/]" if value else "[red]No[/]"
|
|
94
|
+
elif claim == "updated_at" and isinstance(value, int):
|
|
95
|
+
value = format_timestamp(value)
|
|
96
|
+
table.add_row(label, str(value))
|
|
97
|
+
|
|
98
|
+
# Show any additional claims
|
|
99
|
+
for claim, value in userinfo.items():
|
|
100
|
+
if claim not in claim_labels:
|
|
101
|
+
table.add_row(claim, str(value))
|
|
102
|
+
|
|
103
|
+
console.print(Panel(table, title=f"User Info ({provider_name})", style="cyan"))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
__all__ = ["whoami"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""CLI commands for configuration management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pathlib
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
|
|
11
|
+
from kstlib.cli.common import console
|
|
12
|
+
from kstlib.config.export import (
|
|
13
|
+
ConfigExportError,
|
|
14
|
+
ConfigExportOptions,
|
|
15
|
+
export_configuration,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
config_app = typer.Typer(help="Configuration utilities.")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@config_app.command("export", help="Export the default configuration file.")
|
|
22
|
+
def export_command(
|
|
23
|
+
section: Annotated[
|
|
24
|
+
str | None,
|
|
25
|
+
typer.Option(
|
|
26
|
+
"--section",
|
|
27
|
+
help="Optional dotted path selecting a subtree (e.g. utilities.secure_delete).",
|
|
28
|
+
),
|
|
29
|
+
] = None,
|
|
30
|
+
out: Annotated[
|
|
31
|
+
pathlib.Path | None,
|
|
32
|
+
typer.Option(
|
|
33
|
+
"--out",
|
|
34
|
+
help=(
|
|
35
|
+
"Destination file or directory. Defaults to ./kstlib.conf.yml when omitted. "
|
|
36
|
+
"When a directory is provided, the default filename is used."
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
] = None,
|
|
40
|
+
stdout: Annotated[
|
|
41
|
+
bool,
|
|
42
|
+
typer.Option(
|
|
43
|
+
"--stdout",
|
|
44
|
+
help="Write configuration to stdout instead of a file.",
|
|
45
|
+
),
|
|
46
|
+
] = False,
|
|
47
|
+
force: Annotated[
|
|
48
|
+
bool,
|
|
49
|
+
typer.Option(
|
|
50
|
+
"--force",
|
|
51
|
+
help="Overwrite destination if it already exists.",
|
|
52
|
+
),
|
|
53
|
+
] = False,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Export default configuration to a file or stdout."""
|
|
56
|
+
resolved_out = out.expanduser() if isinstance(out, pathlib.Path) else out
|
|
57
|
+
options = ConfigExportOptions(section=section, out_path=resolved_out, stdout=stdout, force=force)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
result = export_configuration(options)
|
|
61
|
+
except ConfigExportError as exc:
|
|
62
|
+
console.print(f"[bold red]{exc}[/bold red]")
|
|
63
|
+
raise typer.Exit(code=1) from exc
|
|
64
|
+
|
|
65
|
+
if stdout:
|
|
66
|
+
if result.content is None:
|
|
67
|
+
console.print("[bold red]Export failed: empty content.[/bold red]")
|
|
68
|
+
raise typer.Exit(code=1)
|
|
69
|
+
console.print(result.content)
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
if result.destination is None:
|
|
73
|
+
console.print("[bold red]Export failed: missing destination file.[/bold red]")
|
|
74
|
+
raise typer.Exit(code=1)
|
|
75
|
+
console.print(
|
|
76
|
+
Panel.fit(
|
|
77
|
+
f"Configuration exported to [bold]{result.destination}[/bold]",
|
|
78
|
+
title="kstlib config export",
|
|
79
|
+
border_style="green",
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def register_cli(app: typer.Typer) -> None:
|
|
85
|
+
"""Register configuration subcommands on the main CLI application."""
|
|
86
|
+
app.add_typer(config_app, name="config")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
__all__ = ["config_app", "register_cli"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""CLI commands for session management (tmux/container)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from .attach import attach
|
|
8
|
+
from .list_sessions import list_sessions
|
|
9
|
+
from .logs import logs
|
|
10
|
+
from .start import start
|
|
11
|
+
from .status import status
|
|
12
|
+
from .stop import stop
|
|
13
|
+
|
|
14
|
+
ops_app = typer.Typer(help="Manage sessions (tmux/container).")
|
|
15
|
+
|
|
16
|
+
# Register commands on the ops_app
|
|
17
|
+
ops_app.command()(start)
|
|
18
|
+
ops_app.command()(stop)
|
|
19
|
+
ops_app.command()(attach)
|
|
20
|
+
ops_app.command()(status)
|
|
21
|
+
ops_app.command()(logs)
|
|
22
|
+
ops_app.command(name="list")(list_sessions)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def register_cli(app: typer.Typer) -> None:
|
|
26
|
+
"""Register the ops sub-commands on the root Typer app."""
|
|
27
|
+
app.add_typer(ops_app, name="ops")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"attach",
|
|
32
|
+
"list_sessions",
|
|
33
|
+
"logs",
|
|
34
|
+
"ops_app",
|
|
35
|
+
"register_cli",
|
|
36
|
+
"start",
|
|
37
|
+
"status",
|
|
38
|
+
"stop",
|
|
39
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Attach to a session."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from kstlib.cli.common import exit_error
|
|
6
|
+
from kstlib.ops.exceptions import OpsError, SessionNotFoundError
|
|
7
|
+
|
|
8
|
+
from .common import (
|
|
9
|
+
BACKEND_OPTION,
|
|
10
|
+
SESSION_ARGUMENT,
|
|
11
|
+
get_session_manager,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def attach(
|
|
16
|
+
name: str = SESSION_ARGUMENT,
|
|
17
|
+
backend: str | None = BACKEND_OPTION,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Attach to a running session.
|
|
20
|
+
|
|
21
|
+
Attaches the current terminal to a tmux session or container.
|
|
22
|
+
This command replaces the current process.
|
|
23
|
+
|
|
24
|
+
For tmux: Use Ctrl+B D to detach.
|
|
25
|
+
For container: Use Ctrl+P Ctrl+Q to detach.
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
kstlib ops attach dev
|
|
29
|
+
kstlib ops attach prod --backend container
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
manager = get_session_manager(name, backend=backend)
|
|
33
|
+
|
|
34
|
+
if not manager.exists():
|
|
35
|
+
exit_error(f"Session '{name}' not found.")
|
|
36
|
+
|
|
37
|
+
if not manager.is_running():
|
|
38
|
+
exit_error(f"Session '{name}' is not running.")
|
|
39
|
+
|
|
40
|
+
# This replaces the current process and does not return
|
|
41
|
+
manager.attach()
|
|
42
|
+
|
|
43
|
+
except SessionNotFoundError:
|
|
44
|
+
exit_error(f"Session '{name}' not found.")
|
|
45
|
+
except OpsError as e:
|
|
46
|
+
exit_error(str(e))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ["attach"]
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Shared options and utilities for ops CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from kstlib.cli.common import CommandResult, CommandStatus, exit_error
|
|
10
|
+
from kstlib.ops import SessionManager
|
|
11
|
+
from kstlib.ops.exceptions import OpsError, SessionAmbiguousError
|
|
12
|
+
from kstlib.ops.manager import auto_detect_backend
|
|
13
|
+
|
|
14
|
+
# ============================================================================
|
|
15
|
+
# Shared Arguments and Options
|
|
16
|
+
# ============================================================================
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
SESSION_ARGUMENT = typer.Argument(
|
|
20
|
+
...,
|
|
21
|
+
help="Session name.",
|
|
22
|
+
metavar="NAME",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
BACKEND_OPTION = typer.Option(
|
|
26
|
+
None,
|
|
27
|
+
"--backend",
|
|
28
|
+
"-b",
|
|
29
|
+
help="Backend type (tmux or container). If not specified, uses config default.",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
IMAGE_OPTION = typer.Option(
|
|
33
|
+
None,
|
|
34
|
+
"--image",
|
|
35
|
+
"-i",
|
|
36
|
+
help="Container image (container backend only).",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
COMMAND_OPTION = typer.Option(
|
|
40
|
+
None,
|
|
41
|
+
"--command",
|
|
42
|
+
"-c",
|
|
43
|
+
help="Command to run in the session.",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
QUIET_OPTION = typer.Option(
|
|
47
|
+
False,
|
|
48
|
+
"--quiet",
|
|
49
|
+
"-q",
|
|
50
|
+
help="Minimal output.",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
JSON_OPTION = typer.Option(
|
|
54
|
+
False,
|
|
55
|
+
"--json",
|
|
56
|
+
help="Output in JSON format.",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
WORKDIR_OPTION = typer.Option(
|
|
60
|
+
None,
|
|
61
|
+
"--workdir",
|
|
62
|
+
"-w",
|
|
63
|
+
help="Working directory for the session.",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
ENV_OPTION = typer.Option(
|
|
67
|
+
None,
|
|
68
|
+
"--env",
|
|
69
|
+
"-e",
|
|
70
|
+
help="Environment variable (KEY=VALUE). Can be repeated.",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
VOLUME_OPTION = typer.Option(
|
|
74
|
+
None,
|
|
75
|
+
"--volume",
|
|
76
|
+
"-v",
|
|
77
|
+
help="Volume mount (host:container). Can be repeated.",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
PORT_OPTION = typer.Option(
|
|
81
|
+
None,
|
|
82
|
+
"--port",
|
|
83
|
+
"-p",
|
|
84
|
+
help="Port mapping (host:container). Can be repeated.",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
FORCE_OPTION = typer.Option(
|
|
88
|
+
False,
|
|
89
|
+
"--force",
|
|
90
|
+
"-f",
|
|
91
|
+
help="Force stop without graceful shutdown.",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
TIMEOUT_OPTION = typer.Option(
|
|
95
|
+
10,
|
|
96
|
+
"--timeout",
|
|
97
|
+
"-t",
|
|
98
|
+
help="Seconds to wait for graceful shutdown.",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
LINES_OPTION = typer.Option(
|
|
102
|
+
100,
|
|
103
|
+
"--lines",
|
|
104
|
+
"-n",
|
|
105
|
+
help="Number of lines to show.",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ============================================================================
|
|
110
|
+
# Helper Functions
|
|
111
|
+
# ============================================================================
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _rebuild_with_overrides(
|
|
115
|
+
name: str,
|
|
116
|
+
manager: SessionManager,
|
|
117
|
+
*,
|
|
118
|
+
backend: str | None,
|
|
119
|
+
image: str | None,
|
|
120
|
+
command: str | None,
|
|
121
|
+
) -> SessionManager:
|
|
122
|
+
"""Rebuild a config-loaded SessionManager with CLI overrides.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
name: Session name.
|
|
126
|
+
manager: Original config-loaded manager.
|
|
127
|
+
backend: CLI backend override (or None).
|
|
128
|
+
image: CLI image override (or None).
|
|
129
|
+
command: CLI command override (or None).
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
New SessionManager with overrides applied.
|
|
133
|
+
"""
|
|
134
|
+
cfg = manager.config
|
|
135
|
+
kwargs: dict[str, Any] = {
|
|
136
|
+
"backend": backend or cfg.backend.value,
|
|
137
|
+
"command": command or cfg.command,
|
|
138
|
+
"working_dir": cfg.working_dir,
|
|
139
|
+
"env": cfg.env,
|
|
140
|
+
"image": image or cfg.image,
|
|
141
|
+
"volumes": list(cfg.volumes),
|
|
142
|
+
"ports": list(cfg.ports),
|
|
143
|
+
"runtime": cfg.runtime,
|
|
144
|
+
"log_volume": cfg.log_volume,
|
|
145
|
+
}
|
|
146
|
+
return SessionManager(name, **kwargs)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _try_from_config(
|
|
150
|
+
name: str,
|
|
151
|
+
*,
|
|
152
|
+
backend: str | None,
|
|
153
|
+
image: str | None,
|
|
154
|
+
command: str | None,
|
|
155
|
+
) -> SessionManager | None:
|
|
156
|
+
"""Try to load a SessionManager from config, with CLI overrides.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
name: Session name.
|
|
160
|
+
backend: CLI backend override (or None).
|
|
161
|
+
image: CLI image override (or None).
|
|
162
|
+
command: CLI command override (or None).
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
SessionManager if found in config, None otherwise.
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
manager = SessionManager.from_config(name)
|
|
169
|
+
except OpsError:
|
|
170
|
+
return None
|
|
171
|
+
if backend or image or command:
|
|
172
|
+
return _rebuild_with_overrides(
|
|
173
|
+
name,
|
|
174
|
+
manager,
|
|
175
|
+
backend=backend,
|
|
176
|
+
image=image,
|
|
177
|
+
command=command,
|
|
178
|
+
)
|
|
179
|
+
return manager
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def get_session_manager(
|
|
183
|
+
name: str,
|
|
184
|
+
*,
|
|
185
|
+
backend: str | None = None,
|
|
186
|
+
image: str | None = None,
|
|
187
|
+
command: str | None = None,
|
|
188
|
+
from_config: bool = True,
|
|
189
|
+
) -> SessionManager:
|
|
190
|
+
"""Get or create a SessionManager.
|
|
191
|
+
|
|
192
|
+
Tries to load session configuration from kstlib.conf.yml first,
|
|
193
|
+
then applies CLI arguments as overrides. Falls back to auto-detection
|
|
194
|
+
or explicit options if the session is not defined in config.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
name: Session name.
|
|
198
|
+
backend: Override backend type.
|
|
199
|
+
image: Container image (for container backend).
|
|
200
|
+
command: Command to run.
|
|
201
|
+
from_config: If True, try to load from config first.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
SessionManager instance.
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
typer.Exit: On configuration error or ambiguous session.
|
|
208
|
+
"""
|
|
209
|
+
try:
|
|
210
|
+
# Try to load from config first (CLI args override config values)
|
|
211
|
+
if from_config:
|
|
212
|
+
manager = _try_from_config(name, backend=backend, image=image, command=command)
|
|
213
|
+
if manager is not None:
|
|
214
|
+
return manager
|
|
215
|
+
|
|
216
|
+
# Auto-detect backend if not specified
|
|
217
|
+
if backend is None:
|
|
218
|
+
detected = auto_detect_backend(name)
|
|
219
|
+
if detected is not None:
|
|
220
|
+
backend = detected.value
|
|
221
|
+
|
|
222
|
+
# Create with explicit options
|
|
223
|
+
kwargs: dict[str, Any] = {}
|
|
224
|
+
if backend:
|
|
225
|
+
kwargs["backend"] = backend
|
|
226
|
+
if image:
|
|
227
|
+
kwargs["image"] = image
|
|
228
|
+
if command:
|
|
229
|
+
kwargs["command"] = command
|
|
230
|
+
|
|
231
|
+
return SessionManager(name, **kwargs)
|
|
232
|
+
except SessionAmbiguousError as e:
|
|
233
|
+
exit_error(str(e))
|
|
234
|
+
except OpsError as e:
|
|
235
|
+
exit_error(str(e))
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def handle_ops_error(e: OpsError) -> CommandResult:
|
|
239
|
+
"""Convert an OpsError to a CommandResult.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
e: The OpsError exception.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
CommandResult with error status.
|
|
246
|
+
"""
|
|
247
|
+
return CommandResult(
|
|
248
|
+
status=CommandStatus.ERROR,
|
|
249
|
+
message=str(e),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
__all__ = [
|
|
254
|
+
"BACKEND_OPTION",
|
|
255
|
+
"COMMAND_OPTION",
|
|
256
|
+
"ENV_OPTION",
|
|
257
|
+
"FORCE_OPTION",
|
|
258
|
+
"IMAGE_OPTION",
|
|
259
|
+
"JSON_OPTION",
|
|
260
|
+
"LINES_OPTION",
|
|
261
|
+
"PORT_OPTION",
|
|
262
|
+
"QUIET_OPTION",
|
|
263
|
+
"SESSION_ARGUMENT",
|
|
264
|
+
"TIMEOUT_OPTION",
|
|
265
|
+
"VOLUME_OPTION",
|
|
266
|
+
"WORKDIR_OPTION",
|
|
267
|
+
"get_session_manager",
|
|
268
|
+
"handle_ops_error",
|
|
269
|
+
]
|