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/auth.py
CHANGED
|
@@ -5,20 +5,46 @@ import webbrowser
|
|
|
5
5
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
6
6
|
from urllib.parse import parse_qs, urlparse
|
|
7
7
|
|
|
8
|
+
import httpx
|
|
8
9
|
import typer
|
|
10
|
+
from pragma_sdk import PragmaClient
|
|
9
11
|
from pragma_sdk.config import load_credentials
|
|
10
12
|
from rich import print
|
|
13
|
+
from rich.console import Console
|
|
11
14
|
|
|
12
|
-
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
|
+
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
app = typer.Typer()
|
|
16
22
|
|
|
17
23
|
CALLBACK_PORT = int(os.getenv("PRAGMA_AUTH_CALLBACK_PORT", "8765"))
|
|
18
24
|
CALLBACK_PATH = os.getenv("PRAGMA_AUTH_CALLBACK_PATH", "/auth/callback")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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}"
|
|
22
48
|
|
|
23
49
|
|
|
24
50
|
class CallbackHandler(BaseHTTPRequestHandler):
|
|
@@ -136,41 +162,50 @@ def clear_credentials(context_name: str | None = None):
|
|
|
136
162
|
|
|
137
163
|
|
|
138
164
|
@app.command()
|
|
139
|
-
def login(
|
|
165
|
+
def login(
|
|
166
|
+
context: str | None = typer.Option(None, help="Context to authenticate for (default: current)"),
|
|
167
|
+
):
|
|
140
168
|
"""Authenticate with Pragma using browser-based Clerk login.
|
|
141
169
|
|
|
142
170
|
Opens your default browser to Clerk authentication page. After successful
|
|
143
171
|
login, your credentials are stored locally in ~/.config/pragma/credentials.
|
|
144
172
|
|
|
145
173
|
Example:
|
|
146
|
-
pragma login
|
|
147
|
-
pragma login --context production
|
|
174
|
+
pragma auth login
|
|
175
|
+
pragma auth login --context production
|
|
148
176
|
|
|
149
177
|
Raises:
|
|
150
178
|
typer.Exit: If context not found or authentication fails/times out.
|
|
151
179
|
"""
|
|
152
180
|
config = load_config()
|
|
181
|
+
|
|
182
|
+
# Use current context if not specified
|
|
183
|
+
if context is None:
|
|
184
|
+
context = config.current_context
|
|
185
|
+
|
|
153
186
|
if context not in config.contexts:
|
|
154
187
|
print(f"[red]\u2717[/red] Context '{context}' not found")
|
|
155
188
|
print(f"Available contexts: {', '.join(config.contexts.keys())}")
|
|
156
189
|
raise typer.Exit(1)
|
|
157
190
|
|
|
158
|
-
|
|
191
|
+
context_config = config.contexts[context]
|
|
192
|
+
auth_url = context_config.get_auth_url()
|
|
193
|
+
login_url = _get_login_url(context_config)
|
|
159
194
|
|
|
160
195
|
print(f"[cyan]Authenticating for context:[/cyan] {context}")
|
|
161
|
-
print(f"[cyan]API URL:[/cyan] {api_url}")
|
|
196
|
+
print(f"[cyan]API URL:[/cyan] {context_config.api_url}")
|
|
162
197
|
print()
|
|
163
198
|
|
|
164
199
|
server = HTTPServer(("localhost", CALLBACK_PORT), CallbackHandler)
|
|
165
200
|
|
|
166
|
-
print(f"[yellow]Opening browser to:[/yellow] {
|
|
201
|
+
print(f"[yellow]Opening browser to:[/yellow] {auth_url}")
|
|
167
202
|
print()
|
|
168
203
|
print("[dim]If browser doesn't open automatically, visit:[/dim]")
|
|
169
|
-
print(f"[dim]{
|
|
204
|
+
print(f"[dim]{login_url}[/dim]")
|
|
170
205
|
print()
|
|
171
206
|
print("[yellow]Waiting for authentication...[/yellow]")
|
|
172
207
|
|
|
173
|
-
webbrowser.open(
|
|
208
|
+
webbrowser.open(login_url)
|
|
174
209
|
|
|
175
210
|
server.timeout = 300
|
|
176
211
|
server.handle_request()
|
|
@@ -218,30 +253,49 @@ def logout(
|
|
|
218
253
|
|
|
219
254
|
@app.command()
|
|
220
255
|
def whoami():
|
|
221
|
-
"""Show current authentication status.
|
|
256
|
+
"""Show current authentication status and user information.
|
|
222
257
|
|
|
223
|
-
Displays
|
|
258
|
+
Displays the current context, authentication state, and user details
|
|
259
|
+
including email and organization name from the API.
|
|
224
260
|
"""
|
|
225
|
-
|
|
226
|
-
|
|
261
|
+
current_context_name, current_context_config = get_current_context()
|
|
262
|
+
token = load_credentials(current_context_name)
|
|
263
|
+
|
|
264
|
+
console.print()
|
|
265
|
+
console.print("[bold]Authentication Status[/bold]")
|
|
266
|
+
console.print()
|
|
267
|
+
|
|
268
|
+
if not token:
|
|
269
|
+
console.print(f" Context: [cyan]{current_context_name}[/cyan]")
|
|
270
|
+
console.print(" Status: [yellow]Not authenticated[/yellow]")
|
|
271
|
+
console.print()
|
|
272
|
+
console.print("[dim]Run 'pragma auth login' to authenticate[/dim]")
|
|
273
|
+
return
|
|
227
274
|
|
|
228
|
-
print()
|
|
229
|
-
print("[
|
|
230
|
-
print()
|
|
275
|
+
console.print(f" Context: [cyan]{current_context_name}[/cyan]")
|
|
276
|
+
console.print(" Status: [green]\u2713 Authenticated[/green]")
|
|
231
277
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
marker = "[green]*[/green]" if context_name == current_context_name else " "
|
|
278
|
+
try:
|
|
279
|
+
client = PragmaClient(base_url=current_context_config.api_url, auth_token=token)
|
|
280
|
+
user_info = client.get_me()
|
|
236
281
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
282
|
+
console.print()
|
|
283
|
+
console.print("[bold]User Information[/bold]")
|
|
284
|
+
console.print()
|
|
285
|
+
console.print(f" User ID: [cyan]{user_info.user_id}[/cyan]")
|
|
286
|
+
if user_info.email:
|
|
287
|
+
console.print(f" Email: [cyan]{user_info.email}[/cyan]")
|
|
240
288
|
else:
|
|
241
|
-
print(
|
|
289
|
+
console.print(" Email: [dim]Not set[/dim]")
|
|
290
|
+
console.print(f" Organization: [cyan]{user_info.organization_name or user_info.organization_id}[/cyan]")
|
|
242
291
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
292
|
+
except httpx.HTTPStatusError as e:
|
|
293
|
+
if e.response.status_code == 401:
|
|
294
|
+
console.print()
|
|
295
|
+
console.print("[yellow]Token expired or invalid. Run 'pragma auth login' to re-authenticate.[/yellow]")
|
|
296
|
+
else:
|
|
297
|
+
console.print()
|
|
298
|
+
console.print(f"[red]Error fetching user info:[/red] {e.response.text}")
|
|
299
|
+
except httpx.RequestError as e:
|
|
300
|
+
console.print()
|
|
301
|
+
console.print(f"[red]Connection error:[/red] {e}")
|
|
@@ -1,10 +1,77 @@
|
|
|
1
|
-
"""CLI auto-completion functions for resource operations."""
|
|
1
|
+
"""CLI auto-completion functions for resource and provider operations."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
|
+
from pragma_sdk import PragmaClient
|
|
6
7
|
|
|
7
|
-
from pragma_cli import
|
|
8
|
+
from pragma_cli.config import get_current_context
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_completion_client() -> PragmaClient | None:
|
|
12
|
+
"""Get a client for shell completion context.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
PragmaClient instance or None if configuration unavailable.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
context_name, context_config = get_current_context()
|
|
19
|
+
if context_config is None:
|
|
20
|
+
return None
|
|
21
|
+
return PragmaClient(
|
|
22
|
+
base_url=context_config.api_url,
|
|
23
|
+
context=context_name,
|
|
24
|
+
require_auth=False,
|
|
25
|
+
)
|
|
26
|
+
except Exception:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def completion_provider_ids(incomplete: str):
|
|
31
|
+
"""Complete provider identifiers based on deployed providers.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
incomplete: Partial input to complete against available providers.
|
|
35
|
+
|
|
36
|
+
Yields:
|
|
37
|
+
Provider IDs matching the incomplete input.
|
|
38
|
+
"""
|
|
39
|
+
client = _get_completion_client()
|
|
40
|
+
if client is None:
|
|
41
|
+
return
|
|
42
|
+
try:
|
|
43
|
+
providers = client.list_providers()
|
|
44
|
+
except Exception:
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
for provider in providers:
|
|
48
|
+
if provider.provider_id.lower().startswith(incomplete.lower()):
|
|
49
|
+
yield provider.provider_id
|
|
50
|
+
|
|
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
|
|
8
75
|
|
|
9
76
|
|
|
10
77
|
def completion_resource_ids(incomplete: str):
|
|
@@ -16,7 +83,9 @@ def completion_resource_ids(incomplete: str):
|
|
|
16
83
|
Yields:
|
|
17
84
|
Resource identifiers matching the incomplete input.
|
|
18
85
|
"""
|
|
19
|
-
client =
|
|
86
|
+
client = _get_completion_client()
|
|
87
|
+
if client is None:
|
|
88
|
+
return
|
|
20
89
|
try:
|
|
21
90
|
resources = client.list_resources()
|
|
22
91
|
except Exception:
|
|
@@ -40,7 +109,9 @@ def completion_resource_names(ctx: typer.Context, incomplete: str):
|
|
|
40
109
|
Yields:
|
|
41
110
|
Resource names matching the incomplete input for the selected resource type.
|
|
42
111
|
"""
|
|
43
|
-
client =
|
|
112
|
+
client = _get_completion_client()
|
|
113
|
+
if client is None:
|
|
114
|
+
return
|
|
44
115
|
resource_id = ctx.params.get("resource_id")
|
|
45
116
|
if not resource_id or "/" not in resource_id:
|
|
46
117
|
return
|
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()
|