fal 1.5.16__py3-none-any.whl → 1.5.17__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.16'
16
- __version_tuple__ = version_tuple = (1, 5, 16)
15
+ __version__ = version = '1.5.17'
16
+ __version_tuple__ = version_tuple = (1, 5, 17)
fal/app.py CHANGED
@@ -23,8 +23,8 @@ from fal._serialization import include_modules_from
23
23
  from fal.api import RouteSignature
24
24
  from fal.exceptions import FalServerlessException, RequestCancelledException
25
25
  from fal.logging import get_logger
26
- from fal.toolkit.file import get_lifecycle_preference
27
- from fal.toolkit.file.providers.fal import GLOBAL_LIFECYCLE_PREFERENCE
26
+ from fal.toolkit.file import request_lifecycle_repference
27
+ from fal.toolkit.file.providers.fal import LIFECYCLE_PREFERENCE
28
28
 
29
29
  REALTIME_APP_REQUIREMENTS = ["websockets", "msgpack"]
30
30
  REQUEST_ID_KEY = "x-fal-request-id"
@@ -342,13 +342,11 @@ class App(fal.api.BaseServable):
342
342
  @app.middleware("http")
343
343
  async def set_global_object_preference(request, call_next):
344
344
  try:
345
- preference_dict = get_lifecycle_preference(request) or {}
346
- expiration_duration = preference_dict.get("expiration_duration_seconds")
347
- if expiration_duration is not None:
348
- GLOBAL_LIFECYCLE_PREFERENCE.expiration_duration_seconds = int(
349
- expiration_duration
350
- )
351
-
345
+ preference_dict = request_lifecycle_repference(request)
346
+ if preference_dict is not None:
347
+ # This will not work properly for apps with multiplexing enabled
348
+ # we may mix up the preferences between requests
349
+ LIFECYCLE_PREFERENCE.set(preference_dict)
352
350
  except Exception:
353
351
  from fastapi.logger import logger
354
352
 
@@ -357,7 +355,12 @@ class App(fal.api.BaseServable):
357
355
  self.__class__.__name__,
358
356
  )
359
357
 
360
- return await call_next(request)
358
+ try:
359
+ return await call_next(request)
360
+ finally:
361
+ # We may miss the global preference if there are operations
362
+ # being done in the background that go beyond the request
363
+ LIFECYCLE_PREFERENCE.set(None)
361
364
 
362
365
  @app.middleware("http")
363
366
  async def set_request_id(request, call_next):
fal/toolkit/file/file.py CHANGED
@@ -22,6 +22,7 @@ else:
22
22
  from pydantic import BaseModel, Field
23
23
 
