fal 1.3.3__py3-none-any.whl → 1.4.0__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.3.3'
16
- __version_tuple__ = version_tuple = (1, 3, 3)
15
+ __version__ = version = '1.4.0'
16
+ __version_tuple__ = version_tuple = (1, 4, 0)
fal/api.py CHANGED
@@ -389,6 +389,8 @@ class FalServerlessHost(Host):
389
389
  _SUPPORTED_KEYS = frozenset(
390
390
  {
391
391
  "machine_type",
392
+ "machine_types",
393
+ "num_gpus",
392
394
  "keep_alive",
393
395
  "max_concurrency",
394
396
  "min_concurrency",
@@ -431,7 +433,7 @@ class FalServerlessHost(Host):
431
433
  environment_options.setdefault("python_version", active_python())
432
434
  environments = [self._connection.define_environment(**environment_options)]
433
435
 
434
- machine_type = options.host.get(
436
+ machine_type: list[str] | str = options.host.get(
435
437
  "machine_type", FAL_SERVERLESS_DEFAULT_MACHINE_TYPE
436
438
  )
437
439
  keep_alive = options.host.get("keep_alive", FAL_SERVERLESS_DEFAULT_KEEP_ALIVE)
@@ -444,7 +446,8 @@ class FalServerlessHost(Host):
444
446
  exposed_port = options.get_exposed_port()
445
447
 
446
448
  machine_requirements = MachineRequirements(
447
- machine_type=machine_type,
449
+ machine_types=machine_type, # type: ignore
450
+ num_gpus=options.host.get("num_gpus"),
448
451
  keep_alive=keep_alive,
449
452
  base_image=base_image,
450
453
  exposed_port=exposed_port,
@@ -501,7 +504,7 @@ class FalServerlessHost(Host):
501
504
  environment_options.setdefault("python_version", active_python())
502
505
  environments = [self._connection.define_environment(**environment_options)]
503
506
 
504
- machine_type = options.host.get(
507
+ machine_type: list[str] | str = options.host.get(
505
508
  "machine_type", FAL_SERVERLESS_DEFAULT_MACHINE_TYPE
506
509
  )
507
510
  keep_alive = options.host.get("keep_alive", FAL_SERVERLESS_DEFAULT_KEEP_ALIVE)
@@ -515,7 +518,8 @@ class FalServerlessHost(Host):
515
518
  setup_function = options.host.get("setup_function", None)
516
519
 
517
520
  machine_requirements = MachineRequirements(
518
- machine_type=machine_type,
521
+ machine_types=machine_type, # type: ignore
522
+ num_gpus=options.host.get("num_gpus"),
519
523
  keep_alive=keep_alive,
520
524
  base_image=base_image,
521
525
  exposed_port=exposed_port,
@@ -684,7 +688,8 @@ def function(
684
688
  max_concurrency: int | None = None,
685
689
  # FalServerlessHost options
686
690
  metadata: dict[str, Any] | None = None,
687
- machine_type: str = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
691
+ machine_type: str | list[str] = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
692
+ num_gpus: int | None = None,
688
693
  keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
689
694
  max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
690
695
  min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
@@ -709,7 +714,8 @@ def function(
709
714
  max_concurrency: int | None = None,
710
715
  # FalServerlessHost options
711
716
  metadata: dict[str, Any] | None = None,
712
- machine_type: str = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
717
+ machine_type: str | list[str] = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
718
+ num_gpus: int | None = None,
713
719
  keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
714
720
  max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
715
721
  min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
@@ -784,7 +790,8 @@ def function(
784
790
  max_concurrency: int | None = None,
785
791
  # FalServerlessHost options
786
792
  metadata: dict[str, Any] | None = None,
787
- machine_type: str = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
793
+ machine_type: str | list[str] = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
794
+ num_gpus: int | None = None,
788
795
  keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
789
796
  max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
790
797
  min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
@@ -814,7 +821,8 @@ def function(
814
821
  max_concurrency: int | None = None,
815
822
  # FalServerlessHost options
816
823
  metadata: dict[str, Any] | None = None,
817
- machine_type: str = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
824
+ machine_type: str | list[str] = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
825
+ num_gpus: int | None = None,
818
826
  keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
819
827
  max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
820
828
  min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
@@ -838,7 +846,8 @@ def function(
838
846
  max_concurrency: int | None = None,
839
847
  # FalServerlessHost options
840
848
  metadata: dict[str, Any] | None = None,
841
- machine_type: str = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
849
+ machine_type: str | list[str] = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
850
+ num_gpus: int | None = None,
842
851
  keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
843
852
  max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
844
853
  min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
@@ -862,7 +871,8 @@ def function(
862
871
  max_concurrency: int | None = None,
863
872
  # FalServerlessHost options
864
873
  metadata: dict[str, Any] | None = None,
865
- machine_type: str = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
874
+ machine_type: str | list[str] = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
875
+ num_gpus: int | None = None,
866
876
  keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
867
877
  max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
868
878
  min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
fal/app.py CHANGED
@@ -60,6 +60,7 @@ def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
60
60
  kind,
61
61
  requirements=cls.requirements,
62
62
  machine_type=cls.machine_type,
63
+ num_gpus=cls.num_gpus,
63
64
  **cls.host_kwargs,
64
65
  **kwargs,
65
66
  metadata=metadata,
@@ -177,6 +178,7 @@ def _to_fal_app_name(name: str) -> str:
177
178
  class App(fal.api.BaseServable):
178
179
  requirements: ClassVar[list[str]] = []
179
180
  machine_type: ClassVar[str] = "S"
181
+ num_gpus: ClassVar[int | None] = None
180
182
  host_kwargs: ClassVar[dict[str, Any]] = {
181
183
  "_scheduler": "nomad",
182
184
  "_scheduler_options": {
fal/cli/deploy.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import argparse
2
2
  from collections import namedtuple
3
3
  from pathlib import Path
4
- from typing import Optional, Union
4
+ from typing import Literal, Optional, Tuple, Union
5
5
 
6
6
  from ._utils import get_app_data_from_toml, is_app_name
7
7
  from .parser import FalClientParser, RefAction
@@ -63,7 +63,10 @@ def _get_user() -> User:
63
63
 
64
64
 
65
65
  def _deploy_from_reference(
66
- app_ref: tuple[Optional[Union[Path, str]], ...], app_name: str, auth: str, args
66
+ app_ref: Tuple[Optional[Union[Path, str]], ...],
67
+ app_name: str,
68
+ auth: Literal["public", "shared", "private"],
69
+ args,
67
70
  ):
68
71
  from fal.api import FalServerlessError, FalServerlessHost
69
72
  from fal.utils import load_function_from
@@ -93,7 +96,7 @@ def _deploy_from_reference(
93
96
  isolated_function = loaded.function
94
97
  app_name = app_name or loaded.app_name # type: ignore
95
98
  app_auth = auth or loaded.app_auth or "private"
96
- deployment_strategy = args.strategy or "default"
99
+ deployment_strategy = args.strategy or "recreate"
97
100
 
98
101
  app_id = host.register(
99
102
  func=isolated_function.func,
@@ -204,9 +207,9 @@ def add_parser(main_subparsers, parents):
204
207
  )
205
208
  parser.add_argument(
206
209
  "--strategy",
207
- choices=["default", "rolling"],
210
+ choices=["recreate", "rolling"],
208
211
  help="Deployment strategy.",
209
- default="default",
212
+ default="recreate",
210
213
  )
211
214
 
212
215
  parser.set_defaults(func=_deploy)
fal/sdk.py CHANGED
@@ -389,7 +389,8 @@ def _from_grpc_hosted_run_result(
389
389
 
390
390
  @dataclass
391
391
  class MachineRequirements:
392
- machine_type: str
392
+ machine_types: list[str]
393
+ num_gpus: int | None = field(default=None)
393
394
  keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE
394
395
  base_image: str | None = None
395
396
  exposed_port: int | None = None
@@ -399,6 +400,16 @@ class MachineRequirements:
399
400
  max_multiplexing: int | None = None
400
401
  min_concurrency: int | None = None
401
402
 
403
+ def __post_init__(self):
404
+ if isinstance(self.machine_types, str):
405
+ self.machine_types = [self.machine_types]
406
+
407
+ if not isinstance(self.machine_types, list):
408
+ raise ValueError("machine_types must be a list of strings.")
409
+
410
+ if not self.machine_types:
411
+ raise ValueError("No machine type provided.")
412
+
402
413
 
403
414
  @dataclass
404
415
  class FalServerlessConnection:
@@ -489,7 +500,10 @@ class FalServerlessConnection:
489
500
  wrapped_function = to_serialized_object(function, serialization_method)
490
501
  if machine_requirements:
491
502
  wrapped_requirements = isolate_proto.MachineRequirements(
492
- machine_type=machine_requirements.machine_type,
503
+ # NOTE: backwards compatibility with old API
504
+ machine_type=machine_requirements.machine_types[0],
505
+ machine_types=machine_requirements.machine_types,
506
+ num_gpus=machine_requirements.num_gpus,
493
507
  keep_alive=machine_requirements.keep_alive,
494
508
  base_image=machine_requirements.base_image,
495
509
  exposed_port=machine_requirements.exposed_port,
@@ -516,9 +530,6 @@ class FalServerlessConnection:
516
530
  struct_metadata = isolate_proto.Struct()
517
531
  struct_metadata.update(metadata)
518
532
 
519
- if deployment_strategy == "default":
520
- deployment_strategy = "recreate"
521
-
522
533
  deployment_strategy_proto = DeploymentStrategy[
523
534
  deployment_strategy.upper()
524
535
  ].to_proto()
@@ -582,7 +593,10 @@ class FalServerlessConnection:
582
593
  wrapped_function = to_serialized_object(function, serialization_method)
583
594
  if machine_requirements:
584
595
  wrapped_requirements = isolate_proto.MachineRequirements(
585
- machine_type=machine_requirements.machine_type,
596
+ # NOTE: backwards compatibility with old API
597
+ machine_type=machine_requirements.machine_types[0],
598
+ machine_types=machine_requirements.machine_types,
599
+ num_gpus=machine_requirements.num_gpus,
586
600
  keep_alive=machine_requirements.keep_alive,
587
601
  base_image=machine_requirements.base_image,
588
602
  exposed_port=machine_requirements.exposed_port,
fal/toolkit/file/file.py CHANGED
@@ -51,7 +51,8 @@ def get_builtin_repository(id: RepositoryId) -> FileRepository:
51
51
 
52
52
  get_builtin_repository.__module__ = "__main__"
53
53
 
54
- DEFAULT_REPOSITORY: FileRepository | RepositoryId = "fal"
54
+ DEFAULT_REPOSITORY: FileRepository | RepositoryId = "fal_v2"
55
+ FALLBACK_REPOSITORY: FileRepository | RepositoryId = "cdn"
55
56
 
56
57
 
57
58
  class File(BaseModel):
@@ -126,6 +127,9 @@ class File(BaseModel):
126
127
  content_type: Optional[str] = None,
127
128
  file_name: Optional[str] = None,
128
129
  repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
130
+ fallback_repository: Optional[
131
+ FileRepository | RepositoryId
132
+ ] = FALLBACK_REPOSITORY,
129
133
  ) -> File:
130
134
  repo = (
131
135
  repository
@@ -135,8 +139,22 @@ class File(BaseModel):
135
139
 
136
140
  fdata = FileData(data, content_type, file_name)
137
141
 
142
+ try:
143
+ url = repo.save(fdata)
144
+ except Exception:
145
+ if not fallback_repository:
146
+ raise
147
+
148
+ fallback_repo = (
149
+ fallback_repository
150
+ if isinstance(fallback_repository, FileRepository)
151
+ else get_builtin_repository(fallback_repository)
152
+ )
153
+
154
+ url = fallback_repo.save(fdata)
155
+
138
156
  return cls(
139
- url=repo.save(fdata),
157
+ url=url,
140
158
  content_type=fdata.content_type,
141
159
  file_name=fdata.file_name,
142
160
  file_size=len(data),
@@ -150,6 +168,9 @@ class File(BaseModel):
150
168
  content_type: Optional[str] = None,
151
169
  repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
152
170
  multipart: bool | None = None,
171
+ fallback_repository: Optional[
172
+ FileRepository | RepositoryId
173
+ ] = FALLBACK_REPOSITORY,
153
174
  ) -> File:
154
175
  file_path = Path(path)
155
176
  if not file_path.exists():
@@ -163,11 +184,28 @@ class File(BaseModel):
163
184
 
164
185
  content_type = content_type or "application/octet-stream"
165
186
 
166
- url, data = repo.save_file(
167
- file_path,
168
- content_type=content_type,
169
- multipart=multipart,
170
- )
187
+ try:
188
+ url, data = repo.save_file(
189
+ file_path,
190
+ content_type=content_type,
191
+ multipart=multipart,
192
+ )
193
+ except Exception:
194
+ if not fallback_repository:
195
+ raise
196
+
197
+ fallback_repo = (
198
+ fallback_repository
199
+ if isinstance(fallback_repository, FileRepository)
200
+ else get_builtin_repository(fallback_repository)
201
+ )
202
+
203
+ url, data = fallback_repo.save_file(
204
+ file_path,
205
+ content_type=content_type,
206
+ multipart=multipart,
207
+ )
208
+
171
209
  return cls(
172
210
  url=url,
173
211
  file_data=data.data if data else None,
@@ -4,8 +4,10 @@ import dataclasses
4
4
  import json
5
5
  import math
6
6
  import os
7
+ import threading
7
8
  from base64 import b64encode
8
9
  from dataclasses import dataclass
10
+ from datetime import datetime, timezone
9
11
  from pathlib import Path
10
12
  from urllib.error import HTTPError
11
13
  from urllib.request import Request, urlopen
@@ -13,10 +15,74 @@ from urllib.request import Request, urlopen
13
15
  from fal.auth import key_credentials
14
16
  from fal.toolkit.exceptions import FileUploadException
15
17
  from fal.toolkit.file.types import FileData, FileRepository
18
+ from fal.toolkit.utils.retry import retry
16
19
 
17
20
  _FAL_CDN = "https://fal.media"
18
21
 
19
22
 
23
+ @dataclass
24
+ class FalV2Token:
25
+ token: str
26
+ token_type: str
27
+ base_url: str
28
+ expires_at: datetime
29
+
30
+ def is_expired(self) -> bool:
31
+ return datetime.now(timezone.utc) >= self.expires_at
32
+
33
+
34
+ class FalV2TokenManager:
35
+ def __init__(self):
36
+ self._token: FalV2Token = FalV2Token(
37
+ token="",
38
+ token_type="",
39
+ base_url="",
40
+ expires_at=datetime.min.replace(tzinfo=timezone.utc),
41
+ )
42
+ self._lock: threading.Lock = threading.Lock()
43
+
44
+ def get_token(self) -> FalV2Token:
45
+ with self._lock:
46
+ if self._token.is_expired():
47
+ self._refresh_token()
48
+ return self._token
49
+
50
+ def _refresh_token(self) -> None:
51
+ key_creds = key_credentials()
52
+ if not key_creds:
53
+ raise FileUploadException("FAL_KEY must be set")
54
+
55
+ key_id, key_secret = key_creds
56
+ headers = {
57
+ "Authorization": f"Key {key_id}:{key_secret}",
58
+ "Accept": "application/json",
59
+ "Content-Type": "application/json",
60
+ }
61
+
62
+ grpc_host = os.environ.get("FAL_HOST", "api.alpha.fal.ai")
63
+ rest_host = grpc_host.replace("api", "rest", 1)
64
+ url = f"https://{rest_host}/storage/auth/token"
65
+
66
+ req = Request(
67
+ url,
68
+ headers=headers,
69
+ data=b"{}",
70
+ method="POST",
71
+ )
72
+ with urlopen(req) as response:
73
+ result = json.load(response)
74
+
75
+ self._token = FalV2Token(
76
+ token=result["token"],
77
+ token_type=result["token_type"],
78
+ base_url=result["base_url"],
79
+ expires_at=datetime.fromisoformat(result["expires_at"]),
80
+ )
81
+
82
+
83
+ fal_v2_token_manager = FalV2TokenManager()
84
+
85
+
20
86
  @dataclass
21
87
  class ObjectLifecyclePreference:
22
88
  expriation_duration_seconds: int
@@ -29,6 +95,7 @@ GLOBAL_LIFECYCLE_PREFERENCE = ObjectLifecyclePreference(
29
95
 
30
96
  @dataclass
31
97
  class FalFileRepositoryBase(FileRepository):
98
+ @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
32
99
  def _save(self, file: FileData, storage_type: str) -> str:
33
100
  key_creds = key_credentials()
34
101
  if not key_creds:
@@ -108,26 +175,14 @@ class MultipartUpload:
108
175
 
109
176
  self._parts: list[dict] = []
110
177
 
111
- key_creds = key_credentials()
112
- if not key_creds:
113
- raise FileUploadException("FAL_KEY must be set")
114
-
115
- key_id, key_secret = key_creds
116
-
117
- self._auth_headers = {
118
- "Authorization": f"Key {key_id}:{key_secret}",
119
- }
120
- grpc_host = os.environ.get("FAL_HOST", "api.alpha.fal.ai")
121
- rest_host = grpc_host.replace("api", "rest", 1)
122
- self._storage_upload_url = f"https://{rest_host}/storage/upload"
123
-
124
178
  def create(self):
179
+ token = fal_v2_token_manager.get_token()
125
180
  try:
126
181
  req = Request(
127
- f"{self._storage_upload_url}/initiate-multipart",
182
+ f"{token.base_url}/upload/initiate-multipart",
128
183
  method="POST",
129
184
  headers={
130
- **self._auth_headers,
185
+ "Authorization": f"{token.token_type} {token.token}",
131
186
  "Accept": "application/json",
132
187
  "Content-Type": "application/json",
133
188
  },
@@ -216,8 +271,33 @@ class MultipartUpload:
216
271
 
217
272
  @dataclass
218
273
  class FalFileRepositoryV2(FalFileRepositoryBase):
274
+ @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
219
275
  def save(self, file: FileData) -> str:
220
- return self._save(file, "fal-cdn")
276
+ token = fal_v2_token_manager.get_token()
277
+ headers = {
278
+ "Authorization": f"{token.token_type} {token.token}",
279
+ "Accept": "application/json",
280
+ "X-Fal-File-Name": file.file_name,
281
+ "Content-Type": file.content_type,
282
+ }
283
+
284
+ storage_url = f"{token.base_url}/upload"
285
+
286
+ try:
287
+ req = Request(
288
+ storage_url,
289
+ data=file.data,
290
+ headers=headers,
291
+ method="PUT",
292
+ )
293
+ with urlopen(req) as response:
294
+ result = json.load(response)
295
+
296
+ return result["file_url"]
297
+ except HTTPError as e:
298
+ raise FileUploadException(
299
+ f"Error initiating upload. Status {e.status}: {e.reason}"
300
+ )
221
301
 
222
302
  def _save_multipart(
223
303
  self,
@@ -280,6 +360,7 @@ class InMemoryRepository(FileRepository):
280
360
 
281
361
  @dataclass
282
362
  class FalCDNFileRepository(FileRepository):
363
+ @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
283
364
  def save(
284
365
  self,
285
366
  file: FileData,
@@ -8,6 +8,7 @@ import uuid
8
8
  from dataclasses import dataclass
9
9
 
10
10
  from fal.toolkit.file.types import FileData, FileRepository
11
+ from fal.toolkit.utils.retry import retry
11
12
 
12
13
  DEFAULT_URL_TIMEOUT = 60 * 15 # 15 minutes
13
14
 
@@ -50,6 +51,7 @@ class GoogleStorageRepository(FileRepository):
50
51
 
51
52
  return self._bucket
52
53
 
54
+ @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
53
55
  def save(self, data: FileData) -> str:
54
56
  destination_path = posixpath.join(
55
57
  self.folder,
@@ -8,6 +8,7 @@ from dataclasses import dataclass
8
8
  from io import BytesIO
9
9
 
10
10
  from fal.toolkit.file.types import FileData, FileRepository
11
+ from fal.toolkit.utils.retry import retry
11
12
 
12
13
  DEFAULT_URL_TIMEOUT = 60 * 15 # 15 minutes
13
14
 
@@ -67,6 +68,7 @@ class R2Repository(FileRepository):
67
68
 
68
69
  return self._bucket
69
70
 
71
+ @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
70
72
  def save(self, data: FileData) -> str:
71
73
  destination_path = posixpath.join(
72
74
  self.key,
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Literal, Optional, Union
6
6
 
7
7
  from pydantic import BaseModel, Field
8
8
 
9
- from fal.toolkit.file.file import DEFAULT_REPOSITORY, File
9
+ from fal.toolkit.file.file import DEFAULT_REPOSITORY, FALLBACK_REPOSITORY, File
10
10
  from fal.toolkit.file.types import FileRepository, RepositoryId
11
11
  from fal.toolkit.utils.download_utils import _download_file_python
12
12
 
@@ -79,12 +79,16 @@ class Image(File):
79
79
  size: ImageSize | None = None,
80
80
  file_name: str | None = None,
81
81
  repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
82
+ fallback_repository: Optional[
83
+ FileRepository | RepositoryId
84
+ ] = FALLBACK_REPOSITORY,
82
85
  ) -> Image:
83
86
  obj = super().from_bytes(
84
87
  data,
85
88
  content_type=f"image/{format}",
86
89
  file_name=file_name,
87
90
  repository=repository,
91
+ fallback_repository=fallback_repository,
88
92
  )
89
93
  obj.width = size.width if size else None
90
94
  obj.height = size.height if size else None
@@ -97,6 +101,9 @@ class Image(File):
97
101
  format: ImageFormat | None = None,
98
102
  file_name: str | None = None,
99
103
  repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
104
+ fallback_repository: Optional[
105
+ FileRepository | RepositoryId
106
+ ] = FALLBACK_REPOSITORY,
100
107
  ) -> Image:
101
108
  size = ImageSize(width=pil_image.width, height=pil_image.height)
102
109
  if format is None:
@@ -110,12 +117,23 @@ class Image(File):
110
117
  # enough result quickly to utilize the underlying resources
111
118
  # efficiently.
112
119
  saving_options["compress_level"] = 1
120
+ elif format == "jpeg":
121
+ # JPEG quality is set to 95 by default, which is a good balance
122
+ # between file size and image quality.
123
+ saving_options["quality"] = 95
113
124
 
114
125
  with io.BytesIO() as f:
115
126
  pil_image.save(f, format=format, **saving_options)
116
127
  raw_image = f.getvalue()
117
128
 
118
- return cls.from_bytes(raw_image, format, size, file_name, repository)
129
+ return cls.from_bytes(
130
+ raw_image,
131
+ format,
132
+ size,
133
+ file_name,
134
+ repository,
135
+ fallback_repository=fallback_repository,
136
+ )
119
137
 
120
138
  def to_pil(self, mode: str = "RGB") -> PILImage.Image:
121
139
  try:
@@ -0,0 +1,42 @@
1
+ import functools
2
+ import random
3
+ import time
4
+ from typing import Any, Callable, Literal
5
+
6
+ BackoffType = Literal["exponential", "fixed"]
7
+
8
+
9
+ def retry(
10
+ max_retries: int = 3,
11
+ base_delay: float = 1.0,
12
+ max_delay: float = 60.0,
13
+ backoff_type: BackoffType = "exponential",
14
+ jitter: bool = False,
15
+ ) -> Callable:
16
+ def decorator(func: Callable) -> Callable:
17
+ @functools.wraps(func)
18
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
19
+ retries = 0
20
+ while retries < max_retries:
21
+ try:
22
+ return func(*args, **kwargs)
23
+ except Exception as e:
24
+ retries += 1
25
+ print(f"Retrying {retries} of {max_retries}...")
26
+ if retries == max_retries:
27
+ print(f"Max retries reached. Raising exception: {e}")
28
+ raise e
29
+
30
+ if backoff_type == "exponential":
31
+ delay = min(base_delay * (2 ** (retries - 1)), max_delay)
32
+ else: # fixed
33
+ delay = min(base_delay, max_delay)
34
+
35
+ if jitter:
36
+ delay *= random.uniform(0.5, 1.5)
37
+
38
+ time.sleep(delay)
39
+
40
+ return wrapper
41
+
42
+ return decorator
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.3.3
3
+ Version: 1.4.0
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,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=VriGPi1kVXIBM0YGAuhpE803XR-FNq1JvTW1Kz2us08,411
3
+ fal/_fal_version.py,sha256=R8-T9fmURjcuoxYpHTAjyNAhgJPDtI2jogCjqYYkfCU,411
4
4
  fal/_serialization.py,sha256=rD2YiSa8iuzCaZohZwN_MPEB-PpSKbWRDeaIDpTEjyY,7653
5
5
  fal/_version.py,sha256=EBGqrknaf1WygENX-H4fBefLvHryvJBBGtVJetaB0NY,266
6
- fal/api.py,sha256=xOPRO8-Y-7tgab_yKkQ2Lh_n4l8av5zd7srs9PCgJ5U,42020
7
- fal/app.py,sha256=mBBwTi6IldCEN-IEeznpEwyjUydqB4HkVCu49J3Vsfw,17639
6
+ fal/api.py,sha256=EuHMoWJ-LUP46moTQCXoBlzXlXCfdb0YQR7zsM694jQ,42513
7
+ fal/app.py,sha256=asJmz8gavn05s_gKXesosafw3F23zMuPUS50BX2HjI4,17712
8
8
  fal/apps.py,sha256=lge7-HITzI20l1oXdlkAzqxdMVtXRfnACIylKRWgCNQ,7151
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=ND3nwZRQDEIC130zbTfTaP0fYpR2KEkQ10e3zvOXylQ,21391
14
+ fal/sdk.py,sha256=Ve9X3WeoRBrAqP-HnJ7hdwmQzXYGU32wJMN3QPDw4yA,22081
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=jx3tGy2R7cN6lLvOzT6lhhlcjmiq64iZls2smVrmQj0,14657
@@ -24,7 +24,7 @@ fal/cli/apps.py,sha256=-DDp-Gvxz5kHho5YjAhbri8vOny_9cftAI_wP2KR5nU,8175
24
24
  fal/cli/auth.py,sha256=--MhfHGwxmtHbRkGioyn1prKn_U-pBzbz0G_QeZou-U,1352
25
25
  fal/cli/create.py,sha256=a8WDq-nJLFTeoIXqpb5cr7GR7YR9ZZrQCawNm34KXXE,627
26
26
  fal/cli/debug.py,sha256=u_urnyFzSlNnrq93zz_GXE9FX4VyVxDoamJJyrZpFI0,1312
27
- fal/cli/deploy.py,sha256=JCTQRNzbPt7Bn7lR8byJ38Ff-vQ2BQoSdzmdp9OlF3A,6790
27
+ fal/cli/deploy.py,sha256=8iVTpkQPvKHGHEaxZLnCTftRSC0MuZYC94Xf-qG27p0,6857
28
28
  fal/cli/doctor.py,sha256=U4ne9LX5gQwNblsYQ27XdO8AYDgbYjTO39EtxhwexRM,983
29
29
  fal/cli/keys.py,sha256=trDpA3LJu9S27qE_K8Hr6fKLK4vwVzbxUHq8TFrV4pw,3157
30
30
  fal/cli/main.py,sha256=_Wh_DQc02qwh-ZN7v41lZm0lDR1WseViXVOcqUlyWLg,2009
@@ -47,13 +47,13 @@ fal/toolkit/__init__.py,sha256=sV95wiUzKoiDqF9vDgq4q-BLa2sD6IpuKSqp5kdTQNE,658
47
47
  fal/toolkit/exceptions.py,sha256=elHZ7dHCJG5zlHGSBbz-ilkZe9QUvQMomJFi8Pt91LA,198
48
48
  fal/toolkit/optimize.py,sha256=p75sovF0SmRP6zxzpIaaOmqlxvXB_xEz3XPNf59EF7w,1339
49
49
  fal/toolkit/file/__init__.py,sha256=FbNl6wD-P0aSSTUwzHt4HujBXrbC3ABmaigPQA4hRfg,70
50
- fal/toolkit/file/file.py,sha256=qk_hj7U3cfvuWO-qF_eC_R8lzzVhudfnt1erWEa8eDQ,6578
50
+ fal/toolkit/file/file.py,sha256=jGdaIHMoYRCwsui3q3J6RTnngBG1-53UeJa0k_bBO2M,7756
51
51
  fal/toolkit/file/types.py,sha256=GymH0CJesJvsZ6wph7GqTGTuNjzvyMgLxQmBBxoKzS0,1627
52
- fal/toolkit/file/providers/fal.py,sha256=ClCWM4GI11hOjEIVv2IJZj2SdzBNO8iS1r1WaXFcF6I,10090
53
- fal/toolkit/file/providers/gcp.py,sha256=pUVH2qNcnO_VrDQQU8MmfYOQZMGaKQIqE4yGnYdQhAc,2003
54
- fal/toolkit/file/providers/r2.py,sha256=WxmOHF5WxHt6tKMcFjWj7ZWO8a1EXysO9lfYv_tB3MI,2627
52
+ fal/toolkit/file/providers/fal.py,sha256=4uIc-SdOo43kanirNBReXls8mbifxIcXLaZJQJ5UMcQ,12534
53
+ fal/toolkit/file/providers/gcp.py,sha256=cxG1j3yuOpFl_Dl_nCEibFE4677qkdXZhuKgb65PnjQ,2126
54
+ fal/toolkit/file/providers/r2.py,sha256=Y3DjhpmrbESUTDUtVcKtg0NMKamMTevf6cJA7OgvalI,2750
55
55
  fal/toolkit/image/__init__.py,sha256=aLcU8HzD7HyOxx-C-Bbx9kYCMHdBhy9tR98FSVJ6gSA,1830
56
- fal/toolkit/image/image.py,sha256=UDIHgkxae8LzmCvWBM9GayMnK8c0JMMfsrVlLnW5rto,4234
56
+ fal/toolkit/image/image.py,sha256=ZTNiHDSNxJuDd8_I6guZpFLgeMbkSGUchRszVl2k2SM,4861
57
57
  fal/toolkit/image/safety_checker.py,sha256=S7ow-HuoVxC6ixHWWcBrAUm2dIlgq3sTAIull6xIbAg,3105
58
58
  fal/toolkit/image/nsfw_filter/__init__.py,sha256=0d9D51EhcnJg8cZLYJjgvQJDZT74CfQu6mpvinRYRpA,216
59
59
  fal/toolkit/image/nsfw_filter/env.py,sha256=iAP2Q3vzIl--DD8nr8o3o0goAwhExN2v0feYE0nIQjs,212
@@ -62,6 +62,7 @@ fal/toolkit/image/nsfw_filter/model.py,sha256=63mu8D15z_IosoRUagRLGHy6VbLqFmrG-y
62
62
  fal/toolkit/image/nsfw_filter/requirements.txt,sha256=3Pmrd0Ny6QAeBqUNHCgffRyfaCARAPJcfSCX5cRYpbM,37
63
63
  fal/toolkit/utils/__init__.py,sha256=CrmM9DyCz5-SmcTzRSm5RaLgxy3kf0ZsSEN9uhnX2Xo,97
64
64
  fal/toolkit/utils/download_utils.py,sha256=9WMpn0mFIhkFelQpPj5KG-pC7RMyyOzGHbNRDSyz07o,17664
65
+ fal/toolkit/utils/retry.py,sha256=qyIf86LMNf9L-Xgqbjl6vf-CZmhmeDQ6Y9I4LWkU6lk,1289
65
66
  openapi_fal_rest/__init__.py,sha256=ziculmF_i6trw63LzZGFX-6W3Lwq9mCR8_UpkpvpaHI,152
66
67
  openapi_fal_rest/client.py,sha256=G6BpJg9j7-JsrAUGddYwkzeWRYickBjPdcVgXoPzxuE,2817
67
68
  openapi_fal_rest/errors.py,sha256=8mXSxdfSGzxT82srdhYbR0fHfgenxJXaUtMkaGgb6iU,470
@@ -125,8 +126,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
125
126
  openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
126
127
  openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
127
128
  openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
128
- fal-1.3.3.dist-info/METADATA,sha256=49757swSphQt8uqSMj5h83T79CeFwOWi0J0iDQHIKe8,3787
129
- fal-1.3.3.dist-info/WHEEL,sha256=UvcQYKBHoFqaQd6LKyqHw9fxEolWLQnlzP0h_LgJAfI,91
130
- fal-1.3.3.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
131
- fal-1.3.3.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
132
- fal-1.3.3.dist-info/RECORD,,
129
+ fal-1.4.0.dist-info/METADATA,sha256=ojpG29xyDDwvd7GB440xIkUa8AkeDhfy-IGQgpNKp3I,3787
130
+ fal-1.4.0.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
131
+ fal-1.4.0.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
132
+ fal-1.4.0.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
133
+ fal-1.4.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.0.0)
2
+ Generator: setuptools (74.1.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5