pragmatiks-cli 0.5.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.
- pragma_cli/__init__.py +32 -0
- pragma_cli/commands/__init__.py +1 -0
- pragma_cli/commands/auth.py +247 -0
- pragma_cli/commands/completions.py +54 -0
- pragma_cli/commands/config.py +79 -0
- pragma_cli/commands/dead_letter.py +233 -0
- pragma_cli/commands/ops.py +14 -0
- pragma_cli/commands/provider.py +1165 -0
- pragma_cli/commands/resources.py +199 -0
- pragma_cli/config.py +86 -0
- pragma_cli/helpers.py +21 -0
- pragma_cli/main.py +67 -0
- pragma_cli/py.typed +0 -0
- pragmatiks_cli-0.5.1.dist-info/METADATA +199 -0
- pragmatiks_cli-0.5.1.dist-info/RECORD +17 -0
- pragmatiks_cli-0.5.1.dist-info/WHEEL +4 -0
- pragmatiks_cli-0.5.1.dist-info/entry_points.txt +3 -0
pragma_cli/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""CLI global client management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pragma_sdk import PragmaClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_client: PragmaClient | None = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_client() -> PragmaClient:
|
|
12
|
+
"""Get the initialized client.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Initialized PragmaClient instance.
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
RuntimeError: If client has not been initialized via main().
|
|
19
|
+
"""
|
|
20
|
+
if _client is None:
|
|
21
|
+
raise RuntimeError("Client not initialized. This should not happen.")
|
|
22
|
+
return _client
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def set_client(client: PragmaClient):
|
|
26
|
+
"""Set the global client instance.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
client: The PragmaClient instance to use globally
|
|
30
|
+
"""
|
|
31
|
+
global _client
|
|
32
|
+
_client = client
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Commands package."""
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Authentication commands for browser-based Clerk login."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import webbrowser
|
|
5
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
6
|
+
from urllib.parse import parse_qs, urlparse
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from pragma_sdk.config import load_credentials
|
|
10
|
+
from rich import print
|
|
11
|
+
|
|
12
|
+
from pragma_cli.config import CREDENTIALS_FILE, get_current_context, load_config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
app = typer.Typer()
|
|
16
|
+
|
|
17
|
+
CALLBACK_PORT = int(os.getenv("PRAGMA_AUTH_CALLBACK_PORT", "8765"))
|
|
18
|
+
CALLBACK_PATH = os.getenv("PRAGMA_AUTH_CALLBACK_PATH", "/auth/callback")
|
|
19
|
+
CLERK_FRONTEND_URL = os.getenv("PRAGMA_CLERK_FRONTEND_URL", "https://app.pragmatiks.io")
|
|
20
|
+
CALLBACK_URL = f"http://localhost:{CALLBACK_PORT}{CALLBACK_PATH}"
|
|
21
|
+
LOGIN_URL = f"{CLERK_FRONTEND_URL}/auth/callback?callback={CALLBACK_URL}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CallbackHandler(BaseHTTPRequestHandler):
|
|
25
|
+
"""HTTP handler for OAuth callback from Clerk."""
|
|
26
|
+
|
|
27
|
+
token = None
|
|
28
|
+
|
|
29
|
+
def do_GET(self):
|
|
30
|
+
"""Handle GET request from Clerk redirect."""
|
|
31
|
+
parsed = urlparse(self.path)
|
|
32
|
+
|
|
33
|
+
if parsed.path == CALLBACK_PATH:
|
|
34
|
+
params = parse_qs(parsed.query)
|
|
35
|
+
token = params.get("token", [None])[0]
|
|
36
|
+
|
|
37
|
+
if token:
|
|
38
|
+
CallbackHandler.token = token
|
|
39
|
+
|
|
40
|
+
self.send_response(200)
|
|
41
|
+
self.send_header("Content-type", "text/html")
|
|
42
|
+
self.end_headers()
|
|
43
|
+
self.wfile.write(
|
|
44
|
+
b"""
|
|
45
|
+
<html>
|
|
46
|
+
<body style="font-family: sans-serif; text-align: center; padding: 50px;">
|
|
47
|
+
<h1 style="color: green;">✓ Authentication Successful</h1>
|
|
48
|
+
<p>You can close this window and return to the terminal.</p>
|
|
49
|
+
</body>
|
|
50
|
+
</html>
|
|
51
|
+
"""
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
self.send_response(400)
|
|
55
|
+
self.send_header("Content-type", "text/html")
|
|
56
|
+
self.end_headers()
|
|
57
|
+
self.wfile.write(
|
|
58
|
+
b"""
|
|
59
|
+
<html>
|
|
60
|
+
<body style="font-family: sans-serif; text-align: center; padding: 50px;">
|
|
61
|
+
<h1 style="color: red;">✗ Authentication Failed</h1>
|
|
62
|
+
<p>No token received. Please try again.</p>
|
|
63
|
+
</body>
|
|
64
|
+
</html>
|
|
65
|
+
"""
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
self.send_response(404)
|
|
69
|
+
self.end_headers()
|
|
70
|
+
|
|
71
|
+
def log_message(self, format, *args):
|
|
72
|
+
"""Suppress server logs."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def save_credentials(token: str, context_name: str = "default"):
|
|
76
|
+
"""Save authentication token to local credentials file.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
token: JWT token from Clerk
|
|
80
|
+
context_name: Context to associate with this token
|
|
81
|
+
"""
|
|
82
|
+
CREDENTIALS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
credentials = {}
|
|
85
|
+
if CREDENTIALS_FILE.exists():
|
|
86
|
+
with open(CREDENTIALS_FILE) as f:
|
|
87
|
+
for line in f:
|
|
88
|
+
line = line.strip()
|
|
89
|
+
if not line or line.startswith("#"):
|
|
90
|
+
continue
|
|
91
|
+
if "=" in line:
|
|
92
|
+
key, value = line.split("=", 1)
|
|
93
|
+
credentials[key.strip()] = value.strip()
|
|
94
|
+
|
|
95
|
+
credentials[context_name] = token
|
|
96
|
+
|
|
97
|
+
with open(CREDENTIALS_FILE, "w") as f:
|
|
98
|
+
for key, value in credentials.items():
|
|
99
|
+
f.write(f"{key}={value}\n")
|
|
100
|
+
|
|
101
|
+
CREDENTIALS_FILE.chmod(0o600)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def clear_credentials(context_name: str | None = None):
|
|
105
|
+
"""Clear stored credentials.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
context_name: Specific context to clear, or None for all
|
|
109
|
+
"""
|
|
110
|
+
if not CREDENTIALS_FILE.exists():
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
if context_name is None:
|
|
114
|
+
CREDENTIALS_FILE.unlink()
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
credentials = {}
|
|
118
|
+
with open(CREDENTIALS_FILE) as f:
|
|
119
|
+
for line in f:
|
|
120
|
+
line = line.strip()
|
|
121
|
+
if not line or line.startswith("#"):
|
|
122
|
+
continue
|
|
123
|
+
if "=" in line:
|
|
124
|
+
key, value = line.split("=", 1)
|
|
125
|
+
credentials[key.strip()] = value.strip()
|
|
126
|
+
|
|
127
|
+
credentials.pop(context_name, None)
|
|
128
|
+
|
|
129
|
+
if credentials:
|
|
130
|
+
with open(CREDENTIALS_FILE, "w") as f:
|
|
131
|
+
for key, value in credentials.items():
|
|
132
|
+
f.write(f"{key}={value}\n")
|
|
133
|
+
CREDENTIALS_FILE.chmod(0o600)
|
|
134
|
+
else:
|
|
135
|
+
CREDENTIALS_FILE.unlink()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@app.command()
|
|
139
|
+
def login(context: str = typer.Option("default", help="Context to authenticate for")):
|
|
140
|
+
"""Authenticate with Pragma using browser-based Clerk login.
|
|
141
|
+
|
|
142
|
+
Opens your default browser to Clerk authentication page. After successful
|
|
143
|
+
login, your credentials are stored locally in ~/.config/pragma/credentials.
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
pragma login
|
|
147
|
+
pragma login --context production
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
typer.Exit: If context not found or authentication fails/times out.
|
|
151
|
+
"""
|
|
152
|
+
config = load_config()
|
|
153
|
+
if context not in config.contexts:
|
|
154
|
+
print(f"[red]\u2717[/red] Context '{context}' not found")
|
|
155
|
+
print(f"Available contexts: {', '.join(config.contexts.keys())}")
|
|
156
|
+
raise typer.Exit(1)
|
|
157
|
+
|
|
158
|
+
api_url = config.contexts[context].api_url
|
|
159
|
+
|
|
160
|
+
print(f"[cyan]Authenticating for context:[/cyan] {context}")
|
|
161
|
+
print(f"[cyan]API URL:[/cyan] {api_url}")
|
|
162
|
+
print()
|
|
163
|
+
|
|
164
|
+
server = HTTPServer(("localhost", CALLBACK_PORT), CallbackHandler)
|
|
165
|
+
|
|
166
|
+
print(f"[yellow]Opening browser to:[/yellow] {CLERK_FRONTEND_URL}")
|
|
167
|
+
print()
|
|
168
|
+
print("[dim]If browser doesn't open automatically, visit:[/dim]")
|
|
169
|
+
print(f"[dim]{LOGIN_URL}[/dim]")
|
|
170
|
+
print()
|
|
171
|
+
print("[yellow]Waiting for authentication...[/yellow]")
|
|
172
|
+
|
|
173
|
+
webbrowser.open(LOGIN_URL)
|
|
174
|
+
|
|
175
|
+
server.timeout = 300
|
|
176
|
+
server.handle_request()
|
|
177
|
+
|
|
178
|
+
if CallbackHandler.token:
|
|
179
|
+
save_credentials(CallbackHandler.token, context)
|
|
180
|
+
print()
|
|
181
|
+
print("[green]\u2713 Successfully authenticated![/green]")
|
|
182
|
+
print(f"[dim]Credentials saved to {CREDENTIALS_FILE}[/dim]")
|
|
183
|
+
print()
|
|
184
|
+
print("[bold]You can now use pragma commands:[/bold]")
|
|
185
|
+
print(" pragma resources list-groups")
|
|
186
|
+
print(" pragma resources get <resource-id>")
|
|
187
|
+
print(" pragma resources apply <file.yaml>")
|
|
188
|
+
else:
|
|
189
|
+
print()
|
|
190
|
+
print("[red]\u2717 Authentication failed or timed out[/red]")
|
|
191
|
+
print("[dim]Please try again[/dim]")
|
|
192
|
+
raise typer.Exit(1)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@app.command()
|
|
196
|
+
def logout(
|
|
197
|
+
context: str | None = typer.Option(None, help="Context to logout from (all if not specified)"),
|
|
198
|
+
all: bool = typer.Option(False, "--all", help="Logout from all contexts"),
|
|
199
|
+
):
|
|
200
|
+
"""Clear stored authentication credentials.
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
pragma logout # Clear current context
|
|
204
|
+
pragma logout --all # Clear all contexts
|
|
205
|
+
pragma logout --context staging # Clear specific context
|
|
206
|
+
"""
|
|
207
|
+
if all:
|
|
208
|
+
clear_credentials(None)
|
|
209
|
+
print("[green]\u2713[/green] Cleared all credentials")
|
|
210
|
+
elif context:
|
|
211
|
+
clear_credentials(context)
|
|
212
|
+
print(f"[green]\u2713[/green] Cleared credentials for context '{context}'")
|
|
213
|
+
else:
|
|
214
|
+
context_name, _ = get_current_context()
|
|
215
|
+
clear_credentials(context_name)
|
|
216
|
+
print(f"[green]\u2713[/green] Cleared credentials for current context '{context_name}'")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@app.command()
|
|
220
|
+
def whoami():
|
|
221
|
+
"""Show current authentication status.
|
|
222
|
+
|
|
223
|
+
Displays which contexts have stored credentials and current authentication state.
|
|
224
|
+
"""
|
|
225
|
+
config = load_config()
|
|
226
|
+
current_context_name, _ = get_current_context()
|
|
227
|
+
|
|
228
|
+
print()
|
|
229
|
+
print("[bold]Authentication Status[/bold]")
|
|
230
|
+
print()
|
|
231
|
+
|
|
232
|
+
has_any_creds = False
|
|
233
|
+
for context_name in config.contexts.keys():
|
|
234
|
+
token = load_credentials(context_name)
|
|
235
|
+
marker = "[green]*[/green]" if context_name == current_context_name else " "
|
|
236
|
+
|
|
237
|
+
if token:
|
|
238
|
+
has_any_creds = True
|
|
239
|
+
print(f"{marker} [cyan]{context_name}[/cyan]: [green]\u2713 Authenticated[/green]")
|
|
240
|
+
else:
|
|
241
|
+
print(f"{marker} [cyan]{context_name}[/cyan]: [dim]Not authenticated[/dim]")
|
|
242
|
+
|
|
243
|
+
print()
|
|
244
|
+
|
|
245
|
+
if not has_any_creds:
|
|
246
|
+
print("[yellow]No stored credentials found[/yellow]")
|
|
247
|
+
print("[dim]Run 'pragma login' to authenticate[/dim]")
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""CLI auto-completion functions for resource operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from pragma_cli import get_client
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def completion_resource_ids(incomplete: str):
|
|
11
|
+
"""Complete resource identifiers in provider/resource format based on existing resources.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
incomplete: Partial input to complete against available resource types.
|
|
15
|
+
|
|
16
|
+
Yields:
|
|
17
|
+
Resource identifiers matching the incomplete input.
|
|
18
|
+
"""
|
|
19
|
+
client = get_client()
|
|
20
|
+
try:
|
|
21
|
+
resources = client.list_resources()
|
|
22
|
+
except Exception:
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
seen = set()
|
|
26
|
+
for res in resources:
|
|
27
|
+
resource_id = f"{res['provider']}/{res['resource']}"
|
|
28
|
+
if resource_id not in seen and resource_id.lower().startswith(incomplete.lower()):
|
|
29
|
+
seen.add(resource_id)
|
|
30
|
+
yield resource_id
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def completion_resource_names(ctx: typer.Context, incomplete: str):
|
|
34
|
+
"""Complete resource instance names.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
ctx: Typer context containing parsed parameters including resource_id.
|
|
38
|
+
incomplete: Partial input to complete.
|
|
39
|
+
|
|
40
|
+
Yields:
|
|
41
|
+
Resource names matching the incomplete input for the selected resource type.
|
|
42
|
+
"""
|
|
43
|
+
client = get_client()
|
|
44
|
+
resource_id = ctx.params.get("resource_id")
|
|
45
|
+
if not resource_id or "/" not in resource_id:
|
|
46
|
+
return
|
|
47
|
+
provider, resource = resource_id.split("/", 1)
|
|
48
|
+
try:
|
|
49
|
+
resources = client.list_resources(provider=provider, resource=resource)
|
|
50
|
+
except Exception:
|
|
51
|
+
return
|
|
52
|
+
for res in resources:
|
|
53
|
+
if res["name"].startswith(incomplete):
|
|
54
|
+
yield res["name"]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Context and configuration management commands."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich import print
|
|
5
|
+
|
|
6
|
+
from pragma_cli.config import ContextConfig, get_current_context, load_config, save_config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
app = typer.Typer()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command()
|
|
13
|
+
def use_context(context_name: str):
|
|
14
|
+
"""Switch to a different context.
|
|
15
|
+
|
|
16
|
+
Raises:
|
|
17
|
+
typer.Exit: If context not found.
|
|
18
|
+
"""
|
|
19
|
+
config = load_config()
|
|
20
|
+
if context_name not in config.contexts:
|
|
21
|
+
print(f"[red]\u2717[/red] Context '{context_name}' not found")
|
|
22
|
+
print(f"Available contexts: {', '.join(config.contexts.keys())}")
|
|
23
|
+
raise typer.Exit(1)
|
|
24
|
+
|
|
25
|
+
config.current_context = context_name
|
|
26
|
+
save_config(config)
|
|
27
|
+
print(f"[green]\u2713[/green] Switched to context '{context_name}'")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.command()
|
|
31
|
+
def get_contexts():
|
|
32
|
+
"""List available contexts."""
|
|
33
|
+
config = load_config()
|
|
34
|
+
print("\n[bold]Available contexts:[/bold]")
|
|
35
|
+
for name, ctx in config.contexts.items():
|
|
36
|
+
marker = "[green]*[/green]" if name == config.current_context else " "
|
|
37
|
+
print(f"{marker} [cyan]{name}[/cyan]: {ctx.api_url}")
|
|
38
|
+
print()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command()
|
|
42
|
+
def current_context():
|
|
43
|
+
"""Show current context."""
|
|
44
|
+
context_name, context_config = get_current_context()
|
|
45
|
+
print(f"[bold]Current context:[/bold] [cyan]{context_name}[/cyan]")
|
|
46
|
+
print(f"[bold]API URL:[/bold] {context_config.api_url}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.command()
|
|
50
|
+
def set_context(
|
|
51
|
+
name: str = typer.Argument(..., help="Context name"),
|
|
52
|
+
api_url: str = typer.Option(..., help="API endpoint URL"),
|
|
53
|
+
):
|
|
54
|
+
"""Create or update a context."""
|
|
55
|
+
config = load_config()
|
|
56
|
+
config.contexts[name] = ContextConfig(api_url=api_url)
|
|
57
|
+
save_config(config)
|
|
58
|
+
print(f"[green]\u2713[/green] Context '{name}' configured")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.command()
|
|
62
|
+
def delete_context(name: str):
|
|
63
|
+
"""Delete a context.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
typer.Exit: If context not found or is current context.
|
|
67
|
+
"""
|
|
68
|
+
config = load_config()
|
|
69
|
+
if name not in config.contexts:
|
|
70
|
+
print(f"[red]\u2717[/red] Context '{name}' not found")
|
|
71
|
+
raise typer.Exit(1)
|
|
72
|
+
|
|
73
|
+
if name == config.current_context:
|
|
74
|
+
print("[red]\u2717[/red] Cannot delete current context")
|
|
75
|
+
raise typer.Exit(1)
|
|
76
|
+
|
|
77
|
+
del config.contexts[name]
|
|
78
|
+
save_config(config)
|
|
79
|
+
print(f"[green]\u2713[/green] Context '{name}' deleted")
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Dead letter event management commands.
|
|
2
|
+
|
|
3
|
+
Commands for listing, inspecting, retrying, and deleting dead letter events
|
|
4
|
+
from the Pragmatiks platform. Dead letter events are failed resource
|
|
5
|
+
operations that exceeded retry attempts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Annotated
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
import typer
|
|
15
|
+
from rich import print
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
from pragma_cli import get_client
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(help="Dead letter event management commands")
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def truncate(text: str, max_length: int = 50) -> str:
|
|
28
|
+
"""Truncate text to max_length, adding ellipsis if needed.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
text: Text to truncate.
|
|
32
|
+
max_length: Maximum length including ellipsis.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Truncated text with ellipsis if exceeded max_length.
|
|
36
|
+
"""
|
|
37
|
+
if len(text) <= max_length:
|
|
38
|
+
return text
|
|
39
|
+
return text[: max_length - 3] + "..."
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command("list")
|
|
43
|
+
def list_events(
|
|
44
|
+
provider: Annotated[
|
|
45
|
+
str | None,
|
|
46
|
+
typer.Option("--provider", "-p", help="Filter by provider name"),
|
|
47
|
+
] = None,
|
|
48
|
+
):
|
|
49
|
+
"""List dead letter events.
|
|
50
|
+
|
|
51
|
+
Displays a table of all dead letter events, optionally filtered by provider.
|
|
52
|
+
The error_message column is truncated to 50 characters for readability.
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
pragma ops dead-letter list
|
|
56
|
+
pragma ops dead-letter list --provider postgres
|
|
57
|
+
"""
|
|
58
|
+
client = get_client()
|
|
59
|
+
events = client.list_dead_letter_events(provider=provider)
|
|
60
|
+
|
|
61
|
+
if not events:
|
|
62
|
+
print("[dim]No dead letter events found.[/dim]")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
table = Table()
|
|
66
|
+
table.add_column("Event ID", style="cyan")
|
|
67
|
+
table.add_column("Provider", style="green")
|
|
68
|
+
table.add_column("Resource Type")
|
|
69
|
+
table.add_column("Resource Name")
|
|
70
|
+
table.add_column("Error Message", style="red")
|
|
71
|
+
table.add_column("Failed At")
|
|
72
|
+
|
|
73
|
+
for event in events:
|
|
74
|
+
table.add_row(
|
|
75
|
+
event.get("id", ""),
|
|
76
|
+
event.get("provider", ""),
|
|
77
|
+
event.get("resource_type", ""),
|
|
78
|
+
event.get("resource_name", ""),
|
|
79
|
+
truncate(event.get("error_message", ""), 50),
|
|
80
|
+
event.get("failed_at", ""),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
console.print(table)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command()
|
|
87
|
+
def show(
|
|
88
|
+
event_id: Annotated[str, typer.Argument(help="Dead letter event ID")],
|
|
89
|
+
):
|
|
90
|
+
"""Show detailed information about a dead letter event.
|
|
91
|
+
|
|
92
|
+
Displays the full event data as formatted JSON.
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
pragma ops dead-letter show evt_123abc
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
typer.Exit: If event not found (code 1).
|
|
99
|
+
""" # noqa: DOC501
|
|
100
|
+
client = get_client()
|
|
101
|
+
try:
|
|
102
|
+
event = client.get_dead_letter_event(event_id)
|
|
103
|
+
except httpx.HTTPStatusError as e:
|
|
104
|
+
if e.response.status_code == 404:
|
|
105
|
+
print(f"[red]Event not found:[/red] {event_id}")
|
|
106
|
+
raise typer.Exit(1)
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
print(json.dumps(event, indent=2, default=str))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@app.command()
|
|
113
|
+
def retry(
|
|
114
|
+
event_id: Annotated[
|
|
115
|
+
str | None,
|
|
116
|
+
typer.Argument(help="Dead letter event ID to retry"),
|
|
117
|
+
] = None,
|
|
118
|
+
all_events: Annotated[
|
|
119
|
+
bool,
|
|
120
|
+
typer.Option("--all", help="Retry all dead letter events"),
|
|
121
|
+
] = False,
|
|
122
|
+
):
|
|
123
|
+
"""Retry dead letter event(s).
|
|
124
|
+
|
|
125
|
+
Retries a single event by ID, or all events with --all flag.
|
|
126
|
+
The --all flag requires confirmation.
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
pragma ops dead-letter retry evt_123abc
|
|
130
|
+
pragma ops dead-letter retry --all
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
typer.Exit: If event not found (code 1) or user cancels (code 0).
|
|
134
|
+
""" # noqa: DOC501
|
|
135
|
+
client = get_client()
|
|
136
|
+
|
|
137
|
+
if all_events:
|
|
138
|
+
events = client.list_dead_letter_events()
|
|
139
|
+
count = len(events)
|
|
140
|
+
|
|
141
|
+
if count == 0:
|
|
142
|
+
print("[dim]No dead letter events to retry.[/dim]")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
if not typer.confirm(f"Retry {count} event(s)?"):
|
|
146
|
+
raise typer.Exit(0)
|
|
147
|
+
|
|
148
|
+
retried = client.retry_all_dead_letter_events()
|
|
149
|
+
print(f"[green]Retried {retried} event(s)[/green]")
|
|
150
|
+
elif event_id:
|
|
151
|
+
try:
|
|
152
|
+
client.retry_dead_letter_event(event_id)
|
|
153
|
+
except httpx.HTTPStatusError as e:
|
|
154
|
+
if e.response.status_code == 404:
|
|
155
|
+
print(f"[red]Event not found:[/red] {event_id}")
|
|
156
|
+
raise typer.Exit(1)
|
|
157
|
+
raise
|
|
158
|
+
|
|
159
|
+
print(f"[green]Retried event:[/green] {event_id}")
|
|
160
|
+
else:
|
|
161
|
+
print("[red]Error:[/red] Provide an event_id or use --all")
|
|
162
|
+
raise typer.Exit(1)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@app.command()
|
|
166
|
+
def delete(
|
|
167
|
+
event_id: Annotated[
|
|
168
|
+
str | None,
|
|
169
|
+
typer.Argument(help="Dead letter event ID to delete"),
|
|
170
|
+
] = None,
|
|
171
|
+
all_events: Annotated[
|
|
172
|
+
bool,
|
|
173
|
+
typer.Option("--all", help="Delete all dead letter events"),
|
|
174
|
+
] = False,
|
|
175
|
+
provider: Annotated[
|
|
176
|
+
str | None,
|
|
177
|
+
typer.Option("--provider", "-p", help="Delete all events for this provider"),
|
|
178
|
+
] = None,
|
|
179
|
+
):
|
|
180
|
+
"""Delete dead letter event(s).
|
|
181
|
+
|
|
182
|
+
Deletes a single event by ID, all events for a provider, or all events.
|
|
183
|
+
The --all and --provider flags require confirmation.
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
pragma ops dead-letter delete evt_123abc
|
|
187
|
+
pragma ops dead-letter delete --all
|
|
188
|
+
pragma ops dead-letter delete --provider postgres
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
typer.Exit: If event not found (code 1) or user cancels (code 0).
|
|
192
|
+
""" # noqa: DOC501
|
|
193
|
+
client = get_client()
|
|
194
|
+
|
|
195
|
+
if all_events:
|
|
196
|
+
events = client.list_dead_letter_events()
|
|
197
|
+
count = len(events)
|
|
198
|
+
|
|
199
|
+
if count == 0:
|
|
200
|
+
print("[dim]No dead letter events to delete.[/dim]")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
if not typer.confirm(f"Delete {count} event(s)?"):
|
|
204
|
+
raise typer.Exit(0)
|
|
205
|
+
|
|
206
|
+
deleted = client.delete_dead_letter_events(all=True)
|
|
207
|
+
print(f"[green]Deleted {deleted} event(s)[/green]")
|
|
208
|
+
elif provider:
|
|
209
|
+
events = client.list_dead_letter_events(provider=provider)
|
|
210
|
+
count = len(events)
|
|
211
|
+
|
|
212
|
+
if count == 0:
|
|
213
|
+
print(f"[dim]No dead letter events found for provider '{provider}'.[/dim]")
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
if not typer.confirm(f"Delete {count} event(s) for provider '{provider}'?"):
|
|
217
|
+
raise typer.Exit(0)
|
|
218
|
+
|
|
219
|
+
deleted = client.delete_dead_letter_events(provider=provider)
|
|
220
|
+
print(f"[green]Deleted {deleted} event(s) for provider '{provider}'[/green]")
|
|
221
|
+
elif event_id:
|
|
222
|
+
try:
|
|
223
|
+
client.delete_dead_letter_event(event_id)
|
|
224
|
+
except httpx.HTTPStatusError as e:
|
|
225
|
+
if e.response.status_code == 404:
|
|
226
|
+
print(f"[red]Event not found:[/red] {event_id}")
|
|
227
|
+
raise typer.Exit(1)
|
|
228
|
+
raise
|
|
229
|
+
|
|
230
|
+
print(f"[green]Deleted event:[/green] {event_id}")
|
|
231
|
+
else:
|
|
232
|
+
print("[red]Error:[/red] Provide an event_id, --provider, or --all")
|
|
233
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Operational commands for platform administration.
|
|
2
|
+
|
|
3
|
+
Commands for managing operational concerns like dead letter events,
|
|
4
|
+
system health, and other administrative tasks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from pragma_cli.commands import dead_letter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Operational commands for platform administration")
|
|
13
|
+
|
|
14
|
+
app.add_typer(dead_letter.app, name="dead-letter")
|