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.

Files changed (160) hide show
  1. {fal-1.2.3 → fal-1.3.0}/PKG-INFO +1 -1
  2. {fal-1.2.3 → fal-1.3.0}/fal.egg-info/PKG-INFO +1 -1
  3. {fal-1.2.3 → fal-1.3.0}/fal.egg-info/SOURCES.txt +1 -0
  4. {fal-1.2.3 → fal-1.3.0}/src/fal/_fal_version.py +2 -2
  5. {fal-1.2.3 → fal-1.3.0}/src/fal/api.py +37 -5
  6. {fal-1.2.3 → fal-1.3.0}/src/fal/app.py +42 -9
  7. fal-1.3.0/src/fal/exceptions/__init__.py +4 -0
  8. fal-1.3.0/src/fal/exceptions/_base.py +50 -0
  9. fal-1.3.0/src/fal/exceptions/_cuda.py +44 -0
  10. {fal-1.2.3 → fal-1.3.0}/tests/test_apps.py +45 -12
  11. fal-1.2.3/src/fal/exceptions/__init__.py +0 -3
  12. fal-1.2.3/src/fal/exceptions/_base.py +0 -7
  13. {fal-1.2.3 → fal-1.3.0}/.gitignore +0 -0
  14. {fal-1.2.3 → fal-1.3.0}/README.md +0 -0
  15. {fal-1.2.3 → fal-1.3.0}/fal.egg-info/dependency_links.txt +0 -0
  16. {fal-1.2.3 → fal-1.3.0}/fal.egg-info/entry_points.txt +0 -0
  17. {fal-1.2.3 → fal-1.3.0}/fal.egg-info/requires.txt +0 -0
  18. {fal-1.2.3 → fal-1.3.0}/fal.egg-info/top_level.txt +0 -0
  19. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/README.md +0 -0
  20. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  21. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  22. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  23. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  24. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  25. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  26. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  27. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  28. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  29. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  30. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  31. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  32. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  33. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  34. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  35. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  36. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  37. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  38. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  39. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  40. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  41. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  42. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  43. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  44. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  45. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  46. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  47. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  48. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  49. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  50. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  51. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  52. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  53. {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
  54. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  55. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  56. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  57. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  58. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  59. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  60. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  61. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  62. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  63. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  64. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  65. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  66. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  67. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  68. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  69. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  70. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  71. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  72. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  73. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  74. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  75. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  76. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  77. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  78. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  79. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  80. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  81. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  82. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  83. {fal-1.2.3 → fal-1.3.0}/openapi-fal-rest/pyproject.toml +0 -0
  84. {fal-1.2.3 → fal-1.3.0}/openapi_rest.config.yaml +0 -0
  85. {fal-1.2.3 → fal-1.3.0}/pyproject.toml +0 -0
  86. {fal-1.2.3 → fal-1.3.0}/setup.cfg +0 -0
  87. {fal-1.2.3 → fal-1.3.0}/src/fal/__init__.py +0 -0
  88. {fal-1.2.3 → fal-1.3.0}/src/fal/__main__.py +0 -0
  89. {fal-1.2.3 → fal-1.3.0}/src/fal/_serialization.py +0 -0
  90. {fal-1.2.3 → fal-1.3.0}/src/fal/_version.py +0 -0
  91. {fal-1.2.3 → fal-1.3.0}/src/fal/apps.py +0 -0
  92. {fal-1.2.3 → fal-1.3.0}/src/fal/auth/__init__.py +0 -0
  93. {fal-1.2.3 → fal-1.3.0}/src/fal/auth/auth0.py +0 -0
  94. {fal-1.2.3 → fal-1.3.0}/src/fal/auth/local.py +0 -0
  95. {fal-1.2.3 → fal-1.3.0}/src/fal/cli/__init__.py +0 -0
  96. {fal-1.2.3 → fal-1.3.0}/src/fal/cli/apps.py +0 -0
  97. {fal-1.2.3 → fal-1.3.0}/src/fal/cli/auth.py +0 -0
  98. {fal-1.2.3 → fal-1.3.0}/src/fal/cli/create.py +0 -0
  99. {fal-1.2.3 → fal-1.3.0}/src/fal/cli/debug.py +0 -0
  100. {fal-1.2.3 → fal-1.3.0}/src/fal/cli/deploy.py +0 -0
  101. {fal-1.2.3 → fal-1.3.0}/src/fal/cli/doctor.py +0 -0
  102. {fal-1.2.3 → fal-1.3.0}/src/fal/cli/keys.py +0 -0
  103. {fal-1.2.3 → fal-1.3.0}/src/fal/cli/main.py +0 -0
  104. {fal-1.2.3 → fal-1.3.0}/src/fal/cli/parser.py +0 -0
  105. {fal-1.2.3 → fal-1.3.0}/src/fal/cli/run.py +0 -0
  106. {fal-1.2.3 → fal-1.3.0}/src/fal/cli/secrets.py +0 -0
  107. {fal-1.2.3 → fal-1.3.0}/src/fal/console/__init__.py +0 -0
  108. {fal-1.2.3 → fal-1.3.0}/src/fal/console/icons.py +0 -0
  109. {fal-1.2.3 → fal-1.3.0}/src/fal/console/ux.py +0 -0
  110. {fal-1.2.3 → fal-1.3.0}/src/fal/container.py +0 -0
  111. {fal-1.2.3 → fal-1.3.0}/src/fal/exceptions/auth.py +0 -0
  112. {fal-1.2.3 → fal-1.3.0}/src/fal/flags.py +0 -0
  113. {fal-1.2.3 → fal-1.3.0}/src/fal/logging/__init__.py +0 -0
  114. {fal-1.2.3 → fal-1.3.0}/src/fal/logging/isolate.py +0 -0
  115. {fal-1.2.3 → fal-1.3.0}/src/fal/logging/style.py +0 -0
  116. {fal-1.2.3 → fal-1.3.0}/src/fal/logging/trace.py +0 -0
  117. {fal-1.2.3 → fal-1.3.0}/src/fal/logging/user.py +0 -0
  118. {fal-1.2.3 → fal-1.3.0}/src/fal/py.typed +0 -0
  119. {fal-1.2.3 → fal-1.3.0}/src/fal/rest_client.py +0 -0
  120. {fal-1.2.3 → fal-1.3.0}/src/fal/sdk.py +0 -0
  121. {fal-1.2.3 → fal-1.3.0}/src/fal/sync.py +0 -0
  122. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/__init__.py +0 -0
  123. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/exceptions.py +0 -0
  124. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/file/__init__.py +0 -0
  125. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/file/file.py +0 -0
  126. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/file/providers/fal.py +0 -0
  127. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/file/providers/gcp.py +0 -0
  128. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/file/providers/r2.py +0 -0
  129. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/file/types.py +0 -0
  130. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/image/__init__.py +0 -0
  131. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/image/image.py +0 -0
  132. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
  133. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
  134. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
  135. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
  136. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
  137. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/image/safety_checker.py +0 -0
  138. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/optimize.py +0 -0
  139. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/utils/__init__.py +0 -0
  140. {fal-1.2.3 → fal-1.3.0}/src/fal/toolkit/utils/download_utils.py +0 -0
  141. {fal-1.2.3 → fal-1.3.0}/src/fal/utils.py +0 -0
  142. {fal-1.2.3 → fal-1.3.0}/src/fal/workflows.py +0 -0
  143. {fal-1.2.3 → fal-1.3.0}/tests/__init__.py +0 -0
  144. {fal-1.2.3 → fal-1.3.0}/tests/cli/__init__.py +0 -0
  145. {fal-1.2.3 → fal-1.3.0}/tests/cli/test_apps.py +0 -0
  146. {fal-1.2.3 → fal-1.3.0}/tests/cli/test_auth.py +0 -0
  147. {fal-1.2.3 → fal-1.3.0}/tests/cli/test_deploy.py +0 -0
  148. {fal-1.2.3 → fal-1.3.0}/tests/cli/test_keys.py +0 -0
  149. {fal-1.2.3 → fal-1.3.0}/tests/cli/test_run.py +0 -0
  150. {fal-1.2.3 → fal-1.3.0}/tests/cli/test_secrets.py +0 -0
  151. {fal-1.2.3 → fal-1.3.0}/tests/conftest.py +0 -0
  152. {fal-1.2.3 → fal-1.3.0}/tests/integration_test.py +0 -0
  153. {fal-1.2.3 → fal-1.3.0}/tests/mainify_package/__init__.py +0 -0
  154. {fal-1.2.3 → fal-1.3.0}/tests/mainify_package/impl.py +0 -0
  155. {fal-1.2.3 → fal-1.3.0}/tests/mainify_package/utils.py +0 -0
  156. {fal-1.2.3 → fal-1.3.0}/tests/mainify_target.py +0 -0
  157. {fal-1.2.3 → fal-1.3.0}/tests/test_stability.py +0 -0
  158. {fal-1.2.3 → fal-1.3.0}/tests/toolkit/file_test.py +0 -0
  159. {fal-1.2.3 → fal-1.3.0}/tests/toolkit/image_test.py +0 -0
  160. {fal-1.2.3 → fal-1.3.0}/tools/demo_script.py +0 -0
@@ -1,6 +1,6 @@
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
@@ -1,6 +1,6 @@
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
@@ -109,6 +109,7 @@ src/fal/console/icons.py
109
109
  src/fal/console/ux.py
110
110
  src/fal/exceptions/__init__.py
111
111
  src/fal/exceptions/_base.py
112
+ src/fal/exceptions/_cuda.py
112
113
  src/fal/exceptions/auth.py
113
114
  src/fal/logging/__init__.py
114
115
  src/fal/logging/isolate.py
@@ -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)
@@ -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()
@@ -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:
@@ -0,0 +1,4 @@
1
+ from __future__ import annotations
2
+
3
+ from ._base import AppException, FalServerlessException, FieldException # noqa: F401
4
+ from ._cuda import CUDAOutOfMemoryException # noqa: F401
@@ -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 reset(self) -> Output:
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
- app = fal.wrap_app(ExceptionApp)
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: str):
608
+ def test_traceback_logs(test_exception_app: AppClient):
596
609
  date = datetime.utcnow().isoformat()
610
+
597
611
  with pytest.raises(HTTPStatusError):
598
- apps.run(test_exception_app, arguments={}, path="/fail")
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&limit=10&since={date}"
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
@@ -1,3 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from ._base import FalServerlessException # noqa: F401
@@ -1,7 +0,0 @@
1
- from __future__ import annotations
2
-
3
-
4
- class FalServerlessException(Exception):
5
- """Base exception type for fal Serverless related flows and APIs."""
6
-
7
- pass
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