fal 1.5.1__py3-none-any.whl → 1.5.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 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.5.1'
16
- __version_tuple__ = version_tuple = (1, 5, 1)
15
+ __version__ = version = '1.5.2'
16
+ __version_tuple__ = version_tuple = (1, 5, 2)
fal/app.py CHANGED
@@ -402,7 +402,10 @@ def _fal_websocket_template(
402
402
  batch.append(next_input)
403
403
 
404
404
  t0 = loop.time()
405
- output = await loop.run_in_executor(None, func, self, *batch) # type: ignore
405
+ if inspect.iscoroutinefunction(func):
406
+ output = await func(self, *batch)
407
+ else:
408
+ output = await loop.run_in_executor(None, func, self, *batch) # type: ignore
406
409
  total_time = loop.time() - t0
407
410
  if not isinstance(output, dict):
408
411
  # Handle pydantic output modal
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."):
@@ -24,6 +25,7 @@ from fal.toolkit.file.providers.fal import (
24
25
  FalCDNFileRepository,
25
26
  FalFileRepository,
26
27
  FalFileRepositoryV2,
28
+ FalFileRepositoryV3,
27
29
  InMemoryRepository,
28
30
  )
29
31
  from fal.toolkit.file.providers.gcp import GoogleStorageRepository
@@ -36,6 +38,7 @@ FileRepositoryFactory = Callable[[], FileRepository]
36
38
  BUILT_IN_REPOSITORIES: dict[RepositoryId, FileRepositoryFactory] = {
37
39
  "fal": lambda: FalFileRepository(),
38
40
  "fal_v2": lambda: FalFileRepositoryV2(),
41
+ "fal_v3": lambda: FalFileRepositoryV3(),
39
42
  "in_memory": lambda: InMemoryRepository(),
40
43
  "gcp_storage": lambda: GoogleStorageRepository(),
41
44
  "r2": lambda: R2Repository(),
@@ -53,6 +56,7 @@ get_builtin_repository.__module__ = "__main__"
53
56
 
54
57
  DEFAULT_REPOSITORY: FileRepository | RepositoryId = "fal_v2"
55
58
  FALLBACK_REPOSITORY: FileRepository | RepositoryId = "cdn"
59
+ OBJECT_LIFECYCLE_PREFERENCE_KEY = "x-fal-object-lifecycle-preference"
56
60
 
57
61
 
58
62
  class File(BaseModel):
@@ -130,6 +134,7 @@ class File(BaseModel):
130
134
  fallback_repository: Optional[
131
135
  FileRepository | RepositoryId
132
136
  ] = FALLBACK_REPOSITORY,
137
+ request: Optional[Request] = None,
133
138
  ) -> File:
134
139
  repo = (
135
140
  repository
@@ -139,8 +144,10 @@ class File(BaseModel):
139
144
 
140
145
  fdata = FileData(data, content_type, file_name)
141
146
 
147
+ object_lifecycle_preference = _get_lifecycle_preference(request)
148
+
142
149
  try:
143
- url = repo.save(fdata)
150
+ url = repo.save(fdata, object_lifecycle_preference)
144
151
  except Exception:
145
152
  if not fallback_repository:
146
153
  raise
@@ -151,7 +158,7 @@ class File(BaseModel):
151
158
  else get_builtin_repository(fallback_repository)
152
159
  )
153
160
 
154
- url = fallback_repo.save(fdata)
161
+ url = fallback_repo.save(fdata, object_lifecycle_preference)
155
162
 
156
163
  return cls(
157
164
  url=url,
@@ -171,6 +178,7 @@ class File(BaseModel):
171
178
  fallback_repository: Optional[
172
179
  FileRepository | RepositoryId
173
180
  ] = FALLBACK_REPOSITORY,
181
+ request: Optional[Request] = None,
174
182
  ) -> File:
175
183
  file_path = Path(path)
176
184
  if not file_path.exists():
@@ -183,12 +191,14 @@ class File(BaseModel):
183
191
  )
184
192
 
185
193
  content_type = content_type or "application/octet-stream"
194
+ object_lifecycle_preference = _get_lifecycle_preference(request)
186
195
 
187
196
  try:
188
197
  url, data = repo.save_file(
189
198
  file_path,
190
199
  content_type=content_type,
191
200
  multipart=multipart,
201
+ object_lifecycle_preference=object_lifecycle_preference,
192
202
  )
193
203
  except Exception:
194
204
  if not fallback_repository:
@@ -204,6 +214,7 @@ class File(BaseModel):
204
214
  file_path,
