fal 1.26.0__py3-none-any.whl → 1.26.2__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.26.0'
21
- __version_tuple__ = version_tuple = (1, 26, 0)
20
+ __version__ = version = '1.26.2'
21
+ __version_tuple__ = version_tuple = (1, 26, 2)
fal/api.py CHANGED
@@ -500,7 +500,7 @@ class FalServerlessHost(Host):
500
500
  partial_func,
501
501
  environments,
502
502
  application_name=application_name,
503
- application_auth_mode=application_auth_mode,
503
+ auth_mode=application_auth_mode,
504
504
  machine_requirements=machine_requirements,
505
505
  metadata=metadata,
506
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)
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,
fal/toolkit/file/file.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import shutil
4
+ import traceback
4
5
  from functools import wraps
5
6
  from pathlib import Path
6
7
  from tempfile import NamedTemporaryFile, mkdtemp
@@ -178,10 +179,16 @@ class File(BaseModel):
178
179
 
179
180
  try:
180
181
  url = repo.save(fdata, **save_kwargs)
181
- except Exception:
182
+ except Exception as exc:
182
183
  if not fallback_repository:
183
184
  raise
184
185
 
186
+ traceback.print_exc()
187
+ print(
188
+ f"Failed to save bytes to repository {repository}: {exc}, "
189
+ f"falling back to {fallback_repository}"
190
+ )
191
+
185
192
  fallback_repo = get_builtin_repository(fallback_repository)
186
193
 
187
194
  url = fallback_repo.save(fdata, **fallback_save_kwargs)
@@ -237,10 +244,16 @@ class File(BaseModel):
237
244
  content_type=content_type,
238
245
  **save_kwargs,
239
246
  )
240
- except Exception:
247
+ except Exception as exc:
241
248
  if not fallback_repository:
242
249
  raise
243
250
 
251
+ traceback.print_exc()
252
+ print(
253
+ f"Failed to save file to repository {repository}: {exc}, "
254
+ f"falling back to {fallback_repository}"
255
+ )
256
+
244
257
  fallback_repo = get_builtin_repository(fallback_repository)
245
258
 
246
259
  url, data = fallback_repo.save_file(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fal
3
- Version: 1.26.0
3
+ Version: 1.26.2
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=Munn2JvNJkaAWn7AFHBzTBfXnoY8sVIidDZY9Jat_1A,513
3
+ fal/_fal_version.py,sha256=C99N_Z6v_dImhQIs0wSP9hjugs8YW_m4cpJMf6e1I-4,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=MRVuGn2gqFqm30YLk78Xgm-Rtw5POT9NCbUr67SWGcY,47397
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=zTT9AlrMZO1f3VZHvxe27gU1HfbO3ucE04_SNKbMzDY,26157
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=xny6t_zVTZ8oml_sfigr7BA1UZW6O-fkPsbfm6ETMr4,10484
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
@@ -59,7 +59,7 @@ fal/toolkit/types.py,sha256=kkbOsDKj1qPGb1UARTBp7yuJ5JUuyy7XQurYUBCdti8,4064
59
59
  fal/toolkit/audio/__init__.py,sha256=sqNVfrKbppWlIGLoFTaaNTxLpVXsFHxOSHLA5VG547A,35
60
60
  fal/toolkit/audio/audio.py,sha256=gt458h989iQ-EhQSH-mCuJuPBY4RneLJE05f_QWU1E0,572
61
61
  fal/toolkit/file/__init__.py,sha256=FbNl6wD-P0aSSTUwzHt4HujBXrbC3ABmaigPQA4hRfg,70
62
- fal/toolkit/file/file.py,sha256=7JWAwDqT3xnfCtPl75x1TpRmtJmWRRVvsGFMNUta3bY,9765
62
+ fal/toolkit/file/file.py,sha256=4K28gr--5q0nmsm3P76SFoKQj3bPROVwxXrdoMjIiUE,10197
63
63
  fal/toolkit/file/types.py,sha256=MMAH_AyLOhowQPesOv1V25wB4qgbJ3vYNlnTPbdSv1M,2304
64
64
  fal/toolkit/file/providers/fal.py,sha256=vt4Mznbfca6blfk0psF1ix-zB6309kpIA0_5Qh7bmFw,47217
65
65
  fal/toolkit/file/providers/gcp.py,sha256=DKeZpm1MjwbvEsYvkdXUtuLIJDr_UNbqXj_Mfv3NTeo,2437
@@ -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.26.0.dist-info/METADATA,sha256=26YySFcK-VDCQFJkuaWR7xvuVJxFMnggSif6dDj3hwc,4089
146
- fal-1.26.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
147
- fal-1.26.0.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
148
- fal-1.26.0.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
149
- fal-1.26.0.dist-info/RECORD,,
145
+ fal-1.26.2.dist-info/METADATA,sha256=kNAhtk3mo4CnZruMDUZOa47W3uh92DKvD0GdAT7YhOE,4089
146
+ fal-1.26.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
147
+ fal-1.26.2.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
148
+ fal-1.26.2.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
149
+ fal-1.26.2.dist-info/RECORD,,
File without changes