fal 1.16.0__tar.gz → 1.17.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fal might be problematic. Click here for more details.

Files changed (179) hide show
  1. {fal-1.16.0/fal.egg-info → fal-1.17.0}/PKG-INFO +1 -1
  2. {fal-1.16.0 → fal-1.17.0/fal.egg-info}/PKG-INFO +1 -1
  3. {fal-1.16.0 → fal-1.17.0}/fal.egg-info/SOURCES.txt +1 -0
  4. {fal-1.16.0 → fal-1.17.0}/src/fal/_fal_version.py +2 -2
  5. fal-1.17.0/src/fal/_version.py +89 -0
  6. {fal-1.16.0 → fal-1.17.0}/src/fal/api.py +7 -7
  7. {fal-1.16.0 → fal-1.17.0}/src/fal/app.py +6 -9
  8. {fal-1.16.0 → fal-1.17.0}/src/fal/auth/__init__.py +7 -2
  9. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/main.py +37 -0
  10. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/profile.py +1 -1
  11. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/runners.py +6 -2
  12. {fal-1.16.0 → fal-1.17.0}/src/fal/config.py +1 -1
  13. {fal-1.16.0 → fal-1.17.0}/src/fal/files.py +34 -21
  14. fal-1.17.0/src/fal/toolkit/utils/endpoint.py +29 -0
  15. {fal-1.16.0 → fal-1.17.0}/tests/test_apps.py +82 -8
  16. fal-1.16.0/src/fal/_version.py +0 -6
  17. {fal-1.16.0 → fal-1.17.0}/.gitignore +0 -0
  18. {fal-1.16.0 → fal-1.17.0}/Makefile +0 -0
  19. {fal-1.16.0 → fal-1.17.0}/README.md +0 -0
  20. {fal-1.16.0 → fal-1.17.0}/docs/conf.py +0 -0
  21. {fal-1.16.0 → fal-1.17.0}/docs/index.rst +0 -0
  22. {fal-1.16.0 → fal-1.17.0}/fal.egg-info/dependency_links.txt +0 -0
  23. {fal-1.16.0 → fal-1.17.0}/fal.egg-info/entry_points.txt +0 -0
  24. {fal-1.16.0 → fal-1.17.0}/fal.egg-info/requires.txt +0 -0
  25. {fal-1.16.0 → fal-1.17.0}/fal.egg-info/top_level.txt +0 -0
  26. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/README.md +0 -0
  27. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  28. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  29. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  30. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  31. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  32. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  33. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  34. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  35. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  36. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  37. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  38. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  39. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  40. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  41. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  42. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  43. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  44. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  45. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  46. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  47. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  48. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  49. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  50. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  51. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  52. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  53. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  54. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  55. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  56. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  57. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  58. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  59. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  60. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
  61. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  62. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  63. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  64. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  65. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  66. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  67. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  68. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  69. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  70. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  71. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  72. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  73. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  74. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  75. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  76. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  77. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  78. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  79. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  80. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  81. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  82. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  83. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  84. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  85. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  86. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  87. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  88. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  89. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  90. {fal-1.16.0 → fal-1.17.0}/openapi-fal-rest/pyproject.toml +0 -0
  91. {fal-1.16.0 → fal-1.17.0}/openapi_rest.config.yaml +0 -0
  92. {fal-1.16.0 → fal-1.17.0}/pyproject.toml +0 -0
  93. {fal-1.16.0 → fal-1.17.0}/setup.cfg +0 -0
  94. {fal-1.16.0 → fal-1.17.0}/src/fal/__init__.py +0 -0
  95. {fal-1.16.0 → fal-1.17.0}/src/fal/__main__.py +0 -0
  96. {fal-1.16.0 → fal-1.17.0}/src/fal/_serialization.py +0 -0
  97. {fal-1.16.0 → fal-1.17.0}/src/fal/apps.py +0 -0
  98. {fal-1.16.0 → fal-1.17.0}/src/fal/auth/auth0.py +0 -0
  99. {fal-1.16.0 → fal-1.17.0}/src/fal/auth/local.py +0 -0
  100. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/__init__.py +0 -0
  101. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/_utils.py +0 -0
  102. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/api.py +0 -0
  103. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/apps.py +0 -0
  104. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/auth.py +0 -0
  105. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/cli_nested_json.py +0 -0
  106. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/create.py +0 -0
  107. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/debug.py +0 -0
  108. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/deploy.py +0 -0
  109. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/doctor.py +0 -0
  110. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/files.py +0 -0
  111. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/keys.py +0 -0
  112. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/parser.py +0 -0
  113. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/run.py +0 -0
  114. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/secrets.py +0 -0
  115. {fal-1.16.0 → fal-1.17.0}/src/fal/cli/teams.py +0 -0
  116. {fal-1.16.0 → fal-1.17.0}/src/fal/console/__init__.py +0 -0
  117. {fal-1.16.0 → fal-1.17.0}/src/fal/console/icons.py +0 -0
  118. {fal-1.16.0 → fal-1.17.0}/src/fal/console/ux.py +0 -0
  119. {fal-1.16.0 → fal-1.17.0}/src/fal/container.py +0 -0
  120. {fal-1.16.0 → fal-1.17.0}/src/fal/exceptions/__init__.py +0 -0
  121. {fal-1.16.0 → fal-1.17.0}/src/fal/exceptions/_base.py +0 -0
  122. {fal-1.16.0 → fal-1.17.0}/src/fal/exceptions/_cuda.py +0 -0
  123. {fal-1.16.0 → fal-1.17.0}/src/fal/exceptions/auth.py +0 -0
  124. {fal-1.16.0 → fal-1.17.0}/src/fal/flags.py +0 -0
  125. {fal-1.16.0 → fal-1.17.0}/src/fal/logging/__init__.py +0 -0
  126. {fal-1.16.0 → fal-1.17.0}/src/fal/logging/isolate.py +0 -0
  127. {fal-1.16.0 → fal-1.17.0}/src/fal/logging/style.py +0 -0
  128. {fal-1.16.0 → fal-1.17.0}/src/fal/logging/trace.py +0 -0
  129. {fal-1.16.0 → fal-1.17.0}/src/fal/logging/user.py +0 -0
  130. {fal-1.16.0 → fal-1.17.0}/src/fal/project.py +0 -0
  131. {fal-1.16.0 → fal-1.17.0}/src/fal/py.typed +0 -0
  132. {fal-1.16.0 → fal-1.17.0}/src/fal/rest_client.py +0 -0
  133. {fal-1.16.0 → fal-1.17.0}/src/fal/sdk.py +0 -0
  134. {fal-1.16.0 → fal-1.17.0}/src/fal/sync.py +0 -0
  135. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/__init__.py +0 -0
  136. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/exceptions.py +0 -0
  137. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/file/__init__.py +0 -0
  138. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/file/file.py +0 -0
  139. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/file/providers/fal.py +0 -0
  140. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/file/providers/gcp.py +0 -0
  141. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/file/providers/r2.py +0 -0
  142. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/file/providers/s3.py +0 -0
  143. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/file/types.py +0 -0
  144. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/image/__init__.py +0 -0
  145. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/image/image.py +0 -0
  146. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
  147. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
  148. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
  149. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
  150. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
  151. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/image/safety_checker.py +0 -0
  152. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/optimize.py +0 -0
  153. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/types.py +0 -0
  154. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/utils/__init__.py +0 -0
  155. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/utils/download_utils.py +0 -0
  156. {fal-1.16.0 → fal-1.17.0}/src/fal/toolkit/utils/retry.py +0 -0
  157. {fal-1.16.0 → fal-1.17.0}/src/fal/utils.py +0 -0
  158. {fal-1.16.0 → fal-1.17.0}/src/fal/workflows.py +0 -0
  159. {fal-1.16.0 → fal-1.17.0}/tests/__init__.py +0 -0
  160. {fal-1.16.0 → fal-1.17.0}/tests/assets/cat.png +0 -0
  161. {fal-1.16.0 → fal-1.17.0}/tests/cli/__init__.py +0 -0
  162. {fal-1.16.0 → fal-1.17.0}/tests/cli/test_apps.py +0 -0
  163. {fal-1.16.0 → fal-1.17.0}/tests/cli/test_auth.py +0 -0
  164. {fal-1.16.0 → fal-1.17.0}/tests/cli/test_deploy.py +0 -0
  165. {fal-1.16.0 → fal-1.17.0}/tests/cli/test_keys.py +0 -0
  166. {fal-1.16.0 → fal-1.17.0}/tests/cli/test_run.py +0 -0
  167. {fal-1.16.0 → fal-1.17.0}/tests/cli/test_secrets.py +0 -0
  168. {fal-1.16.0 → fal-1.17.0}/tests/conftest.py +0 -0
  169. {fal-1.16.0 → fal-1.17.0}/tests/integration_test.py +0 -0
  170. {fal-1.16.0 → fal-1.17.0}/tests/mainify_package/__init__.py +0 -0
  171. {fal-1.16.0 → fal-1.17.0}/tests/mainify_package/impl.py +0 -0
  172. {fal-1.16.0 → fal-1.17.0}/tests/mainify_package/utils.py +0 -0
  173. {fal-1.16.0 → fal-1.17.0}/tests/mainify_target.py +0 -0
  174. {fal-1.16.0 → fal-1.17.0}/tests/test_stability.py +0 -0
  175. {fal-1.16.0 → fal-1.17.0}/tests/toolkit/file_test.py +0 -0
  176. {fal-1.16.0 → fal-1.17.0}/tests/toolkit/image_test.py +0 -0
  177. {fal-1.16.0 → fal-1.17.0}/tests/toolkit/test_types.py +0 -0
  178. {fal-1.16.0 → fal-1.17.0}/tests/toolkit/utils/retry.py +0 -0
  179. {fal-1.16.0 → fal-1.17.0}/tools/demo_script.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fal
