fal 1.0.2__py3-none-any.whl → 1.0.4__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.2'
16
- __version_tuple__ = version_tuple = (1, 0, 2)
15
+ __version__ = version = '1.0.4'
16
+ __version_tuple__ = version_tuple = (1, 0, 4)
fal/api.py CHANGED
@@ -282,7 +282,6 @@ def _handle_grpc_error():
282
282
  "This is likely due to resource overflow. "
283
283
  "You can try again by setting a bigger `machine_type`"
284
284
  )
285
-
286
285
  elif e.code() == grpc.StatusCode.INVALID_ARGUMENT and (
287
286
  "The function function could not be deserialized" in e.details()
288
287
  ):
fal/cli/main.py CHANGED
@@ -45,6 +45,10 @@ def parse_args(argv=None):
45
45
  return args
46
46
 
47
47
 
48
+ def _print_error(msg):
49
+ console.print(f"{CROSS_ICON} {msg}")
50
+
51
+
48
52
  def main(argv=None) -> int:
49
53
  import grpc
50
54
 
@@ -65,18 +69,18 @@ def main(argv=None) -> int:
65
69
  exc.__traceback__,
66
70
  )
67
71
  console.print(tb)
68
- console.print("Unhandled user exception")
72
+ _print_error("Unhandled user exception")
69
73
  except KeyboardInterrupt:
70
- console.print("Aborted.")
74
+ _print_error("Aborted.")
71
75
  except grpc.RpcError as exc:
72
- console.print(exc.details())
76
+ _print_error(exc.details())
73
77
  except FalParserExit as exc:
74
78
  ret = exc.status
75
79
  except Exception as exc:
76
- msg = f"{CROSS_ICON} {str(exc)}"
80
+ msg = str(exc)
77
81
  cause = exc.__cause__
78
82
  if cause is not None:
79
83
  msg += f": {str(cause)}"
80
- console.print(msg)
84
+ _print_error(msg)
81
85
 
82
86
  return ret
fal/cli/parser.py CHANGED
@@ -51,6 +51,22 @@ class DictAction(argparse.Action):
51
51
  setattr(args, self.dest, d)
52
52
 
53
53
 
54
+ def _find_parser(parser, func):
55
+ defaults = parser._defaults
56
+ if not func or func == defaults.get("func"):
57
+ return parser
58
+
59
+ actions = parser._actions
60
+ for action in actions:
61
+ if not isinstance(action.choices, dict):
62
+ continue
63
+ for subparser in action.choices.values():
64
+ par = _find_parser(subparser, func)
65
+ if par:
66
+ return par
67
+ return None
68
+
69
+
54
70
  class FalParser(argparse.ArgumentParser):
55
71
  def __init__(self, *args, **kwargs):
56
72
  kwargs.setdefault("formatter_class", rich_argparse.RawTextRichHelpFormatter)
@@ -61,6 +77,13 @@ class FalParser(argparse.ArgumentParser):
61
77
  self._print_message(message, sys.stderr)
62
78
  raise FalParserExit(status)
63
79
 
80
+ def parse_args(self, args=None, namespace=None):
81
+ args, argv = self.parse_known_args(args, namespace)
82
+ if argv:
83
+ parser = _find_parser(self, getattr(args, "func", None)) or self
84
+ parser.error("unrecognized arguments: %s" % " ".join(argv))
85
+ return args
86
+
64
87
 
65
88
  class FalClientParser(FalParser):
66
89
  def __init__(self, *args, **kwargs):
@@ -20,7 +20,9 @@ class ObjectLifecyclePreference:
20
20
  expriation_duration_seconds: int
21
21
 
22
22
 
23
- GLOBAL_LIFECYCLE_PREFERENCE = ObjectLifecyclePreference(expriation_duration_seconds=2)
23
+ GLOBAL_LIFECYCLE_PREFERENCE = ObjectLifecyclePreference(
24
+ expriation_duration_seconds=86400
25
+ )
24
26
 
25
27
 
26
28
  @dataclass
@@ -4,7 +4,6 @@ import hashlib
4
4
  import shutil
5
5
  import subprocess
6
6
  import sys
7
- from functools import lru_cache
8
7
  from pathlib import Path, PurePath
9
8
  from tempfile import TemporaryDirectory
10
9
  from urllib.parse import urlparse
@@ -40,8 +39,10 @@ def _hash_url(url: str) -> str:
40
39
  return hashlib.sha256(url.encode("utf-8")).hexdigest()
41
40
 
42
41
 
