fal 0.15.2__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/cli/deploy.py ADDED
@@ -0,0 +1,146 @@
1
+ from pathlib import Path
2
+
3
+ from .parser import FalClientParser, RefAction
4
+
5
+
6
+ def _remove_http_and_port_from_url(url):
7
+ # Remove http://
8
+ if "http://" in url:
9
+ url = url.replace("http://", "")
10
+
11
+ # Remove https://
12
+ if "https://" in url:
13
+ url = url.replace("https://", "")
14
+
15
+ # Remove port information
16
+ url_parts = url.split(":")
17
+ if len(url_parts) > 1:
18
+ url = url_parts[0]
19
+
20
+ return url
21
+
22
+
23
+ def _get_user_id() -> str:
24
+ import json
25
+ from http import HTTPStatus
26
+
27
+ import openapi_fal_rest.api.billing.get_user_details as get_user_details
28
+
29
+ from fal.api import FalServerlessError
30
+ from fal.rest_client import REST_CLIENT
31
+
32
+ try:
33
+ user_details_response = get_user_details.sync_detailed(
34
+ client=REST_CLIENT,
35
+ )
36
+ except Exception as e:
37
+ raise FalServerlessError(f"Error fetching user details: {str(e)}")
38
+
39
+ if user_details_response.status_code != HTTPStatus.OK:
40
+ try:
41
+ content = json.loads(user_details_response.content.decode("utf8"))
42
+ except Exception:
43
+ raise FalServerlessError(
44
+ f"Error fetching user details: {user_details_response}"
45
+ )
46
+ else:
47
+ raise FalServerlessError(content["detail"])
48
+ try:
49
+ full_user_id = user_details_response.parsed.user_id
50
+ _provider, _, user_id = full_user_id.partition("|")
51
+ if not user_id:
52
+ user_id = full_user_id
53
+
54
+ return user_id
55
+ except Exception as e:
56
+ raise FalServerlessError(f"Could not parse the user data: {e}")
57
+
58
+
59
+ def _deploy(args):
60
+ from fal.api import FalServerlessError, FalServerlessHost
61
+ from fal.utils import load_function_from
62
+
63
+ file_path, func_name = args.app_ref
64
+ if file_path is None:
65
+ # Try to find a python file in the current directory
66
+ options = list(Path(".").glob("*.py"))
67
+ if len(options) == 0:
68
+ raise FalServerlessError(
69
+ "No python files found in the current directory"
70
+ )
71
+ elif len(options) > 1:
72
+ raise FalServerlessError(
73
+ "Multiple python files found in the current directory. "
74
+ "Please specify the file path of the app you want to deploy."
75
+ )
76
+
77
+ [file_path] = options
78
+ file_path = str(file_path)
79
+
80
+ user_id = _get_user_id()
81
+ host = FalServerlessHost(args.host)
82
+ isolated_function, app_name = load_function_from(
83
+ host,
84
+ file_path,
85
+ func_name,
86
+ )
87
+ app_name = args.app_name or app_name
88
+ app_id = host.register(
89
+ func=isolated_function.func,
90
+ options=isolated_function.options,
91
+ application_name=app_name,
92
+ application_auth_mode=args.auth,
93
+ metadata=isolated_function.options.host.get("metadata", {}),
94
+ )
95
+
96
+ if app_id:
97
+ gateway_host = _remove_http_and_port_from_url(host.url)
98
+ gateway_host = (
99
+ gateway_host.replace("api.", "").replace("alpha.", "").replace("ai", "run")
100
+ )
101
+
102
+ args.console.print(
103
+ "Registered a new revision for function "
104
+ f"'{app_name}' (revision='{app_id}')."
105
+ )
106
+ args.console.print(f"URL: https://{gateway_host}/{user_id}/{app_name}")
107
+
108
+
109
+ def add_parser(main_subparsers, parents):
110
+ from fal.sdk import ALIAS_AUTH_MODES
111
+
112
+ deploy_help = "Deploy a fal application."
113
+ epilog = (
114
+ "Examples:\n"
115
+ " fal deploy\n"
116
+ " fal deploy path/to/myfile.py\n"
117
+ " fal deploy path/to/myfile.py::MyApp\n"
118
+ " fal deploy path/to/myfile.py::MyApp --app-name myapp --auth public\n"
119
+ )
120
+ parser = main_subparsers.add_parser(
121
+ "deploy",
122
+ parents=[*parents, FalClientParser(add_help=False)],
123
+ description=deploy_help,
124
+ help=deploy_help,
125
+ epilog=epilog,
126
+ )
127
+ parser.add_argument(
128
+ "app_ref",
129
+ nargs="?",
130
+ action=RefAction,
131
+ help=(
132
+ "Application reference. "
133
+ "For example: `myfile.py::MyApp`, `myfile.py`."
134
+ ),
135
+ )
136
+ parser.add_argument(
137
+ "--app-name",
138
+ help="Application name to deploy with.",
139
+ )
140
+ parser.add_argument(
141
+ "--auth",
142
+ choices=ALIAS_AUTH_MODES,
143
+ default="private",
144
+ help="Application authentication mode.",
145
+ )
146
+ parser.set_defaults(func=_deploy)
fal/cli/keys.py ADDED
@@ -0,0 +1,118 @@
1
+ from fal.sdk import KeyScope
2
+
3
+ from .parser import FalClientParser
4
+
5
+
6
+ def _create(args):
7
+ from fal.sdk import FalServerlessClient
8
+
9
+ client = FalServerlessClient(args.host)
10
+ with client.connect() as connection:
11
+ parsed_scope = KeyScope(args.scope)
12
+ result = connection.create_user_key(parsed_scope, args.desc)
13
+ args.console.print(
14
+ f"Generated key id and key secret, with the scope `{args.scope}`.\n"
15
+ "This is the only time the secret will be visible.\n"
16
+ "You will need to generate a new key pair if you lose access to this "
17
+ "secret."
18
+ )
19
+ args.console.print(f"FAL_KEY='{result[1]}:{result[0]}'")
20
+
21
+
22
+ def _add_create_parser(subparsers, parents):
23
+ create_help = "Create a key."
24
+ parser = subparsers.add_parser(
25
+ "create",
26
+ description=create_help,
27
+ help=create_help,
28
+ parents=parents,
29
+ )
30
+ parser.add_argument(
31
+ "--scope",
32
+ required=True,
33
+ choices=[KeyScope.ADMIN.value, KeyScope.API.value],
34
+ help="The privilage scope of the key.",
35
+ )
36
+ parser.add_argument(
37
+ "--desc",
38
+ help='Key description (e.g. "My Test Key")',
39
+ )
40
+ parser.set_defaults(func=_create)
41
+
42
+
43
+ def _list(args):
44
+ from rich.table import Table
45
+
46
+ from fal.sdk import FalServerlessClient
47
+
48
+ client = FalServerlessClient(args.host)
49
+ table = Table()
50
+ table.add_column("Key ID")
51
+ table.add_column("Created At")
52
+ table.add_column("Scope")
53
+ table.add_column("Description")
54
+
55
+ with client.connect() as connection:
56
+ keys = connection.list_user_keys()
57
+ for key in keys:
58
+ table.add_row(
59
+ key.key_id, str(key.created_at), str(key.scope.value), key.alias,
60
+ )
61
+
62
+ args.console.print(table)
63
+
64
+
65
+ def _add_list_parser(subparsers, parents):
66
+ list_help = "List keys."
67
+ parser = subparsers.add_parser(
68
+ "list",
69
+ description=list_help,
70
+ help=list_help,
71
+ parents=parents,
72
+ )
73
+ parser.set_defaults(func=_list)
74
+
75
+
76
+ def _revoke(args):
77
+ from fal.sdk import FalServerlessClient
78
+
79
+ client = FalServerlessClient(args.host)
80
+ with client.connect() as connection:
81
+ connection.revoke_user_key(args.key_id)
82
+
83
+
84
+ def _add_revoke_parser(subparsers, parents):
85
+ revoke_help = "Revoke key."
86
+ parser = subparsers.add_parser(
87
+ "revoke",
88
+ description=revoke_help,
89
+ help=revoke_help,
90
+ parents=parents,
91
+ )
92
+ parser.add_argument(
93
+ "key_id",
94
+ help="Key ID.",
95
+ )
96
+ parser.set_defaults(func=_revoke)
97
+
98
+
99
+ def add_parser(main_subparsers, parents):
100
+ keys_help = "Manage fal keys."
101
+ parser = main_subparsers.add_parser(
102
+ "keys",
103
+ aliases=["key"],
104
+ description=keys_help,
105
+ help=keys_help,
106
+ parents=parents,
107
+ )
108
+
109
+ subparsers = parser.add_subparsers(
110
+ title="Commands",
111
+ metavar="command",
112
+ required=True,
113
+ parser_class=FalClientParser,
114
+ )
115
+
116
+ _add_create_parser(subparsers, parents)
117
+ _add_list_parser(subparsers, parents)
118
+ _add_revoke_parser(subparsers, parents)
fal/cli/main.py ADDED
@@ -0,0 +1,82 @@
1
+ import argparse
2
+
3
+ import rich
4
+
5
+ from fal import __version__
6
+ from fal.console import console
7
+ from fal.console.icons import CROSS_ICON
8
+
9
+ from . import apps, auth, deploy, keys, run, secrets
10
+ from .debug import debugtools, get_debug_parser
11
+ from .parser import FalParser, FalParserExit
12
+
13
+
14
+ def _get_main_parser() -> argparse.ArgumentParser:
15
+ parents = [get_debug_parser()]
16
+ parser = FalParser(
17
+ prog="fal",
18
+ parents=parents,
19
+ )
20
+
21
+ parser.add_argument(
22
+ "--version",
23
+ action="version",
24
+ version=__version__,
25
+ help="Show fal version.",
26
+ )
27
+
28
+ subparsers = parser.add_subparsers(
29
+ title="Commands",
30
+ metavar="command",
31
+ required=True,
32
+ )
33
+
34
+ for cmd in [auth, apps, deploy, run, keys, secrets]:
35
+ cmd.add_parser(subparsers, parents)
36
+
37
+ return parser
38
+
39
+
40
+ def parse_args(argv=None):
41
+ parser = _get_main_parser()
42
+ args = parser.parse_args(argv)
43
+ args.console = console
44
+ args.parser = parser
45
+ return args
46
+
47
+
48
+ def main(argv=None) -> int:
49
+ import grpc
50
+
51
+ from fal.api import UserFunctionException
52
+
53
+ ret = 1
54
+ try:
55
+ args = parse_args(argv)
56
+
57
+ with debugtools(args):
58
+ ret = args.func(args)
59
+ except UserFunctionException as _exc:
60
+ cause = _exc.__cause__
61
+ exc: BaseException = cause or _exc
62
+ tb = rich.traceback.Traceback.from_exception(
63
+ type(exc),
64
+ exc,
65
+ exc.__traceback__,
66
+ )
67
+ console.print(tb)
68
+ console.print("Unhandled user exception")
69
+ except KeyboardInterrupt:
70
+ console.print("Aborted.")
71
+ except grpc.RpcError as exc:
72
+ console.print(exc.details())
73
+ except FalParserExit as exc:
74
+ ret = exc.status
75
+ except Exception as exc:
76
+ msg = f"{CROSS_ICON} {str(exc)}"
77
+ cause = exc.__cause__
78
+ if cause is not None:
79
+ msg += f": {str(cause)}"
80
+ console.print(msg)
81
+
82
+ return ret
fal/cli/parser.py ADDED
@@ -0,0 +1,74 @@
1
+ import argparse
2
+ import sys
3
+
4
+ import rich_argparse
5
+
6
+
7
+ class FalParserExit(Exception):
8
+ def __init__(self, status=0):
9
+ self.status = status
10
+
11
+
12
+ class RefAction(argparse.Action):
13
+ def __init__(self, *args, **kwargs):
14
+ kwargs.setdefault("default", (None, None))
15
+ super().__init__(*args, **kwargs)
16
+
17
+ def __call__(self, parser, args, values, option_string=None): # noqa: ARG002
18
+ if isinstance(values, tuple):
19
+ file_path, obj_path = values
20
+ elif values.find("::") > 1:
21
+ file_path, obj_path = values.split("::", 1)
22
+ else:
23
+ file_path, obj_path = values, None
24
+
25
+ setattr(args, self.dest, (file_path, obj_path))
26
+
27
+
28
+ class DictAction(argparse.Action):
29
+ def __init__(self, *args, **kwargs):
30
+ kwargs.setdefault("metavar", "<name>=<value>")
31
+ super().__init__(*args, **kwargs)
32
+
33
+ def __call__(self, parser, args, values, option_string=None): # noqa: ARG002
34
+ d = getattr(args, self.dest) or {}
35
+
36
+ if isinstance(values, list):
37
+ kvs = values
38
+ else:
39
+ kvs = [values]
40
+
41
+ for kv in kvs:
42
+ parts = kv.split("=", 1)
43
+ if len(parts) != 2:
44
+ raise argparse.ArgumentError(
45
+ self,
46
+ f'Could not parse argument "{values}" as k1=v1 k2=v2 ... format',
47
+ )
48
+ key, value = parts
49
+ d[key] = value
50
+
51
+ setattr(args, self.dest, d)
52
+
53
+
54
+ class FalParser(argparse.ArgumentParser):
55
+ def __init__(self, *args, **kwargs):
56
+ kwargs.setdefault("formatter_class", rich_argparse.RawTextRichHelpFormatter)
57
+ super().__init__(*args, **kwargs)
58
+
59
+ def exit(self, status=0, message=None):
60
+ if message:
61
+ self._print_message(message, sys.stderr)
62
+ raise FalParserExit(status)
63
+
64
+
65
+ class FalClientParser(FalParser):
66
+ def __init__(self, *args, **kwargs):
67
+ from fal.flags import GRPC_HOST
68
+
69
+ super().__init__(*args, **kwargs)
70
+ self.add_argument(
71
+ "--host",
72
+ default=GRPC_HOST,
73
+ help=argparse.SUPPRESS,
74
+ )
fal/cli/run.py ADDED
@@ -0,0 +1,33 @@
1
+ from .parser import FalClientParser, RefAction
2
+
3
+
4
+ def _run(args):
5
+ from fal.api import FalServerlessHost
6
+ from fal.utils import load_function_from
7
+
8
+ host = FalServerlessHost(args.host)
9
+ isolated_function, _ = load_function_from(host, *args.func_ref)
10
+ # let our exc handlers handle UserFunctionException
11
+ isolated_function.reraise = False
12
+ isolated_function()
13
+
14
+
15
+ def add_parser(main_subparsers, parents):
16
+ run_help = "Run fal function."
17
+ epilog = (
18
+ "Examples:\n"
19
+ " fal run path/to/myfile.py::myfunc"
20
+ )
21
+ parser = main_subparsers.add_parser(
22
+ "run",
23
+ description=run_help,
24
+ parents=[*parents, FalClientParser(add_help=False)],
25
+ help=run_help,
26
+ epilog=epilog,
27
+ )
28
+ parser.add_argument(
29
+ "func_ref",
30
+ action=RefAction,
31
+ help="Function reference.",
32
+ )
33
+ parser.set_defaults(func=_run)
fal/cli/secrets.py ADDED
@@ -0,0 +1,107 @@
1
+
2
+ from .parser import DictAction, FalClientParser
3
+
4
+
5
+ def _set(args):
6
+ from fal.sdk import FalServerlessClient
7
+ client = FalServerlessClient(args.host)
8
+ with client.connect() as connection:
9
+ for name, value in args.secrets.items():
10
+ connection.set_secret(name, value)
11
+
12
+
13
+ def _add_set_parser(subparsers, parents):
14
+ set_help = "Set a secret."
15
+ epilog = (
16
+ "Examples:\n"
17
+ " fal secrets set HF_TOKEN=hf_***"
18
+ )
19
+
20
+ parser = subparsers.add_parser(
21
+ "set",
22
+ description=set_help,
23
+ help=set_help,
24
+ parents=parents,
25
+ epilog=epilog,
26
+ )
27
+ parser.add_argument(
28
+ "secrets",
29
+ metavar="NAME=VALUE",
30
+ nargs="+",
31
+ action=DictAction,
32
+ help="Secret NAME=VALUE pairs.",
33
+ )
34
+ parser.set_defaults(func=_set)
35
+
36
+
37
+ def _list(args):
38
+ from rich.table import Table
39
+
40
+ from fal.sdk import FalServerlessClient
41
+
42
+ table = Table()
43
+ table.add_column("Secret Name")
44
+ table.add_column("Created At")
45
+
46
+ client = FalServerlessClient(args.host)
47
+ with client.connect() as connection:
48
+ for secret in connection.list_secrets():
49
+ table.add_row(secret.name, str(secret.created_at))
50
+
51
+ args.console.print(table)
52
+
53
+
54
+ def _add_list_parser(subparsers, parents):
55
+ list_help = "List secrets."
56
+ parser = subparsers.add_parser(
57
+ "list",
58
+ description=list_help,
59
+ help=list_help,
60
+ parents=parents,
61
+ )
62
+ parser.set_defaults(func=_list)
63
+
64
+
65
+ def _unset(args):
66
+ from fal.sdk import FalServerlessClient
67
+ client = FalServerlessClient(args.host)
68
+ with client.connect() as connection:
69
+ connection.delete_secret(args.secret)
70
+
71
+
72
+ def _add_unset_parser(subparsers, parents):
73
+ unset_help = "Unset a secret."
74
+ parser = subparsers.add_parser(
75
+ "unset",
76
+ description=unset_help,
77
+ help=unset_help,
78
+ parents=parents,
79
+ )
80
+ parser.add_argument(
81
+ "secret",
82
+ metavar="NAME",
83
+ help="Secret's name.",
84
+ )
85
+ parser.set_defaults(func=_unset)
86
+
87
+
88
+ def add_parser(main_subparsers, parents):
89
+ secrets_help = "Manage fal secrets."
90
+ parser = main_subparsers.add_parser(
91
+ "secrets",
92
+ aliases=["secret"],
93
+ parents=parents,
94
+ description=secrets_help,
95
+ help=secrets_help,
96
+ )
97
+
98
+ subparsers = parser.add_subparsers(
99
+ title="Commands",
100
+ metavar="command",
101
+ required=True,
102
+ parser_class=FalClientParser,
103
+ )
104
+
105
+ _add_set_parser(subparsers, parents)
106
+ _add_list_parser(subparsers, parents)
107
+ _add_unset_parser(subparsers, parents)
@@ -1,32 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from ._base import FalServerlessException # noqa: F401
4
- from .handlers import (
5
- BaseExceptionHandler,
6
- GrpcExceptionHandler,
7
- UserFunctionExceptionHandler,
8
- )
9
-
10
-
11
- class ApplicationExceptionHandler:
12
- """Handle exceptions top-level exceptions.
13
-
14
- This exception handler is capable of handling, i.e. customize the output
15
- and add behavior, of any type of exception. Click handles all `ClickException`
16
- types by default, but prints the stack for other exception not wrapped in
17
- ClickException.
18
-
19
- The handler also allows for central metrics and logging collection.
20
- """
21
-
22
- _handlers: list[BaseExceptionHandler] = [
23
- GrpcExceptionHandler(),
24
- UserFunctionExceptionHandler(),
25
- ]
26
-
27
- def handle(self, exception):
28
- match_handler: BaseExceptionHandler = next(
29
- (h for h in self._handlers if h.should_handle(exception)),
30
- BaseExceptionHandler(),
31
- )
32
- match_handler.handle(exception)
fal/flags.py CHANGED
@@ -27,5 +27,4 @@ FAL_RUN_HOST = (
27
27
  GRPC_HOST.replace("api.", "", 1).replace("alpha.", "", 1).replace(".ai", ".run", 1)
28
28
  )
