fal 0.15.0__py3-none-any.whl → 1.0.0__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.
Potentially problematic release.
This version of fal might be problematic. Click here for more details.
- fal/__init__.py +5 -13
- fal/__main__.py +2 -2
- fal/_fal_version.py +16 -0
- fal/_serialization.py +15 -9
- fal/_version.py +6 -0
- fal/api.py +32 -14
- fal/app.py +54 -5
- fal/auth/__init__.py +2 -1
- fal/auth/auth0.py +4 -2
- fal/auth/local.py +2 -1
- fal/cli/__init__.py +1 -0
- fal/cli/apps.py +313 -0
- fal/cli/auth.py +59 -0
- fal/cli/debug.py +65 -0
- fal/cli/deploy.py +146 -0
- fal/cli/keys.py +118 -0
- fal/cli/main.py +82 -0
- fal/cli/parser.py +74 -0
- fal/cli/run.py +33 -0
- fal/cli/secrets.py +107 -0
- fal/exceptions/__init__.py +0 -28
- fal/flags.py +0 -3
- fal/logging/isolate.py +4 -4
- fal/sdk.py +39 -2
- fal/sync.py +7 -3
- fal/toolkit/file/file.py +14 -6
- fal/toolkit/file/providers/fal.py +20 -3
- fal/toolkit/image/image.py +1 -1
- fal/toolkit/optimize.py +0 -1
- fal/toolkit/utils/download_utils.py +6 -3
- fal/utils.py +55 -0
- fal/workflows.py +7 -2
- {fal-0.15.0.dist-info → fal-1.0.0.dist-info}/METADATA +33 -5
- {fal-0.15.0.dist-info → fal-1.0.0.dist-info}/RECORD +37 -26
- fal-1.0.0.dist-info/entry_points.txt +2 -0
- fal/cli.py +0 -619
- fal/exceptions/handlers.py +0 -58
- fal-0.15.0.dist-info/entry_points.txt +0 -2
- {fal-0.15.0.dist-info → fal-1.0.0.dist-info}/WHEEL +0 -0
- {fal-0.15.0.dist-info → fal-1.0.0.dist-info}/top_level.txt +0 -0
fal/cli.py
DELETED
|
@@ -1,619 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from dataclasses import dataclass, field
|
|
5
|
-
from http import HTTPStatus
|
|
6
|
-
from sys import argv
|
|
7
|
-
from typing import Any, Callable, Literal
|
|
8
|
-
from uuid import uuid4
|
|
9
|
-
|
|
10
|
-
import click
|
|
11
|
-
import openapi_fal_rest.api.billing.get_user_details as get_user_details
|
|
12
|
-
from rich.table import Table
|
|
13
|
-
from rich_click import RichCommand, RichGroup
|
|
14
|
-
|
|
15
|
-
import fal
|
|
16
|
-
import fal.auth as auth
|
|
17
|
-
from fal import _serialization, api, sdk
|
|
18
|
-
from fal.console import console
|
|
19
|
-
from fal.exceptions import ApplicationExceptionHandler
|
|
20
|
-
from fal.logging import get_logger, set_debug_logging
|
|
21
|
-
from fal.logging.trace import get_tracer
|
|
22
|
-
from fal.rest_client import REST_CLIENT
|
|
23
|
-
from fal.sdk import AliasInfo, KeyScope
|
|
24
|
-
|
|
25
|
-
DEFAULT_HOST = "api.alpha.fal.ai"
|
|
26
|
-
HOST_ENVVAR = "FAL_HOST"
|
|
27
|
-
|
|
28
|
-
DEFAULT_PORT = "443"
|
|
29
|
-
PORT_ENVVAR = "FAL_PORT"
|
|
30
|
-
|
|
31
|
-
DEBUG_ENABLED = False
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
logger = get_logger(__name__)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@dataclass
|
|
38
|
-
class State:
|
|
39
|
-
debug: bool = False
|
|
40
|
-
invocation_id: str = field(default_factory=lambda: str(uuid4()))
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def debug_option(*param_decls: str, **kwargs: Any) -> Callable[[click.FC], click.FC]:
|
|
44
|
-
def callback(ctx: click.Context, param: click.Parameter, value: bool) -> None:
|
|
45
|
-
state = ctx.ensure_object(State)
|
|
46
|
-
state.debug = value
|
|
47
|
-
set_debug_logging(value)
|
|
48
|
-
|
|
49
|
-
if not param_decls:
|
|
50
|
-
param_decls = ("--debug",)
|
|
51
|
-
|
|
52
|
-
kwargs.setdefault("is_flag", True)
|
|
53
|
-
kwargs.setdefault("expose_value", False)
|
|
54
|
-
kwargs.setdefault("callback", callback)
|
|
55
|
-
kwargs.setdefault("help", "Enable detailed errors and verbose logging.")
|
|
56
|
-
return click.option(*param_decls, **kwargs)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class MainGroup(RichGroup):
|
|
60
|
-
"""A custom implementation of the top-level group
|
|
61
|
-
(i.e. called on all commands and subcommands).
|
|
62
|
-
|
|
63
|
-
This implementation allows for centralized behavior, including
|
|
64
|
-
exception handling.
|
|
65
|
-
"""
|
|
66
|
-
|
|
67
|
-
_exception_handler = ApplicationExceptionHandler()
|
|
68
|
-
|
|
69
|
-
_tracer = get_tracer(__name__)
|
|
70
|
-
|
|
71
|
-
def invoke(self, ctx):
|
|
72
|
-
from click.exceptions import Abort, ClickException, Exit
|
|
73
|
-
|
|
74
|
-
state = ctx.ensure_object(State)
|
|
75
|
-
qualified_name = " ".join([ctx.info_name] + argv[1:])
|
|
76
|
-
|
|
77
|
-
with self._tracer.start_as_current_span(
|
|
78
|
-
qualified_name, attributes={"invocation_id": state.invocation_id}
|
|
79
|
-
):
|
|
80
|
-
try:
|
|
81
|
-
logger.debug(
|
|
82
|
-
f"Executing command: {qualified_name}",
|
|
83
|
-
command=qualified_name,
|
|
84
|
-
)
|
|
85
|
-
return super().invoke(ctx)
|
|
86
|
-
except (EOFError, KeyboardInterrupt, ClickException, Exit, Abort):
|
|
87
|
-
# let click's main handle these
|
|
88
|
-
raise
|
|
89
|
-
except Exception as exception:
|
|
90
|
-
logger.error(exception)
|
|
91
|
-
if state.debug:
|
|
92
|
-
# Here we supress detailed errors on click lines because
|
|
93
|
-
# they're mostly decorator calls, irrelevant to the dev's error tracing
|
|
94
|
-
console.print_exception(suppress=[click])
|
|
95
|
-
console.print()
|
|
96
|
-
console.print(
|
|
97
|
-
f"The [markdown.code]invocation_id[/] for this operation is: [white]{state.invocation_id}[/]"
|
|
98
|
-
)
|
|
99
|
-
else:
|
|
100
|
-
self._exception_handler.handle(exception)
|
|
101
|
-
|
|
102
|
-
def add_command(
|
|
103
|
-
self,
|
|
104
|
-
cmd: RichCommand,
|
|
105
|
-
name: str | None = None,
|
|
106
|
-
aliases: list[str] | None = None,
|
|
107
|
-
) -> None:
|
|
108
|
-
name = name or cmd.name
|
|
109
|
-
assert name, "Command must have a name"
|
|
110
|
-
|
|
111
|
-
if not aliases:
|
|
112
|
-
aliases = []
|
|
113
|
-
|
|
114
|
-
if aliases:
|
|
115
|
-
# Add aliases to the help text
|
|
116
|
-
cmd.help = (cmd.help or "") + "\n\nAlias: " + ", ".join([name, *aliases])
|
|
117
|
-
cmd.short_help = (
|
|
118
|
-
(cmd.short_help or "") + "(Alias: " + ", ".join(aliases) + ")"
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
super().add_command(cmd, name)
|
|
122
|
-
alias_cmd = AliasCommand(cmd)
|
|
123
|
-
|
|
124
|
-
for alias in aliases:
|
|
125
|
-
self.add_command(alias_cmd, alias)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
class AliasCommand(RichCommand):
|
|
129
|
-
def __init__(self, wrapped):
|
|
130
|
-
self._wrapped = wrapped
|
|
131
|
-
|
|
132
|
-
def __getattribute__(self, __name: str):
|
|
133
|
-
if __name == "_wrapped":
|
|
134
|
-
# To be able to call `self._wrapped` below
|
|
135
|
-
return super().__getattribute__(__name)
|
|
136
|
-
|
|
137
|
-
if __name == "hidden":
|
|
138
|
-
return True
|
|
139
|
-
|
|
140
|
-
return self._wrapped.__getattribute__(__name)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
@click.group(cls=MainGroup)
|
|
144
|
-
@click.version_option()
|
|
145
|
-
@debug_option()
|
|
146
|
-
def cli():
|
|
147
|
-
pass
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
###### Auth group ######
|
|
151
|
-
@click.group(cls=RichGroup)
|
|
152
|
-
@debug_option()
|
|
153
|
-
def auth_cli():
|
|
154
|
-
pass
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
@auth_cli.command(name="login")
|
|
158
|
-
@debug_option()
|
|
159
|
-
def auth_login():
|
|
160
|
-
auth.login()
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
@auth_cli.command(name="logout")
|
|
164
|
-
@debug_option()
|
|
165
|
-
def auth_logout():
|
|
166
|
-
auth.logout()
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
@auth_cli.command(name="hello", hidden=True)
|
|
170
|
-
@debug_option()
|
|
171
|
-
def auth_test():
|
|
172
|
-
"""
|
|
173
|
-
To test auth.
|
|
174
|
-
"""
|
|
175
|
-
print(f"Hello, {auth.USER.info['name']} - '{auth.USER.info['sub']}'")
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
###### Key group ######
|
|
179
|
-
@click.group(cls=RichGroup)
|
|
180
|
-
@click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
|
|
181
|
-
@click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
|
|
182
|
-
@debug_option()
|
|
183
|
-
@click.pass_context
|
|
184
|
-
def key_cli(ctx, host: str, port: str):
|
|
185
|
-
ctx.obj = sdk.FalServerlessClient(f"{host}:{port}")
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
@key_cli.command(name="generate", no_args_is_help=True)
|
|
189
|
-
@click.option(
|
|
190
|
-
"--scope",
|
|
191
|
-
default=None,
|
|
192
|
-
required=True,
|
|
193
|
-
type=click.Choice([KeyScope.ADMIN.value, KeyScope.API.value]),
|
|
194
|
-
help="The privilage scope of the key.",
|
|
195
|
-
)
|
|
196
|
-
@click.option(
|
|
197
|
-
"--alias",
|
|
198
|
-
default=None,
|
|
199
|
-
help="An alias for the key.",
|
|
200
|
-
)
|
|
201
|
-
@debug_option()
|
|
202
|
-
@click.pass_obj
|
|
203
|
-
def key_generate(client: sdk.FalServerlessClient, scope: str, alias: str | None):
|
|
204
|
-
with client.connect() as connection:
|
|
205
|
-
parsed_scope = KeyScope(scope)
|
|
206
|
-
result = connection.create_user_key(parsed_scope, alias)
|
|
207
|
-
print(
|
|
208
|
-
f"Generated key id and key secret, with the scope `{scope}`.\n"
|
|
209
|
-
"This is the only time the secret will be visible.\n"
|
|
210
|
-
"You will need to generate a new key pair if you lose access to this secret."
|
|
211
|
-
)
|
|
212
|
-
print(f"FAL_KEY='{result[1]}:{result[0]}'")
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
@key_cli.command(name="list")
|
|
216
|
-
@debug_option()
|
|
217
|
-
@click.pass_obj
|
|
218
|
-
def key_list(client: sdk.FalServerlessClient):
|
|
219
|
-
table = Table(title="Keys")
|
|
220
|
-
table.add_column("Key ID")
|
|
221
|
-
table.add_column("Created At")
|
|
222
|
-
table.add_column("Scope")
|
|
223
|
-
table.add_column("Alias")
|
|
224
|
-
|
|
225
|
-
with client.connect() as connection:
|
|
226
|
-
keys = connection.list_user_keys()
|
|
227
|
-
for key in keys:
|
|
228
|
-
table.add_row(
|
|
229
|
-
key.key_id, str(key.created_at), str(key.scope.value), key.alias
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
console.print(table)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
@key_cli.command(name="revoke")
|
|
236
|
-
@click.argument("key_id", required=True)
|
|
237
|
-
@debug_option()
|
|
238
|
-
@click.pass_obj
|
|
239
|
-
def key_revoke(client: sdk.FalServerlessClient, key_id: str):
|
|
240
|
-
with client.connect() as connection:
|
|
241
|
-
connection.revoke_user_key(key_id)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
##### Function group #####
|
|
245
|
-
ALIAS_AUTH_OPTIONS = ["public", "private", "shared"]
|
|
246
|
-
ALIAS_AUTH_TYPE = Literal["public", "private", "shared"]
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
@click.group(cls=RichGroup)
|
|
250
|
-
@click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
|
|
251
|
-
@click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
|
|
252
|
-
@debug_option()
|
|
253
|
-
@click.pass_context
|
|
254
|
-
def function_cli(ctx, host: str, port: str):
|
|
255
|
-
ctx.obj = api.FalServerlessHost(f"{host}:{port}")
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def load_function_from(
|
|
259
|
-
host: api.FalServerlessHost,
|
|
260
|
-
file_path: str,
|
|
261
|
-
function_name: str,
|
|
262
|
-
) -> api.IsolatedFunction:
|
|
263
|
-
import runpy
|
|
264
|
-
|
|
265
|
-
module = runpy.run_path(file_path)
|
|
266
|
-
if function_name not in module:
|
|
267
|
-
raise api.FalServerlessError(f"Function '{function_name}' not found in module")
|
|
268
|
-
|
|
269
|
-
# The module for the function is set to <run_path> when runpy is used, in which
|
|
270
|
-
# case we want to manually include the package it is defined in.
|
|
271
|
-
_serialization.include_package_from_path(file_path)
|
|
272
|
-
|
|
273
|
-
target = module[function_name]
|
|
274
|
-
if isinstance(target, type) and issubclass(target, fal.App):
|
|
275
|
-
target = fal.wrap_app(target, host=host)
|
|
276
|
-
|
|
277
|
-
if not isinstance(target, api.IsolatedFunction):
|
|
278
|
-
raise api.FalServerlessError(
|
|
279
|
-
f"Function '{function_name}' is not a fal.function or a fal.App"
|
|
280
|
-
)
|
|
281
|
-
return target
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
@function_cli.command("serve")
|
|
285
|
-
@click.option("--alias", default=None)
|
|
286
|
-
@click.option(
|
|
287
|
-
"--auth",
|
|
288
|
-
"auth_mode",
|
|
289
|
-
type=click.Choice(ALIAS_AUTH_OPTIONS),
|
|
290
|
-
default="private",
|
|
291
|
-
)
|
|
292
|
-
@click.argument("file_path", required=True)
|
|
293
|
-
@click.argument("function_name", required=True)
|
|
294
|
-
@debug_option()
|
|
295
|
-
@click.pass_obj
|
|
296
|
-
def register_application(
|
|
297
|
-
host: api.FalServerlessHost,
|
|
298
|
-
file_path: str,
|
|
299
|
-
function_name: str,
|
|
300
|
-
alias: str | None,
|
|
301
|
-
auth_mode: ALIAS_AUTH_TYPE,
|
|
302
|
-
):
|
|
303
|
-
user_id = _get_user_id()
|
|
304
|
-
|
|
305
|
-
isolated_function = load_function_from(host, file_path, function_name)
|
|
306
|
-
gateway_options = isolated_function.options.gateway
|
|
307
|
-
if "serve" not in gateway_options and "exposed_port" not in gateway_options:
|
|
308
|
-
raise api.FalServerlessError(
|
|
309
|
-
"One of `serve` or `exposed_port` options needs to be specified in the isolated annotation to register a function"
|
|
310
|
-
)
|
|
311
|
-
elif (
|
|
312
|
-
"exposed_port" in gateway_options
|
|
313
|
-
and str(gateway_options.get("exposed_port")) != "8080"
|
|
314
|
-
):
|
|
315
|
-
raise api.FalServerlessError(
|
|
316
|
-
"Must expose port 8080 for now. This will be configurable in the future."
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
id = host.register(
|
|
320
|
-
func=isolated_function.func,
|
|
321
|
-
options=isolated_function.options,
|
|
322
|
-
application_name=alias,
|
|
323
|
-
application_auth_mode=auth_mode,
|
|
324
|
-
metadata=isolated_function.options.host.get("metadata", {}),
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
if id:
|
|
328
|
-
gateway_host = remove_http_and_port_from_url(host.url)
|
|
329
|
-
gateway_host = (
|
|
330
|
-
gateway_host.replace("api.", "").replace("alpha.", "").replace("ai", "run")
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
if alias:
|
|
334
|
-
console.print(
|
|
335
|
-
f"Registered a new revision for function '{alias}' (revision='{id}')."
|
|
336
|
-
)
|
|
337
|
-
console.print(f"URL: https://{gateway_host}/{user_id}/{alias}")
|
|
338
|
-
else:
|
|
339
|
-
console.print(f"Registered anonymous function '{id}'.")
|
|
340
|
-
console.print(f"URL: https://{gateway_host}/{user_id}/{id}")
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
@function_cli.command("run")
|
|
344
|
-
@click.argument("file_path", required=True)
|
|
345
|
-
@click.argument("function_name", required=True)
|
|
346
|
-
@debug_option()
|
|
347
|
-
@click.pass_obj
|
|
348
|
-
def run(host: api.FalServerlessHost, file_path: str, function_name: str):
|
|
349
|
-
isolated_function = load_function_from(host, file_path, function_name)
|
|
350
|
-
# let our exc handlers handle UserFunctionException
|
|
351
|
-
isolated_function.reraise = False
|
|
352
|
-
isolated_function()
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
@function_cli.command("logs")
|
|
356
|
-
@click.option("--lines", default=100)
|
|
357
|
-
@click.option("--url", default=None)
|
|
358
|
-
@debug_option()
|
|
359
|
-
@click.pass_obj
|
|
360
|
-
def get_logs(
|
|
361
|
-
host: api.FalServerlessHost, lines: int | None = 100, url: str | None = None
|
|
362
|
-
):
|
|
363
|
-
console.print(
|
|
364
|
-
"logs command is deprecated. To see logs, got to fal web page: https://www.fal.ai/dashboard/logs"
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
##### Alias group #####
|
|
369
|
-
@click.group(cls=RichGroup)
|
|
370
|
-
@click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
|
|
371
|
-
@click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
|
|
372
|
-
@debug_option()
|
|
373
|
-
@click.pass_context
|
|
374
|
-
def alias_cli(ctx, host: str, port: str):
|
|
375
|
-
ctx.obj = api.FalServerlessClient(f"{host}:{port}")
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
def _alias_table(aliases: list[AliasInfo]):
|
|
379
|
-
table = Table(title="Function Aliases")
|
|
380
|
-
table.add_column("Alias")
|
|
381
|
-
table.add_column("Revision")
|
|
382
|
-
table.add_column("Auth")
|
|
383
|
-
table.add_column("Min Concurrency")
|
|
384
|
-
table.add_column("Max Concurrency")
|
|
385
|
-
table.add_column("Max Multiplexing")
|
|
386
|
-
table.add_column("Keep Alive")
|
|
387
|
-
table.add_column("Active Workers")
|
|
388
|
-
|
|
389
|
-
for app_alias in aliases:
|
|
390
|
-
table.add_row(
|
|
391
|
-
app_alias.alias,
|
|
392
|
-
app_alias.revision,
|
|
393
|
-
app_alias.auth_mode,
|
|
394
|
-
str(app_alias.min_concurrency),
|
|
395
|
-
str(app_alias.max_concurrency),
|
|
396
|
-
str(app_alias.max_multiplexing),
|
|
397
|
-
str(app_alias.keep_alive),
|
|
398
|
-
str(app_alias.active_runners),
|
|
399
|
-
)
|
|
400
|
-
|
|
401
|
-
return table
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
@alias_cli.command("set")
|
|
405
|
-
@click.argument("alias", required=True)
|
|
406
|
-
@click.argument("revision", required=True)
|
|
407
|
-
@click.option(
|
|
408
|
-
"--auth",
|
|
409
|
-
"auth_mode",
|
|
410
|
-
type=click.Choice(ALIAS_AUTH_OPTIONS),
|
|
411
|
-
default="private",
|
|
412
|
-
)
|
|
413
|
-
@debug_option()
|
|
414
|
-
@click.pass_obj
|
|
415
|
-
def alias_set(
|
|
416
|
-
client: api.FalServerlessClient,
|
|
417
|
-
alias: str,
|
|
418
|
-
revision: str,
|
|
419
|
-
auth_mode: ALIAS_AUTH_TYPE,
|
|
420
|
-
):
|
|
421
|
-
with client.connect() as connection:
|
|
422
|
-
connection.create_alias(alias, revision, auth_mode)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
@alias_cli.command("delete")
|
|
426
|
-
@click.argument("alias", required=True)
|
|
427
|
-
@debug_option()
|
|
428
|
-
@click.pass_obj
|
|
429
|
-
def alias_delete(client: api.FalServerlessClient, alias: str):
|
|
430
|
-
with client.connect() as connection:
|
|
431
|
-
application_id = connection.delete_alias(alias)
|
|
432
|
-
|
|
433
|
-
console.print(f"Deleted alias '{alias}' for application '{application_id}'.")
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
@alias_cli.command("list")
|
|
437
|
-
@debug_option()
|
|
438
|
-
@click.pass_obj
|
|
439
|
-
def alias_list(client: api.FalServerlessClient):
|
|
440
|
-
with client.connect() as connection:
|
|
441
|
-
aliases = connection.list_aliases()
|
|
442
|
-
table = _alias_table(aliases)
|
|
443
|
-
|
|
444
|
-
console.print(table)
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
@alias_cli.command("update")
|
|
448
|
-
@click.argument("alias", required=True)
|
|
449
|
-
@click.option("--keep-alive", "-k", type=int)
|
|
450
|
-
@click.option("--max-multiplexing", "-m", type=int)
|
|
451
|
-
@click.option("--max-concurrency", "-c", type=int)
|
|
452
|
-
@click.option("--min-concurrency", type=int)
|
|
453
|
-
# TODO: add auth_mode
|
|
454
|
-
# @click.option(
|
|
455
|
-
# "--auth",
|
|
456
|
-
# "auth_mode",
|
|
457
|
-
# type=click.Choice(ALIAS_AUTH_OPTIONS),
|
|
458
|
-
# )
|
|
459
|
-
@debug_option()
|
|
460
|
-
@click.pass_obj
|
|
461
|
-
def alias_update(
|
|
462
|
-
client: api.FalServerlessClient,
|
|
463
|
-
alias: str,
|
|
464
|
-
keep_alive: int | None,
|
|
465
|
-
max_multiplexing: int | None,
|
|
466
|
-
max_concurrency: int | None,
|
|
467
|
-
min_concurrency: int | None,
|
|
468
|
-
):
|
|
469
|
-
with client.connect() as connection:
|
|
470
|
-
if (
|
|
471
|
-
keep_alive is None
|
|
472
|
-
and max_multiplexing is None
|
|
473
|
-
and max_concurrency is None
|
|
474
|
-
and min_concurrency is None
|
|
475
|
-
):
|
|
476
|
-
console.log("No parameters for update were provided, ignoring.")
|
|
477
|
-
return
|
|
478
|
-
|
|
479
|
-
alias_info = connection.update_application(
|
|
480
|
-
application_name=alias,
|
|
481
|
-
keep_alive=keep_alive,
|
|
482
|
-
max_multiplexing=max_multiplexing,
|
|
483
|
-
max_concurrency=max_concurrency,
|
|
484
|
-
min_concurrency=min_concurrency,
|
|
485
|
-
)
|
|
486
|
-
table = _alias_table([alias_info])
|
|
487
|
-
|
|
488
|
-
console.print(table)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
@alias_cli.command("runners")
|
|
492
|
-
@click.argument("alias", required=True)
|
|
493
|
-
@debug_option()
|
|
494
|
-
@click.pass_obj
|
|
495
|
-
def alias_list_runners(
|
|
496
|
-
client: api.FalServerlessClient,
|
|
497
|
-
alias: str,
|
|
498
|
-
):
|
|
499
|
-
with client.connect() as connection:
|
|
500
|
-
runners = connection.list_alias_runners(alias=alias)
|
|
501
|
-
|
|
502
|
-
table = Table(title="Application Runners")
|
|
503
|
-
table.add_column("Runner ID")
|
|
504
|
-
table.add_column("In Flight Requests")
|
|
505
|
-
table.add_column("Expires in")
|
|
506
|
-
table.add_column("Uptime")
|
|
507
|
-
|
|
508
|
-
for runner in runners:
|
|
509
|
-
table.add_row(
|
|
510
|
-
runner.runner_id,
|
|
511
|
-
str(runner.in_flight_requests),
|
|
512
|
-
(
|
|
513
|
-
"N/A (active)"
|
|
514
|
-
if not runner.expiration_countdown
|
|
515
|
-
else f"{runner.expiration_countdown}s"
|
|
516
|
-
),
|
|
517
|
-
f"{runner.uptime} ({runner.uptime.total_seconds()}s)",
|
|
518
|
-
)
|
|
519
|
-
|
|
520
|
-
console.print(table)
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
##### Secrets group #####
|
|
524
|
-
@click.group(cls=RichGroup)
|
|
525
|
-
@click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
|
|
526
|
-
@click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
|
|
527
|
-
@debug_option()
|
|
528
|
-
@click.pass_context
|
|
529
|
-
def secrets_cli(ctx, host: str, port: str):
|
|
530
|
-
ctx.obj = sdk.FalServerlessClient(f"{host}:{port}")
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
@secrets_cli.command("list")
|
|
534
|
-
@debug_option()
|
|
535
|
-
@click.pass_obj
|
|
536
|
-
def list_secrets(client: api.FalServerlessClient):
|
|
537
|
-
table = Table(title="Secrets")
|
|
538
|
-
table.add_column("Secret Name")
|
|
539
|
-
table.add_column("Created At")
|
|
540
|
-
|
|
541
|
-
with client.connect() as connection:
|
|
542
|
-
for secret in connection.list_secrets():
|
|
543
|
-
table.add_row(secret.name, str(secret.created_at))
|
|
544
|
-
|
|
545
|
-
console.print(table)
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
@secrets_cli.command("set")
|
|
549
|
-
@click.argument("secret_name", required=True)
|
|
550
|
-
@click.argument("secret_value", required=True)
|
|
551
|
-
@debug_option()
|
|
552
|
-
@click.pass_obj
|
|
553
|
-
def set_secret(client: api.FalServerlessClient, secret_name: str, secret_value: str):
|
|
554
|
-
with client.connect() as connection:
|
|
555
|
-
connection.set_secret(secret_name, secret_value)
|
|
556
|
-
console.print(f"Secret '{secret_name}' has set")
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
@secrets_cli.command("delete")
|
|
560
|
-
@click.argument("secret_name", required=True)
|
|
561
|
-
@debug_option()
|
|
562
|
-
@click.pass_obj
|
|
563
|
-
def delete_secret(client: api.FalServerlessClient, secret_name: str):
|
|
564
|
-
with client.connect() as connection:
|
|
565
|
-
connection.delete_secret(secret_name)
|
|
566
|
-
console.print(f"Secret '{secret_name}' has deleted")
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
# Setup of groups
|
|
570
|
-
cli.add_command(auth_cli, name="auth")
|
|
571
|
-
cli.add_command(key_cli, name="key", aliases=["keys"])
|
|
572
|
-
cli.add_command(function_cli, name="function", aliases=["fn"])
|
|
573
|
-
cli.add_command(alias_cli, name="alias", aliases=["aliases"])
|
|
574
|
-
cli.add_command(secrets_cli, name="secret", aliases=["secrets"])
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
def remove_http_and_port_from_url(url):
|
|
578
|
-
# Remove http://
|
|
579
|
-
if "http://" in url:
|
|
580
|
-
url = url.replace("http://", "")
|
|
581
|
-
|
|
582
|
-
# Remove https://
|
|
583
|
-
if "https://" in url:
|
|
584
|
-
url = url.replace("https://", "")
|
|
585
|
-
|
|
586
|
-
# Remove port information
|
|
587
|
-
url_parts = url.split(":")
|
|
588
|
-
if len(url_parts) > 1:
|
|
589
|
-
url = url_parts[0]
|
|
590
|
-
|
|
591
|
-
return url
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
def _get_user_id() -> str:
|
|
595
|
-
try:
|
|
596
|
-
user_details_response = get_user_details.sync_detailed(
|
|
597
|
-
client=REST_CLIENT,
|
|
598
|
-
)
|
|
599
|
-
except Exception as e:
|
|
600
|
-
raise api.FalServerlessError(f"Error fetching user details: {str(e)}")
|
|
601
|
-
|
|
602
|
-
if user_details_response.status_code != HTTPStatus.OK:
|
|
603
|
-
try:
|
|
604
|
-
content = json.loads(user_details_response.content.decode("utf8"))
|
|
605
|
-
except Exception:
|
|
606
|
-
raise api.FalServerlessError(
|
|
607
|
-
f"Error fetching user details: {user_details_response}"
|
|
608
|
-
)
|
|
609
|
-
else:
|
|
610
|
-
raise api.FalServerlessError(content["detail"])
|
|
611
|
-
try:
|
|
612
|
-
full_user_id = user_details_response.parsed.user_id
|
|
613
|
-
_provider, _, user_id = full_user_id.partition("|")
|
|
614
|
-
if not user_id:
|
|
615
|
-
user_id = full_user_id
|
|
616
|
-
|
|
617
|
-
return user_id
|
|
618
|
-
except Exception as e:
|
|
619
|
-
raise api.FalServerlessError(f"Could not parse the user data: {e}")
|
fal/exceptions/handlers.py
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING, Generic, TypeVar
|
|
4
|
-
|
|
5
|
-
from grpc import Call as RpcCall
|
|
6
|
-
|
|
7
|
-
from fal.console import console
|
|
8
|
-
from fal.console.icons import CROSS_ICON
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from fal.api import UserFunctionException
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
ExceptionType = TypeVar("ExceptionType", bound=BaseException)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class BaseExceptionHandler(Generic[ExceptionType]):
|
|
18
|
-
"""Base handler defaults to the string representation of the error"""
|
|
19
|
-
|
|
20
|
-
def should_handle(self, _: Exception) -> bool:
|
|
21
|
-
return True
|
|
22
|
-
|
|
23
|
-
def handle(self, exception: ExceptionType):
|
|
24
|
-
msg = f"{CROSS_ICON} {str(exception)}"
|
|
25
|
-
cause = exception.__cause__
|
|
26
|
-
if cause is not None:
|
|
27
|
-
msg += f": {str(cause)}"
|
|
28
|
-
console.print(msg)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class GrpcExceptionHandler(BaseExceptionHandler[RpcCall]):
|
|
32
|
-
"""Handle GRPC errors. The user message is part of the `details()`"""
|
|
33
|
-
|
|
34
|
-
def should_handle(self, exception: Exception) -> bool:
|
|
35
|
-
return isinstance(exception, RpcCall)
|
|
36
|
-
|
|
37
|
-
def handle(self, exception: RpcCall):
|
|
38
|
-
console.print(exception.details())
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class UserFunctionExceptionHandler(BaseExceptionHandler["UserFunctionException"]):
|
|
42
|
-
def should_handle(self, exception: Exception) -> bool:
|
|
43
|
-
from fal.api import UserFunctionException
|
|
44
|
-
|
|
45
|
-
return isinstance(exception, UserFunctionException)
|
|
46
|
-
|
|
47
|
-
def handle(self, exception: UserFunctionException):
|
|
48
|
-
import rich
|
|
49
|
-
|
|
50
|
-
cause = exception.__cause__
|
|
51
|
-
exc = cause or exception
|
|
52
|
-
tb = rich.traceback.Traceback.from_exception(
|
|
53
|
-
type(exc),
|
|
54
|
-
exc,
|
|
55
|
-
exc.__traceback__,
|
|
56
|
-
)
|
|
57
|
-
console.print(tb)
|
|
58
|
-
super().handle(exception)
|
|
File without changes
|
|
File without changes
|