fal 1.5.14__py3-none-any.whl → 1.5.16__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 +2 -2
- fal/apps.py +134 -1
- fal/cli/machine.py +43 -0
- fal/cli/main.py +2 -2
- fal/sdk.py +4 -0
- {fal-1.5.14.dist-info → fal-1.5.16.dist-info}/METADATA +1 -1
- {fal-1.5.14.dist-info → fal-1.5.16.dist-info}/RECORD +10 -9
- {fal-1.5.14.dist-info → fal-1.5.16.dist-info}/WHEEL +0 -0
- {fal-1.5.14.dist-info → fal-1.5.16.dist-info}/entry_points.txt +0 -0
- {fal-1.5.14.dist-info → fal-1.5.16.dist-info}/top_level.txt +0 -0
fal/_fal_version.py
CHANGED
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,132 @@ 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]) -> bytes:
|
|
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) -> Iterator[str | bytes]:
|
|
322
|
+
while True:
|
|
323
|
+
try:
|
|
324
|
+
with self._recv() as res:
|
|
325
|
+
if self._is_meta(res):
|
|
326
|
+
# Raise so we dont consume the message
|
|
327
|
+
raise _MetaMessageFound()
|
|
328
|
+
|
|
329
|
+
yield res
|
|
330
|
+
except _MetaMessageFound:
|
|
331
|
+
break
|
|
332
|
+
|
|
333
|
+
def recv(self) -> bytes:
|
|
334
|
+
start = self._recv_meta("start")
|
|
335
|
+
request_id = start["request_id"]
|
|
336
|
+
|
|
337
|
+
response = b""
|
|
338
|
+
for part in self._recv_response():
|
|
339
|
+
if isinstance(part, str):
|
|
340
|
+
response += part.encode()
|
|
341
|
+
else:
|
|
342
|
+
response += part
|
|
343
|
+
|
|
344
|
+
end = self._recv_meta("end")
|
|
345
|
+
if end["request_id"] != request_id:
|
|
346
|
+
raise ValueError("Mismatched request_id in end message")
|
|
347
|
+
|
|
348
|
+
return response
|
|
349
|
+
|
|
350
|
+
def stream(self) -> Iterator[str | bytes]:
|
|
351
|
+
start = self._recv_meta("start")
|
|
352
|
+
request_id = start["request_id"]
|
|
353
|
+
|
|
354
|
+
yield from self._recv_response()
|
|
355
|
+
|
|
356
|
+
# Make sure we consume the end message
|
|
357
|
+
end = self._recv_meta("end")
|
|
358
|
+
if end["request_id"] != request_id:
|
|
359
|
+
raise ValueError("Mismatched request_id in end message")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@contextmanager
|
|
363
|
+
def ws(app_id: str, *, path: str = "") -> Iterator[_WSConnection]:
|
|
364
|
+
"""Connect to a HTTP endpoint but with websocket protocol. This is an internal and
|
|
365
|
+
experimental API, use it at your own risk."""
|
|
366
|
+
|
|
367
|
+
from websockets.sync import client
|
|
368
|
+
|
|
369
|
+
app_id = _backwards_compatible_app_id(app_id)
|
|
370
|
+
url = _WS_URL_FORMAT.format(app_id=app_id)
|
|
371
|
+
if path:
|
|
372
|
+
_path = path[len("/") :] if path.startswith("/") else path
|
|
373
|
+
url += "/" + _path
|
|
374
|
+
|
|
375
|
+
creds = get_default_credentials()
|
|
376
|
+
|
|
377
|
+
with client.connect(
|
|
378
|
+
url, additional_headers=creds.to_headers(), open_timeout=90
|
|
379
|
+
) as ws:
|
|
380
|
+
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
|
@@ -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=
|
|
3
|
+
fal/_fal_version.py,sha256=mHfZXquAABTFJcCTtEKL7lltRkyfT160CZojEvwd1Aw,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=
|
|
8
|
+
fal/apps.py,sha256=RpmElElJnDYjsTRQOdNYiJwd74GEOGYA38L5O5GzNEg,11068
|
|
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=
|
|
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/
|
|
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.
|
|
131
|
-
fal-1.5.
|
|
132
|
-
fal-1.5.
|
|
133
|
-
fal-1.5.
|
|
134
|
-
fal-1.5.
|
|
131
|
+
fal-1.5.16.dist-info/METADATA,sha256=rXM_O4xtD7xvwYD3gp1Bsh-Dtgc7M3IvsTv3EiVjVlg,3997
|
|
132
|
+
fal-1.5.16.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
133
|
+
fal-1.5.16.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
|
|
134
|
+
fal-1.5.16.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
|
|
135
|
+
fal-1.5.16.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|