fal 1.2.3__tar.gz → 1.3.0__tar.gz
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-1.2.3 → fal-1.3.0}/PKG-INFO +1 -1
- {fal-1.2.3 → fal-1.3.0}/fal.egg-info/PKG-INFO +1 -1
- {fal-1.2.3 → fal-1.3.0}/fal.egg-info/SOURCES.txt +1 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/_fal_version.py +2 -2
- {fal-1.2.3 → fal-1.3.0}/src/fal/api.py +37 -5
- {fal-1.2.3 → fal-1.3.0}/src/fal/app.py +42 -9
- fal-1.3.0/src/fal/exceptions/__init__.py +4 -0
- fal-1.3.0/src/fal/exceptions/_base.py +50 -0
- fal-1.3.0/src/fal/exceptions/_cuda.py +44 -0
- {fal-1.2.3 → fal-1.3.0}/tests/test_apps.py +45 -12
- fal-1.2.3/src/fal/exceptions/__init__.py +0 -3
- fal-1.2.3/src/fal/exceptions/_base.py +0 -7
- {fal-1.2.3 → fal-1.3.0}/.gitignore +0 -0
- {fal-1.2.3 → fal-1.3.0}/README.md +0 -0
- {fal-1.2.3 → fal-1.3.0}/fal.egg-info/dependency_links.txt +0 -0
- {fal-1.2.3 → fal-1.3.0}/fal.egg-info/entry_points.txt +0 -0
- {fal-1.2.3 → fal-1.3.0}/fal.egg-info/requires.txt +0 -0
- {fal-1.2.3 → fal-1.3.0}/fal.egg-info/top_level.txt +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/README.md +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/pyproject.toml +0 -0
- {fal-1.2.3 → fal-1.3.0}/openapi_rest.config.yaml +0 -0
- {fal-1.2.3 → fal-1.3.0}/pyproject.toml +0 -0
- {fal-1.2.3 → fal-1.3.0}/setup.cfg +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/__main__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/_serialization.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/_version.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/apps.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/auth/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/auth/auth0.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/auth/local.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/cli/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/cli/apps.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/cli/auth.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/cli/create.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/cli/debug.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/cli/deploy.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/cli/doctor.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/cli/keys.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/cli/main.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/cli/parser.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/cli/run.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/cli/secrets.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/console/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/console/icons.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/console/ux.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/container.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/exceptions/auth.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/flags.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/logging/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/logging/isolate.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/logging/style.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/logging/trace.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/logging/user.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/py.typed +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/rest_client.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/sdk.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/sync.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/exceptions.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/file/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/file/file.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/file/providers/fal.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/file/providers/gcp.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/file/providers/r2.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/file/types.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/image/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/image/image.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/image/safety_checker.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/optimize.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/utils/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/utils/download_utils.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/utils.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/src/fal/workflows.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/cli/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/cli/test_apps.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/cli/test_auth.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/cli/test_deploy.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/cli/test_keys.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/cli/test_run.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/cli/test_secrets.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/conftest.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/integration_test.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/mainify_package/__init__.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/mainify_package/impl.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/mainify_package/utils.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/mainify_target.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/test_stability.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/toolkit/file_test.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tests/toolkit/image_test.py +0 -0
- {fal-1.2.3 → fal-1.3.0}/tools/demo_script.py +0 -0
{fal-1.2.3 → fal-1.3.0}/PKG-INFO
RENAMED
|
@@ -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
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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()
|
|
@@ -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(
|
|
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__(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Sequence
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FalServerlessException(Exception):
|
|
8
|
+
"""Base exception type for fal Serverless related flows and APIs."""
|
|
9
|
+
|
|
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
|
+
)
|
|
@@ -10,8 +10,11 @@ import fal.api as api
|
|
|
10
10
|
import httpx
|
|
11
11
|
import pytest
|
|
12
12
|
from fal import apps
|
|
13
|
+
from fal.app import AppClient
|
|
13
14
|
from fal.cli.deploy import _get_user
|
|
14
15
|
from fal.container import ContainerImage
|
|
16
|
+
from fal.exceptions import AppException, FieldException
|
|
17
|
+
from fal.exceptions._cuda import _CUDA_OOM_MESSAGE, _CUDA_OOM_STATUS_CODE
|
|
15
18
|
from fal.rest_client import REST_CLIENT
|
|
16
19
|
from fal.workflows import Workflow
|
|
17
20
|
from fastapi import WebSocket
|
|
@@ -139,9 +142,25 @@ class ExceptionApp(fal.App, keep_alive=300, max_concurrency=1):
|
|
|
139
142
|
machine_type = "XS"
|
|
140
143
|
|
|
141
144
|
@fal.endpoint("/fail")
|
|
142
|
-
def
|
|
145
|
+
def fail(self) -> Output:
|
|
143
146
|
raise Exception("this app is designed to fail!")
|
|
144
147
|
|
|
148
|
+
@fal.endpoint("/app-exception")
|
|
149
|
+
def app_exception(self) -> Output:
|
|
150
|
+
raise AppException(message="this app is designed to fail", status_code=401)
|
|
151
|
+
|
|
152
|
+
@fal.endpoint("/field-exception")
|
|
153
|
+
def field_exception(self, input: Input) -> Output:
|
|
154
|
+
raise FieldException(
|
|
155
|
+
field="rhs",
|
|
156
|
+
message="rhs must be an integer",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@fal.endpoint("/cuda-exception")
|
|
160
|
+
def cuda_exception(self) -> Output:
|
|
161
|
+
# mimicking error message from PyTorch (https://github.com/pytorch/pytorch/blob/6c65fd03942415b68040e102c44cf5109d2d851e/c10/cuda/CUDACachingAllocator.cpp#L1234C12-L1234C30)
|
|
162
|
+
raise RuntimeError("CUDA out of memory")
|
|
163
|
+
|
|
145
164
|
|
|
146
165
|
class RTInput(BaseModel):
|
|
147
166
|
prompt: str
|
|
@@ -270,14 +289,8 @@ def test_stateful_app():
|
|
|
270
289
|
@pytest.fixture(scope="module")
|
|
271
290
|
def test_exception_app():
|
|
272
291
|
# Create a temporary app, register it, and return the ID of it.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
app_revision = app.host.register(
|
|
276
|
-
func=app.func,
|
|
277
|
-
options=app.options,
|
|
278
|
-
)
|
|
279
|
-
user = _get_user()
|
|
280
|
-
yield f"{user.user_id}/{app_revision}"
|
|
292
|
+
with AppClient.connect(ExceptionApp) as client:
|
|
293
|
+
yield client
|
|
281
294
|
|
|
282
295
|
|
|
283
296
|
@pytest.fixture(scope="module")
|
|
@@ -592,10 +605,12 @@ def test_workflows(test_app: str):
|
|
|
592
605
|
assert data["result"] == 10
|
|
593
606
|
|
|
594
607
|
|
|
595
|
-
def test_traceback_logs(test_exception_app:
|
|
608
|
+
def test_traceback_logs(test_exception_app: AppClient):
|
|
596
609
|
date = datetime.utcnow().isoformat()
|
|
610
|
+
|
|
597
611
|
with pytest.raises(HTTPStatusError):
|
|
598
|
-
|
|
612
|
+
test_exception_app.fail({})
|
|
613
|
+
|
|
599
614
|
with httpx.Client(
|
|
600
615
|
base_url=REST_CLIENT.base_url,
|
|
601
616
|
headers=REST_CLIENT.get_headers(),
|
|
@@ -604,7 +619,7 @@ def test_traceback_logs(test_exception_app: str):
|
|
|
604
619
|
# Give some time for logs to propagate through the logging subsystem.
|
|
605
620
|
time.sleep(5)
|
|
606
621
|
response = client.get(
|
|
607
|
-
REST_CLIENT.base_url + f"/logs/?traceback=true&
|
|
622
|
+
REST_CLIENT.base_url + f"/logs/?traceback=true&since={date}"
|
|
608
623
|
)
|
|
609
624
|
for log in json.loads(response.text):
|
|
610
625
|
assert log["message"].count("\n") > 1, "Logs are multi-line"
|
|
@@ -612,3 +627,21 @@ def test_traceback_logs(test_exception_app: str):
|
|
|
612
627
|
assert (
|
|
613
628
|
"this app is designed to fail" in log["message"]
|
|
614
629
|
), "Logs contain the traceback message"
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def test_app_exceptions(test_exception_app: AppClient):
|
|
633
|
+
with pytest.raises(HTTPStatusError) as app_exc:
|
|
634
|
+
test_exception_app.app_exception({})
|
|
635
|
+
|
|
636
|
+
assert app_exc.value.response.status_code == 401
|
|
637
|
+
|
|
638
|
+
with pytest.raises(HTTPStatusError) as field_exc:
|
|
639
|
+
test_exception_app.field_exception({"lhs": 1, "rhs": "2"})
|
|
640
|
+
|
|
641
|
+
assert field_exc.value.response.status_code == 422
|
|
642
|
+
|
|
643
|
+
with pytest.raises(HTTPStatusError) as cuda_exc:
|
|
644
|
+
test_exception_app.cuda_exception({})
|
|
645
|
+
|
|
646
|
+
assert cuda_exc.value.response.status_code == _CUDA_OOM_STATUS_CODE
|
|
647
|
+
assert cuda_exc.value.response.json()["detail"] == _CUDA_OOM_MESSAGE
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py
RENAMED
|
File without changes
|
{fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py
RENAMED
|
File without changes
|
|
File without changes
|
{fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py
RENAMED
|
File without changes
|
|
File without changes
|
{fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py
RENAMED
|
File without changes
|
|
File without changes
|
{fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|