fal 0.12.1__py3-none-any.whl → 0.12.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/__init__.py +12 -3
- fal/_serialization.py +18 -0
- fal/api.py +140 -59
- fal/app.py +309 -86
- fal/apps.py +92 -8
- fal/auth/__init__.py +20 -1
- fal/auth/auth0.py +32 -22
- fal/cli.py +34 -52
- fal/env.py +0 -4
- fal/exceptions/handlers.py +3 -2
- fal/flags.py +5 -0
- fal/logging/__init__.py +0 -2
- fal/logging/trace.py +8 -1
- fal/logging/user.py +2 -1
- fal/rest_client.py +2 -2
- fal/sdk.py +46 -31
- fal/sync.py +3 -3
- fal/toolkit/__init__.py +18 -1
- fal/toolkit/file/file.py +98 -11
- fal/toolkit/file/providers/fal.py +43 -2
- fal/toolkit/file/types.py +1 -1
- fal/toolkit/image/image.py +26 -4
- fal/toolkit/optimize.py +50 -0
- fal/toolkit/utils/download_utils.py +59 -13
- {fal-0.12.1.dist-info → fal-0.12.3.dist-info}/METADATA +7 -7
- fal-0.12.3.dist-info/RECORD +66 -0
- openapi_fal_rest/models/__init__.py +2 -70
- openapi_fal_rest/models/customer_details.py +26 -0
- openapi_fal_rest/models/lock_reason.py +16 -0
- fal/logging/datadog.py +0 -77
- fal-0.12.1.dist-info/RECORD +0 -147
- openapi_fal_rest/api/admin/get_invoice_users.py +0 -142
- openapi_fal_rest/api/admin/get_usage_per_user.py +0 -199
- openapi_fal_rest/api/admin/handle_user_lock.py +0 -191
- openapi_fal_rest/api/admin/set_billing_type.py +0 -186
- openapi_fal_rest/api/applications/get_status_applications_app_user_id_app_alias_or_id_status_get.py +0 -179
- openapi_fal_rest/api/billing/delete_payment_method.py +0 -162
- openapi_fal_rest/api/billing/get_checkout_page.py +0 -198
- openapi_fal_rest/api/billing/get_setup_intent_key.py +0 -141
- openapi_fal_rest/api/billing/get_user_invoices.py +0 -152
- openapi_fal_rest/api/billing/get_user_payment_methods.py +0 -152
- openapi_fal_rest/api/billing/get_user_price.py +0 -186
- openapi_fal_rest/api/billing/get_user_spending.py +0 -192
- openapi_fal_rest/api/billing/handle_stripe_webhook.py +0 -173
- openapi_fal_rest/api/billing/upcoming_invoice.py +0 -143
- openapi_fal_rest/api/billing/update_customer_budget.py +0 -183
- openapi_fal_rest/api/files/delete.py +0 -162
- openapi_fal_rest/api/files/download.py +0 -162
- openapi_fal_rest/api/files/file_exists.py +0 -183
- openapi_fal_rest/api/files/list_directory.py +0 -173
- openapi_fal_rest/api/files/list_root.py +0 -152
- openapi_fal_rest/api/files/upload_from_url.py +0 -179
- openapi_fal_rest/api/health/__init__.py +0 -0
- openapi_fal_rest/api/health/check.py +0 -136
- openapi_fal_rest/api/keys/__init__.py +0 -0
- openapi_fal_rest/api/keys/create_key.py +0 -188
- openapi_fal_rest/api/keys/delete_key.py +0 -162
- openapi_fal_rest/api/keys/list_keys.py +0 -152
- openapi_fal_rest/api/logs/__init__.py +0 -0
- openapi_fal_rest/api/logs/list_since.py +0 -224
- openapi_fal_rest/api/requests/__init__.py +0 -0
- openapi_fal_rest/api/requests/requests.py +0 -247
- openapi_fal_rest/api/storage/__init__.py +0 -0
- openapi_fal_rest/api/storage/get_file_link.py +0 -200
- openapi_fal_rest/api/storage/initiate_upload.py +0 -172
- openapi_fal_rest/api/storage/upload_file.py +0 -172
- openapi_fal_rest/api/tokens/__init__.py +0 -0
- openapi_fal_rest/api/tokens/create_token.py +0 -166
- openapi_fal_rest/api/usage/__init__.py +0 -0
- openapi_fal_rest/api/usage/get_custom_usage_per_machine.py +0 -203
- openapi_fal_rest/api/usage/get_gateway_request_stats.py +0 -247
- openapi_fal_rest/api/usage/get_gateway_request_stats_by_time.py +0 -236
- openapi_fal_rest/api/usage/get_gateway_stats_for_yesterday.py +0 -152
- openapi_fal_rest/api/usage/get_shared_usage_per_app.py +0 -203
- openapi_fal_rest/api/usage/get_usage_records.py +0 -253
- openapi_fal_rest/api/usage/per_machine_usage.py +0 -218
- openapi_fal_rest/api/usage/per_machine_usage_details.py +0 -173
- openapi_fal_rest/api/users/__init__.py +0 -0
- openapi_fal_rest/api/users/handle_user_registration.py +0 -228
- openapi_fal_rest/models/billing_type.py +0 -9
- openapi_fal_rest/models/body_create_token.py +0 -68
- openapi_fal_rest/models/body_upload_file.py +0 -75
- openapi_fal_rest/models/file_spec.py +0 -110
- openapi_fal_rest/models/gateway_stats_by_time.py +0 -115
- openapi_fal_rest/models/gateway_usage_stats.py +0 -147
- openapi_fal_rest/models/get_gateway_request_stats_by_time_response_get_gateway_request_stats_by_time.py +0 -70
- openapi_fal_rest/models/grouped_usage_detail.py +0 -85
- openapi_fal_rest/models/handle_stripe_webhook_response_handle_stripe_webhook.py +0 -43
- openapi_fal_rest/models/initiate_upload_info.py +0 -64
- openapi_fal_rest/models/invoice.py +0 -129
- openapi_fal_rest/models/invoice_item.py +0 -85
- openapi_fal_rest/models/key_scope.py +0 -9
- openapi_fal_rest/models/log_entry.py +0 -104
- openapi_fal_rest/models/log_entry_labels.py +0 -43
- openapi_fal_rest/models/new_user_key.py +0 -64
- openapi_fal_rest/models/payment_method.py +0 -96
- openapi_fal_rest/models/per_app_usage_detail.py +0 -88
- openapi_fal_rest/models/persisted_usage_record.py +0 -118
- openapi_fal_rest/models/persisted_usage_record_meta.py +0 -43
- openapi_fal_rest/models/presigned_upload_url.py +0 -64
- openapi_fal_rest/models/request_io.py +0 -112
- openapi_fal_rest/models/request_io_json_input.py +0 -43
- openapi_fal_rest/models/request_io_json_output.py +0 -43
- openapi_fal_rest/models/run_type.py +0 -9
- openapi_fal_rest/models/stats_timeframe.py +0 -12
- openapi_fal_rest/models/status.py +0 -82
- openapi_fal_rest/models/status_health.py +0 -10
- openapi_fal_rest/models/uploaded_file_result.py +0 -64
- openapi_fal_rest/models/url_file_upload.py +0 -57
- openapi_fal_rest/models/usage_per_machine_type.py +0 -115
- openapi_fal_rest/models/usage_per_user.py +0 -71
- openapi_fal_rest/models/usage_run_detail.py +0 -73
- openapi_fal_rest/models/user_key_info.py +0 -84
- /openapi_fal_rest/api/admin/__init__.py → /fal/py.typed +0 -0
- {fal-0.12.1.dist-info → fal-0.12.3.dist-info}/WHEEL +0 -0
- {fal-0.12.1.dist-info → fal-0.12.3.dist-info}/entry_points.txt +0 -0
fal/toolkit/file/file.py
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from
|
|
4
|
+
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
from zipfile import ZipFile
|
|
5
8
|
|
|
6
|
-
from
|
|
9
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
10
|
+
from pydantic.typing import Optional
|
|
11
|
+
|
|
12
|
+
from fal.toolkit.file.providers.fal import (
|
|
13
|
+
FalCDNFileRepository,
|
|
14
|
+
FalFileRepository,
|
|
15
|
+
InMemoryRepository,
|
|
16
|
+
)
|
|
7
17
|
from fal.toolkit.file.providers.gcp import GoogleStorageRepository
|
|
8
18
|
from fal.toolkit.file.providers.r2 import R2Repository
|
|
9
19
|
from fal.toolkit.file.types import FileData, FileRepository, RepositoryId
|
|
10
20
|
from fal.toolkit.mainify import mainify
|
|
11
|
-
from
|
|
21
|
+
from fal.toolkit.utils.download_utils import download_file
|
|
12
22
|
|
|
13
23
|
FileRepositoryFactory = Callable[[], FileRepository]
|
|
14
24
|
|
|
@@ -17,6 +27,7 @@ BUILT_IN_REPOSITORIES: dict[RepositoryId, FileRepositoryFactory] = {
|
|
|
17
27
|
"in_memory": lambda: InMemoryRepository(),
|
|
18
28
|
"gcp_storage": lambda: GoogleStorageRepository(),
|
|
19
29
|
"r2": lambda: R2Repository(),
|
|
30
|
+
"cdn": lambda: FalCDNFileRepository(),
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
|
|
@@ -37,23 +48,22 @@ class File(BaseModel):
|
|
|
37
48
|
_file_data: FileData = PrivateAttr()
|
|
38
49
|
url: str = Field(
|
|
39
50
|
description="The URL where the file can be downloaded from.",
|
|
40
|
-
examples=["https://url.to/generated/file/z9RV14K95DvU.png"],
|
|
41
51
|
)
|
|
42
|
-
content_type: str = Field(
|
|
52
|
+
content_type: Optional[str] = Field(
|
|
43
53
|
description="The mime type of the file.",
|
|
44
54
|
examples=["image/png"],
|
|
45
55
|
)
|
|
46
|
-
file_name: str = Field(
|
|
56
|
+
file_name: Optional[str] = Field(
|
|
47
57
|
description="The name of the file. It will be auto-generated if not provided.",
|
|
48
58
|
examples=["z9RV14K95DvU.png"],
|
|
49
59
|
)
|
|
50
|
-
file_size: int = Field(
|
|
60
|
+
file_size: Optional[int] = Field(
|
|
51
61
|
description="The size of the file in bytes.", examples=[4404019]
|
|
52
62
|
)
|
|
53
63
|
|
|
54
64
|
def __init__(self, **kwargs):
|
|
55
65
|
if "file_data" in kwargs:
|
|
56
|
-
data = kwargs.pop("file_data")
|
|
66
|
+
data: FileData = kwargs.pop("file_data")
|
|
57
67
|
repository = kwargs.pop("repository", None)
|
|
58
68
|
|
|
59
69
|
repo = (
|
|
@@ -74,12 +84,39 @@ class File(BaseModel):
|
|
|
74
84
|
|
|
75
85
|
super().__init__(**kwargs)
|
|
76
86
|
|
|
87
|
+
# Pydantic custom validator for input type conversion
|
|
88
|
+
@classmethod
|
|
89
|
+
def __get_validators__(cls):
|
|
90
|
+
yield cls.__convert_from_str
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def __convert_from_str(cls, value: Any):
|
|
94
|
+
if isinstance(value, str):
|
|
95
|
+
parsed_url = urlparse(value)
|
|
96
|
+
if parsed_url.scheme not in ["http", "https", "data"]:
|
|
97
|
+
raise ValueError(f"value must be a valid URL")
|
|
98
|
+
return cls._from_url(parsed_url.geturl())
|
|
99
|
+
|
|
100
|
+
return value
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def _from_url(
|
|
104
|
+
cls,
|
|
105
|
+
url: str,
|
|
106
|
+
) -> File:
|
|
107
|
+
return cls(
|
|
108
|
+
url=url,
|
|
109
|
+
content_type=None,
|
|
110
|
+
file_name=None,
|
|
111
|
+
repository=DEFAULT_REPOSITORY,
|
|
112
|
+
)
|
|
113
|
+
|
|
77
114
|
@classmethod
|
|
78
115
|
def from_bytes(
|
|
79
116
|
cls,
|
|
80
117
|
data: bytes,
|
|
81
|
-
content_type: str
|
|
82
|
-
file_name: str
|
|
118
|
+
content_type: Optional[str] = None,
|
|
119
|
+
file_name: Optional[str] = None,
|
|
83
120
|
repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
|
|
84
121
|
) -> File:
|
|
85
122
|
return cls(
|
|
@@ -91,7 +128,7 @@ class File(BaseModel):
|
|
|
91
128
|
def from_path(
|
|
92
129
|
cls,
|
|
93
130
|
path: str | Path,
|
|
94
|
-
content_type: str
|
|
131
|
+
content_type: Optional[str] = None,
|
|
95
132
|
repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
|
|
96
133
|
) -> File:
|
|
97
134
|
file_path = Path(path)
|
|
@@ -104,4 +141,54 @@ class File(BaseModel):
|
|
|
104
141
|
)
|
|
105
142
|
|
|
106
143
|
def as_bytes(self) -> bytes:
|
|
144
|
+
if getattr(self, "_file_data", None) is None:
|
|
145
|
+
raise ValueError("File has not been downloaded")
|
|
146
|
+
|
|
107
147
|
return self._file_data.data
|
|
148
|
+
|
|
149
|
+
def save(self, path: str | Path, overwrite: bool = False) -> Path:
|
|
150
|
+
file_path = Path(path).resolve()
|
|
151
|
+
|
|
152
|
+
if file_path.exists() and not overwrite:
|
|
153
|
+
raise FileExistsError(f"File {file_path} already exists")
|
|
154
|
+
|
|
155
|
+
downloaded_path = download_file(self.url, target_dir=file_path.parent)
|
|
156
|
+
downloaded_path.rename(file_path)
|
|
157
|
+
|
|
158
|
+
return file_path
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@mainify
|
|
162
|
+
class CompressedFile(File):
|
|
163
|
+
_extract_dir: Optional[TemporaryDirectory] = PrivateAttr(default=None)
|
|
164
|
+
|
|
165
|
+
def __init__(self, **kwargs):
|
|
166
|
+
super().__init__(**kwargs)
|
|
167
|
+
self._extract_dir = None
|
|
168
|
+
|
|
169
|
+
def __iter__(self):
|
|
170
|
+
if not self._extract_dir:
|
|
171
|
+
self._extract_files()
|
|
172
|
+
|
|
173
|
+
files = Path(self._extract_dir.name).iterdir() # type: ignore
|
|
174
|
+
return iter(files)
|
|
175
|
+
|
|
176
|
+
def _extract_files(self):
|
|
177
|
+
self._extract_dir = TemporaryDirectory()
|
|
178
|
+
|
|
179
|
+
with NamedTemporaryFile() as temp_file:
|
|
180
|
+
file_path = temp_file.name
|
|
181
|
+
self.save(file_path, overwrite=True)
|
|
182
|
+
|
|
183
|
+
with ZipFile(file_path) as zip_file:
|
|
184
|
+
zip_file.extractall(self._extract_dir.name)
|
|
185
|
+
|
|
186
|
+
def glob(self, pattern: str):
|
|
187
|
+
if not self._extract_dir:
|
|
188
|
+
self._extract_files()
|
|
189
|
+
|
|
190
|
+
return Path(self._extract_dir.name).glob(pattern) # type: ignore
|
|
191
|
+
|
|
192
|
+
def __del__(self):
|
|
193
|
+
if self._extract_dir:
|
|
194
|
+
self._extract_dir.cleanup()
|
|
@@ -7,18 +7,23 @@ from dataclasses import dataclass
|
|
|
7
7
|
from urllib.error import HTTPError
|
|
8
8
|
from urllib.request import Request, urlopen
|
|
9
9
|
|
|
10
|
+
from fal.auth import key_credentials
|
|
10
11
|
from fal.toolkit.exceptions import FileUploadException
|
|
11
12
|
from fal.toolkit.file.types import FileData, FileRepository
|
|
12
13
|
from fal.toolkit.mainify import mainify
|
|
13
14
|
|
|
15
|
+
_FAL_CDN = "https://fal-cdn.batuhan-941.workers.dev"
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
@mainify
|
|
16
19
|
@dataclass
|
|
17
20
|
class FalFileRepository(FileRepository):
|
|
18
21
|
def save(self, file: FileData) -> str:
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
key_creds = key_credentials()
|
|
23
|
+
if not key_creds:
|
|
24
|
+
raise FileUploadException("FAL_KEY must be set")
|
|
21
25
|
|
|
26
|
+
key_id, key_secret = key_creds
|
|
22
27
|
headers = {
|
|
23
28
|
"Authorization": f"Key {key_id}:{key_secret}",
|
|
24
29
|
"Accept": "application/json",
|
|
@@ -70,3 +75,39 @@ class FalFileRepository(FileRepository):
|
|
|
70
75
|
class InMemoryRepository(FileRepository):
|
|
71
76
|
def save(self, file: FileData) -> str:
|
|
72
77
|
return f'data:{file.content_type};base64,{b64encode(file.data).decode("utf-8")}'
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@mainify
|
|
81
|
+
@dataclass
|
|
82
|
+
class FalCDNFileRepository(FileRepository):
|
|
83
|
+
def save(self, file: FileData) -> str:
|
|
84
|
+
headers = {
|
|
85
|
+
**self.auth_headers,
|
|
86
|
+
"Accept": "application/json",
|
|
87
|
+
"Content-Type": file.content_type,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
url = os.getenv("FAL_CDN_HOST", _FAL_CDN) + "/files/upload"
|
|
91
|
+
request = Request(url, headers=headers, method="POST", data=file.data)
|
|
92
|
+
try:
|
|
93
|
+
with urlopen(request) as response:
|
|
94
|
+
result = json.load(response)
|
|
95
|
+
except HTTPError as e:
|
|
96
|
+
raise FileUploadException(
|
|
97
|
+
f"Error initiating upload. Status {e.status}: {e.reason}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
access_url = result["access_url"]
|
|
101
|
+
return access_url
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def auth_headers(self) -> dict[str, str]:
|
|
105
|
+
key_creds = key_credentials()
|
|
106
|
+
if not key_creds:
|
|
107
|
+
raise FileUploadException("FAL_KEY must be set")
|
|
108
|
+
|
|
109
|
+
key_id, key_secret = key_creds
|
|
110
|
+
return {
|
|
111
|
+
"Authorization": f"Bearer {key_id}:{key_secret}",
|
|
112
|
+
"User-Agent": "fal/0.1.0",
|
|
113
|
+
}
|
fal/toolkit/file/types.py
CHANGED
fal/toolkit/image/image.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
|
+
from tempfile import NamedTemporaryFile
|
|
4
5
|
from typing import TYPE_CHECKING, Literal, Optional, Union
|
|
5
6
|
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
6
9
|
from fal.toolkit.file.file import DEFAULT_REPOSITORY, File
|
|
7
10
|
from fal.toolkit.file.types import FileData, FileRepository, RepositoryId
|
|
8
11
|
from fal.toolkit.mainify import mainify
|
|
9
|
-
from
|
|
12
|
+
from fal.toolkit.utils.download_utils import _download_file_python
|
|
10
13
|
|
|
11
14
|
if TYPE_CHECKING:
|
|
12
15
|
from PIL import Image as PILImage
|
|
@@ -43,7 +46,7 @@ IMAGE_SIZE_PRESETS: dict[ImageSizePreset, ImageSize] = {
|
|
|
43
46
|
|
|
44
47
|
ImageSizeInput = Union[ImageSize, ImageSizePreset]
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
@mainify
|
|
47
50
|
def get_image_size(source: ImageSizeInput) -> ImageSize:
|
|
48
51
|
if isinstance(source, ImageSize):
|
|
49
52
|
return source
|
|
@@ -53,8 +56,6 @@ def get_image_size(source: ImageSizeInput) -> ImageSize:
|
|
|
53
56
|
raise TypeError(f"Invalid value for ImageSize: {source}")
|
|
54
57
|
|
|
55
58
|
|
|
56
|
-
get_image_size.__module__ = "__main__"
|
|
57
|
-
|
|
58
59
|
ImageFormat = Literal["png", "jpeg", "jpg", "webp", "gif"]
|
|
59
60
|
|
|
60
61
|
|
|
@@ -117,3 +118,24 @@ class Image(File):
|
|
|
117
118
|
raw_image = f.getvalue()
|
|
118
119
|
|
|
119
120
|
return cls.from_bytes(raw_image, format, size, file_name, repository)
|
|
121
|
+
|
|
122
|
+
def to_pil(self, mode: str = "RGB") -> PILImage.Image:
|
|
123
|
+
try:
|
|
124
|
+
from PIL import Image as PILImage
|
|
125
|
+
from PIL import ImageOps
|
|
126
|
+
except ImportError:
|
|
127
|
+
raise ImportError(
|
|
128
|
+
"The PIL package is required to use Image.to_pil()."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Stream the image data from url to a temp file and convert it to a PIL image
|
|
132
|
+
with NamedTemporaryFile() as temp_file:
|
|
133
|
+
temp_file_path = temp_file.name
|
|
134
|
+
|
|
135
|
+
_download_file_python(self.url, temp_file_path)
|
|
136
|
+
|
|
137
|
+
img = PILImage.open(temp_file_path).convert(mode)
|
|
138
|
+
img = ImageOps.exif_transpose(img)
|
|
139
|
+
|
|
140
|
+
return img
|
|
141
|
+
|
fal/toolkit/optimize.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import traceback
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from fal.toolkit.mainify import mainify
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
import torch
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@mainify
|
|
14
|
+
def optimize(
|
|
15
|
+
module: torch.nn.Module, *, optimization_config: dict[str, Any] | None = None
|
|
16
|
+
) -> torch.nn.Module:
|
|
17
|
+
"""Optimize the given torch module with dynamic compilation and
|
|
18
|
+
quantization techniques. Only applicable under fal's cloud environment.
|
|
19
|
+
|
|
20
|
+
Warning: This function is experimental and may not work as expected.
|
|
21
|
+
"""
|
|
22
|
+
import runpy
|
|
23
|
+
|
|
24
|
+
import torch.nn as nn
|
|
25
|
+
|
|
26
|
+
if not isinstance(module, nn.Module):
|
|
27
|
+
raise TypeError(f"Expected a torch.nn.Module, got {type(module)}.")
|
|
28
|
+
|
|
29
|
+
optimizer_path = os.environ.get("FAL_SPATIAL_OPTIMIZER", None)
|
|
30
|
+
if optimizer_path is None:
|
|
31
|
+
print(
|
|
32
|
+
"[WARNING] FAL_SPATIAL_OPTIMIZER is not set, falling back"
|
|
33
|
+
"to default torch execution"
|
|
34
|
+
)
|
|
35
|
+
return module
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
spatial_optimizer = runpy.run_path(optimizer_path, run_name="spatial_optimizer")
|
|
39
|
+
|
|
40
|
+
return spatial_optimizer["optimize"](
|
|
41
|
+
module,
|
|
42
|
+
optimization_config=optimization_config,
|
|
43
|
+
)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print(
|
|
46
|
+
"[WARNING] Failed to optimize module, falling back "
|
|
47
|
+
"to default torch execution."
|
|
48
|
+
)
|
|
49
|
+
traceback.print_exc()
|
|
50
|
+
return module
|
|
@@ -3,15 +3,16 @@ from __future__ import annotations
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import shutil
|
|
5
5
|
import subprocess
|
|
6
|
+
import sys
|
|
6
7
|
from functools import lru_cache
|
|
7
|
-
from pathlib import Path
|
|
8
|
+
from pathlib import Path, PurePath
|
|
8
9
|
from tempfile import TemporaryDirectory
|
|
9
10
|
from urllib.parse import urlparse
|
|
10
11
|
from urllib.request import Request, urlopen
|
|
11
12
|
|
|
12
13
|
from fal.toolkit.mainify import mainify
|
|
13
14
|
|
|
14
|
-
FAL_PERSISTENT_DIR =
|
|
15
|
+
FAL_PERSISTENT_DIR = PurePath("/data")
|
|
15
16
|
FAL_REPOSITORY_DIR = FAL_PERSISTENT_DIR / ".fal" / "repos"
|
|
16
17
|
FAL_MODEL_WEIGHTS_DIR = FAL_PERSISTENT_DIR / ".fal" / "model_weights"
|
|
17
18
|
|
|
@@ -71,8 +72,13 @@ def _get_remote_file_properties(url: str) -> tuple[str, int]:
|
|
|
71
72
|
content_length = int(response.headers.get("Content-Length", -1))
|
|
72
73
|
|
|
73
74
|
if not file_name:
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
parsed_url = urlparse(url)
|
|
76
|
+
|
|
77
|
+
if parsed_url.scheme == "data":
|
|
78
|
+
file_name = _hash_url(url)
|
|
79
|
+
else:
|
|
80
|
+
url_path = parsed_url.path
|
|
81
|
+
file_name = Path(url_path).name or _hash_url(url)
|
|
76
82
|
|
|
77
83
|
return file_name, content_length
|
|
78
84
|
|
|
@@ -148,7 +154,7 @@ def download_file(
|
|
|
148
154
|
|
|
149
155
|
# If target_dir is not an absolute path, use "/data" as the relative directory
|
|
150
156
|
if not target_dir_path.is_absolute():
|
|
151
|
-
target_dir_path = FAL_PERSISTENT_DIR / target_dir_path
|
|
157
|
+
target_dir_path = FAL_PERSISTENT_DIR / target_dir_path # type: ignore[assignment]
|
|
152
158
|
|
|
153
159
|
target_path = target_dir_path.resolve() / file_name
|
|
154
160
|
|
|
@@ -180,7 +186,7 @@ def download_file(
|
|
|
180
186
|
|
|
181
187
|
|
|
182
188
|
@mainify
|
|
183
|
-
def _download_file_python(url: str, target_path: Path) -> Path:
|
|
189
|
+
def _download_file_python(url: str, target_path: Path | str) -> Path:
|
|
184
190
|
"""Download a file from a given URL and save it to a specified path using a
|
|
185
191
|
Python interface.
|
|
186
192
|
|
|
@@ -215,7 +221,7 @@ def _download_file_python(url: str, target_path: Path) -> Path:
|
|
|
215
221
|
finally:
|
|
216
222
|
Path(temp_file.name).unlink(missing_ok=True)
|
|
217
223
|
|
|
218
|
-
return target_path
|
|
224
|
+
return Path(target_path)
|
|
219
225
|
|
|
220
226
|
|
|
221
227
|
@mainify
|
|
@@ -290,7 +296,7 @@ def download_model_weights(url: str, force: bool = False):
|
|
|
290
296
|
A Path object representing the full path to the downloaded model weights.
|
|
291
297
|
"""
|
|
292
298
|
# This is not a protected path, so the user may change stuff internally
|
|
293
|
-
weights_dir = FAL_MODEL_WEIGHTS_DIR / _hash_url(url)
|
|
299
|
+
weights_dir = Path(FAL_MODEL_WEIGHTS_DIR / _hash_url(url))
|
|
294
300
|
|
|
295
301
|
if weights_dir.exists() and not force:
|
|
296
302
|
try:
|
|
@@ -315,6 +321,7 @@ def clone_repository(
|
|
|
315
321
|
target_dir: str | Path | None = None,
|
|
316
322
|
repo_name: str | None = None,
|
|
317
323
|
force: bool = False,
|
|
324
|
+
include_to_path: bool = False,
|
|
318
325
|
) -> Path:
|
|
319
326
|
"""Clones a Git repository from the specified HTTPS URL into a local
|
|
320
327
|
directory.
|
|
@@ -334,20 +341,34 @@ def clone_repository(
|
|
|
334
341
|
If not provided, the repository's name from the URL is used.
|
|
335
342
|
force: If `True`, the repository is cloned even if it already exists locally
|
|
336
343
|
and its commit hash matches the provided commit hash. Defaults to `False`.
|
|
344
|
+
include_to_path: If `True`, the cloned repository is added to the `sys.path`.
|
|
345
|
+
Defaults to `False`.
|
|
337
346
|
|
|
338
347
|
Returns:
|
|
339
348
|
A Path object representing the full path to the cloned Git repository.
|
|
340
349
|
"""
|
|
341
|
-
target_dir = target_dir or FAL_REPOSITORY_DIR
|
|
350
|
+
target_dir = target_dir or FAL_REPOSITORY_DIR # type: ignore[assignment]
|
|
342
351
|
repo_name = repo_name or Path(https_url).stem
|
|
343
352
|
|
|
344
|
-
local_repo_path = Path(target_dir) / repo_name
|
|
353
|
+
local_repo_path = Path(target_dir) / repo_name # type: ignore[arg-type]
|
|
345
354
|
|
|
346
355
|
if local_repo_path.exists():
|
|
347
356
|
local_repo_commit_hash = _get_git_revision_hash(local_repo_path)
|
|
348
357
|
if local_repo_commit_hash == commit_hash and not force:
|
|
358
|
+
if include_to_path:
|
|
359
|
+
__add_local_path_to_sys_path(local_repo_path)
|
|
349
360
|
return local_repo_path
|
|
350
361
|
else:
|
|
362
|
+
if local_repo_commit_hash != commit_hash:
|
|
363
|
+
print(
|
|
364
|
+
f"Local repository '{local_repo_path}' has a different commit hash "
|
|
365
|
+
f"({local_repo_commit_hash}) than the one provided ({commit_hash})."
|
|
366
|
+
)
|
|
367
|
+
elif force:
|
|
368
|
+
print(
|
|
369
|
+
f"Local repository '{local_repo_path}' already exists. "
|
|
370
|
+
f"Forcing re-download."
|
|
371
|
+
)
|
|
351
372
|
print(f"Removing the existing repository: {local_repo_path} ")
|
|
352
373
|
shutil.rmtree(local_repo_path)
|
|
353
374
|
|
|
@@ -362,6 +383,7 @@ def clone_repository(
|
|
|
362
383
|
clone_command = [
|
|
363
384
|
"git",
|
|
364
385
|
"clone",
|
|
386
|
+
"--recursive",
|
|
365
387
|
https_url,
|
|
366
388
|
temp_dir_path,
|
|
367
389
|
]
|
|
@@ -380,13 +402,37 @@ def clone_repository(
|
|
|
380
402
|
|
|
381
403
|
raise error
|
|
382
404
|
|
|
405
|
+
if include_to_path:
|
|
406
|
+
__add_local_path_to_sys_path(local_repo_path)
|
|
407
|
+
|
|
383
408
|
return local_repo_path
|
|
384
409
|
|
|
385
410
|
|
|
411
|
+
@mainify
|
|
412
|
+
def __add_local_path_to_sys_path(local_path: Path | str):
|
|
413
|
+
local_path_str = str(local_path)
|
|
414
|
+
|
|
415
|
+
if local_path_str not in sys.path:
|
|
416
|
+
sys.path.insert(0, local_path_str)
|
|
417
|
+
|
|
418
|
+
|
|
386
419
|
@mainify
|
|
387
420
|
def _get_git_revision_hash(repo_path: Path) -> str:
|
|
388
421
|
import subprocess
|
|
389
422
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
423
|
+
try:
|
|
424
|
+
return subprocess.check_output(
|
|
425
|
+
["git", "rev-parse", "HEAD"],
|
|
426
|
+
cwd=repo_path,
|
|
427
|
+
text=True,
|
|
428
|
+
stderr=subprocess.STDOUT,
|
|
429
|
+
).strip()
|
|
430
|
+
except subprocess.CalledProcessError as error:
|
|
431
|
+
if "not a git repository" in error.output:
|
|
432
|
+
print(f"Repository '{repo_path}' is not a git repository.")
|
|
433
|
+
return ""
|
|
434
|
+
|
|
435
|
+
print(
|
|
436
|
+
f"{error}\nFailed to get the commit hash of the repository '{repo_path}' ."
|
|
437
|
+
)
|
|
438
|
+
raise error
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fal
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.3
|
|
4
4
|
Summary: fal is an easy-to-use Serverless Python Framework
|
|
5
5
|
Author: Features & Labels
|
|
6
6
|
Author-email: hello@fal.ai
|
|
@@ -11,31 +11,31 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Requires-Dist: attrs (>=21.3.0)
|
|
14
|
-
Requires-Dist: auth0-python (>=4.1.0,<5.0.0)
|
|
15
|
-
Requires-Dist: boto3 (>=1.33.8,<2.0.0)
|
|
16
14
|
Requires-Dist: click (>=8.1.3,<9.0.0)
|
|
17
15
|
Requires-Dist: colorama (>=0.4.6,<0.5.0)
|
|
18
|
-
Requires-Dist: datadog-api-client (==2.12.0)
|
|
19
16
|
Requires-Dist: dill (==0.3.7)
|
|
20
17
|
Requires-Dist: fastapi (==0.99.1)
|
|
21
18
|
Requires-Dist: grpc-interceptor (>=0.15.0,<0.16.0)
|
|
22
19
|
Requires-Dist: grpcio (>=1.50.0,<2.0.0)
|
|
23
|
-
Requires-Dist: httpx (>=0.15.4
|
|
20
|
+
Requires-Dist: httpx (>=0.15.4)
|
|
24
21
|
Requires-Dist: importlib-metadata (>=4.4) ; python_version < "3.10"
|
|
25
|
-
Requires-Dist: isolate-proto (
|
|
22
|
+
Requires-Dist: isolate-proto (==0.3.3)
|
|
26
23
|
Requires-Dist: isolate[build] (>=0.12.3,<1.0)
|
|
24
|
+
Requires-Dist: msgpack (>=1.0.7,<2.0.0)
|
|
27
25
|
Requires-Dist: opentelemetry-api (>=1.15.0,<2.0.0)
|
|
28
26
|
Requires-Dist: opentelemetry-sdk (>=1.15.0,<2.0.0)
|
|
29
27
|
Requires-Dist: packaging (>=21.3)
|
|
30
28
|
Requires-Dist: pathspec (>=0.11.1,<0.12.0)
|
|
29
|
+
Requires-Dist: pillow (>=10.2.0,<11.0.0)
|
|
31
30
|
Requires-Dist: portalocker (>=2.7.0,<3.0.0)
|
|
32
31
|
Requires-Dist: pydantic (<2.0)
|
|
32
|
+
Requires-Dist: pyjwt (>=2.8.0,<3.0.0)
|
|
33
33
|
Requires-Dist: python-dateutil (>=2.8.0,<3.0.0)
|
|
34
|
-
Requires-Dist: requests (>=2.28.1,<3.0.0)
|
|
35
34
|
Requires-Dist: rich (>=13.3.2,<14.0.0)
|
|
36
35
|
Requires-Dist: structlog (>=22.3.0,<23.0.0)
|
|
37
36
|
Requires-Dist: types-python-dateutil (>=2.8.0,<3.0.0)
|
|
38
37
|
Requires-Dist: typing-extensions (>=4.7.1,<5.0.0)
|
|
38
|
+
Requires-Dist: websockets (>=12.0,<13.0)
|
|
39
39
|
Description-Content-Type: text/markdown
|
|
40
40
|
|
|
41
41
|
# fal
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
openapi_fal_rest/__init__.py,sha256=sqsyB55QptrijXTCVFQfIJ6uC__vXez1i5KNvYplk5w,151
|
|
2
|
+
openapi_fal_rest/api/__init__.py,sha256=87ApBzKyGb5zsgTMOkQXDqsLZCmaSFoJMwbGzCDQZMw,47
|
|
3
|
+
openapi_fal_rest/api/applications/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
openapi_fal_rest/api/applications/app_metadata.py,sha256=GqG6Q7jt8Jcyhb3ms_6i0M1B3cy205y3_A8W-AGEapY,5120
|
|
5
|
+
openapi_fal_rest/api/billing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
openapi_fal_rest/api/billing/get_user_details.py,sha256=2HQHRUQj8QwqSKgiV_USBdXCxGlfaVTBbLiPaDsMBUM,4013
|
|
7
|
+
openapi_fal_rest/api/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
openapi_fal_rest/api/files/check_dir_hash.py,sha256=zPNlOwG4YVvnhgfrleQtYLhI1lG0t8YQ1CU3TyvXvfk,4747
|
|
9
|
+
openapi_fal_rest/api/files/upload_local_file.py,sha256=p2lM7hswGbs8KNLg1Pp6vwV7x-1PKtWX-aYmaHUHSDU,5649
|
|
10
|
+
openapi_fal_rest/client.py,sha256=G6BpJg9j7-JsrAUGddYwkzeWRYickBjPdcVgXoPzxuE,2817
|
|
11
|
+
openapi_fal_rest/errors.py,sha256=8mXSxdfSGzxT82srdhYbR0fHfgenxJXaUtMkaGgb6iU,470
|
|
12
|
+
openapi_fal_rest/models/__init__.py,sha256=u2YVZnZwu9YqDPasBnh9GLVIkEJj4PFCThf97Pblx4o,601
|
|
13
|
+
openapi_fal_rest/models/app_metadata_response_app_metadata.py,sha256=swJMfWvbjlMF8dmv-KEqcR9If0UjsRogwj9UqBBlkpc,1251
|
|
14
|
+
openapi_fal_rest/models/body_upload_local_file.py,sha256=rOTEbYBXfwZk8TsywZWSPPQQEfJgvsLIufT6A40RJZs,1980
|
|
15
|
+
openapi_fal_rest/models/customer_details.py,sha256=XQBaO-A5DI54nCau5ZIR0jhCAmsBJKrtDgApuu6PFrU,3912
|
|
16
|
+
openapi_fal_rest/models/hash_check.py,sha256=T9R7n4EdadCxbFUZvresZZFPYwDfyJMZVNxY6wIJEE8,1352
|
|
17
|
+
openapi_fal_rest/models/http_validation_error.py,sha256=2nhqlv8RX2qp6VR7hb8-SKtzJWXSZ0J95ThW9J4agJo,2131
|
|
18
|
+
openapi_fal_rest/models/lock_reason.py,sha256=3b_foCV6bZKvsbyic3hM1_qzvJk_9ZD_5mS1GzSawdw,703
|
|
19
|
+
openapi_fal_rest/models/validation_error.py,sha256=I6tB-HbEOmE0ua27erDX5PX5YUynENv_dgPN3SrwTrQ,2091
|
|
20
|
+
openapi_fal_rest/py.typed,sha256=8ZJUsxZiuOy1oJeVhsTWQhTG_6pTVHVXk5hJL79ebTk,25
|
|
21
|
+
openapi_fal_rest/types.py,sha256=4xaUIOliefW-5jz_p-JT2LO7-V0wKWaniHGtjPBQfvQ,993
|
|
22
|
+
fal/__init__.py,sha256=6SvCuotCb0tuqSWDZSFDjtySktJ5m1QpVIlefumJpvM,1199
|
|
23
|
+
fal/_serialization.py,sha256=l_dZuSX5BT7SogXw1CalYLfT2H3zy3tfq4y6jHuxZqQ,4201
|
|
24
|
+
fal/api.py,sha256=Qack_oYNkvF4qown3P_oKvyvRfTJkhOG7PL1xpa8FUQ,32872
|
|
25
|
+
fal/app.py,sha256=KAIgvBBpvzp6oY8BpH5hFOLDUpG4bjtwlV5jPGj2IE0,12487
|
|
26
|
+
fal/apps.py,sha256=T387WJDtKpKEytu27b2AVqqo0uijKrRT9ymk6FcRiEw,6705
|
|
27
|
+
fal/auth/__init__.py,sha256=4W_9svpsmohRPhBi4yjx9rAPaUeBTHaJvSRpdRzXA5s,3133
|
|
28
|
+
fal/auth/auth0.py,sha256=hQ3ZTqqsgpL62GsNB9KvjE8k_2hxXMIJb5TNpRmaiYs,5485
|
|
29
|
+
fal/auth/local.py,sha256=lZqp4j32l2xFpY8zYvLoIHHyJrNAJDcm5MxgsLpY_pw,1786
|
|
30
|
+
fal/cli.py,sha256=nLk4LJsGvLicA_iW0T1ldYb_igMwYOdC2fQxUsdWCRQ,17236
|
|
31
|
+
fal/console/__init__.py,sha256=ernZ4bzvvliQh5SmrEqQ7lA5eVcbw6Ra2jalKtA7dxg,132
|
|
32
|
+
fal/console/icons.py,sha256=De9MfFaSkO2Lqfne13n3PrYfTXJVIzYZVqYn5BWsdrA,108
|
|
33
|
+
fal/console/ux.py,sha256=4vj1aGA3grRn-ebeMuDLR6u3YjMwUGpqtNgdTG9su5s,485
|
|
34
|
+
fal/env.py,sha256=-fA8x62BbOX3MOuO0maupa-_QJ9PNwr8ogfeG11QUyQ,53
|
|
35
|
+
fal/exceptions/__init__.py,sha256=Q4LCSqIrJ8GFQZWH5BvWL5mDPR0HwYQuIhNvsdiOkEU,938
|
|
36
|
+
fal/exceptions/_base.py,sha256=LeQmx-soL_-s1742WKN18VwTVjUuYP0L0BdQHPJBpM4,460
|
|
37
|
+
fal/exceptions/auth.py,sha256=01Ro7SyGJpwchubdHe14Cl6-Al1jUj16Sy4BvakNWf4,384
|
|
38
|
+
fal/exceptions/handlers.py,sha256=b21a8S13euECArjpgm2N69HsShqLYVqAboIeMoWlWA4,1414
|
|
39
|
+
fal/flags.py,sha256=8OaKkJg_-UvtyRbZf-rW5ZTW3B1xQpzzXnLRNFB7grA,889
|
|
40
|
+
fal/logging/__init__.py,sha256=snqprf7-sKw6oAATS_Yxklf-a3XhLg0vIHICPwLp6TM,1583
|
|
41
|
+
fal/logging/isolate.py,sha256=yDW_P4aR-t53IRmvD2Iprufv1Wn-xQXoBbMB2Ufr59s,2122
|
|
42
|
+
fal/logging/style.py,sha256=ckIgHzvF4DShM5kQh8F133X53z_vF46snuDHVmo_h9g,386
|
|
43
|
+
fal/logging/trace.py,sha256=OhzB6d4rQZimBc18WFLqH_9BGfqFFumKKTAGSsmWRMg,1904
|
|
44
|
+
fal/logging/user.py,sha256=A8vbZX9z13TPZEDzvlbvCDDdD0EL1KrCP3qHdrT58-A,632
|
|
45
|
+
fal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
|
+
fal/rest_client.py,sha256=kGBGmuyHfX1lR910EoKCYPjsyU8MdXawT_cW2q8Sajc,568
|
|
47
|
+
fal/sdk.py,sha256=Z3MQsD8MMQZq_GEC2VjaYChdNafFJtsgdk77-VK6N44,18782
|
|
48
|
+
fal/sync.py,sha256=Ljet584PVFz4r888-0bwV1Kio-tTneF_85TnHvBPvJw,4277
|
|
49
|
+
fal/toolkit/__init__.py,sha256=JDNBT_duflp93geeAzw2kFmGzG5odWnPJEXFLXE2nF4,713
|
|
50
|
+
fal/toolkit/exceptions.py,sha256=--WKKYxUop6WFy_vqAPXK6uH8C-JR98gnNXwhHNCb7E,258
|
|
51
|
+
fal/toolkit/file/__init__.py,sha256=YpUU6YziZV1AMuq12L0EDWToS0sgpHSGWsARbiOEHWk,56
|
|
52
|
+
fal/toolkit/file/file.py,sha256=ku4agJiGXU2gdfZmFrU5mDlVsag834zoeskbo-6ErEU,5926
|
|
53
|
+
fal/toolkit/file/providers/fal.py,sha256=hO59loXzGP4Vg-Q1FFR56nWbbI6BccJRnFsEI6z6EQE,3404
|
|
54
|
+
fal/toolkit/file/providers/gcp.py,sha256=Bq5SJSghXF8YfFnbZ83_mPdrWs2dFhi8ytODp92USgk,1962
|
|
55
|
+
fal/toolkit/file/providers/r2.py,sha256=xJtZfX3cfzJgLXS3F8mHArbrHi0_QBpIMy5M4-tS8H8,2586
|
|
56
|
+
fal/toolkit/file/types.py,sha256=MTIj6Y_ioL4CiMZXMiqx74vlmUifc3SNvcrWAXQfULE,1109
|
|
57
|
+
fal/toolkit/image/__init__.py,sha256=liEq0CqkRqUQ1udnnyGVFBwCXUhR2f6o5ffbtbAlP8o,57
|
|
58
|
+
fal/toolkit/image/image.py,sha256=bF1PzO4cJoFGJFpQYeG0sNaGuw3cC1zmobmbZrxbPFY,4339
|
|
59
|
+
fal/toolkit/mainify.py,sha256=E7gE45nZQZoaJdSlIq0mqajcH-IjcuPBWFmKm5hvhAU,406
|
|
60
|
+
fal/toolkit/optimize.py,sha256=OIhX0T-efRMgUJDpvL0bujdun5SovZgTdKxNOv01b_Y,1394
|
|
61
|
+
fal/toolkit/utils/__init__.py,sha256=b3zVpm50Upx1saXU7RiV9r9in6-Chs4OU9KRjBv7MYI,83
|
|
62
|
+
fal/toolkit/utils/download_utils.py,sha256=bigcLJjLK1OBAGxpYisJ0-5vcQCh0HAPuCykPrcCNd0,15596
|
|
63
|
+
fal-0.12.3.dist-info/METADATA,sha256=0eR9dtKw9ZU7y2Dxjx9NtXp--hmw7XG24LuTylD5BlE,2930
|
|
64
|
+
fal-0.12.3.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
|
|
65
|
+
fal-0.12.3.dist-info/entry_points.txt,sha256=nE9GBVV3PdBosudFwbIzZQUe_9lfPR6EH8K_FdDASnM,62
|
|
66
|
+
fal-0.12.3.dist-info/RECORD,,
|