fal 1.9.4__py3-none-any.whl → 1.11.0__py3-none-any.whl

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

Potentially problematic release.


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

fal/_fal_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '1.9.4'
21
- __version_tuple__ = version_tuple = (1, 9, 4)
20
+ __version__ = version = '1.11.0'
21
+ __version_tuple__ = version_tuple = (1, 11, 0)
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("Expires in")
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 from previous "
239
- "deployment of application with this name, if exists. Otherwise will "
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 apps, auth, create, deploy, doctor, keys, profile, run, runners, secrets
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/container.py CHANGED
@@ -4,6 +4,8 @@ from typing import Dict, Literal
4
4
  Builder = Literal["depot", "service", "worker"]
5
5
  BUILDERS = {"depot", "service", "worker"}
6
6
  DEFAULT_BUILDER: Builder = "depot"
7
+ DEFAULT_COMPRESSION: str = "gzip"
8
+ DEFAULT_FORCE_COMPRESSION: bool = False
7
9
 
8
10
 
9
11
  @dataclass
@@ -16,6 +18,8 @@ class ContainerImage:
16
18
  build_args: Dict[str, str] = field(default_factory=dict)
17
19
  registries: Dict[str, Dict[str, str]] = field(default_factory=dict)
18
20
  builder: Builder = field(default=DEFAULT_BUILDER)
21
+ compression: str = DEFAULT_COMPRESSION
22
+ force_compression: bool = DEFAULT_FORCE_COMPRESSION
19
23
 
20
24
  def __post_init__(self) -> None:
21
25
  if self.registries:
@@ -46,4 +50,6 @@ class ContainerImage:
46
50
  "build_args": self.build_args,
47
51
  "registries": self.registries,
48
52
  "builder": self.builder,
53
+ "compression": self.compression,
54
+ "force_compression": self.force_compression,
49
55
  }
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
  )
@@ -185,8 +185,236 @@ class FalFileRepositoryBase(FileRepository):
185
185
  )
186
186
 
187
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
+
188
407
  @dataclass
189
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)
190
418
  def save(
191
419
  self,
192
420
  file: FileData,
@@ -196,12 +424,67 @@ class FalFileRepository(FalFileRepositoryBase):
196
424
  multipart_max_concurrency: int | None = None,
197
425
  object_lifecycle_preference: dict[str, str] | None = None,
198
426
  ) -> str:
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
+
199
438
  headers = {}
200
439
  if object_lifecycle_preference:
201
440
  headers["X-Fal-Object-Lifecycle"] = json.dumps(object_lifecycle_preference)
202
441
 
203
442
  return self._save(file, "gcs", headers=headers)
204
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
487
+
205
488
 
206
489
  class MultipartUpload:
207
490
  MULTIPART_THRESHOLD = 100 * 1024 * 1024
@@ -1,12 +1,12 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: fal
3
- Version: 1.9.4
3
+ Version: 1.11.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.7.0,>=0.6.7
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,18 +1,18 @@
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=x_dqnWSuZ5m1DFc2qnANNI5KdLpU51P41kIYXxibols,511
3
+ fal/_fal_version.py,sha256=NYxfUyIQwdONRcmiATXsc3iD0kuKImoMYD3H1Vdv9OM,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=i6HnL4sJtwv34N9T5uzXSgwetBLMVJye57CHmdq5m78,43998
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
10
- fal/container.py,sha256=9XslBET-NCG2V3-Wmof8c7eHrRoxCye88Ym7CskqCk0,1639
10
+ fal/container.py,sha256=PM7e1RloTCexZ64uAv7sa2RSZxPI-X8KcxkdaZqEfjw,1914
11
11
  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=SRJ4zp0TAvHmfGPsMtYUoMPaw9yj-qKY1RkzqCcZxWo,23976
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,14 +21,16 @@ 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/apps.py,sha256=KVi_sZQPvznrTtbOHnkbTGH9KUjYqp_b-uXLvFyjKgY,10358
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=Wu8wxR72od2GAp1OF-FYO6WSWt4umaHysL3HrO_bzEo,7764
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=EPqG-Pp71RKkPUHv5QSb8_gWQ1DT8-OdUDNSN1h-TSk,2132
33
+ fal/cli/main.py,sha256=oUY1bUcfEC8LIsNxUBBjT3O6HxWCd1cKEMrJBue47iw,2199
32
34
  fal/cli/parser.py,sha256=edCqFWYAQSOhrxeEK9BtFRlTEUAlG2JUDjS_vhZ_nHE,2868
33
35
  fal/cli/profile.py,sha256=OplQgs8UGQzBH7_BnG0GBMYNQ8jtPnzzX8Q1FM3Y-5s,3320
34
36
  fal/cli/run.py,sha256=nAC12Qss4Fg1XmV0qOS9RdGNLYcdoHeRgQMvbTN4P9I,1202
@@ -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=j3g63b7ywUuTcesZD86WXSnn8Vuv-Lw5td3zSwHUmRw,37169
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.9.4.dist-info/METADATA,sha256=YCKQIxk6JFhLAXYRTUqaWcGUDu4pqN1UIJ9n2liGjtE,4042
135
- fal-1.9.4.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
136
- fal-1.9.4.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
137
- fal-1.9.4.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
138
- fal-1.9.4.dist-info/RECORD,,
136
+ fal-1.11.0.dist-info/METADATA,sha256=gyk3rZlftiD1qXNBRHk5Z_g-CC65Qi9OTS_zuKfQGHE,4043
137
+ fal-1.11.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
138
+ fal-1.11.0.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
139
+ fal-1.11.0.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
140
+ fal-1.11.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.2)
2
+ Generator: setuptools (77.0.3)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5