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.
- pipecatcloud/__init__.py +12 -0
- pipecatcloud/__main__.py +9 -0
- pipecatcloud/__version__.py +6 -0
- pipecatcloud/_utils/__init__.py +0 -0
- pipecatcloud/_utils/agent_utils.py +9 -0
- pipecatcloud/_utils/async_utils.py +20 -0
- pipecatcloud/_utils/auth_utils.py +26 -0
- pipecatcloud/_utils/console_utils.py +25 -0
- pipecatcloud/_utils/deploy_utils.py +18 -0
- pipecatcloud/_utils/http_utils.py +43 -0
- pipecatcloud/agent.py +83 -0
- pipecatcloud/cli/__init__.py +3 -0
- pipecatcloud/cli/agent.py +405 -0
- pipecatcloud/cli/auth.py +274 -0
- pipecatcloud/cli/deploy.py +230 -0
- pipecatcloud/cli/entry_point.py +83 -0
- pipecatcloud/cli/init.py +0 -0
- pipecatcloud/cli/organizations.py +351 -0
- pipecatcloud/cli/run.py +28 -0
- pipecatcloud/cli/secrets.py +426 -0
- pipecatcloud/config.py +158 -0
- pipecatcloud/errors.py +11 -0
- pipecatcloud/exception.py +55 -0
- pipecatcloud-0.0.6.dist-info/METADATA +13 -0
- pipecatcloud-0.0.6.dist-info/RECORD +28 -0
- pipecatcloud-0.0.6.dist-info/WHEEL +5 -0
- pipecatcloud-0.0.6.dist-info/entry_points.txt +3 -0
- pipecatcloud-0.0.6.dist-info/top_level.txt +1 -0
|
@@ -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]")
|
pipecatcloud/cli/run.py
ADDED
|
@@ -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
|