fal 1.25.0__py3-none-any.whl → 1.26.1__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
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '1.25.0'
21
- __version_tuple__ = version_tuple = (1, 25, 0)
20
+ __version__ = version = '1.26.1'
21
+ __version_tuple__ = version_tuple = (1, 26, 1)
fal/api.py CHANGED
@@ -319,24 +319,26 @@ def _handle_grpc_error():
319
319
  try:
320
320
  return fn(*args, **kwargs)
321
321
  except grpc.RpcError as e:
322
+ msg = e.details() or str(e)
322
323
  if e.code() == grpc.StatusCode.UNAVAILABLE:
323
324
  raise FalServerlessError(
324
325
  "Could not reach fal host. "
325
326
  "This is most likely a transient problem. "
326
- "Please, try again."
327
+ "If it persists, please reach out to support@fal.ai with the following details: " # noqa: E501
328
+ f"{msg}"
327
329
  )
328
- elif e.details().endswith("died with <Signals.SIGKILL: 9>.`."):
330
+ elif msg.endswith("died with <Signals.SIGKILL: 9>.`."):
329
331
  raise FalServerlessError(
330
332
  "Isolated function crashed. "
331
333
  "This is likely due to resource overflow. "
332
334
  "You can try again by setting a bigger `machine_type`"
333
335
  )
334
336
  elif e.code() == grpc.StatusCode.INVALID_ARGUMENT and (
335
- "The function function could not be deserialized" in e.details()
337
+ "The function function could not be deserialized" in msg
336
338
  ):
337
- raise FalMissingDependencyError(e.details()) from None
339
+ raise FalMissingDependencyError(msg) from None
338
340
  else:
339
- raise FalServerlessError(e.details())
341
+ raise FalServerlessError(msg)
340
342
 
341
343
  return handler
342
344
 
@@ -498,7 +500,7 @@ class FalServerlessHost(Host):
498
500
  partial_func,
499
501
  environments,
500
502
  application_name=application_name,
501
- application_auth_mode=application_auth_mode,
503
+ auth_mode=application_auth_mode,
502
504
  machine_requirements=machine_requirements,
503
505
  metadata=metadata,
504
506
  deployment_strategy=deployment_strategy,
fal/app.py CHANGED
@@ -197,7 +197,14 @@ class AppClient:
197
197
 
198
198
  @classmethod
199
199
  @contextmanager
200
- def connect(cls, app_cls):
200
+ def connect(
201
+ cls,
202
+ app_cls,
203
+ *,
204
+ health_request_timeout: int = 30,
205
+ startup_timeout: int = 60,
206
+ health_check_interval: float = 0.5,
207
+ ):
201
208
  app = wrap_app(app_cls)
202
209
  info = app.spawn()
203
210
  _shutdown_event = threading.Event()
@@ -214,20 +221,50 @@ class AppClient:
214
221
  _log_printer.start()
215
222
 
216
223
  try:
224
+ if info.url is None:
225
+ raise AppClientError(
226
+ "App spawn failed: no URL returned",
227
+ status_code=500,
228
+ )
229
+
230
+ start_time = time.perf_counter()
231
+ url = info.url + "/health"
232
+ last_error = None
233
+ attempt = 0
234
+
217
235
  with httpx.Client() as client:
