fal 1.0.7__py3-none-any.whl → 1.0.8__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.0.7'
16
- __version_tuple__ = version_tuple = (1, 0, 7)
15
+ __version__ = version = '1.0.8'
16
+ __version_tuple__ = version_tuple = (1, 0, 8)
fal/api.py CHANGED
@@ -5,11 +5,12 @@ import os
5
5
  import sys
6
6
  import threading
7
7
  from collections import defaultdict
8
- from concurrent.futures import ThreadPoolExecutor
8
+ from concurrent.futures import Future, ThreadPoolExecutor
9
9
  from contextlib import asynccontextmanager, suppress
10
10
  from dataclasses import dataclass, field, replace
11
11
  from functools import wraps
12
12
  from os import PathLike
13
+ from queue import Queue
13
14
  from typing import (
14
15
  Any,
15
16
  Callable,
@@ -72,6 +73,8 @@ SERVE_REQUIREMENTS = [
72
73
  ]
73
74
 
74
75
 
76
+ THREAD_POOL = ThreadPoolExecutor()
77
+
75
78
  @dataclass
76
79
  class FalServerlessError(FalServerlessException):
77
80
  message: str
@@ -87,6 +90,31 @@ class FalMissingDependencyError(FalServerlessError):
87
90
  ...
88
91
 
89
92
 
93
+ @dataclass
94
+ class SpawnInfo:
95
+ future: Future | None = None
96
+ logs: Queue = field(default_factory=Queue)
97
+ _url_ready: threading.Event = field(default_factory=threading.Event)
98
+ _url: str | None = None
99
+ stream: Any = None
100
+
101
+ @property
102
+ def return_value(self):
103
+ if self.future is None:
104
+ raise ValueError
105
+ return self.future.result()
106
+
107
+ @property
108
+ def url(self):
109
+ self._url_ready.wait()
110
+ return self._url
111
+
112
+ @url.setter
113
+ def url(self, value):
114
+ self._url_ready.set()
115
+ self._url = value
116
+
117
+
90
118
  @dataclass
91
119
  class Host(Generic[ArgsT, ReturnT]):
92
120
  """The physical environment where the isolated code
@@ -150,6 +178,15 @@ class Host(Generic[ArgsT, ReturnT]):
150
178
  """Run the given function in the isolated environment."""
151
179
  raise NotImplementedError
152
180
 
181
+ def spawn(
182
+ self,
183
+ func: Callable[ArgsT, ReturnT],
184
+ options: Options,
185
+ args: tuple[Any, ...],
186
+ kwargs: dict[str, Any],
187
+ ) -> SpawnInfo:
188
+ raise NotImplementedError
189
+
153
190
 
154
191
  def cached(func: Callable[ArgsT, ReturnT]) -> Callable[ArgsT, ReturnT]:
155
192
  """Cache the result of the given function in-memory."""
@@ -444,12 +481,13 @@ class FalServerlessHost(Host):
444
481
  return None
445
482
 
446
483
  @_handle_grpc_error()
447
- def run(
484
+ def _run(
448
485
  self,
449
486
  func: Callable[..., ReturnT],
450
487
  options: Options,
451
488
  args: tuple[Any, ...],
452
489
  kwargs: dict[str, Any],
490
+ result_handler: Callable[..., None],
453
491
  ) -> ReturnT:
454
492
  environment_options = options.environment.copy()
455
493
  environment_options.setdefault("python_version", active_python())
@@ -490,8 +528,7 @@ class FalServerlessHost(Host):
490
528
  machine_requirements=machine_requirements,
491
529
  setup_function=setup_function,
492
530
  ):
493
- for log in partial_result.logs:
494
- self._log_printer.print(log)
531
+ result_handler(partial_result)
495
532
 
496
533
  if partial_result.status.state is not HostedRunState.IN_PROGRESS:
497
534
  state = partial_result.status.state
@@ -512,6 +549,47 @@ class FalServerlessHost(Host):
512
549
  return cast(ReturnT, return_value)
513
550
 
514
551
 
552
+ def run(
553
+ self,
554
+ func: Callable[..., ReturnT],
555
+ options: Options,
556
+ args: tuple[Any, ...],
557
+ kwargs: dict[str, Any],
558
+ ) -> ReturnT:
559
+ def result_handler(partial_result):
560
+ for log in partial_result.logs:
561
+ self._log_printer.print(log)
562
+
563
+ return self._run(func, options, args, kwargs, result_handler=result_handler)
564
+
565
+ def spawn(
566
+ self,
567
+ func: Callable[..., ReturnT],
568
+ options: Options,
569
+ args: tuple[Any, ...],
570
+ kwargs: dict[str, Any],
571
+ ) -> SpawnInfo:
572
+ ret = SpawnInfo()
573
+
574
+ def result_handler(partial_result):
575
+ ret.stream = partial_result.stream
576
+ for log in partial_result.logs:
577
+ if "Access your exposed service at" in log.message:
578
+ ret.url = log.message.rsplit()[-1]
579
+ ret.logs.put(log)
580
+
581
+ THREAD_POOL.submit(
582
+ self._run,
583
+ func,
584
+ options,
585
+ args,
586
+ kwargs,
587
+ result_handler=result_handler,
588
+ )
589
+
590
+ return ret
591
+
592
+
515
593
  @dataclass
516
594
  class Options:
517
595
  host: BasicConfig = field(default_factory=dict)
@@ -1052,6 +1130,14 @@ class IsolatedFunction(Generic[ArgsT, ReturnT]):
1052
1130
  )
1053
1131
  return future
1054
1132
 
1133
+ def spawn(self, *args: ArgsT.args, **kwargs: ArgsT.kwargs):
1134
+ return self.host.spawn(
1135
+ self.func,
1136
+ self.options,
1137
+ args,
1138
+ kwargs,
1139
+ )
1140
+
1055
1141
  def __call__(self, *args: ArgsT.args, **kwargs: ArgsT.kwargs) -> ReturnT:
1056
1142
  try:
1057
1143
  return self.host.run(
fal/app.py CHANGED
@@ -4,10 +4,12 @@ import inspect
4
4
  import json
5
5
  import os
6
6
  import re
7
+ import time
7
8
  import typing
8
- from contextlib import asynccontextmanager
9
+ from contextlib import asynccontextmanager, contextmanager
9
10
  from typing import Any, Callable, ClassVar, TypeVar
10
11
 
12
+ import httpx
11
13
  from fastapi import FastAPI
12
14
 
13
15
  import fal.api
@@ -69,6 +71,66 @@ def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
69
71
  return fn
70
72
 
71
73
 
74
+ class EndpointClient:
75
+ def __init__(self, url, endpoint, signature):
76
+ self.url = url
77
+ self.endpoint = endpoint
78
+ self.signature = signature
79
+
80
+ annotations = endpoint.__annotations__ or {}
81
+ self.return_type = annotations.get("return") or None
82
+
83
+ def __call__(self, data):
84
+ with httpx.Client() as client:
85
+ resp = client.post(self.url + self.signature.path, json=dict(data))
86
+ resp.raise_for_status()
87
+ resp_dict = resp.json()
88
+
89
+ if not self.return_type:
90
+ return resp_dict
91
+
92
+ return self.return_type(**resp_dict)
93
+
94
+
95
+ class AppClient:
96
+ def __init__(self, cls, url):
97
+ self.url = url
98
+ self.cls = cls
99
+
100
+ for name, endpoint in inspect.getmembers(cls, inspect.isfunction):
101
+ signature = getattr(endpoint, "route_signature", None)
102
+ if signature is None:
103
+ continue
104
+
105
+ setattr(self, name, EndpointClient(self.url, endpoint, signature))
106
+
107
+ @classmethod
108
+ @contextmanager
109
+ def connect(cls, app_cls):
110
+ app = wrap_app(app_cls)
111
+ info = app.spawn()
112
+ try:
113
+ with httpx.Client() as client:
114
+ retries = 100
115
+ while retries:
116
+ resp = client.get(info.url + "/health")
117
+ if resp.is_success:
118
+ break
119
+ elif resp.status_code != 500:
120
+ resp.raise_for_status()
121
+ time.sleep(0.1)
122
+ retries -= 1
123
+
124
+ yield cls(app_cls, info.url)
125
+ finally:
126
+ info.stream.cancel()
127
+
128
+ def health(self):
129
+ with httpx.Client() as client:
130
+ resp = client.get(self.url + "/health")
131
+ resp.raise_for_status()
132
+ return resp.json()
133
+
72
134
 
73
135
  PART_FINDER_RE = re.compile(r"[A-Z][a-z]*")
74
136
 
fal/cli/doctor.py ADDED
@@ -0,0 +1,37 @@
1
+ import os
2
+ import platform
3
+
4
+
5
+ def _doctor(args):
6
+ import isolate
7
+ from rich.table import Table
8
+
9
+ import fal
10
+
11
+ table = Table(show_header=False, show_lines=False, box=None)
12
+ table.add_column("name", no_wrap=True, style="bold")
13
+ table.add_column("value", no_wrap=True)
14
+
15
+ table.add_row("fal", fal.__version__)
16
+ table.add_row("isolate", isolate.__version__)
17
+
18
+ table.add_row("", "")
19
+ table.add_row("python", platform.python_version())
20
+ table.add_row("platform", platform.platform())
21
+
22
+ table.add_row("", "")
23
+ table.add_row("FAL_HOST", fal.flags.GRPC_HOST)
24
+ table.add_row("FAL_KEY", os.getenv("FAL_KEY", "").split(":")[0])
25
+
26
+ args.console.print(table)
27
+
28
+
29
+ def add_parser(main_subparsers, parents):
30
+ doctor_help = "fal version and misc environment information."
31
+ parser = main_subparsers.add_parser(
32
+ "doctor",
33
+ description=doctor_help,
34
+ help=doctor_help,
35
+ parents=parents,
36
+ )
37
+ parser.set_defaults(func=_doctor)
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, deploy, keys, run, secrets
9
+ from . import apps, auth, deploy, doctor, keys, 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]:
34
+ for cmd in [auth, apps, deploy, run, keys, secrets, doctor]:
35
35
  cmd.add_parser(subparsers, parents)
36
36
 
37
37
  return parser
fal/sdk.py CHANGED
@@ -224,6 +224,7 @@ class HostedRunResult(Generic[ResultT]):
224
224
  status: HostedRunStatus
225
225
  logs: list[Log] = field(default_factory=list)
226
226
  result: ResultT | None = None
227
+ stream: Any = None
227
228
 
228
229
 
229
230
  @dataclass
@@ -569,8 +570,11 @@ class FalServerlessConnection:
569
570
  request.setup_func.MergeFrom(
570
571
  to_serialized_object(setup_function, serialization_method)
571
572
  )
572
- for partial_result in self.stub.Run(request):
573
- yield from_grpc(partial_result)
573
+ stream = self.stub.Run(request)
574
+ for partial_result in stream:
575
+ res = from_grpc(partial_result)
576
+ res.stream = stream
577
+ yield res
574
578
 
575
579
  def create_alias(
576
580
  self,
@@ -98,6 +98,7 @@ class FalCDNFileRepository(FileRepository):
98
98
  **self.auth_headers,
99
99
  "Accept": "application/json",
100
100
  "Content-Type": file.content_type,
101
+ "X-Fal-File-Name": file.file_name,
101
102
  "X-Fal-Object-Lifecycle-Preference": json.dumps(
102
103
  dataclasses.asdict(GLOBAL_LIFECYCLE_PREFERENCE)
103
104
  ),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.0.7
3
+ Version: 1.0.8
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
@@ -43,6 +43,7 @@ Requires-Dist: pytest <8 ; extra == 'test'
43
43
  Requires-Dist: pytest-asyncio ; extra == 'test'
44
44
  Requires-Dist: pytest-xdist ; extra == 'test'
45
45
  Requires-Dist: flaky ; extra == 'test'
46
+ Requires-Dist: boto3 ; extra == 'test'
46
47
 
47
48
  [![PyPI](https://img.shields.io/pypi/v/fal.svg?logo=PyPI)](https://pypi.org/project/fal)
48
49
  [![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,16 +1,16 @@
1
1
  fal/__init__.py,sha256=wXs1G0gSc7ZK60-bHe-B2m0l_sA6TrFk4BxY0tMoLe8,784
2
2
  fal/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
3
- fal/_fal_version.py,sha256=B_2jTRPLk-a7dhEz8Rto7VRGwxfpXrqO4mYrudjn7bw,411
3
+ fal/_fal_version.py,sha256=vy0P95Si-KzUl6NsixjrWEHfr5xcmkA_oO_S3bQRmvE,411
4
4
  fal/_serialization.py,sha256=7urrZXw99qmsK-RkjCurk6Va4TMEfDIMajkzKbSW4j4,7655
5
5
  fal/_version.py,sha256=EBGqrknaf1WygENX-H4fBefLvHryvJBBGtVJetaB0NY,266
6
- fal/api.py,sha256=SX8o5Uc5TAl2sZ3u1RuNKEpj7g_OPZOwVTyKsSxNgjk,38045
7
- fal/app.py,sha256=Sublwqso-gRlJ7oNX00JY8YK0P0WLTrB2c57GQH8Oxw,13946
6
+ fal/api.py,sha256=Qkp97ozww5EBXHq1oSikuDMGB0m7ZaYb5TCvROAknsA,40174
7
+ fal/app.py,sha256=duOf_YKE8o30hmhNtF9zvkT8wlKYXW7hdQLJtPrXHik,15793
8
8
  fal/apps.py,sha256=UhR6mq8jBiTAp-QvUnvbnMNcuJ5wHIKSqdlfyx8aBQ8,6829
9
9
  fal/container.py,sha256=V7riyyq8AZGwEX9QaqRQDZyDN_bUKeRKV1OOZArXjL0,622
10
10
  fal/flags.py,sha256=oWN_eidSUOcE9wdPK_77si3A1fpgOC0UEERPsvNLIMc,842
11
11
  fal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  fal/rest_client.py,sha256=kGBGmuyHfX1lR910EoKCYPjsyU8MdXawT_cW2q8Sajc,568
13
- fal/sdk.py,sha256=eHcg5TRouGL5d_9-p44x6lQUCYXVXdul6iEETCUov5Q,19976
13
+ fal/sdk.py,sha256=_1aK9VSzHPoDPbLm3nbhE4213zI66qAsgYs1Y1Wc3J8,20077
14
14
  fal/sync.py,sha256=ZuIJA2-hTPNANG9B_NNJZUsO68EIdTH0dc9MzeVE2VU,4340
15
15
  fal/utils.py,sha256=MFDs-eO3rBgc3jqIwBpfBtvvK5tbzAYWMzHV-tTVLic,1754
16
16
  fal/workflows.py,sha256=4rjqL4xB6GHLJsqTplJmAvpd6uHZJ28sc8su33BFXEo,14682
@@ -22,8 +22,9 @@ fal/cli/apps.py,sha256=Dqcx7ewUNY6dh0bYqRlOCHVDzmkdpFxe9CNThEz7HX4,8144
22
22
  fal/cli/auth.py,sha256=--MhfHGwxmtHbRkGioyn1prKn_U-pBzbz0G_QeZou-U,1352
23
23
  fal/cli/debug.py,sha256=1doDNwoaPDfLQginGNBxpC20dZYs5UxIojflDvV1Q04,1342
24
24
  fal/cli/deploy.py,sha256=S_HIMLqDpGyzDdbiIxudRizwjGoAaHpN-sXcl2uCaQ4,4329
25
+ fal/cli/doctor.py,sha256=U4ne9LX5gQwNblsYQ27XdO8AYDgbYjTO39EtxhwexRM,983
25
26
  fal/cli/keys.py,sha256=-9N6ZY6rW-_IE9tpupgaBPDGjGdKB3HKqU2g9daM3Xc,3109
26
- fal/cli/main.py,sha256=bLwuHlkCiroVDZ21WcL_qAv71k5kNTloWreZHuO0jR4,1977
27
+ fal/cli/main.py,sha256=MxETDhqIT37quMbmofSMxBcAFOhnEHjpQ_pYEtOhApM,1993
27
28
  fal/cli/parser.py,sha256=r1hd5e8Jq6yzDZw8-S0On1EjJbjRtHMuVuHC6MlvUj4,2835
28
29
  fal/cli/run.py,sha256=NXwzkAWCKrRwgoMLsBOgW7RJPJW4IgSTrG85q2iePyk,894
29
30
  fal/cli/secrets.py,sha256=mgHp3gBr8d2su7wBApeADKWHPkYu2ueB6gG3eNMETh8,2595
@@ -44,7 +45,7 @@ fal/toolkit/optimize.py,sha256=p75sovF0SmRP6zxzpIaaOmqlxvXB_xEz3XPNf59EF7w,1339
44
45
  fal/toolkit/file/__init__.py,sha256=FbNl6wD-P0aSSTUwzHt4HujBXrbC3ABmaigPQA4hRfg,70
45
46
  fal/toolkit/file/file.py,sha256=r8PzNCgv8Gkj6s1zM0yW-pcMKIouyaiEH06iBue8MwM,6066
46
47
  fal/toolkit/file/types.py,sha256=9CqDh8SmNJNzfsrvtj468uo2SprJH9rOk8KMhhfU73c,1050
47
- fal/toolkit/file/providers/fal.py,sha256=AtYYXBM72leb8OmP6pvYiNZCWhEmoi9VfpckJ8FuZto,3700
48
+ fal/toolkit/file/providers/fal.py,sha256=ACxkfQ9kmMJ1xP8dNUHzkLkJJLZvoybTpVt8avGb8Nw,3747
48
49
  fal/toolkit/file/providers/gcp.py,sha256=7Lg7BXoHKkFu0jkGv3_vKh2Ks6eRfDMbw31N3mvDUtk,1913
49
50
  fal/toolkit/file/providers/r2.py,sha256=YW5aJBOX41MQxfx1rA_f-IiJhAPMZ5md0cxBcg3y08I,2537
50
51
  fal/toolkit/image/__init__.py,sha256=qNLyXsBWysionUjbeWbohLqWlw3G_UpzunamkZd_JLQ,71
@@ -93,8 +94,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
93
94
  openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
94
95
  openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
95
96
  openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
96
- fal-1.0.7.dist-info/METADATA,sha256=UCGyga5ckEOE5PUeB0rC68NJou9Ehq7j9d34c79HgyY,3738
97
- fal-1.0.7.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
98
- fal-1.0.7.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
99
- fal-1.0.7.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
100
- fal-1.0.7.dist-info/RECORD,,
97
+ fal-1.0.8.dist-info/METADATA,sha256=J6Lpic62ohISriecQ7IwHdL2PjZrNhrWVumFV7vhhJE,3777
98
+ fal-1.0.8.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
99
+ fal-1.0.8.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
100
+ fal-1.0.8.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
101
+ fal-1.0.8.dist-info/RECORD,,
File without changes