fal 1.3.0__tar.gz → 1.3.2__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 (164) hide show
  1. {fal-1.3.0 → fal-1.3.2}/PKG-INFO +3 -2
  2. {fal-1.3.0 → fal-1.3.2}/fal.egg-info/PKG-INFO +3 -2
  3. {fal-1.3.0 → fal-1.3.2}/fal.egg-info/SOURCES.txt +2 -0
  4. {fal-1.3.0 → fal-1.3.2}/fal.egg-info/requires.txt +2 -1
  5. {fal-1.3.0 → fal-1.3.2}/pyproject.toml +2 -1
  6. {fal-1.3.0 → fal-1.3.2}/src/fal/__main__.py +3 -1
  7. {fal-1.3.0 → fal-1.3.2}/src/fal/_fal_version.py +2 -2
  8. {fal-1.3.0 → fal-1.3.2}/src/fal/api.py +2 -0
  9. {fal-1.3.0 → fal-1.3.2}/src/fal/app.py +21 -1
  10. {fal-1.3.0 → fal-1.3.2}/src/fal/apps.py +9 -0
  11. fal-1.3.2/src/fal/cli/_utils.py +34 -0
  12. {fal-1.3.0 → fal-1.3.2}/src/fal/cli/deploy.py +71 -16
  13. {fal-1.3.0 → fal-1.3.2}/src/fal/cli/parser.py +11 -7
  14. {fal-1.3.0 → fal-1.3.2}/src/fal/cli/run.py +12 -1
  15. fal-1.3.2/src/fal/exceptions/__init__.py +9 -0
  16. {fal-1.3.0 → fal-1.3.2}/src/fal/exceptions/_base.py +17 -9
  17. fal-1.3.2/src/fal/files.py +81 -0
  18. {fal-1.3.0 → fal-1.3.2}/src/fal/sdk.py +33 -0
  19. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/file/file.py +21 -4
  20. fal-1.3.2/src/fal/toolkit/file/providers/fal.py +319 -0
  21. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/file/types.py +18 -0
  22. {fal-1.3.0 → fal-1.3.2}/src/fal/utils.py +14 -2
  23. fal-1.3.2/tests/cli/test_deploy.py +148 -0
  24. fal-1.3.2/tests/cli/test_run.py +109 -0
  25. {fal-1.3.0 → fal-1.3.2}/tests/test_apps.py +90 -6
  26. {fal-1.3.0 → fal-1.3.2}/tests/test_stability.py +18 -8
  27. fal-1.3.0/src/fal/exceptions/__init__.py +0 -4
  28. fal-1.3.0/src/fal/toolkit/file/providers/fal.py +0 -143
  29. fal-1.3.0/tests/cli/test_deploy.py +0 -8
  30. fal-1.3.0/tests/cli/test_run.py +0 -8
  31. {fal-1.3.0 → fal-1.3.2}/.gitignore +0 -0
  32. {fal-1.3.0 → fal-1.3.2}/README.md +0 -0
  33. {fal-1.3.0 → fal-1.3.2}/fal.egg-info/dependency_links.txt +0 -0
  34. {fal-1.3.0 → fal-1.3.2}/fal.egg-info/entry_points.txt +0 -0
  35. {fal-1.3.0 → fal-1.3.2}/fal.egg-info/top_level.txt +0 -0
  36. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/README.md +0 -0
  37. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  38. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  39. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  40. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  41. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  42. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  43. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  44. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  45. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  46. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  47. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  48. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  49. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  50. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  51. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  52. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  53. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  54. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  55. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  56. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  57. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  58. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  59. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  60. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  61. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  62. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  63. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  64. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  65. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  66. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  67. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  68. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  69. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  70. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
  71. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  72. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  73. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  74. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  75. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  76. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  77. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  78. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  79. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  80. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  81. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  82. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  83. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  84. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  85. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  86. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  87. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  88. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  89. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  90. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  91. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  92. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  93. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  94. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  95. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  96. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  97. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  98. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  99. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  100. {fal-1.3.0 → fal-1.3.2}/openapi-fal-rest/pyproject.toml +0 -0
  101. {fal-1.3.0 → fal-1.3.2}/openapi_rest.config.yaml +0 -0
  102. {fal-1.3.0 → fal-1.3.2}/setup.cfg +0 -0
  103. {fal-1.3.0 → fal-1.3.2}/src/fal/__init__.py +0 -0
  104. {fal-1.3.0 → fal-1.3.2}/src/fal/_serialization.py +0 -0
  105. {fal-1.3.0 → fal-1.3.2}/src/fal/_version.py +0 -0
  106. {fal-1.3.0 → fal-1.3.2}/src/fal/auth/__init__.py +0 -0
  107. {fal-1.3.0 → fal-1.3.2}/src/fal/auth/auth0.py +0 -0
  108. {fal-1.3.0 → fal-1.3.2}/src/fal/auth/local.py +0 -0
  109. {fal-1.3.0 → fal-1.3.2}/src/fal/cli/__init__.py +0 -0
  110. {fal-1.3.0 → fal-1.3.2}/src/fal/cli/apps.py +0 -0
  111. {fal-1.3.0 → fal-1.3.2}/src/fal/cli/auth.py +0 -0
  112. {fal-1.3.0 → fal-1.3.2}/src/fal/cli/create.py +0 -0
  113. {fal-1.3.0 → fal-1.3.2}/src/fal/cli/debug.py +0 -0
  114. {fal-1.3.0 → fal-1.3.2}/src/fal/cli/doctor.py +0 -0
  115. {fal-1.3.0 → fal-1.3.2}/src/fal/cli/keys.py +0 -0
  116. {fal-1.3.0 → fal-1.3.2}/src/fal/cli/main.py +0 -0
  117. {fal-1.3.0 → fal-1.3.2}/src/fal/cli/secrets.py +0 -0
  118. {fal-1.3.0 → fal-1.3.2}/src/fal/console/__init__.py +0 -0
  119. {fal-1.3.0 → fal-1.3.2}/src/fal/console/icons.py +0 -0
  120. {fal-1.3.0 → fal-1.3.2}/src/fal/console/ux.py +0 -0
  121. {fal-1.3.0 → fal-1.3.2}/src/fal/container.py +0 -0
  122. {fal-1.3.0 → fal-1.3.2}/src/fal/exceptions/_cuda.py +0 -0
  123. {fal-1.3.0 → fal-1.3.2}/src/fal/exceptions/auth.py +0 -0
  124. {fal-1.3.0 → fal-1.3.2}/src/fal/flags.py +0 -0
  125. {fal-1.3.0 → fal-1.3.2}/src/fal/logging/__init__.py +0 -0
  126. {fal-1.3.0 → fal-1.3.2}/src/fal/logging/isolate.py +0 -0
  127. {fal-1.3.0 → fal-1.3.2}/src/fal/logging/style.py +0 -0
  128. {fal-1.3.0 → fal-1.3.2}/src/fal/logging/trace.py +0 -0
  129. {fal-1.3.0 → fal-1.3.2}/src/fal/logging/user.py +0 -0
  130. {fal-1.3.0 → fal-1.3.2}/src/fal/py.typed +0 -0
  131. {fal-1.3.0 → fal-1.3.2}/src/fal/rest_client.py +0 -0
  132. {fal-1.3.0 → fal-1.3.2}/src/fal/sync.py +0 -0
  133. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/__init__.py +0 -0
  134. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/exceptions.py +0 -0
  135. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/file/__init__.py +0 -0
  136. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/file/providers/gcp.py +0 -0
  137. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/file/providers/r2.py +0 -0
  138. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/image/__init__.py +0 -0
  139. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/image/image.py +0 -0
  140. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
  141. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
  142. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
  143. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
  144. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
  145. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/image/safety_checker.py +0 -0
  146. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/optimize.py +0 -0
  147. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/utils/__init__.py +0 -0
  148. {fal-1.3.0 → fal-1.3.2}/src/fal/toolkit/utils/download_utils.py +0 -0
  149. {fal-1.3.0 → fal-1.3.2}/src/fal/workflows.py +0 -0
  150. {fal-1.3.0 → fal-1.3.2}/tests/__init__.py +0 -0
  151. {fal-1.3.0 → fal-1.3.2}/tests/cli/__init__.py +0 -0
  152. {fal-1.3.0 → fal-1.3.2}/tests/cli/test_apps.py +0 -0
  153. {fal-1.3.0 → fal-1.3.2}/tests/cli/test_auth.py +0 -0
  154. {fal-1.3.0 → fal-1.3.2}/tests/cli/test_keys.py +0 -0
  155. {fal-1.3.0 → fal-1.3.2}/tests/cli/test_secrets.py +0 -0
  156. {fal-1.3.0 → fal-1.3.2}/tests/conftest.py +0 -0
  157. {fal-1.3.0 → fal-1.3.2}/tests/integration_test.py +0 -0
  158. {fal-1.3.0 → fal-1.3.2}/tests/mainify_package/__init__.py +0 -0
  159. {fal-1.3.0 → fal-1.3.2}/tests/mainify_package/impl.py +0 -0
  160. {fal-1.3.0 → fal-1.3.2}/tests/mainify_package/utils.py +0 -0
  161. {fal-1.3.0 → fal-1.3.2}/tests/mainify_target.py +0 -0
  162. {fal-1.3.0 → fal-1.3.2}/tests/toolkit/file_test.py +0 -0
  163. {fal-1.3.0 → fal-1.3.2}/tests/toolkit/image_test.py +0 -0
  164. {fal-1.3.0 → fal-1.3.2}/tools/demo_script.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.3.0
