fal 1.5.2__py3-none-any.whl → 1.5.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fal might be problematic. Click here for more details.

fal/_fal_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.5.2'
16
- __version_tuple__ = version_tuple = (1, 5, 2)
15
+ __version__ = version = '1.5.4'
16
+ __version_tuple__ = version_tuple = (1, 5, 4)
fal/api.py CHANGED
@@ -76,6 +76,7 @@ SERVE_REQUIREMENTS = [
76
76
  f"pydantic=={pydantic_version}",
77
77
  "uvicorn",
78
78
  "starlette_exporter",
79
+ "structlog",
79
80
  ]
80
81
 
81
82
 
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,20 +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
- from fal.toolkit.file.providers import fal as fal_provider_module
26
+ from fal.toolkit.file import get_lifecycle_preference
27
+ from fal.toolkit.file.providers.fal import GLOBAL_LIFECYCLE_PREFERENCE
23
28
 
24
29
  REALTIME_APP_REQUIREMENTS = ["websockets", "msgpack"]
30
+ REQUEST_ID_KEY = "x-fal-request-id"
25
31
 
26
32
  EndpointT = TypeVar("EndpointT", bound=Callable[..., Any])
27
33
  logger = get_logger(__name__)
@@ -34,6 +40,48 @@ async def _call_any_fn(fn, *args, **kwargs):
34
40
  return fn(*args, **kwargs)
35
41
 
36
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.exception("Failed to set logger labels")
83
+
84
+
37
85
  def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
38
86
  include_modules_from(cls)
39
87
 
@@ -75,6 +123,12 @@ def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
75
123
  return fn
76
124
 
77
125
 
126
+ @dataclass
127
+ class AppClientError(FalServerlessException):
128
+ message: str
129
+ status_code: int
130
+
131
+
78
132
  class EndpointClient:
79
133
  def __init__(self, url, endpoint, signature, timeout: int | None = None):
80
134
  self.url = url
@@ -87,12 +141,19 @@ class EndpointClient:
87
141
 
88
142
  def __call__(self, data):
89
143
  with httpx.Client() as client:
144
+ url = self.url + self.signature.path
90
145
  resp = client.post(
91
146
  self.url + self.signature.path,
92
147
  json=data.dict() if hasattr(data, "dict") else dict(data),
93
148
  timeout=self.timeout,
94
149
  )
95
- resp.raise_for_status()
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
+ )
96
157
  resp_dict = resp.json()
97
158
 
98
159
  if not self.return_type:
@@ -145,12 +206,16 @@ class AppClient:
145
206
  with httpx.Client() as client:
146
207
  retries = 100
147
208
  for _ in range(retries):
148
- resp = client.get(info.url + "/health", timeout=60)
209
+ url = info.url + "/health"
210
+ resp = client.get(url, timeout=60)
149
211
 
150
212
  if resp.is_success:
151
213
  break
152
214
  elif resp.status_code not in (500, 404):
153
- resp.raise_for_status()
215
+ raise AppClientError(
216
+ f"Failed to GET {url}: {resp.status_code} {resp.text}",
217
+ status_code=resp.status_code,
218
+ )
154
219
  time.sleep(0.1)
155
220
 
156
221
  client = cls(app_cls, info.url)
@@ -191,6 +256,8 @@ class App(fal.api.BaseServable):
191
256
  app_auth: ClassVar[Literal["private", "public", "shared"]] = "private"
192
257
  request_timeout: ClassVar[int | None] = None
193
258
 
259
+ isolate_channel: async_grpc.Channel | None = None
260
+
194
261
  def __init_subclass__(cls, **kwargs):
195
262
  app_name = kwargs.pop("name", None) or _to_fal_app_name(cls.__name__)
196
263
  parent_settings = getattr(cls, "host_kwargs", {})
@@ -266,11 +333,14 @@ class App(fal.api.BaseServable):
266
333
 
267
334
  @app.middleware("http")
268
335
  async def set_global_object_preference(request, call_next):
269
- response = await call_next(request)
270
336
  try:
271
- fal_provider_module.GLOBAL_LIFECYCLE_PREFERENCE = request.headers.get(
272
- "X-Fal-Object-Lifecycle-Preference"
273
- )
337
+ preference_dict = get_lifecycle_preference(request) or {}
338
+ expiration_duration = preference_dict.get("expiration_duration_seconds")
339
+ if expiration_duration is not None:
340
+ GLOBAL_LIFECYCLE_PREFERENCE.expiration_duration_seconds = int(
341
+ expiration_duration
342
+ )
343
+
274
344
  except Exception:
275
345
  from fastapi.logger import logger
276
346
 
@@ -278,7 +348,26 @@ class App(fal.api.BaseServable):
278
348
  "Failed set a global lifecycle preference %s",
279
349
  self.__class__.__name__,
280
350
  )
281
- return response
351
+
352
+ return await call_next(request)
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)
282
371
 
283
372
  @app.exception_handler(RequestCancelledException)
284
373
  async def value_error_exception_handler(
fal/toolkit/file/file.py CHANGED
@@ -144,7 +144,7 @@ class File(BaseModel):
144
144
 
145
145
  fdata = FileData(data, content_type, file_name)
146
146
 
147
- object_lifecycle_preference = _get_lifecycle_preference(request)
147
+ object_lifecycle_preference = get_lifecycle_preference(request)
148
148
 
149
149
  try:
150
150
  url = repo.save(fdata, object_lifecycle_preference)
@@ -191,7 +191,7 @@ class File(BaseModel):
191
191
  )
