fal 1.5.14__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.14'
16
- __version_tuple__ = version_tuple = (1, 5, 14)
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.5.14
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
@@ -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=oG_gCDfdqQpBpru9Up8ucsA7xcxUMXQcBoHZ3jEU3jE,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
@@ -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.14.dist-info/METADATA,sha256=ud8NQFdRYo9OwCPNFEkPYK9xSTgF-GFjDIAeQ4TbwVk,3997
131
- fal-1.5.14.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
132
- fal-1.5.14.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
133
- fal-1.5.14.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
134
- fal-1.5.14.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,,
File without changes