fal 0.12.5__py3-none-any.whl → 0.12.7__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/__main__.py +4 -0
- fal/_serialization.py +28 -0
- fal/api.py +35 -3
- fal/apps.py +6 -2
- fal/auth/auth0.py +12 -14
- fal/cli.py +60 -29
- fal/console/ux.py +7 -10
- fal/exceptions/__init__.py +2 -0
- fal/exceptions/handlers.py +24 -1
- fal/workflows.py +10 -9
- {fal-0.12.5.dist-info → fal-0.12.7.dist-info}/METADATA +7 -2
- {fal-0.12.5.dist-info → fal-0.12.7.dist-info}/RECORD +14 -13
- {fal-0.12.5.dist-info → fal-0.12.7.dist-info}/WHEEL +1 -1
- {fal-0.12.5.dist-info → fal-0.12.7.dist-info}/entry_points.txt +0 -0
fal/__main__.py
ADDED
fal/_serialization.py
CHANGED
|
@@ -137,10 +137,38 @@ def patch_pydantic_class_attributes():
|
|
|
137
137
|
pydantic.utils.DUNDER_ATTRIBUTES.add("__class__")
|
|
138
138
|
|
|
139
139
|
|
|
140
|
+
@mainify
|
|
141
|
+
def patch_exceptions():
|
|
142
|
+
# Adapting tblib.pickling_support.install for dill.
|
|
143
|
+
from types import TracebackType
|
|
144
|
+
|
|
145
|
+
import dill
|
|
146
|
+
from tblib.pickling_support import (
|
|
147
|
+
_get_subclasses,
|
|
148
|
+
pickle_exception,
|
|
149
|
+
pickle_traceback,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
@dill.register(TracebackType)
|
|
153
|
+
def save_traceback(pickler, obj):
|
|
154
|
+
unpickle, args = pickle_traceback(obj)
|
|
155
|
+
pickler.save_reduce(unpickle, args, obj=obj)
|
|
156
|
+
|
|
157
|
+
@dill.register(BaseException)
|
|
158
|
+
def save_exception(pickler, obj):
|
|
159
|
+
unpickle, args = pickle_exception(obj)
|
|
160
|
+
pickler.save_reduce(unpickle, args, obj=obj)
|
|
161
|
+
|
|
162
|
+
for exception_cls in _get_subclasses(BaseException):
|
|
163
|
+
dill.pickle(exception_cls, save_exception)
|
|
164
|
+
|
|
165
|
+
|
|
140
166
|
@mainify
|
|
141
167
|
def patch_dill():
|
|
142
168
|
import dill
|
|
143
169
|
|
|
144
170
|
dill.settings["recurse"] = True
|
|
171
|
+
|
|
172
|
+
patch_exceptions()
|
|
145
173
|
patch_pydantic_class_attributes()
|
|
146
174
|
patch_pydantic_field_serialization()
|
fal/api.py
CHANGED
|
@@ -175,6 +175,21 @@ def cached(func: Callable[ArgsT, ReturnT]) -> Callable[ArgsT, ReturnT]:
|
|
|
175
175
|
return wrapper
|
|
176
176
|
|
|
177
177
|
|
|
178
|
+
@mainify
|
|
179
|
+
class UserFunctionException(Exception):
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def match_class(obj, cls):
|
|
184
|
+
# NOTE: Can't use isinstance because we are not using dill's byref setting when
|
|
185
|
+
# loading/dumping objects in RPC, which means that our exceptions from remote
|
|
186
|
+
# server are created by value and are actually a separate class that only looks
|
|
187
|
+
# like original one.
|
|
188
|
+
#
|
|
189
|
+
# See https://github.com/fal-ai/fal/issues/142
|
|
190
|
+
return type(obj).__name__ == cls.__name__
|
|
191
|
+
|
|
192
|
+
|
|
178
193
|
def _prepare_partial_func(
|
|
179
194
|
func: Callable[ArgsT, ReturnT],
|
|
180
195
|
*args: ArgsT.args,
|
|
@@ -184,9 +199,19 @@ def _prepare_partial_func(
|
|
|
184
199
|
|
|
185
200
|
@wraps(func)
|
|
186
201
|
def wrapper(*remote_args: ArgsT.args, **remote_kwargs: ArgsT.kwargs) -> ReturnT:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
202
|
+
try:
|
|
203
|
+
result = func(*remote_args, *args, **remote_kwargs, **kwargs)
|
|
204
|
+
except Exception as exc:
|
|
205
|
+
tb = exc.__traceback__
|
|
206
|
+
if tb is not None and tb.tb_next is not None:
|
|
207
|
+
# remove our wrapper from user's traceback
|
|
208
|
+
tb = tb.tb_next
|
|
209
|
+
raise UserFunctionException(
|
|
210
|
+
f"Uncaught user function exception: {str(exc)}"
|
|
211
|
+
) from exc.with_traceback(tb)
|
|
212
|
+
finally:
|
|
213
|
+
with suppress(Exception):
|
|
214
|
+
patch_dill()
|
|
190
215
|
return result
|
|
191
216
|
|
|
192
217
|
return wrapper
|
|
@@ -895,6 +920,7 @@ class IsolatedFunction(Generic[ArgsT, ReturnT]):
|
|
|
895
920
|
raw_func: Callable[ArgsT, ReturnT]
|
|
896
921
|
options: Options
|
|
897
922
|
executor: ThreadPoolExecutor = field(default_factory=ThreadPoolExecutor)
|
|
923
|
+
reraise: bool = True
|
|
898
924
|
|
|
899
925
|
def __getstate__(self) -> dict[str, Any]:
|
|
900
926
|
# Ensure that the executor is not pickled.
|
|
@@ -946,6 +972,12 @@ class IsolatedFunction(Generic[ArgsT, ReturnT]):
|
|
|
946
972
|
f"function uses the following modules which weren't present in the environment definition:\n"
|
|
947
973
|
+ "\n".join(lines)
|
|
948
974
|
) from None
|
|
975
|
+
except Exception as exc:
|
|
976
|
+
cause = exc.__cause__
|
|
977
|
+
if self.reraise and match_class(exc, UserFunctionException) and cause:
|
|
978
|
+
# re-raise original exception without our wrappers
|
|
979
|
+
raise cause
|
|
980
|
+
raise
|
|
949
981
|
|
|
950
982
|
@overload
|
|
951
983
|
def on(
|
fal/apps.py
CHANGED
|
@@ -65,8 +65,12 @@ class RequestHandle:
|
|
|
65
65
|
def __post_init__(self):
|
|
66
66
|
app_id = _backwards_compatible_app_id(self.app_id)
|
|
67
67
|
# drop any extra path components
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
parts = app_id.split("/")[:3]
|
|
69
|
+
if parts[0] != "workflows":
|
|
70
|
+
# if the app_id is not a workflow, only keep the first two parts
|
|
71
|
+
parts = parts[:2]
|
|
72
|
+
|
|
73
|
+
self.app_id = "/".join(parts)
|
|
70
74
|
|
|
71
75
|
def status(self, *, logs: bool = False) -> _Status:
|
|
72
76
|
"""Check the status of an async inference request."""
|
fal/auth/auth0.py
CHANGED
|
@@ -9,7 +9,7 @@ import httpx
|
|
|
9
9
|
|
|
10
10
|
from fal.console import console
|
|
11
11
|
from fal.console.icons import CHECK_ICON
|
|
12
|
-
from fal.console.ux import
|
|
12
|
+
from fal.console.ux import maybe_open_browser_tab
|
|
13
13
|
|
|
14
14
|
WEBSITE_URL = "https://fal.ai"
|
|
15
15
|
|
|
@@ -27,19 +27,17 @@ def logout_url(return_url: str):
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def _open_browser(url: str, code: str | None) -> None:
|
|
30
|
-
|
|
31
|
-
console.print()
|
|
30
|
+
maybe_open_browser_tab(url)
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
)
|
|
32
|
+
console.print(
|
|
33
|
+
"If browser didn't open automatically, on your computer or mobile device navigate to"
|
|
34
|
+
)
|
|
35
|
+
console.print(url)
|
|
36
|
+
|
|
37
|
+
if code:
|
|
38
|
+
console.print(
|
|
39
|
+
f"\nConfirm it shows the following code: [markdown.code]{code}[/]\n"
|
|
40
|
+
)
|
|
43
41
|
|
|
44
42
|
|
|
45
43
|
def login() -> dict:
|
|
@@ -183,7 +181,7 @@ def validate_id_token(token: str):
|
|
|
183
181
|
def verify_access_token_expiration(token: str):
|
|
184
182
|
from jwt import decode
|
|
185
183
|
|
|
186
|
-
leeway =
|
|
184
|
+
leeway = 30 * 60 # 30 minutes
|
|
187
185
|
decode(
|
|
188
186
|
token,
|
|
189
187
|
leeway=-leeway, # negative to consider expired before actual expiration
|
fal/cli.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
from dataclasses import dataclass, field
|
|
4
5
|
from http import HTTPStatus
|
|
5
6
|
from sys import argv
|
|
6
|
-
from typing import Literal
|
|
7
|
+
from typing import Any, Callable, Literal
|
|
7
8
|
from uuid import uuid4
|
|
8
9
|
|
|
9
10
|
import click
|
|
10
11
|
import openapi_fal_rest.api.billing.get_user_details as get_user_details
|
|
11
12
|
from rich.table import Table
|
|
13
|
+
from rich_click import RichCommand, RichGroup
|
|
12
14
|
|
|
13
15
|
import fal
|
|
14
16
|
import fal.auth as auth
|
|
@@ -32,16 +34,29 @@ DEBUG_ENABLED = False
|
|
|
32
34
|
logger = get_logger(__name__)
|
|
33
35
|
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
@dataclass
|
|
38
|
+
class State:
|
|
39
|
+
debug: bool = False
|
|
40
|
+
invocation_id: str = field(default_factory=lambda: str(uuid4()))
|
|
38
41
|
|
|
39
|
-
def __init__(self, debug=False):
|
|
40
|
-
self.debug = debug
|
|
41
|
-
self.invocation_id = str(uuid4())
|
|
42
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)
|
|
43
48
|
|
|
44
|
-
|
|
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):
|
|
45
60
|
"""A custom implementation of the top-level group
|
|
46
61
|
(i.e. called on all commands and subcommands).
|
|
47
62
|
|
|
@@ -56,13 +71,11 @@ class MainGroup(click.Group):
|
|
|
56
71
|
def invoke(self, ctx):
|
|
57
72
|
from click.exceptions import Abort, ClickException, Exit
|
|
58
73
|
|
|
59
|
-
|
|
74
|
+
state = ctx.ensure_object(State)
|
|
60
75
|
qualified_name = " ".join([ctx.info_name] + argv[1:])
|
|
61
|
-
invocation_id = execution_info.invocation_id
|
|
62
|
-
set_debug_logging(execution_info.debug)
|
|
63
76
|
|
|
64
77
|
with self._tracer.start_as_current_span(
|
|
65
|
-
qualified_name, attributes={"invocation_id": invocation_id}
|
|
78
|
+
qualified_name, attributes={"invocation_id": state.invocation_id}
|
|
66
79
|
):
|
|
67
80
|
try:
|
|
68
81
|
logger.debug(
|
|
@@ -75,20 +88,20 @@ class MainGroup(click.Group):
|
|
|
75
88
|
raise
|
|
76
89
|
except Exception as exception:
|
|
77
90
|
logger.error(exception)
|
|
78
|
-
if
|
|
91
|
+
if state.debug:
|
|
79
92
|
# Here we supress detailed errors on click lines because
|
|
80
93
|
# they're mostly decorator calls, irrelevant to the dev's error tracing
|
|
81
94
|
console.print_exception(suppress=[click])
|
|
82
95
|
console.print()
|
|
83
96
|
console.print(
|
|
84
|
-
f"The [markdown.code]invocation_id[/] for this operation is: [white]{invocation_id}[/]"
|
|
97
|
+
f"The [markdown.code]invocation_id[/] for this operation is: [white]{state.invocation_id}[/]"
|
|
85
98
|
)
|
|
86
99
|
else:
|
|
87
100
|
self._exception_handler.handle(exception)
|
|
88
101
|
|
|
89
102
|
def add_command(
|
|
90
103
|
self,
|
|
91
|
-
cmd:
|
|
104
|
+
cmd: RichCommand,
|
|
92
105
|
name: str | None = None,
|
|
93
106
|
aliases: list[str] | None = None,
|
|
94
107
|
) -> None:
|
|
@@ -113,7 +126,7 @@ class MainGroup(click.Group):
|
|
|
113
126
|
self.add_command(alias_cmd, alias)
|
|
114
127
|
|
|
115
128
|
|
|
116
|
-
class AliasCommand(
|
|
129
|
+
class AliasCommand(RichCommand):
|
|
117
130
|
def __init__(self, wrapped):
|
|
118
131
|
self._wrapped = wrapped
|
|
119
132
|
|
|
@@ -129,31 +142,33 @@ class AliasCommand(click.Command):
|
|
|
129
142
|
|
|
130
143
|
|
|
131
144
|
@click.group(cls=MainGroup)
|
|
132
|
-
@click.option(
|
|
133
|
-
"--debug", is_flag=True, help="Enable detailed errors and verbose logging."
|
|
134
|
-
)
|
|
135
145
|
@click.version_option()
|
|
136
|
-
|
|
146
|
+
@debug_option()
|
|
147
|
+
def cli():
|
|
137
148
|
pass
|
|
138
149
|
|
|
139
150
|
|
|
140
151
|
###### Auth group ######
|
|
141
|
-
@click.group
|
|
152
|
+
@click.group(cls=RichGroup)
|
|
153
|
+
@debug_option()
|
|
142
154
|
def auth_cli():
|
|
143
155
|
pass
|
|
144
156
|
|
|
145
157
|
|
|
146
158
|
@auth_cli.command(name="login")
|
|
159
|
+
@debug_option()
|
|
147
160
|
def auth_login():
|
|
148
161
|
auth.login()
|
|
149
162
|
|
|
150
163
|
|
|
151
164
|
@auth_cli.command(name="logout")
|
|
165
|
+
@debug_option()
|
|
152
166
|
def auth_logout():
|
|
153
167
|
auth.logout()
|
|
154
168
|
|
|
155
169
|
|
|
156
170
|
@auth_cli.command(name="hello", hidden=True)
|
|
171
|
+
@debug_option()
|
|
157
172
|
def auth_test():
|
|
158
173
|
"""
|
|
159
174
|
To test auth.
|
|
@@ -162,9 +177,10 @@ def auth_test():
|
|
|
162
177
|
|
|
163
178
|
|
|
164
179
|
###### Key group ######
|
|
165
|
-
@click.group
|
|
180
|
+
@click.group(cls=RichGroup)
|
|
166
181
|
@click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
|
|
167
182
|
@click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
|
|
183
|
+
@debug_option()
|
|
168
184
|
@click.pass_context
|
|
169
185
|
def key_cli(ctx, host: str, port: str):
|
|
170
186
|
ctx.obj = sdk.FalServerlessClient(f"{host}:{port}")
|
|
@@ -183,6 +199,7 @@ def key_cli(ctx, host: str, port: str):
|
|
|
183
199
|
default=None,
|
|
184
200
|
help="An alias for the key.",
|
|
185
201
|
)
|
|
202
|
+
@debug_option()
|
|
186
203
|
@click.pass_obj
|
|
187
204
|
def key_generate(client: sdk.FalServerlessClient, scope: str, alias: str | None):
|
|
188
205
|
with client.connect() as connection:
|
|
@@ -197,6 +214,7 @@ def key_generate(client: sdk.FalServerlessClient, scope: str, alias: str | None)
|
|
|
197
214
|
|
|
198
215
|
|
|
199
216
|
@key_cli.command(name="list")
|
|
217
|
+
@debug_option()
|
|
200
218
|
@click.pass_obj
|
|
201
219
|
def key_list(client: sdk.FalServerlessClient):
|
|
202
220
|
table = Table(title="Keys")
|
|
@@ -217,6 +235,7 @@ def key_list(client: sdk.FalServerlessClient):
|
|
|
217
235
|
|
|
218
236
|
@key_cli.command(name="revoke")
|
|
219
237
|
@click.argument("key_id", required=True)
|
|
238
|
+
@debug_option()
|
|
220
239
|
@click.pass_obj
|
|
221
240
|
def key_revoke(client: sdk.FalServerlessClient, key_id: str):
|
|
222
241
|
with client.connect() as connection:
|
|
@@ -228,9 +247,10 @@ ALIAS_AUTH_OPTIONS = ["public", "private", "shared"]
|
|
|
228
247
|
ALIAS_AUTH_TYPE = Literal["public", "private", "shared"]
|
|
229
248
|
|
|
230
249
|
|
|
231
|
-
@click.group
|
|
250
|
+
@click.group(cls=RichGroup)
|
|
232
251
|
@click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
|
|
233
252
|
@click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
|
|
253
|
+
@debug_option()
|
|
234
254
|
@click.pass_context
|
|
235
255
|
def function_cli(ctx, host: str, port: str):
|
|
236
256
|
ctx.obj = api.FalServerlessHost(f"{host}:{port}")
|
|
@@ -272,6 +292,7 @@ def load_function_from(
|
|
|
272
292
|
)
|
|
273
293
|
@click.argument("file_path", required=True)
|
|
274
294
|
@click.argument("function_name", required=True)
|
|
295
|
+
@debug_option()
|
|
275
296
|
@click.pass_obj
|
|
276
297
|
def register_application(
|
|
277
298
|
host: api.FalServerlessHost,
|
|
@@ -323,15 +344,19 @@ def register_application(
|
|
|
323
344
|
@function_cli.command("run")
|
|
324
345
|
@click.argument("file_path", required=True)
|
|
325
346
|
@click.argument("function_name", required=True)
|
|
347
|
+
@debug_option()
|
|
326
348
|
@click.pass_obj
|
|
327
349
|
def run(host: api.FalServerlessHost, file_path: str, function_name: str):
|
|
328
350
|
isolated_function = load_function_from(host, file_path, function_name)
|
|
351
|
+
# let our exc handlers handle UserFunctionException
|
|
352
|
+
isolated_function.reraise = False
|
|
329
353
|
isolated_function()
|
|
330
354
|
|
|
331
355
|
|
|
332
356
|
@function_cli.command("logs")
|
|
333
357
|
@click.option("--lines", default=100)
|
|
334
358
|
@click.option("--url", default=None)
|
|
359
|
+
@debug_option()
|
|
335
360
|
@click.pass_obj
|
|
336
361
|
def get_logs(
|
|
337
362
|
host: api.FalServerlessHost, lines: int | None = 100, url: str | None = None
|
|
@@ -342,9 +367,10 @@ def get_logs(
|
|
|
342
367
|
|
|
343
368
|
|
|
344
369
|
##### Alias group #####
|
|
345
|
-
@click.group
|
|
370
|
+
@click.group(cls=RichGroup)
|
|
346
371
|
@click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
|
|
347
372
|
@click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
|
|
373
|
+
@debug_option()
|
|
348
374
|
@click.pass_context
|
|
349
375
|
def alias_cli(ctx, host: str, port: str):
|
|
350
376
|
ctx.obj = api.FalServerlessClient(f"{host}:{port}")
|
|
@@ -385,6 +411,7 @@ def _alias_table(aliases: list[AliasInfo]):
|
|
|
385
411
|
type=click.Choice(ALIAS_AUTH_OPTIONS),
|
|
386
412
|
default="private",
|
|
387
413
|
)
|
|
414
|
+
@debug_option()
|
|
388
415
|
@click.pass_obj
|
|
389
416
|
def alias_set(
|
|
390
417
|
client: api.FalServerlessClient,
|
|
@@ -398,6 +425,7 @@ def alias_set(
|
|
|
398
425
|
|
|
399
426
|
@alias_cli.command("delete")
|
|
400
427
|
@click.argument("alias", required=True)
|
|
428
|
+
@debug_option()
|
|
401
429
|
@click.pass_obj
|
|
402
430
|
def alias_delete(client: api.FalServerlessClient, alias: str):
|
|
403
431
|
with client.connect() as connection:
|
|
@@ -407,6 +435,7 @@ def alias_delete(client: api.FalServerlessClient, alias: str):
|
|
|
407
435
|
|
|
408
436
|
|
|
409
437
|
@alias_cli.command("list")
|
|
438
|
+
@debug_option()
|
|
410
439
|
@click.pass_obj
|
|
411
440
|
def alias_list(client: api.FalServerlessClient):
|
|
412
441
|
with client.connect() as connection:
|
|
@@ -428,6 +457,7 @@ def alias_list(client: api.FalServerlessClient):
|
|
|
428
457
|
# "auth_mode",
|
|
429
458
|
# type=click.Choice(ALIAS_AUTH_OPTIONS),
|
|
430
459
|
# )
|
|
460
|
+
@debug_option()
|
|
431
461
|
@click.pass_obj
|
|
432
462
|
def alias_update(
|
|
433
463
|
client: api.FalServerlessClient,
|
|
@@ -461,6 +491,7 @@ def alias_update(
|
|
|
461
491
|
|
|
462
492
|
@alias_cli.command("runners")
|
|
463
493
|
@click.argument("alias", required=True)
|
|
494
|
+
@debug_option()
|
|
464
495
|
@click.pass_obj
|
|
465
496
|
def alias_list_runners(
|
|
466
497
|
client: api.FalServerlessClient,
|
|
@@ -491,15 +522,17 @@ def alias_list_runners(
|
|
|
491
522
|
|
|
492
523
|
|
|
493
524
|
##### Secrets group #####
|
|
494
|
-
@click.group
|
|
525
|
+
@click.group(cls=RichGroup)
|
|
495
526
|
@click.option("--host", default=DEFAULT_HOST, envvar=HOST_ENVVAR)
|
|
496
527
|
@click.option("--port", default=DEFAULT_PORT, envvar=PORT_ENVVAR, hidden=True)
|
|
528
|
+
@debug_option()
|
|
497
529
|
@click.pass_context
|
|
498
530
|
def secrets_cli(ctx, host: str, port: str):
|
|
499
531
|
ctx.obj = sdk.FalServerlessClient(f"{host}:{port}")
|
|
500
532
|
|
|
501
533
|
|
|
502
534
|
@secrets_cli.command("list")
|
|
535
|
+
@debug_option()
|
|
503
536
|
@click.pass_obj
|
|
504
537
|
def list_secrets(client: api.FalServerlessClient):
|
|
505
538
|
table = Table(title="Secrets")
|
|
@@ -516,6 +549,7 @@ def list_secrets(client: api.FalServerlessClient):
|
|
|
516
549
|
@secrets_cli.command("set")
|
|
517
550
|
@click.argument("secret_name", required=True)
|
|
518
551
|
@click.argument("secret_value", required=True)
|
|
552
|
+
@debug_option()
|
|
519
553
|
@click.pass_obj
|
|
520
554
|
def set_secret(client: api.FalServerlessClient, secret_name: str, secret_value: str):
|
|
521
555
|
with client.connect() as connection:
|
|
@@ -525,6 +559,7 @@ def set_secret(client: api.FalServerlessClient, secret_name: str, secret_value:
|
|
|
525
559
|
|
|
526
560
|
@secrets_cli.command("delete")
|
|
527
561
|
@click.argument("secret_name", required=True)
|
|
562
|
+
@debug_option()
|
|
528
563
|
@click.pass_obj
|
|
529
564
|
def delete_secret(client: api.FalServerlessClient, secret_name: str):
|
|
530
565
|
with client.connect() as connection:
|
|
@@ -583,7 +618,3 @@ def _get_user_id() -> str:
|
|
|
583
618
|
return user_id
|
|
584
619
|
except Exception as e:
|
|
585
620
|
raise api.FalServerlessError(f"Could not parse the user data: {e}")
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if __name__ == "__main__":
|
|
589
|
-
cli()
|
fal/console/ux.py
CHANGED
|
@@ -3,15 +3,12 @@ from __future__ import annotations
|
|
|
3
3
|
import webbrowser
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def
|
|
7
|
-
"""Gets a reference to the default browser, if available.
|
|
8
|
-
|
|
9
|
-
This allows us to decide on a flow before showing the actual browser window.
|
|
10
|
-
It also avoids unwanted output in the console from the standard `webbrowser.open()`.
|
|
11
|
-
|
|
12
|
-
See https://stackoverflow.com/a/19199794
|
|
13
|
-
"""
|
|
6
|
+
def maybe_open_browser_tab(url) -> None:
|
|
14
7
|
try:
|
|
15
|
-
|
|
8
|
+
# Avoids unwanted output in the console from the standard `webbrowser.open()`.
|
|
9
|
+
# See https://stackoverflow.com/a/19199794
|
|
10
|
+
browser = webbrowser.get()
|
|
11
|
+
|
|
12
|
+
browser.open_new_tab(url)
|
|
16
13
|
except webbrowser.Error:
|
|
17
|
-
|
|
14
|
+
pass
|
fal/exceptions/__init__.py
CHANGED
|
@@ -4,6 +4,7 @@ from .handlers import (
|
|
|
4
4
|
BaseExceptionHandler,
|
|
5
5
|
FalServerlessExceptionHandler,
|
|
6
6
|
GrpcExceptionHandler,
|
|
7
|
+
UserFunctionExceptionHandler,
|
|
7
8
|
)
|
|
8
9
|
|
|
9
10
|
|
|
@@ -20,6 +21,7 @@ class ApplicationExceptionHandler:
|
|
|
20
21
|
_handlers: list[BaseExceptionHandler] = [
|
|
21
22
|
GrpcExceptionHandler(),
|
|
22
23
|
FalServerlessExceptionHandler(),
|
|
24
|
+
UserFunctionExceptionHandler(),
|
|
23
25
|
]
|
|
24
26
|
|
|
25
27
|
def handle(self, exception):
|
fal/exceptions/handlers.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Generic, TypeVar
|
|
3
|
+
from typing import TYPE_CHECKING, Generic, TypeVar
|
|
4
4
|
|
|
5
5
|
from grpc import Call as RpcCall
|
|
6
6
|
from rich.markdown import Markdown
|
|
@@ -8,6 +8,9 @@ from rich.markdown import Markdown
|
|
|
8
8
|
from fal.console import console
|
|
9
9
|
from fal.console.icons import CROSS_ICON
|
|
10
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from fal.api import UserFunctionException
|
|
13
|
+
|
|
11
14
|
from ._base import FalServerlessException
|
|
12
15
|
|
|
13
16
|
ExceptionType = TypeVar("ExceptionType")
|
|
@@ -44,3 +47,23 @@ class GrpcExceptionHandler(BaseExceptionHandler[RpcCall]):
|
|
|
44
47
|
|
|
45
48
|
def handle(self, exception: RpcCall):
|
|
46
49
|
console.print(exception.details())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class UserFunctionExceptionHandler(BaseExceptionHandler["UserFunctionException"]):
|
|
53
|
+
def should_handle(self, exception: Exception) -> bool:
|
|
54
|
+
from fal.api import UserFunctionException, match_class
|
|
55
|
+
|
|
56
|
+
return match_class(exception, UserFunctionException)
|
|
57
|
+
|
|
58
|
+
def handle(self, exception: UserFunctionException):
|
|
59
|
+
import rich
|
|
60
|
+
|
|
61
|
+
cause = exception.__cause__
|
|
62
|
+
exc = cause or exception
|
|
63
|
+
tb = rich.traceback.Traceback.from_exception(
|
|
64
|
+
type(exc),
|
|
65
|
+
exc,
|
|
66
|
+
exc.__traceback__,
|
|
67
|
+
)
|
|
68
|
+
console.print(tb)
|
|
69
|
+
super().handle(exception)
|
fal/workflows.py
CHANGED
|
@@ -12,6 +12,7 @@ import rich
|
|
|
12
12
|
from openapi_fal_rest.api.workflows import (
|
|
13
13
|
create_or_update_workflow_workflows_post as publish_workflow,
|
|
14
14
|
)
|
|
15
|
+
from openapi_fal_rest.models.http_validation_error import HTTPValidationError
|
|
15
16
|
from pydantic import BaseModel
|
|
16
17
|
from rich.syntax import Syntax
|
|
17
18
|
|
|
@@ -363,7 +364,7 @@ class Workflow:
|
|
|
363
364
|
|
|
364
365
|
to_dict = to_json
|
|
365
366
|
|
|
366
|
-
def publish(self, title: str, *, is_public: bool = True)
|
|
367
|
+
def publish(self, title: str, *, is_public: bool = True):
|
|
367
368
|
workflow_contents = publish_workflow.TypedWorkflow(
|
|
368
369
|
name=self.name,
|
|
369
370
|
title=title,
|
|
@@ -376,14 +377,14 @@ class Workflow:
|
|
|
376
377
|
)
|
|
377
378
|
if isinstance(published_workflow, Exception):
|
|
378
379
|
raise published_workflow
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
380
|
+
if isinstance(published_workflow, HTTPValidationError):
|
|
381
|
+
raise RuntimeError(published_workflow.detail)
|
|
382
|
+
if not published_workflow:
|
|
383
|
+
raise RuntimeError("Failed to publish the workflow")
|
|
384
|
+
|
|
385
|
+
# NOTE: dropping the provider prefix from the user_id
|
|
386
|
+
user_id_part = published_workflow.user_id.split("|")[-1]
|
|
387
|
+
return f"{user_id_part}/{published_workflow.name}"
|
|
387
388
|
|
|
388
389
|
|
|
389
390
|
def create_workflow(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fal
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.7
|
|
4
4
|
Summary: fal is an easy-to-use Serverless Python Framework
|
|
5
5
|
Author: Features & Labels
|
|
6
6
|
Author-email: hello@fal.ai
|
|
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.9
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
14
|
Requires-Dist: attrs (>=21.3.0)
|
|
14
15
|
Requires-Dist: click (>=8.1.3,<9.0.0)
|
|
15
16
|
Requires-Dist: colorama (>=0.4.6,<0.5.0)
|
|
@@ -29,15 +30,19 @@ Requires-Dist: pathspec (>=0.11.1,<0.12.0)
|
|
|
29
30
|
Requires-Dist: pillow (>=10.2.0,<11.0.0)
|
|
30
31
|
Requires-Dist: portalocker (>=2.7.0,<3.0.0)
|
|
31
32
|
Requires-Dist: pydantic (<2.0)
|
|
32
|
-
Requires-Dist: pyjwt (>=2.8.0,<3.0.0)
|
|
33
|
+
Requires-Dist: pyjwt[crypto] (>=2.8.0,<3.0.0)
|
|
33
34
|
Requires-Dist: python-dateutil (>=2.8.0,<3.0.0)
|
|
34
35
|
Requires-Dist: rich (>=13.3.2,<14.0.0)
|
|
36
|
+
Requires-Dist: rich_click
|
|
35
37
|
Requires-Dist: structlog (>=22.3.0,<23.0.0)
|
|
36
38
|
Requires-Dist: types-python-dateutil (>=2.8.0,<3.0.0)
|
|
37
39
|
Requires-Dist: typing-extensions (>=4.7.1,<5.0.0)
|
|
38
40
|
Requires-Dist: websockets (>=12.0,<13.0)
|
|
39
41
|
Description-Content-Type: text/markdown
|
|
40
42
|
|
|
43
|
+
[](https://pypi.org/project/fal)
|
|
44
|
+
[](https://github.com/fal-ai/fal/actions)
|
|
45
|
+
|
|
41
46
|
# fal
|
|
42
47
|
|
|
43
48
|
fal is a serverless Python runtime that lets you run and scale code in the cloud with no infra management.
|
|
@@ -41,22 +41,23 @@ openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQ
|
|
|
41
41
|
openapi_fal_rest/py.typed,sha256=8ZJUsxZiuOy1oJeVhsTWQhTG_6pTVHVXk5hJL79ebTk,25
|
|
42
42
|
openapi_fal_rest/types.py,sha256=GLwJwOotUOdfqryo_r0naw55-dh6Ilm4IvxePekSACk,994
|
|
43
43
|
fal/__init__.py,sha256=6SvCuotCb0tuqSWDZSFDjtySktJ5m1QpVIlefumJpvM,1199
|
|
44
|
-
fal/
|
|
45
|
-
fal/
|
|
44
|
+
fal/__main__.py,sha256=8hDtWlaFZK24KhfNq_ZKgtXqYHsDQDetukOCMlsbW0Q,59
|
|
45
|
+
fal/_serialization.py,sha256=PK8E6Z1-_E3IYnt5pNWWViOas0SMZTvpNE7a2arj_3U,4948
|
|
46
|
+
fal/api.py,sha256=YNiX6QCDYDdfqccHzsOd0YYBDslkzVNhlDUxPubYsb4,34033
|
|
46
47
|
fal/app.py,sha256=KAIgvBBpvzp6oY8BpH5hFOLDUpG4bjtwlV5jPGj2IE0,12487
|
|
47
|
-
fal/apps.py,sha256=
|
|
48
|
+
fal/apps.py,sha256=UhR6mq8jBiTAp-QvUnvbnMNcuJ5wHIKSqdlfyx8aBQ8,6829
|
|
48
49
|
fal/auth/__init__.py,sha256=ZnR1fxonzDk0UhS3-i33Kq2xOrN-leYXvJ-Ddnj94xc,3594
|
|
49
|
-
fal/auth/auth0.py,sha256=
|
|
50
|
+
fal/auth/auth0.py,sha256=5y4-9udOSX2-N_zvinLCpFwl10MdaPydZX2v9GQMZEE,5406
|
|
50
51
|
fal/auth/local.py,sha256=lZqp4j32l2xFpY8zYvLoIHHyJrNAJDcm5MxgsLpY_pw,1786
|
|
51
|
-
fal/cli.py,sha256=
|
|
52
|
+
fal/cli.py,sha256=RCjns-r2yyJ6AmDFMkOxhKcZqtK5gY2lruA9ympdcUc,18432
|
|
52
53
|
fal/console/__init__.py,sha256=ernZ4bzvvliQh5SmrEqQ7lA5eVcbw6Ra2jalKtA7dxg,132
|
|
53
54
|
fal/console/icons.py,sha256=De9MfFaSkO2Lqfne13n3PrYfTXJVIzYZVqYn5BWsdrA,108
|
|
54
|
-
fal/console/ux.py,sha256=
|
|
55
|
+
fal/console/ux.py,sha256=KMQs3UHQvVHDxDQQqlot-WskVKoMQXOE3jiVkkfmIMY,356
|
|
55
56
|
fal/env.py,sha256=-fA8x62BbOX3MOuO0maupa-_QJ9PNwr8ogfeG11QUyQ,53
|
|
56
|
-
fal/exceptions/__init__.py,sha256=
|
|
57
|
+
fal/exceptions/__init__.py,sha256=A8oJQQQlb8WQieusFK6O4CBc4s6CUSiNgj0xVKJKvgg,1012
|
|
57
58
|
fal/exceptions/_base.py,sha256=LeQmx-soL_-s1742WKN18VwTVjUuYP0L0BdQHPJBpM4,460
|
|
58
59
|
fal/exceptions/auth.py,sha256=01Ro7SyGJpwchubdHe14Cl6-Al1jUj16Sy4BvakNWf4,384
|
|
59
|
-
fal/exceptions/handlers.py,sha256=
|
|
60
|
+
fal/exceptions/handlers.py,sha256=3z4DGTErw0zW3UW4p3JltlqpsMV10kqMtFOxpniMSBU,2105
|
|
60
61
|
fal/flags.py,sha256=AATQO65M4C87dGp0j7o6cSQWcr62xE-8DnJYsUjFFbw,942
|
|
61
62
|
fal/logging/__init__.py,sha256=snqprf7-sKw6oAATS_Yxklf-a3XhLg0vIHICPwLp6TM,1583
|
|
62
63
|
fal/logging/isolate.py,sha256=yDW_P4aR-t53IRmvD2Iprufv1Wn-xQXoBbMB2Ufr59s,2122
|
|
@@ -81,8 +82,8 @@ fal/toolkit/mainify.py,sha256=E7gE45nZQZoaJdSlIq0mqajcH-IjcuPBWFmKm5hvhAU,406
|
|
|
81
82
|
fal/toolkit/optimize.py,sha256=OIhX0T-efRMgUJDpvL0bujdun5SovZgTdKxNOv01b_Y,1394
|
|
82
83
|
fal/toolkit/utils/__init__.py,sha256=b3zVpm50Upx1saXU7RiV9r9in6-Chs4OU9KRjBv7MYI,83
|
|
83
84
|
fal/toolkit/utils/download_utils.py,sha256=bigcLJjLK1OBAGxpYisJ0-5vcQCh0HAPuCykPrcCNd0,15596
|
|
84
|
-
fal/workflows.py,sha256=
|
|
85
|
-
fal-0.12.
|
|
86
|
-
fal-0.12.
|
|
87
|
-
fal-0.12.
|
|
88
|
-
fal-0.12.
|
|
85
|
+
fal/workflows.py,sha256=hkyDk5KQCDcjyUbo_IhQePGP8t4nxzZ7Uw6rVbLtdq4,14448
|
|
86
|
+
fal-0.12.7.dist-info/METADATA,sha256=qu608UFLzhem9FS6QaA-qjgs4PVSQcVW4aPajpGs5Gg,3263
|
|
87
|
+
fal-0.12.7.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
88
|
+
fal-0.12.7.dist-info/entry_points.txt,sha256=nE9GBVV3PdBosudFwbIzZQUe_9lfPR6EH8K_FdDASnM,62
|
|
89
|
+
fal-0.12.7.dist-info/RECORD,,
|
|
File without changes
|