fal 1.37.0__py3-none-any.whl → 1.39.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.37.0'
32
- __version_tuple__ = version_tuple = (1, 37, 0)
31
+ __version__ = version = '1.39.0'
32
+ __version_tuple__ = version_tuple = (1, 39, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
fal/cli/apps.py CHANGED
@@ -8,7 +8,7 @@ import fal.cli.runners as runners
8
8
  from fal.sdk import RunnerState
9
9
 
10
10
  from ._utils import get_client
11
- from .parser import FalClientParser, get_output_parser
11
+ from .parser import FalClientParser, SinceAction, get_output_parser
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from fal.sdk import AliasInfo, ApplicationInfo
@@ -291,7 +291,14 @@ def _add_set_rev_parser(subparsers, parents):
291
291
  def _runners(args):
292
292
  client = get_client(args.host, args.team)
293
293
  with client.connect() as connection:
294
- alias_runners = connection.list_alias_runners(alias=args.app_name)
294
+ start_time = getattr(args, "since", None)
295
+ alias_runners = connection.list_alias_runners(
296
+ alias=args.app_name, start_time=start_time
297
+ )
298
+ if getattr(args, "state", None):
299
+ states = set(args.state)
300
+ if "all" not in states:
301
+ alias_runners = [r for r in alias_runners if r.state.value in states]
295
302
  if args.output == "pretty":
296
303
  runners_table = runners.runners_table(alias_runners)
297
304
  pending_runners = [
@@ -330,6 +337,24 @@ def _add_runners_parser(subparsers, parents):
330
337
  "app_name",
331
338
  help="Application name.",
332
339
  )
340
+ parser.add_argument(
341
+ "--since",
342
+ default=None,
343
+ action=SinceAction,
344
+ limit="1 day",
345
+ help=(
346
+ "Show dead runners since the given time. "
347
+ "Accepts 'now', relative like '30m', '1h', '1d', "
348
+ "or an ISO timestamp. Max 24 hours."
349
+ ),
350
+ )
351
+ parser.add_argument(
352
+ "--state",
353
+ choices=["all", "running", "pending", "setup", "dead"],
354
+ nargs="+",
355
+ default=None,
356
+ help=("Filter by runner state(s). Choose one or more, or 'all'(default)."),
357
+ )
333
358
  parser.set_defaults(func=_runners)
334
359
 
335
360
 
fal/cli/parser.py CHANGED
@@ -1,5 +1,7 @@
1
1
  import argparse
2
2
  import sys
3
+ from datetime import datetime, timedelta
4
+ from typing import Optional
3
5
 
4
6
  import rich_argparse
5
7
 
@@ -55,6 +57,61 @@ class DictAction(argparse.Action):
55
57
  setattr(args, self.dest, d)
56
58
 
57
59
 
60
+ class SinceAction(argparse.Action):
61
+ LIMIT_LEEWAY = timedelta(minutes=1)
62
+
63
+ def _parse_since(self, value: str) -> Optional[datetime]:
64
+ import dateparser
65
+
66
+ return dateparser.parse(
67
+ value,
68
+ settings={
69
+ "PREFER_DATES_FROM": "past",
70
+ },
71
+ )
72
+
73
+ def __init__(self, *args, **kwargs):
74
+ self._limit = kwargs.pop("limit", None)
75
+ if self._limit:
76
+ if not isinstance(self._limit, str):
77
+ raise ValueError(
78
+ f"Invalid 'limit' value for SinceAction: {self._limit!r}"
79
+ )
80
+
81
+ self._limit_dt = self._parse_since(self._limit)
82
+ if not self._limit_dt:
83
+ raise ValueError(
84
+ f"Invalid 'limit' value for SinceAction: {self._limit!r}"
85
+ )
86
+
87
+ super().__init__(*args, **kwargs)
88
+
89
+ def __call__(self, parser, args, values, option_string=None): # noqa: ARG002
90
+ if values is None:
91
+ setattr(args, self.dest, None)
92
+ return
93
+
94
+ dt = self._parse_since(values)
95
+ if not dt:
96
+ raise argparse.ArgumentError(
97
+ self,
98
+ (
99
+ f"Invalid since value: {values}. "
100
+ "Use 'now', relative like '15m' or '24h ago', "
101
+ "or an ISO timestamp."
102
+ ),
103
+ )
104
+
105
+ if self._limit_dt is not None:
106
+ if dt < self._limit_dt - self.LIMIT_LEEWAY:
107
+ raise argparse.ArgumentError(
108
+ self,
109
+ f"Since value is older than the allowed limit {self._limit}.",
110
+ )
111
+
112
+ setattr(args, self.dest, dt)
113
+
114
+
58
115
  def _find_parser(parser, func):
59
116
  defaults = parser._defaults
60
117
  if not func or func == defaults.get("func"):
fal/cli/runners.py CHANGED
@@ -1,12 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ from datetime import timedelta
4
5
  from typing import List
5
6
 
6
7
  from fal.sdk import RunnerInfo, RunnerState
7
8
 
8
9
  from ._utils import get_client
9
- from .parser import FalClientParser, get_output_parser
10
+ from .parser import FalClientParser, SinceAction, get_output_parser
10
11
 
11
12
 
12
13
  def runners_table(runners: List[RunnerInfo]):
@@ -42,17 +43,20 @@ def runners_table(runners: List[RunnerInfo]):
42
43
  # consistent
43
44
  in_flight = f"{in_flight} [dim]({missing_leases})[/]"
44
45
 
46
+ uptime = timedelta(
47
+ seconds=int(runner.uptime.total_seconds()),
48
+ )
45
49
  table.add_row(
46
50
  runner.alias,
47
51
  # Mark lost runners in red
48
52
  runner.runner_id if present else f"[red]{runner.runner_id}[/]",
49
53
  in_flight,
50
54
  (
51
- "N/A (active)"
55
+ "N/A"
52
56
  if runner.expiration_countdown is None
53
57
  else f"{runner.expiration_countdown}s"
54
58
  ),
55
- f"{runner.uptime} ({runner.uptime.total_seconds()}s)",
59
+ f"{uptime} ({uptime.total_seconds():.0f}s)",
56
60
  runner.revision,
57
61
  runner.state.value,
58
62
  )
@@ -111,17 +115,31 @@ def _list_json(args, runners: list[RunnerInfo]):
111
115
  def _list(args):
112
116
  client = get_client(args.host, args.team)
113
117
  with client.connect() as connection:
114
- runners = connection.list_runners()
118
+ start_time = getattr(args, "since", None)
119
+ runners = connection.list_runners(start_time=start_time)
120
+
121
+ if getattr(args, "state", None):
122
+ states = set(args.state)
123
+ if "all" not in states:
124
+ runners = [r for r in runners if r.state.value in states]
115
125
  pending_runners = [
116
126
  runner for runner in runners if runner.state == RunnerState.PENDING
117
127
  ]
118
128
  setup_runners = [
119
129
  runner for runner in runners if runner.state == RunnerState.SETUP
120
130
  ]
131
+ dead_runners = [
132
+ runner for runner in runners if runner.state == RunnerState.DEAD
133
+ ]
121
134
  if args.output == "pretty":
122
135
  args.console.print(
123
136
  "Runners: "
124
- + str(len(runners) - len(pending_runners) - len(setup_runners))
137
+ + str(
138
+ len(runners)
139
+ - len(pending_runners)
140
+ - len(setup_runners)
141
+ - len(dead_runners)
142
+ )
125
143
  )
126
144
  args.console.print(f"Runners Pending: {len(pending_runners)}")
127
145
  args.console.print(f"Runners Setting Up: {len(setup_runners)}")
@@ -159,6 +177,24 @@ def _add_list_parser(subparsers, parents):
159
177
  help=list_help,
160
178
  parents=[*parents, get_output_parser()],
161
179
  )
