fal 1.3.3__py3-none-any.whl → 1.7.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fal might be problematic. Click here for more details.
- fal/_fal_version.py +2 -2
- fal/api.py +46 -14
- fal/app.py +157 -17
- fal/apps.py +138 -3
- fal/auth/__init__.py +50 -2
- fal/cli/_utils.py +8 -2
- fal/cli/apps.py +1 -1
- fal/cli/deploy.py +34 -8
- fal/cli/main.py +2 -2
- fal/cli/run.py +1 -1
- fal/cli/runners.py +44 -0
- fal/config.py +23 -0
- fal/container.py +1 -1
- fal/sdk.py +34 -9
- fal/toolkit/file/file.py +92 -19
- fal/toolkit/file/providers/fal.py +418 -46
- fal/toolkit/file/providers/gcp.py +8 -1
- fal/toolkit/file/providers/r2.py +8 -1
- fal/toolkit/file/providers/s3.py +80 -0
- fal/toolkit/file/types.py +11 -4
- fal/toolkit/image/__init__.py +3 -3
- fal/toolkit/image/image.py +25 -2
- fal/toolkit/types.py +140 -0
- fal/toolkit/utils/download_utils.py +4 -0
- fal/toolkit/utils/retry.py +45 -0
- fal/workflows.py +10 -4
- {fal-1.3.3.dist-info → fal-1.7.2.dist-info}/METADATA +14 -9
- {fal-1.3.3.dist-info → fal-1.7.2.dist-info}/RECORD +31 -26
- {fal-1.3.3.dist-info → fal-1.7.2.dist-info}/WHEEL +1 -1
- {fal-1.3.3.dist-info → fal-1.7.2.dist-info}/entry_points.txt +0 -0
- {fal-1.3.3.dist-info → fal-1.7.2.dist-info}/top_level.txt +0 -0
fal/toolkit/file/file.py
CHANGED
|
@@ -8,6 +8,7 @@ from urllib.parse import urlparse
|
|
|
8
8
|
from zipfile import ZipFile
|
|
9
9
|
|
|
10
10
|
import pydantic
|
|
11
|
+
from fastapi import Request
|
|
11
12
|
|
|
12
13
|
# https://github.com/pydantic/pydantic/pull/2573
|
|
13
14
|
if not hasattr(pydantic, "__version__") or pydantic.__version__.startswith("1."):
|
|
@@ -21,9 +22,11 @@ else:
|
|
|
21
22
|
from pydantic import BaseModel, Field
|
|
22
23
|
|
|
23
24
|
from fal.toolkit.file.providers.fal import (
|
|
25
|
+
LIFECYCLE_PREFERENCE,
|
|
24
26
|
FalCDNFileRepository,
|
|
25
27
|
FalFileRepository,
|
|
26
28
|
FalFileRepositoryV2,
|
|
29
|
+
FalFileRepositoryV3,
|
|
27
30
|
InMemoryRepository,
|
|
28
31
|
)
|
|
29
32
|
from fal.toolkit.file.providers.gcp import GoogleStorageRepository
|
|
@@ -36,6 +39,7 @@ FileRepositoryFactory = Callable[[], FileRepository]
|
|
|
36
39
|
BUILT_IN_REPOSITORIES: dict[RepositoryId, FileRepositoryFactory] = {
|
|
37
40
|
"fal": lambda: FalFileRepository(),
|
|
38
41
|
"fal_v2": lambda: FalFileRepositoryV2(),
|
|
42
|
+
"fal_v3": lambda: FalFileRepositoryV3(),
|
|
39
43
|
"in_memory": lambda: InMemoryRepository(),
|
|
40
44
|
"gcp_storage": lambda: GoogleStorageRepository(),
|
|
41
45
|
"r2": lambda: R2Repository(),
|
|
@@ -43,7 +47,10 @@ BUILT_IN_REPOSITORIES: dict[RepositoryId, FileRepositoryFactory] = {
|
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
|
|
46
|
-
def get_builtin_repository(id: RepositoryId) -> FileRepository:
|
|
50
|
+
def get_builtin_repository(id: RepositoryId | FileRepository) -> FileRepository:
|
|
51
|
+
if isinstance(id, FileRepository):
|
|
52
|
+
return id
|
|
53
|
+
|
|
47
54
|
if id not in BUILT_IN_REPOSITORIES.keys():
|
|
48
55
|
raise ValueError(f'"{id}" is not a valid built-in file repository')
|
|
49
56
|
return BUILT_IN_REPOSITORIES[id]()
|
|
@@ -51,7 +58,9 @@ def get_builtin_repository(id: RepositoryId) -> FileRepository:
|
|
|
51
58
|
|
|
52
59
|
get_builtin_repository.__module__ = "__main__"
|
|
53
60
|
|
|
54
|
-
DEFAULT_REPOSITORY: FileRepository | RepositoryId = "
|
|
61
|
+
DEFAULT_REPOSITORY: FileRepository | RepositoryId = "fal_v3"
|
|
62
|
+
FALLBACK_REPOSITORY: FileRepository | RepositoryId = "cdn"
|
|
63
|
+
OBJECT_LIFECYCLE_PREFERENCE_KEY = "x-fal-object-lifecycle-preference"
|
|
55
64
|
|
|
56
65
|
|
|
57
66
|
class File(BaseModel):
|
|
@@ -116,7 +125,8 @@ class File(BaseModel):
|
|
|
116
125
|
url=url,
|
|
117
126
|
content_type=None,
|
|
118
127
|
file_name=None,
|
|
119
|
-
|
|
128
|
+
file_size=None,
|
|
129
|
+
file_data=None,
|
|
120
130
|
)
|
|
121
131
|
|
|
122
132
|
@classmethod
|
|
@@ -126,17 +136,38 @@ class File(BaseModel):
|
|
|
126
136
|
content_type: Optional[str] = None,
|
|
127
137
|
file_name: Optional[str] = None,
|
|
128
138
|
repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
|
|
139
|
+
fallback_repository: Optional[
|
|
140
|
+
FileRepository | RepositoryId
|
|
141
|
+
] = FALLBACK_REPOSITORY,
|
|
142
|
+
request: Optional[Request] = None,
|
|
143
|
+
save_kwargs: Optional[dict] = None,
|
|
144
|
+
fallback_save_kwargs: Optional[dict] = None,
|
|
129
145
|
) -> File:
|
|
130
|
-
repo = (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
)
|
|
146
|
+
repo = get_builtin_repository(repository)
|
|
147
|
+
|
|
148
|
+
save_kwargs = save_kwargs or {}
|
|
149
|
+
fallback_save_kwargs = fallback_save_kwargs or {}
|
|
135
150
|
|
|
136
151
|
fdata = FileData(data, content_type, file_name)
|
|
137
152
|
|
|
153
|
+
object_lifecycle_preference = (
|
|
154
|
+
request_lifecycle_preference(request) or LIFECYCLE_PREFERENCE.get()
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
url = repo.save(fdata, object_lifecycle_preference, **save_kwargs)
|
|
159
|
+
except Exception:
|
|
160
|
+
if not fallback_repository:
|
|
161
|
+
raise
|
|
162
|
+
|
|
163
|
+
fallback_repo = get_builtin_repository(fallback_repository)
|
|
164
|
+
|
|
165
|
+
url = fallback_repo.save(
|
|
166
|
+
fdata, object_lifecycle_preference, **fallback_save_kwargs
|
|
167
|
+
)
|
|
168
|
+
|
|
138
169
|
return cls(
|
|
139
|
-
url=
|
|
170
|
+
url=url,
|
|
140
171
|
content_type=fdata.content_type,
|
|
141
172
|
file_name=fdata.file_name,
|
|
142
173
|
file_size=len(data),
|
|
@@ -150,24 +181,49 @@ class File(BaseModel):
|
|
|
150
181
|
content_type: Optional[str] = None,
|
|
151
182
|
repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
|
|
152
183
|
multipart: bool | None = None,
|
|
184
|
+
fallback_repository: Optional[
|
|
185
|
+
FileRepository | RepositoryId
|
|
186
|
+
] = FALLBACK_REPOSITORY,
|
|
187
|
+
request: Optional[Request] = None,
|
|
188
|
+
save_kwargs: Optional[dict] = None,
|
|
189
|
+
fallback_save_kwargs: Optional[dict] = None,
|
|
153
190
|
) -> File:
|
|
154
191
|
file_path = Path(path)
|
|
155
192
|
if not file_path.exists():
|
|
156
193
|
raise FileNotFoundError(f"File {file_path} does not exist")
|
|
157
194
|
|
|
158
|
-
repo = (
|
|
159
|
-
repository
|
|
160
|
-
if isinstance(repository, FileRepository)
|
|
161
|
-
else get_builtin_repository(repository)
|
|
162
|
-
)
|
|
195
|
+
repo = get_builtin_repository(repository)
|
|
163
196
|
|
|
164
|
-
|
|
197
|
+
save_kwargs = save_kwargs or {}
|
|
198
|
+
fallback_save_kwargs = fallback_save_kwargs or {}
|
|
165
199
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
multipart=multipart,
|
|
200
|
+
content_type = content_type or "application/octet-stream"
|
|
201
|
+
object_lifecycle_preference = (
|
|
202
|
+
request_lifecycle_preference(request) or LIFECYCLE_PREFERENCE.get()
|
|
170
203
|
)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
url, data = repo.save_file(
|
|
207
|
+
file_path,
|
|
208
|
+
content_type=content_type,
|
|
209
|
+
multipart=multipart,
|
|
210
|
+
object_lifecycle_preference=object_lifecycle_preference,
|
|
211
|
+
**save_kwargs,
|
|
212
|
+
)
|
|
213
|
+
except Exception:
|
|
214
|
+
if not fallback_repository:
|
|
215
|
+
raise
|
|
216
|
+
|
|
217
|
+
fallback_repo = get_builtin_repository(fallback_repository)
|
|
218
|
+
|
|
219
|
+
url, data = fallback_repo.save_file(
|
|
220
|
+
file_path,
|
|
221
|
+
content_type=content_type,
|
|
222
|
+
multipart=multipart,
|
|
223
|
+
object_lifecycle_preference=object_lifecycle_preference,
|
|
224
|
+
**fallback_save_kwargs,
|
|
225
|
+
)
|
|
226
|
+
|
|
171
227
|
return cls(
|
|
172
228
|
url=url,
|
|
173
229
|
file_data=data.data if data else None,
|
|
@@ -223,3 +279,20 @@ class CompressedFile(File):
|
|
|
223
279
|
def __del__(self):
|
|
224
280
|
if self.extract_dir:
|
|
225
281
|
shutil.rmtree(self.extract_dir)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def request_lifecycle_preference(request: Optional[Request]) -> dict[str, str] | None:
|
|
285
|
+
import json
|
|
286
|
+
|
|
287
|
+
if request is None:
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
preference_str = request.headers.get(OBJECT_LIFECYCLE_PREFERENCE_KEY)
|
|
291
|
+
if preference_str is None:
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
return json.loads(preference_str)
|
|
296
|
+
except Exception as e:
|
|
297
|
+
print(f"Failed to parse object lifecycle preference: {e}")
|
|
298
|
+
return None
|