fal 1.3.1__tar.gz → 1.3.3__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 (163) hide show
  1. {fal-1.3.1 → fal-1.3.3}/PKG-INFO +3 -2
  2. {fal-1.3.1 → fal-1.3.3}/fal.egg-info/PKG-INFO +3 -2
  3. {fal-1.3.1 → fal-1.3.3}/fal.egg-info/SOURCES.txt +2 -0
  4. {fal-1.3.1 → fal-1.3.3}/fal.egg-info/requires.txt +2 -1
  5. {fal-1.3.1 → fal-1.3.3}/pyproject.toml +2 -1
  6. {fal-1.3.1 → fal-1.3.3}/src/fal/_fal_version.py +2 -2
  7. {fal-1.3.1 → fal-1.3.3}/src/fal/api.py +2 -0
  8. {fal-1.3.1 → fal-1.3.3}/src/fal/app.py +12 -0
  9. {fal-1.3.1 → fal-1.3.3}/src/fal/apps.py +9 -0
  10. fal-1.3.3/src/fal/cli/_utils.py +34 -0
  11. {fal-1.3.1 → fal-1.3.3}/src/fal/cli/deploy.py +59 -9
  12. {fal-1.3.1 → fal-1.3.3}/src/fal/cli/parser.py +11 -7
  13. {fal-1.3.1 → fal-1.3.3}/src/fal/cli/run.py +11 -1
  14. fal-1.3.3/src/fal/exceptions/__init__.py +9 -0
  15. {fal-1.3.1 → fal-1.3.3}/src/fal/exceptions/_base.py +7 -0
  16. fal-1.3.3/src/fal/files.py +81 -0
  17. {fal-1.3.1 → fal-1.3.3}/src/fal/sdk.py +36 -0
  18. {fal-1.3.1 → fal-1.3.3}/src/fal/utils.py +2 -1
  19. fal-1.3.3/tests/cli/test_deploy.py +148 -0
  20. fal-1.3.3/tests/cli/test_run.py +109 -0
  21. {fal-1.3.1 → fal-1.3.3}/tests/test_apps.py +73 -2
  22. fal-1.3.1/src/fal/exceptions/__init__.py +0 -4
  23. fal-1.3.1/tests/cli/test_deploy.py +0 -8
  24. fal-1.3.1/tests/cli/test_run.py +0 -8
  25. {fal-1.3.1 → fal-1.3.3}/.gitignore +0 -0
  26. {fal-1.3.1 → fal-1.3.3}/README.md +0 -0
  27. {fal-1.3.1 → fal-1.3.3}/fal.egg-info/dependency_links.txt +0 -0
  28. {fal-1.3.1 → fal-1.3.3}/fal.egg-info/entry_points.txt +0 -0
  29. {fal-1.3.1 → fal-1.3.3}/fal.egg-info/top_level.txt +0 -0
  30. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/README.md +0 -0
  31. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  32. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  33. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  34. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  35. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  36. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  37. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  38. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  39. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  40. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  41. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  42. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  43. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  44. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  45. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  46. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  47. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  48. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  49. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  50. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  51. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  52. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  53. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  54. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  55. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  56. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  57. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  58. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  59. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  60. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  61. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  62. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  63. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  64. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
  65. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  66. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  67. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  68. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  69. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  70. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  71. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  72. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  73. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  74. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  75. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  76. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  77. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  78. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  79. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  80. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  81. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  82. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  83. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  84. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  85. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  86. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  87. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  88. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  89. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  90. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  91. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  92. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  93. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  94. {fal-1.3.1 → fal-1.3.3}/openapi-fal-rest/pyproject.toml +0 -0
  95. {fal-1.3.1 → fal-1.3.3}/openapi_rest.config.yaml +0 -0
  96. {fal-1.3.1 → fal-1.3.3}/setup.cfg +0 -0
  97. {fal-1.3.1 → fal-1.3.3}/src/fal/__init__.py +0 -0
  98. {fal-1.3.1 → fal-1.3.3}/src/fal/__main__.py +0 -0
  99. {fal-1.3.1 → fal-1.3.3}/src/fal/_serialization.py +0 -0
  100. {fal-1.3.1 → fal-1.3.3}/src/fal/_version.py +0 -0
  101. {fal-1.3.1 → fal-1.3.3}/src/fal/auth/__init__.py +0 -0
  102. {fal-1.3.1 → fal-1.3.3}/src/fal/auth/auth0.py +0 -0
  103. {fal-1.3.1 → fal-1.3.3}/src/fal/auth/local.py +0 -0
  104. {fal-1.3.1 → fal-1.3.3}/src/fal/cli/__init__.py +0 -0
  105. {fal-1.3.1 → fal-1.3.3}/src/fal/cli/apps.py +0 -0
  106. {fal-1.3.1 → fal-1.3.3}/src/fal/cli/auth.py +0 -0
  107. {fal-1.3.1 → fal-1.3.3}/src/fal/cli/create.py +0 -0
  108. {fal-1.3.1 → fal-1.3.3}/src/fal/cli/debug.py +0 -0
  109. {fal-1.3.1 → fal-1.3.3}/src/fal/cli/doctor.py +0 -0
  110. {fal-1.3.1 → fal-1.3.3}/src/fal/cli/keys.py +0 -0
  111. {fal-1.3.1 → fal-1.3.3}/src/fal/cli/main.py +0 -0
  112. {fal-1.3.1 → fal-1.3.3}/src/fal/cli/secrets.py +0 -0
  113. {fal-1.3.1 → fal-1.3.3}/src/fal/console/__init__.py +0 -0
  114. {fal-1.3.1 → fal-1.3.3}/src/fal/console/icons.py +0 -0
  115. {fal-1.3.1 → fal-1.3.3}/src/fal/console/ux.py +0 -0
  116. {fal-1.3.1 → fal-1.3.3}/src/fal/container.py +0 -0
  117. {fal-1.3.1 → fal-1.3.3}/src/fal/exceptions/_cuda.py +0 -0
  118. {fal-1.3.1 → fal-1.3.3}/src/fal/exceptions/auth.py +0 -0
  119. {fal-1.3.1 → fal-1.3.3}/src/fal/flags.py +0 -0
  120. {fal-1.3.1 → fal-1.3.3}/src/fal/logging/__init__.py +0 -0
  121. {fal-1.3.1 → fal-1.3.3}/src/fal/logging/isolate.py +0 -0
  122. {fal-1.3.1 → fal-1.3.3}/src/fal/logging/style.py +0 -0
  123. {fal-1.3.1 → fal-1.3.3}/src/fal/logging/trace.py +0 -0
  124. {fal-1.3.1 → fal-1.3.3}/src/fal/logging/user.py +0 -0
  125. {fal-1.3.1 → fal-1.3.3}/src/fal/py.typed +0 -0
  126. {fal-1.3.1 → fal-1.3.3}/src/fal/rest_client.py +0 -0
  127. {fal-1.3.1 → fal-1.3.3}/src/fal/sync.py +0 -0
  128. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/__init__.py +0 -0
  129. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/exceptions.py +0 -0
  130. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/file/__init__.py +0 -0
  131. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/file/file.py +0 -0
  132. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/file/providers/fal.py +0 -0
  133. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/file/providers/gcp.py +0 -0
  134. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/file/providers/r2.py +0 -0
  135. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/file/types.py +0 -0
  136. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/image/__init__.py +0 -0
  137. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/image/image.py +0 -0
  138. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
  139. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
  140. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
  141. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
  142. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
  143. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/image/safety_checker.py +0 -0
  144. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/optimize.py +0 -0
  145. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/utils/__init__.py +0 -0
  146. {fal-1.3.1 → fal-1.3.3}/src/fal/toolkit/utils/download_utils.py +0 -0
  147. {fal-1.3.1 → fal-1.3.3}/src/fal/workflows.py +0 -0
  148. {fal-1.3.1 → fal-1.3.3}/tests/__init__.py +0 -0
  149. {fal-1.3.1 → fal-1.3.3}/tests/cli/__init__.py +0 -0
  150. {fal-1.3.1 → fal-1.3.3}/tests/cli/test_apps.py +0 -0
  151. {fal-1.3.1 → fal-1.3.3}/tests/cli/test_auth.py +0 -0
  152. {fal-1.3.1 → fal-1.3.3}/tests/cli/test_keys.py +0 -0
  153. {fal-1.3.1 → fal-1.3.3}/tests/cli/test_secrets.py +0 -0
  154. {fal-1.3.1 → fal-1.3.3}/tests/conftest.py +0 -0
  155. {fal-1.3.1 → fal-1.3.3}/tests/integration_test.py +0 -0
  156. {fal-1.3.1 → fal-1.3.3}/tests/mainify_package/__init__.py +0 -0
  157. {fal-1.3.1 → fal-1.3.3}/tests/mainify_package/impl.py +0 -0
  158. {fal-1.3.1 → fal-1.3.3}/tests/mainify_package/utils.py +0 -0
  159. {fal-1.3.1 → fal-1.3.3}/tests/mainify_target.py +0 -0
  160. {fal-1.3.1 → fal-1.3.3}/tests/test_stability.py +0 -0
  161. {fal-1.3.1 → fal-1.3.3}/tests/toolkit/file_test.py +0 -0
  162. {fal-1.3.1 → fal-1.3.3}/tests/toolkit/image_test.py +0 -0
  163. {fal-1.3.1 → fal-1.3.3}/tools/demo_script.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.3.1