180
+ parser.add_argument(
181
+ "--since",
182
+ default=None,
183
+ action=SinceAction,
184
+ limit="1 day",
185
+ help=(
186
+ "Show dead runners since the given time. "
187
+ "Accepts 'now', relative like '30m', '1h', '1d', "
188
+ "or an ISO timestamp. Max 24 hours."
189
+ ),
190
+ )
191
+ parser.add_argument(
192
+ "--state",
193
+ choices=["all", "running", "pending", "setup", "dead"],
194
+ nargs="+",
195
+ default=None,
196
+ help=("Filter by runner state(s). Choose one or more, or 'all'(default)."),
197
+ )
162
198
  parser.set_defaults(func=_list)
163
199
 
164
200
 
fal/sdk.py CHANGED
@@ -265,6 +265,7 @@ class RunnerState(Enum):
265
265
  RUNNING = "running"
266
266
  PENDING = "pending"
267
267
  SETUP = "setup"
268
+ DEAD = "dead"
268
269
  UNKNOWN = "unknown"
269
270
 
270
271
  @staticmethod
@@ -275,6 +276,8 @@ class RunnerState(Enum):
275
276
  return RunnerState.PENDING
276
277
  elif proto is isolate_proto.RunnerInfo.State.SETUP:
277
278
  return RunnerState.SETUP
