pipecatcloud 0.0.6__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.
@@ -0,0 +1,351 @@
1
+ import aiohttp
2
+ import questionary
3
+ import typer
4
+ from rich import box
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.table import Table
8
+
9
+ from pipecatcloud import PIPECAT_CLI_NAME
10
+ from pipecatcloud._utils.async_utils import synchronizer
11
+ from pipecatcloud._utils.auth_utils import requires_login
12
+ from pipecatcloud._utils.console_utils import print_api_error
13
+ from pipecatcloud._utils.http_utils import construct_api_url
14
+ from pipecatcloud.cli import PANEL_TITLE_SUCCESS
15
+ from pipecatcloud.config import _store_user_config, config, user_config_path
16
+ from pipecatcloud.exception import AuthError
17
+
18
+ organization_cli = typer.Typer(
19
+ name="organizations", help="User organizations.", no_args_is_help=True
20
+ )
21
+ keys_cli = typer.Typer(name="keys", help="API key management commands.", no_args_is_help=True)
22
+ organization_cli.add_typer(keys_cli)
23
+
24
+
25
+ # ---- Organization Methods ----
26
+
27
+ async def _retrieve_organizations(ctx: typer.Context):
28
+ console = Console()
29
+ token = ctx.obj["token"]
30
+ org_list = []
31
+ with console.status("Fetching user organizations", spinner="dots"):
32
+ async with aiohttp.ClientSession() as session:
33
+ try:
34
+ async with session.get(
35
+ f"{config.get('server_url')}{config.get('organization_path')}",
36
+ headers={"Authorization": f"Bearer {token}"},
37
+ ) as resp:
38
+ if resp.status == 401:
39
+ raise AuthError()
40
+ if resp.status == 200:
41
+ data = await resp.json()
42
+ org_list = data["organizations"]
43
+ else:
44
+ raise Exception(f"Failed to retrieve account organization: {resp.status}")
45
+ except Exception as e:
46
+ console.print(f"[red]Error: {e}[/red]")
47
+
48
+ return org_list
49
+
50
+
51
+ async def _get_api_tokens(org_id: str, token: str):
52
+ if not org_id:
53
+ raise ValueError("Organization ID is required")
54
+ if not token:
55
+ raise ValueError("Token is required")
56
+
57
+ async with aiohttp.ClientSession() as session:
58
+ response = await session.get(
59
+ construct_api_url('api_keys_path').format(org=org_id),
60
+ headers={"Authorization": f"Bearer {token}"},
61
+ )
62
+ response.raise_for_status()
63
+ data = await response.json()
64
+ return data
65
+
66
+
67
+ # ---- Organization Commands ----
68
+ @organization_cli.command(name="select", help="Select an organization to use.")
69
+ @synchronizer.create_blocking
70
+ @requires_login
71
+ async def select(ctx: typer.Context):
72
+ console = Console()
73
+ current_org = ctx.obj["org"]
74
+ org_list = await _retrieve_organizations(ctx)
75
+
76
+ value = await questionary.select(
77
+ "Select active organization",
78
+ choices=[{"name": f"{org['verboseName']} ({org['name']})", "value": (org["name"], org["verboseName"]), "checked": org["name"] == current_org} for org in org_list],
79
+ ).ask_async()
80
+
81
+ if not value:
82
+ return
83
+
84
+ _store_user_config(ctx.obj["token"], value[0])
85
+
86
+ console.print(Panel(
87
+ f"Current organization set to [bold green]{value[1]} [dim]({value[0]})[/dim][/bold green]\n"
88
+ f"[dim]Account updated in {user_config_path}[/dim]",
89
+ title="[green]Organization updated[/green]",
90
+ title_align="left",
91
+ border_style="green",
92
+ ))
93
+
94
+
95
+ @organization_cli.command(name="list", help="List organizations user is a member of.")
96
+ @synchronizer.create_blocking
97
+ @requires_login
98
+ async def list(ctx: typer.Context):
99
+ console = Console()
100
+ current_org = ctx.obj["org"]
101
+ org_list = await _retrieve_organizations(ctx)
102
+
103
+ if len(org_list) == 0:
104
+ console.print("[red]No organizations found[/red]")
105
+ return
106
+ else:
107
+ console.print(f"[green]Found {len(org_list)} organizations[/green]")
108
+
109
+ table = Table(
110
+ border_style="dim",
111
+ show_edge=True,
112
+ show_lines=False)
113
+ table.add_column("Organization", style="white")
114
+ table.add_column("Name", style="white")
115
+ for org in org_list:
116
+ if org["name"] == current_org:
117
+ table.add_row(org["verboseName"], f"[cyan bold]{org['name']} (active)[/cyan bold]")
118
+ else:
119
+ table.add_row(org["verboseName"], org["name"])
120
+
121
+ console.print(table)
122
+
123
+
124
+ # ---- API Token Commands ----
125
+
126
+ @keys_cli.command(name="list", help="List API keys for an organization.")
127
+ @synchronizer.create_blocking
128
+ @requires_login
129
+ async def keys(
130
+ ctx: typer.Context,
131
+ organization: str = typer.Option(
132
+ None,
133
+ "--organization",
134
+ "--org",
135
+ help="Organization to get tokens for",
136
+ ),
137
+ ):
138
+ console = Console()
139
+ token = ctx.obj["token"]
140
+ org = organization or ctx.obj["org"]
141
+
142
+ with console.status(f"Fetching API keys for organization: [bold]'{org}'[/bold]", spinner="dots"):
143
+ data = await _get_api_tokens(org, token)
144
+
145
+ if len(data["public"]) == 0:
146
+ console.print(
147
+ f"[bold]No API keys found.[/bold]\n"
148
+ f"[dim]Create a new API key with the "
149
+ f"[bold]{PIPECAT_CLI_NAME} organizations keys create[/bold] command.[/dim]"
150
+ )
151
+ return
152
+
153
+ table = Table(
154
+ show_header=True,
155
+ show_lines=True,
156
+ border_style="dim",
157
+ box=box.SIMPLE,
158
+ )
159
+ table.add_column("Name")
160
+ table.add_column("Key")
161
+ table.add_column("Created At")
162
+ table.add_column("Status")
163
+
164
+ for key in data["public"]:
165
+ table.add_row(
166
+ key["metadata"]["name"],
167
+ key["key"],
168
+ key["createdAt"],
169
+ "Revoked" if key["revoked"] else "Active",
170
+ style="red" if key["revoked"] else None,
171
+ )
172
+
173
+ console.print(Panel(
174
+ table,
175
+ title=f"[bold]API keys for organization: {org}[/bold]",
176
+ title_align="left",
177
+ ))
178
+
179
+
180
+ @keys_cli.command(name="delete", help="Delete an API key for an organization.")
181
+ @synchronizer.create_blocking
182
+ @requires_login
183
+ async def delete_key(
184
+ ctx: typer.Context,
185
+ organization: str = typer.Option(
186
+ None,
187
+ "--organization",
188
+ "--org",
189
+ help="Organization to get tokens for",
190
+ ),
191
+ ):
192
+ console = Console()
193
+ token = ctx.obj["token"]
194
+ org = organization or ctx.obj["org"]
195
+
196
+ with console.status(f"Fetching API keys for organization: [bold]'{org}'[/bold]", spinner="dots"):
197
+ data = await _get_api_tokens(org, token)
198
+
199
+ if len(data["public"]) == 0:
200
+ console.print(
201
+ f"[bold]No API keys found.[/bold]\n"
202
+ f"[dim]Create a new API key with the "
203
+ f"[bold]{PIPECAT_CLI_NAME} organizations keys create[/bold] command.[/dim]"
204
+ )
205
+ typer.Exit(1)
206
+ return
207
+
208
+ # Prompt user to delete a key
209
+ key_id = await questionary.select(
210
+ "Select API key to delete",
211
+ choices=[{"name": key["metadata"]["name"], "value": key["id"]} for key in data["public"]],
212
+ ).ask_async()
213
+
214
+ if not key_id:
215
+ typer.Exit(1)
216
+
217
+ try:
218
+ error_code = None
219
+ with console.status(f"Deleting API key with ID: [bold]'{key_id}'[/bold]", spinner="dots"):
220
+ async with aiohttp.ClientSession() as session:
221
+ response = await session.delete(
222
+ f"{construct_api_url('api_keys_path').format(org=org)}/{key_id}",
223
+ headers={"Authorization": f"Bearer {token}"},
224
+ )
225
+ if response.status != 204:
226
+ error_code = str(response.status)
227
+ response.raise_for_status()
228
+ except Exception:
229
+ print_api_error(error_code, title="Error deleting API key")
230
+ typer.Exit(1)
231
+
232
+ console.print(f"[green]API key with ID: [bold]'{key_id}'[/bold] deleted successfully.[/green]")
233
+
234
+
235
+ @keys_cli.command(name="create", help="Create an API key for an organization.")
236
+ @synchronizer.create_blocking
237
+ @requires_login
238
+ async def create_key(
239
+ ctx: typer.Context,
240
+ organization: str = typer.Option(
241
+ None,
242
+ "--organization",
243
+ "--org",
244
+ help="Organization to get tokens for",
245
+ ),
246
+ ):
247
+ console = Console()
248
+ token = ctx.obj["token"]
249
+ org = organization or ctx.obj["org"]
250
+
251
+ api_key_name = await questionary.text(
252
+ "Enter human readable name for API key e.g. 'Pipecat Key'"
253
+ ).ask_async()
254
+
255
+ if not api_key_name:
256
+ typer.Exit(1)
257
+
258
+ error_code = None
259
+ data = None
260
+ try:
261
+ with console.status(f"Creating API key with name: [bold]'{api_key_name}'[/bold]", spinner="dots"):
262
+ async with aiohttp.ClientSession() as session:
263
+ response = await session.post(
264
+ f"{construct_api_url('api_keys_path').format(org=org)}",
265
+ headers={"Authorization": f"Bearer {token}"},
266
+ json={"name": api_key_name, "type": "public"},
267
+ )
268
+ if response.status != 200:
269
+ error_code = str(response.status)
270
+ response.raise_for_status()
271
+ data = await response.json()
272
+ except Exception:
273
+ print_api_error(error_code, title="Error creating API key")
274
+ typer.Exit(1)
275
+ return
276
+
277
+ if not data or 'key' not in data:
278
+ console.print("[red]Error: Invalid response from server[/red]")
279
+ typer.Exit(1)
280
+ return
281
+
282
+ table = Table(
283
+ show_header=True,
284
+ show_lines=True,
285
+ border_style="dim",
286
+ box=box.SIMPLE,
287
+ )
288
+ table.add_column("Name")
289
+ table.add_column("Key")
290
+ table.add_column("Organization")
291
+
292
+ table.add_row(
293
+ api_key_name,
294
+ data['key'],
295
+ org,
296
+ )
297
+
298
+ console.print(
299
+ Panel(
300
+ table,
301
+ title=f"[green]{PANEL_TITLE_SUCCESS}[/green]",
302
+ title_align="left",
303
+ border_style="green",
304
+ subtitle=f"Use the key by default by running [bold]{PIPECAT_CLI_NAME} organizations keys use[/bold]",
305
+ subtitle_align="left",
306
+ ))
307
+
308
+
309
+ @keys_cli.command(name="use", help="Set default API key for an organization in local config.")
310
+ @synchronizer.create_blocking
311
+ @requires_login
312
+ async def use_key(
313
+ ctx: typer.Context,
314
+ organization: str = typer.Option(
315
+ None,
316
+ "--organization",
317
+ "--org",
318
+ help="Organization to get tokens for",
319
+ ),
320
+ ):
321
+ console = Console()
322
+ token = ctx.obj["token"]
323
+ org = organization or ctx.obj["org"]
324
+
325
+ with console.status(f"Fetching API keys for organization: [bold]'{org}'[/bold]", spinner="dots"):
326
+ data = await _get_api_tokens(org, token)
327
+
328
+ if len(data["public"]) == 0:
329
+ console.print(
330
+ f"[bold]No API keys found.[/bold]\n"
331
+ f"[dim]Create a new API key with the "
332
+ f"[bold]{PIPECAT_CLI_NAME} organizations keys create[/bold] command.[/dim]"
333
+ )
334
+ typer.Exit(1)
335
+ return
336
+
337
+ # Prompt user to use a key
338
+ key_id = await questionary.select(
339
+ "Select API key to delete",
340
+ choices=[{"name": key["metadata"]["name"], "value": (key["key"], key["metadata"]["name"])} for key in data["public"]],
341
+ ).ask_async()
342
+
343
+ if not key_id:
344
+ typer.Exit(1)
345
+ return
346
+
347
+ _store_user_config(
348
+ token, org, {
349
+ "default_public_key": key_id[0], "default_public_key_name": key_id[1]})
350
+
351
+ console.print(f"[green]API key with ID: [bold]'{key_id}'[/bold] set as default.[/green]")
@@ -0,0 +1,28 @@
1
+ import typer
2
+ from rich.console import Console
3
+ from rich.panel import Panel
4
+
5
+ from pipecatcloud.cli import PANEL_TITLE_NOT_IMPLEMENTED
6
+
7
+ console = Console()
8
+
9
+
10
+ # ----- Run
11
+
12
+
13
+ def create_run_command(app: typer.Typer):
14
+ # Note we wrap the deploy command to avoid circular imports
15
+ @app.command(name="run", help="Run an agent locally")
16
+ def run(
17
+ ctx: typer.Context,
18
+
19
+ ):
20
+ console.print(
21
+ Panel(
22
+ "Local bot runner is not yet implemented.",
23
+ title=PANEL_TITLE_NOT_IMPLEMENTED,
24
+ title_align="left",
25
+ style="yellow",
26
+ border_style="yellow"))
27
+
28
+ return run