192
192
 
193
193
  content_type = content_type or "application/octet-stream"
194
- object_lifecycle_preference = _get_lifecycle_preference(request)
194
+ object_lifecycle_preference = get_lifecycle_preference(request)
195
195
 
196
196
  try:
197
197
  url, data = repo.save_file(
@@ -274,7 +274,7 @@ class CompressedFile(File):
274
274
  shutil.rmtree(self.extract_dir)
275
275
 
276
276
 
277
- def _get_lifecycle_preference(request: Request) -> dict[str, str] | None:
277
+ def get_lifecycle_preference(request: Request) -> dict[str, str] | None:
278
278
  import json
279
279
 
280
280
  preference_str = (
@@ -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 = 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="upload." + parsed_base_url.netloc)
87
+ parsed_base_url._replace(netloc=self.upload_prefix + parsed_base_url.netloc)
80
88
  )
81
89
 
82
- self._token = FalV2Token(
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, user_lifecycle_preference: dict[str, str] | None
555
+ self, file: FileData, object_lifecycle_preference: dict[str, str] | None
419
556
  ) -> str:
420
- object_lifecycle_preference = dataclasses.asdict(GLOBAL_LIFECYCLE_PREFERENCE)
421
-
422
- if user_lifecycle_preference is not None:
423
- object_lifecycle_preference = {
424
- key: user_lifecycle_preference[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 object_lifecycle_preference.items()
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 = fal_v2_token_manager.get_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(self, data: FileData) -> str:
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}",
@@ -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(self, data: FileData) -> str:
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.2
3
+ Version: 1.5.4
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
@@ -38,8 +39,12 @@ Requires-Dist: cookiecutter
38
39
  Requires-Dist: tomli
39
40
  Requires-Dist: importlib-metadata>=4.4; python_version < "3.10"
40
41
  Provides-Extra: dev
41
- Requires-Dist: fal[test]; extra == "dev"
42
+ Requires-Dist: fal[docs,test]; extra == "dev"
42
43
  Requires-Dist: openapi-python-client<1,>=0.14.1; extra == "dev"
44
+ Provides-Extra: docs
45
+ Requires-Dist: sphinx; extra == "docs"
46
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
47
+ Requires-Dist: sphinx-autodoc-typehints; extra == "docs"
43
48
  Provides-Extra: test
44
49
  Requires-Dist: pytest<8; extra == "test"
45
50
  Requires-Dist: pytest-asyncio; extra == "test"
@@ -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=P-JlE1bO3FGbbntvKHqxjgKk7ORvjFhybkK-h8R2s5g,411
3
+ fal/_fal_version.py,sha256=RrBLyJ4gK-bCUwTO6RYVanFzMRY7N_emKX_fTlh7w3Q,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=wmXywHvkdKe0AlsPmXt8_nidPhoC_Ho4BrUi7In4Hek,43278
7
- fal/app.py,sha256=u25k-MLKU_7eux4PuN24iq1zZf5hSeUeYkO7kUIA6uI,18021
6
+ fal/api.py,sha256=oiPONakWiK8KQHaUr41yBIG64FlC9UryFNXFiITVtXk,43295
7
+ fal/app.py,sha256=mWYUuWT3O6Aj36zLbdpCnLhz6jR7-_NGc7kxcWwcyKA,21287
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
@@ -47,11 +47,11 @@ fal/toolkit/__init__.py,sha256=sV95wiUzKoiDqF9vDgq4q-BLa2sD6IpuKSqp5kdTQNE,658
47
47
  fal/toolkit/exceptions.py,sha256=elHZ7dHCJG5zlHGSBbz-ilkZe9QUvQMomJFi8Pt91LA,198
48
48
  fal/toolkit/optimize.py,sha256=p75sovF0SmRP6zxzpIaaOmqlxvXB_xEz3XPNf59EF7w,1339
49
49
  fal/toolkit/file/__init__.py,sha256=FbNl6wD-P0aSSTUwzHt4HujBXrbC3ABmaigPQA4hRfg,70
50
- fal/toolkit/file/file.py,sha256=GuY4zTqYMod2e9pLorZdPJqmGQ2CmmxaYxmtBm4J7vQ,8913
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=W3XKbAsRuCKPDHwVK1IBen_Tdp23Pi8qhgfiVbsPc4s,14777
53
- fal/toolkit/file/providers/gcp.py,sha256=cxG1j3yuOpFl_Dl_nCEibFE4677qkdXZhuKgb65PnjQ,2126
54
- fal/toolkit/file/providers/r2.py,sha256=Y3DjhpmrbESUTDUtVcKtg0NMKamMTevf6cJA7OgvalI,2750
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.2.dist-info/METADATA,sha256=hOKSZaGlMc6uIwV8Te50B9vaqRBxE2YjCeALzTpveRo,3787
130
- fal-1.5.2.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
131
- fal-1.5.2.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
132
- fal-1.5.2.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
133
- fal-1.5.2.dist-info/RECORD,,
129
+ fal-1.5.4.dist-info/METADATA,sha256=Pjq2dRzHeNtgQqNT4U0diBv2TnpsgL-bn0DC2ZTVGuQ,3989
130
+ fal-1.5.4.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
131
+ fal-1.5.4.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
132
+ fal-1.5.4.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
133
+ fal-1.5.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5