279
+ elif proto is isolate_proto.RunnerInfo.State.DEAD:
280
+ return RunnerState.DEAD
278
281
  else:
279
282
  return RunnerState.UNKNOWN
280
283
 
@@ -811,8 +814,18 @@ class FalServerlessConnection:
811
814
  response: isolate_proto.ListAliasesResult = self.stub.ListAliases(request)
812
815
  return [from_grpc(alias) for alias in response.aliases]
813
816
 
814
- def list_alias_runners(self, alias: str) -> list[RunnerInfo]:
815
- request = isolate_proto.ListAliasRunnersRequest(alias=alias, list_pending=True)
817
+ def list_alias_runners(
818
+ self,
819
+ alias: str,
820
+ *,
821
+ list_pending: bool = True,
822
+ start_time: datetime | None = None,
823
+ ) -> list[RunnerInfo]:
824
+ kwargs = {"alias": alias, "list_pending": list_pending}
825
+ if start_time:
826
+ kwargs["start_time"] = isolate_proto.timestamp_from_datetime(start_time)
827
+
828
+ request = isolate_proto.ListAliasRunnersRequest(**kwargs)
816
829
  response = self.stub.ListAliasRunners(request)
817
830
  return [from_grpc(runner) for runner in response.runners]
818
831
 
@@ -839,7 +852,13 @@ class FalServerlessConnection:
839
852
  request = isolate_proto.KillRunnerRequest(runner_id=runner_id)
840
853
  self.stub.KillRunner(request)
841
854
 
842
- def list_runners(self) -> list[RunnerInfo]:
843
- request = isolate_proto.ListRunnersRequest(list_pending=True)
855
+ def list_runners(self, start_time: datetime | None = None) -> list[RunnerInfo]:
856
+ kwargs = {
857
+ "list_pending": True,
858
+ }
859
+ if start_time:
860
+ kwargs["start_time"] = isolate_proto.timestamp_from_datetime(start_time)
861
+
862
+ request = isolate_proto.ListRunnersRequest(**kwargs)
844
863
  response = self.stub.ListRunners(request)
845
864
  return [from_grpc(runner) for runner in response.runners]
@@ -7,18 +7,19 @@ from typing import TYPE_CHECKING
7
7
  from .image import * # noqa: F403
8
8
 
9
9
  if TYPE_CHECKING:
10
- from PIL.Image import Image as PILImage
10
+ # suffix so we don't clash with PILImage from .image
11
+ from PIL.Image import Image as PILImage2
11
12
 
12
13
 
13
14
  def filter_by(
14
15
  has_nsfw_concepts: list[bool],
15
- images: list[PILImage],
16
- ) -> list[PILImage]:
17
- from PIL import Image as PILImage
16
+ images: list[PILImage2],
17
+ ) -> list[PILImage2]:
18
+ from PIL import Image as PILImageModule
18
19
 
