fal 0.15.0__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fal might be problematic. Click here for more details.

fal/cli/apps.py ADDED
@@ -0,0 +1,313 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from .parser import FalClientParser
4
+
5
+ if TYPE_CHECKING:
6
+ from fal.sdk import AliasInfo, ApplicationInfo
7
+
8
+
9
+ def _apps_table(apps: list["AliasInfo"]):
10
+ from rich.table import Table
11
+
12
+ table = Table()
13
+ table.add_column("Name", no_wrap=True)
14
+ table.add_column("Revision")
15
+ table.add_column("Auth")
16
+ table.add_column("Min Concurrency")
17
+ table.add_column("Max Concurrency")
18
+ table.add_column("Max Multiplexing")
19
+ table.add_column("Keep Alive")
20
+ table.add_column("Active Workers")
21
+
22
+ for app in apps:
23
+ table.add_row(
24
+ app.alias,
25
+ app.revision,
26
+ app.auth_mode,
27
+ str(app.min_concurrency),
28
+ str(app.max_concurrency),
29
+ str(app.max_multiplexing),
30
+ str(app.keep_alive),
31
+ str(app.active_runners),
32
+ )
33
+
34
+ return table
35
+
36
+
37
+ def _list(args):
38
+ from fal.sdk import FalServerlessClient
39
+
40
+ client = FalServerlessClient(args.host)
41
+ with client.connect() as connection:
42
+ apps = connection.list_aliases()
43
+ table = _apps_table(apps)
44
+
45
+ args.console.print(table)
46
+
47
+
48
+ def _add_list_parser(subparsers, parents):
49
+ list_help = "List applications."
50
+ parser = subparsers.add_parser(
51
+ "list",
52
+ description=list_help,
53
+ help=list_help,
54
+ parents=parents,
55
+ )
56
+ parser.set_defaults(func=_list)
57
+
58
+
59
+ def _app_rev_table(revs: list["ApplicationInfo"]):
60
+ from rich.table import Table
61
+
62
+ table = Table()
63
+ table.add_column("Revision", no_wrap=True)
64
+ table.add_column("Min Concurrency")
65
+ table.add_column("Max Concurrency")
66
+ table.add_column("Max Multiplexing")
67
+ table.add_column("Keep Alive")
68
+ table.add_column("Active Workers")
69
+
70
+ for rev in revs:
71
+ table.add_row(
72
+ rev.application_id,
73
+ str(rev.min_concurrency),
74
+ str(rev.max_concurrency),
75
+ str(rev.max_multiplexing),
76
+ str(rev.keep_alive),
77
+ str(rev.active_runners),
78
+ )
79
+
80
+ return table
81
+
82
+
83
+ def _list_rev(args):
84
+ from fal.sdk import FalServerlessClient
85
+
86
+ client = FalServerlessClient(args.host)
87
+ with client.connect() as connection:
88
+ revs = connection.list_applications()
89
+ table = _app_rev_table(revs)
90
+
91
+ args.console.print(table)
92
+
93
+
94
+ def _add_list_rev_parser(subparsers, parents):
95
+ list_help = "List application revisions."
96
+ parser = subparsers.add_parser(
97
+ "list-rev",
98
+ description=list_help,
99
+ help=list_help,
100
+ parents=parents,
101
+ )
102
+ parser.set_defaults(func=_list_rev)
103
+
104
+
105
+ def _scale(args):
106
+ from fal.sdk import FalServerlessClient
107
+
108
+ client = FalServerlessClient(args.host)
109
+ with client.connect() as connection:
110
+ if (
111
+ args.keep_alive is None
112
+ and args.max_multiplexing is None
113
+ and args.max_concurrency is None
114
+ and args.min_concurrency is None
115
+ ):
116
+ args.console.log("No parameters for update were provided, ignoring.")
117
+ return
118
+
119
+ alias_info = connection.update_application(
120
+ application_name=args.app_alias,
121
+ keep_alive=args.keep_alive,
122
+ max_multiplexing=args.max_multiplexing,
123
+ max_concurrency=args.max_concurrency,
124
+ min_concurrency=args.min_concurrency,
125
+ )
126
+ table = _apps_table([alias_info])
127
+
128
+ args.console.print(table)
129
+
130
+
131
+ def _add_scale_parser(subparsers, parents):
132
+ scale_help = "Scale application."
133
+ parser = subparsers.add_parser(
134
+ "scale",
135
+ description=scale_help,
136
+ help=scale_help,
137
+ parents=parents,
138
+ )
139
+ parser.add_argument(
140
+ "--keep-alive",
141
+ type=int,
142
+ help="Keep alive (seconds).",
143
+ )
144
+ parser.add_argument(
145
+ "--max-multiplexing",
146
+ type=int,
147
+ help="Maximum multiplexing",
148
+ )
149
+ parser.add_argument(
150
+ "--max-concurrency",
151
+ type=int,
152
+ help="Maximum concurrency.",
153
+ )
154
+ parser.add_argument(
155
+ "--min-concurrency",
156
+ type=int,
157
+ help="Minimum concurrency",
158
+ )
159
+ parser.set_defaults(func=_scale)
160
+
161
+
162
+ def _set_rev(args):
163
+ from fal.sdk import FalServerlessClient
164
+
165
+ client = FalServerlessClient(args.host)
166
+ with client.connect() as connection:
167
+ connection.create_alias(args.app_name, args.app_rev, args.auth)
168
+
169
+
170
+ def _add_set_rev_parser(subparsers, parents):
171
+ from fal.sdk import ALIAS_AUTH_MODES
172
+
173
+ set_help = "Set application to a particular revision."
174
+ parser = subparsers.add_parser(
175
+ "set-rev",
176
+ description=set_help,
177
+ help=set_help,
178
+ parents=parents,
179
+ )
180
+ parser.add_argument(
181
+ "app_name",
182
+ help="Application name.",
183
+ )
184
+ parser.add_argument(
185
+ "app_rev",
186
+ help="Application revision.",
187
+ )
188
+ parser.add_argument(
189
+ "--auth",
190
+ choices=ALIAS_AUTH_MODES,
191
+ default="private",
192
+ help="Application authentication mode.",
193
+ )
194
+ parser.set_defaults(func=_set_rev)
195
+
196
+
197
+ def _runners(args):
198
+ from rich.table import Table
199
+
200
+ from fal.sdk import FalServerlessClient
201
+
202
+
203
+ client = FalServerlessClient(args.host)
204
+ with client.connect() as connection:
205
+ runners = connection.list_alias_runners(alias=args.app_name)
206
+
207
+ table = Table()
208
+ table.add_column("Runner ID")
209
+ table.add_column("In Flight Requests")
210
+ table.add_column("Expires in")
211
+ table.add_column("Uptime")
212
+
213
+ for runner in runners:
214
+ table.add_row(
215
+ runner.runner_id,
216
+ str(runner.in_flight_requests),
217
+ (
218
+ "N/A (active)"
219
+ if not runner.expiration_countdown
220
+ else f"{runner.expiration_countdown}s"
221
+ ),
222
+ f"{runner.uptime} ({runner.uptime.total_seconds()}s)",
223
+ )
224
+
225
+ args.console.print(table)
226
+
227
+
228
+ def _add_runners_parser(subparsers, parents):
229
+ runners_help = "List application runners."
230
+ parser = subparsers.add_parser(
231
+ "runners",
232
+ description=runners_help,
233
+ help=runners_help,
234
+ parents=parents,
235
+ )
236
+ parser.add_argument(
237
+ "app_name",
238
+ help="Application name.",
239
+ )
240
+ parser.set_defaults(func=_runners)
241
+
242
+
243
+ def _delete(args):
244
+ from fal.sdk import FalServerlessClient
245
+
246
+ client = FalServerlessClient(args.host)
247
+ with client.connect() as connection:
248
+ connection.delete_alias(args.app_name)
249
+
250
+
251
+ def _add_delete_parser(subparsers, parents):
252
+ delete_help = "Delete application."
253
+ parser = subparsers.add_parser(
254
+ "delete",
255
+ description=delete_help,
256
+ help=delete_help,
257
+ parents=parents,
258
+ )
259
+ parser.add_argument(
260
+ "app_name",
261
+ help="Application name.",
262
+ )
263
+ parser.set_defaults(func=_delete)
264
+
265
+
266
+ def _delete_rev(args):
267
+ from fal.sdk import FalServerlessClient
268
+
269
+ client = FalServerlessClient(args.host)
270
+ with client.connect() as connection:
271
+ connection.delete_application(args.app_rev)
272
+
273
+
274
+ def _add_delete_rev_parser(subparsers, parents):
275
+ delete_help = "Delete application revision."
276
+ parser = subparsers.add_parser(
277
+ "delete-rev",
278
+ description=delete_help,
279
+ help=delete_help,
280
+ parents=parents,
281
+ )
282
+ parser.add_argument(
283
+ "app_rev",
284
+ help="Application revision.",
285
+ )
286
+ parser.set_defaults(func=_delete_rev)
287
+
288
+
289
+ def add_parser(main_subparsers, parents):
290
+ apps_help = "Manage fal applications."
291
+ parser = main_subparsers.add_parser(
292
+ "apps",
293
+ aliases=["app"],
294
+ description=apps_help,
295
+ help=apps_help,
296
+ parents=parents,
297
+ )
298
+
299
+ subparsers = parser.add_subparsers(
300
+ title="Commands",
301
+ metavar="command",
302
+ required=True,
303
+ )
304
+
305
+ parents = [*parents, FalClientParser(add_help=False)]
306
+
307
+ _add_list_parser(subparsers, parents)
308
+ _add_list_rev_parser(subparsers, parents)
309
+ _add_set_rev_parser(subparsers, parents)
310
+ _add_scale_parser(subparsers, parents)
311
+ _add_runners_parser(subparsers, parents)
312
+ _add_delete_parser(subparsers, parents)
313
+ _add_delete_rev_parser(subparsers, parents)
fal/cli/auth.py ADDED
@@ -0,0 +1,59 @@
1
+ from fal.auth import USER, login, logout
2
+
3
+
4
+ def _login(args):
5
+ login()
6
+
7
+
8
+ def _logout(args):
9
+ logout()
10
+
11
+
12
+ def _whoami(args):
13
+ user_name = USER.info["name"]
14
+ sub = USER.info["sub"]
15
+ args.console.print(f"Hello, {user_name} - '{sub}'")
16
+
17
+
18
+ def add_parser(main_subparsers, parents):
19
+ auth_help = "Authenticate with fal."
20
+ parser = main_subparsers.add_parser(
21
+ "auth",
22
+ description=auth_help,
23
+ help=auth_help,
24
+ parents=parents,
25
+ )
26
+
27
+ subparsers = parser.add_subparsers(
28
+ title="Commands",
29
+ metavar="command",
30
+ dest="cmd",
31
+ required=True,
32
+ )
33
+
34
+ login_help = "Log in a user."
35
+ login_parser = subparsers.add_parser(
36
+ "login",
37
+ description=login_help,
38
+ help=login_help,
39
+ parents=parents,
40
+ )
41
+ login_parser.set_defaults(func=_login)
42
+
43
+ logout_help = "Log out the currently logged-in user."
44
+ logout_parser = subparsers.add_parser(
45
+ "logout",
46
+ description=logout_help,
47
+ help=logout_help,
48
+ parents=parents,
49
+ )
50
+ logout_parser.set_defaults(func=_logout)
51
+
52
+ whoami_help = "Show the currently authenticated user."
53
+ whoami_parser = subparsers.add_parser(
54
+ "whoami",
55
+ description=whoami_help,
56
+ help=whoami_help,
57
+ parents=parents,
58
+ )
59
+ whoami_parser.set_defaults(func=_whoami)
fal/cli/debug.py ADDED
@@ -0,0 +1,65 @@
1
+ from contextlib import ExitStack, contextmanager
2
+
3
+ from .parser import FalParser
4
+
5
+
6
+ @contextmanager
7
+ def _pdb():
8
+ try:
9
+ yield
10
+ except Exception:
11
+ try:
12
+ import ipdb as pdb # noqa: T100
13
+ except ImportError:
14
+ import pdb # noqa: T100
15
+ pdb.post_mortem()
16
+
17
+
18
+ @contextmanager
19
+ def _cprofile():
20
+ import cProfile
21
+
22
+ prof = cProfile.Profile()
23
+ prof.enable()
24
+
25
+ try:
26
+ yield
27
+ finally:
28
+ prof.disable()
29
+ prof.print_stats(sort="cumtime")
30
+
31
+
32
+ @contextmanager
33
+ def debugtools(args):
34
+ with ExitStack() as stack:
35
+ if args.pdb:
36
+ stack.enter_context(_pdb())
37
+ if args.cprofile:
38
+ stack.enter_context(_cprofile())
39
+ try:
40
+ yield
41
+ except Exception:
42
+ if args.debug:
43
+ args.console.print_exception()
44
+ raise
45
+
46
+
47
+ def get_debug_parser():
48
+ parser = FalParser(add_help=False)
49
+ group = parser.add_argument_group(title="Debug")
50
+ group.add_argument(
51
+ "--debug",
52
+ action="store_true",
53
+ help="Show verbose errors."
54
+ )
55
+ group.add_argument(
56
+ "--pdb",
57
+ action="store_true",
58
+ help="Start pdb on error.",
59
+ )
60
+ group.add_argument(
61
+ "--cprofile",
62
+ action="store_true",
63
+ help="Show cProfile report.",
64
+ )
65
+ return parser
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)