fal 1.26.0__py3-none-any.whl → 1.26.1__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 +1 -1
- fal/app.py +50 -13
- fal/cli/apps.py +1 -1
- fal/files.py +96 -6
- fal/sdk.py +17 -11
- {fal-1.26.0.dist-info → fal-1.26.1.dist-info}/METADATA +1 -1
- {fal-1.26.0.dist-info → fal-1.26.1.dist-info}/RECORD +11 -11
- {fal-1.26.0.dist-info → fal-1.26.1.dist-info}/WHEEL +0 -0
- {fal-1.26.0.dist-info → fal-1.26.1.dist-info}/entry_points.txt +0 -0
- {fal-1.26.0.dist-info → fal-1.26.1.dist-info}/top_level.txt +0 -0
fal/_fal_version.py
CHANGED
fal/api.py
CHANGED
|
@@ -500,7 +500,7 @@ class FalServerlessHost(Host):
|
|
|
500
500
|
partial_func,
|
|
501
501
|
environments,
|
|
502
502
|
application_name=application_name,
|
|
503
|
-
|
|
503
|
+
auth_mode=application_auth_mode,
|
|
504
504
|
machine_requirements=machine_requirements,
|
|
505
505
|
metadata=metadata,
|
|
506
506
|
deployment_strategy=deployment_strategy,
|
fal/app.py
CHANGED
|
@@ -197,7 +197,14 @@ class AppClient:
|
|
|
197
197
|
|
|
198
198
|
@classmethod
|
|
199
199
|
@contextmanager
|
|
200
|
-
def connect(
|
|
200
|
+
def connect(
|
|
201
|
+
cls,
|
|
202
|
+
app_cls,
|
|
203
|
+
*,
|
|
204
|
+
health_request_timeout: int = 30,
|
|
205
|
+
startup_timeout: int = 60,
|
|
206
|
+
health_check_interval: float = 0.5,
|
|
207
|
+
):
|
|
201
208
|
app = wrap_app(app_cls)
|
|
202
209
|
info = app.spawn()
|
|
203
210
|
_shutdown_event = threading.Event()
|
|
@@ -214,20 +221,50 @@ class AppClient:
|
|
|
214
221
|
_log_printer.start()
|
|
215
222
|
|
|
216
223
|
try:
|
|
224
|
+
if info.url is None:
|
|
225
|
+
raise AppClientError(
|
|
226
|
+
"App spawn failed: no URL returned",
|
|
227
|
+
status_code=500,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
start_time = time.perf_counter()
|
|
231
|
+
url = info.url + "/health"
|
|
232
|
+
last_error = None
|
|
233
|
+
attempt = 0
|
|
234
|
+
|
|
217
235
|
with httpx.Client() as client:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
raise AppClientError(
|
|
227
|
-
f"Failed to GET {url}: {resp.status_code} {resp.text}",
|
|
228
|
-
status_code=resp.status_code,
|
|
236
|
+
while time.perf_counter() - start_time < startup_timeout:
|
|
237
|
+
attempt += 1
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
resp = client.get(url, timeout=health_request_timeout)
|
|
241
|
+
except httpx.TimeoutException:
|
|
242
|
+
last_error = (
|
|
243
|
+
f"Request timed out after {health_request_timeout} seconds"
|
|
229
244
|
)
|
|
230
|
-
|
|
245
|
+
except httpx.TransportError as e:
|
|
246
|
+
last_error = f"Network error: {e}"
|
|
247
|
+
else:
|
|
248
|
+
if resp.is_success:
|
|
249
|
+
break
|
|
250
|
+
|
|
251
|
+
if resp.status_code in (500, 404):
|
|
252
|
+
last_error = f"Server not ready (HTTP {resp.status_code})"
|
|
253
|
+
else:
|
|
254
|
+
raise AppClientError(
|
|
255
|
+
"Health check failed with non-retryable error: "
|
|
256
|
+
f"{resp.status_code} {resp.text}",
|
|
257
|
+
status_code=resp.status_code,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
time.sleep(health_check_interval)
|
|
261
|
+
else:
|
|
262
|
+
# retry loop completed without success
|
|
263
|
+
raise AppClientError(
|
|
264
|
+
f"Health check failed after {startup_timeout}s "
|
|
265
|
+
f"({attempt} attempts). Last error: {last_error}",
|
|
266
|
+
status_code=500,
|
|
267
|
+
)
|
|
231
268
|
|
|
232
269
|
client = cls(app_cls, info.url)
|
|
233
270
|
yield client
|
fal/cli/apps.py
CHANGED
fal/files.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
import math
|
|
1
3
|
import os
|
|
2
4
|
import posixpath
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
6
|
from functools import cached_property
|
|
4
7
|
from typing import TYPE_CHECKING
|
|
5
8
|
|
|
@@ -9,6 +12,19 @@ if TYPE_CHECKING:
|
|
|
9
12
|
import httpx
|
|
10
13
|
|
|
11
14
|
USER_AGENT = "fal-sdk/1.14.0 (python)"
|
|
15
|
+
MULTIPART_THRESHOLD = 10 * 1024 * 1024 # 10MB
|
|
16
|
+
MULTIPART_CHUNK_SIZE = 10 * 1024 * 1024 # 10MB
|
|
17
|
+
MULTIPART_WORKERS = 2 # only 2 because our REST is currently struggling with more
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _compute_md5(lpath, chunk_size=8192):
|
|
21
|
+
import hashlib
|
|
22
|
+
|
|
23
|
+
hasher = hashlib.md5()
|
|
24
|
+
with open(lpath, "rb") as fobj:
|
|
25
|
+
for chunk in iter(lambda: fobj.read(chunk_size), b""):
|
|
26
|
+
hasher.update(chunk)
|
|
27
|
+
return hasher.hexdigest()
|
|
12
28
|
|
|
13
29
|
|
|
14
30
|
class FalFileSystem(AbstractFileSystem):
|
|
@@ -95,16 +111,90 @@ class FalFileSystem(AbstractFileSystem):
|
|
|
95
111
|
response = self._request("GET", f"/files/file/{rpath}")
|
|
96
112
|
fobj.write(response.content)
|
|
97
113
|
|
|
114
|
+
def _put_file_part(self, rpath, lpath, upload_id, part_number, chunk_size):
|
|
115
|
+
offset = (part_number - 1) * chunk_size
|
|
116
|
+
with open(lpath, "rb") as fobj:
|
|
117
|
+
fobj.seek(offset)
|
|
118
|
+
chunk = fobj.read(chunk_size)
|
|
119
|
+
response = self._request(
|
|
120
|
+
"PUT",
|
|
121
|
+
f"/files/file/multipart/{rpath}/{upload_id}/{part_number}",
|
|
122
|
+
files={"file_upload": (posixpath.basename(lpath), chunk)},
|
|
123
|
+
)
|
|
124
|
+
data = response.json()
|
|
125
|
+
return {
|
|
126
|
+
"part_number": data["part_number"],
|
|
127
|
+
"etag": data["etag"],
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
def _put_file_multipart(self, lpath, rpath, size, progress):
|
|
131
|
+
response = self._request(
|
|
132
|
+
"POST",
|
|
133
|
+
f"/files/file/multipart/{rpath}/initiate",
|
|
134
|
+
)
|
|
135
|
+
upload_id = response.json()["upload_id"]
|
|
136
|
+
|
|
137
|
+
parts = []
|
|
138
|
+
num_parts = math.ceil(size / MULTIPART_CHUNK_SIZE)
|
|
139
|
+
md5 = _compute_md5(lpath)
|
|
140
|
+
|
|
141
|
+
task = progress.add_task(f"{os.path.basename(lpath)}", total=num_parts)
|
|
142
|
+
|
|
143
|
+
with ThreadPoolExecutor(max_workers=MULTIPART_WORKERS) as executor:
|
|
144
|
+
futures = []
|
|
145
|
+
|
|
146
|
+
for part_number in range(1, num_parts + 1):
|
|
147
|
+
futures.append(
|
|
148
|
+
executor.submit(
|
|
149
|
+
self._put_file_part,
|
|
150
|
+
rpath,
|
|
151
|
+
lpath,
|
|
152
|
+
upload_id,
|
|
153
|
+
part_number,
|
|
154
|
+
MULTIPART_CHUNK_SIZE,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
for future in concurrent.futures.as_completed(futures):
|
|
159
|
+
parts.append(future.result())
|
|
160
|
+
progress.advance(task)
|
|
161
|
+
|
|
162
|
+
response = self._request(
|
|
163
|
+
"POST",
|
|
164
|
+
f"/files/file/multipart/{rpath}/{upload_id}/complete",
|
|
165
|
+
json={"parts": parts},
|
|
166
|
+
)
|
|
167
|
+
data = response.json()
|
|
168
|
+
if data["etag"] != md5:
|
|
169
|
+
raise RuntimeError(
|
|
170
|
+
f"MD5 mismatch on {rpath}: {data['etag']} != {md5}, "
|
|
171
|
+
"please contact support"
|
|
172
|
+
)
|
|
173
|
+
|
|
98
174
|
def put_file(self, lpath, rpath, mode="overwrite", **kwargs):
|
|
175
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
|
|
176
|
+
|
|
99
177
|
if os.path.isdir(lpath):
|
|
100
178
|
return
|
|
101
179
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
180
|
+
size = os.path.getsize(lpath)
|
|
181
|
+
with Progress(
|
|
182
|
+
SpinnerColumn(),
|
|
183
|
+
TextColumn("[progress.description]{task.description}"),
|
|
184
|
+
BarColumn(),
|
|
185
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
186
|
+
) as progress:
|
|
187
|
+
if size > MULTIPART_THRESHOLD:
|
|
188
|
+
self._put_file_multipart(lpath, rpath, size, progress)
|
|
189
|
+
else:
|
|
190
|
+
task = progress.add_task(f"{os.path.basename(lpath)}", total=1)
|
|
191
|
+
with open(lpath, "rb") as fobj:
|
|
192
|
+
self._request(
|
|
193
|
+
"POST",
|
|
194
|
+
f"/files/file/local/{rpath}",
|
|
195
|
+
files={"file_upload": (posixpath.basename(lpath), fobj)},
|
|
196
|
+
)
|
|
197
|
+
progress.advance(task)
|
|
108
198
|
self.dircache.clear()
|
|
109
199
|
|
|
110
200
|
def put_file_from_url(self, url, rpath, mode="overwrite", **kwargs):
|
fal/sdk.py
CHANGED
|
@@ -45,6 +45,9 @@ logger = get_logger(__name__)
|
|
|
45
45
|
patch_pickle()
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
AuthMode = Optional[Literal["public", "private", "shared"]]
|
|
49
|
+
|
|
50
|
+
|
|
48
51
|
class ServerCredentials:
|
|
49
52
|
def to_grpc(self) -> grpc.ChannelCredentials:
|
|
50
53
|
raise NotImplementedError
|
|
@@ -565,7 +568,7 @@ class FalServerlessConnection:
|
|
|
565
568
|
function: Callable[..., ResultT],
|
|
566
569
|
environments: list[isolate_proto.EnvironmentDefinition],
|
|
567
570
|
application_name: str | None = None,
|
|
568
|
-
|
|
571
|
+
auth_mode: AuthMode = None,
|
|
569
572
|
*,
|
|
570
573
|
serialization_method: str = _DEFAULT_SERIALIZATION_METHOD,
|
|
571
574
|
machine_requirements: MachineRequirements | None = None,
|
|
@@ -598,13 +601,14 @@ class FalServerlessConnection:
|
|
|
598
601
|
else:
|
|
599
602
|
wrapped_requirements = None
|
|
600
603
|
|
|
601
|
-
auth_mode
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
604
|
+
if auth_mode == "public":
|
|
605
|
+
auth = isolate_proto.ApplicationAuthMode.PUBLIC
|
|
606
|
+
elif auth_mode == "shared":
|
|
607
|
+
auth = isolate_proto.ApplicationAuthMode.SHARED
|
|
608
|
+
elif auth_mode == "private":
|
|
609
|
+
auth = isolate_proto.ApplicationAuthMode.PRIVATE
|
|
610
|
+
else:
|
|
611
|
+
auth = None
|
|
608
612
|
|
|
609
613
|
struct_metadata = None
|
|
610
614
|
if metadata:
|
|
@@ -620,7 +624,7 @@ class FalServerlessConnection:
|
|
|
620
624
|
environments=environments,
|
|
621
625
|
machine_requirements=wrapped_requirements,
|
|
622
626
|
application_name=application_name,
|
|
623
|
-
auth_mode=
|
|
627
|
+
auth_mode=auth,
|
|
624
628
|
metadata=struct_metadata,
|
|
625
629
|
deployment_strategy=deployment_strategy_proto,
|
|
626
630
|
scale=scale,
|
|
@@ -730,14 +734,16 @@ class FalServerlessConnection:
|
|
|
730
734
|
self,
|
|
731
735
|
alias: str,
|
|
732
736
|
revision: str,
|
|
733
|
-
auth_mode:
|
|
737
|
+
auth_mode: AuthMode,
|
|
734
738
|
):
|
|
735
739
|
if auth_mode == "public":
|
|
736
740
|
auth = isolate_proto.ApplicationAuthMode.PUBLIC
|
|
737
741
|
elif auth_mode == "shared":
|
|
738
742
|
auth = isolate_proto.ApplicationAuthMode.SHARED
|
|
739
|
-
|
|
743
|
+
elif auth_mode == "private":
|
|
740
744
|
auth = isolate_proto.ApplicationAuthMode.PRIVATE
|
|
745
|
+
else:
|
|
746
|
+
auth = None
|
|
741
747
|
|
|
742
748
|
request = isolate_proto.SetAliasRequest(
|
|
743
749
|
alias=alias,
|
|
@@ -1,19 +1,19 @@
|
|
|
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=n2TqZzcgqBKPmTHRD9QCk0lFkU1l5_qG0eOl7E-Tdjs,513
|
|
4
4
|
fal/_serialization.py,sha256=npXNsFJ5G7jzBeBIyVMH01Ww34mGY4XWhHpRbSrTtnQ,7598
|
|
5
5
|
fal/_version.py,sha256=1BbTFnucNC_6ldKJ_ZoC722_UkW4S9aDBSW9L0fkKAw,2315
|
|
6
|
-
fal/api.py,sha256=
|
|
7
|
-
fal/app.py,sha256=
|
|
6
|
+
fal/api.py,sha256=wIEt21P1C7U-dYQEcyHUxxuuTnvzFyTpWDoHoaxq7tg,47385
|
|
7
|
+
fal/app.py,sha256=3RDFV6JUiUk8b3WJanKQYA-kW74t5bqaNy8OYuUH5-Q,25491
|
|
8
8
|
fal/apps.py,sha256=pzCd2mrKl5J_4oVc40_pggvPtFahXBCdrZXWpnaEJVs,12130
|
|
9
9
|
fal/config.py,sha256=19tR4zWr2yqsBRWNhFV1jKMiXAASRz2Et62k68-1r9Y,3290
|
|
10
10
|
fal/container.py,sha256=FTsa5hOW4ars-yV1lUoc0BNeIIvAZcpw7Ftyt3A4m_w,2000
|
|
11
|
-
fal/files.py,sha256=
|
|
11
|
+
fal/files.py,sha256=iwR9jXWmAruP5tSS5-NkY-mZG53Pb80C40SqB0pYRrk,6819
|
|
12
12
|
fal/flags.py,sha256=QonyDM7R2GqfAB1bJr46oriu-fHJCkpUwXuSdanePWg,987
|
|
13
13
|
fal/project.py,sha256=QgfYfMKmNobMPufrAP_ga1FKcIAlSbw18Iar1-0qepo,2650
|
|
14
14
|
fal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
fal/rest_client.py,sha256=kGBGmuyHfX1lR910EoKCYPjsyU8MdXawT_cW2q8Sajc,568
|
|
16
|
-
fal/sdk.py,sha256=
|
|
16
|
+
fal/sdk.py,sha256=eX_OJVmD6AY5NsLS7VR-LemXTWNEMfsOEfZBgAqytoo,26158
|
|
17
17
|
fal/sync.py,sha256=ZuIJA2-hTPNANG9B_NNJZUsO68EIdTH0dc9MzeVE2VU,4340
|
|
18
18
|
fal/utils.py,sha256=iQTBG3-i6JZgHkkwbY_I4210g0xoW-as51yrke608u0,2208
|
|
19
19
|
fal/workflows.py,sha256=Zl4f6Bs085hY40zmqScxDUyCu7zXkukDbW02iYOLTTI,14805
|
|
@@ -23,7 +23,7 @@ fal/auth/local.py,sha256=sndkM6vKpeVny6NHTacVlTbiIFqaksOmw0Viqs_RN1U,1790
|
|
|
23
23
|
fal/cli/__init__.py,sha256=padK4o0BFqq61kxAA1qQ0jYr2SuhA2mf90B3AaRkmJA,37
|
|
24
24
|
fal/cli/_utils.py,sha256=anFfy6qouB8QzH0Yho41GulGiJu3q1KKIwgyVQCzgRQ,1593
|
|
25
25
|
fal/cli/api.py,sha256=ZuDE_PIC-czzneTAWMwvC7P7WnwIyluNZSuJqzCFhqI,2640
|
|
26
|
-
fal/cli/apps.py,sha256=
|
|
26
|
+
fal/cli/apps.py,sha256=0ex4OWbxcopjEq_BGkwkxCBSTLqUIHmFDz1MWOv06zk,10479
|
|
27
27
|
fal/cli/auth.py,sha256=Qe-Z3ycXJnOzHimz5PjCQYoni8MF4csmdL19yGN7a1o,5171
|
|
28
28
|
fal/cli/cli_nested_json.py,sha256=veSZU8_bYV3Iu1PAoxt-4BMBraNIqgH5nughbs2UKvE,13539
|
|
29
29
|
fal/cli/create.py,sha256=a8WDq-nJLFTeoIXqpb5cr7GR7YR9ZZrQCawNm34KXXE,627
|
|
@@ -142,8 +142,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
|
|
|
142
142
|
openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
|
|
143
143
|
openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
|
|
144
144
|
openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
|
|
145
|
-
fal-1.26.
|
|
146
|
-
fal-1.26.
|
|
147
|
-
fal-1.26.
|
|
148
|
-
fal-1.26.
|
|
149
|
-
fal-1.26.
|
|
145
|
+
fal-1.26.1.dist-info/METADATA,sha256=Dk0fMzu4rMEi6H9l2mWWBqjkcyS-QvzxFrVmt6rbnGY,4089
|
|
146
|
+
fal-1.26.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
147
|
+
fal-1.26.1.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
|
|
148
|
+
fal-1.26.1.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
|
|
149
|
+
fal-1.26.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|