fal 1.45.2__py3-none-any.whl → 1.46.1__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 +2 -2
- fal/api/__init__.py +1 -0
- fal/api/apps.py +69 -0
- fal/api/client.py +116 -0
- fal/api/deploy.py +211 -0
- fal/api/runners.py +16 -0
- fal/cli/apps.py +51 -60
- fal/cli/deploy.py +29 -181
- fal/cli/queue.py +2 -2
- fal/cli/runners.py +45 -47
- fal/distributed/__init__.py +3 -0
- fal/distributed/utils.py +420 -0
- fal/distributed/worker.py +791 -0
- {fal-1.45.2.dist-info → fal-1.46.1.dist-info}/METADATA +2 -1
- {fal-1.45.2.dist-info → fal-1.46.1.dist-info}/RECORD +19 -11
- /fal/{api.py → api/api.py} +0 -0
- {fal-1.45.2.dist-info → fal-1.46.1.dist-info}/WHEEL +0 -0
- {fal-1.45.2.dist-info → fal-1.46.1.dist-info}/entry_points.txt +0 -0
- {fal-1.45.2.dist-info → fal-1.46.1.dist-info}/top_level.txt +0 -0
fal/cli/deploy.py
CHANGED
|
@@ -1,195 +1,43 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import json
|
|
3
|
-
from collections import namedtuple
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Optional, Tuple, Union, cast
|
|
6
3
|
|
|
7
|
-
from fal.
|
|
4
|
+
from fal.api.client import SyncServerlessClient
|
|
8
5
|
|
|
9
|
-
from ._utils import get_app_data_from_toml, is_app_name
|
|
10
6
|
from .parser import FalClientParser, RefAction, get_output_parser
|
|
11
7
|
|
|
12
|
-
User = namedtuple("User", ["user_id", "username"])
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _remove_http_and_port_from_url(url):
|
|
16
|
-
# Remove http://
|
|
17
|
-
if "http://" in url:
|
|
18
|
-
url = url.replace("http://", "")
|
|
19
|
-
|
|
20
|
-
# Remove https://
|
|
21
|
-
if "https://" in url:
|
|
22
|
-
url = url.replace("https://", "")
|
|
23
|
-
|
|
24
|
-
# Remove port information
|
|
25
|
-
url_parts = url.split(":")
|
|
26
|
-
if len(url_parts) > 1:
|
|
27
|
-
url = url_parts[0]
|
|
28
|
-
|
|
29
|
-
return url
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _get_user() -> User:
|
|
33
|
-
import json
|
|
34
|
-
from http import HTTPStatus
|
|
35
|
-
|
|
36
|
-
import openapi_fal_rest.api.users.get_current_user as get_current_user
|
|
37
|
-
|
|
38
|
-
from fal.api import FalServerlessError
|
|
39
|
-
from fal.rest_client import REST_CLIENT
|
|
40
|
-
|
|
41
|
-
try:
|
|
42
|
-
user_details_response = get_current_user.sync_detailed(
|
|
43
|
-
client=REST_CLIENT,
|
|
44
|
-
)
|
|
45
|
-
except Exception as e:
|
|
46
|
-
raise FalServerlessError(f"Error fetching user details: {str(e)}")
|
|
47
|
-
|
|
48
|
-
if user_details_response.status_code != HTTPStatus.OK:
|
|
49
|
-
try:
|
|
50
|
-
content = json.loads(user_details_response.content.decode("utf8"))
|
|
51
|
-
except Exception:
|
|
52
|
-
raise FalServerlessError(
|
|
53
|
-
f"Error fetching user details: {user_details_response}"
|
|
54
|
-
)
|
|
55
|
-
else:
|
|
56
|
-
raise FalServerlessError(content["detail"])
|
|
57
|
-
try:
|
|
58
|
-
full_user_id = user_details_response.parsed.user_id
|
|
59
|
-
_provider, _, user_id = full_user_id.partition("|")
|
|
60
|
-
if not user_id:
|
|
61
|
-
user_id = full_user_id
|
|
62
|
-
|
|
63
|
-
return User(user_id=user_id, username=user_details_response.parsed.nickname)
|
|
64
|
-
except Exception as e:
|
|
65
|
-
raise FalServerlessError(f"Could not parse the user data: {e}")
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _deploy_from_reference(
|
|
69
|
-
app_ref: Tuple[Optional[Union[Path, str]], ...],
|
|
70
|
-
app_name: str,
|
|
71
|
-
args,
|
|
72
|
-
auth: Optional[AuthModeLiteral],
|
|
73
|
-
deployment_strategy: Optional[DeploymentStrategyLiteral],
|
|
74
|
-
scale: bool,
|
|
75
|
-
):
|
|
76
|
-
from fal.api import FalServerlessError, FalServerlessHost
|
|
77
|
-
from fal.utils import load_function_from
|
|
78
|
-
|
|
79
|
-
file_path, func_name = app_ref
|
|
80
|
-
if file_path is None:
|
|
81
|
-
# Try to find a python file in the current directory
|
|
82
|
-
options = list(Path(".").glob("*.py"))
|
|
83
|
-
if len(options) == 0:
|
|
84
|
-
raise FalServerlessError("No python files found in the current directory")
|
|
85
|
-
elif len(options) > 1:
|
|
86
|
-
raise FalServerlessError(
|
|
87
|
-
"Multiple python files found in the current directory. "
|
|
88
|
-
"Please specify the file path of the app you want to deploy."
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
[file_path] = options
|
|
92
|
-
file_path = str(file_path) # type: ignore
|
|
93
|
-
|
|
94
|
-
user = _get_user()
|
|
95
|
-
host = FalServerlessHost(args.host, local_file_path=str(file_path))
|
|
96
|
-
loaded = load_function_from(
|
|
97
|
-
host,
|
|
98
|
-
file_path, # type: ignore
|
|
99
|
-
func_name, # type: ignore
|
|
100
|
-
)
|
|
101
|
-
isolated_function = loaded.function
|
|
102
|
-
app_name = app_name or loaded.app_name # type: ignore
|
|
103
|
-
app_auth = auth or loaded.app_auth
|
|
104
|
-
deployment_strategy = deployment_strategy or "rolling"
|
|
105
|
-
|
|
106
|
-
app_id = host.register(
|
|
107
|
-
func=isolated_function.func,
|
|
108
|
-
options=isolated_function.options,
|
|
109
|
-
application_name=app_name,
|
|
110
|
-
application_auth_mode=app_auth, # type: ignore
|
|
111
|
-
metadata=isolated_function.options.host.get("metadata", {}),
|
|
112
|
-
deployment_strategy=deployment_strategy,
|
|
113
|
-
scale=scale,
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
if app_id:
|
|
117
|
-
env_host = _remove_http_and_port_from_url(host.url)
|
|
118
|
-
env_host = env_host.replace("api.", "").replace("alpha.", "")
|
|
119
|
-
|
|
120
|
-
env_host_parts = env_host.split(".")
|
|
121
|
-
|
|
122
|
-
# keep the last 3 parts
|
|
123
|
-
playground_host = ".".join(env_host_parts[-3:])
|
|
124
|
-
|
|
125
|
-
# just replace .ai for .run
|
|
126
|
-
endpoint_host = env_host.replace(".ai", ".run")
|
|
127
|
-
|
|
128
|
-
if args.output == "json":
|
|
129
|
-
args.console.print(
|
|
130
|
-
json.dumps(
|
|
131
|
-
{
|
|
132
|
-
"revision": app_id,
|
|
133
|
-
"app_name": app_name,
|
|
134
|
-
}
|
|
135
|
-
)
|
|
136
|
-
)
|
|
137
|
-
elif args.output == "pretty":
|
|
138
|
-
args.console.print(
|
|
139
|
-
"Registered a new revision for function "
|
|
140
|
-
f"'{app_name}' (revision='{app_id}')."
|
|
141
|
-
)
|
|
142
|
-
args.console.print("Playground:")
|
|
143
|
-
for endpoint in loaded.endpoints:
|
|
144
|
-
args.console.print(
|
|
145
|
-
f"\thttps://{playground_host}/models/{user.username}/{app_name}{endpoint}"
|
|
146
|
-
)
|
|
147
|
-
args.console.print("Synchronous Endpoints:")
|
|
148
|
-
for endpoint in loaded.endpoints:
|
|
149
|
-
args.console.print(
|
|
150
|
-
f"\thttps://{endpoint_host}/{user.username}/{app_name}{endpoint}"
|
|
151
|
-
)
|
|
152
|
-
args.console.print("Asynchronous Endpoints (Recommended):")
|
|
153
|
-
for endpoint in loaded.endpoints:
|
|
154
|
-
args.console.print(
|
|
155
|
-
f"\thttps://queue.{endpoint_host}/{user.username}/{app_name}{endpoint}"
|
|
156
|
-
)
|
|
157
|
-
else:
|
|
158
|
-
raise AssertionError(f"Invalid output format: {args.output}")
|
|
159
|
-
|
|
160
8
|
|
|
161
9
|
def _deploy(args):
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
10
|
+
client = SyncServerlessClient(host=args.host, team=args.team)
|
|
11
|
+
res = client.deploy(
|
|
12
|
+
args.app_ref,
|
|
13
|
+
app_name=args.app_name,
|
|
14
|
+
auth=args.auth,
|
|
15
|
+
strategy=args.strategy,
|
|
16
|
+
reset_scale=args.app_scale_settings,
|
|
17
|
+
)
|
|
18
|
+
app_id = res.revision
|
|
19
|
+
resolved_app_name = res.app_name
|
|
167
20
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
21
|
+
if args.output == "json":
|
|
22
|
+
args.console.print(
|
|
23
|
+
json.dumps({"revision": app_id, "app_name": resolved_app_name})
|
|
171
24
|
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
25
|
+
elif args.output == "pretty":
|
|
26
|
+
args.console.print(
|
|
27
|
+
"Registered a new revision for function "
|
|
28
|
+
f"'{resolved_app_name}' (revision='{app_id}')."
|
|
29
|
+
)
|
|
30
|
+
args.console.print("Playground:")
|
|
31
|
+
for url in res.urls.get("playground", {}).values():
|
|
32
|
+
args.console.print(f"\t{url}")
|
|
33
|
+
args.console.print("Synchronous Endpoints:")
|
|
34
|
+
for url in res.urls.get("sync", {}).values():
|
|
35
|
+
args.console.print(f"\t{url}")
|
|
36
|
+
args.console.print("Asynchronous Endpoints (Recommended):")
|
|
37
|
+
for url in res.urls.get("async", {}).values():
|
|
38
|
+
args.console.print(f"\t{url}")
|
|
175
39
|
else:
|
|
176
|
-
|
|
177
|
-
app_name = cast(str, args.app_name)
|
|
178
|
-
# default to be set in the backend
|
|
179
|
-
app_auth = cast(Optional[AuthModeLiteral], args.auth)
|
|
180
|
-
# default comes from the CLI
|
|
181
|
-
app_deployment_strategy = cast(DeploymentStrategyLiteral, args.strategy)
|
|
182
|
-
app_scale_settings = cast(bool, args.app_scale_settings)
|
|
183
|
-
file_path = str(Path(file_path).absolute())
|
|
184
|
-
|
|
185
|
-
_deploy_from_reference(
|
|
186
|
-
(file_path, func_name),
|
|
187
|
-
app_name,
|
|
188
|
-
args,
|
|
189
|
-
app_auth,
|
|
190
|
-
app_deployment_strategy,
|
|
191
|
-
scale=app_scale_settings,
|
|
192
|
-
)
|
|
40
|
+
raise AssertionError(f"Invalid output format: {args.output}")
|
|
193
41
|
|
|
194
42
|
|
|
195
43
|
def add_parser(main_subparsers, parents):
|
fal/cli/queue.py
CHANGED
|
@@ -11,7 +11,7 @@ from .parser import FalClientParser, get_output_parser
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def _queue_size(args):
|
|
14
|
-
from .deploy import _get_user
|
|
14
|
+
from fal.api.deploy import _get_user
|
|
15
15
|
|
|
16
16
|
user = _get_user()
|
|
17
17
|
|
|
@@ -38,7 +38,7 @@ def _queue_size(args):
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def _queue_flush(args):
|
|
41
|
-
from .deploy import _get_user
|
|
41
|
+
from fal.api.deploy import _get_user
|
|
42
42
|
|
|
43
43
|
user = _get_user()
|
|
44
44
|
|
fal/cli/runners.py
CHANGED
|
@@ -12,10 +12,10 @@ from httpx_sse import connect_sse
|
|
|
12
12
|
from rich.console import Console
|
|
13
13
|
from structlog.typing import EventDict
|
|
14
14
|
|
|
15
|
+
from fal.api.client import SyncServerlessClient
|
|
15
16
|
from fal.rest_client import REST_CLIENT
|
|
16
|
-
from fal.sdk import RunnerInfo, RunnerState
|
|
17
|
+
from fal.sdk import FalServerlessClient, RunnerInfo, RunnerState
|
|
17
18
|
|
|
18
|
-
from ._utils import get_client
|
|
19
19
|
from .parser import FalClientParser, SinceAction, get_output_parser
|
|
20
20
|
|
|
21
21
|
|
|
@@ -96,8 +96,10 @@ def runners_requests_table(runners: list[RunnerInfo]):
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
def _kill(args):
|
|
99
|
-
client =
|
|
100
|
-
with
|
|
99
|
+
client = SyncServerlessClient(host=args.host, team=args.team)
|
|
100
|
+
with FalServerlessClient(
|
|
101
|
+
client._grpc_host, client._credentials
|
|
102
|
+
).connect() as connection:
|
|
101
103
|
connection.kill_runner(args.id)
|
|
102
104
|
|
|
103
105
|
|
|
@@ -122,45 +124,41 @@ def _list_json(args, runners: list[RunnerInfo]):
|
|
|
122
124
|
|
|
123
125
|
|
|
124
126
|
def _list(args):
|
|
125
|
-
client =
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
len(runners)
|
|
148
|
-
- len(pending_runners)
|
|
149
|
-
- len(setup_runners)
|
|
150
|
-
- len(dead_runners)
|
|
151
|
-
)
|
|
127
|
+
client = SyncServerlessClient(host=args.host, team=args.team)
|
|
128
|
+
start_time = args.since
|
|
129
|
+
runners = client.runners.list(since=start_time)
|
|
130
|
+
|
|
131
|
+
if args.state:
|
|
132
|
+
states = set(args.state)
|
|
133
|
+
if "all" not in states:
|
|
134
|
+
runners = [r for r in runners if r.state.value in states]
|
|
135
|
+
|
|
136
|
+
pending_runners = [
|
|
137
|
+
runner for runner in runners if runner.state == RunnerState.PENDING
|
|
138
|
+
]
|
|
139
|
+
setup_runners = [runner for runner in runners if runner.state == RunnerState.SETUP]
|
|
140
|
+
dead_runners = [runner for runner in runners if runner.state == RunnerState.DEAD]
|
|
141
|
+
if args.output == "pretty":
|
|
142
|
+
args.console.print(
|
|
143
|
+
"Runners: "
|
|
144
|
+
+ str(
|
|
145
|
+
len(runners)
|
|
146
|
+
- len(pending_runners)
|
|
147
|
+
- len(setup_runners)
|
|
148
|
+
- len(dead_runners)
|
|
152
149
|
)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
150
|
+
)
|
|
151
|
+
args.console.print(f"Runners Pending: {len(pending_runners)}")
|
|
152
|
+
args.console.print(f"Runners Setting Up: {len(setup_runners)}")
|
|
153
|
+
args.console.print(runners_table(runners))
|
|
154
|
+
|
|
155
|
+
requests_table = runners_requests_table(runners)
|
|
156
|
+
args.console.print(f"Requests: {len(requests_table.rows)}")
|
|
157
|
+
args.console.print(requests_table)
|
|
158
|
+
elif args.output == "json":
|
|
159
|
+
_list_json(args, runners)
|
|
160
|
+
else:
|
|
161
|
+
raise AssertionError(f"Invalid output format: {args.output}")
|
|
164
162
|
|
|
165
163
|
|
|
166
164
|
def _add_kill_parser(subparsers, parents):
|
|
@@ -420,17 +418,17 @@ DEFAULT_STREAM_SINCE = timedelta(minutes=1)
|
|
|
420
418
|
|
|
421
419
|
def _logs(args):
|
|
422
420
|
params: dict[str, str] = {"job_id": args.id}
|
|
423
|
-
if
|
|
421
|
+
if args.search is not None:
|
|
424
422
|
params["search"] = args.search
|
|
425
423
|
|
|
426
424
|
runner_info = _get_runner_info(args.id)
|
|
427
|
-
follow: bool =
|
|
428
|
-
since =
|
|
425
|
+
follow: bool = args.follow
|
|
426
|
+
since = args.since
|
|
429
427
|
if follow:
|
|
430
428
|
since = since or (datetime.now(timezone.utc) - DEFAULT_STREAM_SINCE)
|
|
431
429
|
else:
|
|
432
430
|
since = since or runner_info.started_at
|
|
433
|
-
until =
|
|
431
|
+
until = args.until or runner_info.ended_at
|
|
434
432
|
|
|
435
433
|
# Normalize to aware UTC for comparisons
|
|
436
434
|
if since is not None:
|
|
@@ -454,7 +452,7 @@ def _logs(args):
|
|
|
454
452
|
if since is not None and until is not None and until < since:
|
|
455
453
|
since, until = until, since
|
|
456
454
|
|
|
457
|
-
lines_arg =
|
|
455
|
+
lines_arg = args.lines
|
|
458
456
|
lines_count: int | None = None
|
|
459
457
|
lines_oldest = False
|
|
460
458
|
if lines_arg is not None:
|