fal 1.7.5__py3-none-any.whl → 1.7.7__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.7.5'
16
- __version_tuple__ = version_tuple = (1, 7, 5)
15
+ __version__ = version = '1.7.7'
16
+ __version_tuple__ = version_tuple = (1, 7, 7)
fal/app.py CHANGED
@@ -374,6 +374,11 @@ class App(fal.api.BaseServable):
374
374
  @app.middleware("http")
375
375
  async def set_request_id(request, call_next):
376
376
  # NOTE: Setting request_id is not supported for websocket/realtime endpoints
377
+ if not os.getenv("IS_ISOLATE_AGENT") or not os.environ.get(
378
+ "NOMAD_ALLOC_PORT_grpc"
379
+ ):
380
+ # If not running in the expected environment, skip setting request_id
381
+ return await call_next(request)
377
382
 
378
383
  if self.isolate_channel is None:
379
384
  grpc_port = os.environ.get("NOMAD_ALLOC_PORT_grpc")
@@ -190,14 +190,6 @@ class FalFileRepository(FalFileRepositoryBase):
190
190
  return self._save(file, "gcs")
191
191
 
192
192
 
193
- @dataclass
194
- class FalFileRepositoryV3(FalFileRepositoryBase):
195
- def save(
196
- self, file: FileData, object_lifecycle_preference: dict[str, str] | None = None
197
- ) -> str:
198
- return self._save(file, "fal-cdn-v3")
199
-
200
-
201
193
  class MultipartUpload:
202
194
  MULTIPART_THRESHOLD = 100 * 1024 * 1024
203
195
  MULTIPART_CHUNK_SIZE = 100 * 1024 * 1024
@@ -366,6 +358,212 @@ class MultipartUpload:
366
358
  return multipart.complete()
367
359
 
368
360
 
