fal 1.5.3__py3-none-any.whl → 1.5.5__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 -0
- fal/app.py +90 -5
- fal/toolkit/file/providers/fal.py +200 -16
- fal/toolkit/file/providers/gcp.py +6 -1
- fal/toolkit/file/providers/r2.py +6 -1
- {fal-1.5.3.dist-info → fal-1.5.5.dist-info}/METADATA +2 -1
- {fal-1.5.3.dist-info → fal-1.5.5.dist-info}/RECORD +11 -11
- {fal-1.5.3.dist-info → fal-1.5.5.dist-info}/WHEEL +1 -1
- {fal-1.5.3.dist-info → fal-1.5.5.dist-info}/entry_points.txt +0 -0
- {fal-1.5.3.dist-info → fal-1.5.5.dist-info}/top_level.txt +0 -0
fal/_fal_version.py
CHANGED
fal/api.py
CHANGED
fal/app.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import inspect
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
@@ -8,21 +9,25 @@ import re
|
|
|
8
9
|
import threading
|
|
9
10
|
import time
|
|
10
11
|
import typing
|
|
11
|
-
from contextlib import asynccontextmanager, contextmanager
|
|
12
|
+
from contextlib import AsyncExitStack, asynccontextmanager, contextmanager
|
|
13
|
+
from dataclasses import dataclass
|
|
12
14
|
from typing import Any, Callable, ClassVar, Literal, TypeVar
|
|
13
15
|
|
|
16
|
+
import grpc.aio as async_grpc
|
|
14
17
|
import httpx
|
|
15
18
|
from fastapi import FastAPI
|
|
19
|
+
from isolate.server import definitions
|
|
16
20
|
|
|
17
21
|
import fal.api
|
|
18
22
|
from fal._serialization import include_modules_from
|
|
19
23
|
from fal.api import RouteSignature
|
|
20
|
-
from fal.exceptions import RequestCancelledException
|
|
24
|
+
from fal.exceptions import FalServerlessException, RequestCancelledException
|
|
21
25
|
from fal.logging import get_logger
|
|
22
26
|
from fal.toolkit.file import get_lifecycle_preference
|
|
23
27
|
from fal.toolkit.file.providers.fal import GLOBAL_LIFECYCLE_PREFERENCE
|
|
24
28
|
|
|
25
29
|
REALTIME_APP_REQUIREMENTS = ["websockets", "msgpack"]
|
|
30
|
+
REQUEST_ID_KEY = "x-fal-request-id"
|
|
26
31
|
|
|
27
32
|
EndpointT = TypeVar("EndpointT", bound=Callable[..., Any])
|
|
28
33
|
logger = get_logger(__name__)
|
|
@@ -35,6 +40,48 @@ async def _call_any_fn(fn, *args, **kwargs):
|
|
|
35
40
|
return fn(*args, **kwargs)
|
|
36
41
|
|
|
37
42
|
|
|
43
|
+
async def open_isolate_channel(address: str) -> async_grpc.Channel:
|
|
44
|
+
_stack = AsyncExitStack()
|
|
45
|
+
channel = await _stack.enter_async_context(
|
|
46
|
+
async_grpc.insecure_channel(
|
|
47
|
+
address,
|
|
48
|
+
options=[
|
|
49
|
+
("grpc.max_send_message_length", -1),
|
|
50
|
+
("grpc.max_receive_message_length", -1),
|
|
51
|
+
("grpc.min_reconnect_backoff_ms", 0),
|
|
52
|
+
("grpc.max_reconnect_backoff_ms", 100),
|
|
53
|
+
("grpc.dns_min_time_between_resolutions_ms", 100),
|
|
54
|
+
],
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
channel_status = channel.channel_ready()
|
|
59
|
+
try:
|
|
60
|
+
await asyncio.wait_for(channel_status, timeout=1)
|
|
61
|
+
except asyncio.TimeoutError:
|
|
62
|
+
await _stack.aclose()
|
|
63
|
+
raise Exception("Timed out trying to connect to local isolate")
|
|
64
|
+
|
|
65
|
+
return channel
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def _set_logger_labels(
|
|
69
|
+
logger_labels: dict[str, str], channel: async_grpc.Channel
|
|
70
|
+
):
|
|
71
|
+
try:
|
|
72
|
+
isolate = definitions.IsolateStub(channel)
|
|
73
|
+
isolate_request = definitions.SetMetadataRequest(
|
|
74
|
+
# TODO: when submit is shipped, get task_id from an env var
|
|
75
|
+
task_id="RUN",
|
|
76
|
+
metadata=definitions.TaskMetadata(logger_labels=logger_labels),
|
|
77
|
+
)
|
|
78
|
+
res = isolate.SetMetadata(isolate_request)
|
|
79
|
+
code = await res.code()
|
|
80
|
+
assert str(code) == "StatusCode.OK"
|
|
81
|
+
except BaseException:
|
|
82
|
+
logger.debug("Failed to set logger labels", exc_info=True)
|
|
83
|
+
|
|
84
|
+
|
|
38
85
|
def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
|
|
39
86
|
include_modules_from(cls)
|
|
40
87
|
|
|
@@ -76,6 +123,12 @@ def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
|
|
|
76
123
|
return fn
|
|
77
124
|
|
|
78
125
|
|
|
126
|
+
@dataclass
|
|
127
|
+
class AppClientError(FalServerlessException):
|
|
128
|
+
message: str
|
|
129
|
+
status_code: int
|
|
130
|
+
|
|
131
|
+
|
|
79
132
|
class EndpointClient:
|
|
80
133
|
def __init__(self, url, endpoint, signature, timeout: int | None = None):
|
|
81
134
|
self.url = url
|
|
@@ -88,12 +141,19 @@ class EndpointClient:
|
|
|
88
141
|
|
|
89
142
|
def __call__(self, data):
|
|
90
143
|
with httpx.Client() as client:
|
|
144
|
+
url = self.url + self.signature.path
|
|
91
145
|
resp = client.post(
|
|
92
146
|
self.url + self.signature.path,
|
|
93
147
|
json=data.dict() if hasattr(data, "dict") else dict(data),
|
|
94
148
|
timeout=self.timeout,
|
|
95
149
|
)
|
|
96
|
-
resp.
|
|
150
|
+
if not resp.is_success:
|
|
151
|
+
# allow logs to be printed before raising the exception
|
|
152
|
+
time.sleep(1)
|
|
153
|
+
raise AppClientError(
|
|
154
|
+
f"Failed to POST {url}: {resp.status_code} {resp.text}",
|
|
155
|
+
status_code=resp.status_code,
|
|
156
|
+
)
|
|
97
157
|
resp_dict = resp.json()
|
|
98
158
|
|
|
99
159
|
if not self.return_type:
|
|
@@ -146,12 +206,16 @@ class AppClient:
|
|
|
146
206
|
with httpx.Client() as client:
|
|
147
207
|
retries = 100
|
|
148
208
|
for _ in range(retries):
|
|
149
|
-
|
|
209
|
+
url = info.url + "/health"
|
|
210
|
+
resp = client.get(url, timeout=60)
|
|
150
211
|
|
|
151
212
|
if resp.is_success:
|
|
152
213
|
break
|
|
153
214
|
elif resp.status_code not in (500, 404):
|
|
154
|
-
|
|
215
|
+
raise AppClientError(
|
|
216
|
+
f"Failed to GET {url}: {resp.status_code} {resp.text}",
|
|
217
|
+
status_code=resp.status_code,
|
|
218
|
+
)
|
|
155
219
|
time.sleep(0.1)
|
|
156
220
|
|
|
157
221
|
client = cls(app_cls, info.url)
|
|
@@ -192,6 +256,8 @@ class App(fal.api.BaseServable):
|
|
|
192
256
|
app_auth: ClassVar[Literal["private", "public", "shared"]] = "private"
|
|
193
257
|
request_timeout: ClassVar[int | None] = None
|
|
194
258
|
|
|
259
|
+
isolate_channel: async_grpc.Channel | None = None
|
|
260
|
+
|
|
195
261
|
def __init_subclass__(cls, **kwargs):
|
|
196
262
|
app_name = kwargs.pop("name", None) or _to_fal_app_name(cls.__name__)
|
|
197
263
|
parent_settings = getattr(cls, "host_kwargs", {})
|
|
@@ -282,8 +348,27 @@ class App(fal.api.BaseServable):
|
|
|
282
348
|
"Failed set a global lifecycle preference %s",
|
|
283
349
|
self.__class__.__name__,
|
|
284
350
|
)
|
|
351
|
+
|
|
285
352
|
return await call_next(request)
|
|
286
353
|
|
|
354
|
+
@app.middleware("http")
|
|
355
|
+
async def set_request_id(request, call_next):
|
|
356
|
+
if self.isolate_channel is None:
|
|
357
|
+
grpc_port = os.environ.get("NOMAD_ALLOC_PORT_grpc")
|
|
358
|
+
self.isolate_channel = await open_isolate_channel(
|
|
359
|
+
f"localhost:{grpc_port}"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
request_id = request.headers.get(REQUEST_ID_KEY)
|
|
363
|
+
if request_id is not None:
|
|
364
|
+
await _set_logger_labels(
|
|
365
|
+
{"fal_request_id": request_id}, channel=self.isolate_channel
|
|
366
|
+
)
|
|
367
|
+
try:
|
|
368
|
+
return await call_next(request)
|
|
369
|
+
finally:
|
|
370
|
+
await _set_logger_labels({}, channel=self.isolate_channel)
|
|
371
|
+
|
|
287
372
|
@app.exception_handler(RequestCancelledException)
|
|
288
373
|
async def value_error_exception_handler(
|
|
289
374
|
request, exc: RequestCancelledException
|
|
@@ -33,9 +33,17 @@ class FalV2Token:
|
|
|
33
33
|
return datetime.now(timezone.utc) >= self.expires_at
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
class FalV3Token(FalV2Token):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
36
40
|
class FalV2TokenManager:
|
|
41
|
+
token_cls: type[FalV2Token] = FalV2Token
|
|
42
|
+
storage_type: str = "fal-cdn"
|
|
43
|
+
upload_prefix = "upload."
|
|
44
|
+
|
|
37
45
|
def __init__(self):
|
|
38
|
-
self._token: FalV2Token =
|
|
46
|
+
self._token: FalV2Token = self.token_cls(
|
|
39
47
|
token="",
|
|
40
48
|
token_type="",
|
|
41
49
|
base_upload_url="",
|
|
@@ -63,7 +71,7 @@ class FalV2TokenManager:
|
|
|
63
71
|
|
|
64
72
|
grpc_host = os.environ.get("FAL_HOST", "api.alpha.fal.ai")
|
|
65
73
|
rest_host = grpc_host.replace("api", "rest", 1)
|
|
66
|
-
url = f"https://{rest_host}/storage/auth/token"
|
|
74
|
+
url = f"https://{rest_host}/storage/auth/token?storage_type={self.storage_type}"
|
|
67
75
|
|
|
68
76
|
req = Request(
|
|
69
77
|
url,
|
|
@@ -76,10 +84,10 @@ class FalV2TokenManager:
|
|
|
76
84
|
|
|
77
85
|
parsed_base_url = urlparse(result["base_url"])
|
|
78
86
|
base_upload_url = urlunparse(
|
|
79
|
-
parsed_base_url._replace(netloc=
|
|
87
|
+
parsed_base_url._replace(netloc=self.upload_prefix + parsed_base_url.netloc)
|
|
80
88
|
)
|
|
81
89
|
|
|
82
|
-
self._token =
|
|
90
|
+
self._token = self.token_cls(
|
|
83
91
|
token=result["token"],
|
|
84
92
|
token_type=result["token_type"],
|
|
85
93
|
base_upload_url=base_upload_url,
|
|
@@ -87,7 +95,14 @@ class FalV2TokenManager:
|
|
|
87
95
|
)
|
|
88
96
|
|
|
89
97
|
|
|
98
|
+
class FalV3TokenManager(FalV2TokenManager):
|
|
99
|
+
token_cls: type[FalV2Token] = FalV3Token
|
|
100
|
+
storage_type: str = "fal-cdn-v3"
|
|
101
|
+
upload_prefix = ""
|
|
102
|
+
|
|
103
|
+
|
|
90
104
|
fal_v2_token_manager = FalV2TokenManager()
|
|
105
|
+
fal_v3_token_manager = FalV3TokenManager()
|
|
91
106
|
|
|
92
107
|
|
|
93
108
|
@dataclass
|
|
@@ -275,6 +290,128 @@ class MultipartUpload:
|
|
|
275
290
|
return self._file_url
|
|
276
291
|
|
|
277
292
|
|
|
293
|
+
class MultipartUploadV3:
|
|
294
|
+
MULTIPART_THRESHOLD = 100 * 1024 * 1024
|
|
295
|
+
MULTIPART_CHUNK_SIZE = 10 * 1024 * 1024
|
|
296
|
+
MULTIPART_MAX_CONCURRENCY = 10
|
|
297
|
+
|
|
298
|
+
def __init__(
|
|
299
|
+
self,
|
|
300
|
+
file_path: str | Path,
|
|
301
|
+
chunk_size: int | None = None,
|
|
302
|
+
content_type: str | None = None,
|
|
303
|
+
max_concurrency: int | None = None,
|
|
304
|
+
) -> None:
|
|
305
|
+
self.file_path = file_path
|
|
306
|
+
self.chunk_size = chunk_size or self.MULTIPART_CHUNK_SIZE
|
|
307
|
+
self.content_type = content_type or "application/octet-stream"
|
|
308
|
+
self.max_concurrency = max_concurrency or self.MULTIPART_MAX_CONCURRENCY
|
|
309
|
+
self.access_url = None
|
|
310
|
+
self.upload_id = None
|
|
311
|
+
|
|
312
|
+
self._parts: list[dict] = []
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def auth_headers(self) -> dict[str, str]:
|
|
316
|
+
token = fal_v3_token_manager.get_token()
|
|
317
|
+
return {
|
|
318
|
+
"Authorization": f"{token.token_type} {token.token}",
|
|
319
|
+
"User-Agent": "fal/0.1.0",
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
def create(self):
|
|
323
|
+
token = fal_v3_token_manager.get_token()
|
|
324
|
+
try:
|
|
325
|
+
req = Request(
|
|
326
|
+
f"{token.base_upload_url}/files/upload/multipart",
|
|
327
|
+
method="POST",
|
|
328
|
+
headers={
|
|
329
|
+
**self.auth_headers,
|
|
330
|
+
"Accept": "application/json",
|
|
331
|
+
"Content-Type": self.content_type,
|
|
332
|
+
"X-Fal-File-Name": os.path.basename(self.file_path),
|
|
333
|
+
},
|
|
334
|
+
)
|
|
335
|
+
with urlopen(req) as response:
|
|
336
|
+
result = json.load(response)
|
|
337
|
+
self.access_url = result["access_url"]
|
|
338
|
+
self.upload_id = result["uploadId"]
|
|
339
|
+
except HTTPError as exc:
|
|
340
|
+
raise FileUploadException(
|
|
341
|
+
f"Error initiating upload. Status {exc.status}: {exc.reason}"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
@retry(max_retries=5, base_delay=1, backoff_type="exponential", jitter=True)
|
|
345
|
+
def _upload_part(self, url: str, part_number: int) -> dict:
|
|
346
|
+
with open(self.file_path, "rb") as f:
|
|
347
|
+
start = (part_number - 1) * self.chunk_size
|
|
348
|
+
f.seek(start)
|
|
349
|
+
data = f.read(self.chunk_size)
|
|
350
|
+
req = Request(
|
|
351
|
+
url,
|
|
352
|
+
method="PUT",
|
|
353
|
+
headers={
|
|
354
|
+
**self.auth_headers,
|
|
355
|
+
"Content-Type": self.content_type,
|
|
356
|
+
},
|
|
357
|
+
data=data,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
with urlopen(req) as resp:
|
|
362
|
+
return {
|
|
363
|
+
"partNumber": part_number,
|
|
364
|
+
"etag": resp.headers["ETag"],
|
|
365
|
+
}
|
|
366
|
+
except HTTPError as exc:
|
|
367
|
+
raise FileUploadException(
|
|
368
|
+
f"Error uploading part {part_number} to {url}. "
|
|
369
|
+
f"Status {exc.status}: {exc.reason}"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def upload(self) -> None:
|
|
373
|
+
import concurrent.futures
|
|
374
|
+
|
|
375
|
+
parts = math.ceil(os.path.getsize(self.file_path) / self.chunk_size)
|
|
376
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
377
|
+
max_workers=self.max_concurrency
|
|
378
|
+
) as executor:
|
|
379
|
+
futures = []
|
|
380
|
+
for part_number in range(1, parts + 1):
|
|
381
|
+
upload_url = (
|
|
382
|
+
f"{self.access_url}/multipart/{self.upload_id}/{part_number}"
|
|
383
|
+
)
|
|
384
|
+
futures.append(
|
|
385
|
+
executor.submit(self._upload_part, upload_url, part_number)
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
for future in concurrent.futures.as_completed(futures):
|
|
389
|
+
entry = future.result()
|
|
390
|
+
self._parts.append(entry)
|
|
391
|
+
|
|
392
|
+
def complete(self):
|
|
393
|
+
url = f"{self.access_url}/multipart/{self.upload_id}/complete"
|
|
394
|
+
try:
|
|
395
|
+
req = Request(
|
|
396
|
+
url,
|
|
397
|
+
method="POST",
|
|
398
|
+
headers={
|
|
399
|
+
**self.auth_headers,
|
|
400
|
+
"Accept": "application/json",
|
|
401
|
+
"Content-Type": "application/json",
|
|
402
|
+
},
|
|
403
|
+
data=json.dumps({"parts": self._parts}).encode(),
|
|
404
|
+
)
|
|
405
|
+
with urlopen(req):
|
|
406
|
+
pass
|
|
407
|
+
except HTTPError as e:
|
|
408
|
+
raise FileUploadException(
|
|
409
|
+
f"Error completing upload {url}. Status {e.status}: {e.reason}"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
return self.access_url
|
|
413
|
+
|
|
414
|
+
|
|
278
415
|
@dataclass
|
|
279
416
|
class FalFileRepositoryV2(FalFileRepositoryBase):
|
|
280
417
|
@retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
|
|
@@ -415,16 +552,15 @@ class FalCDNFileRepository(FileRepository):
|
|
|
415
552
|
class FalFileRepositoryV3(FileRepository):
|
|
416
553
|
@retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
|
|
417
554
|
def save(
|
|
418
|
-
self, file: FileData,
|
|
555
|
+
self, file: FileData, object_lifecycle_preference: dict[str, str] | None
|
|
419
556
|
) -> str:
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
key
|
|
425
|
-
if key in user_lifecycle_preference
|
|
557
|
+
lifecycle = dataclasses.asdict(GLOBAL_LIFECYCLE_PREFERENCE)
|
|
558
|
+
if object_lifecycle_preference is not None:
|
|
559
|
+
lifecycle = {
|
|
560
|
+
key: object_lifecycle_preference[key]
|
|
561
|
+
if key in object_lifecycle_preference
|
|
426
562
|
else value
|
|
427
|
-
for key, value in
|
|
563
|
+
for key, value in lifecycle.items()
|
|
428
564
|
}
|
|
429
565
|
|
|
430
566
|
headers = {
|
|
@@ -432,9 +568,7 @@ class FalFileRepositoryV3(FileRepository):
|
|
|
432
568
|
"Accept": "application/json",
|
|
433
569
|
"Content-Type": file.content_type,
|
|
434
570
|
"X-Fal-File-Name": file.file_name,
|
|
435
|
-
"X-Fal-Object-Lifecycle-Preference": json.dumps(
|
|
436
|
-
object_lifecycle_preference
|
|
437
|
-
),
|
|
571
|
+
"X-Fal-Object-Lifecycle-Preference": json.dumps(lifecycle),
|
|
438
572
|
}
|
|
439
573
|
url = os.getenv("FAL_CDN_V3_HOST", _FAL_CDN_V3) + "/files/upload"
|
|
440
574
|
request = Request(url, headers=headers, method="POST", data=file.data)
|
|
@@ -451,8 +585,58 @@ class FalFileRepositoryV3(FileRepository):
|
|
|
451
585
|
|
|
452
586
|
@property
|
|
453
587
|
def auth_headers(self) -> dict[str, str]:
|
|
454
|
-
token =
|
|
588
|
+
token = fal_v3_token_manager.get_token()
|
|
455
589
|
return {
|
|
456
590
|
"Authorization": f"{token.token_type} {token.token}",
|
|
457
591
|
"User-Agent": "fal/0.1.0",
|
|
458
592
|
}
|
|
593
|
+
|
|
594
|
+
def _save_multipart(
|
|
595
|
+
self,
|
|
596
|
+
file_path: str | Path,
|
|
597
|
+
chunk_size: int | None = None,
|
|
598
|
+
content_type: str | None = None,
|
|
599
|
+
max_concurrency: int | None = None,
|
|
600
|
+
) -> str:
|
|
601
|
+
multipart = MultipartUploadV3(
|
|
602
|
+
file_path,
|
|
603
|
+
chunk_size=chunk_size,
|
|
604
|
+
content_type=content_type,
|
|
605
|
+
max_concurrency=max_concurrency,
|
|
606
|
+
)
|
|
607
|
+
multipart.create()
|
|
608
|
+
multipart.upload()
|
|
609
|
+
return multipart.complete()
|
|
610
|
+
|
|
611
|
+
def save_file(
|
|
612
|
+
self,
|
|
613
|
+
file_path: str | Path,
|
|
614
|
+
content_type: str,
|
|
615
|
+
multipart: bool | None = None,
|
|
616
|
+
multipart_threshold: int | None = None,
|
|
617
|
+
multipart_chunk_size: int | None = None,
|
|
618
|
+
multipart_max_concurrency: int | None = None,
|
|
619
|
+
object_lifecycle_preference: dict[str, str] | None = None,
|
|
620
|
+
) -> tuple[str, FileData | None]:
|
|
621
|
+
if multipart is None:
|
|
622
|
+
threshold = multipart_threshold or MultipartUpload.MULTIPART_THRESHOLD
|
|
623
|
+
multipart = os.path.getsize(file_path) > threshold
|
|
624
|
+
|
|
625
|
+
if multipart:
|
|
626
|
+
url = self._save_multipart(
|
|
627
|
+
file_path,
|
|
628
|
+
chunk_size=multipart_chunk_size,
|
|
629
|
+
content_type=content_type,
|
|
630
|
+
max_concurrency=multipart_max_concurrency,
|
|
631
|
+
)
|
|
632
|
+
data = None
|
|
633
|
+
else:
|
|
634
|
+
with open(file_path, "rb") as f:
|
|
635
|
+
data = FileData(
|
|
636
|
+
f.read(),
|
|
637
|
+
content_type=content_type,
|
|
638
|
+
file_name=os.path.basename(file_path),
|
|
639
|
+
)
|
|
640
|
+
url = self.save(data, object_lifecycle_preference)
|
|
641
|
+
|
|
642
|
+
return url, data
|
|
@@ -6,6 +6,7 @@ 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
|
|
11
12
|
from fal.toolkit.utils.retry import retry
|
|
@@ -52,7 +53,11 @@ class GoogleStorageRepository(FileRepository):
|
|
|
52
53
|
return self._bucket
|
|
53
54
|
|
|
54
55
|
@retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
|
|
55
|
-
def save(
|
|
56
|
+
def save(
|
|
57
|
+
self,
|
|
58
|
+
data: FileData,
|
|
59
|
+
object_lifecycle_preference: Optional[dict[str, str]] = None,
|
|
60
|
+
) -> str:
|
|
56
61
|
destination_path = posixpath.join(
|
|
57
62
|
self.folder,
|
|
58
63
|
f"{uuid.uuid4().hex}_{data.file_name}",
|
fal/toolkit/file/providers/r2.py
CHANGED
|
@@ -6,6 +6,7 @@ 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
|
|
11
12
|
from fal.toolkit.utils.retry import retry
|
|
@@ -69,7 +70,11 @@ class R2Repository(FileRepository):
|
|
|
69
70
|
return self._bucket
|
|
70
71
|
|
|
71
72
|
@retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
|
|
72
|
-
def save(
|
|
73
|
+
def save(
|
|
74
|
+
self,
|
|
75
|
+
data: FileData,
|
|
76
|
+
object_lifecycle_preference: Optional[dict[str, str]] = None,
|
|
77
|
+
) -> str:
|
|
73
78
|
destination_path = posixpath.join(
|
|
74
79
|
self.key,
|
|
75
80
|
f"{uuid.uuid4().hex}_{data.file_name}",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fal
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.5
|
|
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
|
|
@@ -23,6 +23,7 @@ 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
|
|
@@ -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=
|
|
3
|
+
fal/_fal_version.py,sha256=xsDT6ryShhQQK24Bw1uqyQ-iWYEMz5nJFNa-x4pSQzY,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=
|
|
6
|
+
fal/api.py,sha256=oiPONakWiK8KQHaUr41yBIG64FlC9UryFNXFiITVtXk,43295
|
|
7
|
+
fal/app.py,sha256=pkOTw-69nY6U9PaFtvz4sFjMyyZXIziM9mepzkx-NBo,21298
|
|
8
8
|
fal/apps.py,sha256=lge7-HITzI20l1oXdlkAzqxdMVtXRfnACIylKRWgCNQ,7151
|
|
9
9
|
fal/container.py,sha256=V7riyyq8AZGwEX9QaqRQDZyDN_bUKeRKV1OOZArXjL0,622
|
|
10
10
|
fal/files.py,sha256=QgfYfMKmNobMPufrAP_ga1FKcIAlSbw18Iar1-0qepo,2650
|
|
@@ -49,9 +49,9 @@ fal/toolkit/optimize.py,sha256=p75sovF0SmRP6zxzpIaaOmqlxvXB_xEz3XPNf59EF7w,1339
|
|
|
49
49
|
fal/toolkit/file/__init__.py,sha256=FbNl6wD-P0aSSTUwzHt4HujBXrbC3ABmaigPQA4hRfg,70
|
|
50
50
|
fal/toolkit/file/file.py,sha256=EUSzX8480sKRtQzGisgBc8WUiCCCkrohXmpQIh4otlI,8910
|
|
51
51
|
fal/toolkit/file/types.py,sha256=MjZ6xAhKPv4rowLo2Vcbho0sX7AQ3lm3KFyYDcw0dL4,1845
|
|
52
|
-
fal/toolkit/file/providers/fal.py,sha256=
|
|
53
|
-
fal/toolkit/file/providers/gcp.py,sha256=
|
|
54
|
-
fal/toolkit/file/providers/r2.py,sha256=
|
|
52
|
+
fal/toolkit/file/providers/fal.py,sha256=V93vQbbFevPPpPSGfgkWuaBCYZnSxjtK50Lhk-zQYi8,21016
|
|
53
|
+
fal/toolkit/file/providers/gcp.py,sha256=iQtkoYUqbmKKpC5srVOYtrruZ3reGRm5lz4kM8bshgk,2247
|
|
54
|
+
fal/toolkit/file/providers/r2.py,sha256=G2OHcCH2yWrVtXT4hWHEXUeEjFhbKO0koqHcd7hkczk,2871
|
|
55
55
|
fal/toolkit/image/__init__.py,sha256=aLcU8HzD7HyOxx-C-Bbx9kYCMHdBhy9tR98FSVJ6gSA,1830
|
|
56
56
|
fal/toolkit/image/image.py,sha256=ZSkozciP4XxaGnvrR_mP4utqE3_QhoPN0dau9FJ2Xco,5033
|
|
57
57
|
fal/toolkit/image/safety_checker.py,sha256=S7ow-HuoVxC6ixHWWcBrAUm2dIlgq3sTAIull6xIbAg,3105
|
|
@@ -126,8 +126,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
|
|
|
126
126
|
openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
|
|
127
127
|
openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
|
|
128
128
|
openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
|
|
129
|
-
fal-1.5.
|
|
130
|
-
fal-1.5.
|
|
131
|
-
fal-1.5.
|
|
132
|
-
fal-1.5.
|
|
133
|
-
fal-1.5.
|
|
129
|
+
fal-1.5.5.dist-info/METADATA,sha256=F5-7Nh-ah7B0N4HHsHdeR7rydd5rReOPafzyjSSB0lU,3989
|
|
130
|
+
fal-1.5.5.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
|
|
131
|
+
fal-1.5.5.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
|
|
132
|
+
fal-1.5.5.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
|
|
133
|
+
fal-1.5.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|