3
+ Version: 1.3.3
4
4
  Summary: fal is an easy-to-use Serverless Python Framework
5
5
  Author: Features & Labels <support@fal.ai>
6
6
  Requires-Python: >=3.8
7
7
  Description-Content-Type: text/markdown
8
8
  Requires-Dist: isolate[build]<1.14.0,>=0.13.0
9
- Requires-Dist: isolate-proto==0.5.1
9
+ Requires-Dist: isolate-proto==0.5.3
10
10
  Requires-Dist: grpcio==1.64.0
11
11
  Requires-Dist: dill==0.3.7
12
12
  Requires-Dist: cloudpickle==3.0.0
@@ -36,6 +36,7 @@ Requires-Dist: pillow<11,>=10.2.0
36
36
  Requires-Dist: pyjwt[crypto]<3,>=2.8.0
37
37
  Requires-Dist: uvicorn<1,>=0.29.0
38
38
  Requires-Dist: cookiecutter
39
+ Requires-Dist: tomli
39
40
  Provides-Extra: test
40
41
  Requires-Dist: pytest<8; extra == "test"
41
42
  Requires-Dist: pytest-asyncio; extra == "test"
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.3.1
3
+ Version: 1.3.3
4
4
  Summary: fal is an easy-to-use Serverless Python Framework
