fal 1.5.3__tar.gz → 1.5.4__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 (166) hide show
  1. {fal-1.5.3/fal.egg-info → fal-1.5.4}/PKG-INFO +2 -1
  2. {fal-1.5.3 → fal-1.5.4/fal.egg-info}/PKG-INFO +2 -1
  3. {fal-1.5.3 → fal-1.5.4}/fal.egg-info/requires.txt +1 -0
  4. {fal-1.5.3 → fal-1.5.4}/pyproject.toml +1 -0
  5. {fal-1.5.3 → fal-1.5.4}/src/fal/_fal_version.py +2 -2
  6. {fal-1.5.3 → fal-1.5.4}/src/fal/api.py +1 -0
  7. {fal-1.5.3 → fal-1.5.4}/src/fal/app.py +90 -5
  8. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/file/providers/fal.py +200 -16
  9. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/file/providers/gcp.py +6 -1
  10. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/file/providers/r2.py +6 -1
  11. {fal-1.5.3 → fal-1.5.4}/tests/test_apps.py +9 -9
  12. {fal-1.5.3 → fal-1.5.4}/.gitignore +0 -0
  13. {fal-1.5.3 → fal-1.5.4}/Makefile +0 -0
  14. {fal-1.5.3 → fal-1.5.4}/README.md +0 -0
  15. {fal-1.5.3 → fal-1.5.4}/docs/conf.py +0 -0
  16. {fal-1.5.3 → fal-1.5.4}/docs/index.rst +0 -0
  17. {fal-1.5.3 → fal-1.5.4}/fal.egg-info/SOURCES.txt +0 -0
  18. {fal-1.5.3 → fal-1.5.4}/fal.egg-info/dependency_links.txt +0 -0
  19. {fal-1.5.3 → fal-1.5.4}/fal.egg-info/entry_points.txt +0 -0
  20. {fal-1.5.3 → fal-1.5.4}/fal.egg-info/top_level.txt +0 -0
  21. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/README.md +0 -0
  22. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  23. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  24. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  25. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  26. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  27. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  28. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  29. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  30. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  31. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  32. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  33. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  34. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  35. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  36. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  37. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  38. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  39. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  40. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  41. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  42. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  43. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  44. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  45. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  46. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  47. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  48. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  49. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  50. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  51. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  52. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  53. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  54. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  55. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
  56. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  57. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  58. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  59. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  60. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  61. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  62. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  63. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  64. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  65. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  66. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  67. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  68. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  69. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  70. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  71. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  72. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  73. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  74. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  75. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  76. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  77. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  78. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  79. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  80. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  81. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  82. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  83. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  84. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  85. {fal-1.5.3 → fal-1.5.4}/openapi-fal-rest/pyproject.toml +0 -0
  86. {fal-1.5.3 → fal-1.5.4}/openapi_rest.config.yaml +0 -0
  87. {fal-1.5.3 → fal-1.5.4}/setup.cfg +0 -0
  88. {fal-1.5.3 → fal-1.5.4}/src/fal/__init__.py +0 -0
  89. {fal-1.5.3 → fal-1.5.4}/src/fal/__main__.py +0 -0
  90. {fal-1.5.3 → fal-1.5.4}/src/fal/_serialization.py +0 -0
  91. {fal-1.5.3 → fal-1.5.4}/src/fal/_version.py +0 -0
  92. {fal-1.5.3 → fal-1.5.4}/src/fal/apps.py +0 -0
  93. {fal-1.5.3 → fal-1.5.4}/src/fal/auth/__init__.py +0 -0
  94. {fal-1.5.3 → fal-1.5.4}/src/fal/auth/auth0.py +0 -0
  95. {fal-1.5.3 → fal-1.5.4}/src/fal/auth/local.py +0 -0
  96. {fal-1.5.3 → fal-1.5.4}/src/fal/cli/__init__.py +0 -0
  97. {fal-1.5.3 → fal-1.5.4}/src/fal/cli/_utils.py +0 -0
  98. {fal-1.5.3 → fal-1.5.4}/src/fal/cli/apps.py +0 -0
  99. {fal-1.5.3 → fal-1.5.4}/src/fal/cli/auth.py +0 -0
  100. {fal-1.5.3 → fal-1.5.4}/src/fal/cli/create.py +0 -0
  101. {fal-1.5.3 → fal-1.5.4}/src/fal/cli/debug.py +0 -0
  102. {fal-1.5.3 → fal-1.5.4}/src/fal/cli/deploy.py +0 -0
  103. {fal-1.5.3 → fal-1.5.4}/src/fal/cli/doctor.py +0 -0
  104. {fal-1.5.3 → fal-1.5.4}/src/fal/cli/keys.py +0 -0
  105. {fal-1.5.3 → fal-1.5.4}/src/fal/cli/main.py +0 -0
  106. {fal-1.5.3 → fal-1.5.4}/src/fal/cli/parser.py +0 -0
  107. {fal-1.5.3 → fal-1.5.4}/src/fal/cli/run.py +0 -0
  108. {fal-1.5.3 → fal-1.5.4}/src/fal/cli/secrets.py +0 -0
  109. {fal-1.5.3 → fal-1.5.4}/src/fal/console/__init__.py +0 -0
  110. {fal-1.5.3 → fal-1.5.4}/src/fal/console/icons.py +0 -0
  111. {fal-1.5.3 → fal-1.5.4}/src/fal/console/ux.py +0 -0
  112. {fal-1.5.3 → fal-1.5.4}/src/fal/container.py +0 -0
  113. {fal-1.5.3 → fal-1.5.4}/src/fal/exceptions/__init__.py +0 -0
  114. {fal-1.5.3 → fal-1.5.4}/src/fal/exceptions/_base.py +0 -0
  115. {fal-1.5.3 → fal-1.5.4}/src/fal/exceptions/_cuda.py +0 -0
  116. {fal-1.5.3 → fal-1.5.4}/src/fal/exceptions/auth.py +0 -0
  117. {fal-1.5.3 → fal-1.5.4}/src/fal/files.py +0 -0
  118. {fal-1.5.3 → fal-1.5.4}/src/fal/flags.py +0 -0
  119. {fal-1.5.3 → fal-1.5.4}/src/fal/logging/__init__.py +0 -0
  120. {fal-1.5.3 → fal-1.5.4}/src/fal/logging/isolate.py +0 -0
  121. {fal-1.5.3 → fal-1.5.4}/src/fal/logging/style.py +0 -0
  122. {fal-1.5.3 → fal-1.5.4}/src/fal/logging/trace.py +0 -0
  123. {fal-1.5.3 → fal-1.5.4}/src/fal/logging/user.py +0 -0
  124. {fal-1.5.3 → fal-1.5.4}/src/fal/py.typed +0 -0
  125. {fal-1.5.3 → fal-1.5.4}/src/fal/rest_client.py +0 -0
  126. {fal-1.5.3 → fal-1.5.4}/src/fal/sdk.py +0 -0
  127. {fal-1.5.3 → fal-1.5.4}/src/fal/sync.py +0 -0
  128. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/__init__.py +0 -0
  129. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/exceptions.py +0 -0
  130. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/file/__init__.py +0 -0
  131. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/file/file.py +0 -0
  132. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/file/types.py +0 -0
  133. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/image/__init__.py +0 -0
  134. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/image/image.py +0 -0
  135. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
  136. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
  137. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
  138. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
  139. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
  140. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/image/safety_checker.py +0 -0
  141. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/optimize.py +0 -0
  142. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/utils/__init__.py +0 -0
  143. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/utils/download_utils.py +0 -0
  144. {fal-1.5.3 → fal-1.5.4}/src/fal/toolkit/utils/retry.py +0 -0
  145. {fal-1.5.3 → fal-1.5.4}/src/fal/utils.py +0 -0
  146. {fal-1.5.3 → fal-1.5.4}/src/fal/workflows.py +0 -0
  147. {fal-1.5.3 → fal-1.5.4}/tests/__init__.py +0 -0
  148. {fal-1.5.3 → fal-1.5.4}/tests/assets/cat.png +0 -0
  149. {fal-1.5.3 → fal-1.5.4}/tests/cli/__init__.py +0 -0
  150. {fal-1.5.3 → fal-1.5.4}/tests/cli/test_apps.py +0 -0
  151. {fal-1.5.3 → fal-1.5.4}/tests/cli/test_auth.py +0 -0
  152. {fal-1.5.3 → fal-1.5.4}/tests/cli/test_deploy.py +0 -0
  153. {fal-1.5.3 → fal-1.5.4}/tests/cli/test_keys.py +0 -0
  154. {fal-1.5.3 → fal-1.5.4}/tests/cli/test_run.py +0 -0
  155. {fal-1.5.3 → fal-1.5.4}/tests/cli/test_secrets.py +0 -0
  156. {fal-1.5.3 → fal-1.5.4}/tests/conftest.py +0 -0
  157. {fal-1.5.3 → fal-1.5.4}/tests/integration_test.py +0 -0
  158. {fal-1.5.3 → fal-1.5.4}/tests/mainify_package/__init__.py +0 -0
  159. {fal-1.5.3 → fal-1.5.4}/tests/mainify_package/impl.py +0 -0
  160. {fal-1.5.3 → fal-1.5.4}/tests/mainify_package/utils.py +0 -0
  161. {fal-1.5.3 → fal-1.5.4}/tests/mainify_target.py +0 -0
  162. {fal-1.5.3 → fal-1.5.4}/tests/test_stability.py +0 -0
  163. {fal-1.5.3 → fal-1.5.4}/tests/toolkit/file_test.py +0 -0
  164. {fal-1.5.3 → fal-1.5.4}/tests/toolkit/image_test.py +0 -0
  165. {fal-1.5.3 → fal-1.5.4}/tests/toolkit/utils/retry.py +0 -0
  166. {fal-1.5.3 → fal-1.5.4}/tools/demo_script.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.5.3
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.5.3
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
@@ -16,6 +16,7 @@ rich_argparse
16
16
  packaging>=21.3