3
+ Version: 1.3.2
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.0
3
+ Version: 1.3.2
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]
@@ -1,4 +1,6 @@
1
+ import sys
2
+
1
3
  from .cli import main
2
4
 
3
5
  if __name__ == "__main__":
4
- main()
6
+ sys.exit(main())
@@ -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.0'
16
- __version_tuple__ = version_tuple = (1, 3, 0)
15
+ __version__ = version = '1.3.2'
16
+ __version_tuple__ = version_tuple = (1, 3, 2)
@@ -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
 
@@ -143,7 +144,7 @@ class AppClient:
143
144
  with httpx.Client() as client:
144
145
  retries = 100
145
146
  for _ in range(retries):
146
- resp = client.get(info.url + "/health")
147
+ resp = client.get(info.url + "/health", timeout=60)
147
148
 
148
149
  if resp.is_success:
149
150
  break
@@ -205,6 +206,14 @@ class App(fal.api.BaseServable):
205
206
  "Running apps through SDK is not implemented yet."
206
207
  )
207
208
 
209
+ @classmethod
210
+ def get_endpoints(cls) -> list[str]:
211
+ return [
212
+ signature.path
213
+ for _, endpoint in inspect.getmembers(cls, inspect.isfunction)
214
+ if (signature := getattr(endpoint, "route_signature", None))
215
+ ]
216
+
208
217
  def collect_routes(self) -> dict[RouteSignature, Callable[..., Any]]:
