pragmatiks-cli 0.8.1__py3-none-any.whl → 0.12.5__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/commands/auth.py +41 -12
- pragma_cli/commands/completions.py +25 -0
- pragma_cli/commands/config.py +8 -1
- pragma_cli/commands/providers.py +74 -70
- pragma_cli/commands/resources.py +337 -56
- pragma_cli/config.py +19 -0
- pragma_cli/helpers.py +40 -1
- pragma_cli/main.py +28 -2
- {pragmatiks_cli-0.8.1.dist-info → pragmatiks_cli-0.12.5.dist-info}/METADATA +1 -1
- pragmatiks_cli-0.12.5.dist-info/RECORD +17 -0
- pragmatiks_cli-0.8.1.dist-info/RECORD +0 -17
- {pragmatiks_cli-0.8.1.dist-info → pragmatiks_cli-0.12.5.dist-info}/WHEEL +0 -0
- {pragmatiks_cli-0.8.1.dist-info → pragmatiks_cli-0.12.5.dist-info}/entry_points.txt +0 -0
pragma_cli/commands/auth.py
CHANGED
|
@@ -12,7 +12,7 @@ from pragma_sdk.config import load_credentials
|
|
|
12
12
|
from rich import print
|
|
13
13
|
from rich.console import Console
|
|
14
14
|
|
|
15
|
-
from pragma_cli.config import CREDENTIALS_FILE, get_current_context, load_config
|
|
15
|
+
from pragma_cli.config import CREDENTIALS_FILE, ContextConfig, get_current_context, load_config
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
console = Console()
|
|
@@ -22,9 +22,29 @@ app = typer.Typer()
|
|
|
22
22
|
|
|
23
23
|
CALLBACK_PORT = int(os.getenv("PRAGMA_AUTH_CALLBACK_PORT", "8765"))
|
|
24
24
|
CALLBACK_PATH = os.getenv("PRAGMA_AUTH_CALLBACK_PATH", "/auth/callback")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_callback_url() -> str:
|
|
28
|
+
"""Build the local callback URL for OAuth flow.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Callback URL for the local OAuth server.
|
|
32
|
+
"""
|
|
33
|
+
return f"http://localhost:{CALLBACK_PORT}{CALLBACK_PATH}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_login_url(context_config: ContextConfig) -> str:
|
|
37
|
+
"""Build the login URL for a given context.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
context_config: The context configuration to get auth URL from.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Full login URL with callback parameter.
|
|
44
|
+
"""
|
|
45
|
+
auth_url = context_config.get_auth_url()
|
|
46
|
+
callback_url = _get_callback_url()
|
|
47
|
+
return f"{auth_url}/auth/callback?callback={callback_url}"
|
|
28
48
|
|
|
29
49
|
|
|
30
50
|
class CallbackHandler(BaseHTTPRequestHandler):
|
|
@@ -142,41 +162,50 @@ def clear_credentials(context_name: str | None = None):
|
|
|
142
162
|
|
|
143
163
|
|
|
144
164
|
@app.command()
|
|
145
|
-
def login(
|
|
165
|
+
def login(
|
|
166
|
+
context: str | None = typer.Option(None, help="Context to authenticate for (default: current)"),
|
|
167
|
+
):
|
|
146
168
|
"""Authenticate with Pragma using browser-based Clerk login.
|
|
147
169
|
|
|
148
170
|
Opens your default browser to Clerk authentication page. After successful
|
|
149
171
|
login, your credentials are stored locally in ~/.config/pragma/credentials.
|
|
150
172
|
|
|
151
173
|
Example:
|
|
152
|
-
pragma login
|
|
153
|
-
pragma login --context production
|
|
174
|
+
pragma auth login
|
|
175
|
+
pragma auth login --context production
|
|
154
176
|
|
|
155
177
|
Raises:
|
|
156
178
|
typer.Exit: If context not found or authentication fails/times out.
|
|
157
179
|
"""
|
|
158
180
|
config = load_config()
|
|
181
|
+
|
|
182
|
+
# Use current context if not specified
|
|
183
|
+
if context is None:
|
|
184
|
+
context = config.current_context
|
|
185
|
+
|
|
159
186
|
if context not in config.contexts:
|
|
160
187
|
print(f"[red]\u2717[/red] Context '{context}' not found")
|
|
161
188
|
print(f"Available contexts: {', '.join(config.contexts.keys())}")
|
|
162
189
|
raise typer.Exit(1)
|
|
163
190
|
|
|
164
|
-
|
|
191
|
+
context_config = config.contexts[context]
|
|
192
|
+
auth_url = context_config.get_auth_url()
|
|
193
|
+
login_url = _get_login_url(context_config)
|
|
165
194
|
|
|
166
195
|
print(f"[cyan]Authenticating for context:[/cyan] {context}")
|
|
167
|
-
print(f"[cyan]API URL:[/cyan] {api_url}")
|
|
196
|
+
print(f"[cyan]API URL:[/cyan] {context_config.api_url}")
|
|
168
197
|
print()
|
|
169
198
|
|
|
170
199
|
server = HTTPServer(("localhost", CALLBACK_PORT), CallbackHandler)
|
|
171
200
|
|
|
172
|
-
print(f"[yellow]Opening browser to:[/yellow] {
|
|
201
|
+
print(f"[yellow]Opening browser to:[/yellow] {auth_url}")
|
|
173
202
|
print()
|
|
174
203
|
print("[dim]If browser doesn't open automatically, visit:[/dim]")
|
|
175
|
-
print(f"[dim]{
|
|
204
|
+
print(f"[dim]{login_url}[/dim]")
|
|
176
205
|
print()
|
|
177
206
|
print("[yellow]Waiting for authentication...[/yellow]")
|
|
178
207
|
|
|
179
|
-
webbrowser.open(
|
|
208
|
+
webbrowser.open(login_url)
|
|
180
209
|
|
|
181
210
|
server.timeout = 300
|
|
182
211
|
server.handle_request()
|
|
@@ -49,6 +49,31 @@ def completion_provider_ids(incomplete: str):
|
|
|
49
49
|
yield provider.provider_id
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
def completion_provider_versions(ctx: typer.Context, incomplete: str):
|
|
53
|
+
"""Complete provider versions based on available builds.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
ctx: Typer context containing parsed parameters including provider_id.
|
|
57
|
+
incomplete: Partial input to complete against available versions.
|
|
58
|
+
|
|
59
|
+
Yields:
|
|
60
|
+
Version strings matching the incomplete input.
|
|
61
|
+
"""
|
|
62
|
+
client = _get_completion_client()
|
|
63
|
+
if client is None:
|
|
64
|
+
return
|
|
65
|
+
provider_id = ctx.params.get("provider_id")
|
|
66
|
+
if not provider_id:
|
|
67
|
+
return
|
|
68
|
+
try:
|
|
69
|
+
builds = client.list_builds(provider_id)
|
|
70
|
+
except Exception:
|
|
71
|
+
return
|
|
72
|
+
for build in builds:
|
|
73
|
+
if build.version.startswith(incomplete):
|
|
74
|
+
yield build.version
|
|
75
|
+
|
|
76
|
+
|
|
52
77
|
def completion_resource_ids(incomplete: str):
|
|
53
78
|
"""Complete resource identifiers in provider/resource format based on existing resources.
|
|
54
79
|
|
pragma_cli/commands/config.py
CHANGED
|
@@ -44,18 +44,25 @@ def current_context():
|
|
|
44
44
|
context_name, context_config = get_current_context()
|
|
45
45
|
print(f"[bold]Current context:[/bold] [cyan]{context_name}[/cyan]")
|
|
46
46
|
print(f"[bold]API URL:[/bold] {context_config.api_url}")
|
|
47
|
+
print(f"[bold]Auth URL:[/bold] {context_config.get_auth_url()}")
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
@app.command()
|
|
50
51
|
def set_context(
|
|
51
52
|
name: str = typer.Argument(..., help="Context name"),
|
|
52
53
|
api_url: str = typer.Option(..., help="API endpoint URL"),
|
|
54
|
+
auth_url: str | None = typer.Option(None, help="Auth endpoint URL (derived from api_url if not set)"),
|
|
53
55
|
):
|
|
54
56
|
"""Create or update a context."""
|
|
55
57
|
config = load_config()
|
|
56
|
-
config.contexts[name] = ContextConfig(api_url=api_url)
|
|
58
|
+
config.contexts[name] = ContextConfig(api_url=api_url, auth_url=auth_url)
|
|
57
59
|
save_config(config)
|
|
60
|
+
|
|
61
|
+
# Show the effective auth URL
|
|
62
|
+
effective_auth = config.contexts[name].get_auth_url()
|
|
58
63
|
print(f"[green]\u2713[/green] Context '{name}' configured")
|
|
64
|
+
print(f" API URL: {api_url}")
|
|
65
|
+
print(f" Auth URL: {effective_auth}")
|
|
59
66
|
|
|
60
67
|
|
|
61
68
|
@app.command()
|
pragma_cli/commands/providers.py
CHANGED
|
@@ -15,7 +15,7 @@ import copier
|
|
|
15
15
|
import httpx
|
|
16
16
|
import typer
|
|
17
17
|
from pragma_sdk import (
|
|
18
|
-
|
|
18
|
+
BuildInfo,
|
|
19
19
|
BuildStatus,
|
|
20
20
|
DeploymentStatus,
|
|
21
21
|
PragmaClient,
|
|
@@ -28,7 +28,8 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
|
28
28
|
from rich.table import Table
|
|
29
29
|
|
|
30
30
|
from pragma_cli import get_client
|
|
31
|
-
from pragma_cli.commands.completions import completion_provider_ids
|
|
31
|
+
from pragma_cli.commands.completions import completion_provider_ids, completion_provider_versions
|
|
32
|
+
from pragma_cli.helpers import OutputFormat, output_data
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
app = typer.Typer(help="Provider management commands")
|
|
@@ -117,7 +118,9 @@ def get_template_source() -> str:
|
|
|
117
118
|
|
|
118
119
|
|
|
119
120
|
@app.command("list")
|
|
120
|
-
def list_providers(
|
|
121
|
+
def list_providers(
|
|
122
|
+
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format")] = OutputFormat.TABLE,
|
|
123
|
+
):
|
|
121
124
|
"""List all deployed providers.
|
|
122
125
|
|
|
123
126
|
Shows providers with their deployment status. Displays:
|
|
@@ -126,8 +129,9 @@ def list_providers():
|
|
|
126
129
|
- Status (running/stopped)
|
|
127
130
|
- Last deployed timestamp
|
|
128
131
|
|
|
129
|
-
|
|
132
|
+
Examples:
|
|
130
133
|
pragma providers list
|
|
134
|
+
pragma providers list -o json
|
|
131
135
|
|
|
132
136
|
Raises:
|
|
133
137
|
typer.Exit: If authentication is missing or API call fails.
|
|
@@ -158,7 +162,12 @@ def list_providers():
|
|
|
158
162
|
console.print("[dim]No providers found.[/dim]")
|
|
159
163
|
return
|
|
160
164
|
|
|
161
|
-
|
|
165
|
+
if output == OutputFormat.TABLE:
|
|
166
|
+
_print_providers_table(providers)
|
|
167
|
+
else:
|
|
168
|
+
# Convert Pydantic models to dicts for JSON/YAML output
|
|
169
|
+
data = [p.model_dump(mode="json") for p in providers]
|
|
170
|
+
output_data(data, output)
|
|
162
171
|
|
|
163
172
|
|
|
164
173
|
def _print_providers_table(providers: list[ProviderInfo]) -> None:
|
|
@@ -422,7 +431,7 @@ def push(
|
|
|
422
431
|
client = get_client()
|
|
423
432
|
|
|
424
433
|
if client._auth is None:
|
|
425
|
-
console.print("[red]Error:[/red] Authentication required. Run 'pragma login' first.")
|
|
434
|
+
console.print("[red]Error:[/red] Authentication required. Run 'pragma auth login' first.")
|
|
426
435
|
raise typer.Exit(1)
|
|
427
436
|
|
|
428
437
|
try:
|
|
@@ -431,10 +440,10 @@ def push(
|
|
|
431
440
|
if not wait:
|
|
432
441
|
console.print()
|
|
433
442
|
console.print("[dim]Build running in background. Check status with:[/dim]")
|
|
434
|
-
console.print(f" pragma providers
|
|
443
|
+
console.print(f" pragma providers builds {provider_id} {push_result.version}")
|
|
435
444
|
return
|
|
436
445
|
|
|
437
|
-
_wait_for_build(client, provider_id, push_result.
|
|
446
|
+
_wait_for_build(client, provider_id, push_result.version, logs)
|
|
438
447
|
|
|
439
448
|
if deploy:
|
|
440
449
|
console.print()
|
|
@@ -455,7 +464,7 @@ def _upload_code(client: PragmaClient, provider_id: str, tarball: bytes) -> Push
|
|
|
455
464
|
tarball: Gzipped tarball bytes of provider source.
|
|
456
465
|
|
|
457
466
|
Returns:
|
|
458
|
-
PushResult with build
|
|
467
|
+
PushResult with build details including version.
|
|
459
468
|
"""
|
|
460
469
|
with Progress(
|
|
461
470
|
SpinnerColumn(),
|
|
@@ -466,42 +475,42 @@ def _upload_code(client: PragmaClient, provider_id: str, tarball: bytes) -> Push
|
|
|
466
475
|
progress.add_task("Uploading code...", total=None)
|
|
467
476
|
push_result = client.push_provider(provider_id, tarball)
|
|
468
477
|
|
|
469
|
-
console.print(f"[green]Build started:[/green] {push_result.
|
|
478
|
+
console.print(f"[green]Build started:[/green] {push_result.version}")
|
|
470
479
|
return push_result
|
|
471
480
|
|
|
472
481
|
|
|
473
482
|
def _wait_for_build(
|
|
474
483
|
client: PragmaClient,
|
|
475
484
|
provider_id: str,
|
|
476
|
-
|
|
485
|
+
version: str,
|
|
477
486
|
logs: bool,
|
|
478
|
-
) ->
|
|
487
|
+
) -> BuildInfo:
|
|
479
488
|
"""Wait for build to complete, optionally streaming logs.
|
|
480
489
|
|
|
481
490
|
Args:
|
|
482
491
|
client: SDK client instance.
|
|
483
492
|
provider_id: Provider identifier.
|
|
484
|
-
|
|
493
|
+
version: CalVer version string.
|
|
485
494
|
logs: Whether to stream build logs.
|
|
486
495
|
|
|
487
496
|
Returns:
|
|
488
|
-
Final
|
|
497
|
+
Final BuildInfo.
|
|
489
498
|
|
|
490
499
|
Raises:
|
|
491
500
|
typer.Exit: On build failure or timeout.
|
|
492
501
|
"""
|
|
493
502
|
if logs:
|
|
494
|
-
_stream_build_logs(client, provider_id,
|
|
503
|
+
_stream_build_logs(client, provider_id, version)
|
|
495
504
|
else:
|
|
496
|
-
build_result = _poll_build_status(client, provider_id,
|
|
505
|
+
build_result = _poll_build_status(client, provider_id, version)
|
|
497
506
|
|
|
498
507
|
if build_result.status == BuildStatus.FAILED:
|
|
499
508
|
console.print(f"[red]Build failed:[/red] {build_result.error_message}")
|
|
500
509
|
raise typer.Exit(1)
|
|
501
510
|
|
|
502
|
-
console.print(f"[green]Build successful:[/green] {build_result.
|
|
511
|
+
console.print(f"[green]Build successful:[/green] {build_result.version}")
|
|
503
512
|
|
|
504
|
-
final_build = client.get_build_status(provider_id,
|
|
513
|
+
final_build = client.get_build_status(provider_id, version)
|
|
505
514
|
|
|
506
515
|
if final_build.status != BuildStatus.SUCCESS:
|
|
507
516
|
console.print(f"[red]Build failed:[/red] {final_build.error_message}")
|
|
@@ -510,16 +519,16 @@ def _wait_for_build(
|
|
|
510
519
|
return final_build
|
|
511
520
|
|
|
512
521
|
|
|
513
|
-
def _poll_build_status(client: PragmaClient, provider_id: str,
|
|
522
|
+
def _poll_build_status(client: PragmaClient, provider_id: str, version: str) -> BuildInfo:
|
|
514
523
|
"""Poll build status until completion or timeout.
|
|
515
524
|
|
|
516
525
|
Args:
|
|
517
526
|
client: SDK client instance.
|
|
518
527
|
provider_id: Provider identifier.
|
|
519
|
-
|
|
528
|
+
version: CalVer version string.
|
|
520
529
|
|
|
521
530
|
Returns:
|
|
522
|
-
Final
|
|
531
|
+
Final BuildInfo.
|
|
523
532
|
|
|
524
533
|
Raises:
|
|
525
534
|
typer.Exit: If build times out.
|
|
@@ -535,7 +544,7 @@ def _poll_build_status(client: PragmaClient, provider_id: str, job_name: str) ->
|
|
|
535
544
|
task = progress.add_task("Building...", total=None)
|
|
536
545
|
|
|
537
546
|
while True:
|
|
538
|
-
build_result = client.get_build_status(provider_id,
|
|
547
|
+
build_result = client.get_build_status(provider_id, version)
|
|
539
548
|
|
|
540
549
|
if build_result.status in (BuildStatus.SUCCESS, BuildStatus.FAILED):
|
|
541
550
|
return build_result
|
|
@@ -549,33 +558,31 @@ def _poll_build_status(client: PragmaClient, provider_id: str, job_name: str) ->
|
|
|
549
558
|
time.sleep(BUILD_POLL_INTERVAL)
|
|
550
559
|
|
|
551
560
|
|
|
552
|
-
def _stream_build_logs(client: PragmaClient, provider_id: str,
|
|
561
|
+
def _stream_build_logs(client: PragmaClient, provider_id: str, version: str) -> None:
|
|
553
562
|
"""Stream build logs to console.
|
|
554
563
|
|
|
555
564
|
Args:
|
|
556
565
|
client: SDK client instance.
|
|
557
566
|
provider_id: Provider identifier.
|
|
558
|
-
|
|
567
|
+
version: CalVer version string.
|
|
559
568
|
"""
|
|
560
569
|
console.print()
|
|
561
570
|
console.print("[bold]Build logs:[/bold]")
|
|
562
571
|
console.print("-" * 40)
|
|
563
572
|
|
|
564
573
|
try:
|
|
565
|
-
with client.stream_build_logs(provider_id,
|
|
574
|
+
with client.stream_build_logs(provider_id, version) as response:
|
|
566
575
|
for line in response.iter_lines():
|
|
567
576
|
console.print(line)
|
|
568
577
|
except httpx.HTTPError as e:
|
|
569
578
|
console.print(f"[yellow]Warning:[/yellow] Could not stream logs: {e}")
|
|
570
579
|
console.print("[dim]Falling back to polling...[/dim]")
|
|
571
|
-
_poll_build_status(client, provider_id,
|
|
580
|
+
_poll_build_status(client, provider_id, version)
|
|
572
581
|
|
|
573
582
|
console.print("-" * 40)
|
|
574
583
|
|
|
575
584
|
|
|
576
|
-
def _deploy_provider(
|
|
577
|
-
client: PragmaClient, provider_id: str, version: str | None = None
|
|
578
|
-
) -> None:
|
|
585
|
+
def _deploy_provider(client: PragmaClient, provider_id: str, version: str | None = None) -> None:
|
|
579
586
|
"""Deploy the provider to a specific version.
|
|
580
587
|
|
|
581
588
|
Args:
|
|
@@ -592,20 +599,27 @@ def _deploy_provider(
|
|
|
592
599
|
progress.add_task("Deploying...", total=None)
|
|
593
600
|
deploy_result = client.deploy_provider(provider_id, version)
|
|
594
601
|
|
|
595
|
-
console.print(f"[green]Deployment started:[/green] {
|
|
596
|
-
|
|
602
|
+
console.print(f"[green]Deployment started:[/green] {provider_id}")
|
|
603
|
+
if deploy_result.image:
|
|
604
|
+
console.print(f"[dim]Image:[/dim] {deploy_result.image}")
|
|
597
605
|
console.print(f"[dim]Status:[/dim] {deploy_result.status.value}")
|
|
598
606
|
|
|
599
607
|
|
|
600
608
|
@app.command()
|
|
601
609
|
def deploy(
|
|
610
|
+
provider_id: Annotated[
|
|
611
|
+
str,
|
|
612
|
+
typer.Argument(
|
|
613
|
+
help="Provider ID to deploy (e.g., 'postgres', 'my-provider')",
|
|
614
|
+
autocompletion=completion_provider_ids,
|
|
615
|
+
),
|
|
616
|
+
],
|
|
602
617
|
version: Annotated[
|
|
603
618
|
str | None,
|
|
604
|
-
typer.
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
typer.Option("--package", "-p", help="Provider package name (auto-detected if not specified)"),
|
|
619
|
+
typer.Argument(
|
|
620
|
+
help="Version to deploy (e.g., 20250115.120000). Defaults to latest.",
|
|
621
|
+
autocompletion=completion_provider_versions,
|
|
622
|
+
),
|
|
609
623
|
] = None,
|
|
610
624
|
):
|
|
611
625
|
"""Deploy a provider to a specific version.
|
|
@@ -614,23 +628,14 @@ def deploy(
|
|
|
614
628
|
the latest successful build. Use 'pragma providers builds' to see available versions.
|
|
615
629
|
|
|
616
630
|
Deploy latest:
|
|
617
|
-
pragma providers deploy
|
|
631
|
+
pragma providers deploy postgres
|
|
618
632
|
|
|
619
633
|
Deploy specific version:
|
|
620
|
-
pragma providers deploy
|
|
634
|
+
pragma providers deploy postgres 20250115.120000
|
|
621
635
|
|
|
622
636
|
Raises:
|
|
623
637
|
typer.Exit: If deployment fails.
|
|
624
638
|
"""
|
|
625
|
-
provider_name = package or detect_provider_package()
|
|
626
|
-
|
|
627
|
-
if not provider_name:
|
|
628
|
-
console.print("[red]Error:[/red] Could not detect provider package.")
|
|
629
|
-
console.print("Run from a provider directory or specify --package")
|
|
630
|
-
raise typer.Exit(1)
|
|
631
|
-
|
|
632
|
-
provider_id = provider_name.replace("_", "-").removesuffix("-provider")
|
|
633
|
-
|
|
634
639
|
console.print(f"[bold]Deploying provider:[/bold] {provider_id}")
|
|
635
640
|
if version:
|
|
636
641
|
console.print(f"[dim]Version:[/dim] {version}")
|
|
@@ -717,7 +722,7 @@ def delete(
|
|
|
717
722
|
client = get_client()
|
|
718
723
|
|
|
719
724
|
if client._auth is None:
|
|
720
|
-
console.print("[red]Error:[/red] Authentication required. Run 'pragma login' first.")
|
|
725
|
+
console.print("[red]Error:[/red] Authentication required. Run 'pragma auth login' first.")
|
|
721
726
|
raise typer.Exit(1)
|
|
722
727
|
|
|
723
728
|
console.print(f"[bold]Provider:[/bold] {provider_id}")
|
|
@@ -765,23 +770,12 @@ def _print_delete_result(result: ProviderDeleteResult) -> None:
|
|
|
765
770
|
"""
|
|
766
771
|
console.print()
|
|
767
772
|
console.print(f"[green]Provider deleted:[/green] {result.provider_id}")
|
|
768
|
-
console.print()
|
|
769
773
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
table.add_column("Deleted", justify="right")
|
|
773
|
-
|
|
774
|
-
table.add_row("Builds Cancelled", str(result.builds_cancelled))
|
|
775
|
-
table.add_row("Source Archives", str(result.source_archives_deleted))
|
|
776
|
-
table.add_row("Deployment", "Yes" if result.deployment_deleted else "No (not found)")
|
|
777
|
-
table.add_row("Resources", str(result.resources_deleted))
|
|
778
|
-
table.add_row("Resource Definitions", str(result.resource_definitions_deleted))
|
|
779
|
-
table.add_row("Outbox Events", str(result.outbox_events_deleted))
|
|
780
|
-
table.add_row("Dead Letter Events", str(result.dead_letter_events_deleted))
|
|
781
|
-
table.add_row("NATS Messages Purged", str(result.messages_purged))
|
|
782
|
-
table.add_row("NATS Consumer", "Deleted" if result.consumer_deleted else "Not found")
|
|
774
|
+
if result.deployment_deleted:
|
|
775
|
+
console.print("[dim]Deployment stopped[/dim]")
|
|
783
776
|
|
|
784
|
-
|
|
777
|
+
if result.resources_deleted > 0:
|
|
778
|
+
console.print(f"[dim]Resources deleted: {result.resources_deleted}[/dim]")
|
|
785
779
|
|
|
786
780
|
|
|
787
781
|
@app.command()
|
|
@@ -793,6 +787,7 @@ def status(
|
|
|
793
787
|
autocompletion=completion_provider_ids,
|
|
794
788
|
),
|
|
795
789
|
],
|
|
790
|
+
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format")] = OutputFormat.TABLE,
|
|
796
791
|
):
|
|
797
792
|
"""Check the deployment status of a provider.
|
|
798
793
|
|
|
@@ -802,9 +797,10 @@ def status(
|
|
|
802
797
|
- Health status
|
|
803
798
|
- Last updated timestamp
|
|
804
799
|
|
|
805
|
-
|
|
800
|
+
Examples:
|
|
806
801
|
pragma providers status postgres
|
|
807
802
|
pragma providers status my-provider
|
|
803
|
+
pragma providers status postgres -o json
|
|
808
804
|
|
|
809
805
|
Raises:
|
|
810
806
|
typer.Exit: If deployment not found or status check fails.
|
|
@@ -827,7 +823,13 @@ def status(
|
|
|
827
823
|
console.print(f"[red]Error:[/red] {e}")
|
|
828
824
|
raise typer.Exit(1)
|
|
829
825
|
|
|
830
|
-
|
|
826
|
+
if output == OutputFormat.TABLE:
|
|
827
|
+
_print_deployment_status(provider_id, result)
|
|
828
|
+
else:
|
|
829
|
+
# Convert Pydantic model to dict for JSON/YAML output
|
|
830
|
+
data = result.model_dump(mode="json")
|
|
831
|
+
data["provider_id"] = provider_id # Include provider_id in output
|
|
832
|
+
output_data(data, output)
|
|
831
833
|
|
|
832
834
|
|
|
833
835
|
def _print_deployment_status(provider_id: str, result) -> None:
|
|
@@ -835,7 +837,7 @@ def _print_deployment_status(provider_id: str, result) -> None:
|
|
|
835
837
|
|
|
836
838
|
Args:
|
|
837
839
|
provider_id: Provider identifier.
|
|
838
|
-
result:
|
|
840
|
+
result: DeploymentResult from the API.
|
|
839
841
|
"""
|
|
840
842
|
status_colors = {
|
|
841
843
|
"pending": "yellow",
|
|
@@ -855,10 +857,12 @@ def _print_deployment_status(provider_id: str, result) -> None:
|
|
|
855
857
|
|
|
856
858
|
table.add_row("Status", f"[{status_color}]{result.status.value}[/{status_color}]")
|
|
857
859
|
|
|
858
|
-
if result.
|
|
859
|
-
table.add_row("
|
|
860
|
+
if result.image:
|
|
861
|
+
table.add_row("Image", result.image)
|
|
860
862
|
|
|
861
|
-
|
|
863
|
+
# Healthy is determined by having ready replicas
|
|
864
|
+
healthy = result.ready_replicas > 0
|
|
865
|
+
healthy_display = "[green]yes[/green]" if healthy else "[red]no[/red]"
|
|
862
866
|
table.add_row("Healthy", healthy_display)
|
|
863
867
|
|
|
864
868
|
if result.updated_at:
|
|
@@ -892,7 +896,7 @@ def builds(
|
|
|
892
896
|
client = get_client()
|
|
893
897
|
|
|
894
898
|
if client._auth is None:
|
|
895
|
-
console.print("[red]Error:[/red] Authentication required. Run 'pragma login' first.")
|
|
899
|
+
console.print("[red]Error:[/red] Authentication required. Run 'pragma auth login' first.")
|
|
896
900
|
raise typer.Exit(1)
|
|
897
901
|
|
|
898
902
|
try:
|
pragma_cli/commands/resources.py
CHANGED
|
@@ -6,24 +6,83 @@ import json
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Annotated
|
|
8
8
|
|
|
9
|
+
import httpx
|
|
9
10
|
import typer
|
|
10
11
|
import yaml
|
|
11
12
|
from rich import print
|
|
12
13
|
from rich.console import Console
|
|
13
14
|
from rich.markup import escape
|
|
15
|
+
from rich.table import Table
|
|
14
16
|
|
|
15
17
|
from pragma_cli import get_client
|
|
16
18
|
from pragma_cli.commands.completions import (
|
|
17
19
|
completion_resource_ids,
|
|
18
20
|
completion_resource_names,
|
|
19
21
|
)
|
|
20
|
-
from pragma_cli.helpers import parse_resource_id
|
|
22
|
+
from pragma_cli.helpers import OutputFormat, output_data, parse_resource_id
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
console = Console()
|
|
24
26
|
app = typer.Typer()
|
|
25
27
|
|
|
26
28
|
|
|
29
|
+
def _format_api_error(error: httpx.HTTPStatusError) -> str:
|
|
30
|
+
"""Format an API error response with structured details.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Formatted error message with details extracted from JSON response.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
detail = error.response.json().get("detail", {})
|
|
37
|
+
except (json.JSONDecodeError, ValueError):
|
|
38
|
+
# Fall back to plain text if not JSON
|
|
39
|
+
return error.response.text or str(error)
|
|
40
|
+
|
|
41
|
+
# Handle simple string details
|
|
42
|
+
if isinstance(detail, str):
|
|
43
|
+
return detail
|
|
44
|
+
|
|
45
|
+
# Handle structured error responses
|
|
46
|
+
message = detail.get("message", str(error))
|
|
47
|
+
parts = [message]
|
|
48
|
+
|
|
49
|
+
# DependencyValidationError details
|
|
50
|
+
if missing := detail.get("missing_dependencies"):
|
|
51
|
+
parts.append("\n Missing dependencies:")
|
|
52
|
+
for dep_id in missing:
|
|
53
|
+
parts.append(f" - {dep_id}")
|
|
54
|
+
if not_ready := detail.get("not_ready_dependencies"):
|
|
55
|
+
parts.append("\n Dependencies not ready:")
|
|
56
|
+
for item in not_ready:
|
|
57
|
+
if isinstance(item, dict):
|
|
58
|
+
parts.append(f" - {item['id']} (state: {item['state']})")
|
|
59
|
+
else:
|
|
60
|
+
parts.append(f" - {item}")
|
|
61
|
+
|
|
62
|
+
# FieldReferenceError details
|
|
63
|
+
if field := detail.get("field"):
|
|
64
|
+
ref_parts = [
|
|
65
|
+
detail.get("reference_provider", ""),
|
|
66
|
+
detail.get("reference_resource", ""),
|
|
67
|
+
detail.get("reference_name", ""),
|
|
68
|
+
]
|
|
69
|
+
ref_id = "/".join(filter(None, ref_parts))
|
|
70
|
+
if ref_id:
|
|
71
|
+
parts.append(f"\n Reference: {ref_id}#{field}")
|
|
72
|
+
|
|
73
|
+
# InvalidLifecycleTransitionError details
|
|
74
|
+
if current_state := detail.get("current_state"):
|
|
75
|
+
target_state = detail.get("target_state", "unknown")
|
|
76
|
+
parts.append(f"\n Current state: {current_state}")
|
|
77
|
+
parts.append(f" Target state: {target_state}")
|
|
78
|
+
|
|
79
|
+
# ResourceInProcessingError details
|
|
80
|
+
if resource_id := detail.get("resource_id"):
|
|
81
|
+
parts.append(f"\n Resource: {resource_id}")
|
|
82
|
+
|
|
83
|
+
return "".join(parts)
|
|
84
|
+
|
|
85
|
+
|
|
27
86
|
def resolve_file_references(resource: dict, base_dir: Path) -> dict:
|
|
28
87
|
"""Resolve file references in secret resource config.
|
|
29
88
|
|
|
@@ -55,7 +114,7 @@ def resolve_file_references(resource: dict, base_dir: Path) -> dict:
|
|
|
55
114
|
resolved_data = {}
|
|
56
115
|
for key, value in data.items():
|
|
57
116
|
if isinstance(value, str) and value.startswith("@"):
|
|
58
|
-
file_path = Path(value[1:])
|
|
117
|
+
file_path = Path(value[1:]).expanduser()
|
|
59
118
|
if not file_path.is_absolute():
|
|
60
119
|
file_path = base_dir / file_path
|
|
61
120
|
|
|
@@ -85,13 +144,76 @@ def format_state(state: str) -> str:
|
|
|
85
144
|
return escape(f"[{state}]")
|
|
86
145
|
|
|
87
146
|
|
|
147
|
+
def _print_resource_types_table(types: list[dict]) -> None:
|
|
148
|
+
"""Print resource types in a formatted table.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
types: List of resource type dictionaries to display.
|
|
152
|
+
"""
|
|
153
|
+
console.print()
|
|
154
|
+
table = Table(show_header=True, header_style="bold")
|
|
155
|
+
table.add_column("Provider")
|
|
156
|
+
table.add_column("Resource")
|
|
157
|
+
table.add_column("Description")
|
|
158
|
+
|
|
159
|
+
for resource_type in types:
|
|
160
|
+
description = resource_type.get("description") or "[dim]—[/dim]"
|
|
161
|
+
table.add_row(
|
|
162
|
+
resource_type["provider"],
|
|
163
|
+
resource_type["resource"],
|
|
164
|
+
description,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
console.print(table)
|
|
168
|
+
console.print()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@app.command("types")
|
|
172
|
+
def list_resource_types(
|
|
173
|
+
provider: Annotated[str | None, typer.Option("--provider", "-p", help="Filter by provider")] = None,
|
|
174
|
+
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format")] = OutputFormat.TABLE,
|
|
175
|
+
):
|
|
176
|
+
"""List available resource types from deployed providers.
|
|
177
|
+
|
|
178
|
+
Displays resource definitions (types) that have been registered by providers.
|
|
179
|
+
Use this to discover what resources you can create.
|
|
180
|
+
|
|
181
|
+
Examples:
|
|
182
|
+
pragma resources types
|
|
183
|
+
pragma resources types --provider gcp
|
|
184
|
+
pragma resources types -o json
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
typer.Exit: If an error occurs while fetching resource types.
|
|
188
|
+
"""
|
|
189
|
+
client = get_client()
|
|
190
|
+
try:
|
|
191
|
+
types = client.list_resource_types(provider=provider)
|
|
192
|
+
except httpx.HTTPStatusError as e:
|
|
193
|
+
console.print(f"[red]Error:[/red] {_format_api_error(e)}")
|
|
194
|
+
raise typer.Exit(1)
|
|
195
|
+
|
|
196
|
+
if not types:
|
|
197
|
+
console.print("[dim]No resource types found.[/dim]")
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
output_data(types, output, table_renderer=_print_resource_types_table)
|
|
201
|
+
|
|
202
|
+
|
|
88
203
|
@app.command("list")
|
|
89
204
|
def list_resources(
|
|
90
205
|
provider: Annotated[str | None, typer.Option("--provider", "-p", help="Filter by provider")] = None,
|
|
91
206
|
resource: Annotated[str | None, typer.Option("--resource", "-r", help="Filter by resource type")] = None,
|
|
92
207
|
tags: Annotated[list[str] | None, typer.Option("--tag", "-t", help="Filter by tags")] = None,
|
|
208
|
+
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format")] = OutputFormat.TABLE,
|
|
93
209
|
):
|
|
94
|
-
"""List resources, optionally filtered by provider, resource type, or tags.
|
|
210
|
+
"""List resources, optionally filtered by provider, resource type, or tags.
|
|
211
|
+
|
|
212
|
+
Examples:
|
|
213
|
+
pragma resources list
|
|
214
|
+
pragma resources list --provider gcp
|
|
215
|
+
pragma resources list -o json
|
|
216
|
+
"""
|
|
95
217
|
client = get_client()
|
|
96
218
|
resources = list(client.list_resources(provider=provider, resource=resource, tags=tags))
|
|
97
219
|
|
|
@@ -99,24 +221,211 @@ def list_resources(
|
|
|
99
221
|
console.print("[dim]No resources found.[/dim]")
|
|
100
222
|
return
|
|
101
223
|
|
|
224
|
+
output_data(resources, output, table_renderer=_print_resources_table)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _print_resources_table(resources: list[dict]) -> None:
|
|
228
|
+
"""Print resources in a formatted table.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
resources: List of resource dictionaries to display.
|
|
232
|
+
"""
|
|
233
|
+
table = Table(show_header=True, header_style="bold")
|
|
234
|
+
table.add_column("Provider")
|
|
235
|
+
table.add_column("Resource")
|
|
236
|
+
table.add_column("Name")
|
|
237
|
+
table.add_column("State")
|
|
238
|
+
table.add_column("Updated")
|
|
239
|
+
|
|
240
|
+
# Track failed resources to show errors after table
|
|
241
|
+
failed_resources: list[tuple[str, str]] = []
|
|
242
|
+
|
|
102
243
|
for res in resources:
|
|
103
|
-
|
|
244
|
+
state = _format_state_color(res["lifecycle_state"])
|
|
245
|
+
updated = res.get("updated_at", "[dim]-[/dim]")
|
|
246
|
+
if updated and updated != "[dim]-[/dim]":
|
|
247
|
+
# Truncate to datetime portion if it's a full ISO string
|
|
248
|
+
updated = updated[:19].replace("T", " ") if len(updated) > 19 else updated
|
|
249
|
+
|
|
250
|
+
table.add_row(
|
|
251
|
+
res["provider"],
|
|
252
|
+
res["resource"],
|
|
253
|
+
res["name"],
|
|
254
|
+
state,
|
|
255
|
+
updated,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Track failed resources for error display
|
|
259
|
+
if res.get("lifecycle_state") == "failed" and res.get("error"):
|
|
260
|
+
resource_id = f"{res['provider']}/{res['resource']}/{res['name']}"
|
|
261
|
+
failed_resources.append((resource_id, res["error"]))
|
|
262
|
+
|
|
263
|
+
console.print(table)
|
|
264
|
+
|
|
265
|
+
# Show errors for failed resources below the table
|
|
266
|
+
for resource_id, error in failed_resources:
|
|
267
|
+
console.print(f" [red]{resource_id}:[/red] {escape(error)}")
|
|
104
268
|
|
|
105
269
|
|
|
106
270
|
@app.command()
|
|
107
271
|
def get(
|
|
108
272
|
resource_id: Annotated[str, typer.Argument(autocompletion=completion_resource_ids)],
|
|
109
273
|
name: Annotated[str | None, typer.Argument(autocompletion=completion_resource_names)] = None,
|
|
274
|
+
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format")] = OutputFormat.TABLE,
|
|
110
275
|
):
|
|
111
|
-
"""Get resources by provider/resource type, optionally filtered by name.
|
|
276
|
+
"""Get resources by provider/resource type, optionally filtered by name.
|
|
277
|
+
|
|
278
|
+
Examples:
|
|
279
|
+
pragma resources get gcp/secret
|
|
280
|
+
pragma resources get gcp/secret my-secret
|
|
281
|
+
pragma resources get gcp/secret my-secret -o json
|
|
282
|
+
"""
|
|
112
283
|
client = get_client()
|
|
113
284
|
provider, resource = parse_resource_id(resource_id)
|
|
114
285
|
if name:
|
|
115
286
|
res = client.get_resource(provider=provider, resource=resource, name=name)
|
|
116
|
-
|
|
287
|
+
output_data([res], output, table_renderer=_print_resources_table)
|
|
117
288
|
else:
|
|
118
|
-
|
|
119
|
-
|
|
289
|
+
resources = list(client.list_resources(provider=provider, resource=resource))
|
|
290
|
+
if not resources:
|
|
291
|
+
console.print("[dim]No resources found.[/dim]")
|
|
292
|
+
return
|
|
293
|
+
output_data(resources, output, table_renderer=_print_resources_table)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _format_state_color(state: str) -> str:
|
|
297
|
+
"""Format lifecycle state with color markup.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
State string wrapped in Rich color markup.
|
|
301
|
+
"""
|
|
302
|
+
state_colors = {
|
|
303
|
+
"draft": "dim",
|
|
304
|
+
"pending": "yellow",
|
|
305
|
+
"processing": "cyan",
|
|
306
|
+
"ready": "green",
|
|
307
|
+
"failed": "red",
|
|
308
|
+
}
|
|
309
|
+
color = state_colors.get(state.lower(), "white")
|
|
310
|
+
return f"[{color}]{state}[/{color}]"
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _format_config_value(value, *, redact_keys: set[str] | None = None) -> str:
|
|
314
|
+
"""Format a config value, redacting sensitive fields.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Formatted string representation with sensitive values masked.
|
|
318
|
+
"""
|
|
319
|
+
redact_keys = redact_keys or {"credentials", "password", "secret", "token", "key", "data"}
|
|
320
|
+
if isinstance(value, dict):
|
|
321
|
+
# Check if this is a FieldReference
|
|
322
|
+
if "provider" in value and "resource" in value and "name" in value and "field" in value:
|
|
323
|
+
return f"{value['provider']}/{value['resource']}/{value['name']}#{value['field']}"
|
|
324
|
+
# Recursively format nested dicts
|
|
325
|
+
formatted = {}
|
|
326
|
+
for k, v in value.items():
|
|
327
|
+
if k.lower() in redact_keys:
|
|
328
|
+
formatted[k] = "********"
|
|
329
|
+
else:
|
|
330
|
+
formatted[k] = _format_config_value(v, redact_keys=redact_keys)
|
|
331
|
+
return str(formatted)
|
|
332
|
+
elif isinstance(value, list):
|
|
333
|
+
return str([_format_config_value(v, redact_keys=redact_keys) for v in value])
|
|
334
|
+
return str(value)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _print_resource_details(res: dict) -> None:
|
|
338
|
+
"""Print resource details in a formatted table."""
|
|
339
|
+
resource_id = f"{res['provider']}/{res['resource']}/{res['name']}"
|
|
340
|
+
|
|
341
|
+
console.print()
|
|
342
|
+
console.print(f"[bold]Resource:[/bold] {resource_id}")
|
|
343
|
+
console.print()
|
|
344
|
+
|
|
345
|
+
# Main properties table
|
|
346
|
+
table = Table(show_header=True, header_style="bold")
|
|
347
|
+
table.add_column("Property")
|
|
348
|
+
table.add_column("Value")
|
|
349
|
+
|
|
350
|
+
# State with color
|
|
351
|
+
table.add_row("State", _format_state_color(res["lifecycle_state"]))
|
|
352
|
+
|
|
353
|
+
# Error if failed
|
|
354
|
+
if res.get("error"):
|
|
355
|
+
table.add_row("Error", f"[red]{escape(res['error'])}[/red]")
|
|
356
|
+
|
|
357
|
+
# Timestamps
|
|
358
|
+
if res.get("created_at"):
|
|
359
|
+
table.add_row("Created", res["created_at"])
|
|
360
|
+
if res.get("updated_at"):
|
|
361
|
+
table.add_row("Updated", res["updated_at"])
|
|
362
|
+
|
|
363
|
+
console.print(table)
|
|
364
|
+
|
|
365
|
+
# Config section
|
|
366
|
+
config = res.get("config", {})
|
|
367
|
+
if config:
|
|
368
|
+
console.print()
|
|
369
|
+
console.print("[bold]Config:[/bold]")
|
|
370
|
+
for key, value in config.items():
|
|
371
|
+
formatted = _format_config_value(value)
|
|
372
|
+
console.print(f" {key}: {formatted}")
|
|
373
|
+
|
|
374
|
+
# Outputs section
|
|
375
|
+
outputs = res.get("outputs", {})
|
|
376
|
+
if outputs:
|
|
377
|
+
console.print()
|
|
378
|
+
console.print("[bold]Outputs:[/bold]")
|
|
379
|
+
for key, value in outputs.items():
|
|
380
|
+
console.print(f" {key}: {value}")
|
|
381
|
+
|
|
382
|
+
# Dependencies section
|
|
383
|
+
dependencies = res.get("dependencies", [])
|
|
384
|
+
if dependencies:
|
|
385
|
+
console.print()
|
|
386
|
+
console.print("[bold]Dependencies:[/bold]")
|
|
387
|
+
for dep in dependencies:
|
|
388
|
+
dep_id = f"{dep['provider']}/{dep['resource']}/{dep['name']}"
|
|
389
|
+
console.print(f" - {dep_id}")
|
|
390
|
+
|
|
391
|
+
# Tags section
|
|
392
|
+
tags = res.get("tags", [])
|
|
393
|
+
if tags:
|
|
394
|
+
console.print()
|
|
395
|
+
console.print("[bold]Tags:[/bold]")
|
|
396
|
+
console.print(f" {', '.join(tags)}")
|
|
397
|
+
|
|
398
|
+
console.print()
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@app.command()
|
|
402
|
+
def describe(
|
|
403
|
+
resource_id: Annotated[str, typer.Argument(autocompletion=completion_resource_ids)],
|
|
404
|
+
name: Annotated[str, typer.Argument(autocompletion=completion_resource_names)],
|
|
405
|
+
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format")] = OutputFormat.TABLE,
|
|
406
|
+
):
|
|
407
|
+
"""Show detailed information about a resource.
|
|
408
|
+
|
|
409
|
+
Displays the resource's config, outputs, dependencies, and error messages.
|
|
410
|
+
|
|
411
|
+
Examples:
|
|
412
|
+
pragma resources describe gcp/secret my-test-secret
|
|
413
|
+
pragma resources describe postgres/database my-db
|
|
414
|
+
pragma resources describe gcp/secret my-secret -o json
|
|
415
|
+
|
|
416
|
+
Raises:
|
|
417
|
+
typer.Exit: If the resource is not found or an error occurs.
|
|
418
|
+
"""
|
|
419
|
+
client = get_client()
|
|
420
|
+
provider, resource = parse_resource_id(resource_id)
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
res = client.get_resource(provider=provider, resource=resource, name=name)
|
|
424
|
+
except httpx.HTTPStatusError as e:
|
|
425
|
+
console.print(f"[red]Error:[/red] {_format_api_error(e)}")
|
|
426
|
+
raise typer.Exit(1)
|
|
427
|
+
|
|
428
|
+
output_data(res, output, table_renderer=_print_resource_details)
|
|
120
429
|
|
|
121
430
|
|
|
122
431
|
@app.command()
|
|
@@ -134,6 +443,9 @@ def apply(
|
|
|
134
443
|
For pragma/secret resources, file references in config.data values
|
|
135
444
|
are resolved before submission. Use '@path/to/file' syntax to inline
|
|
136
445
|
file contents.
|
|
446
|
+
|
|
447
|
+
Raises:
|
|
448
|
+
typer.Exit: If the apply operation fails.
|
|
137
449
|
"""
|
|
138
450
|
client = get_client()
|
|
139
451
|
for f in file:
|
|
@@ -144,9 +456,14 @@ def apply(
|
|
|
144
456
|
resource = resolve_file_references(resource, base_dir)
|
|
145
457
|
if pending:
|
|
146
458
|
resource["lifecycle_state"] = "pending"
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
459
|
+
res_id = f"{resource.get('provider', '?')}/{resource.get('resource', '?')}/{resource.get('name', '?')}"
|
|
460
|
+
try:
|
|
461
|
+
result = client.apply_resource(resource=resource)
|
|
462
|
+
res_id = f"{result['provider']}/{result['resource']}/{result['name']}"
|
|
463
|
+
print(f"Applied {res_id} {format_state(result['lifecycle_state'])}")
|
|
464
|
+
except httpx.HTTPStatusError as e:
|
|
465
|
+
console.print(f"[red]Error applying {res_id}:[/red] {_format_api_error(e)}")
|
|
466
|
+
raise typer.Exit(1)
|
|
150
467
|
|
|
151
468
|
|
|
152
469
|
@app.command()
|
|
@@ -154,52 +471,16 @@ def delete(
|
|
|
154
471
|
resource_id: Annotated[str, typer.Argument(autocompletion=completion_resource_ids)],
|
|
155
472
|
name: Annotated[str, typer.Argument(autocompletion=completion_resource_names)],
|
|
156
473
|
):
|
|
157
|
-
"""Delete a resource.
|
|
158
|
-
client = get_client()
|
|
159
|
-
provider, resource = parse_resource_id(resource_id)
|
|
160
|
-
client.delete_resource(provider=provider, resource=resource, name=name)
|
|
161
|
-
print(f"Deleted {resource_id}/{name}")
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
@app.command()
|
|
165
|
-
def register(
|
|
166
|
-
resource_id: Annotated[str, typer.Argument(help="Resource type in provider/resource format")],
|
|
167
|
-
description: Annotated[str | None, typer.Option("--description", "-d", help="Resource type description")] = None,
|
|
168
|
-
schema_file: Annotated[typer.FileText | None, typer.Option("--schema", "-s", help="JSON schema file")] = None,
|
|
169
|
-
tags: Annotated[list[str] | None, typer.Option("--tag", "-t", help="Tags for categorization")] = None,
|
|
170
|
-
):
|
|
171
|
-
"""Register a new resource type.
|
|
172
|
-
|
|
173
|
-
Registers a resource type so that resources of this type can be created.
|
|
174
|
-
Providers use this to declare what resources they can manage.
|
|
175
|
-
"""
|
|
176
|
-
client = get_client()
|
|
177
|
-
provider, resource = parse_resource_id(resource_id)
|
|
178
|
-
|
|
179
|
-
schema = None
|
|
180
|
-
if schema_file:
|
|
181
|
-
schema = json.load(schema_file)
|
|
182
|
-
|
|
183
|
-
client.register_resource(
|
|
184
|
-
provider=provider,
|
|
185
|
-
resource=resource,
|
|
186
|
-
schema=schema,
|
|
187
|
-
description=description,
|
|
188
|
-
tags=tags,
|
|
189
|
-
)
|
|
190
|
-
print(f"Registered {resource_id}")
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
@app.command()
|
|
194
|
-
def unregister(
|
|
195
|
-
resource_id: Annotated[str, typer.Argument(autocompletion=completion_resource_ids)],
|
|
196
|
-
):
|
|
197
|
-
"""Unregister a resource type.
|
|
474
|
+
"""Delete a resource.
|
|
198
475
|
|
|
199
|
-
|
|
200
|
-
|
|
476
|
+
Raises:
|
|
477
|
+
typer.Exit: If the resource is not found or deletion fails.
|
|
201
478
|
"""
|
|
202
479
|
client = get_client()
|
|
203
480
|
provider, resource = parse_resource_id(resource_id)
|
|
204
|
-
|
|
205
|
-
|
|
481
|
+
try:
|
|
482
|
+
client.delete_resource(provider=provider, resource=resource, name=name)
|
|
483
|
+
print(f"Deleted {resource_id}/{name}")
|
|
484
|
+
except httpx.HTTPStatusError as e:
|
|
485
|
+
console.print(f"[red]Error deleting {resource_id}/{name}:[/red] {_format_api_error(e)}")
|
|
486
|
+
raise typer.Exit(1)
|
pragma_cli/config.py
CHANGED
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
from pathlib import Path
|
|
7
|
+
from urllib.parse import urlparse
|
|
7
8
|
|
|
8
9
|
import yaml
|
|
9
10
|
from pydantic import BaseModel
|
|
@@ -30,6 +31,24 @@ class ContextConfig(BaseModel):
|
|
|
30
31
|
"""Configuration for a single CLI context."""
|
|
31
32
|
|
|
32
33
|
api_url: str
|
|
34
|
+
auth_url: str | None = None
|
|
35
|
+
|
|
36
|
+
def get_auth_url(self) -> str:
|
|
37
|
+
"""Get the auth URL, deriving from api_url if not explicitly set.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Auth URL for Clerk authentication.
|
|
41
|
+
"""
|
|
42
|
+
if self.auth_url:
|
|
43
|
+
return self.auth_url
|
|
44
|
+
|
|
45
|
+
# Handle localhost: default to port 3000 for web app
|
|
46
|
+
parsed = urlparse(self.api_url)
|
|
47
|
+
if parsed.hostname in ("localhost", "127.0.0.1"):
|
|
48
|
+
return "http://localhost:3000"
|
|
49
|
+
|
|
50
|
+
# Derive from api_url: api.pragmatiks.io -> app.pragmatiks.io
|
|
51
|
+
return self.api_url.replace("://api.", "://app.")
|
|
33
52
|
|
|
34
53
|
|
|
35
54
|
class PragmaConfig(BaseModel):
|
pragma_cli/helpers.py
CHANGED
|
@@ -1,7 +1,46 @@
|
|
|
1
|
-
"""CLI helper functions for parsing resource identifiers."""
|
|
1
|
+
"""CLI helper functions for parsing resource identifiers and output formatting."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OutputFormat(StrEnum):
|
|
17
|
+
"""Output format options for CLI commands."""
|
|
18
|
+
|
|
19
|
+
TABLE = "table"
|
|
20
|
+
JSON = "json"
|
|
21
|
+
YAML = "yaml"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def output_data(
|
|
25
|
+
data: list[dict[str, Any]] | dict[str, Any],
|
|
26
|
+
format: OutputFormat,
|
|
27
|
+
table_renderer: Callable[..., None] | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Output data in the specified format.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
data: Data to output (list of dicts or single dict).
|
|
33
|
+
format: Output format (table, json, yaml).
|
|
34
|
+
table_renderer: Function to render table output. Required for TABLE format.
|
|
35
|
+
"""
|
|
36
|
+
if format == OutputFormat.TABLE:
|
|
37
|
+
if table_renderer:
|
|
38
|
+
table_renderer(data)
|
|
39
|
+
elif format == OutputFormat.JSON:
|
|
40
|
+
print(json.dumps(data, indent=2, default=str))
|
|
41
|
+
elif format == OutputFormat.YAML:
|
|
42
|
+
print(yaml.dump(data, default_flow_style=False, sort_keys=False))
|
|
43
|
+
|
|
5
44
|
|
|
6
45
|
def parse_resource_id(resource_id: str) -> tuple[str, str]:
|
|
7
46
|
"""Parse resource identifier into provider and resource type.
|
pragma_cli/main.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""CLI entry point with Typer application setup and command routing."""
|
|
2
2
|
|
|
3
|
+
from importlib.metadata import version as get_version
|
|
3
4
|
from typing import Annotated
|
|
4
5
|
|
|
5
6
|
import typer
|
|
@@ -13,9 +14,34 @@ from pragma_cli.config import get_current_context
|
|
|
13
14
|
app = typer.Typer()
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
def _version_callback(value: bool) -> None:
|
|
18
|
+
"""Print version and exit if --version flag is provided.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
value: True if --version flag was provided.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
typer.Exit: Always exits after displaying version.
|
|
25
|
+
"""
|
|
26
|
+
if value:
|
|
27
|
+
package_version = get_version("pragmatiks-cli")
|
|
28
|
+
typer.echo(f"pragma {package_version}")
|
|
29
|
+
raise typer.Exit()
|
|
30
|
+
|
|
31
|
+
|
|
16
32
|
@app.callback()
|
|
17
33
|
def main(
|
|
18
34
|
ctx: typer.Context,
|
|
35
|
+
version: Annotated[
|
|
36
|
+
bool | None,
|
|
37
|
+
typer.Option(
|
|
38
|
+
"--version",
|
|
39
|
+
"-V",
|
|
40
|
+
help="Show version and exit",
|
|
41
|
+
callback=_version_callback,
|
|
42
|
+
is_eager=True,
|
|
43
|
+
),
|
|
44
|
+
] = None,
|
|
19
45
|
context: Annotated[
|
|
20
46
|
str | None,
|
|
21
47
|
typer.Option(
|
|
@@ -37,14 +63,14 @@ def main(
|
|
|
37
63
|
"""Pragma CLI - Declarative resource management.
|
|
38
64
|
|
|
39
65
|
Authentication (industry-standard pattern):
|
|
40
|
-
- CLI writes credentials: 'pragma login' stores tokens in ~/.config/pragma/credentials
|
|
66
|
+
- CLI writes credentials: 'pragma auth login' stores tokens in ~/.config/pragma/credentials
|
|
41
67
|
- SDK reads credentials: Automatic token discovery via precedence chain
|
|
42
68
|
|
|
43
69
|
Token Discovery Precedence:
|
|
44
70
|
1. --token flag (explicit override)
|
|
45
71
|
2. PRAGMA_AUTH_TOKEN_<CONTEXT> context-specific environment variable
|
|
46
72
|
3. PRAGMA_AUTH_TOKEN environment variable
|
|
47
|
-
4. ~/.config/pragma/credentials file (from pragma login)
|
|
73
|
+
4. ~/.config/pragma/credentials file (from pragma auth login)
|
|
48
74
|
5. No authentication
|
|
49
75
|
"""
|
|
50
76
|
context_name, context_config = get_current_context(context)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
pragma_cli/__init__.py,sha256=9REbOdKs9CeuOd-rxeFs17gWtou1dUdCogYU8G5Cz6c,682
|
|
2
|
+
pragma_cli/commands/__init__.py,sha256=zltFPaCZgkeTdOH1YWrUEqqBF9Dg6tokgAFcmqP4_n4,24
|
|
3
|
+
pragma_cli/commands/auth.py,sha256=ADpPH8jDbpwhyRzwvG_4PAg2oxVbt2vGx1LrAZNIq-I,9968
|
|
4
|
+
pragma_cli/commands/completions.py,sha256=0SFfq1V0QLjLc4Kr6wcRvQF0UQpB9lZ6V5fU6SVtsbM,3610
|
|
5
|
+
pragma_cli/commands/config.py,sha256=fq4G6DwgTaEZLZIcGdLXzEh4Zrv6M6_ew0IU7WytQk0,2648
|
|
6
|
+
pragma_cli/commands/dead_letter.py,sha256=8Mh_QVZiwkbOA1fYkw1O9BeHgPdqepis6tSJOAY3vhA,6754
|
|
7
|
+
pragma_cli/commands/ops.py,sha256=ztx0Gx2L2mEqJQpbgDHgfOUZ4uaD132NxgKohaPOWv8,361
|
|
8
|
+
pragma_cli/commands/providers.py,sha256=Ab7Oh-WGYvl8CauGiG3e2bFJVFo4kB4NbHXsolsFNoo,29265
|
|
9
|
+
pragma_cli/commands/resources.py,sha256=HpgxD-Rx8IWk1CBVwRcxuhEYd8xe0vqSrsPwfzotiJU,16533
|
|
10
|
+
pragma_cli/config.py,sha256=2r9kcBrh700AWF0H3gMIpYX8uXFSe2Yk2T8tuXzjCaU,2984
|
|
11
|
+
pragma_cli/helpers.py,sha256=lR-s_Q7YNuWcPeluHZO3RrsdpKq8ndwWYLSlzRvY35w,1681
|
|
12
|
+
pragma_cli/main.py,sha256=_S2X3QLfuGcULBfwezp4RBK1_PFkHY_L_8j0hZbT2gk,2676
|
|
13
|
+
pragma_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
pragmatiks_cli-0.12.5.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
|
|
15
|
+
pragmatiks_cli-0.12.5.dist-info/entry_points.txt,sha256=9xeQQlnHxq94dks6mlJ2I9LuMUKmqxuJzyKSZCb9iJM,48
|
|
16
|
+
pragmatiks_cli-0.12.5.dist-info/METADATA,sha256=E8HFQsJjup_TPTswEsrHq8MFuKm8HpPdt5MoRCZWXF0,4466
|
|
17
|
+
pragmatiks_cli-0.12.5.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
pragma_cli/__init__.py,sha256=9REbOdKs9CeuOd-rxeFs17gWtou1dUdCogYU8G5Cz6c,682
|
|
2
|
-
pragma_cli/commands/__init__.py,sha256=zltFPaCZgkeTdOH1YWrUEqqBF9Dg6tokgAFcmqP4_n4,24
|
|
3
|
-
pragma_cli/commands/auth.py,sha256=HSv4mMuA-2QjYDYf69IQd-yXFANr8Uzw-oz3bAy_R0o,9311
|
|
4
|
-
pragma_cli/commands/completions.py,sha256=DwiyVANoYfDIDSNKwgb4QV_tDH2k5RjXgt3Z7TNNi-A,2872
|
|
5
|
-
pragma_cli/commands/config.py,sha256=Bow71Tg_zeprHlbn_6y8g2YsV_k9nRobA6ooYfaxtWE,2281
|
|
6
|
-
pragma_cli/commands/dead_letter.py,sha256=8Mh_QVZiwkbOA1fYkw1O9BeHgPdqepis6tSJOAY3vhA,6754
|
|
7
|
-
pragma_cli/commands/ops.py,sha256=ztx0Gx2L2mEqJQpbgDHgfOUZ4uaD132NxgKohaPOWv8,361
|
|
8
|
-
pragma_cli/commands/providers.py,sha256=5OI1Rlgk4bN8OqMVhCVIQCOo3OqAZStDuV_4YXBgWeg,29168
|
|
9
|
-
pragma_cli/commands/resources.py,sha256=_1Te1HqEayOEcpKIIJPI6ud_yk1F19Uaw-SRNQ8kCSs,7031
|
|
10
|
-
pragma_cli/config.py,sha256=kcU4tJUV1DfnXC_ydQbgQoJzjdGB1s-6-7g4cwA1nZw,2340
|
|
11
|
-
pragma_cli/helpers.py,sha256=dVxokT-sqF08oY0O35QZ64QyNC0PHZEBJYTvZ726UtI,650
|
|
12
|
-
pragma_cli/main.py,sha256=DBclLuSeymcockptWgzX5I1euqrRrHgnrTgGAsF74Xc,1973
|
|
13
|
-
pragma_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
pragmatiks_cli-0.8.1.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
|
|
15
|
-
pragmatiks_cli-0.8.1.dist-info/entry_points.txt,sha256=9xeQQlnHxq94dks6mlJ2I9LuMUKmqxuJzyKSZCb9iJM,48
|
|
16
|
-
pragmatiks_cli-0.8.1.dist-info/METADATA,sha256=qQTi7V0AAFGf_EsVWV36cQgslubzeBa0O2mcZmsAyy0,4465
|
|
17
|
-
pragmatiks_cli-0.8.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|