43
- @lru_cache
44
- def _get_remote_file_properties(url: str) -> tuple[str, int]:
42
+ def _get_remote_file_properties(
43
+ url: str,
44
+ request_headers: dict[str, str] | None = None,
45
+ ) -> tuple[str, int]:
45
46
  """Retrieves the file name and content length of a remote file.
46
47
 
47
48
  This function sends an HTTP request to the remote URL and retrieves the
@@ -60,11 +61,15 @@ def _get_remote_file_properties(url: str) -> tuple[str, int]:
60
61
 
61
62
  Args:
62
63
  url: The URL of the remote file.
64
+ request_headers: A dictionary containing additional headers to be included in
65
+ the HTTP request.
63
66
 
64
67
  Returns:
65
68
  A tuple containing the file name and the content length of the remote file.
66
69
  """
67
- request = Request(url, headers=TEMP_HEADERS)
70
+ headers = {**TEMP_HEADERS, **(request_headers or {})}
71
+ request = Request(url, headers=headers)
72
+
68
73
  with urlopen(request) as response:
69
74
  file_name = response.headers.get_filename()
70
75
  content_length = int(response.headers.get("Content-Length", -1))
@@ -81,7 +86,9 @@ def _get_remote_file_properties(url: str) -> tuple[str, int]:
81
86
  return file_name, content_length
82
87
 
83
88
 
84
- def _file_content_length_matches(url: str, file_path: Path) -> bool:
89
+ def _file_content_length_matches(
90
+ url: str, file_path: Path, request_headers: dict[str, str] | None = None
91
+ ) -> bool:
85
92
  """Check if the local file's content length matches the expected remote
86
93
  file's content length.
87
94
 
@@ -95,13 +102,15 @@ def _file_content_length_matches(url: str, file_path: Path) -> bool:
95
102
  Args:
96
103
  url: The URL of the remote file.
97
104
  file_path: The local path to the file being compared.
105
+ request_headers: A dictionary containing additional headers to be included in
106
+ the HTTP request.
98
107
 
99
108
  Returns:
100
109
  bool: `True` if the local file's content length matches the remote file's
101
110
  content length, `False` otherwise.
102
111
  """
103
112
  local_file_content_length = file_path.stat().st_size
104
- remote_file_content_length = _get_remote_file_properties(url)[1]
113
+ remote_file_content_length = _get_remote_file_properties(url, request_headers)[1]
105
114
 
106
115
  return local_file_content_length == remote_file_content_length
107
116
 
@@ -111,6 +120,7 @@ def download_file(
111
120
  target_dir: str | Path,
112
121
  *,
113
122
  force: bool = False,
123
+ request_headers: dict[str, str] | None = None,
114
124
  ) -> Path:
115
125
  """Downloads a file from the specified URL to the target directory.
116
126
 
@@ -134,6 +144,9 @@ def download_file(
134
144
  force: If `True`, the file is downloaded even if it already exists locally and
135
145
  its content length matches the expected content length from the remote file.
136
146
  Defaults to `False`.
147
+ request_headers: A dictionary containing additional headers to be included in
148
+ the HTTP request. Defaults to `None`.
149
+
137
150
 
138
151
  Returns:
139
152
  A Path object representing the full path to the downloaded file.
@@ -142,7 +155,11 @@ def download_file(
142
155
  ValueError: If the provided `file_name` contains a forward slash ('/').
143
156
  DownloadError: If an error occurs during the download process.
144
157
  """
145
- file_name = _get_remote_file_properties(url)[0]
158
+ try:
159
+ file_name = _get_remote_file_properties(url, request_headers)[0]
160
+ except Exception as e:
161
+ raise DownloadError(f"Failed to get remote file properties for {url}") from e
162
+
146
163
  if "/" in file_name:
147
164
  raise ValueError(f"File name '{file_name}' cannot contain a slash.")
148
165
 