5
5
  Author: Features & Labels <support@fal.ai>
6
6
  Requires-Python: >=3.8
7
7
  Description-Content-Type: text/markdown
8
8
  Requires-Dist: isolate[build]<1.14.0,>=0.13.0
9
- Requires-Dist: isolate-proto==0.5.1
9
+ Requires-Dist: isolate-proto==0.5.3
10
10
  Requires-Dist: grpcio==1.64.0
11
11
  Requires-Dist: dill==0.3.7
12
12
  Requires-Dist: cloudpickle==3.0.0
@@ -36,6 +36,7 @@ Requires-Dist: pillow<11,>=10.2.0
36
36
  Requires-Dist: pyjwt[crypto]<3,>=2.8.0
37
37
  Requires-Dist: uvicorn<1,>=0.29.0
38
38
  Requires-Dist: cookiecutter
39
+ Requires-Dist: tomli
39
40
  Provides-Extra: test
40
41
  Requires-Dist: pytest<8; extra == "test"
41
42
  Requires-Dist: pytest-asyncio; extra == "test"
@@ -82,6 +82,7 @@ src/fal/api.py
82
82
  src/fal/app.py
83
83
  src/fal/apps.py
84
84
  src/fal/container.py
85
+ src/fal/files.py
85
86
  src/fal/flags.py
86
87
  src/fal/py.typed
87
88
  src/fal/rest_client.py
@@ -93,6 +94,7 @@ src/fal/auth/__init__.py
93
94
  src/fal/auth/auth0.py
94
95
  src/fal/auth/local.py
95
96
  src/fal/cli/__init__.py
97
+ src/fal/cli/_utils.py
96
98
  src/fal/cli/apps.py
97
99
  src/fal/cli/auth.py
