fal 1.3.3__py3-none-any.whl → 1.7.3__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 +571 -83
- 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.3.dist-info}/METADATA +14 -9
- {fal-1.3.3.dist-info → fal-1.7.3.dist-info}/RECORD +31 -26
- {fal-1.3.3.dist-info → fal-1.7.3.dist-info}/WHEEL +1 -1
- {fal-1.3.3.dist-info → fal-1.7.3.dist-info}/entry_points.txt +0 -0
- {fal-1.3.3.dist-info → fal-1.7.3.dist-info}/top_level.txt +0 -0
|
@@ -6,8 +6,10 @@ import os
|
|
|
6
6
|
import posixpath
|
|
7
7
|
import uuid
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
+
from typing import Optional
|
|
9
10
|
|
|
10
11
|
from fal.toolkit.file.types import FileData, FileRepository
|
|
12
|
+
from fal.toolkit.utils.retry import retry
|
|
11
13
|
|
|
12
14
|
DEFAULT_URL_TIMEOUT = 60 * 15 # 15 minutes
|
|
13
15
|
|
|
@@ -50,7 +52,12 @@ class GoogleStorageRepository(FileRepository):
|
|
|
50
52
|
|
|
51
53
|
return self._bucket
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
@retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
|
|
56
|
+
def save(
|
|
57
|
+
self,
|
|
58
|
+
data: FileData,
|
|
59
|
+
object_lifecycle_preference: Optional[dict[str, str]] = None,
|
|
60
|
+
) -> str:
|
|
54
61
|
destination_path = posixpath.join(
|
|
55
62
|
self.folder,
|
|
56
63
|
f"{uuid.uuid4().hex}_{data.file_name}",
|
fal/toolkit/file/providers/r2.py
CHANGED
|
@@ -6,8 +6,10 @@ import posixpath
|
|
|
6
6
|
import uuid
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from io import BytesIO
|
|
9
|
+
from typing import Optional
|
|
9
10
|
|
|
10
11
|
from fal.toolkit.file.types import FileData, FileRepository
|
|
12
|
+
from fal.toolkit.utils.retry import retry
|
|
11
13
|
|
|
12
14
|
DEFAULT_URL_TIMEOUT = 60 * 15 # 15 minutes
|
|
13
15
|
|
|
@@ -67,7 +69,12 @@ class R2Repository(FileRepository):
|
|
|
67
69
|
|
|
68
70
|
return self._bucket
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
@retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
|
|
73
|
+
def save(
|
|
74
|
+
self,
|
|
75
|
+
data: FileData,
|
|
76
|
+
object_lifecycle_preference: Optional[dict[str, str]] = None,
|
|
77
|
+
) -> str:
|
|
71
78
|
destination_path = posixpath.join(
|
|
72
79
|
self.key,
|
|
73
80
|
f"{uuid.uuid4().hex}_{data.file_name}",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import posixpath
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from io import BytesIO
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from fal.toolkit.file.types import FileData, FileRepository
|
|
11
|
+
from fal.toolkit.utils.retry import retry
|
|
12
|
+
|
|
13
|
+
DEFAULT_URL_TIMEOUT = 60 * 15 # 15 minutes
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class S3Repository(FileRepository):
|
|
18
|
+
bucket_name: str = "fal_file_storage"
|
|
19
|
+
url_expiration: int = DEFAULT_URL_TIMEOUT
|
|
20
|
+
aws_access_key_id: str | None = None
|
|
21
|
+
aws_secret_access_key: str | None = None
|
|
22
|
+
|
|
23
|
+
_s3_client = None
|
|
24
|
+
|
|
25
|
+
def __post_init__(self):
|
|
26
|
+
try:
|
|
27
|
+
import boto3
|
|
28
|
+
from botocore.client import Config
|
|
29
|
+
except ImportError:
|
|
30
|
+
raise Exception("boto3 is not installed")
|
|
31
|
+
|
|
32
|
+
if self.aws_access_key_id is None:
|
|
33
|
+
self.aws_access_key_id = os.environ.get("AWS_ACCESS_KEY_ID")
|
|
34
|
+
if self.aws_access_key_id is None:
|
|
35
|
+
raise Exception("AWS_ACCESS_KEY_ID environment variable is not set")
|
|
36
|
+
|
|
37
|
+
if self.aws_secret_access_key is None:
|
|
38
|
+
self.aws_secret_access_key = os.environ.get("AWS_SECRET_ACCESS_KEY")
|
|
39
|
+
if self.aws_secret_access_key is None:
|
|
40
|
+
raise Exception("AWS_SECRET_ACCESS_KEY environment variable is not set")
|
|
41
|
+
|
|
42
|
+
self._s3_client = boto3.client(
|
|
43
|
+
"s3",
|
|
44
|
+
aws_access_key_id=self.aws_access_key_id,
|
|
45
|
+
aws_secret_access_key=self.aws_secret_access_key,
|
|
46
|
+
config=Config(signature_version="s3v4"),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def storage_client(self):
|
|
51
|
+
if self._s3_client is None:
|
|
52
|
+
raise Exception("S3 client is not initialized")
|
|
53
|
+
|
|
54
|
+
return self._s3_client
|
|
55
|
+
|
|
56
|
+
@retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
|
|
57
|
+
def save(
|
|
58
|
+
self,
|
|
59
|
+
data: FileData,
|
|
60
|
+
object_lifecycle_preference: Optional[dict[str, str]] = None,
|
|
61
|
+
key: Optional[str] = None,
|
|
62
|
+
) -> str:
|
|
63
|
+
destination_path = posixpath.join(
|
|
64
|
+
key or "",
|
|
65
|
+
f"{uuid.uuid4().hex}_{data.file_name}",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self.storage_client.upload_fileobj(
|
|
69
|
+
BytesIO(data.data),
|
|
70
|
+
self.bucket_name,
|
|
71
|
+
destination_path,
|
|
72
|
+
ExtraArgs={"ContentType": data.content_type},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
public_url = self.storage_client.generate_presigned_url(
|
|
76
|
+
ClientMethod="get_object",
|
|
77
|
+
Params={"Bucket": self.bucket_name, "Key": destination_path},
|
|
78
|
+
ExpiresIn=self.url_expiration,
|
|
79
|
+
)
|
|
80
|
+
return public_url
|
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[
|
|
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(
|
|
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
|
fal/toolkit/image/__init__.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import urllib.request
|
|
3
4
|
from functools import lru_cache
|
|
4
5
|
from typing import TYPE_CHECKING
|
|
5
|
-
from urllib.request import Request, urlopen
|
|
6
6
|
|
|
7
7
|
from .image import * # noqa: F403
|
|
8
8
|
|
|
@@ -62,8 +62,8 @@ def read_image_from_url(
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
try:
|
|
65
|
-
request = Request(url, headers=TEMP_HEADERS)
|
|
66
|
-
response = urlopen(request)
|
|
65
|
+
request = urllib.request.Request(url, headers=TEMP_HEADERS)
|
|
66
|
+
response = urllib.request.urlopen(request)
|
|
67
67
|
image_pil = Image.open(response)
|
|
68
68
|
except Exception:
|
|
69
69
|
import traceback
|
fal/toolkit/image/image.py
CHANGED
|
@@ -4,9 +4,10 @@ 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
|
-
from fal.toolkit.file.file import DEFAULT_REPOSITORY, File
|
|
10
|
+
from fal.toolkit.file.file import DEFAULT_REPOSITORY, FALLBACK_REPOSITORY, File
|
|
10
11
|
from fal.toolkit.file.types import FileRepository, RepositoryId
|
|
11
12
|
from fal.toolkit.utils.download_utils import _download_file_python
|
|
12
13
|
|
|
@@ -79,12 +80,18 @@ class Image(File):
|
|
|
79
80
|
size: ImageSize | None = None,
|
|
80
81
|
file_name: str | None = None,
|
|
81
82
|
repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
|
|
83
|
+
fallback_repository: Optional[
|
|
84
|
+
FileRepository | RepositoryId
|
|
85
|
+
] = FALLBACK_REPOSITORY,
|
|
86
|
+
request: Optional[Request] = None,
|
|
82
87
|
) -> Image:
|
|
83
88
|
obj = super().from_bytes(
|
|
84
89
|
data,
|
|
85
90
|
content_type=f"image/{format}",
|
|
86
91
|
file_name=file_name,
|
|
87
92
|
repository=repository,
|
|
93
|
+
fallback_repository=fallback_repository,
|
|
94
|
+
request=request,
|
|
88
95
|
)
|
|
89
96
|
obj.width = size.width if size else None
|
|
90
97
|
obj.height = size.height if size else None
|
|
@@ -97,6 +104,10 @@ class Image(File):
|
|
|
97
104
|
format: ImageFormat | None = None,
|
|
98
105
|
file_name: str | None = None,
|
|
99
106
|
repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
|
|
107
|
+
fallback_repository: Optional[
|
|
108
|
+
FileRepository | RepositoryId
|
|
109
|
+
] = FALLBACK_REPOSITORY,
|
|
110
|
+
request: Optional[Request] = None,
|
|
100
111
|
) -> Image:
|
|
101
112
|
size = ImageSize(width=pil_image.width, height=pil_image.height)
|
|
102
113
|
if format is None:
|
|
@@ -110,12 +121,24 @@ class Image(File):
|
|
|
110
121
|
# enough result quickly to utilize the underlying resources
|
|
111
122
|
# efficiently.
|
|
112
123
|
saving_options["compress_level"] = 1
|
|
124
|
+
elif format == "jpeg":
|
|
125
|
+
# JPEG quality is set to 95 by default, which is a good balance
|
|
126
|
+
# between file size and image quality.
|
|
127
|
+
saving_options["quality"] = 95
|
|
113
128
|
|
|
114
129
|
with io.BytesIO() as f:
|
|
115
130
|
pil_image.save(f, format=format, **saving_options)
|
|
116
131
|
raw_image = f.getvalue()
|
|
117
132
|
|
|
118
|
-
return cls.from_bytes(
|
|
133
|
+
return cls.from_bytes(
|
|
134
|
+
raw_image,
|
|
135
|
+
format,
|
|
136
|
+
size,
|
|
137
|
+
file_name,
|
|
138
|
+
repository,
|
|
139
|
+
fallback_repository=fallback_repository,
|
|
140
|
+
request=request,
|
|
141
|
+
)
|
|
119
142
|
|
|
120
143
|
def to_pil(self, mode: str = "RGB") -> PILImage.Image:
|
|
121
144
|
try:
|
fal/toolkit/types.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import tempfile
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, Generator, Union
|
|
6
|
+
|
|
7
|
+
import pydantic
|
|
8
|
+
from pydantic.utils import update_not_none
|
|
9
|
+
|
|
10
|
+
from fal.toolkit.image import read_image_from_url
|
|
11
|
+
from fal.toolkit.utils.download_utils import download_file
|
|
12
|
+
|
|
13
|
+
# https://github.com/pydantic/pydantic/pull/2573
|
|
14
|
+
if not hasattr(pydantic, "__version__") or pydantic.__version__.startswith("1."):
|
|
15
|
+
IS_PYDANTIC_V2 = False
|
|
16
|
+
else:
|
|
17
|
+
IS_PYDANTIC_V2 = True
|
|
18
|
+
|
|
19
|
+
MAX_DATA_URI_LENGTH = 10 * 1024 * 1024
|
|
20
|
+
MAX_HTTPS_URL_LENGTH = 2048
|
|
21
|
+
|
|
22
|
+
HTTP_URL_REGEX = (
|
|
23
|
+
r"^https:\/\/(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?::\d{1,5})?(?:\/[^\s]*)?$"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DownloadFileMixin:
|
|
28
|
+
@contextmanager
|
|
29
|
+
def as_temp_file(self) -> Generator[Path, None, None]:
|
|
30
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
31
|
+
yield download_file(str(self), temp_dir)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DownloadImageMixin:
|
|
35
|
+
def to_pil(self):
|
|
36
|
+
return read_image_from_url(str(self))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DataUri(DownloadFileMixin, str):
|
|
40
|
+
if IS_PYDANTIC_V2:
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def __get_pydantic_core_schema__(cls, source_type: Any, handler) -> Any:
|
|
44
|
+
return {
|
|
45
|
+
"type": "str",
|
|
46
|
+
"pattern": "^data:",
|
|
47
|
+
"max_length": MAX_DATA_URI_LENGTH,
|
|
48
|
+
"strip_whitespace": True,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
def __get_pydantic_json_schema__(cls, core_schema, handler) -> Dict[str, Any]:
|
|
52
|
+
json_schema = handler(core_schema)
|
|
53
|
+
json_schema.update(format="data-uri")
|
|
54
|
+
return json_schema
|
|
55
|
+
else:
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def __get_validators__(cls):
|
|
59
|
+
yield cls.validate
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def validate(cls, value: Any) -> "DataUri":
|
|
63
|
+
from pydantic.validators import str_validator
|
|
64
|
+
|
|
65
|
+
value = str_validator(value)
|
|
66
|
+
value = value.strip()
|
|
67
|
+
|
|
68
|
+
if not value.startswith("data:"):
|
|
69
|
+
raise ValueError("Data URI must start with 'data:'")
|
|
70
|
+
|
|
71
|
+
if len(value) > MAX_DATA_URI_LENGTH:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Data URI is too long. Max length is {MAX_DATA_URI_LENGTH} bytes."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return cls(value)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
|
|
80
|
+
update_not_none(field_schema, format="data-uri")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class HttpsUrl(DownloadFileMixin, str):
|
|
84
|
+
if IS_PYDANTIC_V2:
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def __get_pydantic_core_schema__(cls, source_type: Any, handler) -> Any:
|
|
88
|
+
return {
|
|
89
|
+
"type": "str",
|
|
90
|
+
"pattern": HTTP_URL_REGEX,
|
|
91
|
+
"max_length": MAX_HTTPS_URL_LENGTH,
|
|
92
|
+
"strip_whitespace": True,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def __get_pydantic_json_schema__(cls, core_schema, handler) -> Dict[str, Any]:
|
|
96
|
+
json_schema = handler(core_schema)
|
|
97
|
+
json_schema.update(format="https-url")
|
|
98
|
+
return json_schema
|
|
99
|
+
|
|
100
|
+
else:
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def __get_validators__(cls):
|
|
104
|
+
yield cls.validate
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def validate(cls, value: Any) -> "HttpsUrl":
|
|
108
|
+
from pydantic.validators import str_validator
|
|
109
|
+
|
|
110
|
+
value = str_validator(value)
|
|
111
|
+
value = value.strip()
|
|
112
|
+
|
|
113
|
+
if not re.match(HTTP_URL_REGEX, value):
|
|
114
|
+
raise ValueError(
|
|
115
|
+
"URL must start with 'https://' and follow the correct format."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if len(value) > MAX_HTTPS_URL_LENGTH:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"HTTPS URL is too long. Max length is "
|
|
121
|
+
f"{MAX_HTTPS_URL_LENGTH} characters."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return cls(value)
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
|
|
128
|
+
update_not_none(field_schema, format="https-url")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ImageHttpsUrl(DownloadImageMixin, HttpsUrl):
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class ImageDataUri(DownloadImageMixin, DataUri):
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
FileInput = Union[HttpsUrl, DataUri]
|
|
140
|
+
ImageInput = Union[ImageHttpsUrl, ImageDataUri]
|
|
@@ -84,6 +84,9 @@ def _get_remote_file_properties(
|
|
|
84
84
|
url_path = parsed_url.path
|
|
85
85
|
file_name = Path(url_path).name or _hash_url(url)
|
|
86
86
|
|
|
87
|
+
# file name can still contain a forward slash if the server returns a relative path
|
|
88
|
+
file_name = Path(file_name).name
|
|
89
|
+
|
|
87
90
|
return file_name, content_length
|
|
88
91
|
|
|
89
92
|
|
|
@@ -159,6 +162,7 @@ def download_file(
|
|
|
159
162
|
try:
|
|
160
163
|
file_name = _get_remote_file_properties(url, request_headers)[0]
|
|
161
164
|
except Exception as e:
|
|
165
|
+
print(f"GOt error: {e}")
|
|
162
166
|
raise DownloadError(f"Failed to get remote file properties for {url}") from e
|
|
163
167
|
|
|
164
168
|
if "/" in file_name:
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import random
|
|
3
|
+
import time
|
|
4
|
+
import traceback
|
|
5
|
+
from typing import Any, Callable, Literal
|
|
6
|
+
|
|
7
|
+
BackoffType = Literal["exponential", "fixed"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def retry(
|
|
11
|
+
max_retries: int = 3,
|
|
12
|
+
base_delay: float = 1.0,
|
|
13
|
+
max_delay: float = 60.0,
|
|
14
|
+
backoff_type: BackoffType = "exponential",
|
|
15
|
+
jitter: bool = False,
|
|
16
|
+
) -> Callable:
|
|
17
|
+
def decorator(func: Callable) -> Callable:
|
|
18
|
+
@functools.wraps(func)
|
|
19
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
20
|
+
retries = 0
|
|
21
|
+
while retries < max_retries:
|
|
22
|
+
try:
|
|
23
|
+
return func(*args, **kwargs)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
retries += 1
|
|
26
|
+
print(f"Retrying {retries} of {max_retries}...")
|
|
27
|
+
if retries == max_retries:
|
|
28
|
+
print(f"Max retries reached. Raising exception: {e}")
|
|
29
|
+
traceback.print_exc()
|
|
30
|
+
|
|
31
|
+
raise e
|
|
32
|
+
|
|
33
|
+
if backoff_type == "exponential":
|
|
34
|
+
delay = min(base_delay * (2 ** (retries - 1)), max_delay)
|
|
35
|
+
else: # fixed
|
|
36
|
+
delay = min(base_delay, max_delay)
|
|
37
|
+
|
|
38
|
+
if jitter:
|
|
39
|
+
delay *= random.uniform(0.5, 1.5)
|
|
40
|
+
|
|
41
|
+
time.sleep(delay)
|
|
42
|
+
|
|
43
|
+
return wrapper
|
|
44
|
+
|
|
45
|
+
return decorator
|
fal/workflows.py
CHANGED
|
@@ -5,7 +5,7 @@ import webbrowser
|
|
|
5
5
|
from argparse import ArgumentParser
|
|
6
6
|
from collections import Counter
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
|
-
from typing import Any, Iterator, Union, cast
|
|
8
|
+
from typing import Any, Dict, Iterator, List, Union, cast
|
|
9
9
|
|
|
10
10
|
import graphlib
|
|
11
11
|
import rich
|
|
@@ -21,8 +21,8 @@ from fal import flags
|
|
|
21
21
|
from fal.exceptions import FalServerlessException
|
|
22
22
|
from fal.rest_client import REST_CLIENT
|
|
23
23
|
|
|
24
|
-
JSONType = Union[
|
|
25
|
-
SchemaType =
|
|
24
|
+
JSONType = Union[Dict[str, Any], List[Any], str, int, float, bool, None, "Leaf"]
|
|
25
|
+
SchemaType = Dict[str, Any]
|
|
26
26
|
|
|
27
27
|
VARIABLE_PREFIX = "$"
|
|
28
28
|
INPUT_VARIABLE_NAME = "input"
|
|
@@ -50,7 +50,13 @@ def parse_leaf(raw_leaf: str) -> Leaf:
|
|
|
50
50
|
f"Invalid leaf: {raw_leaf} (must start with a reference)"
|
|
51
51
|
)
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
# remove the $ prefix
|
|
54
|
+
_reference = (
|
|
55
|
+
reference[len(VARIABLE_PREFIX) :]
|
|
56
|
+
if reference.startswith(VARIABLE_PREFIX)
|
|
57
|
+
else reference
|
|
58
|
+
)
|
|
59
|
+
leaf: Leaf = ReferenceLeaf(_reference)
|
|
54
60
|
for raw_part in raw_parts:
|
|
55
61
|
if raw_part.isdigit():
|
|
56
62
|
leaf = IndexLeaf(leaf, int(raw_part))
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: fal
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.3
|
|
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
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
|
-
Requires-Dist: isolate[build]<
|
|
9
|
-
Requires-Dist: isolate-proto
|
|
8
|
+
Requires-Dist: isolate[build]<0.16.0,>=0.15.0
|
|
9
|
+
Requires-Dist: isolate-proto<0.7.0,>=0.6.0
|
|
10
10
|
Requires-Dist: grpcio==1.64.0
|
|
11
11
|
Requires-Dist: dill==0.3.7
|
|
12
12
|
Requires-Dist: cloudpickle==3.0.0
|
|
@@ -19,16 +19,18 @@ Requires-Dist: grpc-interceptor<1,>=0.15.0
|
|
|
19
19
|
Requires-Dist: colorama<1,>=0.4.6
|
|
20
20
|
Requires-Dist: portalocker<3,>=2.7.0
|
|
21
21
|
Requires-Dist: rich<14,>=13.3.2
|
|
22
|
-
Requires-Dist:
|
|
22
|
+
Requires-Dist: rich_argparse
|
|
23
23
|
Requires-Dist: packaging>=21.3
|
|
24
24
|
Requires-Dist: pathspec<1,>=0.11.1
|
|
25
25
|
Requires-Dist: pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3
|
|
26
|
+
Requires-Dist: structlog>=22.0
|
|
26
27
|
Requires-Dist: fastapi<1,>=0.99.1
|
|
27
28
|
Requires-Dist: starlette-exporter>=0.21.0
|
|
28
29
|
Requires-Dist: httpx>=0.15.4
|
|
29
30
|
Requires-Dist: attrs>=21.3.0
|
|
30
31
|
Requires-Dist: python-dateutil<3,>=2.8.0
|
|
31
32
|
Requires-Dist: types-python-dateutil<3,>=2.8.0
|
|
33
|
+
Requires-Dist: importlib-metadata>=4.4; python_version < "3.10"
|
|
32
34
|
Requires-Dist: msgpack<2,>=1.0.7
|
|
33
35
|
Requires-Dist: websockets<13,>=12.0
|
|
34
36
|
Requires-Dist: pillow<11,>=10.2.0
|
|
@@ -36,16 +38,19 @@ Requires-Dist: pyjwt[crypto]<3,>=2.8.0
|
|
|
36
38
|
Requires-Dist: uvicorn<1,>=0.29.0
|
|
37
39
|
Requires-Dist: cookiecutter
|
|
38
40
|
Requires-Dist: tomli
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Requires-Dist:
|
|
42
|
-
Requires-Dist:
|
|
41
|
+
Provides-Extra: docs
|
|
42
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
43
|
+
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
|
44
|
+
Requires-Dist: sphinx-autodoc-typehints; extra == "docs"
|
|
43
45
|
Provides-Extra: test
|
|
44
46
|
Requires-Dist: pytest<8; extra == "test"
|
|
45
47
|
Requires-Dist: pytest-asyncio; extra == "test"
|
|
46
48
|
Requires-Dist: pytest-xdist; extra == "test"
|
|
47
49
|
Requires-Dist: flaky; extra == "test"
|
|
48
50
|
Requires-Dist: boto3; extra == "test"
|
|
51
|
+
Provides-Extra: dev
|
|
52
|
+
Requires-Dist: fal[docs,test]; extra == "dev"
|
|
53
|
+
Requires-Dist: openapi-python-client<1,>=0.14.1; extra == "dev"
|
|
49
54
|
|
|
50
55
|
[](https://pypi.org/project/fal)
|
|
51
56
|
[](https://github.com/fal-ai/fal/actions)
|
|
@@ -1,35 +1,37 @@
|
|
|
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=Lv0gR-NbC-8DxwfmwXEmOzSq6Hgx6MH4xF1fYh_opXo,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=
|
|
8
|
-
fal/apps.py,sha256=
|
|
9
|
-
fal/
|
|
6
|
+
fal/api.py,sha256=u9QfJtb1nLDJu9kegKCrdvW-Cp0mfMSGTPm5X1ywoeE,43388
|
|
7
|
+
fal/app.py,sha256=C1dTWjit90XdTKmrwd5Aqv3SD0MA1JDZoLLtmStn2Xc,22917
|
|
8
|
+
fal/apps.py,sha256=RpmElElJnDYjsTRQOdNYiJwd74GEOGYA38L5O5GzNEg,11068
|
|
9
|
+
fal/config.py,sha256=hgI3kW4_2NoFsrYEiPss0mnDTr8_Td2z0pVgm93wi9o,600
|
|
10
|
+
fal/container.py,sha256=EjokKTULJ3fPUjDttjir-jmg0gqcUDe0iVzW2j5njec,634
|
|
10
11
|
fal/files.py,sha256=QgfYfMKmNobMPufrAP_ga1FKcIAlSbw18Iar1-0qepo,2650
|
|
11
12
|
fal/flags.py,sha256=oWN_eidSUOcE9wdPK_77si3A1fpgOC0UEERPsvNLIMc,842
|
|
12
13
|
fal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
14
|
fal/rest_client.py,sha256=kGBGmuyHfX1lR910EoKCYPjsyU8MdXawT_cW2q8Sajc,568
|
|
14
|
-
fal/sdk.py,sha256=
|
|
15
|
+
fal/sdk.py,sha256=HjlToPJkG0Z5h_D0D2FK43i3JFKeO4r2IhCGx4B82Z8,22564
|
|
15
16
|
fal/sync.py,sha256=ZuIJA2-hTPNANG9B_NNJZUsO68EIdTH0dc9MzeVE2VU,4340
|
|
16
17
|
fal/utils.py,sha256=9q_QrQBlQN3nZYA1kEGRfhJWi4RjnO4H1uQswfaei9w,2146
|
|
17
|
-
fal/workflows.py,sha256=
|
|
18
|
-
fal/auth/__init__.py,sha256=
|
|
18
|
+
fal/workflows.py,sha256=Zl4f6Bs085hY40zmqScxDUyCu7zXkukDbW02iYOLTTI,14805
|
|
19
|
+
fal/auth/__init__.py,sha256=MXwS5zyY1SYJWEkc6s39et73Dkg3cDJg1ZwxRhXNj4c,4704
|
|
19
20
|
fal/auth/auth0.py,sha256=rSG1mgH-QGyKfzd7XyAaj1AYsWt-ho8Y_LZ-FUVWzh4,5421
|
|
20
21
|
fal/auth/local.py,sha256=sndkM6vKpeVny6NHTacVlTbiIFqaksOmw0Viqs_RN1U,1790
|
|
21
22
|
fal/cli/__init__.py,sha256=padK4o0BFqq61kxAA1qQ0jYr2SuhA2mf90B3AaRkmJA,37
|
|
22
|
-
fal/cli/_utils.py,sha256=
|
|
23
|
-
fal/cli/apps.py,sha256
|
|
23
|
+
fal/cli/_utils.py,sha256=45G0LEz2bW-69MUQKPdatVE_CBC2644gC-V0qdNEsco,1252
|
|
24
|
+
fal/cli/apps.py,sha256=Fo4iUpd6FGTUcIp22WcssE1CaEn_BLKzK_E4JPsXhVI,8179
|
|
24
25
|
fal/cli/auth.py,sha256=--MhfHGwxmtHbRkGioyn1prKn_U-pBzbz0G_QeZou-U,1352
|
|
25
26
|
fal/cli/create.py,sha256=a8WDq-nJLFTeoIXqpb5cr7GR7YR9ZZrQCawNm34KXXE,627
|
|
26
27
|
fal/cli/debug.py,sha256=u_urnyFzSlNnrq93zz_GXE9FX4VyVxDoamJJyrZpFI0,1312
|
|
27
|
-
fal/cli/deploy.py,sha256
|
|
28
|
+
fal/cli/deploy.py,sha256=-woTZObzntUenPFmWJwDaeCmBl3Vb7jqSkhPCIfk2SM,7581
|
|
28
29
|
fal/cli/doctor.py,sha256=U4ne9LX5gQwNblsYQ27XdO8AYDgbYjTO39EtxhwexRM,983
|
|
29
30
|
fal/cli/keys.py,sha256=trDpA3LJu9S27qE_K8Hr6fKLK4vwVzbxUHq8TFrV4pw,3157
|
|
30
|
-
fal/cli/main.py,sha256=
|
|
31
|
+
fal/cli/main.py,sha256=O0i9wdLPxcd1u4CvXit-ufkT_UnON-baTN6v9HaHPmw,2027
|
|
31
32
|
fal/cli/parser.py,sha256=edCqFWYAQSOhrxeEK9BtFRlTEUAlG2JUDjS_vhZ_nHE,2868
|
|
32
|
-
fal/cli/run.py,sha256=
|
|
33
|
+
fal/cli/run.py,sha256=nAC12Qss4Fg1XmV0qOS9RdGNLYcdoHeRgQMvbTN4P9I,1202
|
|
34
|
+
fal/cli/runners.py,sha256=5pXuKq7nSkf0VpnppNnvxwP8XDq0SWkc6mkfizDwWMQ,1046
|
|
33
35
|
fal/cli/secrets.py,sha256=740msFm7d41HruudlcfqUXlFl53N-WmChsQP9B9M9Po,2572
|
|
34
36
|
fal/console/__init__.py,sha256=ernZ4bzvvliQh5SmrEqQ7lA5eVcbw6Ra2jalKtA7dxg,132
|
|
35
37
|
fal/console/icons.py,sha256=De9MfFaSkO2Lqfne13n3PrYfTXJVIzYZVqYn5BWsdrA,108
|
|
@@ -46,14 +48,16 @@ fal/logging/user.py,sha256=0Xvb8n6tSb9l_V51VDzv6SOdYEFNouV_6nF_W9e7uNQ,642
|
|
|
46
48
|
fal/toolkit/__init__.py,sha256=sV95wiUzKoiDqF9vDgq4q-BLa2sD6IpuKSqp5kdTQNE,658
|
|
47
49
|
fal/toolkit/exceptions.py,sha256=elHZ7dHCJG5zlHGSBbz-ilkZe9QUvQMomJFi8Pt91LA,198
|
|
48
50
|
fal/toolkit/optimize.py,sha256=p75sovF0SmRP6zxzpIaaOmqlxvXB_xEz3XPNf59EF7w,1339
|
|
51
|
+
fal/toolkit/types.py,sha256=kkbOsDKj1qPGb1UARTBp7yuJ5JUuyy7XQurYUBCdti8,4064
|
|
49
52
|
fal/toolkit/file/__init__.py,sha256=FbNl6wD-P0aSSTUwzHt4HujBXrbC3ABmaigPQA4hRfg,70
|
|
50
|
-
fal/toolkit/file/file.py,sha256
|
|
51
|
-
fal/toolkit/file/types.py,sha256=
|
|
52
|
-
fal/toolkit/file/providers/fal.py,sha256=
|
|
53
|
-
fal/toolkit/file/providers/gcp.py,sha256=
|
|
54
|
-
fal/toolkit/file/providers/r2.py,sha256=
|
|
55
|
-
fal/toolkit/
|
|
56
|
-
fal/toolkit/image/
|
|
53
|
+
fal/toolkit/file/file.py,sha256=-gccCKnarTu6Nfm_0yQ0sJM9aadB5tUNvKS1PTqxiFc,9071
|
|
54
|
+
fal/toolkit/file/types.py,sha256=MjZ6xAhKPv4rowLo2Vcbho0sX7AQ3lm3KFyYDcw0dL4,1845
|
|
55
|
+
fal/toolkit/file/providers/fal.py,sha256=X7vz0QQg4xFdglbHvOzjgL78dleFMeUzUh1xX68K-zQ,25831
|
|
56
|
+
fal/toolkit/file/providers/gcp.py,sha256=iQtkoYUqbmKKpC5srVOYtrruZ3reGRm5lz4kM8bshgk,2247
|
|
57
|
+
fal/toolkit/file/providers/r2.py,sha256=G2OHcCH2yWrVtXT4hWHEXUeEjFhbKO0koqHcd7hkczk,2871
|
|
58
|
+
fal/toolkit/file/providers/s3.py,sha256=CfiA6rTBFfP-empp0cB9OW2c9F5iy0Z-kGwCs5HBICU,2524
|
|
59
|
+
fal/toolkit/image/__init__.py,sha256=m3OatPbBhcEOYyaTu_dgToxunUKoJu4bJVCWUoN7HX4,1838
|
|
60
|
+
fal/toolkit/image/image.py,sha256=ZSkozciP4XxaGnvrR_mP4utqE3_QhoPN0dau9FJ2Xco,5033
|
|
57
61
|
fal/toolkit/image/safety_checker.py,sha256=S7ow-HuoVxC6ixHWWcBrAUm2dIlgq3sTAIull6xIbAg,3105
|
|
58
62
|
fal/toolkit/image/nsfw_filter/__init__.py,sha256=0d9D51EhcnJg8cZLYJjgvQJDZT74CfQu6mpvinRYRpA,216
|
|
59
63
|
fal/toolkit/image/nsfw_filter/env.py,sha256=iAP2Q3vzIl--DD8nr8o3o0goAwhExN2v0feYE0nIQjs,212
|
|
@@ -61,7 +65,8 @@ fal/toolkit/image/nsfw_filter/inference.py,sha256=BhIPF_zxRLetThQYxDDF0sdx9VRwvu
|
|
|
61
65
|
fal/toolkit/image/nsfw_filter/model.py,sha256=63mu8D15z_IosoRUagRLGHy6VbLqFmrG-yZqnu2vVm4,457
|
|
62
66
|
fal/toolkit/image/nsfw_filter/requirements.txt,sha256=3Pmrd0Ny6QAeBqUNHCgffRyfaCARAPJcfSCX5cRYpbM,37
|
|
63
67
|
fal/toolkit/utils/__init__.py,sha256=CrmM9DyCz5-SmcTzRSm5RaLgxy3kf0ZsSEN9uhnX2Xo,97
|
|
64
|
-
fal/toolkit/utils/download_utils.py,sha256=
|
|
68
|
+
fal/toolkit/utils/download_utils.py,sha256=fFrKoSJPBSurrD636ncNHhJv-cS3zReIv6ltiU3tMZU,17823
|
|
69
|
+
fal/toolkit/utils/retry.py,sha256=mHcQvvNIpu-Hi29P1HXSZuyvolRd48dMaJToqzlG0NY,1353
|
|
65
70
|
openapi_fal_rest/__init__.py,sha256=ziculmF_i6trw63LzZGFX-6W3Lwq9mCR8_UpkpvpaHI,152
|
|
66
71
|
openapi_fal_rest/client.py,sha256=G6BpJg9j7-JsrAUGddYwkzeWRYickBjPdcVgXoPzxuE,2817
|
|
67
72
|
openapi_fal_rest/errors.py,sha256=8mXSxdfSGzxT82srdhYbR0fHfgenxJXaUtMkaGgb6iU,470
|
|
@@ -125,8 +130,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
|
|
|
125
130
|
openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
|
|
126
131
|
openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
|
|
127
132
|
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.
|
|
133
|
+
fal-1.7.3.dist-info/METADATA,sha256=eTRggSeYlEsdzY5D68R6NkJbD65PrD-nkIRMd2MPa5Q,3996
|
|
134
|
+
fal-1.7.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
135
|
+
fal-1.7.3.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
|
|
136
|
+
fal-1.7.3.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
|
|
137
|
+
fal-1.7.3.dist-info/RECORD,,
|