fal 1.3.4__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 +2 -2
- fal/api.py +20 -10
- fal/app.py +2 -0
- fal/sdk.py +20 -3
- fal/toolkit/file/file.py +45 -7
- fal/toolkit/file/providers/fal.py +97 -16
- fal/toolkit/file/providers/gcp.py +2 -0
- fal/toolkit/file/providers/r2.py +2 -0
- fal/toolkit/image/image.py +16 -2
- fal/toolkit/utils/retry.py +42 -0
- {fal-1.3.4.dist-info → fal-1.4.0.dist-info}/METADATA +1 -1
- {fal-1.3.4.dist-info → fal-1.4.0.dist-info}/RECORD +15 -14
- {fal-1.3.4.dist-info → fal-1.4.0.dist-info}/WHEEL +1 -1
- {fal-1.3.4.dist-info → fal-1.4.0.dist-info}/entry_points.txt +0 -0
- {fal-1.3.4.dist-info → fal-1.4.0.dist-info}/top_level.txt +0 -0
fal/_fal_version.py
CHANGED
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
|
-
|
|
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
|
-
|
|
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/sdk.py
CHANGED
|
@@ -389,7 +389,8 @@ def _from_grpc_hosted_run_result(
|
|
|
389
389
|
|
|
390
390
|
@dataclass
|
|
391
391
|
class MachineRequirements:
|
|
392
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -579,7 +593,10 @@ class FalServerlessConnection:
|
|
|
579
593
|
wrapped_function = to_serialized_object(function, serialization_method)
|
|
580
594
|
if machine_requirements:
|
|
581
595
|
wrapped_requirements = isolate_proto.MachineRequirements(
|
|
582
|
-
|
|
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,
|
|
583
600
|
keep_alive=machine_requirements.keep_alive,
|
|
584
601
|
base_image=machine_requirements.base_image,
|
|
585
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 = "
|
|
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=
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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"{
|
|
182
|
+
f"{token.base_url}/upload/initiate-multipart",
|
|
128
183
|
method="POST",
|
|
129
184
|
headers={
|
|
130
|
-
|
|
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
|
-
|
|
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,
|
fal/toolkit/file/providers/r2.py
CHANGED
|
@@ -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,
|
fal/toolkit/image/image.py
CHANGED
|
@@ -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:
|
|
@@ -119,7 +126,14 @@ class Image(File):
|
|
|
119
126
|
pil_image.save(f, format=format, **saving_options)
|
|
120
127
|
raw_image = f.getvalue()
|
|
121
128
|
|
|
122
|
-
return cls.from_bytes(
|
|
129
|
+
return cls.from_bytes(
|
|
130
|
+
raw_image,
|
|
131
|
+
format,
|
|
132
|
+
size,
|
|
133
|
+
file_name,
|
|
134
|
+
repository,
|
|
135
|
+
fallback_repository=fallback_repository,
|
|
136
|
+
)
|
|
123
137
|
|
|
124
138
|
def to_pil(self, mode: str = "RGB") -> PILImage.Image:
|
|
125
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,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=
|
|
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=
|
|
7
|
-
fal/app.py,sha256=
|
|
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=
|
|
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
|
|
@@ -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=
|
|
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=
|
|
53
|
-
fal/toolkit/file/providers/gcp.py,sha256=
|
|
54
|
-
fal/toolkit/file/providers/r2.py,sha256=
|
|
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=
|
|
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.
|
|
129
|
-
fal-1.
|
|
130
|
-
fal-1.
|
|
131
|
-
fal-1.
|
|
132
|
-
fal-1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|