218
- retries = 100
219
- for _ in range(retries):
220
- url = info.url + "/health"
221
- resp = client.get(url, timeout=60)
222
-
223
- if resp.is_success:
224
- break
225
- elif resp.status_code not in (500, 404):
226
- raise AppClientError(
227
- f"Failed to GET {url}: {resp.status_code} {resp.text}",
228
- status_code=resp.status_code,
236
+ while time.perf_counter() - start_time < startup_timeout:
237
+ attempt += 1
238
+
239
+ try:
240
+ resp = client.get(url, timeout=health_request_timeout)
241
+ except httpx.TimeoutException:
242
+ last_error = (
243
+ f"Request timed out after {health_request_timeout} seconds"
229
244
  )
230
- time.sleep(0.1)
245
+ except httpx.TransportError as e:
246
+ last_error = f"Network error: {e}"
247
+ else:
248
+ if resp.is_success:
249
+ break
250
+
251
+ if resp.status_code in (500, 404):
252
+ last_error = f"Server not ready (HTTP {resp.status_code})"
253
+ else:
254
+ raise AppClientError(
255
+ "Health check failed with non-retryable error: "
256
+ f"{resp.status_code} {resp.text}",
257
+ status_code=resp.status_code,
258
+ )
259
+
260
+ time.sleep(health_check_interval)
261
+ else:
262
+ # retry loop completed without success
263
+ raise AppClientError(
264
+ f"Health check failed after {startup_timeout}s "
265
+ f"({attempt} attempts). Last error: {last_error}",
266
+ status_code=500,
267
+ )
231
268
 
232
269
  client = cls(app_cls, info.url)
233
270
  yield client
fal/cli/apps.py CHANGED
@@ -270,7 +270,7 @@ def _add_set_rev_parser(subparsers, parents):
270
270
  parser.add_argument(
271
271
  "--auth",
272
272
  choices=ALIAS_AUTH_MODES,
273
- default="private",
273
+ default=None,
274
274
  help="Application authentication mode.",
275
275
  )
276
276
  parser.set_defaults(func=_set_rev)
@@ -310,7 +310,11 @@ def _add_runners_parser(subparsers, parents):
310
310
  def _delete(args):
311
311
  client = get_client(args.host, args.team)
312
312
  with client.connect() as connection:
313
- connection.delete_alias(args.app_name)
313
+ res = connection.delete_alias(args.app_name)
314
+ if res is None:
315
+ args.console.print(f"Application {args.app_name!r} not found.")
316
+ else:
317
+ args.console.print(f"Application {args.app_name!r} deleted ({res})")
314
318
 
315
319
 
316
320
  def _add_delete_parser(subparsers, parents):
fal/files.py CHANGED
@@ -1,5 +1,8 @@
1
+ import concurrent.futures
2
+ import math
1
3
  import os
2
4
  import posixpath
5
+ from concurrent.futures import ThreadPoolExecutor
3
6
  from functools import cached_property
4
7
  from typing import TYPE_CHECKING
5
8
 
@@ -9,6 +12,19 @@ if TYPE_CHECKING:
9
12
  import httpx
10
13
 
11
14
  USER_AGENT = "fal-sdk/1.14.0 (python)"
15
+ MULTIPART_THRESHOLD = 10 * 1024 * 1024 # 10MB
16
+ MULTIPART_CHUNK_SIZE = 10 * 1024 * 1024 # 10MB
17
+ MULTIPART_WORKERS = 2 # only 2 because our REST is currently struggling with more
18
+
19
+
20
+ def _compute_md5(lpath, chunk_size=8192):
21
+ import hashlib
22
+
23
+ hasher = hashlib.md5()
24
+ with open(lpath, "rb") as fobj:
25
+ for chunk in iter(lambda: fobj.read(chunk_size), b""):
26
+ hasher.update(chunk)
27
+ return hasher.hexdigest()
12
28
 
13
29
 
14
30
  class FalFileSystem(AbstractFileSystem):
@@ -95,16 +111,90 @@ class FalFileSystem(AbstractFileSystem):
95
111
  response = self._request("GET", f"/files/file/{rpath}")
96
112
  fobj.write(response.content)
97
113
 
114
+ def _put_file_part(self, rpath, lpath, upload_id, part_number, chunk_size):
115
+ offset = (part_number - 1) * chunk_size
116
+ with open(lpath, "rb") as fobj:
117
+ fobj.seek(offset)
118
+ chunk = fobj.read(chunk_size)
119
+ response = self._request(
120
+ "PUT",
121
+ f"/files/file/multipart/{rpath}/{upload_id}/{part_number}",
122
+ files={"file_upload": (posixpath.basename(lpath), chunk)},
123
+ )
124
+ data = response.json()
125
+ return {
126
+ "part_number": data["part_number"],
127
+ "etag": data["etag"],
128
+ }
129
+
130
+ def _put_file_multipart(self, lpath, rpath, size, progress):
131
+ response = self._request(
132
+ "POST",
133
+ f"/files/file/multipart/{rpath}/initiate",
134
+ )
135
+ upload_id = response.json()["upload_id"]
136
+
137
+ parts = []
138
+ num_parts = math.ceil(size / MULTIPART_CHUNK_SIZE)
139
+ md5 = _compute_md5(lpath)
140
+
141
+ task = progress.add_task(f"{os.path.basename(lpath)}", total=num_parts)
142
+
143
+ with ThreadPoolExecutor(max_workers=MULTIPART_WORKERS) as executor:
144
+ futures = []
145
+
146
+ for part_number in range(1, num_parts + 1):
147
+ futures.append(
148
+ executor.submit(
149
+ self._put_file_part,
150
+ rpath,
151
+ lpath,
152
+ upload_id,
153
+ part_number,
154
+ MULTIPART_CHUNK_SIZE,
155
+ )
156
+ )
157
+
158
+ for future in concurrent.futures.as_completed(futures):
159
+ parts.append(future.result())
160
+ progress.advance(task)
161
+
162
+ response = self._request(
163
+ "POST",
164
+ f"/files/file/multipart/{rpath}/{upload_id}/complete",
165
+ json={"parts": parts},
166
+ )
167
+ data = response.json()
168
+ if data["etag"] != md5:
169
+ raise RuntimeError(
170
+ f"MD5 mismatch on {rpath}: {data['etag']} != {md5}, "
171
+ "please contact support"
172
+ )
173
+
98
174
  def put_file(self, lpath, rpath, mode="overwrite", **kwargs):
175
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
176
+
99
177
  if os.path.isdir(lpath):
100
178
  return
101
179
 
102
- with open(lpath, "rb") as fobj:
103
- self._request(
104
- "POST",
105
- f"/files/file/local/{rpath}",
106
- files={"file_upload": (posixpath.basename(lpath), fobj, "text/plain")},
107
- )
180
+ size = os.path.getsize(lpath)
181
+ with Progress(
182
+ SpinnerColumn(),
183
+ TextColumn("[progress.description]{task.description}"),
184
+ BarColumn(),
185
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
186
+ ) as progress:
187
+ if size > MULTIPART_THRESHOLD:
188
+ self._put_file_multipart(lpath, rpath, size, progress)
189
+ else:
190
+ task = progress.add_task(f"{os.path.basename(lpath)}", total=1)
191
+ with open(lpath, "rb") as fobj:
192
+ self._request(
193
+ "POST",
194
+ f"/files/file/local/{rpath}",
195
+ files={"file_upload": (posixpath.basename(lpath), fobj)},
196
+ )
197
+ progress.advance(task)
108
198
  self.dircache.clear()
109
199
 
110
200
  def put_file_from_url(self, url, rpath, mode="overwrite", **kwargs):
fal/sdk.py CHANGED
@@ -45,6 +45,9 @@ logger = get_logger(__name__)
45
45
  patch_pickle()
46
46
 
47
47
 
48
+ AuthMode = Optional[Literal["public", "private", "shared"]]
49
+
50
+
48
51
  class ServerCredentials:
49
52
  def to_grpc(self) -> grpc.ChannelCredentials:
50
53
  raise NotImplementedError
@@ -565,7 +568,7 @@ class FalServerlessConnection:
565
568
  function: Callable[..., ResultT],
566
569
  environments: list[isolate_proto.EnvironmentDefinition],
567
570
  application_name: str | None = None,
568
- application_auth_mode: Literal["public", "private", "shared"] | None = None,
571
+ auth_mode: AuthMode = None,
569
572
  *,
570
573
  serialization_method: str = _DEFAULT_SERIALIZATION_METHOD,
571
574
  machine_requirements: MachineRequirements | None = None,
@@ -598,13 +601,14 @@ class FalServerlessConnection:
598
601
  else:
599
602
  wrapped_requirements = None
600
603
 
601
- auth_mode = None
602
- if application_auth_mode == "public":
603
- auth_mode = isolate_proto.ApplicationAuthMode.PUBLIC
604
- elif application_auth_mode == "shared":
605
- auth_mode = isolate_proto.ApplicationAuthMode.SHARED
606
- elif application_auth_mode == "private":
607
- auth_mode = isolate_proto.ApplicationAuthMode.PRIVATE
604
+ if auth_mode == "public":
605
+ auth = isolate_proto.ApplicationAuthMode.PUBLIC
606
+ elif auth_mode == "shared":
607
+ auth = isolate_proto.ApplicationAuthMode.SHARED
608
+ elif auth_mode == "private":
609
+ auth = isolate_proto.ApplicationAuthMode.PRIVATE
610
+ else:
611
+ auth = None
608
612
 
609
613
  struct_metadata = None
610
614
  if metadata:
@@ -620,7 +624,7 @@ class FalServerlessConnection:
620
624
  environments=environments,
621
625
  machine_requirements=wrapped_requirements,
622
626
  application_name=application_name,
623
- auth_mode=auth_mode,
627
+ auth_mode=auth,
624
628
  metadata=struct_metadata,
625
629
  deployment_strategy=deployment_strategy_proto,
626
630
  scale=scale,
@@ -730,14 +734,16 @@ class FalServerlessConnection:
730
734
  self,
731
735
  alias: str,
732
736
  revision: str,
733
- auth_mode: Literal["public", "private", "shared"],
737
+ auth_mode: AuthMode,
734
738
  ):