209
218
  return {
210
219
  signature: endpoint
@@ -264,6 +273,17 @@ class App(fal.api.BaseServable):
264
273
  )
265
274
  return response
266
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
+
267
287
  def _add_extra_routes(self, app: FastAPI):
268
288
  @app.get("/health")
269
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,23 +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
- isolated_function, app_name, app_auth = load_function_from(
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
- app_name = args.app_name or app_name
90
- app_auth = args.auth or app_auth or "private"
93
+ isolated_function = loaded.function
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
+
91
98
  app_id = host.register(
92
99
  func=isolated_function.func,
93
100
  options=isolated_function.options,
94
101
  application_name=app_name,
95
102
  application_auth_mode=app_auth,
96
103
  metadata=isolated_function.options.host.get("metadata", {}),
104
+ deployment_strategy=deployment_strategy,
97
105
  )
98
106
 
99
107
  if app_id:
@@ -106,12 +114,36 @@ def _deploy(args):
106
114
  "Registered a new revision for function "
107
115
  f"'{app_name}' (revision='{app_id}')."
108
116
  )
109
- args.console.print(
110
- f"Playground: https://fal.ai/models/{user.username}/{app_name}"
111
- )
112
- args.console.print(
113
- f"Endpoint: https://{gateway_host}/{user.username}/{app_name}"
114
- )
117
+ args.console.print("Playground:")
118
+ for endpoint in loaded.endpoints:
119
+ args.console.print(
120
+ f"\thttps://fal.ai/models/{user.username}/{app_name}{endpoint}"
121
+ )
122
+ args.console.print("Endpoints:")
123
+ for endpoint in loaded.endpoints:
124
+ args.console.print(
125
+ f"\thttps://{gateway_host}/{user.username}/{app_name}{endpoint}"
126
+ )
127
+
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)
115
147
 
116
148
 
117
149
  def add_parser(main_subparsers, parents):
@@ -122,14 +154,22 @@ def add_parser(main_subparsers, parents):
122
154
  raise argparse.ArgumentTypeError(f"{option} is not a auth option")
123
155
  return option
124
156
 
