fal 1.41.1__py3-none-any.whl → 1.43.0__py3-none-any.whl

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

Potentially problematic release.


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

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