735
739
  if auth_mode == "public":
736
740
  auth = isolate_proto.ApplicationAuthMode.PUBLIC
737
741
  elif auth_mode == "shared":
738
742
  auth = isolate_proto.ApplicationAuthMode.SHARED
739
- else:
743
+ elif auth_mode == "private":
740
744
  auth = isolate_proto.ApplicationAuthMode.PRIVATE
745
+ else:
746
+ auth = None
741
747
 
742
748
  request = isolate_proto.SetAliasRequest(
743
749
  alias=alias,
@@ -746,10 +752,15 @@ class FalServerlessConnection:
746
752
  )
747
753
  self.stub.SetAlias(request)
748
754
 
749
- def delete_alias(self, alias: str) -> str:
755
+ def delete_alias(self, alias: str) -> str | None:
750
756
  request = isolate_proto.DeleteAliasRequest(alias=alias)
751
- res: isolate_proto.DeleteAliasResult = self.stub.DeleteAlias(request)
752
- return res.revision
757
+ try:
758
+ res: isolate_proto.DeleteAliasResult = self.stub.DeleteAlias(request)
759
+ return res.revision
760
+ except grpc.RpcError as e:
761
+ if e.code() == grpc.StatusCode.NOT_FOUND:
762
+ return None
763
+ raise
753
764
 
