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.
@@ -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
- CLERK_FRONTEND_URL = os.getenv("PRAGMA_CLERK_FRONTEND_URL", "https://app.pragmatiks.io")
20
- CALLBACK_URL = f"http://localhost:{CALLBACK_PORT}{CALLBACK_PATH}"
21
- LOGIN_URL = f"{CLERK_FRONTEND_URL}/auth/callback?callback={CALLBACK_URL}"
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(context: str = typer.Option("default", help="Context to authenticate for")):
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
- api_url = config.contexts[context].api_url
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] {CLERK_FRONTEND_URL}")
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]{LOGIN_URL}[/dim]")
204
+ print(f"[dim]{login_url}[/dim]")
170
205
  print()
171
206
  print("[yellow]Waiting for authentication...[/yellow]")
172
207
 
173
- webbrowser.open(LOGIN_URL)
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 which contexts have stored credentials and current authentication state.
258
+ Displays the current context, authentication state, and user details
259
+ including email and organization name from the API.
224
260
  """
225
- config = load_config()
226
- current_context_name, _ = get_current_context()
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("[bold]Authentication Status[/bold]")
230
- print()
275
+ console.print(f" Context: [cyan]{current_context_name}[/cyan]")
276
+ console.print(" Status: [green]\u2713 Authenticated[/green]")
231
277
 
232
- has_any_creds = False
233
- for context_name in config.contexts.keys():
234
- token = load_credentials(context_name)
235
- marker = "[green]*[/green]" if context_name == current_context_name else " "
278
+ try:
279
+ client = PragmaClient(base_url=current_context_config.api_url, auth_token=token)
280
+ user_info = client.get_me()
236
281
 
237
- if token:
238
- has_any_creds = True
239
- print(f"{marker} [cyan]{context_name}[/cyan]: [green]\u2713 Authenticated[/green]")
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(f"{marker} [cyan]{context_name}[/cyan]: [dim]Not authenticated[/dim]")
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
- print()
244
-
245
- if not has_any_creds:
246
- print("[yellow]No stored credentials found[/yellow]")
247
- print("[dim]Run 'pragma login' to authenticate[/dim]")
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 get_client
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 = get_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 = get_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
@@ -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()