98
100
  src/fal/cli/create.py
@@ -1,5 +1,5 @@
1
1
  isolate[build]<1.14.0,>=0.13.0
2
- isolate-proto==0.5.1
2
+ isolate-proto==0.5.3
3
3
  grpcio==1.64.0
4
4
  dill==0.3.7
5
5
  cloudpickle==3.0.0
@@ -28,6 +28,7 @@ pillow<11,>=10.2.0
28
28
  pyjwt[crypto]<3,>=2.8.0
29
29
  uvicorn<1,>=0.29.0
30
30
  cookiecutter
31
+ tomli
31
32
 
32
33
  [:python_version < "3.10"]
33
34
  importlib-metadata>=4.4
@@ -23,7 +23,7 @@ readme = "README.md"
23
23
  requires-python = ">=3.8"
24
24
  dependencies = [
25
25
  "isolate[build]>=0.13.0,<1.14.0",
26
- "isolate-proto==0.5.1",
26
+ "isolate-proto==0.5.3",
27
27
  "grpcio==1.64.0",
28
28
  "dill==0.3.7",
29
29
  "cloudpickle==3.0.0",
@@ -56,6 +56,7 @@ dependencies = [
56
56
  "pyjwt[crypto]>=2.8.0,<3",
57
57
  "uvicorn>=0.29.0,<1",
58
58
  "cookiecutter",
59
+ "tomli"
59
60
  ]
60
61
 
61
62
  [project.optional-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.3.1'
16
- __version_tuple__ = version_tuple = (1, 3, 1)
15
+ __version__ = version = '1.3.3'
16
+ __version_tuple__ = version_tuple = (1, 3, 3)
@@ -425,6 +425,7 @@ class FalServerlessHost(Host):
425
425
  application_name: str | None = None,
426
426
  application_auth_mode: Literal["public", "shared", "private"] | None = None,
427
427
  metadata: dict[str, Any] | None = None,
428
+ deployment_strategy: Literal["recreate", "rolling"] = "recreate",
428
429
  ) -> str | None:
429
430
  environment_options = options.environment.copy()
430
431
  environment_options.setdefault("python_version", active_python())
@@ -477,6 +478,7 @@ class FalServerlessHost(Host):
477
478
  application_auth_mode=application_auth_mode,
478
479
  machine_requirements=machine_requirements,
479
480
  metadata=metadata,
481
+ deployment_strategy=deployment_strategy,
480
482
  ):
481
483
  for log in partial_result.logs:
482
484
  self._log_printer.print(log)
@@ -17,6 +17,7 @@ from fastapi import FastAPI
17
17
  import fal.api
18
18
  from fal._serialization import include_modules_from
19
19
  from fal.api import RouteSignature
20
+ from fal.exceptions import RequestCancelledException
20
21
  from fal.logging import get_logger
21
22
  from fal.toolkit.file.providers import fal as fal_provider_module
22
23
 
@@ -272,6 +273,17 @@ class App(fal.api.BaseServable):
272
273
  )
273
274
  return response
274
275
 
276
+ @app.exception_handler(RequestCancelledException)
277
+ async def value_error_exception_handler(
278
+ request, exc: RequestCancelledException
279
+ ):
280
+ from fastapi.responses import JSONResponse
281
+
282
+ # A 499 status code is not an officially recognized HTTP status code,
283
+ # but it is sometimes used by servers to indicate that a client has closed
284
+ # the connection without receiving a response
285
+ return JSONResponse({"detail": str(exc)}, 499)
286
+
275
287
  def _add_extra_routes(self, app: FastAPI):
276
288
  @app.get("/health")
277
289
  def health():
@@ -97,6 +97,15 @@ class RequestHandle:
97
97
  else:
98
98
  raise ValueError(f"Unknown status: {data['status']}")
99
99
 