361
+ class MultipartUploadV3:
362
+ MULTIPART_THRESHOLD = 100 * 1024 * 1024
363
+ MULTIPART_CHUNK_SIZE = 10 * 1024 * 1024
364
+ MULTIPART_MAX_CONCURRENCY = 10
365
+
366
+ def __init__(
367
+ self,
368
+ file_name: str,
369
+ chunk_size: int | None = None,
370
+ content_type: str | None = None,
371
+ max_concurrency: int | None = None,
372
+ ) -> None:
373
+ self.file_name = file_name
374
+ self.chunk_size = chunk_size or self.MULTIPART_CHUNK_SIZE
375
+ self.content_type = content_type or "application/octet-stream"
376
+ self.max_concurrency = max_concurrency or self.MULTIPART_MAX_CONCURRENCY
377
+
378
+ self._access_url: str | None = None
379
+ self._upload_url: str | None = None
380
+
381
+ self._parts: list[dict] = []
382
+
383
+ @property
384
+ def access_url(self) -> str:
385
+ if not self._access_url:
386
+ raise FileUploadException("Upload not initiated")
387
+ return self._access_url
388
+
389
+ @property
390
+ def upload_url(self) -> str:
391
+ if not self._upload_url:
392
+ raise FileUploadException("Upload not initiated")
393
+ return self._upload_url
394
+
395
+ @property
396
+ def auth_headers(self) -> dict[str, str]:
397
+ fal_key = key_credentials()
398
+ if not fal_key:
399
+ raise FileUploadException("FAL_KEY must be set")
400
+
401
+ key_id, key_secret = fal_key
402
+ return {
403
+ "Authorization": f"Key {key_id}:{key_secret}",
404
+ }
405
+
406
+ def create(self):
407
+ grpc_host = os.environ.get("FAL_HOST", "api.alpha.fal.ai")
408
+ rest_host = grpc_host.replace("api", "rest", 1)
409
+ url = f"https://{rest_host}/storage/upload/initiate-multipart?storage_type=fal-cdn-v3"
410
+
411
+ try:
412
+ req = Request(
413
+ url,
414
+ method="POST",
415
+ headers={
416
+ **self.auth_headers,
417
+ "Accept": "application/json",
418
+ },
419
+ data=json.dumps(
420
+ {
421
+ "file_name": self.file_name,
422
+ "content_type": self.content_type,
423
+ }
424
+ ).encode(),
425
+ )
426
+
427
+ with urlopen(req) as response:
428
+ result = json.load(response)
429
+ self._access_url = result["file_url"]
430
+ self._upload_url = result["upload_url"]
431
+
432
+ except HTTPError as exc:
433
+ raise FileUploadException(
434
+ f"Error initiating upload. Status {exc.status}: {exc.reason}"
435
+ )
436
+
437
+ @retry(max_retries=5, base_delay=1, backoff_type="exponential", jitter=True)
438
+ def upload_part(self, part_number: int, data: bytes) -> None:
439
+ parsed = urlparse(self.upload_url)
440
+ part_path = parsed.path + f"/{part_number}"
441
+ url = urlunparse(parsed._replace(path=part_path))
442
+
443
+ req = Request(
444
+ url,
445
+ method="PUT",
446
+ headers={
447
+ "Content-Type": self.content_type,
448
+ },
449
+ data=data,
450
+ )
451
+
452
+ try:
453
+ with urlopen(req) as resp:
454
+ self._parts.append(
455
+ {
456
+ "partNumber": part_number,
457
+ "etag": resp.headers["ETag"],
458
+ }
459
+ )
460
+ except HTTPError as exc:
461
+ raise FileUploadException(
462
+ f"Error uploading part {part_number} to {url}. "
463
+ f"Status {exc.status}: {exc.reason}"
464
+ )
465
+
466
+ def complete(self) -> str:
467
+ parsed = urlparse(self.upload_url)
468
+ complete_path = parsed.path + "/complete"
469
+ url = urlunparse(parsed._replace(path=complete_path))
470
+
471
+ try:
472
+ req = Request(
473
+ url,
474
+ method="POST",
475
+ headers={
476
+ "Accept": "application/json",
477
+ "Content-Type": "application/json",
478
+ },
479
+ data=json.dumps({"parts": self._parts}).encode(),
480
+ )
481
+ with urlopen(req):
482
+ pass
483
+ except HTTPError as e:
484
+ raise FileUploadException(
485
+ f"Error completing upload {url}. Status {e.status}: {e.reason}"
486
+ )
487
+
488
+ return self.access_url
489
+
490
+ @classmethod
491
+ def save(
492
+ cls,
493
+ file: FileData,
494
+ chunk_size: int | None = None,
495
+ max_concurrency: int | None = None,
496
+ ):
497
+ import concurrent.futures
498
+
499
+ multipart = cls(
500
+ file.file_name,
501
+ chunk_size=chunk_size,
502
+ content_type=file.content_type,
503
+ max_concurrency=max_concurrency,
504
+ )
505
+ multipart.create()
506
+
507
+ parts = math.ceil(len(file.data) / multipart.chunk_size)
508
+ with concurrent.futures.ThreadPoolExecutor(
509
+ max_workers=multipart.max_concurrency
510
+ ) as executor:
511
+ futures = []
512
+ for part_number in range(1, parts + 1):
513
+ start = (part_number - 1) * multipart.chunk_size
514
+ data = file.data[start : start + multipart.chunk_size]
515
+ futures.append(
516
+ executor.submit(multipart.upload_part, part_number, data)
517
+ )
518
+
519
+ for future in concurrent.futures.as_completed(futures):
520
+ future.result()
521
+
522
+ return multipart.complete()
523
+
524
+ @classmethod
525
+ def save_file(
526
+ cls,
527
+ file_path: str | Path,
528
+ chunk_size: int | None = None,
529
+ content_type: str | None = None,
530
+ max_concurrency: int | None = None,
531
+ ) -> str:
532
+ import concurrent.futures
533
+
534
+ file_name = os.path.basename(file_path)
535
+ size = os.path.getsize(file_path)
536
+
537
+ multipart = cls(
538
+ file_name,
539
+ chunk_size=chunk_size,
540
+ content_type=content_type,
541
+ max_concurrency=max_concurrency,
542
+ )
543
+ multipart.create()
544
+
545
+ parts = math.ceil(size / multipart.chunk_size)
546
+ with concurrent.futures.ThreadPoolExecutor(
547
+ max_workers=multipart.max_concurrency
548
+ ) as executor:
549
+ futures = []
550
+ for part_number in range(1, parts + 1):
551
+
552
+ def _upload_part(pn: int) -> None:
553
+ with open(file_path, "rb") as f:
554
+ start = (pn - 1) * multipart.chunk_size
555
+ f.seek(start)
556
+ data = f.read(multipart.chunk_size)
557
+ multipart.upload_part(pn, data)
558
+
559
+ futures.append(executor.submit(_upload_part, part_number))
560
+
561
+ for future in concurrent.futures.as_completed(futures):
562
+ future.result()
563
+
564
+ return multipart.complete()
565
+
566
+
369
567
  class InternalMultipartUploadV3:
370
568
  MULTIPART_THRESHOLD = 100 * 1024 * 1024
371
569
  MULTIPART_CHUNK_SIZE = 10 * 1024 * 1024
@@ -703,6 +901,122 @@ class FalCDNFileRepository(FileRepository):
703
901
  }
704
902
 
705
903
 