3
- Version: 1.16.0
3
+ Version: 1.17.0
4
4
  Summary: fal is an easy-to-use Serverless Python Framework
5
5
  Author: Features & Labels <support@fal.ai>
6
6
  Requires-Python: >=3.8
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fal
3
- Version: 1.16.0
3
+ Version: 1.17.0
4
4
  Summary: fal is an easy-to-use Serverless Python Framework
5
5
  Author: Features & Labels <support@fal.ai>
6
6
  Requires-Python: >=3.8
@@ -150,6 +150,7 @@ src/fal/toolkit/image/nsfw_filter/model.py
150
150
  src/fal/toolkit/image/nsfw_filter/requirements.txt
151
151
  src/fal/toolkit/utils/__init__.py
152
152
  src/fal/toolkit/utils/download_utils.py
153
+ src/fal/toolkit/utils/endpoint.py
153
154
  src/fal/toolkit/utils/retry.py
154
155
  tests/__init__.py
155
156
  tests/conftest.py
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '1.16.0'
21
- __version_tuple__ = version_tuple = (1, 16, 0)
20
+ __version__ = version = '1.17.0'
21
+ __version_tuple__ = version_tuple = (1, 17, 0)
@@ -0,0 +1,89 @@
1
+ import json
2
+ import os
3
+ import tempfile
4
+ from typing import Any, Dict, Optional
5
+
6
+ try:
7
+ from ._fal_version import version as __version__ # type: ignore[import]
8
+ from ._fal_version import version_tuple # type: ignore[import]
9
+ except ImportError:
10
+ __version__ = "UNKNOWN"
11
+ version_tuple = (0, 0, __version__) # type: ignore[assignment]
12
+
13
+
14
+ _PYPI_URL = "https://pypi.org/pypi/fal/json"
15
+ _PYPI_CACHE_TTL = 60 * 60 # 1 hour
16
+ _PYPI_CACHE_PATH = os.path.expanduser("~/.fal/cache/pypi.json")
17
+ _URLOPEN_TIMEOUT = 1
18
+
19
+
20
+ def _write_pypi_cache(data: Dict[str, Any]) -> None:
21
+ cache_dir = os.path.dirname(_PYPI_CACHE_PATH)
22
+ os.makedirs(cache_dir, exist_ok=True)
23
+ prefix = os.path.basename(_PYPI_CACHE_PATH) + ".tmp."
24
+ with tempfile.NamedTemporaryFile(
25
+ mode="w",
26
+ dir=cache_dir,
27
+ prefix=prefix,
28
+ delete=False,
29
+ ) as fobj:
30
+ fobj.write(json.dumps(data))
31
+ os.rename(fobj.name, _PYPI_CACHE_PATH)
32
+
33
+
34
+ def _get_pypi_cache() -> Optional[Dict[str, Any]]:
35
+ import time
36
+
37
+ try:
38
+ mtime = os.path.getmtime(_PYPI_CACHE_PATH)
39
+ except FileNotFoundError:
40
+ return None
41
+
42
+ if mtime + _PYPI_CACHE_TTL < time.time():
43
+ return None
44
+
45
+ with open(_PYPI_CACHE_PATH) as fobj:
46
+ try:
47
+ return json.load(fobj)
48
+ except ValueError:
49
+ return None
50
+
51
+
52
+ def _fetch_pypi_data() -> Dict[str, Any]:
53
+ from urllib.request import urlopen
54
+
55
+ response = urlopen(_PYPI_URL, timeout=_URLOPEN_TIMEOUT)
56
+ if response.status != 200:
57
+ raise Exception(f"Failed to fetch {_PYPI_URL}")
58
+
59
+ data = response.read()
60
+ return json.loads(data)
61
+
62
+
63
+ def get_latest_version() -> str:
64
+ from fal.logging import get_logger
65
+
66
+ logger = get_logger(__name__)
67
+
68
+ try:
69
+ data = _get_pypi_cache()
70
+ except Exception:
71
+ logger.warning("Failed to get pypi cache", exc_info=True)
72
+ data = None
73
+
74
+ if data is None:
75
+ try:
76
+ data = _fetch_pypi_data()
77
+ except Exception:
78
+ logger.warning("Failed to get latest fal version", exc_info=True)
79
+ data = {}
80
+
81
+ try:
82
+ _write_pypi_cache(data)
83
+ except Exception:
84
+ logger.warning("Failed to write pypi cache", exc_info=True)
85
+
86
+ try:
87
+ return data["info"]["version"]
88
+ except KeyError:
89
+ return "0.0.0"
@@ -501,12 +501,7 @@ class FalServerlessHost(Host):
501
501
  if isinstance(func, ServeWrapper):