19
20
  return [
20
21
  (
21
- PILImage.new("RGB", (image.width, image.height), (0, 0, 0))
22
+ PILImageModule.new("RGB", (image.width, image.height), (0, 0, 0))
22
23
  if has_nsfw
23
24
  else image
24
25
  )
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fal
3
- Version: 1.37.0
3
+ Version: 1.39.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.20.0,>=0.18.0
9
- Requires-Dist: isolate-proto<0.15.0,>=0.14.1
9
+ Requires-Dist: isolate-proto<0.17.0,>=0.16.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
@@ -17,7 +17,7 @@ Requires-Dist: opentelemetry-sdk<2,>=1.15.0
17
17
  Requires-Dist: grpc-interceptor<1,>=0.15.0
18
18
  Requires-Dist: colorama<1,>=0.4.6
19
19
  Requires-Dist: portalocker<3,>=2.7.0
20
- Requires-Dist: rich<14,>=13.3.2
20
+ Requires-Dist: rich<15,>=13.3.2
21
21
  Requires-Dist: rich_argparse
22
22
  Requires-Dist: packaging>=21.3
23
23
  Requires-Dist: pathspec<1,>=0.11.1
@@ -28,10 +28,12 @@ Requires-Dist: httpx>=0.15.4
28
28
  Requires-Dist: attrs>=21.3.0
29
29
  Requires-Dist: python-dateutil<3,>=2.8.0
30
30
  Requires-Dist: types-python-dateutil<3,>=2.8.0
31
+ Requires-Dist: dateparser<2,>=1.2.0
32
+ Requires-Dist: types-dateparser<2,>=1.2.0
31
33
  Requires-Dist: importlib-metadata>=4.4; python_version < "3.10"
32
34
  Requires-Dist: msgpack<2,>=1.0.7
33
35
  Requires-Dist: websockets>=12.0
34
- Requires-Dist: pillow<11,>=10.2.0
36
+ Requires-Dist: pillow<12,>=10.2.0
35
37
  Requires-Dist: pyjwt[crypto]<3,>=2.8.0
36
38
  Requires-Dist: uvicorn<1,>=0.29.0
37
39
  Requires-Dist: cookiecutter
@@ -1,6 +1,6 @@
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=7iM6FVCB7yNzIZ7z59U_U2d_x0RefLoRa1YrPtAszvk,706
3
+ fal/_fal_version.py,sha256=Dj2QaXELg6PpeXeLgj_oylZJy58D2f8eumyiKVf19pQ,706
4
4
  fal/_serialization.py,sha256=npXNsFJ5G7jzBeBIyVMH01Ww34mGY4XWhHpRbSrTtnQ,7598
5
5
  fal/_version.py,sha256=1BbTFnucNC_6ldKJ_ZoC722_UkW4S9aDBSW9L0fkKAw,2315
6
6
  fal/api.py,sha256=oWSPxefwyYF93s54mtyYGKWYMax9ll9B-wxijOyARb4,49244
@@ -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=cs7AlrgXEOavTSF8c8MgZy6QyLe_SowaDGlHagFcYJg,27523
16
+ fal/sdk.py,sha256=iKpyL4RrWCdaFMK4wiIM3zOuM2H0VsN0wv8wF9OBxHM,28099
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
@@ -23,7 +23,7 @@ fal/auth/local.py,sha256=sndkM6vKpeVny6NHTacVlTbiIFqaksOmw0Viqs_RN1U,1790
23
23
  fal/cli/__init__.py,sha256=padK4o0BFqq61kxAA1qQ0jYr2SuhA2mf90B3AaRkmJA,37
24
24
  fal/cli/_utils.py,sha256=ulYezhr3G29nTIF8MDQ6tsW01Oj1zPo-YSqMoBi05Ic,1871
25
25
  fal/cli/api.py,sha256=ZuDE_PIC-czzneTAWMwvC7P7WnwIyluNZSuJqzCFhqI,2640
26
- fal/cli/apps.py,sha256=W66wBeq7ChtDsPuNjb0HXVe9zW3e1ORZkxbUTOF1CkQ,11698
26
+ fal/cli/apps.py,sha256=8ChoOYf2GeRSDN0w5VgDnWqdAqROlyDyQunciL-C8z4,12545
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
@@ -33,10 +33,10 @@ 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=s_LxEyz9z64dewk3oiGEI33_h3vJ3IVdu8aY3qydkMo,3345
36
- fal/cli/parser.py,sha256=WxbO5pgMhcerEFl4uUdaCoFlTQF14PJVctiWRo55jHQ,3459
36
+ fal/cli/parser.py,sha256=PZi5MWS4Z-3YSPe6np_F87ay4kF6gaYxlP0avByPr-0,5222
37
37
  fal/cli/profile.py,sha256=PAY_ffifCT71VJ8VxfDVaXPT0U1oN8drvWZDFRXwvek,6678
38
38
  fal/cli/run.py,sha256=nAC12Qss4Fg1XmV0qOS9RdGNLYcdoHeRgQMvbTN4P9I,1202
39
- fal/cli/runners.py,sha256=INdIDk0Ro_33Zea3mDEFO2CiuqIM7qaU7hAr2tLXovI,5369
39
+ fal/cli/runners.py,sha256=AXUB2pq9Ot0VU2cOeJydSgmgTlUm4i6iNgJOClO7ZZw,6533
40
40
  fal/cli/secrets.py,sha256=HfIeO2IZpCEiBC6Cs5Kpi3zckfDnc7GsLwLdgj3NnPU,3085
41
41
  fal/cli/teams.py,sha256=_JcNcf659ZoLBFOxKnVP5A6Pyk1jY1vh4_xzMweYIDo,1285
42
42
  fal/console/__init__.py,sha256=lGPUuTqIM9IKTa1cyyA-MA2iZJKVHp2YydsITZVlb6g,148
@@ -65,7 +65,7 @@ fal/toolkit/file/providers/fal.py,sha256=z2Htg7DJoBxq_y4Q71KOGM1mHvJzUQCGtz2_m2l
65
65
  fal/toolkit/file/providers/gcp.py,sha256=DKeZpm1MjwbvEsYvkdXUtuLIJDr_UNbqXj_Mfv3NTeo,2437
66
66
  fal/toolkit/file/providers/r2.py,sha256=YqnYkkAo_ZKIa-xoSuDnnidUFwJWHdziAR34PE6irdI,3061
67
67
  fal/toolkit/file/providers/s3.py,sha256=EI45T54Mox7lHZKROss_O8o0DIn3CHP9k1iaNYVrxvg,2714
68
- fal/toolkit/image/__init__.py,sha256=m3OatPbBhcEOYyaTu_dgToxunUKoJu4bJVCWUoN7HX4,1838
68
+ fal/toolkit/image/__init__.py,sha256=7DK6eZYdJpQJih3JISk6JCLLChK28_n8Kv3hMonCIdU,1910
69
69
  fal/toolkit/image/image.py,sha256=xmuIKU42DonGHnPS6V5MbLv-wBSlpVvfPTAy2A_Vds0,5614
70
70
  fal/toolkit/image/safety_checker.py,sha256=S7ow-HuoVxC6ixHWWcBrAUm2dIlgq3sTAIull6xIbAg,3105
71
71
  fal/toolkit/image/nsfw_filter/__init__.py,sha256=0d9D51EhcnJg8cZLYJjgvQJDZT74CfQu6mpvinRYRpA,216
@@ -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.37.0.dist-info/METADATA,sha256=yU-eG6bbAOSUvjTxN-z8fB_9bHOR-DUg6aeB-uZ4tdQ,4054
147
- fal-1.37.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
148
- fal-1.37.0.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
149
- fal-1.37.0.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
150
- fal-1.37.0.dist-info/RECORD,,
146
+ fal-1.39.0.dist-info/METADATA,sha256=1nkxD7yxfTSJDAsw7XVEfxyzfRGwvekOwBliYo4smPI,4132
147
+ fal-1.39.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
148
+ fal-1.39.0.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
149
+ fal-1.39.0.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
150
+ fal-1.39.0.dist-info/RECORD,,
File without changes