754
765
  def list_aliases(self) -> list[AliasInfo]:
755
766
  request = isolate_proto.ListAliasesRequest()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fal
3
- Version: 1.25.0
3
+ Version: 1.26.1
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,19 +1,19 @@
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=IqBVQrZCUvJywOgeXHpI90KZx4iWxpQ8Zw_N9Il6uS4,513
3
+ fal/_fal_version.py,sha256=n2TqZzcgqBKPmTHRD9QCk0lFkU1l5_qG0eOl7E-Tdjs,513
4
4
  fal/_serialization.py,sha256=npXNsFJ5G7jzBeBIyVMH01Ww34mGY4XWhHpRbSrTtnQ,7598
5
5
  fal/_version.py,sha256=1BbTFnucNC_6ldKJ_ZoC722_UkW4S9aDBSW9L0fkKAw,2315
6
- fal/api.py,sha256=vTYWeKnNg3japot1vOV_JSk812aeB6a2Y7lC1ed-VmM,47277
7
- fal/app.py,sha256=pMf7P9iVEcw5HiFCYgSdHkjX6f-1SPe06EOwYH1ImGA,24079
6
+ fal/api.py,sha256=wIEt21P1C7U-dYQEcyHUxxuuTnvzFyTpWDoHoaxq7tg,47385
7
+ fal/app.py,sha256=3RDFV6JUiUk8b3WJanKQYA-kW74t5bqaNy8OYuUH5-Q,25491
8
8
  fal/apps.py,sha256=pzCd2mrKl5J_4oVc40_pggvPtFahXBCdrZXWpnaEJVs,12130