@@ -156,7 +173,7 @@ def download_file(
156
173
 
157
174
  if (
158
175
  target_path.exists()
159
- and _file_content_length_matches(url, target_path)
176
+ and _file_content_length_matches(url, target_path, request_headers)
160
177
  and not force
161
178
  ):
162
179
  return target_path
@@ -170,7 +187,9 @@ def download_file(
170
187
  target_path.parent.mkdir(parents=True, exist_ok=True)
171
188
 
172
189
  try:
173
- _download_file_python(url=url, target_path=target_path)
190
+ _download_file_python(
191
+ url=url, target_path=target_path, request_headers=request_headers
192
+ )
174
193
  except Exception as e:
175
194
  msg = f"Failed to download {url} to {target_path}"
176
195
 
@@ -181,13 +200,17 @@ def download_file(
181
200
  return target_path
182
201
 
183
202
 
184
- def _download_file_python(url: str, target_path: Path | str) -> Path:
203
+ def _download_file_python(
204
+ url: str, target_path: Path | str, request_headers: dict[str, str] | None = None
205
+ ) -> Path:
185
206
  """Download a file from a given URL and save it to a specified path using a
186
207
  Python interface.
187
208
 
188
209
  Args:
189
210
  url: The URL of the file to be downloaded.
190
211
  target_path: The path where the downloaded file will be saved.
212
+ request_headers: A dictionary containing additional headers to be included in
213
+ the HTTP request. Defaults to `None`.
191
214
 
192
215
  Returns:
193
216
  The path where the downloaded file has been saved.
@@ -199,11 +222,14 @@ def _download_file_python(url: str, target_path: Path | str) -> Path:
199
222
  try:
200
223
  file_path = temp_file.name
201
224
 
202
- for (progress, total_size) in _stream_url_data_to_file(url, temp_file.name):
225
+ for progress, total_size in _stream_url_data_to_file(
226
+ url, temp_file.name, request_headers=request_headers
227
+ ):
203
228
  if total_size:
204
229
  progress_msg = f"Downloading {url} ... {progress:.2%}"
205
230
  else:
206
231
  progress_msg = f"Downloading {url} ... {progress:.2f} MB"
232
+
207
233
  print(progress_msg, end="\r\n")
208
234
 
209
235
  # Move the file when the file is downloaded completely. Since the
@@ -219,7 +245,12 @@ def _download_file_python(url: str, target_path: Path | str) -> Path:
219
245
  return Path(target_path)
220
246
 
221
247
 
222
- def _stream_url_data_to_file(url: str, file_path: str, chunk_size_in_mb: int = 64):
248
+ def _stream_url_data_to_file(
249
+ url: str,
250
+ file_path: str,
251
+ chunk_size_in_mb: int = 64,
252
+ request_headers: dict[str, str] | None = None,
253
+ ):
223
254
  """Download data from a URL and stream it to a file.
224
255
 
225
256
  Note:
@@ -233,6 +264,8 @@ def _stream_url_data_to_file(url: str, file_path: str, chunk_size_in_mb: int = 6
233
264
  file_path: The path to the file where the downloaded data will be saved.
234
265
  chunk_size_in_mb: The size of each download chunk in megabytes.
235
266
  Defaults to 64.
267
+ request_headers: A dictionary containing additional headers to be included in
268
+ the HTTP request. Defaults to `None`.
236
269
 
237
270
  Yields:
238
271
  A tuple containing two elements:
@@ -244,7 +277,8 @@ def _stream_url_data_to_file(url: str, file_path: str, chunk_size_in_mb: int = 6
244
277
  """
245
278
  ONE_MB = 1024**2
246
279
 
247
- request = Request(url, headers=TEMP_HEADERS)
280
+ headers = {**TEMP_HEADERS, **(request_headers or {})}
281
+ request = Request(url, headers=headers)
248
282
 
249
283
  received_size = 0
250
284
  total_size = 0
@@ -267,7 +301,9 @@ def _stream_url_data_to_file(url: str, file_path: str, chunk_size_in_mb: int = 6
267
301
  raise DownloadError("Received less data than expected from the server.")
268
302
 
269
303
 
270
- def download_model_weights(url: str, force: bool = False):
304
+ def download_model_weights(
305
+ url: str, force: bool = False, request_headers: dict[str, str] | None = None
306
+ ) -> Path:
271
307
  """Downloads model weights from the specified URL and saves them to a
272
308
  predefined directory.
273
309
 
@@ -284,6 +320,8 @@ def download_model_weights(url: str, force: bool = False):
284
320
  force: If `True`, the model weights are downloaded even if they already exist
285
321
  locally and their content length matches the expected content length from
286
322
  the remote file. Defaults to `False`.
323
+ request_headers: A dictionary containing additional headers to be included in
324
+ the HTTP request. Defaults to `None`.
287
325
 
288
326
  Returns:
289
327
  A Path object representing the full path to the downloaded model weights.
@@ -303,6 +341,7 @@ def download_model_weights(url: str, force: bool = False):
303
341
  url,
304
342
  target_dir=weights_dir,
305
343
  force=force,
344
+ request_headers=request_headers,
306
345
  )
307
346
 
308
347
 
fal/utils.py CHANGED
@@ -34,7 +34,6 @@ def load_function_from(
34
34
  else:
35
35
  app_name = None
36
36
 
37
- module = runpy.run_path(file_path)
38
37
  if function_name not in module:
39
38
  raise FalServerlessError(f"Function '{function_name}' not found in module")
40
39
 
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: fal is an easy-to-use Serverless Python Framework
5
- Author: Features & Labels <hello@fal.ai>
5
+ Author: Features & Labels <support@fal.ai>
6
6
  Requires-Python: >=3.8
7
7
  Description-Content-Type: text/markdown
8
8
  Requires-Dist: isolate[build] <1.0,>=0.12.3
@@ -20,7 +20,7 @@ Requires-Dist: colorama <1,>=0.4.6
20
20
  Requires-Dist: portalocker <3,>=2.7.0
21
21
  Requires-Dist: rich <14,>=13.3.2
22
22
  Requires-Dist: rich-argparse
23
- Requires-Dist: packaging <22,>=21.3
23
+ Requires-Dist: packaging >=21.3
24
24
  Requires-Dist: pathspec <1,>=0.11.1
25
25
  Requires-Dist: pydantic !=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3
26
26
  Requires-Dist: fastapi <1,>=0.99.1
@@ -1,9 +1,9 @@
1
1
  fal/__init__.py,sha256=suif79hYcYwlZ8dAaVXCErKhuD2AYf8uU78rty8jow8,721
2
2
  fal/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
3
- fal/_fal_version.py,sha256=BGgbj7AHZwtVw29XgDIjcG5woR-f3jQy2X-ArcRT22E,411
3
+ fal/_fal_version.py,sha256=9acUHRb1fq-uv5dr49LlEh189daKw4-SeypW5NxDolA,411
4
4
  fal/_serialization.py,sha256=7urrZXw99qmsK-RkjCurk6Va4TMEfDIMajkzKbSW4j4,7655
5
5
  fal/_version.py,sha256=EBGqrknaf1WygENX-H4fBefLvHryvJBBGtVJetaB0NY,266
6
- fal/api.py,sha256=g8ypixM_3AZMdDbGQTUkBE92kphGE3dpLJPsY2cJMIk,36287
6
+ fal/api.py,sha256=PBBLmVYNfIstLXJoRRoCQSfspPxklh9PNSWZEMfi0s8,36286
7
7
  fal/app.py,sha256=s9ba4t4D5KJrPFGKeRzL3XsdKH-W1Be6NmDQgYjPnCw,13826
8
8
  fal/apps.py,sha256=UhR6mq8jBiTAp-QvUnvbnMNcuJ5wHIKSqdlfyx8aBQ8,6829
9
9
  fal/flags.py,sha256=oWN_eidSUOcE9wdPK_77si3A1fpgOC0UEERPsvNLIMc,842
@@ -11,7 +11,7 @@ fal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  fal/rest_client.py,sha256=kGBGmuyHfX1lR910EoKCYPjsyU8MdXawT_cW2q8Sajc,568
12
12
  fal/sdk.py,sha256=eHcg5TRouGL5d_9-p44x6lQUCYXVXdul6iEETCUov5Q,19976
13
13
  fal/sync.py,sha256=ZuIJA2-hTPNANG9B_NNJZUsO68EIdTH0dc9MzeVE2VU,4340
14
- fal/utils.py,sha256=PfmRrW3EFJ4Y07XsRRfydS1NT4Fll5t7oChmFvusr3g,1793
14
+ fal/utils.py,sha256=MFDs-eO3rBgc3jqIwBpfBtvvK5tbzAYWMzHV-tTVLic,1754
15
15
  fal/workflows.py,sha256=4rjqL4xB6GHLJsqTplJmAvpd6uHZJ28sc8su33BFXEo,14682
16
16
  fal/auth/__init__.py,sha256=r8iA2-5ih7-Fik3gEC4HEWNFbGoxpYnXpZu1icPIoS0,3561
17
17
  fal/auth/auth0.py,sha256=rSG1mgH-QGyKfzd7XyAaj1AYsWt-ho8Y_LZ-FUVWzh4,5421
@@ -22,8 +22,8 @@ fal/cli/auth.py,sha256=--MhfHGwxmtHbRkGioyn1prKn_U-pBzbz0G_QeZou-U,1352
22
22
  fal/cli/debug.py,sha256=1doDNwoaPDfLQginGNBxpC20dZYs5UxIojflDvV1Q04,1342
23
23
  fal/cli/deploy.py,sha256=S_HIMLqDpGyzDdbiIxudRizwjGoAaHpN-sXcl2uCaQ4,4329
24
24
  fal/cli/keys.py,sha256=-9N6ZY6rW-_IE9tpupgaBPDGjGdKB3HKqU2g9daM3Xc,3109
25
- fal/cli/main.py,sha256=RSMXLUyzYmZhS0Wcq9phXJcMJM_UpQD3su7F7j8Wr3M,1933
26
- fal/cli/parser.py,sha256=8W9VhxDBOSru6Vs_HsUm_RA7_YMnzXLTzUrXA0mGVRA,2109
25
+ fal/cli/main.py,sha256=bLwuHlkCiroVDZ21WcL_qAv71k5kNTloWreZHuO0jR4,1977
26
+ fal/cli/parser.py,sha256=r1hd5e8Jq6yzDZw8-S0On1EjJbjRtHMuVuHC6MlvUj4,2835
27
27
  fal/cli/run.py,sha256=NXwzkAWCKrRwgoMLsBOgW7RJPJW4IgSTrG85q2iePyk,894
28
28
  fal/cli/secrets.py,sha256=mgHp3gBr8d2su7wBApeADKWHPkYu2ueB6gG3eNMETh8,2595
29
29
  fal/console/__init__.py,sha256=ernZ4bzvvliQh5SmrEqQ7lA5eVcbw6Ra2jalKtA7dxg,132
@@ -43,13 +43,13 @@ fal/toolkit/optimize.py,sha256=p75sovF0SmRP6zxzpIaaOmqlxvXB_xEz3XPNf59EF7w,1339
43
43
  fal/toolkit/file/__init__.py,sha256=FbNl6wD-P0aSSTUwzHt4HujBXrbC3ABmaigPQA4hRfg,70
44
44
  fal/toolkit/file/file.py,sha256=r8PzNCgv8Gkj6s1zM0yW-pcMKIouyaiEH06iBue8MwM,6066
45
45
  fal/toolkit/file/types.py,sha256=9CqDh8SmNJNzfsrvtj468uo2SprJH9rOk8KMhhfU73c,1050
46
- fal/toolkit/file/providers/fal.py,sha256=AfNwdnvt_IIFrzvNZPFjGwfVOQ2OzPfG1ySz6-2tnvc,3690
46
+ fal/toolkit/file/providers/fal.py,sha256=AtYYXBM72leb8OmP6pvYiNZCWhEmoi9VfpckJ8FuZto,3700
47
47
  fal/toolkit/file/providers/gcp.py,sha256=7Lg7BXoHKkFu0jkGv3_vKh2Ks6eRfDMbw31N3mvDUtk,1913
48
48
  fal/toolkit/file/providers/r2.py,sha256=YW5aJBOX41MQxfx1rA_f-IiJhAPMZ5md0cxBcg3y08I,2537
49
49
  fal/toolkit/image/__init__.py,sha256=qNLyXsBWysionUjbeWbohLqWlw3G_UpzunamkZd_JLQ,71
50
50
  fal/toolkit/image/image.py,sha256=2q1ZCBSSdmDx9q1S4ahoCOniNaRcfSFnCS31f3b8ZZ0,4252
51
51
  fal/toolkit/utils/__init__.py,sha256=CrmM9DyCz5-SmcTzRSm5RaLgxy3kf0ZsSEN9uhnX2Xo,97
52
- fal/toolkit/utils/download_utils.py,sha256=M-xUAV8kX6o1zcojozSTaArGIC_LgQriagCC8AoG0mM,15487
52
+ fal/toolkit/utils/download_utils.py,sha256=fDUITgdGW0wRXLE0NuQg29YJnJS3yr6l0zbRKqX6zMU,17006
53
53
  openapi_fal_rest/__init__.py,sha256=ziculmF_i6trw63LzZGFX-6W3Lwq9mCR8_UpkpvpaHI,152
54
54
  openapi_fal_rest/client.py,sha256=G6BpJg9j7-JsrAUGddYwkzeWRYickBjPdcVgXoPzxuE,2817
55
55
  openapi_fal_rest/errors.py,sha256=8mXSxdfSGzxT82srdhYbR0fHfgenxJXaUtMkaGgb6iU,470
@@ -92,8 +92,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
92
92
  openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
93
93
  openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
94
94
  openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
95
- fal-1.0.2.dist-info/METADATA,sha256=aCks8skI_xU27Mx6dtEPGPz0_H4USs9l9DtM9MOqV7I,3740
96
- fal-1.0.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
97
- fal-1.0.2.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
98
- fal-1.0.2.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
99
- fal-1.0.2.dist-info/RECORD,,
95
+ fal-1.0.4.dist-info/METADATA,sha256=y6kcDVRBEAnrq9SCiVvyL6NdHNHDasO9-DBmNukr1L4,3738
96
+ fal-1.0.4.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
97
+ fal-1.0.4.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
98
+ fal-1.0.4.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
99
+ fal-1.0.4.dist-info/RECORD,,
File without changes