100
+ def cancel(self) -> None:
101
+ """Cancel an async inference request."""
102
+ url = (
103
+ _QUEUE_URL_FORMAT.format(app_id=self.app_id)
104
+ + f"/requests/{self.request_id}/cancel"
105
+ )
106
+ response = _HTTP_CLIENT.put(url, headers=self._creds.to_headers())
107
+ response.raise_for_status()
108
+
100
109
  def iter_events(
101
110
  self,
102
111
  *,
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from fal.files import find_pyproject_toml, parse_pyproject_toml
4
+
5
+
6
+ def is_app_name(app_ref: tuple[str, str | None]) -> bool:
7
+ is_single_file = app_ref[1] is None
8
+ is_python_file = app_ref[0].endswith(".py")
9
+
10
+ return is_single_file and not is_python_file
11
+
12
+
13
+ def get_app_data_from_toml(app_name):
14
+ toml_path = find_pyproject_toml()
15
+
16
+ if toml_path is None:
17
+ raise ValueError("No pyproject.toml file found.")
18
+
19
+ fal_data = parse_pyproject_toml(toml_path)
20
+ apps = fal_data.get("apps", {})
21
+
22
+ try:
23
+ app_data = apps[app_name]
24
+ except KeyError:
25
+ raise ValueError(f"App {app_name} not found in pyproject.toml")
26
+
27
+ try:
28
+ app_ref = app_data["ref"]
29
+ except KeyError:
30
+ raise ValueError(f"App {app_name} does not have a ref key in pyproject.toml")
31
+
32
+ app_auth = app_data.get("auth", "private")
33
+
34
+ return app_ref, app_auth
@@ -1,7 +1,9 @@
1
1
  import argparse
2
2
  from collections import namedtuple
3
3
  from pathlib import Path
4
+ from typing import Optional, Union
4
5
 
6
+ from ._utils import get_app_data_from_toml, is_app_name
5
7
  from .parser import FalClientParser, RefAction
6
8
 
7
9
  User = namedtuple("User", ["user_id", "username"])
@@ -60,11 +62,13 @@ def _get_user() -> User:
60
62
  raise FalServerlessError(f"Could not parse the user data: {e}")
61
63
 
62
64
 
63
- def _deploy(args):
65
+ def _deploy_from_reference(
66
+ app_ref: tuple[Optional[Union[Path, str]], ...], app_name: str, auth: str, args
67
+ ):
64
68
  from fal.api import FalServerlessError, FalServerlessHost
65
69
  from fal.utils import load_function_from
66
70
 
67
- file_path, func_name = args.app_ref
71
+ file_path, func_name = app_ref
68
72
  if file_path is None:
69
73
  # Try to find a python file in the current directory
70
74
  options = list(Path(".").glob("*.py"))
@@ -77,24 +81,27 @@ def _deploy(args):
77
81
  )
78
82
 
79
83
  [file_path] = options
80
- file_path = str(file_path)
84
+ file_path = str(file_path) # type: ignore
81
85
 
82
86
  user = _get_user()
83
87
  host = FalServerlessHost(args.host)
84
88
  loaded = load_function_from(
85
89
  host,
86
- file_path,
87
- func_name,
90
+ file_path, # type: ignore
91
+ func_name, # type: ignore
88
92
  )
89
93
  isolated_function = loaded.function
90
- app_name = args.app_name or loaded.app_name
91
- app_auth = args.auth or loaded.app_auth or "private"
94
+ app_name = app_name or loaded.app_name # type: ignore
95
+ app_auth = auth or loaded.app_auth or "private"
96
+ deployment_strategy = args.strategy or "default"
97
+
92
98
  app_id = host.register(
93
99
  func=isolated_function.func,
94
100
  options=isolated_function.options,
95
101
  application_name=app_name,
96
102
  application_auth_mode=app_auth,
97
103
  metadata=isolated_function.options.host.get("metadata", {}),
104
+ deployment_strategy=deployment_strategy,
98
105
  )
99
106
 
100
107
  if app_id:
@@ -119,6 +126,26 @@ def _deploy(args):
119
126
  )
120
127
 
121
128
 
129
+ def _deploy(args):
130
+ # my-app
131
+ if is_app_name(args.app_ref):
132
+ # we do not allow --app-name and --auth to be used with app name
133
+ if args.app_name or args.auth:
134
+ raise ValueError("Cannot use --app-name or --auth with app name reference.")
135
+
136
+ app_name = args.app_ref[0]
137
+ app_ref, app_auth = get_app_data_from_toml(app_name)
138
+ file_path, func_name = RefAction.split_ref(app_ref)
139
+
140
+ # path/to/myfile.py::MyApp
141
+ else:
142
+ file_path, func_name = args.app_ref
143
+ app_name = args.app_name
144
+ app_auth = args.auth
145
+
146
+ _deploy_from_reference((file_path, func_name), app_name, app_auth, args)
147
+
148
+
122
149
  def add_parser(main_subparsers, parents):