9
9
  fal/config.py,sha256=19tR4zWr2yqsBRWNhFV1jKMiXAASRz2Et62k68-1r9Y,3290
10
10
  fal/container.py,sha256=FTsa5hOW4ars-yV1lUoc0BNeIIvAZcpw7Ftyt3A4m_w,2000
11
- fal/files.py,sha256=1eOyrj1M0hTixZdbtQ1ogJqWpLd7UfiOt1Rxihnqh8g,3565
11
+ fal/files.py,sha256=iwR9jXWmAruP5tSS5-NkY-mZG53Pb80C40SqB0pYRrk,6819
12
12
  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=63W0o2bp_EAJmUHL6o6lrSn1d89fRzccXeZ3ww5YYYo,25994
16
+ fal/sdk.py,sha256=eX_OJVmD6AY5NsLS7VR-LemXTWNEMfsOEfZBgAqytoo,26158
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=anFfy6qouB8QzH0Yho41GulGiJu3q1KKIwgyVQCzgRQ,1593
25
25
  fal/cli/api.py,sha256=ZuDE_PIC-czzneTAWMwvC7P7WnwIyluNZSuJqzCFhqI,2640
26
- fal/cli/apps.py,sha256=FSd6uhNlmJUSToeG89gq2159RBkg9ImXquTCrvxTfQo,10283
26
+ fal/cli/apps.py,sha256=0ex4OWbxcopjEq_BGkwkxCBSTLqUIHmFDz1MWOv06zk,10479
27
27
  fal/cli/auth.py,sha256=Qe-Z3ycXJnOzHimz5PjCQYoni8MF4csmdL19yGN7a1o,5171
28
28
  fal/cli/cli_nested_json.py,sha256=veSZU8_bYV3Iu1PAoxt-4BMBraNIqgH5nughbs2UKvE,13539
29
29
  fal/cli/create.py,sha256=a8WDq-nJLFTeoIXqpb5cr7GR7YR9ZZrQCawNm34KXXE,627
@@ -142,8 +142,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
142
142
  openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
143
143
  openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
144
144
  openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
145
- fal-1.25.0.dist-info/METADATA,sha256=8l0JvTAUiYbMT6irgvfJooJ26Bkq3kD0G4eFGCDkgRk,4089
146
- fal-1.25.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
147
- fal-1.25.0.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
148
- fal-1.25.0.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
149
- fal-1.25.0.dist-info/RECORD,,
145
+ fal-1.26.1.dist-info/METADATA,sha256=Dk0fMzu4rMEi6H9l2mWWBqjkcyS-QvzxFrVmt6rbnGY,4089
146
+ fal-1.26.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
147
+ fal-1.26.1.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
148
+ fal-1.26.1.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
149
+ fal-1.26.1.dist-info/RECORD,,
File without changes