pragmatiks-cli 0.5.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 +86 -32
- pragma_cli/commands/completions.py +75 -4
- pragma_cli/commands/config.py +8 -1
- pragma_cli/commands/{provider.py → providers.py} +130 -332
- pragma_cli/commands/resources.py +344 -57
- pragma_cli/config.py +19 -0
- pragma_cli/helpers.py +40 -1
- pragma_cli/main.py +30 -4
- {pragmatiks_cli-0.5.1.dist-info → pragmatiks_cli-0.12.5.dist-info}/METADATA +9 -7
- pragmatiks_cli-0.12.5.dist-info/RECORD +17 -0
- {pragmatiks_cli-0.5.1.dist-info → pragmatiks_cli-0.12.5.dist-info}/WHEEL +2 -2
- pragmatiks_cli-0.5.1.dist-info/RECORD +0 -17
- {pragmatiks_cli-0.5.1.dist-info → pragmatiks_cli-0.12.5.dist-info}/entry_points.txt +0 -0
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,32 +144,288 @@ 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
|
-
|
|
97
|
-
|
|
218
|
+
resources = list(client.list_resources(provider=provider, resource=resource, tags=tags))
|
|
219
|
+
|
|
220
|
+
if not resources:
|
|
221
|
+
console.print("[dim]No resources found.[/dim]")
|
|
222
|
+
return
|
|
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
|
+
|
|
243
|
+
for res in resources:
|
|
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)}")
|
|
98
268
|
|
|
99
269
|
|
|
100
270
|
@app.command()
|
|
101
271
|
def get(
|
|
102
272
|
resource_id: Annotated[str, typer.Argument(autocompletion=completion_resource_ids)],
|
|
103
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,
|
|
104
275
|
):
|
|
105
|
-
"""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
|
+
"""
|
|
106
283
|
client = get_client()
|
|
107
284
|
provider, resource = parse_resource_id(resource_id)
|
|
108
285
|
if name:
|
|
109
286
|
res = client.get_resource(provider=provider, resource=resource, name=name)
|
|
110
|
-
|
|
287
|
+
output_data([res], output, table_renderer=_print_resources_table)
|
|
111
288
|
else:
|
|
112
|
-
|
|
113
|
-
|
|
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)
|
|
114
429
|
|
|
115
430
|
|
|
116
431
|
@app.command()
|
|
@@ -128,6 +443,9 @@ def apply(
|
|
|
128
443
|
For pragma/secret resources, file references in config.data values
|
|
129
444
|
are resolved before submission. Use '@path/to/file' syntax to inline
|
|
130
445
|
file contents.
|
|
446
|
+
|
|
447
|
+
Raises:
|
|
448
|
+
typer.Exit: If the apply operation fails.
|
|
131
449
|
"""
|
|
132
450
|
client = get_client()
|
|
133
451
|
for f in file:
|
|
@@ -138,9 +456,14 @@ def apply(
|
|
|
138
456
|
resource = resolve_file_references(resource, base_dir)
|
|
139
457
|
if pending:
|
|
140
458
|
resource["lifecycle_state"] = "pending"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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)
|
|
144
467
|
|
|
145
468
|
|
|
146
469
|
@app.command()
|
|
@@ -148,52 +471,16 @@ def delete(
|
|
|
148
471
|
resource_id: Annotated[str, typer.Argument(autocompletion=completion_resource_ids)],
|
|
149
472
|
name: Annotated[str, typer.Argument(autocompletion=completion_resource_names)],
|
|
150
473
|
):
|
|
151
|
-
"""Delete a resource.
|
|
152
|
-
client = get_client()
|
|
153
|
-
provider, resource = parse_resource_id(resource_id)
|
|
154
|
-
client.delete_resource(provider=provider, resource=resource, name=name)
|
|
155
|
-
print(f"Deleted {resource_id}/{name}")
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
@app.command()
|
|
159
|
-
def register(
|
|
160
|
-
resource_id: Annotated[str, typer.Argument(help="Resource type in provider/resource format")],
|
|
161
|
-
description: Annotated[str | None, typer.Option("--description", "-d", help="Resource type description")] = None,
|
|
162
|
-
schema_file: Annotated[typer.FileText | None, typer.Option("--schema", "-s", help="JSON schema file")] = None,
|
|
163
|
-
tags: Annotated[list[str] | None, typer.Option("--tag", "-t", help="Tags for categorization")] = None,
|
|
164
|
-
):
|
|
165
|
-
"""Register a new resource type.
|
|
474
|
+
"""Delete a resource.
|
|
166
475
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
"""
|
|
170
|
-
client = get_client()
|
|
171
|
-
provider, resource = parse_resource_id(resource_id)
|
|
172
|
-
|
|
173
|
-
schema = None
|
|
174
|
-
if schema_file:
|
|
175
|
-
schema = json.load(schema_file)
|
|
176
|
-
|
|
177
|
-
client.register_resource(
|
|
178
|
-
provider=provider,
|
|
179
|
-
resource=resource,
|
|
180
|
-
schema=schema,
|
|
181
|
-
description=description,
|
|
182
|
-
tags=tags,
|
|
183
|
-
)
|
|
184
|
-
print(f"Registered {resource_id}")
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
@app.command()
|
|
188
|
-
def unregister(
|
|
189
|
-
resource_id: Annotated[str, typer.Argument(autocompletion=completion_resource_ids)],
|
|
190
|
-
):
|
|
191
|
-
"""Unregister a resource type.
|
|
192
|
-
|
|
193
|
-
Removes a resource type registration. Existing resources of this type
|
|
194
|
-
will no longer be manageable.
|
|
476
|
+
Raises:
|
|
477
|
+
typer.Exit: If the resource is not found or deletion fails.
|
|
195
478
|
"""
|
|
196
479
|
client = get_client()
|
|
197
480
|
provider, resource = parse_resource_id(resource_id)
|
|
198
|
-
|
|
199
|
-
|
|
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,21 +1,47 @@
|
|
|
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
|
|
6
7
|
from pragma_sdk import PragmaClient
|
|
7
8
|
|
|
8
9
|
from pragma_cli import set_client
|
|
9
|
-
from pragma_cli.commands import auth, config, ops,
|
|
10
|
+
from pragma_cli.commands import auth, config, ops, providers, resources
|
|
10
11
|
from pragma_cli.config import get_current_context
|
|
11
12
|
|
|
12
13
|
|
|
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)
|
|
@@ -61,7 +87,7 @@ app.add_typer(resources.app, name="resources")
|
|
|
61
87
|
app.add_typer(auth.app, name="auth")
|
|
62
88
|
app.add_typer(config.app, name="config")
|
|
63
89
|
app.add_typer(ops.app, name="ops")
|
|
64
|
-
app.add_typer(
|
|
90
|
+
app.add_typer(providers.app, name="providers")
|
|
65
91
|
|
|
66
92
|
if __name__ == "__main__": # pragma: no cover
|
|
67
93
|
app()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pragmatiks-cli
|
|
3
|
-
Version: 0.5
|
|
3
|
+
Version: 0.12.5
|
|
4
4
|
Summary: Command-line interface for Pragmatiks
|
|
5
5
|
Requires-Dist: typer>=0.15.3
|
|
6
6
|
Requires-Dist: pragmatiks-sdk>=0.6.0
|
|
@@ -10,19 +10,21 @@ Requires-Dist: rich>=13.9.0
|
|
|
10
10
|
Requires-Python: >=3.13
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
<p align="center">
|
|
14
|
+
<img src="assets/wordmark.png" alt="Pragma-OS" width="800">
|
|
15
|
+
</p>
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
# Pragma CLI
|
|
18
|
+
|
|
19
|
+
[](https://deepwiki.com/pragmatiks/pragma-cli)
|
|
16
20
|
[](https://pypi.org/project/pragmatiks-cli/)
|
|
17
21
|
[](https://www.python.org/downloads/)
|
|
18
22
|
[](https://opensource.org/licenses/MIT)
|
|
19
23
|
[](https://github.com/astral-sh/ruff)
|
|
20
24
|
|
|
21
|
-
**[Documentation](https://docs.pragmatiks.io/cli/overview)** | **[SDK](https://github.com/pragmatiks/sdk)** | **[Providers](https://github.com/pragmatiks/providers)**
|
|
22
|
-
|
|
23
|
-
Command-line interface for managing Pragmatiks resources.
|
|
25
|
+
**[Documentation](https://docs.pragmatiks.io/cli/overview)** | **[SDK](https://github.com/pragmatiks/pragma-sdk)** | **[Providers](https://github.com/pragmatiks/pragma-providers)**
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
Command-line interface for managing pragma-os resources.
|
|
26
28
|
|
|
27
29
|
## Quick Start
|
|
28
30
|
|
|
@@ -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,,
|