123
150
  from fal.sdk import ALIAS_AUTH_MODES
124
151
 
@@ -127,14 +154,22 @@ def add_parser(main_subparsers, parents):
127
154
  raise argparse.ArgumentTypeError(f"{option} is not a auth option")
128
155
  return option
129
156
 
130
- deploy_help = "Deploy a fal application."
157
+ deploy_help = (
158
+ "Deploy a fal application. "
159
+ "If no app reference is provided, the command will look for a "
160
+ "pyproject.toml file with a [tool.fal.apps] section and deploy the "
161
+ "application specified with the provided app name."
162
+ )
163
+
131
164
  epilog = (
132
165
  "Examples:\n"
133
166
  " fal deploy\n"
134
167
  " fal deploy path/to/myfile.py\n"
135
168
  " fal deploy path/to/myfile.py::MyApp\n"
136
169
  " fal deploy path/to/myfile.py::MyApp --app-name myapp --auth public\n"
170
+ " fal deploy my-app\n"
137
171
  )
172
+
138
173
  parser = main_subparsers.add_parser(
139
174
  "deploy",
140
175
  parents=[*parents, FalClientParser(add_help=False)],
@@ -142,21 +177,36 @@ def add_parser(main_subparsers, parents):
142
177
  help=deploy_help,
143
178
  epilog=epilog,
144
179
  )
180
+
145
181
  parser.add_argument(
146
182
  "app_ref",
147
183
  nargs="?",
148
184
  action=RefAction,
149
185
  help=(
150
- "Application reference. " "For example: `myfile.py::MyApp`, `myfile.py`."
186
+ "Application reference. Either a file path or a file path and a "
187
+ "function name separated by '::'. If no reference is provided, the "
188
+ "command will look for a pyproject.toml file with a [tool.fal.apps] "
189
+ "section and deploy the application specified with the provided app name.\n"
190
+ "File path example: path/to/myfile.py::MyApp\n"
191
+ "App name example: my-app\n"
151
192
  ),
152
193
  )
194
+
153
195
  parser.add_argument(
154
196
  "--app-name",
155
197
  help="Application name to deploy with.",
156
198
  )
199
+
157
200
  parser.add_argument(
158
201
  "--auth",
159
202
  type=valid_auth_option,
160
203
  help="Application authentication mode (private, public).",
161
204
  )
205
+ parser.add_argument(
206
+ "--strategy",
207
+ choices=["default", "rolling"],
208
+ help="Deployment strategy.",
209
+ default="default",
210
+ )
211
+
162
212
  parser.set_defaults(func=_deploy)
@@ -14,14 +14,18 @@ class RefAction(argparse.Action):
14
14
  kwargs.setdefault("default", (None, None))
15
15
  super().__init__(*args, **kwargs)
16
16
 
17
- def __call__(self, parser, args, values, option_string=None): # noqa: ARG002
18
- if isinstance(values, tuple):
19
- file_path, obj_path = values
20
- elif values.find("::") > 1:
21
- file_path, obj_path = values.split("::", 1)
22
- else:
23
- file_path, obj_path = values, None
17
+ @classmethod
18
+ def split_ref(cls, value):
19
+ if isinstance(value, tuple):
20
+ return value
21
+
22
+ if value.find("::") > 1:
23
+ return value.split("::", 1)
24
24
 
25
+ return value, None
26
+
27
+ def __call__(self, parser, args, values, option_string=None): # noqa: ARG002
28
+ file_path, obj_path = self.split_ref(values)
25
29
  setattr(args, self.dest, (file_path, obj_path))
26
30
 
27
31
 
@@ -1,3 +1,4 @@
1
+ from ._utils import get_app_data_from_toml, is_app_name
1
2
  from .parser import FalClientParser, RefAction
2
3
 
3
4
 
@@ -6,7 +7,16 @@ def _run(args):
6
7
  from fal.utils import load_function_from
