fal 1.41.1__py3-none-any.whl → 1.43.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 +2 -2
- fal/api.py +21 -5
- fal/app.py +3 -2
- fal/cli/_utils.py +15 -7
- fal/cli/apps.py +16 -2
- fal/cli/deploy.py +13 -9
- fal/cli/parser.py +32 -1
- fal/cli/runners.py +354 -3
- fal/sdk.py +15 -4
- {fal-1.41.1.dist-info → fal-1.43.0.dist-info}/METADATA +3 -2
- {fal-1.41.1.dist-info → fal-1.43.0.dist-info}/RECORD +14 -14
- {fal-1.41.1.dist-info → fal-1.43.0.dist-info}/WHEEL +0 -0
- {fal-1.41.1.dist-info → fal-1.43.0.dist-info}/entry_points.txt +0 -0
- {fal-1.41.1.dist-info → fal-1.43.0.dist-info}/top_level.txt +0 -0
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.
|
|
32
|
-
__version_tuple__ = version_tuple = (1,
|
|
31
|
+
__version__ = version = '1.43.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 43, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
fal/api.py
CHANGED
|
@@ -21,6 +21,7 @@ from typing import (
|
|
|
21
21
|
Iterator,
|
|
22
22
|
Literal,
|
|
23
23
|
NamedTuple,
|
|
24
|
+
Optional,
|
|
24
25
|
TypeVar,
|
|
25
26
|
cast,
|
|
26
27
|
overload,
|
|
@@ -53,10 +54,13 @@ from fal.exceptions._cuda import _is_cuda_oom_exception
|
|
|
53
54
|
from fal.logging.isolate import IsolateLogPrinter
|
|
54
55
|
from fal.sdk import (
|
|
55
56
|
FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER,
|
|
57
|
+
FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER_PERC,
|
|
56
58
|
FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
|
|
57
59
|
FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
58
60
|
FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
|
|
61
|
+
AuthModeLiteral,
|
|
59
62
|
Credentials,
|
|
63
|
+
DeploymentStrategyLiteral,
|
|
60
64
|
FalServerlessClient,
|
|
61
65
|
FalServerlessConnection,
|
|
62
66
|
HostedRunState,
|
|
@@ -424,6 +428,7 @@ class FalServerlessHost(Host):
|
|
|
424
428
|
"max_concurrency",
|
|
425
429
|
"min_concurrency",
|
|
426
430
|
"concurrency_buffer",
|
|
431
|
+
"concurrency_buffer_perc",
|
|
427
432
|
"max_multiplexing",
|
|
428
433
|
"setup_function",
|
|
429
434
|
"metadata",
|
|
@@ -465,12 +470,13 @@ class FalServerlessHost(Host):
|
|
|
465
470
|
self,
|
|
466
471
|
func: Callable[ArgsT, ReturnT],
|
|
467
472
|
options: Options,
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
473
|
+
*,
|
|
474
|
+
application_name: Optional[str] = None,
|
|
475
|
+
application_auth_mode: Optional[AuthModeLiteral] = None,
|
|
476
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
477
|
+
deployment_strategy: DeploymentStrategyLiteral,
|
|
472
478
|
scale: bool = True,
|
|
473
|
-
) -> str
|
|
479
|
+
) -> Optional[str]:
|
|
474
480
|
from isolate.backends.common import active_python
|
|
475
481
|
|
|
476
482
|
environment_options = options.environment.copy()
|
|
@@ -487,6 +493,7 @@ class FalServerlessHost(Host):
|
|
|
487
493
|
max_concurrency = options.host.get("max_concurrency")
|
|
488
494
|
min_concurrency = options.host.get("min_concurrency")
|
|
489
495
|
concurrency_buffer = options.host.get("concurrency_buffer")
|
|
496
|
+
concurrency_buffer_perc = options.host.get("concurrency_buffer_perc")
|
|
490
497
|
max_multiplexing = options.host.get("max_multiplexing")
|
|
491
498
|
exposed_port = options.get_exposed_port()
|
|
492
499
|
request_timeout = options.host.get("request_timeout")
|
|
@@ -503,6 +510,7 @@ class FalServerlessHost(Host):
|
|
|
503
510
|
max_concurrency=max_concurrency,
|
|
504
511
|
min_concurrency=min_concurrency,
|
|
505
512
|
concurrency_buffer=concurrency_buffer,
|
|
513
|
+
concurrency_buffer_perc=concurrency_buffer_perc,
|
|
506
514
|
request_timeout=request_timeout,
|
|
507
515
|
startup_timeout=startup_timeout,
|
|
508
516
|
)
|
|
@@ -560,6 +568,7 @@ class FalServerlessHost(Host):
|
|
|
560
568
|
max_concurrency = options.host.get("max_concurrency")
|
|
561
569
|
min_concurrency = options.host.get("min_concurrency")
|
|
562
570
|
concurrency_buffer = options.host.get("concurrency_buffer")
|
|
571
|
+
concurrency_buffer_perc = options.host.get("concurrency_buffer_perc")
|
|
563
572
|
max_multiplexing = options.host.get("max_multiplexing")
|
|
564
573
|
base_image = options.host.get("_base_image", None)
|
|
565
574
|
scheduler = options.host.get("_scheduler", None)
|
|
@@ -580,6 +589,7 @@ class FalServerlessHost(Host):
|
|
|
580
589
|
max_concurrency=max_concurrency,
|
|
581
590
|
min_concurrency=min_concurrency,
|
|
582
591
|
concurrency_buffer=concurrency_buffer,
|
|
592
|
+
concurrency_buffer_perc=concurrency_buffer_perc,
|
|
583
593
|
request_timeout=request_timeout,
|
|
584
594
|
startup_timeout=startup_timeout,
|
|
585
595
|
)
|
|
@@ -770,6 +780,7 @@ def function(
|
|
|
770
780
|
max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
771
781
|
min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
|
|
772
782
|
concurrency_buffer: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER,
|
|
783
|
+
concurrency_buffer_perc: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER_PERC,
|
|
773
784
|
request_timeout: int | None = None,
|
|
774
785
|
startup_timeout: int | None = None,
|
|
775
786
|
setup_function: Callable[..., None] | None = None,
|
|
@@ -800,6 +811,7 @@ def function(
|
|
|
800
811
|
max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
801
812
|
min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
|
|
802
813
|
concurrency_buffer: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER,
|
|
814
|
+
concurrency_buffer_perc: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER_PERC,
|
|
803
815
|
request_timeout: int | None = None,
|
|
804
816
|
startup_timeout: int | None = None,
|
|
805
817
|
setup_function: Callable[..., None] | None = None,
|
|
@@ -882,6 +894,7 @@ def function(
|
|
|
882
894
|
max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
883
895
|
min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
|
|
884
896
|
concurrency_buffer: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER,
|
|
897
|
+
concurrency_buffer_perc: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER_PERC,
|
|
885
898
|
request_timeout: int | None = None,
|
|
886
899
|
startup_timeout: int | None = None,
|
|
887
900
|
setup_function: Callable[..., None] | None = None,
|
|
@@ -917,6 +930,7 @@ def function(
|
|
|
917
930
|
max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
918
931
|
min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
|
|
919
932
|
concurrency_buffer: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER,
|
|
933
|
+
concurrency_buffer_perc: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER_PERC,
|
|
920
934
|
request_timeout: int | None = None,
|
|
921
935
|
startup_timeout: int | None = None,
|
|
922
936
|
setup_function: Callable[..., None] | None = None,
|
|
@@ -946,6 +960,7 @@ def function(
|
|
|
946
960
|
max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
947
961
|
min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
|
|
948
962
|
concurrency_buffer: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER,
|
|
963
|
+
concurrency_buffer_perc: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER_PERC,
|
|
949
964
|
request_timeout: int | None = None,
|
|
950
965
|
startup_timeout: int | None = None,
|
|
951
966
|
setup_function: Callable[..., None] | None = None,
|
|
@@ -975,6 +990,7 @@ def function(
|
|
|
975
990
|
max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
976
991
|
min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
|
|
977
992
|
concurrency_buffer: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER,
|
|
993
|
+
concurrency_buffer_perc: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER_PERC,
|
|
978
994
|
request_timeout: int | None = None,
|
|
979
995
|
startup_timeout: int | None = None,
|
|
980
996
|
setup_function: Callable[..., None] | None = None,
|
fal/app.py
CHANGED
|
@@ -11,7 +11,7 @@ import time
|
|
|
11
11
|
import typing
|
|
12
12
|
from contextlib import asynccontextmanager, contextmanager
|
|
13
13
|
from dataclasses import dataclass
|
|
14
|
-
from typing import Any, Callable, ClassVar,
|
|
14
|
+
from typing import Any, Callable, ClassVar, Optional, TypeVar
|
|
15
15
|
|
|
16
16
|
import fastapi
|
|
17
17
|
import grpc.aio as async_grpc
|
|
@@ -29,6 +29,7 @@ from fal.api import (
|
|
|
29
29
|
)
|
|
30
30
|
from fal.exceptions import FalServerlessException, RequestCancelledException
|
|
31
31
|
from fal.logging import get_logger
|
|
32
|
+
from fal.sdk import AuthModeLiteral
|
|
32
33
|
from fal.toolkit.file import request_lifecycle_preference
|
|
33
34
|
from fal.toolkit.file.providers.fal import LIFECYCLE_PREFERENCE
|
|
34
35
|
|
|
@@ -311,7 +312,7 @@ class App(BaseServable):
|
|
|
311
312
|
"keep_alive": 60,
|
|
312
313
|
}
|
|
313
314
|
app_name: ClassVar[str]
|
|
314
|
-
app_auth: ClassVar[
|
|
315
|
+
app_auth: ClassVar[Optional[AuthModeLiteral]] = None
|
|
315
316
|
request_timeout: ClassVar[int | None] = None
|
|
316
317
|
startup_timeout: ClassVar[int | None] = None
|
|
317
318
|
|
fal/cli/_utils.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
3
5
|
from fal.project import find_project_root, find_pyproject_toml, parse_pyproject_toml
|
|
6
|
+
from fal.sdk import AuthModeLiteral, DeploymentStrategyLiteral
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
def get_client(host: str, team: str | None = None):
|
|
7
|
-
from fal.sdk import FalServerlessClient, get_default_credentials
|
|
10
|
+
from fal.sdk import FalServerlessClient, get_default_credentials # noqa: PLC0415
|
|
8
11
|
|
|
9
12
|
credentials = get_default_credentials(team=team)
|
|
10
13
|
return FalServerlessClient(host, credentials)
|
|
@@ -17,7 +20,9 @@ def is_app_name(app_ref: tuple[str, str | None]) -> bool:
|
|
|
17
20
|
return is_single_file and not is_python_file
|
|
18
21
|
|
|
19
22
|
|
|
20
|
-
def get_app_data_from_toml(
|
|
23
|
+
def get_app_data_from_toml(
|
|
24
|
+
app_name,
|
|
25
|
+
) -> tuple[str, Optional[AuthModeLiteral], Optional[DeploymentStrategyLiteral], bool]:
|
|
21
26
|
toml_path = find_pyproject_toml()
|
|
22
27
|
|
|
23
28
|
if toml_path is None:
|
|
@@ -27,12 +32,12 @@ def get_app_data_from_toml(app_name):
|
|
|
27
32
|
apps = fal_data.get("apps", {})
|
|
28
33
|
|
|
29
34
|
try:
|
|
30
|
-
app_data = apps[app_name]
|
|
35
|
+
app_data: dict[str, Any] = apps[app_name]
|
|
31
36
|
except KeyError:
|
|
32
37
|
raise ValueError(f"App {app_name} not found in pyproject.toml")
|
|
33
38
|
|
|
34
39
|
try:
|
|
35
|
-
app_ref = app_data.pop("ref")
|
|
40
|
+
app_ref: str = app_data.pop("ref")
|
|
36
41
|
except KeyError:
|
|
37
42
|
raise ValueError(f"App {app_name} does not have a ref key in pyproject.toml")
|
|
38
43
|
|
|
@@ -40,12 +45,15 @@ def get_app_data_from_toml(app_name):
|
|
|
40
45
|
project_root, _ = find_project_root(None)
|
|
41
46
|
app_ref = str(project_root / app_ref)
|
|
42
47
|
|
|
43
|
-
app_auth = app_data.pop("auth",
|
|
44
|
-
app_deployment_strategy = app_data.pop(
|
|
48
|
+
app_auth: Optional[AuthModeLiteral] = app_data.pop("auth", None)
|
|
49
|
+
app_deployment_strategy: Optional[DeploymentStrategyLiteral] = app_data.pop(
|
|
50
|
+
"deployment_strategy", None
|
|
51
|
+
)
|
|
45
52
|
|
|
53
|
+
app_reset_scale: bool
|
|
46
54
|
if "no_scale" in app_data:
|
|
47
55
|
# Deprecated
|
|
48
|
-
app_no_scale = app_data.pop("no_scale")
|
|
56
|
+
app_no_scale: bool = app_data.pop("no_scale")
|
|
49
57
|
print("[WARNING] no_scale is deprecated, use app_scale_settings instead")
|
|
50
58
|
app_reset_scale = not app_no_scale
|
|
51
59
|
else:
|
fal/cli/apps.py
CHANGED
|
@@ -33,13 +33,20 @@ def _apps_table(apps: list[AliasInfo]):
|
|
|
33
33
|
table.add_column("Regions")
|
|
34
34
|
|
|
35
35
|
for app in apps:
|
|
36
|
+
if app.concurrency_buffer_perc > 0:
|
|
37
|
+
concurrency_buffer_str = (
|
|
38
|
+
f"{app.concurrency_buffer_perc}%, min {app.concurrency_buffer}"
|
|
39
|
+
)
|
|
40
|
+
else:
|
|
41
|
+
concurrency_buffer_str = str(app.concurrency_buffer)
|
|
42
|
+
|
|
36
43
|
table.add_row(
|
|
37
44
|
app.alias,
|
|
38
45
|
app.revision,
|
|
39
46
|
app.auth_mode,
|
|
40
47
|
str(app.min_concurrency),
|
|
41
48
|
str(app.max_concurrency),
|
|
42
|
-
|
|
49
|
+
concurrency_buffer_str,
|
|
43
50
|
str(app.max_multiplexing),
|
|
44
51
|
str(app.keep_alive),
|
|
45
52
|
str(app.request_timeout),
|
|
@@ -165,6 +172,7 @@ def _scale(args):
|
|
|
165
172
|
and args.max_concurrency is None
|
|
166
173
|
and args.min_concurrency is None
|
|
167
174
|
and args.concurrency_buffer is None
|
|
175
|
+
and args.concurrency_buffer_perc is None
|
|
168
176
|
and args.request_timeout is None
|
|
169
177
|
and args.startup_timeout is None
|
|
170
178
|
and args.machine_types is None
|
|
@@ -180,6 +188,7 @@ def _scale(args):
|
|
|
180
188
|
max_concurrency=args.max_concurrency,
|
|
181
189
|
min_concurrency=args.min_concurrency,
|
|
182
190
|
concurrency_buffer=args.concurrency_buffer,
|
|
191
|
+
concurrency_buffer_perc=args.concurrency_buffer_perc,
|
|
183
192
|
request_timeout=args.request_timeout,
|
|
184
193
|
startup_timeout=args.startup_timeout,
|
|
185
194
|
machine_types=args.machine_types,
|
|
@@ -225,7 +234,12 @@ def _add_scale_parser(subparsers, parents):
|
|
|
225
234
|
parser.add_argument(
|
|
226
235
|
"--concurrency-buffer",
|
|
227
236
|
type=int,
|
|
228
|
-
help="Concurrency buffer",
|
|
237
|
+
help="Concurrency buffer (min)",
|
|
238
|
+
)
|
|
239
|
+
parser.add_argument(
|
|
240
|
+
"--concurrency-buffer-perc",
|
|
241
|
+
type=int,
|
|
242
|
+
help="Concurrency buffer %",
|
|
229
243
|
)
|
|
230
244
|
parser.add_argument(
|
|
231
245
|
"--request-timeout",
|
fal/cli/deploy.py
CHANGED
|
@@ -2,7 +2,9 @@ import argparse
|
|
|
2
2
|
import json
|
|
3
3
|
from collections import namedtuple
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Optional, Tuple, Union, cast
|
|
6
|
+
|
|
7
|
+
from fal.sdk import AuthModeLiteral, DeploymentStrategyLiteral
|
|
6
8
|
|
|
7
9
|
from ._utils import get_app_data_from_toml, is_app_name
|
|
8
10
|
from .parser import FalClientParser, RefAction, get_output_parser
|
|
@@ -67,8 +69,8 @@ def _deploy_from_reference(
|
|
|
67
69
|
app_ref: Tuple[Optional[Union[Path, str]], ...],
|
|
68
70
|
app_name: str,
|
|
69
71
|
args,
|
|
70
|
-
auth: Optional[
|
|
71
|
-
deployment_strategy: Optional[
|
|
72
|
+
auth: Optional[AuthModeLiteral],
|
|
73
|
+
deployment_strategy: Optional[DeploymentStrategyLiteral],
|
|
72
74
|
scale: bool,
|
|
73
75
|
):
|
|
74
76
|
from fal.api import FalServerlessError, FalServerlessHost
|
|
@@ -99,7 +101,7 @@ def _deploy_from_reference(
|
|
|
99
101
|
isolated_function = loaded.function
|
|
100
102
|
app_name = app_name or loaded.app_name # type: ignore
|
|
101
103
|
app_auth = auth or loaded.app_auth
|
|
102
|
-
deployment_strategy = deployment_strategy or "
|
|
104
|
+
deployment_strategy = deployment_strategy or "rolling"
|
|
103
105
|
|
|
104
106
|
app_id = host.register(
|
|
105
107
|
func=isolated_function.func,
|
|
@@ -172,10 +174,12 @@ def _deploy(args):
|
|
|
172
174
|
# path/to/myfile.py::MyApp
|
|
173
175
|
else:
|
|
174
176
|
file_path, func_name = args.app_ref
|
|
175
|
-
app_name = args.app_name
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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)
|
|
179
183
|
|
|
180
184
|
_deploy_from_reference(
|
|
181
185
|
(file_path, func_name),
|
|
@@ -251,7 +255,7 @@ def add_parser(main_subparsers, parents):
|
|
|
251
255
|
"--strategy",
|
|
252
256
|
choices=["recreate", "rolling"],
|
|
253
257
|
help="Deployment strategy.",
|
|
254
|
-
default="
|
|
258
|
+
default="rolling",
|
|
255
259
|
)
|
|
256
260
|
parser.add_argument(
|
|
257
261
|
"--no-scale",
|
fal/cli/parser.py
CHANGED
|
@@ -86,6 +86,37 @@ class SinceAction(argparse.Action):
|
|
|
86
86
|
|
|
87
87
|
super().__init__(*args, **kwargs)
|
|
88
88
|
|
|
89
|
+
# If a default is provided as a string like "1h ago", parse it into a datetime
|
|
90
|
+
# so callers can rely on receiving a datetime even when the flag isn't passed.
|
|
91
|
+
default_value = getattr(self, "default", None)
|
|
92
|
+
if default_value is not None and default_value is not argparse.SUPPRESS:
|
|
93
|
+
if isinstance(default_value, str):
|
|
94
|
+
dt = self._parse_since(default_value)
|
|
95
|
+
if not dt:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Invalid 'default' value for SinceAction: {default_value!r}"
|
|
98
|
+
)
|
|
99
|
+
if (
|
|
100
|
+
self._limit
|
|
101
|
+
and self._limit_dt is not None
|
|
102
|
+
and dt < self._limit_dt - self.LIMIT_LEEWAY
|
|
103
|
+
):
|
|
104
|
+
raise ValueError(
|
|
105
|
+
"Default since value is older than the allowed limit "
|
|
106
|
+
f"{self._limit}."
|
|
107
|
+
)
|
|
108
|
+
self.default = dt
|
|
109
|
+
elif isinstance(default_value, datetime):
|
|
110
|
+
if (
|
|
111
|
+
self._limit
|
|
112
|
+
and self._limit_dt is not None
|
|
113
|
+
and default_value < self._limit_dt - self.LIMIT_LEEWAY
|
|
114
|
+
):
|
|
115
|
+
raise ValueError(
|
|
116
|
+
"Default since value is older than the allowed limit "
|
|
117
|
+
f"{self._limit}."
|
|
118
|
+
)
|
|
119
|
+
|
|
89
120
|
def __call__(self, parser, args, values, option_string=None): # noqa: ARG002
|
|
90
121
|
if values is None:
|
|
91
122
|
setattr(args, self.dest, None)
|
|
@@ -102,7 +133,7 @@ class SinceAction(argparse.Action):
|
|
|
102
133
|
),
|
|
103
134
|
)
|
|
104
135
|
|
|
105
|
-
if self._limit_dt is not None:
|
|
136
|
+
if self._limit and self._limit_dt is not None:
|
|
106
137
|
if dt < self._limit_dt - self.LIMIT_LEEWAY:
|
|
107
138
|
raise argparse.ArgumentError(
|
|
108
139
|
self,
|
fal/cli/runners.py
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from
|
|
5
|
-
from
|
|
6
|
-
|
|
4
|
+
from collections import deque
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
from http import HTTPStatus
|
|
8
|
+
from typing import Iterator, List
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from httpx_sse import connect_sse
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from structlog.typing import EventDict
|
|
14
|
+
|
|
15
|
+
from fal.rest_client import REST_CLIENT
|
|
7
16
|
from fal.sdk import RunnerInfo, RunnerState
|
|
8
17
|
|
|
9
18
|
from ._utils import get_client
|
|
@@ -198,6 +207,347 @@ def _add_list_parser(subparsers, parents):
|
|
|
198
207
|
parser.set_defaults(func=_list)
|
|
199
208
|
|
|
200
209
|
|
|
210
|
+
def _to_iso_naive(dt: datetime) -> str:
|
|
211
|
+
return dt.astimezone(timezone.utc).isoformat()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _parse_ts(ts: str) -> datetime:
|
|
215
|
+
# Support both 'Z' and offset formats
|
|
216
|
+
ts_norm = ts.replace("Z", "+00:00")
|
|
217
|
+
return datetime.fromisoformat(ts_norm)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _to_aware_utc(dt: datetime) -> datetime:
|
|
221
|
+
# Treat naive datetimes as UTC
|
|
222
|
+
if dt.tzinfo is None:
|
|
223
|
+
return dt.replace(tzinfo=timezone.utc)
|
|
224
|
+
return dt.astimezone(timezone.utc)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _post_history(
|
|
228
|
+
client: httpx.Client,
|
|
229
|
+
base_params: dict[str, str],
|
|
230
|
+
since: datetime | None,
|
|
231
|
+
until: datetime | None,
|
|
232
|
+
page_size: int,
|
|
233
|
+
) -> tuple[list, str | None]:
|
|
234
|
+
params: dict[str, str] = dict(base_params)
|
|
235
|
+
if since is not None:
|
|
236
|
+
params["since"] = _to_iso_naive(since)
|
|
237
|
+
if until is not None:
|
|
238
|
+
params["until"] = _to_iso_naive(until)
|
|
239
|
+
params["page_size"] = str(page_size)
|
|
240
|
+
resp = client.post("/logs/history", params=params)
|
|
241
|
+
if resp.status_code != HTTPStatus.OK:
|
|
242
|
+
try:
|
|
243
|
+
detail = resp.json().get("detail", resp.text)
|
|
244
|
+
except Exception:
|
|
245
|
+
detail = resp.text
|
|
246
|
+
raise RuntimeError(f"Failed to fetch logs history: {detail}")
|
|
247
|
+
data = resp.json()
|
|
248
|
+
items = data.get("items", []) if isinstance(data, dict) else []
|
|
249
|
+
next_until = data.get("next_until") if isinstance(data, dict) else None
|
|
250
|
+
if not isinstance(items, list):
|
|
251
|
+
raise RuntimeError("Unexpected logs history response format")
|
|
252
|
+
return items, next_until
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@dataclass
|
|
256
|
+
class RestRunnerInfo:
|
|
257
|
+
started_at: datetime | None
|
|
258
|
+
ended_at: datetime | None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _get_runner_info(runner_id: str) -> RestRunnerInfo:
|
|
262
|
+
headers = REST_CLIENT.get_headers()
|
|
263
|
+
with httpx.Client(
|
|
264
|
+
base_url=REST_CLIENT.base_url, headers=headers, timeout=30
|
|
265
|
+
) as client:
|
|
266
|
+
resp = client.get(f"/runners/{runner_id}")
|
|
267
|
+
if resp.status_code == HTTPStatus.NOT_FOUND:
|
|
268
|
+
raise RuntimeError(f"Runner {runner_id} not found")
|
|
269
|
+
if resp.status_code != HTTPStatus.OK:
|
|
270
|
+
raise RuntimeError(
|
|
271
|
+
f"Failed to fetch runner info: {resp.status_code} {resp.text}"
|
|
272
|
+
)
|
|
273
|
+
data = resp.json()
|
|
274
|
+
if not isinstance(data, dict):
|
|
275
|
+
raise RuntimeError(f"Unexpected runner info response format: {resp.text}")
|
|
276
|
+
|
|
277
|
+
start: datetime | None = None
|
|
278
|
+
end: datetime | None = None
|
|
279
|
+
|
|
280
|
+
started_at = data.get("started_at")
|
|
281
|
+
if started_at is not None:
|
|
282
|
+
try:
|
|
283
|
+
start = _to_aware_utc(_parse_ts(started_at))
|
|
284
|
+
except Exception:
|
|
285
|
+
start = None
|
|
286
|
+
|
|
287
|
+
ended_at = data.get("ended_at")
|
|
288
|
+
if ended_at is not None:
|
|
289
|
+
try:
|
|
290
|
+
end = _to_aware_utc(_parse_ts(ended_at))
|
|
291
|
+
except Exception:
|
|
292
|
+
end = None
|
|
293
|
+
|
|
294
|
+
return RestRunnerInfo(started_at=start, ended_at=end)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _stream_logs(
|
|
298
|
+
base_params: dict[str, str], since: datetime | None, until: datetime | None
|
|
299
|
+
) -> Iterator[dict]:
|
|
300
|
+
headers = REST_CLIENT.get_headers()
|
|
301
|
+
params: dict[str, str] = base_params.copy()
|
|
302
|
+
if since is not None:
|
|
303
|
+
params["since"] = _to_iso_naive(since)
|
|
304
|
+
if until is not None:
|
|
305
|
+
params["until"] = _to_iso_naive(until)
|
|
306
|
+
with httpx.Client(
|
|
307
|
+
base_url=REST_CLIENT.base_url,
|
|
308
|
+
headers=headers,
|
|
309
|
+
timeout=None,
|
|
310
|
+
follow_redirects=True,
|
|
311
|
+
) as client:
|
|
312
|
+
with connect_sse(
|
|
313
|
+
client,
|
|
314
|
+
method="POST",
|
|
315
|
+
url="/logs/stream",
|
|
316
|
+
params=params,
|
|
317
|
+
headers={"Accept": "text/event-stream"},
|
|
318
|
+
) as event_source:
|
|
319
|
+
for sse in event_source.iter_sse():
|
|
320
|
+
if not sse.data:
|
|
321
|
+
continue
|
|
322
|
+
if sse.event == "error":
|
|
323
|
+
raise RuntimeError(f"Error streaming logs: {sse.data}")
|
|
324
|
+
try:
|
|
325
|
+
yield json.loads(sse.data)
|
|
326
|
+
except Exception:
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
DEFAULT_PAGE_SIZE = 1000
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _iter_logs(
|
|
334
|
+
base_params: dict[str, str], start: datetime | None, end: datetime | None
|
|
335
|
+
) -> Iterator[dict]:
|
|
336
|
+
headers = REST_CLIENT.get_headers()
|
|
337
|
+
with httpx.Client(
|
|
338
|
+
base_url=REST_CLIENT.base_url,
|
|
339
|
+
headers=headers,
|
|
340
|
+
timeout=300,
|
|
341
|
+
follow_redirects=True,
|
|
342
|
+
) as client:
|
|
343
|
+
cursor_until = end
|
|
344
|
+
while True:
|
|
345
|
+
items, next_until = _post_history(
|
|
346
|
+
client, base_params, start, cursor_until, DEFAULT_PAGE_SIZE
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
yield from items
|
|
350
|
+
|
|
351
|
+
if not next_until:
|
|
352
|
+
break
|
|
353
|
+
|
|
354
|
+
new_until_dt = _to_aware_utc(_parse_ts(next_until))
|
|
355
|
+
if start is not None and new_until_dt <= start:
|
|
356
|
+
break
|
|
357
|
+
cursor_until = new_until_dt
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _get_logs(
|
|
361
|
+
params: dict[str, str],
|
|
362
|
+
since: datetime | None,
|
|
363
|
+
until: datetime | None,
|
|
364
|
+
lines_count: int | None,
|
|
365
|
+
*,
|
|
366
|
+
oldest: bool = False,
|
|
367
|
+
) -> Iterator[dict]:
|
|
368
|
+
if lines_count is None:
|
|
369
|
+
yield from _iter_logs(params, since, until)
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
if oldest:
|
|
373
|
+
produced = 0
|
|
374
|
+
for log in _iter_logs(params, since, until):
|
|
375
|
+
if produced >= lines_count:
|
|
376
|
+
break
|
|
377
|
+
produced += 1
|
|
378
|
+
yield log
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
# newest tail: collect into a fixed-size deque, then yield
|
|
382
|
+
tail: deque[dict] = deque(maxlen=lines_count)
|
|
383
|
+
for log in _iter_logs(params, since, until):
|
|
384
|
+
tail.append(log)
|
|
385
|
+
for log in tail:
|
|
386
|
+
yield log
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class LogPrinter:
|
|
390
|
+
def __init__(self, console: Console) -> None:
|
|
391
|
+
from structlog.dev import ConsoleRenderer
|
|
392
|
+
|
|
393
|
+
from fal.logging.style import LEVEL_STYLES
|
|
394
|
+
|
|
395
|
+
self._console = console
|
|
396
|
+
self._renderer = ConsoleRenderer(level_styles=LEVEL_STYLES)
|
|
397
|
+
|
|
398
|
+
def _render_log(self, log: dict) -> str:
|
|
399
|
+
ts_str: str = log["timestamp"]
|
|
400
|
+
timestamp = _to_aware_utc(_parse_ts(ts_str))
|
|
401
|
+
local_ts = timestamp.astimezone()
|
|
402
|
+
tz_offset = local_ts.strftime("%z")
|
|
403
|
+
# Insert ':' into offset for readability, e.g. +0300 -> +03:00
|
|
404
|
+
if tz_offset and len(tz_offset) == 5:
|
|
405
|
+
tz_offset = tz_offset[:3] + ":" + tz_offset[3:]
|
|
406
|
+
|
|
407
|
+
event: EventDict = {
|
|
408
|
+
"event": log.get("message", ""),
|
|
409
|
+
"level": str(log.get("level", "")).upper(),
|
|
410
|
+
"timestamp": f"{local_ts.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]}{tz_offset}",
|
|
411
|
+
}
|
|
412
|
+
return self._renderer(logger={}, name=event["level"], event_dict=event)
|
|
413
|
+
|
|
414
|
+
def print(self, log: dict) -> None:
|
|
415
|
+
self._console.print(self._render_log(log), highlight=False)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
DEFAULT_STREAM_SINCE = timedelta(minutes=1)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _logs(args):
|
|
422
|
+
params: dict[str, str] = {"job_id": args.id}
|
|
423
|
+
if getattr(args, "search", None) is not None:
|
|
424
|
+
params["search"] = args.search
|
|
425
|
+
|
|
426
|
+
runner_info = _get_runner_info(args.id)
|
|
427
|
+
follow: bool = getattr(args, "follow", False)
|
|
428
|
+
since = getattr(args, "since", None)
|
|
429
|
+
if follow:
|
|
430
|
+
since = since or (datetime.now(timezone.utc) - DEFAULT_STREAM_SINCE)
|
|
431
|
+
else:
|
|
432
|
+
since = since or runner_info.started_at
|
|
433
|
+
until = getattr(args, "until", None) or runner_info.ended_at
|
|
434
|
+
|
|
435
|
+
# Normalize to aware UTC for comparisons
|
|
436
|
+
if since is not None:
|
|
437
|
+
since = _to_aware_utc(since)
|
|
438
|
+
if until is not None:
|
|
439
|
+
until = _to_aware_utc(until)
|
|
440
|
+
|
|
441
|
+
# Sanity limiters: clamp within runner lifetime when known
|
|
442
|
+
if runner_info.started_at is not None:
|
|
443
|
+
if since is not None and since < runner_info.started_at:
|
|
444
|
+
since = runner_info.started_at
|
|
445
|
+
if until is not None and until < runner_info.started_at:
|
|
446
|
+
until = runner_info.started_at
|
|
447
|
+
if runner_info.ended_at is not None:
|
|
448
|
+
if since is not None and since > runner_info.ended_at:
|
|
449
|
+
since = runner_info.ended_at
|
|
450
|
+
if until is not None and until > runner_info.ended_at:
|
|
451
|
+
until = runner_info.ended_at
|
|
452
|
+
|
|
453
|
+
# Ensure ordering if both are present
|
|
454
|
+
if since is not None and until is not None and until < since:
|
|
455
|
+
since, until = until, since
|
|
456
|
+
|
|
457
|
+
lines_arg = getattr(args, "lines", None)
|
|
458
|
+
lines_count: int | None = None
|
|
459
|
+
lines_oldest = False
|
|
460
|
+
if lines_arg is not None:
|
|
461
|
+
if lines_arg.startswith("+"):
|
|
462
|
+
lines_str = lines_arg[1:]
|
|
463
|
+
lines_oldest = True
|
|
464
|
+
else:
|
|
465
|
+
lines_str = lines_arg
|
|
466
|
+
try:
|
|
467
|
+
lines_count = int(lines_str)
|
|
468
|
+
except ValueError:
|
|
469
|
+
args.parser.error("Invalid -n|--lines value. Use an integer or +integer.")
|
|
470
|
+
|
|
471
|
+
if follow:
|
|
472
|
+
logs_gen = _stream_logs(params, since, until)
|
|
473
|
+
else:
|
|
474
|
+
logs_gen = _get_logs(params, since, until, lines_count, oldest=lines_oldest)
|
|
475
|
+
|
|
476
|
+
printer = LogPrinter(args.console)
|
|
477
|
+
|
|
478
|
+
if follow:
|
|
479
|
+
for log in logs_gen:
|
|
480
|
+
if args.output == "json":
|
|
481
|
+
args.console.print(json.dumps(log))
|
|
482
|
+
else:
|
|
483
|
+
printer.print(log)
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
if args.output == "json":
|
|
487
|
+
args.console.print(json.dumps({"logs": list(logs_gen)}))
|
|
488
|
+
else:
|
|
489
|
+
for log in reversed(list(logs_gen)):
|
|
490
|
+
printer.print(log)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _add_logs_parser(subparsers, parents):
|
|
494
|
+
logs_help = "Show logs for a runner."
|
|
495
|
+
parser = subparsers.add_parser(
|
|
496
|
+
"logs",
|
|
497
|
+
aliases=["log"],
|
|
498
|
+
description=logs_help,
|
|
499
|
+
help=logs_help,
|
|
500
|
+
parents=[*parents, get_output_parser()],
|
|
501
|
+
)
|
|
502
|
+
parser.add_argument(
|
|
503
|
+
"id",
|
|
504
|
+
help="Runner ID.",
|
|
505
|
+
)
|
|
506
|
+
parser.add_argument(
|
|
507
|
+
"--search",
|
|
508
|
+
default=None,
|
|
509
|
+
help="Search for string in logs.",
|
|
510
|
+
)
|
|
511
|
+
parser.add_argument(
|
|
512
|
+
"--since",
|
|
513
|
+
default=None,
|
|
514
|
+
action=SinceAction,
|
|
515
|
+
help=(
|
|
516
|
+
"Show logs since the given time. "
|
|
517
|
+
"Accepts 'now', relative like '30m', '1h', or an ISO timestamp. "
|
|
518
|
+
"Defaults to runner start time or to '1m ago' in --follow mode."
|
|
519
|
+
),
|
|
520
|
+
)
|
|
521
|
+
parser.add_argument(
|
|
522
|
+
"--until",
|
|
523
|
+
default=None,
|
|
524
|
+
action=SinceAction,
|
|
525
|
+
help=(
|
|
526
|
+
"Show logs until the given time. "
|
|
527
|
+
"Accepts 'now', relative like '30m', '1h', or an ISO timestamp. "
|
|
528
|
+
"Defaults to runner finish time or 'now' if it is still running."
|
|
529
|
+
),
|
|
530
|
+
)
|
|
531
|
+
parser.add_argument(
|
|
532
|
+
"--follow",
|
|
533
|
+
"-f",
|
|
534
|
+
action="store_true",
|
|
535
|
+
help="Follow logs live. If --since is not specified, implies '--since 1m ago'.",
|
|
536
|
+
)
|
|
537
|
+
parser.add_argument(
|
|
538
|
+
"--lines",
|
|
539
|
+
"-n",
|
|
540
|
+
default=None,
|
|
541
|
+
type=str,
|
|
542
|
+
help=(
|
|
543
|
+
"Only show latest N log lines. "
|
|
544
|
+
"If '+' prefix is used, show oldest N log lines. "
|
|
545
|
+
"Ignored if --follow is used."
|
|
546
|
+
),
|
|
547
|
+
)
|
|
548
|
+
parser.set_defaults(func=_logs)
|
|
549
|
+
|
|
550
|
+
|
|
201
551
|
def add_parser(main_subparsers, parents):
|
|
202
552
|
runners_help = "Manage fal runners."
|
|
203
553
|
parser = main_subparsers.add_parser(
|
|
@@ -217,3 +567,4 @@ def add_parser(main_subparsers, parents):
|
|
|
217
567
|
|
|
218
568
|
_add_kill_parser(subparsers, parents)
|
|
219
569
|
_add_list_parser(subparsers, parents)
|
|
570
|
+
_add_logs_parser(subparsers, parents)
|
fal/sdk.py
CHANGED
|
@@ -38,6 +38,7 @@ FAL_SERVERLESS_DEFAULT_KEEP_ALIVE = 10
|
|
|
38
38
|
FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING = 1
|
|
39
39
|
FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY = 0
|
|
40
40
|
FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER = 0
|
|
41
|
+
FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER_PERC = 0
|
|
41
42
|
ALIAS_AUTH_MODES = ["public", "private", "shared"]
|
|
42
43
|
|
|
43
44
|
logger = get_logger(__name__)
|
|
@@ -45,7 +46,8 @@ logger = get_logger(__name__)
|
|
|
45
46
|
patch_pickle()
|
|
46
47
|
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
AuthModeLiteral = Literal["public", "private", "shared"]
|
|
50
|
+
DeploymentStrategyLiteral = Literal["recreate", "rolling"]
|
|
49
51
|
|
|
50
52
|
|
|
51
53
|
class ServerCredentials:
|
|
@@ -237,6 +239,7 @@ class ApplicationInfo:
|
|
|
237
239
|
active_runners: int
|
|
238
240
|
min_concurrency: int
|
|
239
241
|
concurrency_buffer: int
|
|
242
|
+
concurrency_buffer_perc: int
|
|
240
243
|
machine_types: list[str]
|
|
241
244
|
request_timeout: int
|
|
242
245
|
startup_timeout: int
|
|
@@ -255,6 +258,7 @@ class AliasInfo:
|
|
|
255
258
|
active_runners: int
|
|
256
259
|
min_concurrency: int
|
|
257
260
|
concurrency_buffer: int
|
|
261
|
+
concurrency_buffer_perc: int
|
|
258
262
|
machine_types: list[str]
|
|
259
263
|
request_timeout: int
|
|
260
264
|
startup_timeout: int
|
|
@@ -403,6 +407,7 @@ def _from_grpc_application_info(
|
|
|
403
407
|
active_runners=message.active_runners,
|
|
404
408
|
min_concurrency=message.min_concurrency,
|
|
405
409
|
concurrency_buffer=message.concurrency_buffer,
|
|
410
|
+
concurrency_buffer_perc=message.concurrency_buffer_perc,
|
|
406
411
|
machine_types=list(message.machine_types),
|
|
407
412
|
request_timeout=message.request_timeout,
|
|
408
413
|
startup_timeout=message.startup_timeout,
|
|
@@ -432,6 +437,7 @@ def _from_grpc_alias_info(message: isolate_proto.AliasInfo) -> AliasInfo:
|
|
|
432
437
|
active_runners=message.active_runners,
|
|
433
438
|
min_concurrency=message.min_concurrency,
|
|
434
439
|
concurrency_buffer=message.concurrency_buffer,
|
|
440
|
+
concurrency_buffer_perc=message.concurrency_buffer_perc,
|
|
435
441
|
machine_types=list(message.machine_types),
|
|
436
442
|
request_timeout=message.request_timeout,
|
|
437
443
|
startup_timeout=message.startup_timeout,
|
|
@@ -524,6 +530,7 @@ class MachineRequirements:
|
|
|
524
530
|
max_multiplexing: int | None = None
|
|
525
531
|
min_concurrency: int | None = None
|
|
526
532
|
concurrency_buffer: int | None = None
|
|
533
|
+
concurrency_buffer_perc: int | None = None
|
|
527
534
|
request_timeout: int | None = None
|
|
528
535
|
startup_timeout: int | None = None
|
|
529
536
|
|
|
@@ -617,12 +624,12 @@ class FalServerlessConnection:
|
|
|
617
624
|
function: Callable[..., ResultT],
|
|
618
625
|
environments: list[isolate_proto.EnvironmentDefinition],
|
|
619
626
|
application_name: str | None = None,
|
|
620
|
-
auth_mode:
|
|
627
|
+
auth_mode: Optional[AuthModeLiteral] = None,
|
|
621
628
|
*,
|
|
622
629
|
serialization_method: str = _DEFAULT_SERIALIZATION_METHOD,
|
|
623
630
|
machine_requirements: MachineRequirements | None = None,
|
|
624
631
|
metadata: dict[str, Any] | None = None,
|
|
625
|
-
deployment_strategy:
|
|
632
|
+
deployment_strategy: DeploymentStrategyLiteral,
|
|
626
633
|
scale: bool = True,
|
|
627
634
|
private_logs: bool = False,
|
|
628
635
|
) -> Iterator[isolate_proto.RegisterApplicationResult]:
|
|
@@ -643,6 +650,7 @@ class FalServerlessConnection:
|
|
|
643
650
|
max_concurrency=machine_requirements.max_concurrency,
|
|
644
651
|
min_concurrency=machine_requirements.min_concurrency,
|
|
645
652
|
concurrency_buffer=machine_requirements.concurrency_buffer,
|
|
653
|
+
concurrency_buffer_perc=machine_requirements.concurrency_buffer_perc,
|
|
646
654
|
max_multiplexing=machine_requirements.max_multiplexing,
|
|
647
655
|
request_timeout=machine_requirements.request_timeout,
|
|
648
656
|
startup_timeout=machine_requirements.startup_timeout,
|
|
@@ -693,6 +701,7 @@ class FalServerlessConnection:
|
|
|
693
701
|
max_concurrency: int | None = None,
|
|
694
702
|
min_concurrency: int | None = None,
|
|
695
703
|
concurrency_buffer: int | None = None,
|
|
704
|
+
concurrency_buffer_perc: int | None = None,
|
|
696
705
|
request_timeout: int | None = None,
|
|
697
706
|
startup_timeout: int | None = None,
|
|
698
707
|
valid_regions: list[str] | None = None,
|
|
@@ -705,6 +714,7 @@ class FalServerlessConnection:
|
|
|
705
714
|
max_concurrency=max_concurrency,
|
|
706
715
|
min_concurrency=min_concurrency,
|
|
707
716
|
concurrency_buffer=concurrency_buffer,
|
|
717
|
+
concurrency_buffer_perc=concurrency_buffer_perc,
|
|
708
718
|
request_timeout=request_timeout,
|
|
709
719
|
startup_timeout=startup_timeout,
|
|
710
720
|
valid_regions=valid_regions,
|
|
@@ -758,6 +768,7 @@ class FalServerlessConnection:
|
|
|
758
768
|
max_multiplexing=machine_requirements.max_multiplexing,
|
|
759
769
|
min_concurrency=machine_requirements.min_concurrency,
|
|
760
770
|
concurrency_buffer=machine_requirements.concurrency_buffer,
|
|
771
|
+
concurrency_buffer_perc=machine_requirements.concurrency_buffer_perc,
|
|
761
772
|
request_timeout=machine_requirements.request_timeout,
|
|
762
773
|
startup_timeout=machine_requirements.startup_timeout,
|
|
763
774
|
)
|
|
@@ -783,7 +794,7 @@ class FalServerlessConnection:
|
|
|
783
794
|
self,
|
|
784
795
|
alias: str,
|
|
785
796
|
revision: str,
|
|
786
|
-
auth_mode:
|
|
797
|
+
auth_mode: Optional[AuthModeLiteral],
|
|
787
798
|
) -> AliasInfo:
|
|
788
799
|
if auth_mode == "public":
|
|
789
800
|
auth = isolate_proto.ApplicationAuthMode.PUBLIC
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fal
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.43.0
|
|
4
4
|
Summary: fal is an easy-to-use Serverless Python Framework
|
|
5
5
|
Author: Features & Labels <support@fal.ai>
|
|
6
6
|
Requires-Python: >=3.8
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Requires-Dist: isolate[build]<0.21.0,>=0.18.0
|
|
9
|
-
Requires-Dist: isolate-proto<0.
|
|
9
|
+
Requires-Dist: isolate-proto<0.19.0,>=0.18.0
|
|
10
10
|
Requires-Dist: grpcio<2,>=1.64.0
|
|
11
11
|
Requires-Dist: dill==0.3.7
|
|
12
12
|
Requires-Dist: cloudpickle==3.0.0
|
|
@@ -25,6 +25,7 @@ Requires-Dist: pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<2.11
|
|
|
25
25
|
Requires-Dist: fastapi<1,>=0.99.1
|
|
26
26
|
Requires-Dist: starlette-exporter>=0.21.0
|
|
27
27
|
Requires-Dist: httpx>=0.15.4
|
|
28
|
+
Requires-Dist: httpx-sse
|
|
28
29
|
Requires-Dist: attrs>=21.3.0
|
|
29
30
|
Requires-Dist: python-dateutil<3,>=2.8.0
|
|
30
31
|
Requires-Dist: types-python-dateutil<3,>=2.8.0
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
fal/__init__.py,sha256=wXs1G0gSc7ZK60-bHe-B2m0l_sA6TrFk4BxY0tMoLe8,784
|
|
2
2
|
fal/__main__.py,sha256=4JMK66Wj4uLZTKbF-sT3LAxOsr6buig77PmOkJCRRxw,83
|
|
3
|
-
fal/_fal_version.py,sha256=
|
|
3
|
+
fal/_fal_version.py,sha256=9ytRZ2Yka4_txqqqY2iXWd2yQiXsjbUMCnR5a5N5uWw,706
|
|
4
4
|
fal/_serialization.py,sha256=npXNsFJ5G7jzBeBIyVMH01Ww34mGY4XWhHpRbSrTtnQ,7598
|
|
5
5
|
fal/_version.py,sha256=1BbTFnucNC_6ldKJ_ZoC722_UkW4S9aDBSW9L0fkKAw,2315
|
|
6
|
-
fal/api.py,sha256=
|
|
7
|
-
fal/app.py,sha256=
|
|
6
|
+
fal/api.py,sha256=6LkGbbqGUC4tcMBlTL-l7DBkl7t9FpZFSZY1doIdI5o,50284
|
|
7
|
+
fal/app.py,sha256=4CGoHBxHQkpjpSlfYi-CCjVQ2A6BDX3qaH2JYv_zaoc,26008
|
|
8
8
|
fal/apps.py,sha256=pzCd2mrKl5J_4oVc40_pggvPtFahXBCdrZXWpnaEJVs,12130
|
|
9
9
|
fal/config.py,sha256=1HRaOJFOAjB7fbQoEPCSH85gMvEEMIMPeupVWgrHVgU,3572
|
|
10
10
|
fal/container.py,sha256=FTsa5hOW4ars-yV1lUoc0BNeIIvAZcpw7Ftyt3A4m_w,2000
|
|
@@ -13,7 +13,7 @@ fal/flags.py,sha256=QonyDM7R2GqfAB1bJr46oriu-fHJCkpUwXuSdanePWg,987
|
|
|
13
13
|
fal/project.py,sha256=QgfYfMKmNobMPufrAP_ga1FKcIAlSbw18Iar1-0qepo,2650
|
|
14
14
|
fal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
fal/rest_client.py,sha256=kGBGmuyHfX1lR910EoKCYPjsyU8MdXawT_cW2q8Sajc,568
|
|
16
|
-
fal/sdk.py,sha256=
|
|
16
|
+
fal/sdk.py,sha256=13NXGsuoiXM94zzZi9p7PwWSeucYH8Yez6obWa64LBc,28891
|
|
17
17
|
fal/sync.py,sha256=ZuIJA2-hTPNANG9B_NNJZUsO68EIdTH0dc9MzeVE2VU,4340
|
|
18
18
|
fal/utils.py,sha256=iQTBG3-i6JZgHkkwbY_I4210g0xoW-as51yrke608u0,2208
|
|
19
19
|
fal/workflows.py,sha256=Zl4f6Bs085hY40zmqScxDUyCu7zXkukDbW02iYOLTTI,14805
|
|
@@ -21,23 +21,23 @@ fal/auth/__init__.py,sha256=mtyQou8DGHC-COjW9WbtRyyzjyt7fMlhVmsB4U-CBh4,6509
|
|
|
21
21
|
fal/auth/auth0.py,sha256=g5OgEKe4rsbkLQp6l7EauOAVL6WsmKjuA1wmzmyvvhc,5354
|
|
22
22
|
fal/auth/local.py,sha256=sndkM6vKpeVny6NHTacVlTbiIFqaksOmw0Viqs_RN1U,1790
|
|
23
23
|
fal/cli/__init__.py,sha256=padK4o0BFqq61kxAA1qQ0jYr2SuhA2mf90B3AaRkmJA,37
|
|
24
|
-
fal/cli/_utils.py,sha256=
|
|
24
|
+
fal/cli/_utils.py,sha256=XwYoJr8SahaKB9OkGkw178FBpSeFAB-GqDXUQgGoFRE,2196
|
|
25
25
|
fal/cli/api.py,sha256=ZuDE_PIC-czzneTAWMwvC7P7WnwIyluNZSuJqzCFhqI,2640
|
|
26
|
-
fal/cli/apps.py,sha256=
|
|
26
|
+
fal/cli/apps.py,sha256=YZGF9slwGYtkU6PjMypatcICu606lLnpIwldO2N4p1I,13045
|
|
27
27
|
fal/cli/auth.py,sha256=ZLjxuF4LobETJ2CLGMj_QurE0PiJxzKdFJZkux8uLHM,5977
|
|
28
28
|
fal/cli/cli_nested_json.py,sha256=veSZU8_bYV3Iu1PAoxt-4BMBraNIqgH5nughbs2UKvE,13539
|
|
29
29
|
fal/cli/create.py,sha256=a8WDq-nJLFTeoIXqpb5cr7GR7YR9ZZrQCawNm34KXXE,627
|
|
30
30
|
fal/cli/debug.py,sha256=mTCjSpEZaNKcX225VZtry-BspFKSHURUuxUFuX6x5Cc,1488
|
|
31
|
-
fal/cli/deploy.py,sha256=
|
|
31
|
+
fal/cli/deploy.py,sha256=vX8TpLwoyoLZnK03B005MEBi3wP0M5Pm6AKQ2tHOyjM,8903
|
|
32
32
|
fal/cli/doctor.py,sha256=8SZrYG9Ku0F6LLUHtFdKopdIgZfFkw5E3Mwrxa9KOSk,1613
|
|
33
33
|
fal/cli/files.py,sha256=-j0q4g53A7CWSczGLdfeUCTSd4zXoV3pfZFdman7JOw,3450
|
|
34
34
|
fal/cli/keys.py,sha256=iQVMr3WT8CUqSQT3qeCCiy6rRwoux9F-UEaC4bCwMWo,3754
|
|
35
35
|
fal/cli/main.py,sha256=LDy3gze9TRsvGa4uSNc8NMFmWMLpsyoC-msteICNiso,3371
|
|
36
|
-
fal/cli/parser.py,sha256=
|
|
36
|
+
fal/cli/parser.py,sha256=siSY1kxqczZIs3l_jLwug_BpVzY_ZqHpewON3am83Ow,6658
|
|
37
37
|
fal/cli/profile.py,sha256=PAY_ffifCT71VJ8VxfDVaXPT0U1oN8drvWZDFRXwvek,6678
|
|
38
38
|
fal/cli/queue.py,sha256=9Kid3zR6VOFfAdDgnqi2TNN4ocIv5Vs61ASEZnwMa9o,2713
|
|
39
39
|
fal/cli/run.py,sha256=nAC12Qss4Fg1XmV0qOS9RdGNLYcdoHeRgQMvbTN4P9I,1202
|
|
40
|
-
fal/cli/runners.py,sha256=
|
|
40
|
+
fal/cli/runners.py,sha256=OWSsvk01IkwQhibewZQgC-iWMOXl43tWJSi9F81x8n4,17481
|
|
41
41
|
fal/cli/secrets.py,sha256=HfIeO2IZpCEiBC6Cs5Kpi3zckfDnc7GsLwLdgj3NnPU,3085
|
|
42
42
|
fal/cli/teams.py,sha256=_JcNcf659ZoLBFOxKnVP5A6Pyk1jY1vh4_xzMweYIDo,1285
|
|
43
43
|
fal/console/__init__.py,sha256=lGPUuTqIM9IKTa1cyyA-MA2iZJKVHp2YydsITZVlb6g,148
|
|
@@ -143,8 +143,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
|
|
|
143
143
|
openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
|
|
144
144
|
openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
|
|
145
145
|
openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
|
|
146
|
-
fal-1.
|
|
147
|
-
fal-1.
|
|
148
|
-
fal-1.
|
|
149
|
-
fal-1.
|
|
150
|
-
fal-1.
|
|
146
|
+
fal-1.43.0.dist-info/METADATA,sha256=zPgp2LNXDvJIgI1bRTlb9nkfzNwQFD9ev1jwK9lPT9Q,4157
|
|
147
|
+
fal-1.43.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
148
|
+
fal-1.43.0.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
|
|
149
|
+
fal-1.43.0.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
|
|
150
|
+
fal-1.43.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|