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 ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import cli
2
+
3
+ if __name__ == "__main__":
4
+ cli()
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
- result = func(*remote_args, *args, **remote_kwargs, **kwargs)
188
- with suppress(Exception):
189
- patch_dill()
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
- user_id, app_name = app_id.split("/")[:2]
69
- self.app_id = f"{user_id}/{app_name}"
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 get_browser
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
- browser = get_browser()
31
- console.print()
30
+ maybe_open_browser_tab(url)
32
31
 
33
- if browser is not None and click.confirm(
34
- "Open browser automatically ('no' to show URL)?", default=True, err=True
35
- ):
36
- browser.open_new_tab(url)
37
- else:
38
- console.print(f"On your computer or mobile device navigate to: {url}")
39
- if code:
40
- console.print(
41
- f"Confirm it shows the following code: [markdown.code]{code}[/]"
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 = 60 * 30 * 60 # 30 minutes
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
- class ExecutionInfo:
36
- debug: bool
37
- invocation_id: str
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
- class MainGroup(click.Group):
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
- execution_info = ExecutionInfo(debug=ctx.params["debug"])
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 execution_info.debug:
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: click.Command,
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(click.Command):
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
- def cli(debug):
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 get_browser() -> webbrowser.BaseBrowser | None:
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
- return webbrowser.get()
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
- return None
14
+ pass
@@ -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):
@@ -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) -> None:
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
- return (
381
- REST_CLIENT.base_url
382
- + "/workflows/"
383
- + published_workflow.user_id
384
- + "/"
385
- + published_workflow.name
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.5
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
+ [![PyPI](https://img.shields.io/pypi/v/fal.svg?logo=PyPI)](https://pypi.org/project/fal)
44
+ [![Tests](https://img.shields.io/github/actions/workflow/status/fal-ai/fal/integration_tests.yaml?label=Tests)](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/_serialization.py,sha256=l_dZuSX5BT7SogXw1CalYLfT2H3zy3tfq4y6jHuxZqQ,4201
45
- fal/api.py,sha256=Qack_oYNkvF4qown3P_oKvyvRfTJkhOG7PL1xpa8FUQ,32872
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=T387WJDtKpKEytu27b2AVqqo0uijKrRT9ymk6FcRiEw,6705
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=Lb63u99fjZVIpbkAicVL1B5V6iPb2mE04bwHmorXbu4,5542
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=mvYl6-wlrofY9wFzdjAO31ut2RVvwnXf6aIqFkxNWmQ,17526
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=4vj1aGA3grRn-ebeMuDLR6u3YjMwUGpqtNgdTG9su5s,485
55
+ fal/console/ux.py,sha256=KMQs3UHQvVHDxDQQqlot-WskVKoMQXOE3jiVkkfmIMY,356
55
56
  fal/env.py,sha256=-fA8x62BbOX3MOuO0maupa-_QJ9PNwr8ogfeG11QUyQ,53
56
- fal/exceptions/__init__.py,sha256=Q4LCSqIrJ8GFQZWH5BvWL5mDPR0HwYQuIhNvsdiOkEU,938
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=b21a8S13euECArjpgm2N69HsShqLYVqAboIeMoWlWA4,1414
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=2xMC4dybiAv05eEua_YpKRAs395YR2UVKvhGS0HZdm8,14155
85
- fal-0.12.5.dist-info/entry_points.txt,sha256=nE9GBVV3PdBosudFwbIzZQUe_9lfPR6EH8K_FdDASnM,62
86
- fal-0.12.5.dist-info/METADATA,sha256=CG9k8-tOXQ9rvTFC4mLc44pj7FDNLpTpyYwEeBp5n4w,2937
87
- fal-0.12.5.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
88
- fal-0.12.5.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.4.0
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any