904
+ @dataclass
905
+ class FalFileRepositoryV3(FileRepository):
906
+ @property
907
+ def auth_headers(self) -> dict[str, str]:
908
+ fal_key = key_credentials()
909
+ if not fal_key:
910
+ raise FileUploadException("FAL_KEY must be set")
911
+
912
+ key_id, key_secret = fal_key
913
+ return {
914
+ "Authorization": f"Key {key_id}:{key_secret}",
915
+ "User-Agent": "fal/0.1.0",
916
+ }
917
+
918
+ @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
919
+ def save(
920
+ self,
921
+ file: FileData,
922
+ multipart: bool | None = None,
923
+ multipart_threshold: int | None = None,
924
+ multipart_chunk_size: int | None = None,
925
+ multipart_max_concurrency: int | None = None,
926
+ object_lifecycle_preference: dict[str, str] | None = None,
927
+ ) -> str:
928
+ if multipart is None:
929
+ threshold = multipart_threshold or MultipartUploadV3.MULTIPART_THRESHOLD
930
+ multipart = len(file.data) > threshold
931
+
932
+ if multipart:
933
+ return MultipartUploadV3.save(
934
+ file,
935
+ chunk_size=multipart_chunk_size,
936
+ max_concurrency=multipart_max_concurrency,
937
+ )
938
+
939
+ headers = {
940
+ **self.auth_headers,
941
+ "Accept": "application/json",
942
+ "Content-Type": "application/json",
943
+ }
944
+
945
+ grpc_host = os.environ.get("FAL_HOST", "api.alpha.fal.ai")
946
+ rest_host = grpc_host.replace("api", "rest", 1)
947
+ url = f"https://{rest_host}/storage/upload/initiate?storage_type=fal-cdn-v3"
948
+
949
+ request = Request(
950
+ url,
951
+ headers=headers,
952
+ method="POST",
953
+ data=json.dumps(
954
+ {
955
+ "file_name": file.file_name,
956
+ "content_type": file.content_type,
957
+ }
958
+ ).encode(),
959
+ )
960
+ try:
961
+ with urlopen(request) as response:
962
+ result = json.load(response)
963
+ file_url = result["file_url"]
964
+ upload_url = result["upload_url"]
965
+ except HTTPError as e:
966
+ raise FileUploadException(
967
+ f"Error initiating upload. Status {e.status}: {e.reason}"
968
+ )
969
+
970
+ request = Request(
971
+ upload_url,
972
+ headers={"Content-Type": file.content_type},
973
+ method="PUT",
974
+ data=file.data,
975
+ )
976
+ try:
977
+ with urlopen(request):
978
+ pass
979
+ except HTTPError as e:
980
+ raise FileUploadException(
981
+ f"Error uploading file. Status {e.status}: {e.reason}"
982
+ )
983
+
984
+ return file_url
985
+
986
+ def save_file(
987
+ self,
988
+ file_path: str | Path,
989
+ content_type: str,
990
+ multipart: bool | None = None,
991
+ multipart_threshold: int | None = None,
992
+ multipart_chunk_size: int | None = None,
993
+ multipart_max_concurrency: int | None = None,
994
+ object_lifecycle_preference: dict[str, str] | None = None,
995
+ ) -> tuple[str, FileData | None]:
996
+ if multipart is None:
997
+ threshold = multipart_threshold or MultipartUploadV3.MULTIPART_THRESHOLD
998
+ multipart = os.path.getsize(file_path) > threshold
999
+
1000
+ if multipart:
1001
+ url = MultipartUploadV3.save_file(
1002
+ file_path,
1003
+ chunk_size=multipart_chunk_size,
1004
+ content_type=content_type,
1005
+ max_concurrency=multipart_max_concurrency,
1006
+ )
1007
+ data = None
1008
+ else:
1009
+ with open(file_path, "rb") as f:
1010
+ data = FileData(
1011
+ f.read(),
1012
+ content_type=content_type,
1013
+ file_name=os.path.basename(file_path),
1014
+ )
1015
+ url = self.save(data, object_lifecycle_preference)
1016
+
1017
+ return url, data
1018
+
1019
+
706
1020
  # This is only available for internal users to have long-lived access tokens
707
1021
  @dataclass
708
1022
  class InternalFalFileRepositoryV3(FileRepository):
@@ -784,11 +1098,13 @@ class InternalFalFileRepositoryV3(FileRepository):
784
1098
  object_lifecycle_preference: dict[str, str] | None = None,
785
1099
  ) -> tuple[str, FileData | None]:
786
1100
  if multipart is None:
787
- threshold = multipart_threshold or MultipartUpload.MULTIPART_THRESHOLD
1101
+ threshold = (
1102
+ multipart_threshold or InternalMultipartUploadV3.MULTIPART_THRESHOLD
1103
+ )
788
1104
  multipart = os.path.getsize(file_path) > threshold