205
215
  content_type=content_type,
206
216
  multipart=multipart,
217
+ object_lifecycle_preference=object_lifecycle_preference,
207
218
  )
208
219
 
209
220
  return cls(
@@ -261,3 +272,23 @@ class CompressedFile(File):
261
272
  def __del__(self):
262
273
  if self.extract_dir:
263
274
  shutil.rmtree(self.extract_dir)
275
+
276
+
277
+ def _get_lifecycle_preference(request: Request) -> dict[str, str] | None:
278
+ import json
279
+
280
+ preference_str = (
281
+ request.headers.get(OBJECT_LIFECYCLE_PREFERENCE_KEY)
282
+ if request is not None
283
+ else None
284
+ )
285
+ if preference_str is None:
286
+ return None
287
+
288
+ object_lifecycle_preference = {}
289
+ try:
290
+ object_lifecycle_preference = json.loads(preference_str)
291
+ return object_lifecycle_preference
292
+ except Exception as e:
293
+ print(f"Failed to parse object lifecycle preference: {e}")
294
+ return None
@@ -19,6 +19,7 @@ from fal.toolkit.file.types import FileData, FileRepository
19
19
  from fal.toolkit.utils.retry import retry
20
20
 
21
21
  _FAL_CDN = "https://fal.media"
22
+ _FAL_CDN_V3 = "https://v3.fal.media"
22
23
 
23
24
 
24
25
  @dataclass
@@ -91,11 +92,11 @@ fal_v2_token_manager = FalV2TokenManager()
91
92
 
92
93
  @dataclass
93
94
  class ObjectLifecyclePreference:
94
- expriation_duration_seconds: int
95
+ expiration_duration_seconds: int
95
96
 
96
97
 
97
98
  GLOBAL_LIFECYCLE_PREFERENCE = ObjectLifecyclePreference(
98
- expriation_duration_seconds=86400
99
+ expiration_duration_seconds=86400
99
100
  )
100
101
 
101
102
 
@@ -158,7 +159,9 @@ class FalFileRepositoryBase(FileRepository):
158
159
 
159
160
  @dataclass
160
161
  class FalFileRepository(FalFileRepositoryBase):
161
- def save(self, file: FileData) -> str:
162
+ def save(
163
+ self, file: FileData, object_lifecycle_preference: dict[str, str] | None = None
164
+ ) -> str:
162
165
  return self._save(file, "gcs")
163
166
 
164
167
 
@@ -275,7 +278,9 @@ class MultipartUpload:
275
278
  @dataclass
276
279
  class FalFileRepositoryV2(FalFileRepositoryBase):
277
280
  @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
278
- def save(self, file: FileData) -> str:
281
+ def save(
282
+ self, file: FileData, object_lifecycle_preference: dict[str, str] | None = None
283
+ ) -> str:
279
284
  token = fal_v2_token_manager.get_token()
280
285
  headers = {
281
286
  "Authorization": f"{token.token_type} {token.token}",
@@ -327,6 +332,7 @@ class FalFileRepositoryV2(FalFileRepositoryBase):
327
332
  multipart_threshold: int | None = None,
328
333
  multipart_chunk_size: int | None = None,
329
334
  multipart_max_concurrency: int | None = None,
335
+ object_lifecycle_preference: dict[str, str] | None = None,
330
336
  ) -> tuple[str, FileData | None]:
331
337
  if multipart is None:
332
338
  threshold = multipart_threshold or MultipartUpload.MULTIPART_THRESHOLD
@@ -347,7 +353,7 @@ class FalFileRepositoryV2(FalFileRepositoryBase):
347
353
  content_type=content_type,
348
354
  file_name=os.path.basename(file_path),
349
355
  )
350
- url = self.save(data)
356
+ url = self.save(data, object_lifecycle_preference)
351
357
 
352
358
  return url, data
353
359
 
@@ -357,6 +363,7 @@ class InMemoryRepository(FileRepository):
357
363
  def save(
358
364
  self,
359
365
  file: FileData,
366
+ object_lifecycle_preference: dict[str, str] | None = None,
360
367
  ) -> str:
361
368
  return f'data:{file.content_type};base64,{b64encode(file.data).decode("utf-8")}'
362
369
 
@@ -367,6 +374,7 @@ class FalCDNFileRepository(FileRepository):
367
374
  def save(
368
375
  self,
369
376
  file: FileData,
377
+ object_lifecycle_preference: dict[str, str] | None = None,
370
378
  ) -> str:
371
379
  headers = {
372
380
  **self.auth_headers,
@@ -401,3 +409,50 @@ class FalCDNFileRepository(FileRepository):
401
409
  "Authorization": f"Bearer {key_id}:{key_secret}",
402
410
  "User-Agent": "fal/0.1.0",
403
411
  }
412
+
413
+
414
+ @dataclass
415
+ class FalFileRepositoryV3(FileRepository):
416
+ @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
417
+ def save(
418
+ self, file: FileData, user_lifecycle_preference: dict[str, str] | None
419
+ ) -> str:
420
+ object_lifecycle_preference = dataclasses.asdict(GLOBAL_LIFECYCLE_PREFERENCE)
421
+
422
+ if user_lifecycle_preference is not None:
423
+ object_lifecycle_preference = {
424
+ key: user_lifecycle_preference[key]
425
+ if key in user_lifecycle_preference
426
+ else value
427
+ for key, value in object_lifecycle_preference.items()
428
+ }
429
+
430
+ headers = {
431
+ **self.auth_headers,
432
+ "Accept": "application/json",
433
+ "Content-Type": file.content_type,
434
+ "X-Fal-File-Name": file.file_name,
435
+ "X-Fal-Object-Lifecycle-Preference": json.dumps(
436
+ object_lifecycle_preference
437
+ ),
438
+ }
439
+ url = os.getenv("FAL_CDN_V3_HOST", _FAL_CDN_V3) + "/files/upload"
440
+ request = Request(url, headers=headers, method="POST", data=file.data)
441
+ try:
442
+ with urlopen(request) as response:
443
+ result = json.load(response)
444
+ except HTTPError as e:
445
+ raise FileUploadException(
446
+ f"Error initiating upload. Status {e.status}: {e.reason}"
447
+ )
448
+
449
+ access_url = result["access_url"]
450
+ return access_url
451
+
452
+ @property
453
+ def auth_headers(self) -> dict[str, str]:
454
+ token = fal_v2_token_manager.get_token()
455
+ return {
456
+ "Authorization": f"{token.token_type} {token.token}",
457
+ "User-Agent": "fal/0.1.0",
458
+ }
fal/toolkit/file/types.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from mimetypes import guess_extension, guess_type
5
5
  from pathlib import Path
6
- from typing import Literal
6
+ from typing import Literal, Optional
7
7
  from uuid import uuid4
8
8
 
9
9
 
@@ -29,12 +29,18 @@ class FileData:
29
29
  self.file_name = file_name
30
30
 
31
31
 
32
- RepositoryId = Literal["fal", "fal_v2", "in_memory", "gcp_storage", "r2", "cdn"]
32
+ RepositoryId = Literal[
33
+ "fal", "fal_v2", "fal_v3", "in_memory", "gcp_storage", "r2", "cdn"
34
+ ]
33
35
 
34
36
 
35
37
  @dataclass
36
38
  class FileRepository:
37
- def save(self, data: FileData) -> str:
39
+ def save(
40
+ self,
41
+ data: FileData,
42
+ object_lifecycle_preference: Optional[dict[str, str]] = None,
43
+ ) -> str:
38
44
  raise NotImplementedError()
39
45
 
40
46
  def save_file(
@@ -45,6 +51,7 @@ class FileRepository:
45
51
  multipart_threshold: int | None = None,
46
52
  multipart_chunk_size: int | None = None,
47
53
  multipart_max_concurrency: int | None = None,
54
+ object_lifecycle_preference: Optional[dict[str, str]] = None,
48
55
  ) -> tuple[str, FileData | None]:
49
56
  if multipart:
50
57
  raise NotImplementedError()
@@ -52,4 +59,4 @@ class FileRepository:
52
59
  with open(file_path, "rb") as fobj:
53
60
  data = FileData(fobj.read(), content_type, Path(file_path).name)
54
61
 
55
- return self.save(data), data
62
+ return self.save(data, object_lifecycle_preference), data
@@ -4,6 +4,7 @@ import io
4
4
  from tempfile import NamedTemporaryFile
5
5
  from typing import TYPE_CHECKING, Literal, Optional, Union
6
6
 
7
+ from fastapi import Request
7
8
  from pydantic import BaseModel, Field
8
9
 
9
10
  from fal.toolkit.file.file import DEFAULT_REPOSITORY, FALLBACK_REPOSITORY, File
@@ -82,6 +83,7 @@ class Image(File):
82
83
  fallback_repository: Optional[
83
84
  FileRepository | RepositoryId
84
85
  ] = FALLBACK_REPOSITORY,
