fal 1.2.3__py3-none-any.whl → 1.3.0__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.2.3'
16
- __version_tuple__ = version_tuple = (1, 2, 3)
15
+ __version__ = version = '1.3.0'
16
+ __version_tuple__ = version_tuple = (1, 3, 0)
fal/api.py CHANGED
@@ -44,7 +44,13 @@ from typing_extensions import Concatenate, ParamSpec
44
44
  import fal.flags as flags
45
45
  from fal._serialization import include_modules_from, patch_pickle
46
46
  from fal.container import ContainerImage
47
- from fal.exceptions import FalServerlessException
47
+ from fal.exceptions import (
48
+ AppException,
49
+ CUDAOutOfMemoryException,
50
+ FalServerlessException,
51
+ FieldException,
52
+ )
53
+ from fal.exceptions._cuda import _is_cuda_oom_exception
48
54
  from fal.logging.isolate import IsolateLogPrinter
49
55
  from fal.sdk import (
50
56
  FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
@@ -1002,13 +1008,39 @@ class BaseServable:
1002
1008
  # If it's not a generic 404, just return the original message.
1003
1009
  return JSONResponse({"detail": exc.detail}, 404)
1004
1010
 
1011
+ @_app.exception_handler(AppException)
1012
+ async def app_exception_handler(request: Request, exc: AppException):
1013
+ return JSONResponse({"detail": exc.message}, exc.status_code)
1014
+
1015
+ @_app.exception_handler(FieldException)
1016
+ async def field_exception_handler(request: Request, exc: FieldException):
1017
+ return JSONResponse(exc.to_pydantic_format(), exc.status_code)
1018
+
1019
+ @_app.exception_handler(CUDAOutOfMemoryException)
1020
+ async def cuda_out_of_memory_exception_handler(
1021
+ request: Request, exc: CUDAOutOfMemoryException
1022
+ ):
1023
+ return JSONResponse({"detail": exc.message}, exc.status_code)
1024
+
1005
1025
  @_app.exception_handler(Exception)
1006
1026
  async def traceback_logging_exception_handler(request: Request, exc: Exception):
1007
- print(
1008
- json.dumps(
1009
- {"traceback": "".join(traceback.format_exception(exc)[::-1])} # type: ignore
1027
+ _, MINOR, *_ = sys.version_info
1028
+
1029
+ # traceback.format_exception() has a different signature in Python >=3.10
1030
+ if MINOR >= 10:
1031
+ formatted_exception = traceback.format_exception(exc) # type: ignore
1032
+ else:
1033
+ formatted_exception = traceback.format_exception(
1034
+ type(exc), exc, exc.__traceback__
1010
1035
  )
1011
- )
1036
+
1037
+ print(json.dumps({"traceback": "".join(formatted_exception[::-1])}))
1038
+
1039
+ if _is_cuda_oom_exception(exc):
1040
+ return await cuda_out_of_memory_exception_handler(
1041
+ request, CUDAOutOfMemoryException()
1042
+ )
1043
+
1012
1044
  return JSONResponse({"detail": "Internal Server Error"}, 500)
1013
1045
 
1014
1046
  routes = self.collect_routes()
fal/app.py CHANGED
@@ -3,7 +3,9 @@ from __future__ import annotations
3
3
  import inspect
4
4
  import json
5
5
  import os
6
+ import queue
6
7
  import re
8
+ import threading
7
9
  import time
8
10
  import typing
9
11
  from contextlib import asynccontextmanager, contextmanager
@@ -72,17 +74,22 @@ def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
72
74
 
73
75
 
74
76
  class EndpointClient:
75
- def __init__(self, url, endpoint, signature):
77
+ def __init__(self, url, endpoint, signature, timeout: int | None = None):
76
78
  self.url = url
77
79
  self.endpoint = endpoint
78
80
  self.signature = signature
81
+ self.timeout = timeout
79
82
 
80
83
  annotations = endpoint.__annotations__ or {}
81
84
  self.return_type = annotations.get("return") or None
82
85
 
83
86
  def __call__(self, data):
84
87
  with httpx.Client() as client:
85
- resp = client.post(self.url + self.signature.path, json=dict(data))
88
+ resp = client.post(
89
+ self.url + self.signature.path,
90
+ json=data.dict() if hasattr(data, "dict") else dict(data),
91
+ timeout=self.timeout,
92
+ )
86
93
  resp.raise_for_status()
87
94
  resp_dict = resp.json()
88
95
 
@@ -93,7 +100,12 @@ class EndpointClient:
93
100
 
94
101
 
95
102
  class AppClient:
96
- def __init__(self, cls, url):
103
+ def __init__(
104
+ self,
105
+ cls,
106
+ url,
107
+ timeout: int | None = None,
108
+ ):
97
109
  self.url = url
98
110
  self.cls = cls
99
111
 
@@ -101,29 +113,50 @@ class AppClient:
101
113
  signature = getattr(endpoint, "route_signature", None)
102
114
  if signature is None:
103
115
  continue
104
-
105
- setattr(self, name, EndpointClient(self.url, endpoint, signature))
116
+ endpoint_client = EndpointClient(
117
+ self.url,
118
+ endpoint,
119
+ signature,
120
+ timeout=timeout,
121
+ )
122
+ setattr(self, name, endpoint_client)
106
123
 
107
124
  @classmethod
108
125
  @contextmanager
109
126
  def connect(cls, app_cls):
110
127
  app = wrap_app(app_cls)
111
128
  info = app.spawn()
129
+ _shutdown_event = threading.Event()
130
+
131
+ def _print_logs():
132
+ while not _shutdown_event.is_set():
133
+ try:
134
+ log = info.logs.get(timeout=0.1)
135
+ except queue.Empty:
136
+ continue
137
+ print(log)
138
+
139
+ _log_printer = threading.Thread(target=_print_logs, daemon=True)
140
+ _log_printer.start()
141
+
112
142
  try:
113
143
  with httpx.Client() as client:
114
144
  retries = 100
115
- while retries:
145
+ for _ in range(retries):
116
146
  resp = client.get(info.url + "/health")
147
+
117
148
  if resp.is_success:
118
149
  break
119
- elif resp.status_code != 500:
150
+ elif resp.status_code not in (500, 404):
120
151
  resp.raise_for_status()
121
152
  time.sleep(0.1)
122
- retries -= 1
123
153
 
124
- yield cls(app_cls, info.url)
154
+ client = cls(app_cls, info.url)
155
+ yield client
125
156
  finally:
126
157
  info.stream.cancel()
158
+ _shutdown_event.set()
159
+ _log_printer.join()
127
160
 
128
161
  def health(self):
129
162
  with httpx.Client() as client:
@@ -1,3 +1,4 @@
1
1
  from __future__ import annotations
2
2
 
3
- from ._base import FalServerlessException # noqa: F401
3
+ from ._base import AppException, FalServerlessException, FieldException # noqa: F401
4
+ from ._cuda import CUDAOutOfMemoryException # noqa: F401
fal/exceptions/_base.py CHANGED
@@ -1,7 +1,50 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from dataclasses import dataclass
4
+ from typing import Sequence
5
+
3
6
 
4
7
  class FalServerlessException(Exception):
5
8
  """Base exception type for fal Serverless related flows and APIs."""
6
9
 
7
10
  pass
11
+
12
+
13
+ @dataclass
14
+ class AppException(FalServerlessException):
15
+ """
16
+ Base exception class for application-specific errors.
17
+
18
+ Attributes:
19
+ message: A descriptive message explaining the error.
20
+ status_code: The HTTP status code associated with the error.
21
+ """
22
+
23
+ message: str
24
+ status_code: int
25
+
26
+
27
+ @dataclass
28
+ class FieldException(FalServerlessException):
29
+ """Exception raised for errors related to specific fields.
30
+
31
+ Attributes:
32
+ field: The field that caused the error.
33
+ message: A descriptive message explaining the error.
34
+ status_code: The HTTP status code associated with the error. Defaults to 422
35
+ type: The type of error. Defaults to "value_error"
36
+ """
37
+
38
+ field: str
39
+ message: str
40
+ status_code: int = 422
41
+ type: str = "value_error"
42
+
43
+ def to_pydantic_format(self) -> Sequence[dict]:
44
+ return [
45
+ {
46
+ "loc": ["body", self.field],
47
+ "msg": self.message,
48
+ "type": self.type,
49
+ }
50
+ ]
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from ._base import AppException
6
+
7
+ # PyTorch error message for out of memory
8
+ _CUDA_OOM_MESSAGE = "CUDA error: out of memory"
9
+
10
+ # Special status code for CUDA out of memory errors
11
+ _CUDA_OOM_STATUS_CODE = 503
12
+
13
+
14
+ @dataclass
15
+ class CUDAOutOfMemoryException(AppException):
16
+ """Exception raised when a CUDA operation runs out of memory."""
17
+
18
+ message: str = _CUDA_OOM_MESSAGE
19
+ status_code: int = _CUDA_OOM_STATUS_CODE
20
+
21
+
22
+ # based on https://github.com/Lightning-AI/pytorch-lightning/blob/37e04d075a5532c69b8ac7457795b4345cca30cc/src/lightning/pytorch/utilities/memory.py#L49
23
+ def _is_cuda_oom_exception(exception: BaseException) -> bool:
24
+ return _is_cuda_out_of_memory(exception) or _is_cudnn_snafu(exception)
25
+
26
+
27
+ # based on https://github.com/BlackHC/toma/blob/master/toma/torch_cuda_memory.py
28
+ def _is_cuda_out_of_memory(exception: BaseException) -> bool:
29
+ return (
30
+ isinstance(exception, RuntimeError)
31
+ and len(exception.args) == 1
32
+ and "CUDA" in exception.args[0]
33
+ and "out of memory" in exception.args[0]
34
+ )
35
+
36
+
37
+ # based on https://github.com/BlackHC/toma/blob/master/toma/torch_cuda_memory.py
38
+ def _is_cudnn_snafu(exception: BaseException) -> bool:
39
+ # For/because of https://github.com/pytorch/pytorch/issues/4107
40
+ return (
41
+ isinstance(exception, RuntimeError)
42
+ and len(exception.args) == 1
43
+ and "cuDNN error: CUDNN_STATUS_NOT_SUPPORTED." in exception.args[0]
44
+ )
@@ -1,50 +1,50 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.2.3
3
+ Version: 1.3.0
4
4
  Summary: fal is an easy-to-use Serverless Python Framework
5
5
  Author: Features & Labels <support@fal.ai>
6
6
  Requires-Python: >=3.8
7
7
  Description-Content-Type: text/markdown
8
- Requires-Dist: isolate[build] <1.14.0,>=0.13.0
9
- Requires-Dist: isolate-proto ==0.5.1
10
- Requires-Dist: grpcio ==1.64.0
11
- Requires-Dist: dill ==0.3.7
12
- Requires-Dist: cloudpickle ==3.0.0
13
- Requires-Dist: typing-extensions <5,>=4.7.1
14
- Requires-Dist: click <9,>=8.1.3
15
- Requires-Dist: structlog <23,>=22.3.0
16
- Requires-Dist: opentelemetry-api <2,>=1.15.0
17
- Requires-Dist: opentelemetry-sdk <2,>=1.15.0
18
- Requires-Dist: grpc-interceptor <1,>=0.15.0
19
- Requires-Dist: colorama <1,>=0.4.6
20
- Requires-Dist: portalocker <3,>=2.7.0
21
- Requires-Dist: rich <14,>=13.3.2
8
+ Requires-Dist: isolate[build]<1.14.0,>=0.13.0
9
+ Requires-Dist: isolate-proto==0.5.1
10
+ Requires-Dist: grpcio==1.64.0
11
+ Requires-Dist: dill==0.3.7
12
+ Requires-Dist: cloudpickle==3.0.0
13
+ Requires-Dist: typing-extensions<5,>=4.7.1
14
+ Requires-Dist: click<9,>=8.1.3
15
+ Requires-Dist: structlog<23,>=22.3.0
16
+ Requires-Dist: opentelemetry-api<2,>=1.15.0
17
+ Requires-Dist: opentelemetry-sdk<2,>=1.15.0
18
+ Requires-Dist: grpc-interceptor<1,>=0.15.0
19
+ Requires-Dist: colorama<1,>=0.4.6
20
+ Requires-Dist: portalocker<3,>=2.7.0
21
+ Requires-Dist: rich<14,>=13.3.2
22
22
  Requires-Dist: rich-argparse
23
- Requires-Dist: packaging >=21.3
24
- Requires-Dist: pathspec <1,>=0.11.1
25
- Requires-Dist: pydantic !=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3
26
- Requires-Dist: fastapi <1,>=0.99.1
27
- Requires-Dist: starlette-exporter >=0.21.0
28
- Requires-Dist: httpx >=0.15.4
29
- Requires-Dist: attrs >=21.3.0
30
- Requires-Dist: python-dateutil <3,>=2.8.0
31
- Requires-Dist: types-python-dateutil <3,>=2.8.0
32
- Requires-Dist: msgpack <2,>=1.0.7
33
- Requires-Dist: websockets <13,>=12.0
34
- Requires-Dist: pillow <11,>=10.2.0
35
- Requires-Dist: pyjwt[crypto] <3,>=2.8.0
36
- Requires-Dist: uvicorn <1,>=0.29.0
23
+ Requires-Dist: packaging>=21.3
24
+ Requires-Dist: pathspec<1,>=0.11.1
25
+ Requires-Dist: pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3
26
+ Requires-Dist: fastapi<1,>=0.99.1
27
+ Requires-Dist: starlette-exporter>=0.21.0
28
+ Requires-Dist: httpx>=0.15.4
29
+ Requires-Dist: attrs>=21.3.0
30
+ Requires-Dist: python-dateutil<3,>=2.8.0
31
+ Requires-Dist: types-python-dateutil<3,>=2.8.0
32
+ Requires-Dist: msgpack<2,>=1.0.7
33
+ Requires-Dist: websockets<13,>=12.0
34
+ Requires-Dist: pillow<11,>=10.2.0
35
+ Requires-Dist: pyjwt[crypto]<3,>=2.8.0
36
+ Requires-Dist: uvicorn<1,>=0.29.0
37
37
  Requires-Dist: cookiecutter
38
- Requires-Dist: importlib-metadata >=4.4 ; python_version < "3.10"
38
+ Requires-Dist: importlib-metadata>=4.4; python_version < "3.10"
39
39
  Provides-Extra: dev
40
- Requires-Dist: fal[test] ; extra == 'dev'
41
- Requires-Dist: openapi-python-client <1,>=0.14.1 ; extra == 'dev'
40
+ Requires-Dist: fal[test]; extra == "dev"
41
+ Requires-Dist: openapi-python-client<1,>=0.14.1; extra == "dev"
42
42
  Provides-Extra: test
43
- Requires-Dist: pytest <8 ; extra == 'test'
44
- Requires-Dist: pytest-asyncio ; extra == 'test'
45
- Requires-Dist: pytest-xdist ; extra == 'test'
46
- Requires-Dist: flaky ; extra == 'test'
47
- Requires-Dist: boto3 ; extra == 'test'
43
+ Requires-Dist: pytest<8; extra == "test"
44
+ Requires-Dist: pytest-asyncio; extra == "test"
45
+ Requires-Dist: pytest-xdist; extra == "test"
46
+ Requires-Dist: flaky; extra == "test"
47
+ Requires-Dist: boto3; extra == "test"
48
48
 
49
49
  [![PyPI](https://img.shields.io/pypi/v/fal.svg?logo=PyPI)](https://pypi.org/project/fal)
50
50
  [![Tests](https://img.shields.io/github/actions/workflow/status/fal-ai/fal/integration_tests.yaml?label=Tests)](https://github.com/fal-ai/fal/actions)
@@ -1,10 +1,10 @@
1
1
  fal/__init__.py,sha256=wXs1G0gSc7ZK60-bHe-B2m0l_sA6TrFk4BxY0tMoLe8,784
2
2
  fal/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
3
- fal/_fal_version.py,sha256=VUBVzEBuW57s195P22MF-rLGH7GWx3G_z5nV-l_8MBE,411
3
+ fal/_fal_version.py,sha256=HGwtpza1HCPtlyqElUvIyH97K44TO13CYiYVZNezQ1M,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=LAPl5Hf6ZWzEjv4lFUtsisWgrnXH_qNUHdJrEHT_A5Y,40602
7
- fal/app.py,sha256=9HJGu_64ArtW8W91BC0U4Etr2gA31LXaHgR6HzoOops,15903
6
+ fal/api.py,sha256=bOCxmOQSbdcR6h6VLPEuvsD4i0j_Mod6E2UNP07cAQo,41893
7
+ fal/app.py,sha256=PGx-6Zr4evqe4Fzs4g4-MxKnaOp_7hW5G7vU1PpPvkI,16800
8
8
  fal/apps.py,sha256=FrKmaAUo8U9vE_fcva0GQvk4sCrzaTEr62lGtu3Ld5M,6825
9
9
  fal/container.py,sha256=V7riyyq8AZGwEX9QaqRQDZyDN_bUKeRKV1OOZArXjL0,622
10
10
  fal/flags.py,sha256=oWN_eidSUOcE9wdPK_77si3A1fpgOC0UEERPsvNLIMc,842
@@ -32,8 +32,9 @@ fal/cli/secrets.py,sha256=740msFm7d41HruudlcfqUXlFl53N-WmChsQP9B9M9Po,2572
32
32
  fal/console/__init__.py,sha256=ernZ4bzvvliQh5SmrEqQ7lA5eVcbw6Ra2jalKtA7dxg,132
33
33
  fal/console/icons.py,sha256=De9MfFaSkO2Lqfne13n3PrYfTXJVIzYZVqYn5BWsdrA,108
34
34
  fal/console/ux.py,sha256=KMQs3UHQvVHDxDQQqlot-WskVKoMQXOE3jiVkkfmIMY,356
35
- fal/exceptions/__init__.py,sha256=x3fp97qMr5zCQJghMq6k2ESXWSrkWumO1BZebh3pWsI,92
36
- fal/exceptions/_base.py,sha256=oF2XfitbiDGObmSF1IX50uAdV8IUvOfR-YsGmMQSE0A,161
35
+ fal/exceptions/__init__.py,sha256=5b2rFnvLf2Q4fPGedcYN_JVLv-j6iOy5DNG7jB6FNUk,180
36
+ fal/exceptions/_base.py,sha256=zFiiRceThgdnR8DEYtzpkq-IDO_D0EHGLd0oZjX5Gqk,1256
37
+ fal/exceptions/_cuda.py,sha256=q5EPFYEb7Iyw03cHrQlRHnH5xOvjwTwQdM6a9N3GB8k,1494
37
38
  fal/exceptions/auth.py,sha256=gxRago5coI__vSIcdcsqhhq1lRPkvCnwPAueIaXTAdw,329
38
39
  fal/logging/__init__.py,sha256=snqprf7-sKw6oAATS_Yxklf-a3XhLg0vIHICPwLp6TM,1583
39
40
  fal/logging/isolate.py,sha256=jJSgDHkFg4sB0xElYSqCYF6IAxy6jEgSfjwFuKJIZbA,2305
@@ -122,8 +123,8 @@ openapi_fal_rest/models/workflow_node_type.py,sha256=-FzyeY2bxcNmizKbJI8joG7byRi
122
123
  openapi_fal_rest/models/workflow_schema.py,sha256=4K5gsv9u9pxx2ItkffoyHeNjBBYf6ur5bN4m_zePZNY,2019
123
124
  openapi_fal_rest/models/workflow_schema_input.py,sha256=2OkOXWHTNsCXHWS6EGDFzcJKkW5FIap-2gfO233EvZQ,1191
124
125
  openapi_fal_rest/models/workflow_schema_output.py,sha256=EblwSPAGfWfYVWw_WSSaBzQVju296is9o28rMBAd0mc,1196
125
- fal-1.2.3.dist-info/METADATA,sha256=tUsGGaLLvqfYpEePodE0fuc5nm_XSoYE7AWTrci2nEY,3805
126
- fal-1.2.3.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
127
- fal-1.2.3.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
128
- fal-1.2.3.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
129
- fal-1.2.3.dist-info/RECORD,,
126
+ fal-1.3.0.dist-info/METADATA,sha256=sP1Mdzh8ciPGfXFrY_GzmRm8IH0Ew6gv9IUjBsHWCe4,3766
127
+ fal-1.3.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
128
+ fal-1.3.0.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
129
+ fal-1.3.0.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
130
+ fal-1.3.0.dist-info/RECORD,,
File without changes