789
1105
 
790
1106
  if multipart:
791
- url = MultipartUpload.save_file(
1107
+ url = InternalMultipartUploadV3.save_file(
792
1108
  file_path,
793
1109
  chunk_size=multipart_chunk_size,
794
1110
  content_type=content_type,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: fal
3
- Version: 1.7.5
3
+ Version: 1.7.7
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
@@ -64,11 +64,13 @@ With fal, you can build pipelines, serve ML models and scale them up to many use
64
64
  ## Quickstart
65
65
 
66
66
  First, you need to install the `fal` package. You can do so using pip:
67
+
67
68
  ```shell
68
69
  pip install fal
69
70
  ```
70
71
 
71
72
  Then you need to authenticate:
73
+
72
74
  ```shell
73
75
  fal auth login
74
76
  ```
@@ -117,7 +119,7 @@ pytest
117
119
 
118
120
  ### Pre-commit
119
121
 
120
- ```
122
+ ```bash
121
123
  cd projects/fal
122
124
  pre-commit install
123
125
  ```
@@ -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=1PmSWdbIl6FG3GO33tTDihvotemU0-1S8uTOfaXKRIY,411
3
+ fal/_fal_version.py,sha256=ZBOMWZOUYZwPmwzK5AfGuLs4eexHW3xVZhNkn6LSpEQ,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=u9QfJtb1nLDJu9kegKCrdvW-Cp0mfMSGTPm5X1ywoeE,43388
7
- fal/app.py,sha256=C1dTWjit90XdTKmrwd5Aqv3SD0MA1JDZoLLtmStn2Xc,22917
7
+ fal/app.py,sha256=0cm7wZXdusZXyV9nJmlFJl-7Jgxir6954OaO9Lj2ITk,23178
8
8
  fal/apps.py,sha256=RpmElElJnDYjsTRQOdNYiJwd74GEOGYA38L5O5GzNEg,11068
9
9
  fal/config.py,sha256=hgI3kW4_2NoFsrYEiPss0mnDTr8_Td2z0pVgm93wi9o,600
10
10
  fal/container.py,sha256=EjokKTULJ3fPUjDttjir-jmg0gqcUDe0iVzW2j5njec,634
@@ -52,7 +52,7 @@ fal/toolkit/types.py,sha256=kkbOsDKj1qPGb1UARTBp7yuJ5JUuyy7XQurYUBCdti8,4064
52
52
  fal/toolkit/file/__init__.py,sha256=FbNl6wD-P0aSSTUwzHt4HujBXrbC3ABmaigPQA4hRfg,70
53
53
  fal/toolkit/file/file.py,sha256=-gccCKnarTu6Nfm_0yQ0sJM9aadB5tUNvKS1PTqxiFc,9071
54
54
  fal/toolkit/file/types.py,sha256=MjZ6xAhKPv4rowLo2Vcbho0sX7AQ3lm3KFyYDcw0dL4,1845
55
- fal/toolkit/file/providers/fal.py,sha256=X7vz0QQg4xFdglbHvOzjgL78dleFMeUzUh1xX68K-zQ,25831
55
+ fal/toolkit/file/providers/fal.py,sha256=7JWTFXvAbtqakCIlA5gfKI8qU1umlWgWhvU5cXqzGVQ,36050
56
56
  fal/toolkit/file/providers/gcp.py,sha256=iQtkoYUqbmKKpC5srVOYtrruZ3reGRm5lz4kM8bshgk,2247
57
57
  fal/toolkit/file/providers/r2.py,sha256=G2OHcCH2yWrVtXT4hWHEXUeEjFhbKO0koqHcd7hkczk,2871
58
58
  fal/toolkit/file/providers/s3.py,sha256=CfiA6rTBFfP-empp0cB9OW2c9F5iy0Z-kGwCs5HBICU,2524
@@ -130,8 +130,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
130
130
  openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
131
131
  openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
132
132
  openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
133
- fal-1.7.5.dist-info/METADATA,sha256=4sk9VPsm0DqhrFWOypGkBhAQMomJvP0jTXoxJ2_eENk,3996
134
- fal-1.7.5.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
135
- fal-1.7.5.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
136
- fal-1.7.5.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
137
- fal-1.7.5.dist-info/RECORD,,
133
+ fal-1.7.7.dist-info/METADATA,sha256=HWsZ9TnJjqWxDoTaa2QqWTHXEX19JTW-FEBg1U-Ri9U,4002
134
+ fal-1.7.7.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
135
+ fal-1.7.7.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
136
+ fal-1.7.7.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
137
+ fal-1.7.7.dist-info/RECORD,,
File without changes