29
29
 
30
- FORCE_SETUP = bool_envvar("FAL_FORCE_SETUP")
31
30
  DONT_OPEN_LINKS = bool_envvar("FAL_DONT_OPEN_LINKS")
fal/sdk.py CHANGED
@@ -28,6 +28,7 @@ _DEFAULT_SERIALIZATION_METHOD = "cloudpickle"
28
28
  FAL_SERVERLESS_DEFAULT_KEEP_ALIVE = 10
29
29
  FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING = 1
30
30
  FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY = 0
31
+ ALIAS_AUTH_MODES = ["public", "private", "shared"]
31
32
 
32
33
  logger = get_logger(__name__)
33
34
 
fal/utils.py ADDED
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ import fal._serialization
4
+ from fal import App, wrap_app
5
+
6
+ from .api import FalServerlessError, FalServerlessHost, IsolatedFunction
7
+
8
+
9
+ def load_function_from(
10
+ host: FalServerlessHost,
11
+ file_path: str,
12
+ function_name: str | None = None,
13
+ ) -> tuple[IsolatedFunction, str | None]:
14
+ import runpy
15
+
16
+ module = runpy.run_path(file_path)
17
+ if function_name is None:
18
+ fal_objects = {
19
+ obj.app_name: obj_name
20
+ for obj_name, obj in module.items()
21
+ if isinstance(obj, type)
22
+ and issubclass(obj, fal.App)
23
+ and hasattr(obj, "app_name")
24
+ }
25
+ if len(fal_objects) == 0:
26
+ raise FalServerlessError("No fal.App found in the module.")
27
+ elif len(fal_objects) > 1:
28
+ raise FalServerlessError(
29
+ "Multiple fal.Apps found in the module. "
30
+ "Please specify the name of the app."
31
+ )
32
+
33
+ [(app_name, function_name)] = fal_objects.items()
34
+ else:
35
+ app_name = None
36
+
37
+ module = runpy.run_path(file_path)
38
+ if function_name not in module:
39
+ raise FalServerlessError(f"Function '{function_name}' not found in module")
40
+
41
+ # The module for the function is set to <run_path> when runpy is used, in which
42
+ # case we want to manually include the package it is defined in.
43
+ fal._serialization.include_package_from_path(file_path)
44
+
45
+ target = module[function_name]
46
+ if isinstance(target, type) and issubclass(target, App):
47
+ app_name = target.app_name
48
+ target = wrap_app(target, host=host)
49
+
50
+ if not isinstance(target, IsolatedFunction):
51
+ raise FalServerlessError(
52
+ f"Function '{function_name}' is not a fal.function or a fal.App"
53
+ )
54
+ return target, app_name
55
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 0.15.2
3
+ Version: 1.0.0
4
4
  Summary: fal is an easy-to-use Serverless Python Framework
5
5
  Author: Features & Labels <hello@fal.ai>
6
6
  Requires-Python: >=3.8
@@ -19,7 +19,7 @@ Requires-Dist: grpc-interceptor <1,>=0.15.0
19
19
  Requires-Dist: colorama <1,>=0.4.6
20
20
  Requires-Dist: portalocker <3,>=2.7.0
21
21
  Requires-Dist: rich <14,>=13.3.2
22
- Requires-Dist: rich-click
22
+ Requires-Dist: rich-argparse
23
23
  Requires-Dist: packaging <22,>=21.3
24
24
  Requires-Dist: pathspec <1,>=0.11.1
25
25
  Requires-Dist: pydantic !=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3