fal 1.3.4__py3-none-any.whl → 1.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fal might be problematic. Click here for more details.
- fal/_fal_version.py +2 -2
- fal/api.py +20 -10
- fal/app.py +2 -0
- fal/cli/_utils.py +5 -1
- fal/sdk.py +20 -3
- fal/toolkit/file/file.py +45 -7
- fal/toolkit/file/providers/fal.py +106 -22
- 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.1.dist-info}/METADATA +1 -1
- {fal-1.3.4.dist-info → fal-1.4.1.dist-info}/RECORD +16 -15
- {fal-1.3.4.dist-info → fal-1.4.1.dist-info}/WHEEL +1 -1
- {fal-1.3.4.dist-info → fal-1.4.1.dist-info}/entry_points.txt +0 -0
- {fal-1.3.4.dist-info → fal-1.4.1.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/cli/_utils.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from fal.files import find_pyproject_toml, parse_pyproject_toml
|
|
3
|
+
from fal.files import find_project_root, find_pyproject_toml, parse_pyproject_toml
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def is_app_name(app_ref: tuple[str, str | None]) -> bool:
|
|
@@ -29,6 +29,10 @@ def get_app_data_from_toml(app_name):
|
|
|
29
29
|
except KeyError:
|
|
30
30
|
raise ValueError(f"App {app_name} does not have a ref key in pyproject.toml")
|
|
31
31
|
|
|
32
|
+
# Convert the app_ref to a path relative to the project root
|
|
33
|
+
project_root, _ = find_project_root(None)
|
|
34
|
+
app_ref = str(project_root / app_ref)
|
|
35
|
+
|
|
32
36
|
app_auth = app_data.get("auth", "private")
|
|
33
37
|
|
|
34
38
|
return app_ref, app_auth
|
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,19 +4,91 @@ 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
|
|
13
|
+
from urllib.parse import urlparse, urlunparse
|
|
11
14
|
from urllib.request import Request, urlopen
|
|
12
15
|
|
|
13
16
|
from fal.auth import key_credentials
|
|
14
17
|
from fal.toolkit.exceptions import FileUploadException
|
|
15
18
|
from fal.toolkit.file.types import FileData, FileRepository
|
|
19
|
+
from fal.toolkit.utils.retry import retry
|
|
16
20
|
|
|
17
21
|
_FAL_CDN = "https://fal.media"
|
|
18
22
|
|
|
19
23
|
|
|
24
|
+
@dataclass
|
|
25
|
+
class FalV2Token:
|
|
26
|
+
token: str
|
|
27
|
+
token_type: str
|
|
28
|
+
base_upload_url: str
|
|
29
|
+
expires_at: datetime
|
|
30
|
+
|
|
31
|
+
def is_expired(self) -> bool:
|
|
32
|
+
return datetime.now(timezone.utc) >= self.expires_at
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FalV2TokenManager:
|
|
36
|
+
def __init__(self):
|
|
37
|
+
self._token: FalV2Token = FalV2Token(
|
|
38
|
+
token="",
|
|
39
|
+
token_type="",
|
|
40
|
+
base_upload_url="",
|
|
41
|
+
expires_at=datetime.min.replace(tzinfo=timezone.utc),
|
|
42
|
+
)
|
|
43
|
+
self._lock: threading.Lock = threading.Lock()
|
|
44
|
+
|
|
45
|
+
def get_token(self) -> FalV2Token:
|
|
46
|
+
with self._lock:
|
|
47
|
+
if self._token.is_expired():
|
|
48
|
+
self._refresh_token()
|
|
49
|
+
return self._token
|
|
50
|
+
|
|
51
|
+
def _refresh_token(self) -> None:
|
|
52
|
+
key_creds = key_credentials()
|
|
53
|
+
if not key_creds:
|
|
54
|
+
raise FileUploadException("FAL_KEY must be set")
|
|
55
|
+
|
|
56
|
+
key_id, key_secret = key_creds
|
|
57
|
+
headers = {
|
|
58
|
+
"Authorization": f"Key {key_id}:{key_secret}",
|
|
59
|
+
"Accept": "application/json",
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
grpc_host = os.environ.get("FAL_HOST", "api.alpha.fal.ai")
|
|
64
|
+
rest_host = grpc_host.replace("api", "rest", 1)
|
|
65
|
+
url = f"https://{rest_host}/storage/auth/token"
|
|
66
|
+
|
|
67
|
+
req = Request(
|
|
68
|
+
url,
|
|
69
|
+
headers=headers,
|
|
70
|
+
data=b"{}",
|
|
71
|
+
method="POST",
|
|
72
|
+
)
|
|
73
|
+
with urlopen(req) as response:
|
|
74
|
+
result = json.load(response)
|
|
75
|
+
|
|
76
|
+
parsed_base_url = urlparse(result["base_url"])
|
|
77
|
+
base_upload_url = urlunparse(
|
|
78
|
+
parsed_base_url._replace(netloc="upload." + parsed_base_url.netloc)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
self._token = FalV2Token(
|
|
82
|
+
token=result["token"],
|
|
83
|
+
token_type=result["token_type"],
|
|
84
|
+
base_upload_url=base_upload_url,
|
|
85
|
+
expires_at=datetime.fromisoformat(result["expires_at"]),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
fal_v2_token_manager = FalV2TokenManager()
|
|
90
|
+
|
|
91
|
+
|
|
20
92
|
@dataclass
|
|
21
93
|
class ObjectLifecyclePreference:
|
|
22
94
|
expriation_duration_seconds: int
|
|
@@ -29,6 +101,7 @@ GLOBAL_LIFECYCLE_PREFERENCE = ObjectLifecyclePreference(
|
|
|
29
101
|
|
|
30
102
|
@dataclass
|
|
31
103
|
class FalFileRepositoryBase(FileRepository):
|
|
104
|
+
@retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
|
|
32
105
|
def _save(self, file: FileData, storage_type: str) -> str:
|
|
33
106
|
key_creds = key_credentials()
|
|
34
107
|
if not key_creds:
|
|
@@ -108,26 +181,14 @@ class MultipartUpload:
|
|
|
108
181
|
|
|
109
182
|
self._parts: list[dict] = []
|
|
110
183
|
|
|
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
184
|
def create(self):
|
|
185
|
+
token = fal_v2_token_manager.get_token()
|
|
125
186
|
try:
|
|
126
187
|
req = Request(
|
|
127
|
-
f"{
|
|
188
|
+
f"{token.base_upload_url}/upload/initiate-multipart",
|
|
128
189
|
method="POST",
|
|
129
190
|
headers={
|
|
130
|
-
|
|
191
|
+
"Authorization": f"{token.token_type} {token.token}",
|
|
131
192
|
"Accept": "application/json",
|
|
132
193
|
"Content-Type": "application/json",
|
|
133
194
|
},
|
|
@@ -140,7 +201,7 @@ class MultipartUpload:
|
|
|
140
201
|
)
|
|
141
202
|
with urlopen(req) as response:
|
|
142
203
|
result = json.load(response)
|
|
143
|
-
self.
|
|
204
|
+
self._upload_url = result["upload_url"]
|
|
144
205
|
self._file_url = result["file_url"]
|
|
145
206
|
except HTTPError as exc:
|
|
146
207
|
raise FileUploadException(
|
|
@@ -180,10 +241,7 @@ class MultipartUpload:
|
|
|
180
241
|
) as executor:
|
|
181
242
|
futures = []
|
|
182
243
|
for part_number in range(1, parts + 1):
|
|
183
|
-
upload_url =
|
|
184
|
-
f"{self._file_url}?upload_id={self._upload_id}"
|
|
185
|
-
f"&part_number={part_number}"
|
|
186
|
-
)
|
|
244
|
+
upload_url = f"{self._upload_url}&part_number={part_number}"
|
|
187
245
|
futures.append(
|
|
188
246
|
executor.submit(self._upload_part, upload_url, part_number)
|
|
189
247
|
)
|
|
@@ -193,7 +251,7 @@ class MultipartUpload:
|
|
|
193
251
|
self._parts.append(entry)
|
|
194
252
|
|
|
195
253
|
def complete(self):
|
|
196
|
-
url =
|
|
254
|
+
url = self._upload_url
|
|
197
255
|
try:
|
|
198
256
|
req = Request(
|
|
199
257
|
url,
|
|
@@ -216,8 +274,33 @@ class MultipartUpload:
|
|
|
216
274
|
|
|
217
275
|
@dataclass
|
|
218
276
|
class FalFileRepositoryV2(FalFileRepositoryBase):
|
|
277
|
+
@retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
|
|
219
278
|
def save(self, file: FileData) -> str:
|
|
220
|
-
|
|
279
|
+
token = fal_v2_token_manager.get_token()
|
|
280
|
+
headers = {
|
|
281
|
+
"Authorization": f"{token.token_type} {token.token}",
|
|
282
|
+
"Accept": "application/json",
|
|
283
|
+
"X-Fal-File-Name": file.file_name,
|
|
284
|
+
"Content-Type": file.content_type,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
storage_url = f"{token.base_upload_url}/upload"
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
req = Request(
|
|
291
|
+
storage_url,
|
|
292
|
+
data=file.data,
|
|
293
|
+
headers=headers,
|
|
294
|
+
method="PUT",
|
|
295
|
+
)
|
|
296
|
+
with urlopen(req) as response:
|
|
297
|
+
result = json.load(response)
|
|
298
|
+
|
|
299
|
+
return result["file_url"]
|
|
300
|
+
except HTTPError as e:
|
|
301
|
+
raise FileUploadException(
|
|
302
|
+
f"Error initiating upload. Status {e.status}: {e.reason}"
|
|
303
|
+
)
|
|
221
304
|
|
|
222
305
|
def _save_multipart(
|
|
223
306
|
self,
|
|
@@ -280,6 +363,7 @@ class InMemoryRepository(FileRepository):
|
|
|
280
363
|
|
|
281
364
|
@dataclass
|
|
282
365
|
class FalCDNFileRepository(FileRepository):
|
|
366
|
+
@retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
|
|
283
367
|
def save(
|
|
284
368
|
self,
|
|
285
369
|
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=oFZsPxoSsCY6D2DiWMSueNvMDRRQN5ssWrPdQtlLJ_o,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
|
|
@@ -19,7 +19,7 @@ fal/auth/__init__.py,sha256=r8iA2-5ih7-Fik3gEC4HEWNFbGoxpYnXpZu1icPIoS0,3561
|
|
|
19
19
|
fal/auth/auth0.py,sha256=rSG1mgH-QGyKfzd7XyAaj1AYsWt-ho8Y_LZ-FUVWzh4,5421
|
|
20
20
|
fal/auth/local.py,sha256=sndkM6vKpeVny6NHTacVlTbiIFqaksOmw0Viqs_RN1U,1790
|
|
21
21
|
fal/cli/__init__.py,sha256=padK4o0BFqq61kxAA1qQ0jYr2SuhA2mf90B3AaRkmJA,37
|
|
22
|
-
fal/cli/_utils.py,sha256=
|
|
22
|
+
fal/cli/_utils.py,sha256=Fxq-tmYIgNDYwWtL7vMmJVDQn7Y2kA_ePmss48FPelo,1084
|
|
23
23
|
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
|
|
@@ -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=f07ps_P8qyYCeLA9fDnG46c7uIfDZOgbL7MD5LxTUQs,12677
|
|
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.1.dist-info/METADATA,sha256=2ZAk-oh2yPEg9Ib638qsxPKm8HXkDtWJNRSq_mSNTwI,3787
|
|
130
|
+
fal-1.4.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
131
|
+
fal-1.4.1.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
|
|
132
|
+
fal-1.4.1.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
|
|
133
|
+
fal-1.4.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|