fal 1.2.4__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.4'
16
- __version_tuple__ = version_tuple = (1, 2, 4)
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
@@ -142,15 +142,14 @@ class AppClient:
142
142
  try:
143
143
  with httpx.Client() as client:
144
144
  retries = 100
145
- while retries:
145
+ for _ in range(retries):
146
146
  resp = client.get(info.url + "/health")
147
147
 
148
148
  if resp.is_success:
149
149
  break
150
- elif resp.status_code != 500:
150
+ elif resp.status_code not in (500, 404):
151
151
  resp.raise_for_status()
152
152
  time.sleep(0.1)
153
- retries -= 1
154
153
 
155
154
  client = cls(app_cls, info.url)
156
155
  yield 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.4
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=DFpsAdSrahcTSWRccxC8nEJpgcmby0LdmRoAddZy2zA,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=BM4lk6741Z0DKr3bYLLhEvRBuDkqZqo883LvPXjgOPQ,16812
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.4.dist-info/METADATA,sha256=yCcxwr4BEx0DlAY2xi7FQzd_Qi-IChNr-aQ9QstwzUg,3805
126
- fal-1.2.4.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
127
- fal-1.2.4.dist-info/entry_points.txt,sha256=32zwTUC1U1E7nSTIGCoANQOQ3I7-qHG5wI6gsVz5pNU,37
128
- fal-1.2.4.dist-info/top_level.txt,sha256=r257X1L57oJL8_lM0tRrfGuXFwm66i1huwQygbpLmHw,21
129
- fal-1.2.4.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