17
17
  pathspec<1,>=0.11.1
18
18
  pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3
19
+ structlog>=22.0
19
20
  fastapi<1,>=0.99.1
20
21
  starlette-exporter>=0.21.0
21
22
  httpx>=0.15.4
@@ -41,6 +41,7 @@ dependencies = [
41
41
  "pathspec>=0.11.1,<1",
42
42
  "pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3",
43
43
  # serve=True dependencies
44
+ "structlog>=22.0",
44
45
  "fastapi>=0.99.1,<1",
45
46
  "starlette-exporter>=0.21.0",
46
47
  # rest-api-client dependencies
@@ -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.3'
16
- __version_tuple__ = version_tuple = (1, 5, 3)
15
+ __version__ = version = '1.5.4'
16
+ __version_tuple__ = version_tuple = (1, 5, 4)
@@ -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
 
@@ -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,21 +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
26
  from fal.toolkit.file import get_lifecycle_preference
23
27
  from fal.toolkit.file.providers.fal import GLOBAL_LIFECYCLE_PREFERENCE
24
28
 
25
29
  REALTIME_APP_REQUIREMENTS = ["websockets", "msgpack"]
30
+ REQUEST_ID_KEY = "x-fal-request-id"
26
31
 
27
32
  EndpointT = TypeVar("EndpointT", bound=Callable[..., Any])
28
33
  logger = get_logger(__name__)
@@ -35,6 +40,48 @@ async def _call_any_fn(fn, *args, **kwargs):
35
40
  return fn(*args, **kwargs)
36
41
 
37
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
+
38
85
  def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
39
86
  include_modules_from(cls)
40
87
 
@@ -76,6 +123,12 @@ def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
76
123
  return fn
77
124
 
78
125
 
126
+ @dataclass
127
+ class AppClientError(FalServerlessException):
128
+ message: str
129
+ status_code: int
130
+
131
+
79
132
  class EndpointClient:
80
133
  def __init__(self, url, endpoint, signature, timeout: int | None = None):
81
134
  self.url = url
@@ -88,12 +141,19 @@ class EndpointClient:
88
141
 
89
142
  def __call__(self, data):
90
143
  with httpx.Client() as client:
144
+ url = self.url + self.signature.path
91
145
  resp = client.post(
92
146
  self.url + self.signature.path,
93
147
  json=data.dict() if hasattr(data, "dict") else dict(data),
94
148
  timeout=self.timeout,
95
149
  )
96
- 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
+ )
97
157
  resp_dict = resp.json()