7
8
 
8
9
  host = FalServerlessHost(args.host)
9
- loaded = load_function_from(host, *args.func_ref)
10
+
11
+ if is_app_name(args.func_ref):
12
+ app_name = args.func_ref[0]
13
+ app_ref, _ = get_app_data_from_toml(app_name)
14
+ file_path, func_name = RefAction.split_ref(app_ref)
15
+ else:
16
+ file_path, func_name = args.func_ref
17
+
18
+ loaded = load_function_from(host, file_path, func_name)
19
+
10
20
  isolated_function = loaded.function
11
21
  # let our exc handlers handle UserFunctionException
12
22
  isolated_function.reraise = False
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from ._base import (
4
+ AppException, # noqa: F401
5
+ FalServerlessException, # noqa: F401
6
+ FieldException, # noqa: F401
7
+ RequestCancelledException, # noqa: F401
8
+ )
9
+ from ._cuda import CUDAOutOfMemoryException # noqa: F401
@@ -49,3 +49,10 @@ class FieldException(FalServerlessException):
49
49
  }
50
50
  ]
51
51
  )
52
+
53
+
54
+ @dataclass
55
+ class RequestCancelledException(FalServerlessException):
56
+ """Exception raised when the request is cancelled by the client."""
57
+
58
+ message: str = "Request cancelled by the client."
@@ -0,0 +1,81 @@
1
+ from functools import lru_cache
2
+ from pathlib import Path
3
+ from typing import Any, Dict, Optional, Sequence, Tuple, Union
4
+
5
+ import tomli
6
+
7
+
8
+ @lru_cache
9
+ def _load_toml(path: Union[Path, str]) -> Dict[str, Any]:
10
+ with open(path, "rb") as f:
11
+ return tomli.load(f)
12
+
13
+
14
+ @lru_cache
15
+ def _cached_resolve(path: Path) -> Path:
16
+ return path.resolve()
17
+
18
+
19
+ @lru_cache
20
+ def find_project_root(srcs: Optional[Sequence[str]]) -> Tuple[Path, str]:
21
+ """Return a directory containing .git, or pyproject.toml.
22
+
23
+ That directory will be a common parent of all files and directories
24
+ passed in `srcs`.
25
+
26
+ If no directory in the tree contains a marker that would specify it's the
27
+ project root, the root of the file system is returned.
28
+
29
+ Returns a two-tuple with the first element as the project root path and
30
+ the second element as a string describing the method by which the
31
+ project root was discovered.
32
+ """
33
+ if not srcs:
34
+ srcs = [str(_cached_resolve(Path.cwd()))]
35
+
36
+ path_srcs = [_cached_resolve(Path(Path.cwd(), src)) for src in srcs]
37
+
38
+ # A list of lists of parents for each 'src'. 'src' is included as a
39
+ # "parent" of itself if it is a directory
40
+ src_parents = [
41
+ list(path.parents) + ([path] if path.is_dir() else []) for path in path_srcs
42
+ ]
43
+
44
+ common_base = max(
45
+ set.intersection(*(set(parents) for parents in src_parents)),
46
+ key=lambda path: path.parts,
47
+ )
48
+
49
+ for directory in (common_base, *common_base.parents):
50
+ if (directory / ".git").exists():
51
+ return directory, ".git directory"
52
+
53
+ if (directory / "pyproject.toml").is_file():
54
+ pyproject_toml = _load_toml(directory / "pyproject.toml")
55
+ if "fal" in pyproject_toml.get("tool", {}):
56
+ return directory, "pyproject.toml"
57
+
58
+ return directory, "file system root"
59
+
60
+
61
+ def find_pyproject_toml(
62
+ path_search_start: Optional[Tuple[str, ...]] = None,
63
+ ) -> Optional[str]:
64
+ """Find the absolute filepath to a pyproject.toml if it exists"""
65
+ path_project_root, _ = find_project_root(path_search_start)
66
+ path_pyproject_toml = path_project_root / "pyproject.toml"
67
+
68
+ if path_pyproject_toml.is_file():
69
+ return str(path_pyproject_toml)
70
+
71
+
72
+ def parse_pyproject_toml(path_config: str) -> Dict[str, Any]:
73
+ """Parse a pyproject toml file, pulling out relevant parts for fal.
74
+
75
+ If parsing fails, will raise a tomli.TOMLDecodeError.
76
+ """
77
+ pyproject_toml = _load_toml(path_config)
78
+ config: Dict[str, Any] = pyproject_toml.get("tool", {}).get("fal", {})
79
+ config = {k.replace("--", "").replace("-", "_"): v for k, v in config.items()}
80
+
81
+ return config
@@ -275,6 +275,33 @@ class KeyScope(enum.Enum):
275
275
  raise ValueError(f"Unknown KeyScope: {proto}")
