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,426 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
import aiohttp
|
|
5
|
+
import questionary
|
|
6
|
+
import typer
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from pipecatcloud._utils.async_utils import synchronizer
|
|
13
|
+
from pipecatcloud._utils.auth_utils import requires_login
|
|
14
|
+
from pipecatcloud._utils.http_utils import construct_api_url
|
|
15
|
+
from pipecatcloud.cli import PANEL_TITLE_ERROR, PANEL_TITLE_SUCCESS
|
|
16
|
+
|
|
17
|
+
secrets_cli = typer.Typer(
|
|
18
|
+
name="secrets", help="Secret management.", no_args_is_help=True
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_secrets(secrets: dict):
|
|
25
|
+
valid_name_pattern = re.compile(r'^[a-zA-Z0-9_-]+$')
|
|
26
|
+
|
|
27
|
+
for key, value in secrets.items():
|
|
28
|
+
if not key or not value:
|
|
29
|
+
console.print(
|
|
30
|
+
"[red]Error: Secrets must be provided as key-value pairs. Please reference --help for more information.[/red]")
|
|
31
|
+
return typer.Exit(1)
|
|
32
|
+
|
|
33
|
+
if len(key) > 64:
|
|
34
|
+
console.print(
|
|
35
|
+
"[red]Error: Secret names must not exceed 64 characters in length.[/red]")
|
|
36
|
+
return typer.Exit(1)
|
|
37
|
+
|
|
38
|
+
if not valid_name_pattern.match(key):
|
|
39
|
+
console.print(
|
|
40
|
+
"[red]Error: Secret names must contain only alphanumeric characters, underscores, and hyphens.[/red]")
|
|
41
|
+
return typer.Exit(1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def _get_secret_set(name: str, org: str, token: str):
|
|
45
|
+
request_url = f"{construct_api_url('secrets_path').format(org=org)}/{name}"
|
|
46
|
+
logger.debug(f"Requesting secret set information from {request_url}")
|
|
47
|
+
|
|
48
|
+
async with aiohttp.ClientSession() as session:
|
|
49
|
+
response = await session.get(request_url, headers={"Authorization": f"Bearer {token}"})
|
|
50
|
+
if response.status != 200:
|
|
51
|
+
return None
|
|
52
|
+
return await response.json()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def _create_secret(name: str, secret_key: str, secret_value: str, org: str, token: str):
|
|
56
|
+
request_url = f"{construct_api_url('secrets_path').format(org=org)}/{name}"
|
|
57
|
+
async with aiohttp.ClientSession() as session:
|
|
58
|
+
response = await session.put(request_url, headers={"Authorization": f"Bearer {token}"}, json={
|
|
59
|
+
"name": name,
|
|
60
|
+
"isImagePullSecret": False,
|
|
61
|
+
"secretKey": secret_key,
|
|
62
|
+
"secretValue": secret_value
|
|
63
|
+
})
|
|
64
|
+
response.raise_for_status()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def _delete_secret_set(name: str, org: str, token: str):
|
|
68
|
+
request_url = f"{construct_api_url('secrets_path').format(org=org)}/{name}"
|
|
69
|
+
logger.debug(f"Deleting secret set {name} from {request_url}")
|
|
70
|
+
|
|
71
|
+
async with aiohttp.ClientSession() as session:
|
|
72
|
+
response = await session.delete(request_url, headers={"Authorization": f"Bearer {token}"})
|
|
73
|
+
response.raise_for_status()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def _delete_secret(name: str, secret_name: str, org: str, token: str):
|
|
77
|
+
request_url = f"{construct_api_url('secrets_path').format(org=org)}/{name}/{secret_name}"
|
|
78
|
+
logger.debug(f"Deleting secret {secret_name} from {request_url}")
|
|
79
|
+
|
|
80
|
+
async with aiohttp.ClientSession() as session:
|
|
81
|
+
response = await session.delete(request_url, headers={"Authorization": f"Bearer {token}"})
|
|
82
|
+
response.raise_for_status()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def _create_image_pull_secret(name: str, host: str, credentials: str, org: str, token: str):
|
|
86
|
+
request_url = f"{construct_api_url('secrets_path').format(org=org)}/{name}"
|
|
87
|
+
async with aiohttp.ClientSession() as session:
|
|
88
|
+
response = await session.put(request_url, headers={"Authorization": f"Bearer {token}"}, json={
|
|
89
|
+
"isImagePullSecret": True,
|
|
90
|
+
"secretValue": credentials,
|
|
91
|
+
"host": host
|
|
92
|
+
})
|
|
93
|
+
response.raise_for_status()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@secrets_cli.command(name="set", help="Create a new secret set for active organization")
|
|
97
|
+
@synchronizer.create_blocking
|
|
98
|
+
@requires_login
|
|
99
|
+
async def set(
|
|
100
|
+
ctx: typer.Context,
|
|
101
|
+
name: str = typer.Argument(
|
|
102
|
+
None,
|
|
103
|
+
help="Name of the secret set to create e.g. 'my-secret-set'"
|
|
104
|
+
),
|
|
105
|
+
secrets: list[str] = typer.Argument(
|
|
106
|
+
None,
|
|
107
|
+
help="List of secret key-value pairs e.g. 'KEY1=value1 KEY2=\"value with spaces\"'",
|
|
108
|
+
)
|
|
109
|
+
):
|
|
110
|
+
# Parse secrets
|
|
111
|
+
if not secrets:
|
|
112
|
+
console.print(
|
|
113
|
+
"[red]Error: Secrets must be provided as key-value pairs. Please reference --help for more information.[/red]")
|
|
114
|
+
return typer.Exit(1)
|
|
115
|
+
|
|
116
|
+
secrets_dict = {}
|
|
117
|
+
for secret in secrets:
|
|
118
|
+
if '=' not in secret:
|
|
119
|
+
console.print(
|
|
120
|
+
"[red]Error: Secrets must be provided as key-value pairs using '=' separator. Example: KEY=value[/red]")
|
|
121
|
+
return typer.Exit(1)
|
|
122
|
+
|
|
123
|
+
key, value = secret.split('=', 1) # Split on first = only
|
|
124
|
+
key = key.strip()
|
|
125
|
+
|
|
126
|
+
# Handle quoted values while preserving quotes within the value
|
|
127
|
+
value = value.strip()
|
|
128
|
+
if value.startswith('"') and value.endswith('"'):
|
|
129
|
+
# Remove only the enclosing quotes
|
|
130
|
+
value = value[1:-1]
|
|
131
|
+
|
|
132
|
+
if not key or not value:
|
|
133
|
+
console.print(
|
|
134
|
+
"[red]Error: Both key and value must be provided for each secret.[/red]")
|
|
135
|
+
return typer.Exit(1)
|
|
136
|
+
|
|
137
|
+
secrets_dict[key] = value
|
|
138
|
+
|
|
139
|
+
validate_secrets(secrets_dict)
|
|
140
|
+
|
|
141
|
+
# Prompt for set name if not provided
|
|
142
|
+
if not name:
|
|
143
|
+
name = typer.prompt("Enter the name for your secret set")
|
|
144
|
+
|
|
145
|
+
# Confirm if we are sure we want to create a new secret set (if one doesn't already exist)
|
|
146
|
+
existing_set = None
|
|
147
|
+
with console.status(f"Retrieving secret set [bold]'{name}'[/bold]", spinner="dots"):
|
|
148
|
+
try:
|
|
149
|
+
existing_set = await _get_secret_set(name, ctx.obj["org"], ctx.obj["token"])
|
|
150
|
+
except Exception as e:
|
|
151
|
+
console.print(Panel(
|
|
152
|
+
f"[red]Unable to retrieve secret set. Operation failed with error: {e}[/red]",
|
|
153
|
+
title=f"[red]{PANEL_TITLE_ERROR}[/red]",
|
|
154
|
+
title_align="left",
|
|
155
|
+
border_style="red",
|
|
156
|
+
))
|
|
157
|
+
return typer.Exit(1)
|
|
158
|
+
|
|
159
|
+
# Check for overlapping secret names
|
|
160
|
+
if existing_set:
|
|
161
|
+
existing_secret_names = {secret['fieldName'] for secret in existing_set['secrets']}
|
|
162
|
+
overlapping_secrets = existing_secret_names.intersection(secrets_dict.keys())
|
|
163
|
+
|
|
164
|
+
if overlapping_secrets:
|
|
165
|
+
create = await questionary.confirm(
|
|
166
|
+
f"The following secrets already exist and will be overwritten: {', '.join(overlapping_secrets)}. Would you like to continue?").ask_async()
|
|
167
|
+
if not create:
|
|
168
|
+
console.print("[bold red]Secret set creation cancelled[/bold red]")
|
|
169
|
+
return typer.Exit(1)
|
|
170
|
+
else:
|
|
171
|
+
create = await questionary.confirm(
|
|
172
|
+
f"Secret set with name '{name}' does not exist. Would you like to create it?").ask_async()
|
|
173
|
+
if not create:
|
|
174
|
+
console.print("[bold red]Secret set creation cancelled[/bold red]")
|
|
175
|
+
return typer.Exit(1)
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
with console.status(f"{'Modifying' if existing_set else 'Creating'} secret set [bold]'{name}'[/bold]", spinner="dots"):
|
|
179
|
+
for key, value in secrets_dict.items():
|
|
180
|
+
await _create_secret(name, key, value, ctx.obj["org"], ctx.obj["token"])
|
|
181
|
+
except Exception as e:
|
|
182
|
+
console.print(Panel(
|
|
183
|
+
f"[red]Unable to create secret set. Operation failed with error: {e}[/red]",
|
|
184
|
+
title=f"[red]{PANEL_TITLE_ERROR}[/red]",
|
|
185
|
+
title_align="left",
|
|
186
|
+
border_style="red",
|
|
187
|
+
))
|
|
188
|
+
return typer.Exit(1)
|
|
189
|
+
|
|
190
|
+
action = "created" if not existing_set else "modified"
|
|
191
|
+
console.print(
|
|
192
|
+
Panel(
|
|
193
|
+
f"[bold green]Secret set '{name}' {action} successfully[/bold green]",
|
|
194
|
+
title=f"[green]{PANEL_TITLE_SUCCESS}[/green]",
|
|
195
|
+
title_align="left",
|
|
196
|
+
border_style="green",
|
|
197
|
+
))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@secrets_cli.command(name="unset", help="Delete a secret within specified secret set")
|
|
201
|
+
@synchronizer.create_blocking
|
|
202
|
+
@requires_login
|
|
203
|
+
async def unset(
|
|
204
|
+
ctx: typer.Context,
|
|
205
|
+
name: str = typer.Argument(
|
|
206
|
+
None,
|
|
207
|
+
help="Name of the secret set to delete a secret from e.g. 'my-secret-set'"
|
|
208
|
+
),
|
|
209
|
+
secret_key: str = typer.Argument(
|
|
210
|
+
None,
|
|
211
|
+
help="Name of the secret to delete e.g. 'my-secret'",
|
|
212
|
+
)
|
|
213
|
+
):
|
|
214
|
+
if not name or not secret_key:
|
|
215
|
+
console.print(
|
|
216
|
+
"[red]Error: Secret set name and secret name must be provided. Please reference --help for more information.[/red]")
|
|
217
|
+
return typer.Exit(1)
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
with console.status(f"Deleting secret [bold]'{secret_key}'[/bold] from secret set [bold]'{name}'[/bold]", spinner="dots"):
|
|
221
|
+
await _delete_secret(name, secret_key, ctx.obj["org"], ctx.obj["token"])
|
|
222
|
+
except Exception:
|
|
223
|
+
console.print(
|
|
224
|
+
Panel(
|
|
225
|
+
f"[red]Unable to delete secret '{secret_key}' from secret set '{name}'. Are you sure it exists?[/red]",
|
|
226
|
+
title=f"[red]{PANEL_TITLE_ERROR}[/red]",
|
|
227
|
+
title_align="left",
|
|
228
|
+
border_style="red",
|
|
229
|
+
))
|
|
230
|
+
return typer.Exit(1)
|
|
231
|
+
|
|
232
|
+
console.print(
|
|
233
|
+
Panel(
|
|
234
|
+
f"[bold green]Secret '{secret_key}' deleted successfully from secret set '{name}'[/bold green]",
|
|
235
|
+
title=f"[green]{PANEL_TITLE_SUCCESS}[/green]",
|
|
236
|
+
title_align="left",
|
|
237
|
+
border_style="green",
|
|
238
|
+
))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@secrets_cli.command(name="list", help="List secrets for active organization")
|
|
242
|
+
@synchronizer.create_blocking
|
|
243
|
+
@requires_login
|
|
244
|
+
async def list(ctx: typer.Context, name: str = typer.Argument(
|
|
245
|
+
None,
|
|
246
|
+
help="Name of the secret set to list secrets from e.g. 'my-secret-set'"
|
|
247
|
+
)):
|
|
248
|
+
token = ctx.obj["token"]
|
|
249
|
+
org = ctx.obj["org"]
|
|
250
|
+
|
|
251
|
+
if not name:
|
|
252
|
+
request_url = f"{construct_api_url('secrets_path').format(org=org)}"
|
|
253
|
+
else:
|
|
254
|
+
request_url = f"{construct_api_url('secrets_path').format(org=org)}/{name}"
|
|
255
|
+
|
|
256
|
+
logger.debug(f"Requesting secrets from {request_url}")
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
with console.status("Retrieving secret sets for organization", spinner="dots"):
|
|
260
|
+
async with aiohttp.ClientSession() as session:
|
|
261
|
+
response = await session.get(request_url, headers={"Authorization": f"Bearer {token}"})
|
|
262
|
+
response.raise_for_status()
|
|
263
|
+
data = await response.json()
|
|
264
|
+
|
|
265
|
+
if name:
|
|
266
|
+
for s in data["secrets"]:
|
|
267
|
+
table = Table(
|
|
268
|
+
border_style="dim",
|
|
269
|
+
show_edge=True,
|
|
270
|
+
show_lines=False)
|
|
271
|
+
table.add_column(name, style="white")
|
|
272
|
+
table.add_row(s["fieldName"])
|
|
273
|
+
|
|
274
|
+
console.print(table)
|
|
275
|
+
else:
|
|
276
|
+
console.print(f"[bold green]Secret sets for organization {org}:[/bold green]")
|
|
277
|
+
table = Table(
|
|
278
|
+
border_style="dim",
|
|
279
|
+
show_edge=True,
|
|
280
|
+
show_lines=False)
|
|
281
|
+
table.add_column("Secret Set Name", style="white")
|
|
282
|
+
table.add_column("Type", style="white")
|
|
283
|
+
for s in data["sets"]:
|
|
284
|
+
if s["type"] == "imagePullSecret":
|
|
285
|
+
table.add_row(s["name"], "Image Pull Secret")
|
|
286
|
+
else:
|
|
287
|
+
table.add_row(s["name"], "Secret")
|
|
288
|
+
|
|
289
|
+
console.print(table)
|
|
290
|
+
|
|
291
|
+
except Exception:
|
|
292
|
+
console.print(Panel(
|
|
293
|
+
"[red]Unable to retrieve secret sets. Please contact support.[/red]",
|
|
294
|
+
title=f"[red]{PANEL_TITLE_ERROR}[/red]",
|
|
295
|
+
title_align="left",
|
|
296
|
+
border_style="red",
|
|
297
|
+
))
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@secrets_cli.command(name="delete", help="Delete a secret set from active organization")
|
|
301
|
+
@synchronizer.create_blocking
|
|
302
|
+
@requires_login
|
|
303
|
+
async def delete(
|
|
304
|
+
ctx: typer.Context,
|
|
305
|
+
name: str = typer.Argument(
|
|
306
|
+
None,
|
|
307
|
+
help="Name of the secret set to delete e.g. 'my-secret-set'"
|
|
308
|
+
),
|
|
309
|
+
):
|
|
310
|
+
if not name:
|
|
311
|
+
console.print(
|
|
312
|
+
"[red]Error: Secret set name must be provided. Please reference --help for more information.[/red]")
|
|
313
|
+
return typer.Exit(1)
|
|
314
|
+
|
|
315
|
+
# Confirm to proceed
|
|
316
|
+
confirm = await questionary.confirm(
|
|
317
|
+
f"Are you sure you want to delete secret set '{name}'?").ask_async()
|
|
318
|
+
if not confirm:
|
|
319
|
+
console.print("[bold red]Secret deletion cancelled[/bold red]")
|
|
320
|
+
return typer.Exit(1)
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
with console.status(f"Deleting secret set [bold]'{name}'[/bold]", spinner="dots"):
|
|
324
|
+
await _delete_secret_set(name, ctx.obj["org"], ctx.obj["token"])
|
|
325
|
+
except Exception:
|
|
326
|
+
console.print(Panel(
|
|
327
|
+
f"[red]Unable to delete secret set '{name}'. Are you sure it exists?[/red]",
|
|
328
|
+
title=f"[red]{PANEL_TITLE_ERROR}[/red]",
|
|
329
|
+
title_align="left",
|
|
330
|
+
border_style="red",
|
|
331
|
+
))
|
|
332
|
+
return typer.Exit(1)
|
|
333
|
+
|
|
334
|
+
console.print(
|
|
335
|
+
Panel(
|
|
336
|
+
f"[bold green]Secret set '{name}' deleted successfully[/bold green]",
|
|
337
|
+
title=f"[green]{PANEL_TITLE_SUCCESS}[/green]",
|
|
338
|
+
title_align="left",
|
|
339
|
+
border_style="green",
|
|
340
|
+
))
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@secrets_cli.command(name="image-pull-secret",
|
|
344
|
+
help="Create an image pull secret for active organization. See https://docs.pipecat.cloud/deployment/agent-images for more information.")
|
|
345
|
+
@synchronizer.create_blocking
|
|
346
|
+
@requires_login
|
|
347
|
+
async def image_pull_secret(
|
|
348
|
+
ctx: typer.Context,
|
|
349
|
+
name: str = typer.Argument(
|
|
350
|
+
None,
|
|
351
|
+
help="Name of the image pull secret to reference in deployment e.g. 'my-image-pull-secret'"
|
|
352
|
+
),
|
|
353
|
+
host: str = typer.Argument(
|
|
354
|
+
None,
|
|
355
|
+
help="Host address of the image repository e.g. https://index.docker.io/v1/"
|
|
356
|
+
),
|
|
357
|
+
credentials: str = typer.Argument(
|
|
358
|
+
None,
|
|
359
|
+
help="Credentials of the image repository e.g. 'username:password'"
|
|
360
|
+
),
|
|
361
|
+
base64encode: bool = typer.Option(
|
|
362
|
+
True,
|
|
363
|
+
"--base64encode",
|
|
364
|
+
help="Encode credentials in base64 format"
|
|
365
|
+
)
|
|
366
|
+
):
|
|
367
|
+
org = ctx.obj["org"]
|
|
368
|
+
token = ctx.obj["token"]
|
|
369
|
+
|
|
370
|
+
if not name or not host:
|
|
371
|
+
console.print(
|
|
372
|
+
"[red]Error: Name and host must be provided. Please reference --help for more information.[/red]")
|
|
373
|
+
return typer.Exit(1)
|
|
374
|
+
|
|
375
|
+
if not credentials:
|
|
376
|
+
username = await questionary.text(
|
|
377
|
+
f"Enter username for image repository '{host}'").ask_async()
|
|
378
|
+
password = await questionary.password(
|
|
379
|
+
f"Enter password for image repository '{host}'").ask_async()
|
|
380
|
+
if not username or not password:
|
|
381
|
+
console.print("[bold red]Image pull secret creation cancelled[/bold red]")
|
|
382
|
+
return typer.Exit(1)
|
|
383
|
+
credentials = f"{username}:{password}"
|
|
384
|
+
|
|
385
|
+
if base64encode:
|
|
386
|
+
credentials = base64.b64encode(credentials.encode()).decode()
|
|
387
|
+
|
|
388
|
+
# Check if secret already exists
|
|
389
|
+
with console.status(f"Checking if image pull secret '{name}' already exists", spinner="dots"):
|
|
390
|
+
request_url = f"{construct_api_url('secrets_path').format(org=org)}"
|
|
391
|
+
async with aiohttp.ClientSession() as session:
|
|
392
|
+
response = await session.get(request_url, headers={"Authorization": f"Bearer {token}"})
|
|
393
|
+
response.raise_for_status()
|
|
394
|
+
data = await response.json()
|
|
395
|
+
existing_secret = next(
|
|
396
|
+
(s for s in data["sets"] if s["name"] == name and s["type"] == "imagePullSecret"), None)
|
|
397
|
+
if existing_secret:
|
|
398
|
+
console.print(
|
|
399
|
+
Panel(
|
|
400
|
+
f"[red]Image pull secret '{name}' already exists. Please choose a different name.[/red]",
|
|
401
|
+
title=f"[red]{PANEL_TITLE_ERROR}[/red]",
|
|
402
|
+
title_align="left",
|
|
403
|
+
border_style="red",
|
|
404
|
+
))
|
|
405
|
+
return typer.Exit(1)
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
with console.status(f"Creating image pull secret [bold]'{name}'[/bold]", spinner="dots"):
|
|
409
|
+
await _create_image_pull_secret(name, host, credentials, org, token)
|
|
410
|
+
except Exception as e:
|
|
411
|
+
console.print(
|
|
412
|
+
Panel(
|
|
413
|
+
f"[red]Unable to create image pull secret '{name}'. Operation failed with error: {e}[/red]",
|
|
414
|
+
title=f"[red]{PANEL_TITLE_ERROR}[/red]",
|
|
415
|
+
title_align="left",
|
|
416
|
+
border_style="red",
|
|
417
|
+
))
|
|
418
|
+
return typer.Exit(1)
|
|
419
|
+
|
|
420
|
+
console.print(
|
|
421
|
+
Panel(
|
|
422
|
+
f"[bold green]Image pull secret '{name}' for {host} created successfully[/bold green]",
|
|
423
|
+
title=f"[green]{PANEL_TITLE_SUCCESS}[/green]",
|
|
424
|
+
title_align="left",
|
|
425
|
+
border_style="green",
|
|
426
|
+
))
|
pipecatcloud/config.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import typing
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from pipecatcloud import PIPECAT_CREDENTIALS_PATH, PIPECAT_DEPLOY_CONFIG_PATH
|
|
6
|
+
from pipecatcloud.exception import ConfigError
|
|
7
|
+
|
|
8
|
+
user_config_path: str = os.environ.get("PIPECAT_CONFIG_PATH") or os.path.expanduser(
|
|
9
|
+
PIPECAT_CREDENTIALS_PATH
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
deploy_config_path: str = os.environ.get("PIPECAT_DEPLOY_CONFIG_PATH") or os.path.expanduser(
|
|
13
|
+
PIPECAT_DEPLOY_CONFIG_PATH
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
api_host: str = os.environ.get("PIPECAT_API_HOST") or "https://api.pipecat.cloud"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _read_user_config():
|
|
20
|
+
config_data = {}
|
|
21
|
+
if os.path.exists(user_config_path):
|
|
22
|
+
import toml
|
|
23
|
+
try:
|
|
24
|
+
with open(user_config_path) as f:
|
|
25
|
+
config_data = toml.load(f)
|
|
26
|
+
except Exception as exc:
|
|
27
|
+
config_problem = str(exc)
|
|
28
|
+
else:
|
|
29
|
+
if not all(isinstance(e, dict) for e in config_data.values()):
|
|
30
|
+
raise ConfigError(
|
|
31
|
+
"Pipecat Cloud config file is not valid TOML. Please log out and log back in.")
|
|
32
|
+
else:
|
|
33
|
+
config_problem = ""
|
|
34
|
+
if config_problem:
|
|
35
|
+
raise ConfigError(config_problem)
|
|
36
|
+
return config_data
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Initialize _user_config first
|
|
40
|
+
_user_config = _read_user_config()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def config_profiles():
|
|
44
|
+
"""List the available profiles in the toml file."""
|
|
45
|
+
return _user_config.keys()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _config_active_profile() -> typing.Optional[str]:
|
|
49
|
+
if not _user_config:
|
|
50
|
+
return None
|
|
51
|
+
return next(iter(_user_config.keys()))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _write_user_config(user_config):
|
|
55
|
+
import toml
|
|
56
|
+
|
|
57
|
+
with open(user_config_path, "w") as f:
|
|
58
|
+
toml.dump(user_config, f)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _remove_user_config():
|
|
62
|
+
os.remove(user_config_path)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _store_user_config(token: str, org: str, additional_data: Optional[dict] = None):
|
|
66
|
+
# @TODO: Make method more robust
|
|
67
|
+
if not org:
|
|
68
|
+
raise ValueError("Account organization is required")
|
|
69
|
+
if not token:
|
|
70
|
+
raise ValueError("Token is required")
|
|
71
|
+
config_data = {
|
|
72
|
+
org: {
|
|
73
|
+
"token": token,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if additional_data is not None:
|
|
77
|
+
config_data[org].update(additional_data)
|
|
78
|
+
|
|
79
|
+
_write_user_config(config_data)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Set _profile after _config_active_profile is defined
|
|
83
|
+
_profile = os.environ.get("PIPECAT_PROFILE") or _config_active_profile()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class _Setting(typing.NamedTuple):
|
|
87
|
+
default: typing.Any = None
|
|
88
|
+
transform: typing.Callable[[str], typing.Any] = lambda x: x # noqa: E731
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
_SETTINGS = {
|
|
92
|
+
"server_url": _Setting(api_host),
|
|
93
|
+
"onboarding_path": _Setting("/v1/onboarding"),
|
|
94
|
+
"login_path": _Setting("/auth/login"),
|
|
95
|
+
"login_status_path": _Setting("/auth/status"),
|
|
96
|
+
"whoami_path": _Setting("/v1/users"),
|
|
97
|
+
"organization_path": _Setting("/v1/organizations"),
|
|
98
|
+
"services_path": _Setting("/v1/organizations/{org}/services"),
|
|
99
|
+
"services_logs_path": _Setting("/v1/organizations/{org}/services/{service}/logs"),
|
|
100
|
+
"services_deployments_path": _Setting("/v1/organizations/{org}/services/{service}/deployments"),
|
|
101
|
+
"start_path": _Setting("/v1/public/{service}/proxy"),
|
|
102
|
+
"api_keys_path": _Setting("/v1/organizations/{org}/apiKeys"),
|
|
103
|
+
"secrets_path": _Setting("/v1/organizations/{org}/secrets"),
|
|
104
|
+
"user_config_path": _Setting(user_config_path),
|
|
105
|
+
"token": _Setting(),
|
|
106
|
+
"org": _Setting(),
|
|
107
|
+
"default_public_key": _Setting(),
|
|
108
|
+
"default_public_key_name": _Setting(),
|
|
109
|
+
"cli_log_level": _Setting("INFO"),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Config:
|
|
114
|
+
"""Singleton that holds configuration used by PipecatCloud internally."""
|
|
115
|
+
|
|
116
|
+
def __init__(self):
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
def get(self, key, profile=None, use_env=True):
|
|
120
|
+
"""Looks up a configuration value.
|
|
121
|
+
|
|
122
|
+
Will check (in decreasing order of priority):
|
|
123
|
+
1. Any environment variable of the form PIPECAT_FOO_BAR (when use_env is True)
|
|
124
|
+
2. Settings in the user's .toml configuration file
|
|
125
|
+
3. The default value of the setting
|
|
126
|
+
"""
|
|
127
|
+
if profile is None:
|
|
128
|
+
profile = _profile
|
|
129
|
+
|
|
130
|
+
s = _SETTINGS[key]
|
|
131
|
+
env_var_key = "PIPECAT_" + key.upper()
|
|
132
|
+
if use_env and env_var_key in os.environ:
|
|
133
|
+
return s.transform(os.environ[env_var_key])
|
|
134
|
+
elif profile is not None and profile in _user_config and key in _user_config[profile]:
|
|
135
|
+
return s.transform(_user_config[profile][key])
|
|
136
|
+
elif key == "org":
|
|
137
|
+
return profile
|
|
138
|
+
else:
|
|
139
|
+
return s.default
|
|
140
|
+
|
|
141
|
+
def override_locally(self, key: str, value: str):
|
|
142
|
+
try:
|
|
143
|
+
self.get(key)
|
|
144
|
+
os.environ["PIPECAT_" + key.upper()] = value
|
|
145
|
+
except KeyError:
|
|
146
|
+
os.environ[key.upper()] = value
|
|
147
|
+
|
|
148
|
+
def __getitem__(self, key):
|
|
149
|
+
return self.get(key)
|
|
150
|
+
|
|
151
|
+
def __repr__(self):
|
|
152
|
+
return repr(self.to_dict())
|
|
153
|
+
|
|
154
|
+
def to_dict(self):
|
|
155
|
+
return {key: self.get(key) for key in _SETTINGS.keys()}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
config = Config()
|
pipecatcloud/errors.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
ERROR_CODES = {
|
|
2
|
+
"401": "Unauthorized / token expired. Please run `pipecat auth login` to login again.",
|
|
3
|
+
"404": "API endpoint not found / agent deployment not found",
|
|
4
|
+
"PCC-1000": "Unable to start agent",
|
|
5
|
+
"PCC-1001": "Attempt to start agent when deployment is not in ready state",
|
|
6
|
+
"PCC-1002": "Attempt to start agent without public api key",
|
|
7
|
+
"PCC-1003": "Unknown error occured. Please check logs for more information.",
|
|
8
|
+
"PCC-1004": "Billing credentials not set. Please set billing credentials via the Pipecat Cloud dashboard.",
|
|
9
|
+
"PCC-1005": "Agent deployment with name not found",
|
|
10
|
+
"PCC-1006": "Not authorized / invalid API key",
|
|
11
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pipecatcloud.errors import ERROR_CODES
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Error(Exception):
|
|
7
|
+
"""Base class for exceptions in this module."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthError(Error):
|
|
13
|
+
"""Exception raised for authentication errors."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
message: str = "Unauthorized / token expired. Please run `pipecat auth login` to login again."):
|
|
18
|
+
self.message = message
|
|
19
|
+
super().__init__(self.message)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InvalidError(Error):
|
|
23
|
+
"""Raised when user does something invalid."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConfigError(Error):
|
|
27
|
+
"""Raised when user does something invalid."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AgentNotHealthyError(Error):
|
|
31
|
+
"""Raised when agent is not healthy and cannot be started."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
message: str = "Agent deployment is not in a ready state and cannot be started.",
|
|
36
|
+
error_code: Optional[str] = None):
|
|
37
|
+
self.message = f"{message} (Error code: {error_code})"
|
|
38
|
+
self.error_code = error_code
|
|
39
|
+
super().__init__(self.message)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AgentStartError(Error):
|
|
43
|
+
"""Raised when agent start request fails."""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
message: str = "Agent start request failed.",
|
|
48
|
+
error_code: Optional[str] = None):
|
|
49
|
+
|
|
50
|
+
error_message = message if not error_code else ERROR_CODES.get(
|
|
51
|
+
error_code, message)
|
|
52
|
+
|
|
53
|
+
self.message = f"{error_message} (Error code: {error_code})"
|
|
54
|
+
self.error_code = error_code
|
|
55
|
+
super().__init__(self.message)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: pipecatcloud
|
|
3
|
+
Version: 0.0.6
|
|
4
|
+
Requires-Python: >=3.10
|
|
5
|
+
Requires-Dist: aiohttp~=3.10.10
|
|
6
|
+
Requires-Dist: fastapi~=0.115.5
|
|
7
|
+
Requires-Dist: loguru~=0.7.2
|
|
8
|
+
Requires-Dist: python-dotenv~=1.0.1
|
|
9
|
+
Requires-Dist: uvicorn~=0.32.0
|
|
10
|
+
Requires-Dist: typer~=0.13.0
|
|
11
|
+
Requires-Dist: synchronicity~=0.7.6
|
|
12
|
+
Requires-Dist: toml~=0.10.2
|
|
13
|
+
Requires-Dist: questionary~=2.1.0
|