24
24
  from fal.toolkit.file.providers.fal import (
25
+ LIFECYCLE_PREFERENCE,
25
26
  FalCDNFileRepository,
26
27
  FalFileRepository,
27
28
  FalFileRepositoryV2,
@@ -149,7 +150,9 @@ class File(BaseModel):
149
150
 
150
151
  fdata = FileData(data, content_type, file_name)
151
152
 
152
- object_lifecycle_preference = get_lifecycle_preference(request)
153
+ object_lifecycle_preference = (
154
+ request_lifecycle_repference(request) or LIFECYCLE_PREFERENCE.get()
155
+ )
153
156
 
154
157
  try:
155
158
  url = repo.save(fdata, object_lifecycle_preference, **save_kwargs)
@@ -203,7 +206,9 @@ class File(BaseModel):
203
206
  fallback_save_kwargs = fallback_save_kwargs or {}
204
207
 
205
208
  content_type = content_type or "application/octet-stream"
206
- object_lifecycle_preference = get_lifecycle_preference(request)
209
+ object_lifecycle_preference = (
210
+ request_lifecycle_repference(request) or LIFECYCLE_PREFERENCE.get()
211
+ )
207
212
 
208
213
  try:
209
214
  url, data = repo.save_file(
@@ -288,21 +293,18 @@ class CompressedFile(File):
288
293
  shutil.rmtree(self.extract_dir)
289
294
 
290
295
 
291
- def get_lifecycle_preference(request: Request) -> dict[str, str] | None:
296
+ def request_lifecycle_repference(request: Optional[Request]) -> dict[str, str] | None:
292
297
  import json
293
298
 
294
- preference_str = (
295
- request.headers.get(OBJECT_LIFECYCLE_PREFERENCE_KEY)
296
- if request is not None
297
- else None
298
- )
299
+ if request is None:
300
+ return None
301
+
302
+ preference_str = request.headers.get(OBJECT_LIFECYCLE_PREFERENCE_KEY)
299
303
  if preference_str is None:
300
304
  return None
301
305
 
302
- object_lifecycle_preference = {}
303
306
  try:
304
- object_lifecycle_preference = json.loads(preference_str)
305
- return object_lifecycle_preference
307
+ return json.loads(preference_str)
306
308
  except Exception as e:
307
309
  print(f"Failed to parse object lifecycle preference: {e}")
308
310
  return None
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import dataclasses
4
3
  import json
5
4
  import math
6
5
  import os
@@ -9,6 +8,7 @@ from base64 import b64encode
9
8
  from dataclasses import dataclass
10
9
  from datetime import datetime, timezone
11
10
  from pathlib import Path
11
+ from typing import Generic, TypeVar
12
12
  from urllib.error import HTTPError
13
13
  from urllib.parse import urlparse, urlunparse
14
14
  from urllib.request import Request, urlopen
@@ -105,14 +105,21 @@ fal_v2_token_manager = FalV2TokenManager()
105
105
  fal_v3_token_manager = FalV3TokenManager()
106
106
 
107
107
 
108
- @dataclass
109
- class ObjectLifecyclePreference:
110
- expiration_duration_seconds: int
108
+ VariableType = TypeVar("VariableType")
109
+
110
+
111
+ class VariableReference(Generic[VariableType]):
112
+ def __init__(self, value: VariableType) -> None:
113
+ self.set(value)
111
114
 
115
+ def get(self) -> VariableType:
116
+ return self.value
112
117
 
113
- GLOBAL_LIFECYCLE_PREFERENCE = ObjectLifecyclePreference(
114
- expiration_duration_seconds=86400
115
- )
118
+ def set(self, value: VariableType) -> None:
119
+ self.value = value
120
+
121
+
122
+ LIFECYCLE_PREFERENCE: VariableReference[dict[str, str] | None] = VariableReference(None)
116
123
 
117
124
 
118
125
  @dataclass
@@ -298,7 +305,7 @@ class MultipartUpload:
298
305
  return self._file_url
299
306
 
300
307
 
301
- class MultipartUploadV3:
308
+ class InternalMultipartUploadV3:
302
309
  MULTIPART_THRESHOLD = 100 * 1024 * 1024
303
310
  MULTIPART_CHUNK_SIZE = 10 * 1024 * 1024
304
311
  MULTIPART_MAX_CONCURRENCY = 10
@@ -314,11 +321,23 @@ class MultipartUploadV3:
314
321
  self.chunk_size = chunk_size or self.MULTIPART_CHUNK_SIZE
315
322
  self.content_type = content_type or "application/octet-stream"
316
323
  self.max_concurrency = max_concurrency or self.MULTIPART_MAX_CONCURRENCY
317
- self.access_url = None
318
- self.upload_id = None
324
+ self._access_url: str | None = None
325
+ self._upload_id: str | None = None
319
326
 
320
327
  self._parts: list[dict] = []
321
328
 
329
+ @property
330
+ def access_url(self) -> str:
331
+ if not self._access_url:
332
+ raise FileUploadException("Upload not initiated")
333
+ return self._access_url
334
+
335
+ @property
336
+ def upload_id(self) -> str:
337
+ if not self._upload_id:
338
+ raise FileUploadException("Upload not initiated")
339
+ return self._upload_id
340
+
322
341
  @property
323
342
  def auth_headers(self) -> dict[str, str]:
324
343
  token = fal_v3_token_manager.get_token()
@@ -342,8 +361,9 @@ class MultipartUploadV3:
342
361
  )
343
362
  with urlopen(req) as response:
344
363
  result = json.load(response)
345
- self.access_url = result["access_url"]
346
- self.upload_id = result["uploadId"]
364
+ self._access_url = result["access_url"]
365
+ self._upload_id = result["uploadId"]
366
+
347
367
  except HTTPError as exc:
348
368
  raise FileUploadException(
349
369
  f"Error initiating upload. Status {exc.status}: {exc.reason}"
@@ -397,7 +417,7 @@ class MultipartUploadV3:
397
417
  entry = future.result()
398
418
  self._parts.append(entry)
399
419
 
400
- def complete(self):
420
+ def complete(self) -> str:
401
421
  url = f"{self.access_url}/multipart/{self.upload_id}/complete"
402
422
  try:
403
423
  req = Request(
@@ -515,6 +535,16 @@ class InMemoryRepository(FileRepository):
515
535
 
516
536
  @dataclass
517
537
  class FalCDNFileRepository(FileRepository):
538
+ def _object_lifecycle_headers(
539
+ self,
540
+ headers: dict[str, str],
541
+ object_lifecycle_preference: dict[str, str] | None,
542
+ ):
543
+ if object_lifecycle_preference:
544
+ headers["X-Fal-Object-Lifecycle-Preference"] = json.dumps(
545
+ object_lifecycle_preference
546
+ )
547
+
518
548
  @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
519
549
  def save(
520
550
  self,
@@ -526,10 +556,10 @@ class FalCDNFileRepository(FileRepository):
526
556
  "Accept": "application/json",
527
557
  "Content-Type": file.content_type,
528
558
  "X-Fal-File-Name": file.file_name,
529
- "X-Fal-Object-Lifecycle-Preference": json.dumps(
530
- dataclasses.asdict(GLOBAL_LIFECYCLE_PREFERENCE)
531
- ),
532
559
  }
560
+
561
+ self._object_lifecycle_headers(headers, object_lifecycle_preference)
562
+
533
563
  url = os.getenv("FAL_CDN_HOST", _FAL_CDN) + "/files/upload"
534
564
  request = Request(url, headers=headers, method="POST", data=file.data)
535
565
  try:
@@ -565,26 +595,27 @@ class InternalFalFileRepositoryV3(FileRepository):
565
595
  That way it can avoid the need to refresh the token for every upload.
566
596
  """
567
597
 
598
+ def _object_lifecycle_headers(
599
+ self,
600
+ headers: dict[str, str],
601
+ object_lifecycle_preference: dict[str, str] | None,
602
+ ):
603
+ if object_lifecycle_preference:
604
+ headers["X-Fal-Object-Lifecycle"] = json.dumps(object_lifecycle_preference)
605
+
568
606
  @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
569
607
  def save(
570
608
  self, file: FileData, object_lifecycle_preference: dict[str, str] | None
571
609
  ) -> str:
572
- lifecycle = dataclasses.asdict(GLOBAL_LIFECYCLE_PREFERENCE)
573
- if object_lifecycle_preference is not None:
574
- lifecycle = {
575
- key: object_lifecycle_preference[key]
576
- if key in object_lifecycle_preference
577
- else value
578
- for key, value in lifecycle.items()
579
- }
580
-
581
610
  headers = {
582
611
  **self.auth_headers,
583
612
  "Accept": "application/json",
584
613
  "Content-Type": file.content_type,
585
614
  "X-Fal-File-Name": file.file_name,
586
- "X-Fal-Object-Lifecycle-Preference": json.dumps(lifecycle),
587
615
  }
616
+
617
+ self._object_lifecycle_headers(headers, object_lifecycle_preference)
618
+
588
619
  url = os.getenv("FAL_CDN_V3_HOST", _FAL_CDN_V3) + "/files/upload"
589
620
  request = Request(url, headers=headers, method="POST", data=file.data)
590
621
  try:
@@ -613,7 +644,7 @@ class InternalFalFileRepositoryV3(FileRepository):
613
644
  content_type: str | None = None,
614
645
  max_concurrency: int | None = None,
615
646
  ) -> str:
616
- multipart = MultipartUploadV3(
647
+ multipart = InternalMultipartUploadV3(
617
648
  file_path,
618
649
  chunk_size=chunk_size,
619
650
  content_type=content_type,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.5.16
3
+ Version: 1.5.17
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=mHfZXquAABTFJcCTtEKL7lltRkyfT160CZojEvwd1Aw,413
3
+ fal/_fal_version.py,sha256=pqPZqU0SMvSiE-_JolScA8UT2UZT9TmDka1pcSoM4sg,413
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=xTtPvDqaEHsq2lFsMwRZiHb4hzjVY3y6lV-xbzkSetI,43375
7
- fal/app.py,sha256=nLku84uTyK2VJRH_dGe_Ym8fNRsTvC3_5yolgjh9wlY,22429
7
+ fal/app.py,sha256=_0bWVe4BmbN01UoMso21-eJlLugDJ1CxVnzQdCtK93k,22636
8
8
  fal/apps.py,sha256=RpmElElJnDYjsTRQOdNYiJwd74GEOGYA38L5O5GzNEg,11068
9
9
  fal/container.py,sha256=V7riyyq8AZGwEX9QaqRQDZyDN_bUKeRKV1OOZArXjL0,622
10
10
  fal/files.py,sha256=QgfYfMKmNobMPufrAP_ga1FKcIAlSbw18Iar1-0qepo,2650
@@ -48,9 +48,9 @@ fal/toolkit/__init__.py,sha256=sV95wiUzKoiDqF9vDgq4q-BLa2sD6IpuKSqp5kdTQNE,658
48
48
  fal/toolkit/exceptions.py,sha256=elHZ7dHCJG5zlHGSBbz-ilkZe9QUvQMomJFi8Pt91LA,198
49
49
  fal/toolkit/optimize.py,sha256=p75sovF0SmRP6zxzpIaaOmqlxvXB_xEz3XPNf59EF7w,1339
50
50
  fal/toolkit/file/__init__.py,sha256=FbNl6wD-P0aSSTUwzHt4HujBXrbC3ABmaigPQA4hRfg,70
51
- fal/toolkit/file/file.py,sha256=tq-zMoNLJ1NJc9g8_RTnoIZABnRhtTtQchyMULimMTY,9442
51
+ fal/toolkit/file/file.py,sha256=yWzR0ZIqDwxwQDRiVv89_ShepzdBJoXdK5QYNph_9gQ,9475
52
52
  fal/toolkit/file/types.py,sha256=MjZ6xAhKPv4rowLo2Vcbho0sX7AQ3lm3KFyYDcw0dL4,1845
53
- fal/toolkit/file/providers/fal.py,sha256=9Rmbu_0XbMzzsfLuGJaCuRfVMFtdp2gS7ct1y7wqzVg,21549
53
+ fal/toolkit/file/providers/fal.py,sha256=V5CZz6EKmIs2-nm_mWeN9YxUOZCKIuPsZFjkZyazrgk,22375
54
54
  fal/toolkit/file/providers/gcp.py,sha256=iQtkoYUqbmKKpC5srVOYtrruZ3reGRm5lz4kM8bshgk,2247
55
55
  fal/toolkit/file/providers/r2.py,sha256=G2OHcCH2yWrVtXT4hWHEXUeEjFhbKO0koqHcd7hkczk,2871
56
56
  fal/toolkit/file/providers/s3.py,sha256=CfiA6rTBFfP-empp0cB9OW2c9F5iy0Z-kGwCs5HBICU,2524
@@ -128,8 +128,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
128
128
  openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
129
129
  openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
130
130
  openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
131
- fal-1.5.16.dist-info/METADATA,sha256=rXM_O4xtD7xvwYD3gp1Bsh-Dtgc7M3IvsTv3EiVjVlg,3997
132
- fal-1.5.16.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
133
- fal-1.5.16.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
134
- fal-1.5.16.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
135
- fal-1.5.16.dist-info/RECORD,,
131
+ fal-1.5.17.dist-info/METADATA,sha256=8UnuHRmBcWkQTTAkojd_Ab7oXnJQdCg7wHaKfvgfUu4,3997
132
+ fal-1.5.17.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
133
+ fal-1.5.17.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
134
+ fal-1.5.17.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
135
+ fal-1.5.17.dist-info/RECORD,,
File without changes