125
- 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
+
126
164
  epilog = (
127
165
  "Examples:\n"
128
166
  " fal deploy\n"
129
167
  " fal deploy path/to/myfile.py\n"
130
168
  " fal deploy path/to/myfile.py::MyApp\n"
131
169
  " fal deploy path/to/myfile.py::MyApp --app-name myapp --auth public\n"
170
+ " fal deploy my-app\n"
132
171
  )
172
+
133
173
  parser = main_subparsers.add_parser(
134
174
  "deploy",
135
175
  parents=[*parents, FalClientParser(add_help=False)],
@@ -137,21 +177,36 @@ def add_parser(main_subparsers, parents):
137
177
  help=deploy_help,
138
178
  epilog=epilog,
139
179
  )
180
+
140
181
  parser.add_argument(
141
182
  "app_ref",
142
183
  nargs="?",
143
184
  action=RefAction,
144
185
  help=(
145
- "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"
146
192
  ),
147
193
  )
194
+
148
195
  parser.add_argument(
149
196
  "--app-name",
150
197
  help="Application name to deploy with.",
151
198
  )
199
+
152
200
  parser.add_argument(
153
201
  "--auth",
154
202
  type=valid_auth_option,
155
203
  help="Application authentication mode (private, public).",
156
204
  )
205
+ parser.add_argument(
206
+ "--strategy",
207
+ choices=["default", "rolling"],
208
+ help="Deployment strategy.",
209
+ default="default",
210
+ )
211
+
157
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,17 @@ def _run(args):
6
7
  from fal.utils import load_function_from
7
8
 
8
9
  host = FalServerlessHost(args.host)
9
- isolated_function, _, _ = 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
+
20
+ isolated_function = loaded.function
10
21
  # let our exc handlers handle UserFunctionException
11
22
  isolated_function.reraise = False
12
23
  isolated_function()
@@ -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
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
- from typing import Sequence
5
4
 
6
5
 
7
6
  class FalServerlessException(Exception):
@@ -40,11 +39,20 @@ class FieldException(FalServerlessException):
40
39
  status_code: int = 422
41
40
  type: str = "value_error"
42
41
 
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
- ]
42
+ def to_pydantic_format(self) -> dict[str, list[dict]]:
43
+ return dict(
44
+ detail=[
45
+ {
46
+ "loc": ["body", self.field],
47
+ "msg": self.message,
48
+ "type": self.type,
49
+ }
50
+ ]
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,10 @@ class FalServerlessConnection:
488
516
  struct_metadata = isolate_proto.Struct()
489
517
  struct_metadata.update(metadata)
490
518
 
519
+ deployment_strategy_proto = DeploymentStrategy[
520
+ deployment_strategy.upper()
521
+ ].to_proto()
522
+
491
523
  request = isolate_proto.RegisterApplicationRequest(
492
524
  function=wrapped_function,
493
525
  environments=environments,
@@ -495,6 +527,7 @@ class FalServerlessConnection:
495
527
  application_name=application_name,
496
528
  auth_mode=auth_mode,
497
529
  metadata=struct_metadata,
530
+ deployment_strategy=deployment_strategy_proto,
498
531
  )
499
532
  for partial_result in self.stub.RegisterApplication(request):
500
533
  yield from_grpc(partial_result)
@@ -149,14 +149,31 @@ class File(BaseModel):
149
149
  path: str | Path,
150
150
  content_type: Optional[str] = None,
151
151
  repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
152
+ multipart: bool | None = None,
152
153
  ) -> File:
153
154
  file_path = Path(path)
154
155
  if not file_path.exists():
155
156
  raise FileNotFoundError(f"File {file_path} does not exist")
156
- with open(file_path, "rb") as f:
157
- data = f.read()
158
- return File.from_bytes(
159
- data, content_type, file_name=file_path.name, repository=repository
157
+
158
+ repo = (
159
+ repository
160
+ if isinstance(repository, FileRepository)
161
+ else get_builtin_repository(repository)
162
+ )
163
+
164
+ content_type = content_type or "application/octet-stream"
165
+
166
+ url, data = repo.save_file(
167
+ file_path,
168
+ content_type=content_type,
169
+ multipart=multipart,
170
+ )
171
+ return cls(
172
+ url=url,
173
+ file_data=data.data if data else None,
174
+ content_type=content_type,
175
+ file_name=file_path.name,
176
+ file_size=file_path.stat().st_size,
160
177
  )
161
178
 
162
179
  def as_bytes(self) -> bytes: