fal 1.5.13__py3-none-any.whl → 1.5.15__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
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.5.13'
16
- __version_tuple__ = version_tuple = (1, 5, 13)
15
+ __version__ = version = '1.5.15'
16
+ __version_tuple__ = version_tuple = (1, 5, 15)
fal/apps.py CHANGED
@@ -4,15 +4,19 @@ import json
4
4
  import time
5
5
  from contextlib import contextmanager
6
6
  from dataclasses import dataclass, field
7
- from typing import Any, Iterator
7
+ from typing import TYPE_CHECKING, Any, Iterator
8
8
 
9
9
  import httpx
10
10
 
11
11
  from fal import flags
12
12
  from fal.sdk import Credentials, get_default_credentials
13
13
 
14
+ if TYPE_CHECKING:
15
+ from websockets.sync.connection import Connection
16
+
14
17
  _QUEUE_URL_FORMAT = f"https://queue.{flags.FAL_RUN_HOST}/{{app_id}}"
15
18
  _REALTIME_URL_FORMAT = f"wss://{flags.FAL_RUN_HOST}/{{app_id}}"
19
+ _WS_URL_FORMAT = f"wss://ws.{flags.FAL_RUN_HOST}/{{app_id}}"
16
20
 
17
21
 
18
22
  def _backwards_compatible_app_id(app_id: str) -> str:
@@ -245,3 +249,127 @@ def _connect(app_id: str, *, path: str = "/realtime") -> Iterator[_RealtimeConne
245
249
  url, additional_headers=creds.to_headers(), open_timeout=90
246
250
  ) as ws:
247
251
  yield _RealtimeConnection(ws)
252
+
253
+
254
+ class _MetaMessageFound(Exception): ...
255
+
256
+
257
+ @dataclass
258
+ class _WSConnection:
259
+ """A WS connection to an HTTP Fal app."""
260
+
261
+ _ws: Connection
262
+ _buffer: str | bytes | None = None
263
+
264
+ def run(self, arguments: dict[str, Any]) -> dict[str, Any]:
265
+ """Run an inference task on the app and return the result."""
266
+ self.send(arguments)
267
+ return self.recv()
268
+
269
+ def send(self, arguments: dict[str, Any]) -> None:
270
+ import json
271
+
272
+ payload = json.dumps(arguments)
273
+ self._ws.send(payload)
274
+
275
+ def _peek(self) -> bytes | str:
276
+ if self._buffer is None:
277
+ self._buffer = self._ws.recv()
278
+
279
+ return self._buffer
280
+
281
+ def _consume(self) -> None:
282
+ if self._buffer is None:
283
+ raise ValueError("No data to consume")
284
+
285
+ self._buffer = None
286
+
287
+ @contextmanager
288
+ def _recv(self) -> Iterator[str | bytes]:
289
+ res = self._peek()
290
+
291
+ yield res
292
+
293
+ # Only consume if it went through the context manager without raising
294
+ self._consume()
295
+
296
+ def _is_meta(self, res: str | bytes) -> bool:
297
+ if not isinstance(res, str):
298
+ return False
299
+
300
+ try:
301
+ json_payload: Any = json.loads(res)
302
+ except json.JSONDecodeError:
303
+ return False
304
+
305
+ if not isinstance(json_payload, dict):
306
+ return False
307
+
308
+ return "type" in json_payload and "request_id" in json_payload
309
+
310
+ def _recv_meta(self, type: str) -> dict[str, Any]:
311
+ with self._recv() as res:
312
+ if not self._is_meta(res):
313
+ raise ValueError(f"Expected a {type} message")
314
+
315
+ json_payload: dict = json.loads(res)
316
+ if json_payload.get("type") != type:
317
+ raise ValueError(f"Expected a {type} message")
318
+
319
+ return json_payload
320
+
321
+ def _recv_response(self) -> Any:
322
+ import msgpack
323
+
324
+ body: bytes = b""
325
+ while True:
326
+ try:
327
+ with self._recv() as res:
328
+ if self._is_meta(res):
329
+ # Keep the meta message for later
330
+ raise _MetaMessageFound()
331
+
332
+ if isinstance(res, str):
333
+ return res
334
+ else:
335
+ body += res
336
+ except _MetaMessageFound:
337
+ break
338
+
339
+ if not body:
340
+ raise ValueError("Empty response body")
341
+
342
+ return msgpack.unpackb(body)
343
+
344
+ def recv(self) -> Any:
345
+ start = self._recv_meta("start")
346
+ request_id = start["request_id"]
347
+
348
+ response = self._recv_response()
349
+
350
+ end = self._recv_meta("end")
351
+ if end["request_id"] != request_id:
352
+ raise ValueError("Mismatched request_id in end message")
353
+
354
+ return response
355
+
356
+
357
+ @contextmanager
358
+ def ws(app_id: str, *, path: str = "") -> Iterator[_WSConnection]:
359
+ """Connect to a HTTP endpoint but with websocket protocol. This is an internal and
360
+ experimental API, use it at your own risk."""
361
+
362
+ from websockets.sync import client
363
+
364
+ app_id = _backwards_compatible_app_id(app_id)
365
+ url = _WS_URL_FORMAT.format(app_id=app_id)
366
+ if path:
367
+ _path = path[len("/") :] if path.startswith("/") else path
368
+ url += "/" + _path
369
+
370
+ creds = get_default_credentials()
371
+
372
+ with client.connect(
373
+ url, additional_headers=creds.to_headers(), open_timeout=90
374
+ ) as ws:
375
+ yield _WSConnection(ws)
fal/cli/machine.py ADDED
@@ -0,0 +1,43 @@
1
+ from .parser import FalClientParser
2
+
3
+
4
+ def _kill(args):
5
+ from fal.sdk import FalServerlessClient
6
+
7
+ client = FalServerlessClient(args.host)
8
+ with client.connect() as connection:
9
+ connection.kill_runner(args.id)
10
+
11
+
12
+ def _add_kill_parser(subparsers, parents):
13
+ kill_help = "Kill a machine."
14
+ parser = subparsers.add_parser(
15
+ "kill",
16
+ description=kill_help,
17
+ help=kill_help,
18
+ parents=parents,
19
+ )
20
+ parser.add_argument(
21
+ "id",
22
+ help="Runner ID.",
23
+ )
24
+ parser.set_defaults(func=_kill)
25
+
26
+
27
+ def add_parser(main_subparsers, parents):
28
+ machine_help = "Manage fal machines."
29
+ parser = main_subparsers.add_parser(
30
+ "machine",
31
+ description=machine_help,
32
+ help=machine_help,
33
+ parents=parents,
34
+ )
35
+
36
+ subparsers = parser.add_subparsers(
37
+ title="Commands",
38
+ metavar="command",
39
+ required=True,
40
+ parser_class=FalClientParser,
41
+ )
42
+
43
+ _add_kill_parser(subparsers, parents)
fal/cli/main.py CHANGED
@@ -6,7 +6,7 @@ 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, run, secrets
9
+ from . import apps, auth, create, deploy, doctor, keys, machine, run, secrets
10
10
  from .debug import debugtools, get_debug_parser
11
11
  from .parser import FalParser, FalParserExit
12
12
 
@@ -31,7 +31,7 @@ def _get_main_parser() -> argparse.ArgumentParser:
31
31
  required=True,
32
32
  )
33
33
 
34
- for cmd in [auth, apps, deploy, run, keys, secrets, doctor, create]:
34
+ for cmd in [auth, apps, deploy, run, keys, secrets, doctor, create, machine]:
35
35
  cmd.add_parser(subparsers, parents)
36
36
 
37
37
  return parser
fal/sdk.py CHANGED
@@ -686,3 +686,7 @@ class FalServerlessConnection:
686
686
  )
687
687
  for secret in response.secrets
688
688
  ]
689
+
690
+ def kill_runner(self, runner_id: str) -> None:
691
+ request = isolate_proto.KillRunnerRequest(runner_id=runner_id)
692
+ self.stub.KillRunner(request)
@@ -180,6 +180,14 @@ class FalFileRepository(FalFileRepositoryBase):
180
180
  return self._save(file, "gcs")
181
181
 
182
182
 
183
+ @dataclass
184
+ class FalFileRepositoryV3(FalFileRepositoryBase):
185
+ def save(
186
+ self, file: FileData, object_lifecycle_preference: dict[str, str] | None = None
187
+ ) -> str:
188
+ return self._save(file, "fal-cdn-v3")
189
+
190
+
183
191
  class MultipartUpload:
184
192
  MULTIPART_THRESHOLD = 100 * 1024 * 1024
185
193
  MULTIPART_CHUNK_SIZE = 100 * 1024 * 1024
@@ -548,8 +556,15 @@ class FalCDNFileRepository(FileRepository):
548
556
  }
549
557
 
550
558
 
559
+ # This is only available for internal users to have long-lived access tokens
551
560
  @dataclass
552
- class FalFileRepositoryV3(FileRepository):
561
+ class InternalFalFileRepositoryV3(FileRepository):
562
+ """
563
+ InternalFalFileRepositoryV3 is a file repository that uses the FAL CDN V3.
564
+ But generates and uses long-lived access tokens.
565
+ That way it can avoid the need to refresh the token for every upload.
566
+ """
567
+
553
568
  @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