86
+ request: Optional[Request] = None,
85
87
  ) -> Image:
86
88
  obj = super().from_bytes(
87
89
  data,
@@ -89,6 +91,7 @@ class Image(File):
89
91
  file_name=file_name,
90
92
  repository=repository,
91
93
  fallback_repository=fallback_repository,
94
+ request=request,
92
95
  )
93
96
  obj.width = size.width if size else None
94
97
  obj.height = size.height if size else None
@@ -104,6 +107,7 @@ class Image(File):
104
107
  fallback_repository: Optional[
105
108
  FileRepository | RepositoryId
106
109
  ] = FALLBACK_REPOSITORY,
110
+ request: Optional[Request] = None,
107
111
  ) -> Image:
108
112
  size = ImageSize(width=pil_image.width, height=pil_image.height)
109
113
  if format is None:
@@ -133,6 +137,7 @@ class Image(File):
133
137
  file_name,
134
138
  repository,
135
139
  fallback_repository=fallback_repository,
140
+ request=request,
136
141
  )
137
142
 
138
143
  def to_pil(self, mode: str = "RGB") -> PILImage.Image:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.5.1
3
+ Version: 1.5.2
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,10 +1,10 @@
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=W6YuN1JOd6M-rSt9HDXK91AutRDYXTjJT_LQg3rCsjk,411
3
+ fal/_fal_version.py,sha256=P-JlE1bO3FGbbntvKHqxjgKk7ORvjFhybkK-h8R2s5g,411
4
4
  fal/_serialization.py,sha256=rD2YiSa8iuzCaZohZwN_MPEB-PpSKbWRDeaIDpTEjyY,7653
5
5
  fal/_version.py,sha256=EBGqrknaf1WygENX-H4fBefLvHryvJBBGtVJetaB0NY,266
6
6
  fal/api.py,sha256=wmXywHvkdKe0AlsPmXt8_nidPhoC_Ho4BrUi7In4Hek,43278
7
- fal/app.py,sha256=kzjHA325RDwUl-_9lTaL77L-DD1Lh-KEZFuJLPkOC9U,17899
7
+ fal/app.py,sha256=u25k-MLKU_7eux4PuN24iq1zZf5hSeUeYkO7kUIA6uI,18021
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
@@ -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=jGdaIHMoYRCwsui3q3J6RTnngBG1-53UeJa0k_bBO2M,7756
51
- fal/toolkit/file/types.py,sha256=GymH0CJesJvsZ6wph7GqTGTuNjzvyMgLxQmBBxoKzS0,1627
52
- fal/toolkit/file/providers/fal.py,sha256=f07ps_P8qyYCeLA9fDnG46c7uIfDZOgbL7MD5LxTUQs,12677
50
+ fal/toolkit/file/file.py,sha256=GuY4zTqYMod2e9pLorZdPJqmGQ2CmmxaYxmtBm4J7vQ,8913
51
+ fal/toolkit/file/types.py,sha256=MjZ6xAhKPv4rowLo2Vcbho0sX7AQ3lm3KFyYDcw0dL4,1845
52
+ fal/toolkit/file/providers/fal.py,sha256=W3XKbAsRuCKPDHwVK1IBen_Tdp23Pi8qhgfiVbsPc4s,14777
53
53
  fal/toolkit/file/providers/gcp.py,sha256=cxG1j3yuOpFl_Dl_nCEibFE4677qkdXZhuKgb65PnjQ,2126
54
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=ZTNiHDSNxJuDd8_I6guZpFLgeMbkSGUchRszVl2k2SM,4861
56
+ fal/toolkit/image/image.py,sha256=ZSkozciP4XxaGnvrR_mP4utqE3_QhoPN0dau9FJ2Xco,5033
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
@@ -126,8 +126,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
126
126
  openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
127
127
  openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
128
128
  openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
129
- fal-1.5.1.dist-info/METADATA,sha256=sQTFRl_qxwkO_g6moEVwRL4LRCSyrvkpuD_AHbUhwFI,3787
130
- fal-1.5.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
131
- fal-1.5.1.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
132
- fal-1.5.1.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
133
- fal-1.5.1.dist-info/RECORD,,
129
+ fal-1.5.2.dist-info/METADATA,sha256=hOKSZaGlMc6uIwV8Te50B9vaqRBxE2YjCeALzTpveRo,3787
130
+ fal-1.5.2.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
131
+ fal-1.5.2.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
132
+ fal-1.5.2.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
133
+ fal-1.5.2.dist-info/RECORD,,
File without changes