98
158
 
99
159
  if not self.return_type:
@@ -146,12 +206,16 @@ class AppClient:
146
206
  with httpx.Client() as client:
147
207
  retries = 100
148
208
  for _ in range(retries):
149
- resp = client.get(info.url + "/health", timeout=60)
209
+ url = info.url + "/health"
210
+ resp = client.get(url, timeout=60)
150
211
 
151
212
  if resp.is_success:
152
213
  break
153
214
  elif resp.status_code not in (500, 404):
154
- 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
+ )
155
219
  time.sleep(0.1)
156
220
 
157
221
  client = cls(app_cls, info.url)
@@ -192,6 +256,8 @@ class App(fal.api.BaseServable):
192
256
  app_auth: ClassVar[Literal["private", "public", "shared"]] = "private"
193
257
  request_timeout: ClassVar[int | None] = None
194
258
 
259
+ isolate_channel: async_grpc.Channel | None = None
260
+
195
261
  def __init_subclass__(cls, **kwargs):
196
262
  app_name = kwargs.pop("name", None) or _to_fal_app_name(cls.__name__)
197
263
  parent_settings = getattr(cls, "host_kwargs", {})
@@ -282,8 +348,27 @@ class App(fal.api.BaseServable):
282
348
  "Failed set a global lifecycle preference %s",