276
276
 
277
277
 
278
+ class DeploymentStrategy(enum.Enum):
279
+ RECREATE = "recreate"
280
+ ROLLING = "rolling"
281
+
282
+ @staticmethod
283
+ def from_proto(
284
+ proto: isolate_proto.DeploymentStrategy.ValueType | None,
285
+ ) -> DeploymentStrategy:
286
+ if proto is None:
287
+ return DeploymentStrategy.RECREATE
288
+
289
+ if proto is isolate_proto.DeploymentStrategy.RECREATE:
290
+ return DeploymentStrategy.RECREATE
291
+ elif proto is isolate_proto.DeploymentStrategy.ROLLING:
292
+ return DeploymentStrategy.ROLLING
293
+ else:
294
+ raise ValueError(f"Unknown DeploymentStrategy: {proto}")
295
+
296
+ def to_proto(self) -> isolate_proto.DeploymentStrategy.ValueType:
297
+ if self is DeploymentStrategy.RECREATE:
298
+ return isolate_proto.DeploymentStrategy.RECREATE
299
+ elif self is DeploymentStrategy.ROLLING:
300
+ return isolate_proto.DeploymentStrategy.ROLLING
301
+ else:
302
+ raise ValueError(f"Unknown DeploymentStrategy: {self}")
303
+
304
+
278
305
  @from_grpc.register(isolate_proto.ApplicationInfo)
279
306
  def _from_grpc_application_info(
280
307
  message: isolate_proto.ApplicationInfo,
@@ -457,6 +484,7 @@ class FalServerlessConnection:
457
484
  serialization_method: str = _DEFAULT_SERIALIZATION_METHOD,
458
485
  machine_requirements: MachineRequirements | None = None,
459
486
  metadata: dict[str, Any] | None = None,
487
+ deployment_strategy: Literal["recreate", "rolling"] = "recreate",
460
488
  ) -> Iterator[isolate_proto.RegisterApplicationResult]:
461
489
  wrapped_function = to_serialized_object(function, serialization_method)
462
490
  if machine_requirements:
@@ -488,6 +516,13 @@ class FalServerlessConnection:
488
516
  struct_metadata = isolate_proto.Struct()
489
517
  struct_metadata.update(metadata)
490
518
 
519
+ if deployment_strategy == "default":
520
+ deployment_strategy = "recreate"
521
+
522
+ deployment_strategy_proto = DeploymentStrategy[
523
+ deployment_strategy.upper()
524
+ ].to_proto()
525
+
491
526
  request = isolate_proto.RegisterApplicationRequest(
492
527
  function=wrapped_function,
493
528
  environments=environments,
@@ -495,6 +530,7 @@ class FalServerlessConnection:
495
530
  application_name=application_name,
496
531
  auth_mode=auth_mode,
497
532
  metadata=struct_metadata,
533
+ deployment_strategy=deployment_strategy_proto,
498
534
  )
499
535
  for partial_result in self.stub.RegisterApplication(request):
500
536
  yield from_grpc(partial_result)
@@ -55,10 +55,11 @@ def load_function_from(
55
55
  fal._serialization.include_package_from_path(file_path)
56
56
 
57
57
  target = module[function_name]
58
- endpoints = target.get_endpoints() or ["/"]
58
+ endpoints = ["/"]
59
59
  if isinstance(target, type) and issubclass(target, App):
60
60
  app_name = target.app_name
61
61
  app_auth = target.app_auth
62
+ endpoints = target.get_endpoints() or ["/"]
62
63
  target = wrap_app(target, host=host)
63
64
 
64
65
  if not isinstance(target, IsolatedFunction):