fal 1.45.2__py3-none-any.whl → 1.46.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/_fal_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.45.2'
32
- __version_tuple__ = version_tuple = (1, 45, 2)
31
+ __version__ = version = '1.46.0'
32
+ __version_tuple__ = version_tuple = (1, 46, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
fal/api/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .api import * # noqa: F403
fal/api/apps.py ADDED
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import TYPE_CHECKING, List, Optional
5
+
6
+ from fal.sdk import AliasInfo, FalServerlessClient, RunnerInfo
7
+
8
+ if TYPE_CHECKING:
9
+ from .client import SyncServerlessClient
10
+
11
+
12
+ def list_apps(
13
+ client: SyncServerlessClient,
14
+ *,
15
+ filter: Optional[str] = None,
16
+ ) -> List[AliasInfo]:
17
+ with FalServerlessClient(client._grpc_host, client._credentials).connect() as conn:
18
+ apps = conn.list_aliases()
19
+
20
+ if filter:
21
+ apps = [a for a in apps if filter in a.alias]
22
+ return apps
23
+
24
+
25
+ def apps_runners(
26
+ client: SyncServerlessClient,
27
+ app_name: str,
28
+ *,
29
+ since: Optional[datetime] = None,
30
+ state: Optional[list[str]] = None,
31
+ ) -> List[RunnerInfo]:
32
+ with FalServerlessClient(client._grpc_host, client._credentials).connect() as conn:
33
+ alias_runners = conn.list_alias_runners(alias=app_name, start_time=since)
34
+
35
+ if state and "all" not in set(state):
36
+ states = set(state)
37
+ alias_runners = [r for r in alias_runners if r.state.value in states]
38
+ return alias_runners
39
+
40
+
41
+ def scale_app(
42
+ client: SyncServerlessClient,
43
+ app_name: str,
44
+ *,
45
+ keep_alive: int | None = None,
46
+ max_multiplexing: int | None = None,
47
+ max_concurrency: int | None = None,
48
+ min_concurrency: int | None = None,
49
+ concurrency_buffer: int | None = None,
50
+ concurrency_buffer_perc: int | None = None,
51
+ request_timeout: int | None = None,
52
+ startup_timeout: int | None = None,
53
+ machine_types: list[str] | None = None,
54
+ regions: list[str] | None = None,
55
+ ) -> AliasInfo:
56
+ with FalServerlessClient(client._grpc_host, client._credentials).connect() as conn:
57
+ return conn.update_application(
58
+ application_name=app_name,
59
+ keep_alive=keep_alive,
60
+ max_multiplexing=max_multiplexing,
61
+ max_concurrency=max_concurrency,
62
+ min_concurrency=min_concurrency,
63
+ concurrency_buffer=concurrency_buffer,
64
+ concurrency_buffer_perc=concurrency_buffer_perc,
65
+ request_timeout=request_timeout,
66
+ startup_timeout=startup_timeout,
67
+ machine_types=machine_types,
68
+ valid_regions=regions,
69
+ )
fal/api/client.py ADDED
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from typing import List, Optional
6
+
7
+ from fal.api import FAL_SERVERLESS_DEFAULT_URL
8
+ from fal.sdk import (
9
+ AliasInfo,
10
+ Credentials,
11
+ RunnerInfo,
12
+ )
13
+
14
+ from . import apps as apps_api
15
+ from . import deploy as deploy_api
16
+ from . import runners as runners_api
17
+
18
+
19
+ class _AppsNamespace:
20
+ def __init__(self, client: SyncServerlessClient):
21
+ self.client = client
22
+
23
+ def list(self, *, filter: str | None = None) -> List[AliasInfo]:
24
+ return apps_api.list_apps(self.client, filter=filter)
25
+
26
+ def runners(
27
+ self, app_name: str, *, since=None, state: List[str] | None = None
28
+ ) -> List[RunnerInfo]:
29
+ return apps_api.apps_runners(self.client, app_name, since=since, state=state)
30
+
31
+ def scale(
32
+ self,
33
+ app_name: str,
34
+ *,
35
+ keep_alive: int | None = None,
36
+ max_multiplexing: int | None = None,
37
+ max_concurrency: int | None = None,
38
+ min_concurrency: int | None = None,
39
+ concurrency_buffer: int | None = None,
40
+ concurrency_buffer_perc: int | None = None,
41
+ request_timeout: int | None = None,
42
+ startup_timeout: int | None = None,
43
+ machine_types: List[str] | None = None,
44
+ regions: List[str] | None = None,
45
+ ) -> apps_api.AliasInfo:
46
+ return apps_api.scale_app(
47
+ self.client,
48
+ app_name,
49
+ keep_alive=keep_alive,
50
+ max_multiplexing=max_multiplexing,
51
+ max_concurrency=max_concurrency,
52
+ min_concurrency=min_concurrency,
53
+ concurrency_buffer=concurrency_buffer,
54
+ concurrency_buffer_perc=concurrency_buffer_perc,
55
+ request_timeout=request_timeout,
56
+ startup_timeout=startup_timeout,
57
+ machine_types=machine_types,
58
+ regions=regions,
59
+ )
60
+
61
+
62
+ class _RunnersNamespace:
63
+ def __init__(self, client: SyncServerlessClient):
64
+ self.client = client
65
+
66
+ def list(self, *, since=None) -> List[RunnerInfo]:
67
+ return runners_api.list_runners(self.client, since=since)
68
+
69
+
70
+ @dataclass
71
+ class SyncServerlessClient:
72
+ host: Optional[str] = None
73
+ api_key: Optional[str] = None
74
+ profile: Optional[str] = None
75
+ team: Optional[str] = None
76
+
77
+ def __post_init__(self) -> None:
78
+ self.apps = _AppsNamespace(self)
79
+ self.runners = _RunnersNamespace(self)
80
+
81
+ # Top-level verbs
82
+ def deploy(self, *args, **kwargs):
83
+ return deploy_api.deploy(self, *args, **kwargs)
84
+
85
+ # Internal helpers
86
+ @property
87
+ def _grpc_host(self) -> str:
88
+ return self.host or FAL_SERVERLESS_DEFAULT_URL
89
+
90
+ @property
91
+ def _credentials(self) -> Credentials:
92
+ from fal.sdk import FalServerlessKeyCredentials, get_default_credentials
93
+
94
+ if self.api_key:
95
+ if self.team:
96
+ raise ValueError(
97
+ "Using explicit team with key credentials is not allowed"
98
+ )
99
+ try:
100
+ key_id, key_secret = self.api_key.split(":", 1)
101
+ except ValueError:
102
+ raise ValueError("api_key must be in 'KEY_ID:KEY_SECRET' format")
103
+ return FalServerlessKeyCredentials(key_id, key_secret)
104
+
105
+ if self.profile:
106
+ prev = os.environ.get("FAL_PROFILE")
107
+ os.environ["FAL_PROFILE"] = self.profile
108
+ try:
109
+ return get_default_credentials(team=self.team)
110
+ finally:
111
+ if prev is None:
112
+ os.environ.pop("FAL_PROFILE", None)
113
+ else:
114
+ os.environ["FAL_PROFILE"] = prev
115
+
116
+ return get_default_credentials(team=self.team)
fal/api/deploy.py ADDED
@@ -0,0 +1,211 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, Optional
6
+
7
+ from fal.sdk import AuthModeLiteral, DeploymentStrategyLiteral
8
+
9
+ if TYPE_CHECKING:
10
+ from .client import SyncServerlessClient
11
+
12
+
13
+ import json
14
+ from collections import namedtuple
15
+ from typing import Tuple, Union, cast
16
+
17
+ from fal.cli._utils import get_app_data_from_toml, is_app_name
18
+ from fal.cli.parser import RefAction
19
+
20
+ User = namedtuple("User", ["user_id", "username"])
21
+
22
+
23
+ @dataclass
24
+ class DeploymentResult:
25
+ revision: str
26
+ app_name: str
27
+ urls: dict[str, dict[str, str]]
28
+
29
+
30
+ def _remove_http_and_port_from_url(url):
31
+ # Remove http://
32
+ if "http://" in url:
33
+ url = url.replace("http://", "")
34
+
35
+ # Remove https://
36
+ if "https://" in url:
37
+ url = url.replace("https://", "")
38
+
39
+ # Remove port information
40
+ url_parts = url.split(":")
41
+ if len(url_parts) > 1:
42
+ url = url_parts[0]
43
+
44
+ return url
45
+
46
+
47
+ def _get_user() -> User:
48
+ from http import HTTPStatus
49
+
50
+ import openapi_fal_rest.api.users.get_current_user as get_current_user
51
+
52
+ from fal.api import FalServerlessError
53
+ from fal.rest_client import REST_CLIENT
54
+
55
+ try:
56
+ user_details_response = get_current_user.sync_detailed(
57
+ client=REST_CLIENT,
58
+ )
59
+ except Exception as e:
60
+ raise FalServerlessError(f"Error fetching user details: {str(e)}")
61
+
62
+ if user_details_response.status_code != HTTPStatus.OK:
63
+ try:
64
+ content = json.loads(user_details_response.content.decode("utf8"))
65
+ except Exception:
66
+ raise FalServerlessError(
67
+ f"Error fetching user details: {user_details_response}"
68
+ )
69
+ else:
70
+ raise FalServerlessError(content["detail"])
71
+ try:
72
+ full_user_id = user_details_response.parsed.user_id
73
+ _provider, _, user_id = full_user_id.partition("|")
74
+ if not user_id:
75
+ user_id = full_user_id
76
+
77
+ return User(user_id=user_id, username=user_details_response.parsed.nickname)
78
+ except Exception as e:
79
+ raise FalServerlessError(f"Could not parse the user data: {e}")
80
+
81
+
82
+ def _deploy_from_reference(
83
+ client: SyncServerlessClient,
84
+ app_ref: Tuple[Optional[Union[Path, str]], ...],
85
+ app_name: str,
86
+ auth: Optional[AuthModeLiteral],
87
+ strategy: Optional[DeploymentStrategyLiteral],
88
+ scale: bool,
89
+ ) -> DeploymentResult:
90
+ from fal.api import FalServerlessError, FalServerlessHost
91
+ from fal.utils import load_function_from
92
+
93
+ file_path, func_name = app_ref
94
+ if file_path is None:
95
+ # Try to find a python file in the current directory
96
+ options = list(Path(".").glob("*.py"))
97
+ if len(options) == 0:
98
+ raise FalServerlessError("No python files found in the current directory")
99
+ elif len(options) > 1:
100
+ raise FalServerlessError(
101
+ "Multiple python files found in the current directory. "
102
+ "Please specify the file path of the app you want to deploy."
103
+ )
104
+
105
+ [file_path] = options
106
+ file_path = str(file_path) # type: ignore
107
+
108
+ user = _get_user()
109
+ host = FalServerlessHost(client._grpc_host, local_file_path=str(file_path))
110
+ loaded = load_function_from(
111
+ host,
112
+ file_path, # type: ignore
113
+ func_name, # type: ignore
114
+ )
115
+ isolated_function = loaded.function
116
+ app_name = app_name or loaded.app_name # type: ignore
117
+ app_auth = auth or loaded.app_auth
118
+ strategy = strategy or "rolling"
119
+
120
+ app_id = host.register(
121
+ func=isolated_function.func,
122
+ options=isolated_function.options,
123
+ application_name=app_name,
124
+ application_auth_mode=app_auth, # type: ignore
125
+ metadata=isolated_function.options.host.get("metadata", {}),
126
+ deployment_strategy=strategy,
127
+ scale=scale,
128
+ )
129
+
130
+ assert app_id
131
+ env_host = _remove_http_and_port_from_url(host.url)
132
+ env_host = env_host.replace("api.", "").replace("alpha.", "")
133
+
134
+ env_host_parts = env_host.split(".")
135
+
136
+ # keep the last 3 parts
137
+ playground_host = ".".join(env_host_parts[-3:])
138
+
139
+ # just replace .ai for .run
140
+ endpoint_host = env_host.replace(".ai", ".run")
141
+
142
+ urls: dict[str, dict[str, str]] = {
143
+ "playground": {},
144
+ "sync": {},
145
+ "async": {},
146
+ }
147
+ for endpoint in loaded.endpoints:
148
+ urls["playground"][endpoint] = (
149
+ f"https://{playground_host}/models/{user.username}/{app_name}{endpoint}"
150
+ )
151
+ urls["sync"][endpoint] = (
152
+ f"https://{endpoint_host}/{user.username}/{app_name}{endpoint}"
153
+ )
154
+ urls["async"][endpoint] = (
155
+ f"https://queue.{endpoint_host}/{user.username}/{app_name}{endpoint}"
156
+ )
157
+
158
+ return DeploymentResult(
159
+ revision=app_id,
160
+ app_name=app_name,
161
+ urls=urls,
162
+ )
163
+
164
+
165
+ def deploy(
166
+ client: SyncServerlessClient,
167
+ app_ref: str | tuple[str, str] | None = None,
168
+ *,
169
+ app_name: str | None = None,
170
+ auth: AuthModeLiteral | None = None,
171
+ strategy: DeploymentStrategyLiteral = "rolling",
172
+ reset_scale: bool = False,
173
+ ) -> DeploymentResult:
174
+ if isinstance(app_ref, tuple):
175
+ app_ref_tuple = app_ref
176
+ elif app_ref:
177
+ app_ref_tuple = RefAction.split_ref(app_ref)
178
+ else:
179
+ raise ValueError("Invalid app reference")
180
+
181
+ # my-app
182
+ if is_app_name(app_ref_tuple):
183
+ # we do not allow --app-name and --auth to be used with app name
184
+ if app_name or auth:
185
+ raise ValueError("Cannot use --app-name or --auth with app name reference.")
186
+
187
+ app_name = app_ref_tuple[0]
188
+ app_ref, app_auth, app_strategy, app_scale_settings = get_app_data_from_toml(
189
+ app_name
190
+ )
191
+ file_path, func_name = RefAction.split_ref(app_ref)
192
+
193
+ # path/to/myfile.py::MyApp
194
+ else:
195
+ file_path, func_name = app_ref_tuple
196
+ app_name = cast(str, app_name)
197
+ # default to be set in the backend
198
+ app_auth = cast(Optional[AuthModeLiteral], auth)
199
+ # default comes from the CLI
200
+ app_strategy = cast(DeploymentStrategyLiteral, strategy)
201
+ app_scale_settings = cast(bool, reset_scale)
202
+ file_path = str(Path(file_path).absolute())
203
+
204
+ return _deploy_from_reference(
205
+ client,
206
+ (file_path, func_name),
207
+ app_name, # type: ignore
208
+ app_auth,
209
+ strategy=app_strategy,
210
+ scale=app_scale_settings,
211
+ )
fal/api/runners.py ADDED
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import TYPE_CHECKING, List, Optional
5
+
6
+ from fal.sdk import FalServerlessClient, RunnerInfo
7
+
8
+ if TYPE_CHECKING:
9
+ from .client import SyncServerlessClient
10
+
11
+
12
+ def list_runners(
13
+ client: SyncServerlessClient, *, since: Optional[datetime] = None
14
+ ) -> List[RunnerInfo]:
15
+ with FalServerlessClient(client._grpc_host, client._credentials).connect() as conn:
16
+ return conn.list_runners(start_time=since)
fal/cli/apps.py CHANGED
@@ -5,6 +5,7 @@ from dataclasses import asdict
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  import fal.cli.runners as runners
8
+ from fal.api.client import SyncServerlessClient
8
9
  from fal.sdk import RunnerState
9
10
 
10
11
  from ._utils import get_client
@@ -60,27 +61,23 @@ def _apps_table(apps: list[AliasInfo]):
60
61
 
61
62
 
62
63
  def _list(args):
63
- client = get_client(args.host, args.team)
64
- with client.connect() as connection:
65
- apps = connection.list_aliases()
64
+ client = SyncServerlessClient(host=args.host, team=args.team)
65
+ apps = client.apps.list(filter=args.filter)
66
66
 
67
- if args.filter:
68
- apps = [app for app in apps if args.filter in app.alias]
67
+ if args.sort_by_runners:
68
+ apps.sort(key=lambda x: x.active_runners)
69
+ else:
70
+ apps.sort(key=lambda x: x.alias)
69
71
 
70
- if args.sort_by_runners:
71
- apps.sort(key=lambda x: x.active_runners)
72
- else:
73
- apps.sort(key=lambda x: x.alias)
74
-
75
- if args.output == "pretty":
76
- table = _apps_table(apps)
77
- args.console.print(table)
78
- elif args.output == "json":
79
- apps_as_dicts = [asdict(a) for a in apps]
80
- res = json.dumps({"apps": apps_as_dicts})
81
- args.console.print(res)
82
- else:
83
- raise AssertionError(f"Invalid output format: {args.output}")
72
+ if args.output == "pretty":
73
+ table = _apps_table(apps)
74
+ args.console.print(table)
75
+ elif args.output == "json":
76
+ apps_as_dicts = [asdict(a) for a in apps]
77
+ json_res = json.dumps({"apps": apps_as_dicts})
78
+ args.console.print(json_res)
79
+ else:
80
+ raise AssertionError(f"Invalid output format: {args.output}")
84
81
 
85
82
 
86
83
  def _add_list_parser(subparsers, parents):
@@ -164,37 +161,36 @@ def _add_list_rev_parser(subparsers, parents):
164
161
 
165
162
 
166
163
  def _scale(args):
167
- client = get_client(args.host, args.team)
168
- with client.connect() as connection:
169
- if (
170
- args.keep_alive is None
171
- and args.max_multiplexing is None
172
- and args.max_concurrency is None
173
- and args.min_concurrency is None
174
- and args.concurrency_buffer is None
175
- and args.concurrency_buffer_perc is None
176
- and args.request_timeout is None
177
- and args.startup_timeout is None
178
- and args.machine_types is None
179
- and args.regions is None
180
- ):
181
- args.console.log("No parameters for update were provided, ignoring.")
182
- return
183
-
184
- alias_info = connection.update_application(
185
- application_name=args.app_name,
186
- keep_alive=args.keep_alive,
187
- max_multiplexing=args.max_multiplexing,
188
- max_concurrency=args.max_concurrency,
189
- min_concurrency=args.min_concurrency,
190
- concurrency_buffer=args.concurrency_buffer,
191
- concurrency_buffer_perc=args.concurrency_buffer_perc,
192
- request_timeout=args.request_timeout,
193
- startup_timeout=args.startup_timeout,
194
- machine_types=args.machine_types,
195
- valid_regions=args.regions,
196
- )
197
- table = _apps_table([alias_info])
164
+ client = SyncServerlessClient(host=args.host, team=args.team)
165
+ if (
166
+ args.keep_alive is None
167
+ and args.max_multiplexing is None
168
+ and args.max_concurrency is None
169
+ and args.min_concurrency is None
170
+ and args.concurrency_buffer is None
171
+ and args.concurrency_buffer_perc is None
172
+ and args.request_timeout is None
173
+ and args.startup_timeout is None
174
+ and args.machine_types is None
175
+ and args.regions is None
176
+ ):
177
+ args.console.log("No parameters for update were provided, ignoring.")
178
+ return
179
+
180
+ app_info = client.apps.scale(
181
+ args.app_name,
182
+ keep_alive=args.keep_alive,
183
+ max_multiplexing=args.max_multiplexing,
184
+ max_concurrency=args.max_concurrency,
185
+ min_concurrency=args.min_concurrency,
186
+ concurrency_buffer=args.concurrency_buffer,
187
+ concurrency_buffer_perc=args.concurrency_buffer_perc,
188
+ request_timeout=args.request_timeout,
189
+ startup_timeout=args.startup_timeout,
190
+ machine_types=args.machine_types,
191
+ regions=args.regions,
192
+ )
193
+ table = _apps_table([app_info])
198
194
 
199
195
  args.console.print(table)
200
196
 
@@ -303,16 +299,11 @@ def _add_set_rev_parser(subparsers, parents):
303
299
 
304
300
 
305
301
  def _runners(args):
306
- client = get_client(args.host, args.team)
307
- with client.connect() as connection:
308
- start_time = getattr(args, "since", None)
309
- alias_runners = connection.list_alias_runners(
310
- alias=args.app_name, start_time=start_time
311
- )
312
- if getattr(args, "state", None):
313
- states = set(args.state)
314
- if "all" not in states:
315
- alias_runners = [r for r in alias_runners if r.state.value in states]
302
+ client = SyncServerlessClient(host=args.host, team=args.team)
303
+ start_time = args.since
304
+ alias_runners = client.apps.runners(
305
+ args.app_name, since=start_time, state=args.state
306
+ )
316
307
  if args.output == "pretty":
317
308
  runners_table = runners.runners_table(alias_runners)
318
309
  pending_runners = [