283
349
  self.__class__.__name__,
284
350
  )
351
+
285
352
  return await call_next(request)
286
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)
371
+
287
372
  @app.exception_handler(RequestCancelledException)
288
373
  async def value_error_exception_handler(
289
374
  request, exc: RequestCancelledException
@@ -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}",
@@ -12,7 +12,7 @@ import fal.api as api
12
12
  import httpx
13
13
  import pytest
14
14
  from fal import apps
15
- from fal.app import AppClient
15
+ from fal.app import AppClient, AppClientError
16
16
  from fal.cli.deploy import _get_user
17
17
  from fal.container import ContainerImage
18
18
  from fal.exceptions import AppException, FieldException, RequestCancelledException
@@ -692,7 +692,7 @@ def test_workflows(test_app: str):
692
692
  def test_traceback_logs(test_exception_app: AppClient):
693
693
  date = datetime.utcnow().isoformat()
694
694
 
695
- with pytest.raises(HTTPStatusError):
695
+ with pytest.raises(AppClientError):
696
696
  test_exception_app.fail({})
697
697
 
698
698
  with httpx.Client(
@@ -714,18 +714,18 @@ def test_traceback_logs(test_exception_app: AppClient):
714
714
 
715
715
 
716
716
  def test_app_exceptions(test_exception_app: AppClient):
717
- with pytest.raises(HTTPStatusError) as app_exc:
717
+ with pytest.raises(AppClientError) as app_exc:
718
718
  test_exception_app.app_exception({})
719
719
 
720
- assert app_exc.value.response.status_code == 401
720
+ assert app_exc.value.status_code == 401
721
721
 
722
- with pytest.raises(HTTPStatusError) as field_exc:
722
+ with pytest.raises(AppClientError) as field_exc:
723
723
  test_exception_app.field_exception({"lhs": 1, "rhs": "2"})
724
724
 
725
- assert field_exc.value.response.status_code == 422
725
+ assert field_exc.value.status_code == 422
726
726
 
727
- with pytest.raises(HTTPStatusError) as cuda_exc:
727
+ with pytest.raises(AppClientError) as cuda_exc:
728
728
  test_exception_app.cuda_exception({})
729
729
 
730
- assert cuda_exc.value.response.status_code == _CUDA_OOM_STATUS_CODE
731
- assert cuda_exc.value.response.json()["detail"] == _CUDA_OOM_MESSAGE
730
+ assert cuda_exc.value.status_code == _CUDA_OOM_STATUS_CODE
731
+ assert _CUDA_OOM_MESSAGE in cuda_exc.value.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
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