502
502
  # Assigning in a separate property leaving a place for the user
503
503
  # to add more metadata in the future
504
- try:
505
- metadata["openapi"] = func.openapi()
506
- except Exception as e:
507
- print(
508
- f"[warning] Failed to generate OpenAPI metadata for function: {e}"
509
- )
504
+ metadata["openapi"] = func.openapi()
510
505
 
511
506
  for partial_result in self._connection.register(
512
507
  partial_func,
@@ -1169,7 +1164,12 @@ class BaseServable:
1169
1164
  Build the OpenAPI specification for the served function.
1170
1165
  Attach needed metadata for a better integration to fal.
1171
1166
  """
1172
- return self._build_app().openapi()
1167
+ try:
1168
+ return self._build_app().openapi()
1169
+ except Exception as e:
1170
+ raise FalServerlessException(
1171
+ "Failed to generate OpenAPI metadata for function"
1172
+ ) from e
1173
1173
 
1174
1174
  def serve(self) -> None:
1175
1175
  import asyncio
@@ -105,15 +105,12 @@ def wrap_app(cls: type[App], **kwargs) -> IsolatedFunction:
105
105
  app.serve()
106
106
 
107
107
  metadata = {}
108
- try:
109
- app = cls(_allow_init=True)
110
- metadata["openapi"] = app.openapi()
111
- except Exception:
112
- logger.warning("Failed to build OpenAPI specification for %s", cls.__name__)
113
- realtime_app = False
114
- else:
115
- routes = app.collect_routes()
116
- realtime_app = any(route.is_websocket for route in routes)
108
+ app = cls(_allow_init=True)
109
+
110
+ metadata["openapi"] = app.openapi()
111
+
112
+ routes = app.collect_routes()
113
+ realtime_app = any(route.is_websocket for route in routes)
117
114
 
118
115
  kind = cls.host_kwargs.pop("kind", "virtualenv")
119
116
  if kind == "container":
@@ -63,8 +63,13 @@ def key_credentials() -> tuple[str, str] | None:
63
63
 
64
64
  key = os.environ.get("FAL_KEY") or config.get("key") or get_colab_token()
65
65
  if key:
66
- key_id, key_secret = key.split(":", 1)
67
- return (key_id, key_secret)
66
+ try:
67
+ key_id, key_secret = key.split(":", 1)
68
+ return (key_id, key_secret)
69
+ except ValueError:
70
+ print(f"Invalid key format: {key}")
71
+ return None
72
+
68
73
  elif "FAL_KEY_ID" in os.environ and "FAL_KEY_SECRET" in os.environ:
69
74
  return (os.environ["FAL_KEY_ID"], os.environ["FAL_KEY_SECRET"])
70
75
  else:
@@ -77,11 +77,48 @@ def _print_error(msg):
77
77
  console.print(f"{CROSS_ICON} {msg}")
78
78
 
79
79
 
80
+ def _check_latest_version():
81
+ from packaging.version import parse
82
+ from rich.emoji import Emoji
83
+ from rich.panel import Panel
84
+ from rich.text import Text
85
+
86
+ from fal._version import get_latest_version, version_tuple
87
+
88
+ latest_version = get_latest_version()
89
+ parsed = parse(latest_version)
90
+ latest_version_tuple = (parsed.major, parsed.minor, parsed.micro)
91
+ if latest_version_tuple <= version_tuple:
92
+ return
93
+
94
+ if not console.is_terminal:
95
+ return
96
+
97
+ line1 = Text.assemble(
98
+ (Emoji.replace(":warning-emoji: "), "bold white"),
99
+ ("A new version of fal is available: ", "bold white"),
100
+ (latest_version, "bold green"),
101
+ )
102
+ line2 = Text.assemble(("pip install --upgrade fal", "bold cyan"))
103
+ line2.align("center", width=len(line1))
104
+
105
+ panel = Panel(
106
+ line1 + "\n\n" + line2,
107
+ border_style="yellow",
108
+ padding=(1, 2),
109
+ highlight=True,
110
+ expand=False,
111
+ )
112
+ console.print(panel)
113
+
114
+
80
115
  def main(argv=None) -> int:
81
116
  import grpc
82
117
 
83
118
  from fal.api import UserFunctionException
84
119
 
120
+ _check_latest_version()
121
+
85
122
  ret = 1
86
123
  try:
87
124
  args = parse_args(argv)
@@ -66,7 +66,7 @@ def _delete(args):
66
66
  if config.profile == args.PROFILE:
67
67
  config.set_internal("profile", None)
68
68
 
69
- config.delete(args.PROFILE)
69
+ config.delete_profile(args.PROFILE)
70
70
  args.console.print(f"Profile [cyan]{args.PROFILE}[/] deleted.")
71
71
 
72
72
 
@@ -21,17 +21,21 @@ def runners_table(runners: List[RunnerInfo]):
21
21
  table.add_column("Revision")
22
22
 
23
23
  for runner in runners:
24
+ external_metadata = runner.external_metadata
25
+ present = external_metadata.get("present_in_group", True)
26
+
24
27
  num_leases_with_request = len(
25
28
  [
26
29
  lease
27
- for lease in runner.external_metadata.get("leases", [])
30
+ for lease in external_metadata.get("leases", [])
28
31
  if lease.get("request_id") is not None
29
32
  ]
30
33
  )
31
34
 
32
35
  table.add_row(
33
36
  runner.alias,
34
- runner.runner_id,
37
+ # Mark lost runners in red
38
+ runner.runner_id if present else f"[red]{runner.runner_id}[/]",
35
39
  str(runner.in_flight_requests),
36
40
  str(runner.in_flight_requests - num_leases_with_request),
37
41
  (
@@ -99,7 +99,7 @@ class Config:
99
99
  def unset_internal(self, key: str) -> None:
100
100
  self._config.get(SETTINGS_SECTION, {}).pop(key, None)
101
101
 
102
- def delete(self, profile: str) -> None:
102
+ def delete_profile(self, profile: str) -> None:
103
103
  del self._config[profile]
104
104
 
105
105
  @contextmanager
@@ -28,26 +28,39 @@ class FalFileSystem(AbstractFileSystem):
28
28
  },
29
29
  )
30
30
 
31
+ def _ls(self, path):
32
+ response = self._client.get(f"/files/list/{path}")
33
+ response.raise_for_status()
34
+ files = response.json()
35
+ return sorted(
36
+ (
37
+ {
38
+ "name": entry["path"],
39
+ "size": entry["size"],
40
+ "type": "file" if entry["is_file"] else "directory",
41
+ "mtime": entry["updated_time"],
42
+ }
43
+ for entry in files
44
+ ),
45
+ key=lambda x: x["name"],
46
+ )
47
+
31
48
  def ls(self, path, detail=True, **kwargs):
32
- if path in self.dircache:
33
- entries = self.dircache[path]
49
+ abs_path = "/" + path.lstrip("/")
50
+ if abs_path in self.dircache:
51
+ entries = self.dircache[abs_path]
52
+ elif abs_path in ["/", "", "."]:
53
+ entries = [
54
+ {
55
+ "name": "/data",
56
+ "size": 0,
57
+ "type": "directory",
58
+ "mtime": 0,
59
+ }
60
+ ]
34
61
  else:
35
- response = self._client.get(f"/files/list/{path.lstrip('/')}")
36
- response.raise_for_status()
37
- files = response.json()
38
- entries = sorted(
39
- (
40
- {
41
- "name": entry["path"].lstrip("/data/"),
42
- "size": entry["size"],
43
- "type": "file" if entry["is_file"] else "directory",
44
- "mtime": entry["updated_time"],
45
- }
46
- for entry in files
47
- ),
48
- key=lambda x: x["name"],
49
- )
50
- self.dircache[path] = entries
62
+ entries = self._ls(abs_path)
63
+ self.dircache[abs_path] = entries
51
64
 
52
65
  if detail:
53
66
  return entries
@@ -68,7 +81,7 @@ class FalFileSystem(AbstractFileSystem):
68
81
  return
69
82
 
70
83
  with open(lpath, "wb") as fobj:
71
- response = self._client.get(f"/files/file/{rpath.lstrip('/')}")
84
+ response = self._client.get(f"/files/file/{rpath}")
72
85
  response.raise_for_status()
73
86
  fobj.write(response.content)
74
87
 
@@ -78,13 +91,13 @@ class FalFileSystem(AbstractFileSystem):
78
91
 
79
92
  with open(lpath, "rb") as fobj:
80
93
  response = self._client.post(
81
- f"/files/file/local/{rpath.lstrip('/')}",
94
+ f"/files/file/local/{rpath}",
82
95
  files={"file_upload": (posixpath.basename(lpath), fobj, "text/plain")},
83
96
  )
84
97
  response.raise_for_status()
85
98
  self.dircache.clear()
86
99
 
87
100
  def rm(self, path, **kwargs):
88
- response = self._client.delete(f"/files/file/{path.lstrip('/')}")
101
+ response = self._client.delete(f"/files/file/{path}")
89
102
  response.raise_for_status()
90
103
  self.dircache.clear()
@@ -0,0 +1,29 @@
1
+ from contextlib import asynccontextmanager
2
+
3
+ from anyio import create_task_group
4
+ from fastapi import Request
5
+
6
+
7
+ @asynccontextmanager
8
+ async def cancel_on_disconnect(request: Request):
9
+ """
10
+ Async context manager for async code that needs to be cancelled if client
11
+ disconnects prematurely.
12
+ The client disconnect is monitored through the Request object.
13
+ """
14
+ async with create_task_group() as tg:
15
+
16
+ async def watch_disconnect():
17
+ while True:
18
+ message = await request.receive()
19
+
20
+ if message["type"] == "http.disconnect":
21
+ tg.cancel_scope.cancel()
22
+ break
23
+
24
+ tg.start_soon(watch_disconnect)
25
+
26
+ try:
27
+ yield
28
+ finally:
29
+ tg.cancel_scope.cancel()
@@ -10,7 +10,7 @@ from typing import Generator, List, Tuple
10
10
 
11
11
  import httpx
12
12
  import pytest
13
- from fastapi import WebSocket
13
+ from fastapi import Request, WebSocket
14
14
  from httpx import HTTPStatusError
15
15
  from isolate.backends.common import active_python
16
16
  from openapi_fal_rest.api.applications import app_metadata
@@ -23,9 +23,15 @@ from fal import apps
23
23
  from fal.app import AppClient, AppClientError
24
24
  from fal.cli.deploy import User, _get_user
25
25
  from fal.container import ContainerImage
26
- from fal.exceptions import AppException, FieldException, RequestCancelledException
26
+ from fal.exceptions import (
27
+ AppException,
28
+ FalServerlessException,
29
+ FieldException,
30
+ RequestCancelledException,
31
+ )
27
32
  from fal.exceptions._cuda import _CUDA_OOM_MESSAGE, _CUDA_OOM_STATUS_CODE
28
33
  from fal.rest_client import REST_CLIENT
34
+ from fal.toolkit.utils.endpoint import cancel_on_disconnect
29
35
  from fal.workflows import Workflow
30
36
 
31
37
 
@@ -228,13 +234,17 @@ class ExceptionApp(fal.App, keep_alive=300, max_concurrency=1):
228
234
  raise RuntimeError("cuDNN error: CUDNN_STATUS_INTERNAL_ERROR")
229
235
 
230
236
 
231
- class CancellableApp(fal.App, keep_alive=300, max_concurrency=1):
237
+ class CancellableApp(fal.App, keep_alive=300, max_concurrency=1, request_timeout=10):
232
238
  task = None
239
+ running = 0
240
+
241
+ async def _sleep(self, input: Input):
242
+ if self.running > 0:
243
+ raise Exception("App is already running")
233
244
 
234
- @fal.endpoint("/")
235
- async def sleep(self, input: Input) -> Output:
236
245
  self.task = asyncio.create_task(asyncio.sleep(input.wait_time))
237
246
  try:
247
+ self.running += 1
238
248
  await self.task
239
249
  except asyncio.CancelledError:
240
250
  print("Task was cancelled")
@@ -244,9 +254,20 @@ class CancellableApp(fal.App, keep_alive=300, max_concurrency=1):
244
254
  await self.task
245
255
 
246
256
  raise RequestCancelledException("Request cancelled by the client.")
247
-
257
+ finally:
258
+ self.task = None
259
+ self.running -= 1
248
260
  return Output(result=input.lhs + input.rhs)
249
261
 
262
+ @fal.endpoint("/")
263
+ async def sleep(self, input: Input) -> Output:
264
+ return await self._sleep(input)
265
+
266
+ @fal.endpoint("/well-handled")
267
+ async def well_handled(self, input: Input, request: Request) -> Output:
268
+ async with cancel_on_disconnect(request):
269
+ return await self._sleep(input)
270
+
250
271
  @fal.endpoint("/cancel")
251
272
  async def cancel_handler(self) -> Output:
252
273
  if self.task:
@@ -301,6 +322,14 @@ class RealtimeApp(fal.App, keep_alive=300, max_concurrency=1):
301
322
  return RTOutputs(texts=[input.prompt] + [i.prompt for i in inputs])
302
323
 
303
324
 
325
+ class BrokenApp(fal.App, keep_alive=300, max_concurrency=1):
326
+ machine_type = "S"
327
+
328
+ @fal.endpoint("/")
329
+ def broken(self) -> Exception:
330
+ raise Exception("this app is designed to fail")
331
+
332
+
304
333
  @pytest.fixture(scope="module")
305
334
  def host() -> Generator[api.FalServerlessHost, None, None]:
306
335
  yield addition_app.host
@@ -400,6 +429,13 @@ def test_realtime_app(host: api.FalServerlessHost, user: User):
400
429
  yield f"{user.username}/{app_alias}"
401
430
 
402
431
 
432
+ def test_broken_app_failure(host: api.FalServerlessHost, user: User):
433
+ with pytest.raises(FalServerlessException) as e:
434
+ fal.wrap_app(BrokenApp)
435
+
436
+ assert "Failed to generate OpenAPI" in str(e)
437
+
438
+
403
439
  def test_app_client(test_app: str, test_nomad_app: str):
404
440
  response = apps.run(test_app, arguments={"lhs": 1, "rhs": 2})
405
441
  assert response["result"] == 3
@@ -467,7 +503,7 @@ def test_stateful_app_client(test_stateful_app: str):
467
503
 
468
504
  def test_app_cancellation(test_app: str, test_cancellable_app: str):
469
505
  request_handle = apps.submit(
470
- test_cancellable_app, arguments={"lhs": 1, "rhs": 2, "wait_time": 10}
506
+ test_cancellable_app, arguments={"lhs": 1, "rhs": 2, "wait_time": 5}
471
507
  )
472
508
 
473
509
  while True:
@@ -487,7 +523,7 @@ def test_app_cancellation(test_app: str, test_cancellable_app: str):
487
523
 
488
524
  # normal app should just ignore the cancellation
489
525
  request_handle = apps.submit(
490
- test_app, arguments={"lhs": 1, "rhs": 2, "wait_time": 10}
526
+ test_app, arguments={"lhs": 1, "rhs": 2, "wait_time": 5}
491
527
  )
492
528
 
493
529
  while True:
@@ -504,6 +540,44 @@ def test_app_cancellation(test_app: str, test_cancellable_app: str):
504
540
  assert response == {"result": 3}
505
541
 
506
542
 
543
+ def test_app_disconnect_behavior(test_app: str, test_cancellable_app: str):
544
+ with pytest.raises(HTTPStatusError) as e:
545
+ apps.run(
546
+ test_cancellable_app,
547
+ arguments={"lhs": 1, "rhs": 2, "wait_time": 20},
548
+ path="/well-handled",
549
+ )
550
+ assert (
551
+ e.value.response.status_code == 504
552
+ ), "Expected Gateway Timeout even though the app handled it"
553
+
554
+ # and running it again shows the app "handled" it
555
+ response = apps.run(
556
+ test_cancellable_app,
557
+ arguments={"lhs": 1, "rhs": 2, "wait_time": 1},
558
+ path="/well-handled",
559
+ )
560
+ assert response == {"result": 3}
561
+
562
+ # vs on an unhandled one
563
+
564
+ with pytest.raises(HTTPStatusError) as e:
565
+ apps.run(
566
+ test_cancellable_app,
567
+ arguments={"lhs": 1, "rhs": 2, "wait_time": 20},
568
+ )
569
+ assert (
570
+ e.value.response.status_code == 504
571
+ ), "Expected Gateway Timeout even though the app handled it"
572
+
573
+ with pytest.raises(HTTPStatusError) as e:
574
+ apps.run(
575
+ test_cancellable_app,
576
+ arguments={"lhs": 1, "rhs": 2, "wait_time": 1},
577
+ )
578
+ assert e.value.response.status_code == 500
579
+
580
+
507
581
  @pytest.mark.xfail(
508
582
  reason="Temporary disabled while investigating backend issue. Ping @efiop"
509
583
  )
@@ -1,6 +0,0 @@
1
- try:
2
- from ._fal_version import version as __version__ # type: ignore[import]
3
- from ._fal_version import version_tuple # type: ignore[import]
4
- except ImportError:
5
- __version__ = "UNKNOWN"
6
- version_tuple = (0, 0, __version__) # type: ignore[assignment]
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