fal 1.9.3__py3-none-any.whl → 1.10.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 +12 -0
- fal/cli/api.py +72 -0
- fal/cli/apps.py +39 -1
- fal/cli/cli_nested_json.py +421 -0
- fal/cli/deploy.py +3 -3
- fal/cli/main.py +14 -1
- fal/cli/profile.py +14 -6
- fal/sdk.py +10 -0
- fal/toolkit/file/providers/fal.py +292 -2
- {fal-1.9.3.dist-info → fal-1.10.0.dist-info}/METADATA +2 -2
- {fal-1.9.3.dist-info → fal-1.10.0.dist-info}/RECORD +15 -13
- {fal-1.9.3.dist-info → fal-1.10.0.dist-info}/WHEEL +1 -1
- {fal-1.9.3.dist-info → fal-1.10.0.dist-info}/entry_points.txt +0 -0
- {fal-1.9.3.dist-info → fal-1.10.0.dist-info}/top_level.txt +0 -0
fal/_fal_version.py
CHANGED
fal/api.py
CHANGED
|
@@ -53,6 +53,7 @@ from fal.exceptions import (
|
|
|
53
53
|
from fal.exceptions._cuda import _is_cuda_oom_exception
|
|
54
54
|
from fal.logging.isolate import IsolateLogPrinter
|
|
55
55
|
from fal.sdk import (
|
|
56
|
+
FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER,
|
|
56
57
|
FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
|
|
57
58
|
FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
58
59
|
FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
|
|
@@ -400,6 +401,7 @@ class FalServerlessHost(Host):
|
|
|
400
401
|
"keep_alive",
|
|
401
402
|
"max_concurrency",
|
|
402
403
|
"min_concurrency",
|
|
404
|
+
"concurrency_buffer",
|
|
403
405
|
"max_multiplexing",
|
|
404
406
|
"setup_function",
|
|
405
407
|
"metadata",
|
|
@@ -451,6 +453,7 @@ class FalServerlessHost(Host):
|
|
|
451
453
|
scheduler_options = options.host.get("_scheduler_options", None)
|
|
452
454
|
max_concurrency = options.host.get("max_concurrency")
|
|
453
455
|
min_concurrency = options.host.get("min_concurrency")
|
|
456
|
+
concurrency_buffer = options.host.get("concurrency_buffer")
|
|
454
457
|
max_multiplexing = options.host.get("max_multiplexing")
|
|
455
458
|
exposed_port = options.get_exposed_port()
|
|
456
459
|
request_timeout = options.host.get("request_timeout")
|
|
@@ -466,6 +469,7 @@ class FalServerlessHost(Host):
|
|
|
466
469
|
max_multiplexing=max_multiplexing,
|
|
467
470
|
max_concurrency=max_concurrency,
|
|
468
471
|
min_concurrency=min_concurrency,
|
|
472
|
+
concurrency_buffer=concurrency_buffer,
|
|
469
473
|
request_timeout=request_timeout,
|
|
470
474
|
startup_timeout=startup_timeout,
|
|
471
475
|
)
|
|
@@ -523,6 +527,7 @@ class FalServerlessHost(Host):
|
|
|
523
527
|
keep_alive = options.host.get("keep_alive", FAL_SERVERLESS_DEFAULT_KEEP_ALIVE)
|
|
524
528
|
max_concurrency = options.host.get("max_concurrency")
|
|
525
529
|
min_concurrency = options.host.get("min_concurrency")
|
|
530
|
+
concurrency_buffer = options.host.get("concurrency_buffer")
|
|
526
531
|
max_multiplexing = options.host.get("max_multiplexing")
|
|
527
532
|
base_image = options.host.get("_base_image", None)
|
|
528
533
|
scheduler = options.host.get("_scheduler", None)
|
|
@@ -542,6 +547,7 @@ class FalServerlessHost(Host):
|
|
|
542
547
|
max_multiplexing=max_multiplexing,
|
|
543
548
|
max_concurrency=max_concurrency,
|
|
544
549
|
min_concurrency=min_concurrency,
|
|
550
|
+
concurrency_buffer=concurrency_buffer,
|
|
545
551
|
request_timeout=request_timeout,
|
|
546
552
|
startup_timeout=startup_timeout,
|
|
547
553
|
)
|
|
@@ -709,6 +715,7 @@ def function(
|
|
|
709
715
|
keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
|
|
710
716
|
max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
711
717
|
min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
|
|
718
|
+
concurrency_buffer: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER,
|
|
712
719
|
request_timeout: int | None = None,
|
|
713
720
|
startup_timeout: int | None = None,
|
|
714
721
|
setup_function: Callable[..., None] | None = None,
|
|
@@ -737,6 +744,7 @@ def function(
|
|
|
737
744
|
keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
|
|
738
745
|
max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
739
746
|
min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
|
|
747
|
+
concurrency_buffer: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER,
|
|
740
748
|
request_timeout: int | None = None,
|
|
741
749
|
startup_timeout: int | None = None,
|
|
742
750
|
setup_function: Callable[..., None] | None = None,
|
|
@@ -815,6 +823,7 @@ def function(
|
|
|
815
823
|
keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
|
|
816
824
|
max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
817
825
|
min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
|
|
826
|
+
concurrency_buffer: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER,
|
|
818
827
|
request_timeout: int | None = None,
|
|
819
828
|
startup_timeout: int | None = None,
|
|
820
829
|
setup_function: Callable[..., None] | None = None,
|
|
@@ -848,6 +857,7 @@ def function(
|
|
|
848
857
|
keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
|
|
849
858
|
max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
850
859
|
min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
|
|
860
|
+
concurrency_buffer: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER,
|
|
851
861
|
request_timeout: int | None = None,
|
|
852
862
|
startup_timeout: int | None = None,
|
|
853
863
|
setup_function: Callable[..., None] | None = None,
|
|
@@ -875,6 +885,7 @@ def function(
|
|
|
875
885
|
keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
|
|
876
886
|
max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
877
887
|
min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
|
|
888
|
+
concurrency_buffer: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER,
|
|
878
889
|
request_timeout: int | None = None,
|
|
879
890
|
startup_timeout: int | None = None,
|
|
880
891
|
setup_function: Callable[..., None] | None = None,
|
|
@@ -902,6 +913,7 @@ def function(
|
|
|
902
913
|
keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
|
|
903
914
|
max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
|
|
904
915
|
min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
|
|
916
|
+
concurrency_buffer: int = FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER,
|
|
905
917
|
request_timeout: int | None = None,
|
|
906
918
|
startup_timeout: int | None = None,
|
|
907
919
|
setup_function: Callable[..., None] | None = None,
|
fal/cli/api.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
|
|
5
|
+
import fal.apps
|
|
6
|
+
|
|
7
|
+
# = or := only
|
|
8
|
+
KV_SPLIT_RE = re.compile(r"(=|:=)")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _api(args):
|
|
12
|
+
"""Handle the api command execution."""
|
|
13
|
+
from rich.console import Group
|
|
14
|
+
from rich.live import Live
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.text import Text
|
|
17
|
+
|
|
18
|
+
from . import cli_nested_json
|
|
19
|
+
|
|
20
|
+
params = [KV_SPLIT_RE.split(param) for param in args.params]
|
|
21
|
+
params = cli_nested_json.interpret_nested_json( # type: ignore
|
|
22
|
+
[(key, value) for key, _, value in params]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
handle = fal.apps.submit(args.model_id, params) # type: ignore
|
|
26
|
+
logs = [] # type: ignore
|
|
27
|
+
|
|
28
|
+
with Live(auto_refresh=False) as live:
|
|
29
|
+
for event in handle.iter_events(logs=True):
|
|
30
|
+
if isinstance(event, fal.apps.Queued):
|
|
31
|
+
status = Text(f"⏳ Queued (position: {event.position})", style="yellow")
|
|
32
|
+
elif isinstance(event, fal.apps.InProgress):
|
|
33
|
+
status = Text("🔄 In Progress", style="blue")
|
|
34
|
+
if event.logs:
|
|
35
|
+
logs.extend(log.get("message", str(log)) for log in event.logs)
|
|
36
|
+
logs = logs[-10:] # Keep only last 10 logs
|
|
37
|
+
else:
|
|
38
|
+
status = Text("✅ Done", style="green")
|
|
39
|
+
|
|
40
|
+
status_panel = Panel(status, title="Status")
|
|
41
|
+
logs_panel = Panel("\n".join(logs), title="Logs")
|
|
42
|
+
|
|
43
|
+
live.update(Group(status_panel, logs_panel))
|
|
44
|
+
live.refresh()
|
|
45
|
+
|
|
46
|
+
# Show final result
|
|
47
|
+
result = handle.get()
|
|
48
|
+
live.update(rich.pretty.Pretty(result))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def add_parser(main_subparsers, parents):
|
|
52
|
+
"""Add the api command to the main parser."""
|
|
53
|
+
api_help = "Call a fal API endpoint directly"
|
|
54
|
+
parser = main_subparsers.add_parser(
|
|
55
|
+
"api",
|
|
56
|
+
description=api_help,
|
|
57
|
+
help=api_help,
|
|
58
|
+
parents=parents,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"model_id",
|
|
63
|
+
help="Name of the Model ID to call",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"params",
|
|
68
|
+
nargs="*",
|
|
69
|
+
help="Key-value pairs (e.g. key=value or nested[a][b]=value)",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
parser.set_defaults(func=_api)
|
fal/cli/apps.py
CHANGED
|
@@ -17,6 +17,7 @@ def _apps_table(apps: list[AliasInfo]):
|
|
|
17
17
|
table.add_column("Auth")
|
|
18
18
|
table.add_column("Min Concurrency")
|
|
19
19
|
table.add_column("Max Concurrency")
|
|
20
|
+
table.add_column("Concurrency Buffer")
|
|
20
21
|
table.add_column("Max Multiplexing")
|
|
21
22
|
table.add_column("Keep Alive")
|
|
22
23
|
table.add_column("Request Timeout")
|
|
@@ -32,6 +33,7 @@ def _apps_table(apps: list[AliasInfo]):
|
|
|
32
33
|
app.auth_mode,
|
|
33
34
|
str(app.min_concurrency),
|
|
34
35
|
str(app.max_concurrency),
|
|
36
|
+
str(app.concurrency_buffer),
|
|
35
37
|
str(app.max_multiplexing),
|
|
36
38
|
str(app.keep_alive),
|
|
37
39
|
str(app.request_timeout),
|
|
@@ -50,6 +52,15 @@ def _list(args):
|
|
|
50
52
|
client = FalServerlessClient(args.host)
|
|
51
53
|
with client.connect() as connection:
|
|
52
54
|
apps = connection.list_aliases()
|
|
55
|
+
|
|
56
|
+
if args.filter:
|
|
57
|
+
apps = [app for app in apps if args.filter in app.alias]
|
|
58
|
+
|
|
59
|
+
if args.sort_by_runners:
|
|
60
|
+
apps.sort(key=lambda x: x.active_runners)
|
|
61
|
+
else:
|
|
62
|
+
apps.sort(key=lambda x: x.alias)
|
|
63
|
+
|
|
53
64
|
table = _apps_table(apps)
|
|
54
65
|
|
|
55
66
|
args.console.print(table)
|
|
@@ -63,6 +74,16 @@ def _add_list_parser(subparsers, parents):
|
|
|
63
74
|
help=list_help,
|
|
64
75
|
parents=parents,
|
|
65
76
|
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--sort-by-runners",
|
|
79
|
+
action="store_true",
|
|
80
|
+
help="Sort by number of runners ascending",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--filter",
|
|
84
|
+
type=str,
|
|
85
|
+
help="Filter applications by alias contents",
|
|
86
|
+
)
|
|
66
87
|
parser.set_defaults(func=_list)
|
|
67
88
|
|
|
68
89
|
|
|
@@ -130,6 +151,7 @@ def _scale(args):
|
|
|
130
151
|
and args.max_multiplexing is None
|
|
131
152
|
and args.max_concurrency is None
|
|
132
153
|
and args.min_concurrency is None
|
|
154
|
+
and args.concurrency_buffer is None
|
|
133
155
|
and args.request_timeout is None
|
|
134
156
|
and args.startup_timeout is None
|
|
135
157
|
and args.machine_types is None
|
|
@@ -144,6 +166,7 @@ def _scale(args):
|
|
|
144
166
|
max_multiplexing=args.max_multiplexing,
|
|
145
167
|
max_concurrency=args.max_concurrency,
|
|
146
168
|
min_concurrency=args.min_concurrency,
|
|
169
|
+
concurrency_buffer=args.concurrency_buffer,
|
|
147
170
|
request_timeout=args.request_timeout,
|
|
148
171
|
startup_timeout=args.startup_timeout,
|
|
149
172
|
machine_types=args.machine_types,
|
|
@@ -186,6 +209,11 @@ def _add_scale_parser(subparsers, parents):
|
|
|
186
209
|
type=int,
|
|
187
210
|
help="Minimum concurrency",
|
|
188
211
|
)
|
|
212
|
+
parser.add_argument(
|
|
213
|
+
"--concurrency-buffer",
|
|
214
|
+
type=int,
|
|
215
|
+
help="Concurrency buffer",
|
|
216
|
+
)
|
|
189
217
|
parser.add_argument(
|
|
190
218
|
"--request-timeout",
|
|
191
219
|
type=int,
|
|
@@ -258,13 +286,23 @@ def _runners(args):
|
|
|
258
286
|
table = Table()
|
|
259
287
|
table.add_column("Runner ID")
|
|
260
288
|
table.add_column("In Flight Requests")
|
|
261
|
-
table.add_column("
|
|
289
|
+
table.add_column("Missing Leases")
|
|
290
|
+
table.add_column("Expires In")
|
|
262
291
|
table.add_column("Uptime")
|
|
263
292
|
|
|
264
293
|
for runner in runners:
|
|
294
|
+
num_leases_with_request = len(
|
|
295
|
+
[
|
|
296
|
+
lease
|
|
297
|
+
for lease in runner.external_metadata.get("leases", [])
|
|
298
|
+
if lease.get("request_id") is not None
|
|
299
|
+
]
|
|
300
|
+
)
|
|
301
|
+
|
|
265
302
|
table.add_row(
|
|
266
303
|
runner.runner_id,
|
|
267
304
|
str(runner.in_flight_requests),
|
|
305
|
+
str(runner.in_flight_requests - num_leases_with_request),
|
|
268
306
|
(
|
|
269
307
|
"N/A (active)"
|
|
270
308
|
if runner.expiration_countdown is None
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# Originally from https://github.com/httpie/cli/blob/master/httpie/cli/nested_json/interpret.py
|
|
2
|
+
#
|
|
3
|
+
# Copyright © 2012-2022 Jakub Roztocil <jakub@roztocil.co>
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
# this list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# 2. Redistributions in binary form must reproduce the above copyright
|
|
12
|
+
# notice, this list of conditions and the following disclaimer in the
|
|
13
|
+
# documentation and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# 3. Neither the name of the copyright holder nor the names of its contributors
|
|
16
|
+
# may be used to endorse or promote products derived from this software
|
|
17
|
+
# without specific prior written permission.
|
|
18
|
+
#
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
20
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
21
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
23
|
+
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
24
|
+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
25
|
+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
26
|
+
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
27
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
28
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from collections.abc import Iterable, Iterator
|
|
33
|
+
from enum import Enum, auto
|
|
34
|
+
from typing import Any, NamedTuple, Union
|
|
35
|
+
|
|
36
|
+
EMPTY_STRING = ""
|
|
37
|
+
HIGHLIGHTER = "^"
|
|
38
|
+
OPEN_BRACKET = "["
|
|
39
|
+
CLOSE_BRACKET = "]"
|
|
40
|
+
BACKSLASH = "\\"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TokenKind(Enum):
|
|
44
|
+
TEXT = auto()
|
|
45
|
+
NUMBER = auto()
|
|
46
|
+
LEFT_BRACKET = auto()
|
|
47
|
+
RIGHT_BRACKET = auto()
|
|
48
|
+
PSEUDO = auto() # Not a real token, use when representing location only.
|
|
49
|
+
|
|
50
|
+
def to_name(self) -> str:
|
|
51
|
+
for key, value in OPERATORS.items():
|
|
52
|
+
if value is self:
|
|
53
|
+
return repr(key)
|
|
54
|
+
return "a " + self.name.lower()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
OPERATORS = {
|
|
58
|
+
OPEN_BRACKET: TokenKind.LEFT_BRACKET,
|
|
59
|
+
CLOSE_BRACKET: TokenKind.RIGHT_BRACKET,
|
|
60
|
+
}
|
|
61
|
+
SPECIAL_CHARS = OPERATORS.keys() | {BACKSLASH}
|
|
62
|
+
LITERAL_TOKENS = [
|
|
63
|
+
TokenKind.TEXT,
|
|
64
|
+
TokenKind.NUMBER,
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Token(NamedTuple):
|
|
69
|
+
kind: TokenKind
|
|
70
|
+
value: str | int
|
|
71
|
+
start: int
|
|
72
|
+
end: int
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PathAction(Enum):
|
|
76
|
+
KEY = auto()
|
|
77
|
+
INDEX = auto()
|
|
78
|
+
APPEND = auto()
|
|
79
|
+
# Pseudo action, used by the interpreter
|
|
80
|
+
SET = auto()
|
|
81
|
+
|
|
82
|
+
def to_string(self) -> str:
|
|
83
|
+
return self.name.lower()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Path:
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
kind: PathAction,
|
|
90
|
+
accessor: str | int | None = None,
|
|
91
|
+
tokens: list[Token] | None = None,
|
|
92
|
+
is_root: bool = False,
|
|
93
|
+
):
|
|
94
|
+
self.kind = kind
|
|
95
|
+
self.accessor = accessor
|
|
96
|
+
self.tokens = tokens or []
|
|
97
|
+
self.is_root = is_root
|
|
98
|
+
|
|
99
|
+
def reconstruct(self) -> str:
|
|
100
|
+
if self.kind is PathAction.KEY:
|
|
101
|
+
if self.is_root:
|
|
102
|
+
return str(self.accessor)
|
|
103
|
+
return OPEN_BRACKET + str(self.accessor) + CLOSE_BRACKET
|
|
104
|
+
elif self.kind is PathAction.INDEX:
|
|
105
|
+
return OPEN_BRACKET + str(self.accessor) + CLOSE_BRACKET
|
|
106
|
+
elif self.kind is PathAction.APPEND:
|
|
107
|
+
return OPEN_BRACKET + CLOSE_BRACKET
|
|
108
|
+
else:
|
|
109
|
+
raise ValueError(f"Unexpected path action: {self.kind}")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class NestedJSONArray(list):
|
|
113
|
+
"""Denotes a top-level JSON array."""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class NestedJSONSyntaxError(ValueError):
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
source: str,
|
|
120
|
+
token: Token | None,
|
|
121
|
+
message: str,
|
|
122
|
+
message_kind: str = "Syntax",
|
|
123
|
+
) -> None:
|
|
124
|
+
self.source = source
|
|
125
|
+
self.token = token
|
|
126
|
+
self.message = message
|
|
127
|
+
self.message_kind = message_kind
|
|
128
|
+
|
|
129
|
+
def __str__(self):
|
|
130
|
+
lines = [f"Nested JSON Error {self.message_kind} Error: {self.message}"]
|
|
131
|
+
if self.token is not None:
|
|
132
|
+
lines.append(self.source)
|
|
133
|
+
lines.append(
|
|
134
|
+
" " * self.token.start
|
|
135
|
+
+ HIGHLIGHTER * (self.token.end - self.token.start)
|
|
136
|
+
)
|
|
137
|
+
return "\n".join(lines)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def parse(source: str) -> Iterator[Path]:
|
|
141
|
+
"""
|
|
142
|
+
start: root_path path*
|
|
143
|
+
root_path: (literal | index_path | append_path)
|
|
144
|
+
literal: TEXT | NUMBER
|
|
145
|
+
|
|
146
|
+
path:
|
|
147
|
+
key_path
|
|
148
|
+
| index_path
|
|
149
|
+
| append_path
|
|
150
|
+
key_path: LEFT_BRACKET TEXT RIGHT_BRACKET
|
|
151
|
+
index_path: LEFT_BRACKET NUMBER RIGHT_BRACKET
|
|
152
|
+
append_path: LEFT_BRACKET RIGHT_BRACKET
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
tokens = list(tokenize(source))
|
|
157
|
+
cursor = 0
|
|
158
|
+
|
|
159
|
+
def can_advance():
|
|
160
|
+
return cursor < len(tokens)
|
|
161
|
+
|
|
162
|
+
# noinspection PyShadowingNames
|
|
163
|
+
def expect(*kinds):
|
|
164
|
+
nonlocal cursor
|
|
165
|
+
assert kinds
|
|
166
|
+
if can_advance():
|
|
167
|
+
token = tokens[cursor]
|
|
168
|
+
cursor += 1
|
|
169
|
+
if token.kind in kinds:
|
|
170
|
+
return token
|
|
171
|
+
elif tokens:
|
|
172
|
+
token = tokens[-1]._replace(
|
|
173
|
+
start=tokens[-1].end + 0,
|
|
174
|
+
end=tokens[-1].end + 1,
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
token = None
|
|
178
|
+
if len(kinds) == 1:
|
|
179
|
+
suffix = kinds[0].to_name()
|
|
180
|
+
else:
|
|
181
|
+
suffix = ", ".join(kind.to_name() for kind in kinds[:-1])
|
|
182
|
+
suffix += " or " + kinds[-1].to_name()
|
|
183
|
+
message = f"Expecting {suffix}"
|
|
184
|
+
raise NestedJSONSyntaxError(source, token, message)
|
|
185
|
+
|
|
186
|
+
# noinspection PyShadowingNames
|
|
187
|
+
def parse_root():
|
|
188
|
+
tokens = []
|
|
189
|
+
if not can_advance():
|
|
190
|
+
return Path(kind=PathAction.KEY, accessor=EMPTY_STRING, is_root=True)
|
|
191
|
+
# (literal | index_path | append_path)?
|
|
192
|
+
token = expect(*LITERAL_TOKENS, TokenKind.LEFT_BRACKET)
|
|
193
|
+
tokens.append(token)
|
|
194
|
+
if token.kind in LITERAL_TOKENS:
|
|
195
|
+
action = PathAction.KEY
|
|
196
|
+
value = str(token.value)
|
|
197
|
+
elif token.kind is TokenKind.LEFT_BRACKET:
|
|
198
|
+
token = expect(TokenKind.NUMBER, TokenKind.RIGHT_BRACKET)
|
|
199
|
+
tokens.append(token)
|
|
200
|
+
if token.kind is TokenKind.NUMBER:
|
|
201
|
+
action = PathAction.INDEX
|
|
202
|
+
value = token.value
|
|
203
|
+
tokens.append(expect(TokenKind.RIGHT_BRACKET))
|
|
204
|
+
elif token.kind is TokenKind.RIGHT_BRACKET:
|
|
205
|
+
action = PathAction.APPEND
|
|
206
|
+
value = None
|
|
207
|
+
else:
|
|
208
|
+
assert_cant_happen()
|
|
209
|
+
else:
|
|
210
|
+
assert_cant_happen()
|
|
211
|
+
# noinspection PyUnboundLocalVariable
|
|
212
|
+
return Path(kind=action, accessor=value, tokens=tokens, is_root=True)
|
|
213
|
+
|
|
214
|
+
yield parse_root()
|
|
215
|
+
|
|
216
|
+
# path*
|
|
217
|
+
while can_advance():
|
|
218
|
+
path_tokens = [expect(TokenKind.LEFT_BRACKET)]
|
|
219
|
+
token = expect(TokenKind.TEXT, TokenKind.NUMBER, TokenKind.RIGHT_BRACKET)
|
|
220
|
+
path_tokens.append(token)
|
|
221
|
+
if token.kind is TokenKind.RIGHT_BRACKET:
|
|
222
|
+
path = Path(PathAction.APPEND, tokens=path_tokens)
|
|
223
|
+
elif token.kind is TokenKind.TEXT:
|
|
224
|
+
path = Path(PathAction.KEY, token.value, tokens=path_tokens)
|
|
225
|
+
path_tokens.append(expect(TokenKind.RIGHT_BRACKET))
|
|
226
|
+
elif token.kind is TokenKind.NUMBER:
|
|
227
|
+
path = Path(PathAction.INDEX, token.value, tokens=path_tokens)
|
|
228
|
+
path_tokens.append(expect(TokenKind.RIGHT_BRACKET))
|
|
229
|
+
else:
|
|
230
|
+
assert_cant_happen()
|
|
231
|
+
# noinspection PyUnboundLocalVariable
|
|
232
|
+
yield path
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def tokenize(source: str) -> Iterator[Token]:
|
|
236
|
+
cursor = 0
|
|
237
|
+
backslashes = 0
|
|
238
|
+
buffer: list[str] = []
|
|
239
|
+
|
|
240
|
+
def send_buffer() -> Iterator[Token]:
|
|
241
|
+
nonlocal backslashes
|
|
242
|
+
if not buffer:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
value = "".join(buffer)
|
|
246
|
+
kind = TokenKind.TEXT
|
|
247
|
+
if not backslashes:
|
|
248
|
+
for variation, kind in [
|
|
249
|
+
(int, TokenKind.NUMBER),
|
|
250
|
+
(check_escaped_int, TokenKind.TEXT),
|
|
251
|
+
]:
|
|
252
|
+
try:
|
|
253
|
+
value = variation(value) # type: ignore[operator]
|
|
254
|
+
except ValueError:
|
|
255
|
+
continue
|
|
256
|
+
else:
|
|
257
|
+
break
|
|
258
|
+
yield Token(
|
|
259
|
+
kind=kind,
|
|
260
|
+
value=value,
|
|
261
|
+
start=cursor - (len(buffer) + backslashes),
|
|
262
|
+
end=cursor,
|
|
263
|
+
)
|
|
264
|
+
buffer.clear()
|
|
265
|
+
backslashes = 0
|
|
266
|
+
|
|
267
|
+
def can_advance() -> bool:
|
|
268
|
+
return cursor < len(source)
|
|
269
|
+
|
|
270
|
+
while can_advance():
|
|
271
|
+
index = source[cursor]
|
|
272
|
+
if index in OPERATORS:
|
|
273
|
+
yield from send_buffer()
|
|
274
|
+
yield Token(OPERATORS[index], index, cursor, cursor + 1)
|
|
275
|
+
elif index == BACKSLASH and can_advance():
|
|
276
|
+
if source[cursor + 1] in SPECIAL_CHARS:
|
|
277
|
+
backslashes += 1
|
|
278
|
+
else:
|
|
279
|
+
buffer.append(index)
|
|
280
|
+
buffer.append(source[cursor + 1])
|
|
281
|
+
cursor += 1
|
|
282
|
+
else:
|
|
283
|
+
buffer.append(index)
|
|
284
|
+
cursor += 1
|
|
285
|
+
|
|
286
|
+
yield from send_buffer()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def check_escaped_int(value: str) -> str:
|
|
290
|
+
if not value.startswith(BACKSLASH):
|
|
291
|
+
raise ValueError("Not an escaped int")
|
|
292
|
+
try:
|
|
293
|
+
int(value[1:])
|
|
294
|
+
except ValueError as exc:
|
|
295
|
+
raise ValueError("Not an escaped int") from exc
|
|
296
|
+
else:
|
|
297
|
+
return value[1:]
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def assert_cant_happen():
|
|
301
|
+
raise ValueError("Unexpected value")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
JSONType = type[Union[dict, list, int, float, str]]
|
|
305
|
+
JSON_TYPE_MAPPING = {
|
|
306
|
+
dict: "object",
|
|
307
|
+
list: "array",
|
|
308
|
+
int: "number",
|
|
309
|
+
float: "number",
|
|
310
|
+
str: "string",
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def interpret_nested_json(pairs: Iterable[tuple[str, str]]) -> dict:
|
|
315
|
+
context = None
|
|
316
|
+
for key, value in pairs:
|
|
317
|
+
context = interpret(context, key, value)
|
|
318
|
+
return wrap_with_dict(context)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def interpret(context: Any, key: str, value: Any) -> Any:
|
|
322
|
+
cursor = context
|
|
323
|
+
paths = list(parse(key))
|
|
324
|
+
paths.append(Path(PathAction.SET, value))
|
|
325
|
+
|
|
326
|
+
# noinspection PyShadowingNames
|
|
327
|
+
def type_check(index: int, path: Path, expected_type: JSONType):
|
|
328
|
+
if not isinstance(cursor, expected_type):
|
|
329
|
+
if path.tokens:
|
|
330
|
+
pseudo_token = Token(
|
|
331
|
+
kind=TokenKind.PSEUDO,
|
|
332
|
+
value="",
|
|
333
|
+
start=path.tokens[0].start,
|
|
334
|
+
end=path.tokens[-1].end,
|
|
335
|
+
)
|
|
336
|
+
else:
|
|
337
|
+
pseudo_token = None
|
|
338
|
+
cursor_type = JSON_TYPE_MAPPING.get(type(cursor), type(cursor).__name__)
|
|
339
|
+
required_type = JSON_TYPE_MAPPING[expected_type]
|
|
340
|
+
message = f"Cannot perform {path.kind.to_string()!r} based access on "
|
|
341
|
+
message += repr("".join(path.reconstruct() for path in paths[:index]))
|
|
342
|
+
message += f" which has a type of {cursor_type!r} but this operation"
|
|
343
|
+
message += f" requires a type of {required_type!r}."
|
|
344
|
+
raise NestedJSONSyntaxError(
|
|
345
|
+
source=key,
|
|
346
|
+
token=pseudo_token,
|
|
347
|
+
message=message,
|
|
348
|
+
message_kind="Type",
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
def object_for(kind: PathAction) -> Any:
|
|
352
|
+
if kind is PathAction.KEY:
|
|
353
|
+
return {}
|
|
354
|
+
elif kind in {PathAction.INDEX, PathAction.APPEND}:
|
|
355
|
+
return []
|
|
356
|
+
else:
|
|
357
|
+
assert_cant_happen()
|
|
358
|
+
|
|
359
|
+
for index, (path, next_path) in enumerate(zip(paths, paths[1:])):
|
|
360
|
+
# If there is no context yet, set it.
|
|
361
|
+
if cursor is None:
|
|
362
|
+
context = cursor = object_for(path.kind)
|
|
363
|
+
if path.kind is PathAction.KEY:
|
|
364
|
+
type_check(index, path, dict)
|
|
365
|
+
if next_path.kind is PathAction.SET:
|
|
366
|
+
cursor[path.accessor] = next_path.accessor
|
|
367
|
+
break
|
|
368
|
+
cursor = cursor.setdefault(path.accessor, object_for(next_path.kind))
|
|
369
|
+
elif path.kind is PathAction.INDEX:
|
|
370
|
+
type_check(index, path, list)
|
|
371
|
+
assert isinstance(path.accessor, int)
|
|
372
|
+
if path.accessor < 0:
|
|
373
|
+
raise NestedJSONSyntaxError(
|
|
374
|
+
source=key,
|
|
375
|
+
token=path.tokens[1],
|
|
376
|
+
message="Negative indexes are not supported.",
|
|
377
|
+
message_kind="Value",
|
|
378
|
+
)
|
|
379
|
+
cursor.extend([None] * (path.accessor - len(cursor) + 1))
|
|
380
|
+
if next_path.kind is PathAction.SET:
|
|
381
|
+
cursor[path.accessor] = next_path.accessor
|
|
382
|
+
break
|
|
383
|
+
if cursor[path.accessor] is None:
|
|
384
|
+
cursor[path.accessor] = object_for(next_path.kind)
|
|
385
|
+
cursor = cursor[path.accessor]
|
|
386
|
+
elif path.kind is PathAction.APPEND:
|
|
387
|
+
type_check(index, path, list)
|
|
388
|
+
if next_path.kind is PathAction.SET:
|
|
389
|
+
cursor.append(next_path.accessor)
|
|
390
|
+
break
|
|
391
|
+
cursor.append(object_for(next_path.kind))
|
|
392
|
+
cursor = cursor[-1]
|
|
393
|
+
else:
|
|
394
|
+
assert_cant_happen()
|
|
395
|
+
|
|
396
|
+
return context
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def wrap_with_dict(context):
|
|
400
|
+
if context is None:
|
|
401
|
+
return {}
|
|
402
|
+
elif isinstance(context, list):
|
|
403
|
+
return {
|
|
404
|
+
EMPTY_STRING: NestedJSONArray(context),
|
|
405
|
+
}
|
|
406
|
+
else:
|
|
407
|
+
assert isinstance(context, dict)
|
|
408
|
+
return context
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def unwrap_top_level_list_if_needed(data: dict):
|
|
412
|
+
"""
|
|
413
|
+
Propagate the top-level list, if that's what we got.
|
|
414
|
+
|
|
415
|
+
"""
|
|
416
|
+
if len(data) == 1:
|
|
417
|
+
key, value = list(data.items())[0]
|
|
418
|
+
if isinstance(value, NestedJSONArray):
|
|
419
|
+
assert key == EMPTY_STRING
|
|
420
|
+
return value
|
|
421
|
+
return data
|
fal/cli/deploy.py
CHANGED
|
@@ -235,9 +235,9 @@ def add_parser(main_subparsers, parents):
|
|
|
235
235
|
"--no-scale",
|
|
236
236
|
action="store_true",
|
|
237
237
|
help=(
|
|
238
|
-
"Use min_concurrency/max_concurrency/max_multiplexing
|
|
239
|
-
"deployment of application with this name, if exists.
|
|
240
|
-
"use the values from the application code."
|
|
238
|
+
"Use min_concurrency/max_concurrency/concurrency_buffer/max_multiplexing "
|
|
239
|
+
"from previous deployment of application with this name, if exists. "
|
|
240
|
+
"Otherwise will use the values from the application code."
|
|
241
241
|
),
|
|
242
242
|
)
|
|
243
243
|
|
fal/cli/main.py
CHANGED
|
@@ -6,7 +6,19 @@ from fal import __version__
|
|
|
6
6
|
from fal.console import console
|
|
7
7
|
from fal.console.icons import CROSS_ICON
|
|
8
8
|
|
|
9
|
-
from . import
|
|
9
|
+
from . import (
|
|
10
|
+
api,
|
|
11
|
+
apps,
|
|
12
|
+
auth,
|
|
13
|
+
create,
|
|
14
|
+
deploy,
|
|
15
|
+
doctor,
|
|
16
|
+
keys,
|
|
17
|
+
profile,
|
|
18
|
+
run,
|
|
19
|
+
runners,
|
|
20
|
+
secrets,
|
|
21
|
+
)
|
|
10
22
|
from .debug import debugtools, get_debug_parser
|
|
11
23
|
from .parser import FalParser, FalParserExit
|
|
12
24
|
|
|
@@ -32,6 +44,7 @@ def _get_main_parser() -> argparse.ArgumentParser:
|
|
|
32
44
|
)
|
|
33
45
|
|
|
34
46
|
for cmd in [
|
|
47
|
+
api,
|
|
35
48
|
auth,
|
|
36
49
|
apps,
|
|
37
50
|
deploy,
|
fal/cli/profile.py
CHANGED
|
@@ -26,6 +26,10 @@ def _set(args):
|
|
|
26
26
|
config.set_internal("profile", args.PROFILE)
|
|
27
27
|
args.console.print(f"Default profile set to [cyan]{args.PROFILE}[/].")
|
|
28
28
|
config.profile = args.PROFILE
|
|
29
|
+
if not config.get("key"):
|
|
30
|
+
args.console.print(
|
|
31
|
+
"No key set for profile. Use [bold]fal profile key[/] to set a key."
|
|
32
|
+
)
|
|
29
33
|
config.save()
|
|
30
34
|
|
|
31
35
|
|
|
@@ -39,8 +43,16 @@ def _unset(args):
|
|
|
39
43
|
|
|
40
44
|
def _key_set(args):
|
|
41
45
|
config = Config()
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
|
|
47
|
+
while True:
|
|
48
|
+
key = input("Enter the key: ")
|
|
49
|
+
if ":" in key:
|
|
50
|
+
break
|
|
51
|
+
args.console.print(
|
|
52
|
+
"[red]Invalid key. The key must be in the format [bold]key:value[/].[/]"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
config.set("key", key)
|
|
44
56
|
args.console.print(f"Key set for profile [cyan]{config.profile}[/].")
|
|
45
57
|
config.save()
|
|
46
58
|
|
|
@@ -109,10 +121,6 @@ def add_parser(main_subparsers, parents):
|
|
|
109
121
|
help=key_set_help,
|
|
110
122
|
parents=parents,
|
|
111
123
|
)
|
|
112
|
-
key_set_parser.add_argument(
|
|
113
|
-
"KEY",
|
|
114
|
-
help="Key ID and secret separated by a colon.",
|
|
115
|
-
)
|
|
116
124
|
key_set_parser.set_defaults(func=_key_set)
|
|
117
125
|
|
|
118
126
|
delete_help = "Delete profile."
|
fal/sdk.py
CHANGED
|
@@ -29,6 +29,7 @@ _DEFAULT_SERIALIZATION_METHOD = "cloudpickle"
|
|
|
29
29
|
FAL_SERVERLESS_DEFAULT_KEEP_ALIVE = 10
|
|
30
30
|
FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING = 1
|
|
31
31
|
FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY = 0
|
|
32
|
+
FAL_SERVERLESS_DEFAULT_CONCURRENCY_BUFFER = 0
|
|
32
33
|
ALIAS_AUTH_MODES = ["public", "private", "shared"]
|
|
33
34
|
|
|
34
35
|
logger = get_logger(__name__)
|
|
@@ -197,6 +198,7 @@ class ApplicationInfo:
|
|
|
197
198
|
max_multiplexing: int
|
|
198
199
|
active_runners: int
|
|
199
200
|
min_concurrency: int
|
|
201
|
+
concurrency_buffer: int
|
|
200
202
|
machine_types: list[str]
|
|
201
203
|
request_timeout: int
|
|
202
204
|
startup_timeout: int
|
|
@@ -213,6 +215,7 @@ class AliasInfo:
|
|
|
213
215
|
max_multiplexing: int
|
|
214
216
|
active_runners: int
|
|
215
217
|
min_concurrency: int
|
|
218
|
+
concurrency_buffer: int
|
|
216
219
|
machine_types: list[str]
|
|
217
220
|
request_timeout: int
|
|
218
221
|
startup_timeout: int
|
|
@@ -323,6 +326,7 @@ def _from_grpc_application_info(
|
|
|
323
326
|
max_multiplexing=message.max_multiplexing,
|
|
324
327
|
active_runners=message.active_runners,
|
|
325
328
|
min_concurrency=message.min_concurrency,
|
|
329
|
+
concurrency_buffer=message.concurrency_buffer,
|
|
326
330
|
machine_types=list(message.machine_types),
|
|
327
331
|
request_timeout=message.request_timeout,
|
|
328
332
|
startup_timeout=message.startup_timeout,
|
|
@@ -350,6 +354,7 @@ def _from_grpc_alias_info(message: isolate_proto.AliasInfo) -> AliasInfo:
|
|
|
350
354
|
max_multiplexing=message.max_multiplexing,
|
|
351
355
|
active_runners=message.active_runners,
|
|
352
356
|
min_concurrency=message.min_concurrency,
|
|
357
|
+
concurrency_buffer=message.concurrency_buffer,
|
|
353
358
|
machine_types=list(message.machine_types),
|
|
354
359
|
request_timeout=message.request_timeout,
|
|
355
360
|
startup_timeout=message.startup_timeout,
|
|
@@ -423,6 +428,7 @@ class MachineRequirements:
|
|
|
423
428
|
max_concurrency: int | None = None
|
|
424
429
|
max_multiplexing: int | None = None
|
|
425
430
|
min_concurrency: int | None = None
|
|
431
|
+
concurrency_buffer: int | None = None
|
|
426
432
|
request_timeout: int | None = None
|
|
427
433
|
startup_timeout: int | None = None
|
|
428
434
|
|
|
@@ -540,6 +546,7 @@ class FalServerlessConnection:
|
|
|
540
546
|
),
|
|
541
547
|
max_concurrency=machine_requirements.max_concurrency,
|
|
542
548
|
min_concurrency=machine_requirements.min_concurrency,
|
|
549
|
+
concurrency_buffer=machine_requirements.concurrency_buffer,
|
|
543
550
|
max_multiplexing=machine_requirements.max_multiplexing,
|
|
544
551
|
request_timeout=machine_requirements.request_timeout,
|
|
545
552
|
startup_timeout=machine_requirements.startup_timeout,
|
|
@@ -586,6 +593,7 @@ class FalServerlessConnection:
|
|
|
586
593
|
max_multiplexing: int | None = None,
|
|
587
594
|
max_concurrency: int | None = None,
|
|
588
595
|
min_concurrency: int | None = None,
|
|
596
|
+
concurrency_buffer: int | None = None,
|
|
589
597
|
request_timeout: int | None = None,
|
|
590
598
|
startup_timeout: int | None = None,
|
|
591
599
|
valid_regions: list[str] | None = None,
|
|
@@ -597,6 +605,7 @@ class FalServerlessConnection:
|
|
|
597
605
|
max_multiplexing=max_multiplexing,
|
|
598
606
|
max_concurrency=max_concurrency,
|
|
599
607
|
min_concurrency=min_concurrency,
|
|
608
|
+
concurrency_buffer=concurrency_buffer,
|
|
600
609
|
request_timeout=request_timeout,
|
|
601
610
|
startup_timeout=startup_timeout,
|
|
602
611
|
valid_regions=valid_regions,
|
|
@@ -645,6 +654,7 @@ class FalServerlessConnection:
|
|
|
645
654
|
max_concurrency=machine_requirements.max_concurrency,
|
|
646
655
|
max_multiplexing=machine_requirements.max_multiplexing,
|
|
647
656
|
min_concurrency=machine_requirements.min_concurrency,
|
|
657
|
+
concurrency_buffer=machine_requirements.concurrency_buffer,
|
|
648
658
|
request_timeout=machine_requirements.request_timeout,
|
|
649
659
|
startup_timeout=machine_requirements.startup_timeout,
|
|
650
660
|
)
|
|
@@ -125,7 +125,9 @@ LIFECYCLE_PREFERENCE: VariableReference[dict[str, str] | None] = VariableReferen
|
|
|
125
125
|
@dataclass
|
|
126
126
|
class FalFileRepositoryBase(FileRepository):
|
|
127
127
|
@retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
|
|
128
|
-
def _save(
|
|
128
|
+
def _save(
|
|
129
|
+
self, file: FileData, storage_type: str, headers: dict[str, str] | None = None
|
|
130
|
+
) -> str:
|
|
129
131
|
key_creds = key_credentials()
|
|
130
132
|
if not key_creds:
|
|
131
133
|
raise FileUploadException("FAL_KEY must be set")
|
|
@@ -135,6 +137,7 @@ class FalFileRepositoryBase(FileRepository):
|
|
|
135
137
|
"Authorization": f"Key {key_id}:{key_secret}",
|
|
136
138
|
"Accept": "application/json",
|
|
137
139
|
"Content-Type": "application/json",
|
|
140
|
+
**(headers or {}),
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
grpc_host = os.environ.get("FAL_HOST", "api.alpha.fal.ai")
|
|
@@ -182,8 +185,236 @@ class FalFileRepositoryBase(FileRepository):
|
|
|
182
185
|
)
|
|
183
186
|
|
|
184
187
|
|
|
188
|
+
class MultipartUploadGCS:
|
|
189
|
+
MULTIPART_THRESHOLD = 100 * 1024 * 1024
|
|
190
|
+
MULTIPART_CHUNK_SIZE = 10 * 1024 * 1024
|
|
191
|
+
MULTIPART_MAX_CONCURRENCY = 10
|
|
192
|
+
|
|
193
|
+
def __init__(
|
|
194
|
+
self,
|
|
195
|
+
file_name: str,
|
|
196
|
+
chunk_size: int | None = None,
|
|
197
|
+
content_type: str | None = None,
|
|
198
|
+
max_concurrency: int | None = None,
|
|
199
|
+
) -> None:
|
|
200
|
+
self.file_name = file_name
|
|
201
|
+
self.chunk_size = chunk_size or self.MULTIPART_CHUNK_SIZE
|
|
202
|
+
self.content_type = content_type or "application/octet-stream"
|
|
203
|
+
self.max_concurrency = max_concurrency or self.MULTIPART_MAX_CONCURRENCY
|
|
204
|
+
|
|
205
|
+
self._access_url: str | None = None
|
|
206
|
+
self._upload_url: str | None = None
|
|
207
|
+
|
|
208
|
+
self._parts: list[dict] = []
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def access_url(self) -> str:
|
|
212
|
+
if not self._access_url:
|
|
213
|
+
raise FileUploadException("Upload not initiated")
|
|
214
|
+
return self._access_url
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def upload_url(self) -> str:
|
|
218
|
+
if not self._upload_url:
|
|
219
|
+
raise FileUploadException("Upload not initiated")
|
|
220
|
+
return self._upload_url
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def auth_headers(self) -> dict[str, str]:
|
|
224
|
+
fal_key = key_credentials()
|
|
225
|
+
if not fal_key:
|
|
226
|
+
raise FileUploadException("FAL_KEY must be set")
|
|
227
|
+
|
|
228
|
+
key_id, key_secret = fal_key
|
|
229
|
+
return {
|
|
230
|
+
"Authorization": f"Key {key_id}:{key_secret}",
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
def create(self):
|
|
234
|
+
grpc_host = os.environ.get("FAL_HOST", "api.alpha.fal.ai")
|
|
235
|
+
rest_host = grpc_host.replace("api", "rest", 1)
|
|
236
|
+
url = f"https://{rest_host}/storage/upload/initiate-multipart?storage_type=gcs"
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
req = Request(
|
|
240
|
+
url,
|
|
241
|
+
method="POST",
|
|
242
|
+
headers={
|
|
243
|
+
**self.auth_headers,
|
|
244
|
+
"Content-Type": "application/json",
|
|
245
|
+
"Accept": "application/json",
|
|
246
|
+
},
|
|
247
|
+
data=json.dumps(
|
|
248
|
+
{
|
|
249
|
+
"file_name": self.file_name,
|
|
250
|
+
"content_type": self.content_type,
|
|
251
|
+
}
|
|
252
|
+
).encode(),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
with urlopen(req) as response:
|
|
256
|
+
result = json.load(response)
|
|
257
|
+
self._access_url = result["file_url"]
|
|
258
|
+
self._upload_url = result["upload_url"]
|
|
259
|
+
|
|
260
|
+
except HTTPError as exc:
|
|
261
|
+
raise FileUploadException(
|
|
262
|
+
f"Error initiating upload. Status {exc.status}: {exc.reason}"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
@retry(max_retries=5, base_delay=1, backoff_type="exponential", jitter=True)
|
|
266
|
+
def upload_part(self, part_number: int, data: bytes) -> None:
|
|
267
|
+
initiate_upload_url = self.upload_url + f"/{part_number}"
|
|
268
|
+
req = Request(
|
|
269
|
+
initiate_upload_url,
|
|
270
|
+
method="POST",
|
|
271
|
+
headers=self.auth_headers,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
with urlopen(req) as response:
|
|
276
|
+
result = json.load(response)
|
|
277
|
+
upload_url = result["upload_url"]
|
|
278
|
+
except HTTPError as exc:
|
|
279
|
+
raise FileUploadException(
|
|
280
|
+
f"Error initiating part {part_number} to {initiate_upload_url}. "
|
|
281
|
+
f"Status {exc.status}: {exc.reason}"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
req = Request(
|
|
285
|
+
upload_url,
|
|
286
|
+
method="PUT",
|
|
287
|
+
data=data,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
with urlopen(req) as resp:
|
|
292
|
+
self._parts.append(
|
|
293
|
+
{
|
|
294
|
+
"part_number": part_number,
|
|
295
|
+
"etag": resp.headers["ETag"].strip('"'),
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
except HTTPError as exc:
|
|
299
|
+
raise FileUploadException(
|
|
300
|
+
f"Error uploading part {part_number} to {upload_url}. "
|
|
301
|
+
f"Status {exc.status}: {exc.reason}"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def complete(self) -> str:
|
|
305
|
+
url = self.upload_url + "/complete"
|
|
306
|
+
try:
|
|
307
|
+
req = Request(
|
|
308
|
+
url,
|
|
309
|
+
method="POST",
|
|
310
|
+
headers={
|
|
311
|
+
**self.auth_headers,
|
|
312
|
+
"Accept": "application/json",
|
|
313
|
+
"Content-Type": "application/json",
|
|
314
|
+
},
|
|
315
|
+
data=json.dumps(
|
|
316
|
+
{
|
|
317
|
+
"parts": self._parts,
|
|
318
|
+
}
|
|
319
|
+
).encode(),
|
|
320
|
+
)
|
|
321
|
+
with urlopen(req):
|
|
322
|
+
pass
|
|
323
|
+
except HTTPError as e:
|
|
324
|
+
raise FileUploadException(
|
|
325
|
+
f"Error completing upload {url}. Status {e.status}: {e.reason}"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
return self.access_url
|
|
329
|
+
|
|
330
|
+
@classmethod
|
|
331
|
+
def save(
|
|
332
|
+
cls,
|
|
333
|
+
file: FileData,
|
|
334
|
+
chunk_size: int | None = None,
|
|
335
|
+
max_concurrency: int | None = None,
|
|
336
|
+
):
|
|
337
|
+
import concurrent.futures
|
|
338
|
+
|
|
339
|
+
multipart = cls(
|
|
340
|
+
file.file_name,
|
|
341
|
+
chunk_size=chunk_size,
|
|
342
|
+
content_type=file.content_type,
|
|
343
|
+
max_concurrency=max_concurrency,
|
|
344
|
+
)
|
|
345
|
+
multipart.create()
|
|
346
|
+
|
|
347
|
+
parts = math.ceil(len(file.data) / multipart.chunk_size)
|
|
348
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
349
|
+
max_workers=multipart.max_concurrency
|
|
350
|
+
) as executor:
|
|
351
|
+
futures = []
|
|
352
|
+
for part_number in range(1, parts + 1):
|
|
353
|
+
start = (part_number - 1) * multipart.chunk_size
|
|
354
|
+
data = file.data[start : start + multipart.chunk_size]
|
|
355
|
+
futures.append(
|
|
356
|
+
executor.submit(multipart.upload_part, part_number, data)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
for future in concurrent.futures.as_completed(futures):
|
|
360
|
+
future.result()
|
|
361
|
+
|
|
362
|
+
return multipart.complete()
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def save_file(
|
|
366
|
+
cls,
|
|
367
|
+
file_path: str | Path,
|
|
368
|
+
chunk_size: int | None = None,
|
|
369
|
+
content_type: str | None = None,
|
|
370
|
+
max_concurrency: int | None = None,
|
|
371
|
+
) -> str:
|
|
372
|
+
import concurrent.futures
|
|
373
|
+
|
|
374
|
+
file_name = os.path.basename(file_path)
|
|
375
|
+
size = os.path.getsize(file_path)
|
|
376
|
+
|
|
377
|
+
multipart = cls(
|
|
378
|
+
file_name,
|
|
379
|
+
chunk_size=chunk_size,
|
|
380
|
+
content_type=content_type,
|
|
381
|
+
max_concurrency=max_concurrency,
|
|
382
|
+
)
|
|
383
|
+
multipart.create()
|
|
384
|
+
|
|
385
|
+
parts = math.ceil(size / multipart.chunk_size)
|
|
386
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
387
|
+
max_workers=multipart.max_concurrency
|
|
388
|
+
) as executor:
|
|
389
|
+
futures = []
|
|
390
|
+
for part_number in range(1, parts + 1):
|
|
391
|
+
|
|
392
|
+
def _upload_part(pn: int) -> None:
|
|
393
|
+
with open(file_path, "rb") as f:
|
|
394
|
+
start = (pn - 1) * multipart.chunk_size
|
|
395
|
+
f.seek(start)
|
|
396
|
+
data = f.read(multipart.chunk_size)
|
|
397
|
+
multipart.upload_part(pn, data)
|
|
398
|
+
|
|
399
|
+
futures.append(executor.submit(_upload_part, part_number))
|
|
400
|
+
|
|
401
|
+
for future in concurrent.futures.as_completed(futures):
|
|
402
|
+
future.result()
|
|
403
|
+
|
|
404
|
+
return multipart.complete()
|
|
405
|
+
|
|
406
|
+
|
|
185
407
|
@dataclass
|
|
186
408
|
class FalFileRepository(FalFileRepositoryBase):
|
|
409
|
+
def _object_lifecycle_headers(
|
|
410
|
+
self,
|
|
411
|
+
headers: dict[str, str],
|
|
412
|
+
object_lifecycle_preference: dict[str, str] | None,
|
|
413
|
+
):
|
|
414
|
+
if object_lifecycle_preference:
|
|
415
|
+
headers["X-Fal-Object-Lifecycle"] = json.dumps(object_lifecycle_preference)
|
|
416
|
+
|
|
417
|
+
@retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
|
|
187
418
|
def save(
|
|
188
419
|
self,
|
|
189
420
|
file: FileData,
|
|
@@ -193,7 +424,66 @@ class FalFileRepository(FalFileRepositoryBase):
|
|
|
193
424
|
multipart_max_concurrency: int | None = None,
|
|
194
425
|
object_lifecycle_preference: dict[str, str] | None = None,
|
|
195
426
|
) -> str:
|
|
196
|
-
|
|
427
|
+
if multipart is None:
|
|
428
|
+
threshold = multipart_threshold or MultipartUploadGCS.MULTIPART_THRESHOLD
|
|
429
|
+
multipart = len(file.data) > threshold
|
|
430
|
+
|
|
431
|
+
if multipart:
|
|
432
|
+
return MultipartUploadGCS.save(
|
|
433
|
+
file,
|
|
434
|
+
chunk_size=multipart_chunk_size,
|
|
435
|
+
max_concurrency=multipart_max_concurrency,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
headers = {}
|
|
439
|
+
if object_lifecycle_preference:
|
|
440
|
+
headers["X-Fal-Object-Lifecycle"] = json.dumps(object_lifecycle_preference)
|
|
441
|
+
|
|
442
|
+
return self._save(file, "gcs", headers=headers)
|
|
443
|
+
|
|
444
|
+
@property
|
|
445
|
+
def auth_headers(self) -> dict[str, str]:
|
|
446
|
+
token = fal_v3_token_manager.get_token()
|
|
447
|
+
return {
|
|
448
|
+
"Authorization": f"{token.token_type} {token.token}",
|
|
449
|
+
"User-Agent": "fal/0.1.0",
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
def save_file(
|
|
453
|
+
self,
|
|
454
|
+
file_path: str | Path,
|
|
455
|
+
content_type: str,
|
|
456
|
+
multipart: bool | None = None,
|
|
457
|
+
multipart_threshold: int | None = None,
|
|
458
|
+
multipart_chunk_size: int | None = None,
|
|
459
|
+
multipart_max_concurrency: int | None = None,
|
|
460
|
+
object_lifecycle_preference: dict[str, str] | None = None,
|
|
461
|
+
) -> tuple[str, FileData | None]:
|
|
462
|
+
if multipart is None:
|
|
463
|
+
threshold = multipart_threshold or MultipartUploadGCS.MULTIPART_THRESHOLD
|
|
464
|
+
multipart = os.path.getsize(file_path) > threshold
|
|
465
|
+
|
|
466
|
+
if multipart:
|
|
467
|
+
url = MultipartUploadGCS.save_file(
|
|
468
|
+
file_path,
|
|
469
|
+
chunk_size=multipart_chunk_size,
|
|
470
|
+
content_type=content_type,
|
|
471
|
+
max_concurrency=multipart_max_concurrency,
|
|
472
|
+
)
|
|
473
|
+
data = None
|
|
474
|
+
else:
|
|
475
|
+
with open(file_path, "rb") as f:
|
|
476
|
+
data = FileData(
|
|
477
|
+
f.read(),
|
|
478
|
+
content_type=content_type,
|
|
479
|
+
file_name=os.path.basename(file_path),
|
|
480
|
+
)
|
|
481
|
+
url = self.save(
|
|
482
|
+
data,
|
|
483
|
+
object_lifecycle_preference=object_lifecycle_preference,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
return url, data
|
|
197
487
|
|
|
198
488
|
|
|
199
489
|
class MultipartUpload:
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: fal
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.10.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.17.0,>=0.16.1
|
|
9
|
-
Requires-Dist: isolate-proto<0.
|
|
9
|
+
Requires-Dist: isolate-proto<0.8.0,>=0.7.0
|
|
10
10
|
Requires-Dist: grpcio==1.64.0
|
|
11
11
|
Requires-Dist: dill==0.3.7
|
|
12
12
|
Requires-Dist: cloudpickle==3.0.0
|
|
@@ -1,9 +1,9 @@
|
|
|
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=9brE8vcIUIWw9xiAY1wMoTwnJjicuHrJOrYpw1MWJTA,513
|
|
4
4
|
fal/_serialization.py,sha256=rD2YiSa8iuzCaZohZwN_MPEB-PpSKbWRDeaIDpTEjyY,7653
|
|
5
5
|
fal/_version.py,sha256=EBGqrknaf1WygENX-H4fBefLvHryvJBBGtVJetaB0NY,266
|
|
6
|
-
fal/api.py,sha256=
|
|
6
|
+
fal/api.py,sha256=jqmQfhRvZMYpWWvbTfARxjywP_72c314pnLPTb5dGwA,44755
|
|
7
7
|
fal/app.py,sha256=3WhjRgJdJ2ajAeZ3IeFb20_Zm6EH19a_WIuDtanaMHE,23308
|
|
8
8
|
fal/apps.py,sha256=RpmElElJnDYjsTRQOdNYiJwd74GEOGYA38L5O5GzNEg,11068
|
|
9
9
|
fal/config.py,sha256=aVv0k2fxMZurlra4c7ZIKQQCNPI-Dm_Mns6PsYWdh-c,2264
|
|
@@ -12,7 +12,7 @@ fal/files.py,sha256=QgfYfMKmNobMPufrAP_ga1FKcIAlSbw18Iar1-0qepo,2650
|
|
|
12
12
|
fal/flags.py,sha256=oWN_eidSUOcE9wdPK_77si3A1fpgOC0UEERPsvNLIMc,842
|
|
13
13
|
fal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
fal/rest_client.py,sha256=kGBGmuyHfX1lR910EoKCYPjsyU8MdXawT_cW2q8Sajc,568
|
|
15
|
-
fal/sdk.py,sha256=
|
|
15
|
+
fal/sdk.py,sha256=mMUIPB3F91TDssG_J5BFen2hWW6GFYWLh6dedpvZ9mU,24480
|
|
16
16
|
fal/sync.py,sha256=ZuIJA2-hTPNANG9B_NNJZUsO68EIdTH0dc9MzeVE2VU,4340
|
|
17
17
|
fal/utils.py,sha256=9q_QrQBlQN3nZYA1kEGRfhJWi4RjnO4H1uQswfaei9w,2146
|
|
18
18
|
fal/workflows.py,sha256=Zl4f6Bs085hY40zmqScxDUyCu7zXkukDbW02iYOLTTI,14805
|
|
@@ -21,16 +21,18 @@ fal/auth/auth0.py,sha256=rSG1mgH-QGyKfzd7XyAaj1AYsWt-ho8Y_LZ-FUVWzh4,5421
|
|
|
21
21
|
fal/auth/local.py,sha256=sndkM6vKpeVny6NHTacVlTbiIFqaksOmw0Viqs_RN1U,1790
|
|
22
22
|
fal/cli/__init__.py,sha256=padK4o0BFqq61kxAA1qQ0jYr2SuhA2mf90B3AaRkmJA,37
|
|
23
23
|
fal/cli/_utils.py,sha256=45G0LEz2bW-69MUQKPdatVE_CBC2644gC-V0qdNEsco,1252
|
|
24
|
-
fal/cli/
|
|
24
|
+
fal/cli/api.py,sha256=-rl50A00CxqVZtDh0iZmpCHMFY0jZySaumbPCe3MSoQ,2090
|
|
25
|
+
fal/cli/apps.py,sha256=KhweXdLdd1wZquhAMVwgl38dZAyFns-j3ZdIZLOLGWg,11504
|
|
25
26
|
fal/cli/auth.py,sha256=--MhfHGwxmtHbRkGioyn1prKn_U-pBzbz0G_QeZou-U,1352
|
|
27
|
+
fal/cli/cli_nested_json.py,sha256=veSZU8_bYV3Iu1PAoxt-4BMBraNIqgH5nughbs2UKvE,13539
|
|
26
28
|
fal/cli/create.py,sha256=a8WDq-nJLFTeoIXqpb5cr7GR7YR9ZZrQCawNm34KXXE,627
|
|
27
29
|
fal/cli/debug.py,sha256=u_urnyFzSlNnrq93zz_GXE9FX4VyVxDoamJJyrZpFI0,1312
|
|
28
|
-
fal/cli/deploy.py,sha256=
|
|
30
|
+
fal/cli/deploy.py,sha256=JX1jwyAFE0-u-PGGqU-pphKMCUpaPFbYMjRMhK0VmoQ,7783
|
|
29
31
|
fal/cli/doctor.py,sha256=U4ne9LX5gQwNblsYQ27XdO8AYDgbYjTO39EtxhwexRM,983
|
|
30
32
|
fal/cli/keys.py,sha256=trDpA3LJu9S27qE_K8Hr6fKLK4vwVzbxUHq8TFrV4pw,3157
|
|
31
|
-
fal/cli/main.py,sha256=
|
|
33
|
+
fal/cli/main.py,sha256=oUY1bUcfEC8LIsNxUBBjT3O6HxWCd1cKEMrJBue47iw,2199
|
|
32
34
|
fal/cli/parser.py,sha256=edCqFWYAQSOhrxeEK9BtFRlTEUAlG2JUDjS_vhZ_nHE,2868
|
|
33
|
-
fal/cli/profile.py,sha256=
|
|
35
|
+
fal/cli/profile.py,sha256=OplQgs8UGQzBH7_BnG0GBMYNQ8jtPnzzX8Q1FM3Y-5s,3320
|
|
34
36
|
fal/cli/run.py,sha256=nAC12Qss4Fg1XmV0qOS9RdGNLYcdoHeRgQMvbTN4P9I,1202
|
|
35
37
|
fal/cli/runners.py,sha256=5pXuKq7nSkf0VpnppNnvxwP8XDq0SWkc6mkfizDwWMQ,1046
|
|
36
38
|
fal/cli/secrets.py,sha256=740msFm7d41HruudlcfqUXlFl53N-WmChsQP9B9M9Po,2572
|
|
@@ -53,7 +55,7 @@ fal/toolkit/types.py,sha256=kkbOsDKj1qPGb1UARTBp7yuJ5JUuyy7XQurYUBCdti8,4064
|
|
|
53
55
|
fal/toolkit/file/__init__.py,sha256=FbNl6wD-P0aSSTUwzHt4HujBXrbC3ABmaigPQA4hRfg,70
|
|
54
56
|
fal/toolkit/file/file.py,sha256=Kb-mdR66OiSNTS2EGLLJYUqnAw-KN7diqhxvjS7EAZ0,9353
|
|
55
57
|
fal/toolkit/file/types.py,sha256=MMAH_AyLOhowQPesOv1V25wB4qgbJ3vYNlnTPbdSv1M,2304
|
|
56
|
-
fal/toolkit/file/providers/fal.py,sha256=
|
|
58
|
+
fal/toolkit/file/providers/fal.py,sha256=NI9TX5gdFkyc6hHl-5FKNuYvGYhYFHD5FvXRf3d-oRU,46409
|
|
57
59
|
fal/toolkit/file/providers/gcp.py,sha256=DKeZpm1MjwbvEsYvkdXUtuLIJDr_UNbqXj_Mfv3NTeo,2437
|
|
58
60
|
fal/toolkit/file/providers/r2.py,sha256=YqnYkkAo_ZKIa-xoSuDnnidUFwJWHdziAR34PE6irdI,3061
|
|
59
61
|
fal/toolkit/file/providers/s3.py,sha256=EI45T54Mox7lHZKROss_O8o0DIn3CHP9k1iaNYVrxvg,2714
|
|
@@ -131,8 +133,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
|
|
|
131
133
|
openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
|
|
132
134
|
openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
|
|
133
135
|
openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
|
|
134
|
-
fal-1.
|
|
135
|
-
fal-1.
|
|
136
|
-
fal-1.
|
|
137
|
-
fal-1.
|
|
138
|
-
fal-1.
|
|
136
|
+
fal-1.10.0.dist-info/METADATA,sha256=a6lPJ7h0sfH7o_INV6XW3RWKJ9nfu0m_-Jh7I6In70w,4043
|
|
137
|
+
fal-1.10.0.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
|
138
|
+
fal-1.10.0.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
|
|
139
|
+
fal-1.10.0.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
|
|
140
|
+
fal-1.10.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|