554
569
  def save(
555
570
  self, file: FileData, object_lifecycle_preference: dict[str, str] | None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.5.13
3
+ Version: 1.5.15
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
@@ -19,7 +19,7 @@ Requires-Dist: grpc-interceptor<1,>=0.15.0
19
19
  Requires-Dist: colorama<1,>=0.4.6
20
20
  Requires-Dist: portalocker<3,>=2.7.0
21
21
  Requires-Dist: rich<14,>=13.3.2
22
- Requires-Dist: rich-argparse
22
+ Requires-Dist: rich_argparse
23
23
  Requires-Dist: packaging>=21.3
24
24
  Requires-Dist: pathspec<1,>=0.11.1
25
25
  Requires-Dist: pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3
@@ -30,6 +30,7 @@ Requires-Dist: httpx>=0.15.4
30
30
  Requires-Dist: attrs>=21.3.0
31
31
  Requires-Dist: python-dateutil<3,>=2.8.0
32
32
  Requires-Dist: types-python-dateutil<3,>=2.8.0
33
+ Requires-Dist: importlib-metadata>=4.4; python_version < "3.10"
33
34
  Requires-Dist: msgpack<2,>=1.0.7
34
35
  Requires-Dist: websockets<13,>=12.0
35
36
  Requires-Dist: pillow<11,>=10.2.0
@@ -37,10 +38,6 @@ Requires-Dist: pyjwt[crypto]<3,>=2.8.0
37
38
  Requires-Dist: uvicorn<1,>=0.29.0
38
39
  Requires-Dist: cookiecutter
39
40
  Requires-Dist: tomli
40
- Requires-Dist: importlib-metadata>=4.4; python_version < "3.10"
41
- Provides-Extra: dev
42
- Requires-Dist: fal[docs,test]; extra == "dev"
43
- Requires-Dist: openapi-python-client<1,>=0.14.1; extra == "dev"
44
41
  Provides-Extra: docs
45
42
  Requires-Dist: sphinx; extra == "docs"
46
43
  Requires-Dist: sphinx-rtd-theme; extra == "docs"
@@ -51,6 +48,9 @@ Requires-Dist: pytest-asyncio; extra == "test"
51
48
  Requires-Dist: pytest-xdist; extra == "test"
52
49
  Requires-Dist: flaky; extra == "test"
53
50
  Requires-Dist: boto3; extra == "test"
51
+ Provides-Extra: dev
52
+ Requires-Dist: fal[docs,test]; extra == "dev"
53
+ Requires-Dist: openapi-python-client<1,>=0.14.1; extra == "dev"
54
54
 
55
55
  [![PyPI](https://img.shields.io/pypi/v/fal.svg?logo=PyPI)](https://pypi.org/project/fal)
56
56
  [![Tests](https://img.shields.io/github/actions/workflow/status/fal-ai/fal/integration_tests.yaml?label=Tests)](https://github.com/fal-ai/fal/actions)
@@ -1,17 +1,17 @@
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=FA7UTvfx0_o4BJaj2jDQirEbUZnaIJumpWncG7rF-Rs,413
3
+ fal/_fal_version.py,sha256=299zYncL2JUBDyICATkgM5qBt_4iztg0ZseMesrRfvI,413
4
4
  fal/_serialization.py,sha256=rD2YiSa8iuzCaZohZwN_MPEB-PpSKbWRDeaIDpTEjyY,7653
5
5
  fal/_version.py,sha256=EBGqrknaf1WygENX-H4fBefLvHryvJBBGtVJetaB0NY,266
6
6
  fal/api.py,sha256=xTtPvDqaEHsq2lFsMwRZiHb4hzjVY3y6lV-xbzkSetI,43375
7
7
  fal/app.py,sha256=nLku84uTyK2VJRH_dGe_Ym8fNRsTvC3_5yolgjh9wlY,22429
8
- fal/apps.py,sha256=PjxIdwCL9tYxohajhdFo0MwtZGwVTJODtlerOrmSA6o,7251
8
+ fal/apps.py,sha256=-s3xVOclIQxAevLdbMcyhGkPGsx8x0VAZbRoYQG-F_I,10800
9
9
  fal/container.py,sha256=V7riyyq8AZGwEX9QaqRQDZyDN_bUKeRKV1OOZArXjL0,622
10
10
  fal/files.py,sha256=QgfYfMKmNobMPufrAP_ga1FKcIAlSbw18Iar1-0qepo,2650
11
11
  fal/flags.py,sha256=oWN_eidSUOcE9wdPK_77si3A1fpgOC0UEERPsvNLIMc,842
12
12
  fal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  fal/rest_client.py,sha256=kGBGmuyHfX1lR910EoKCYPjsyU8MdXawT_cW2q8Sajc,568
14
- fal/sdk.py,sha256=HAkOv0q53h4LPBdvjJHu_FST0Iq-SYzNKhx1qeKJZfs,22403
14
+ fal/sdk.py,sha256=HjlToPJkG0Z5h_D0D2FK43i3JFKeO4r2IhCGx4B82Z8,22564
15
15
  fal/sync.py,sha256=ZuIJA2-hTPNANG9B_NNJZUsO68EIdTH0dc9MzeVE2VU,4340
16
16
  fal/utils.py,sha256=9q_QrQBlQN3nZYA1kEGRfhJWi4RjnO4H1uQswfaei9w,2146
17
17
  fal/workflows.py,sha256=Zl4f6Bs085hY40zmqScxDUyCu7zXkukDbW02iYOLTTI,14805
@@ -27,7 +27,8 @@ fal/cli/debug.py,sha256=u_urnyFzSlNnrq93zz_GXE9FX4VyVxDoamJJyrZpFI0,1312
27
27
  fal/cli/deploy.py,sha256=ZBM4pLDDj9ZntlSoFvK_-ZGO-lAOHoZFkYXS-OAxXT0,7461
28
28
  fal/cli/doctor.py,sha256=U4ne9LX5gQwNblsYQ27XdO8AYDgbYjTO39EtxhwexRM,983
29
29
  fal/cli/keys.py,sha256=trDpA3LJu9S27qE_K8Hr6fKLK4vwVzbxUHq8TFrV4pw,3157
30
- fal/cli/main.py,sha256=_Wh_DQc02qwh-ZN7v41lZm0lDR1WseViXVOcqUlyWLg,2009
30
+ fal/cli/machine.py,sha256=BY8v7B3uGhjd6fKuPBGUOGShIntjMkphOlmF1EwFMzI,992
31
+ fal/cli/main.py,sha256=ivLtV5XYDUNB5FydVjBTL_yZCwFSYekODYh9RGDwYG0,2027
31
32
  fal/cli/parser.py,sha256=edCqFWYAQSOhrxeEK9BtFRlTEUAlG2JUDjS_vhZ_nHE,2868
32
33
  fal/cli/run.py,sha256=J1lSZ_wJIhrygSduMr0Wf2pQ8OUJlFbyH5KKUjxDF6w,1204
33
34
  fal/cli/secrets.py,sha256=740msFm7d41HruudlcfqUXlFl53N-WmChsQP9B9M9Po,2572
@@ -49,7 +50,7 @@ fal/toolkit/optimize.py,sha256=p75sovF0SmRP6zxzpIaaOmqlxvXB_xEz3XPNf59EF7w,1339
49
50
  fal/toolkit/file/__init__.py,sha256=FbNl6wD-P0aSSTUwzHt4HujBXrbC3ABmaigPQA4hRfg,70
50
51
  fal/toolkit/file/file.py,sha256=tq-zMoNLJ1NJc9g8_RTnoIZABnRhtTtQchyMULimMTY,9442
51
52
  fal/toolkit/file/types.py,sha256=MjZ6xAhKPv4rowLo2Vcbho0sX7AQ3lm3KFyYDcw0dL4,1845
52
- fal/toolkit/file/providers/fal.py,sha256=V93vQbbFevPPpPSGfgkWuaBCYZnSxjtK50Lhk-zQYi8,21016
53
+ fal/toolkit/file/providers/fal.py,sha256=9Rmbu_0XbMzzsfLuGJaCuRfVMFtdp2gS7ct1y7wqzVg,21549
53
54
  fal/toolkit/file/providers/gcp.py,sha256=iQtkoYUqbmKKpC5srVOYtrruZ3reGRm5lz4kM8bshgk,2247
54
55
  fal/toolkit/file/providers/r2.py,sha256=G2OHcCH2yWrVtXT4hWHEXUeEjFhbKO0koqHcd7hkczk,2871
55
56
  fal/toolkit/file/providers/s3.py,sha256=CfiA6rTBFfP-empp0cB9OW2c9F5iy0Z-kGwCs5HBICU,2524
@@ -127,8 +128,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
127
128
  openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
128
129
  openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
129
130
  openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
130
- fal-1.5.13.dist-info/METADATA,sha256=KnN1wHCLlqb8yKwr5voS0_O4n92BUNrnRhtGh0mYbl8,3997
131
- fal-1.5.13.dist-info/WHEEL,sha256=a7TGlA-5DaHMRrarXjVbQagU3Man_dCnGIWMJr5kRWo,91
132
- fal-1.5.13.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
133
- fal-1.5.13.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
134
- fal-1.5.13.dist-info/RECORD,,
131
+ fal-1.5.15.dist-info/METADATA,sha256=C4sigcaEzk4Yv8fuA7rJtKcr0AhaH3zyS7Qb1PQrFF4,3997
132
+ fal-1.5.15.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
133
+ fal-1.5.15.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
134
+ fal-1.5.15.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
135
+ fal-1.5.15.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.4.0)
2
+ Generator: setuptools (75.6.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5