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/__init__.py +4 -0
- fal/__main__.py +2 -2
- fal/_fal_version.py +16 -0
- fal/_version.py +6 -0
- fal/api.py +10 -1
- fal/app.py +29 -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 -29
- fal/flags.py +0 -1
- fal/sdk.py +1 -0
- fal/utils.py +55 -0
- {fal-0.15.2.dist-info → fal-1.0.0.dist-info}/METADATA +2 -2
- {fal-0.15.2.dist-info → fal-1.0.0.dist-info}/RECORD +25 -14
- fal-1.0.0.dist-info/entry_points.txt +2 -0
- fal/cli.py +0 -622
- fal/exceptions/handlers.py +0 -58
- fal-0.15.2.dist-info/entry_points.txt +0 -2
- {fal-0.15.2.dist-info → fal-1.0.0.dist-info}/WHEEL +0 -0
- {fal-0.15.2.dist-info → fal-1.0.0.dist-info}/top_level.txt +0 -0
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)
|
fal/exceptions/__init__.py
CHANGED
|
@@ -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
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.
|
|
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-
|
|
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
|