fal 1.26.0__tar.gz → 1.26.1__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 (185) hide show
  1. {fal-1.26.0/fal.egg-info → fal-1.26.1}/PKG-INFO +1 -1
  2. {fal-1.26.0 → fal-1.26.1/fal.egg-info}/PKG-INFO +1 -1
  3. {fal-1.26.0 → fal-1.26.1}/src/fal/_fal_version.py +2 -2
  4. {fal-1.26.0 → fal-1.26.1}/src/fal/api.py +1 -1
  5. {fal-1.26.0 → fal-1.26.1}/src/fal/app.py +50 -13
  6. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/apps.py +1 -1
  7. {fal-1.26.0 → fal-1.26.1}/src/fal/files.py +96 -6
  8. {fal-1.26.0 → fal-1.26.1}/src/fal/sdk.py +17 -11
  9. {fal-1.26.0 → fal-1.26.1}/tests/test_apps.py +3 -3
  10. {fal-1.26.0 → fal-1.26.1}/.gitignore +0 -0
  11. {fal-1.26.0 → fal-1.26.1}/Makefile +0 -0
  12. {fal-1.26.0 → fal-1.26.1}/README.md +0 -0
  13. {fal-1.26.0 → fal-1.26.1}/docs/conf.py +0 -0
  14. {fal-1.26.0 → fal-1.26.1}/docs/index.rst +0 -0
  15. {fal-1.26.0 → fal-1.26.1}/fal.egg-info/SOURCES.txt +0 -0
  16. {fal-1.26.0 → fal-1.26.1}/fal.egg-info/dependency_links.txt +0 -0
  17. {fal-1.26.0 → fal-1.26.1}/fal.egg-info/entry_points.txt +0 -0
  18. {fal-1.26.0 → fal-1.26.1}/fal.egg-info/requires.txt +0 -0
  19. {fal-1.26.0 → fal-1.26.1}/fal.egg-info/top_level.txt +0 -0
  20. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/README.md +0 -0
  21. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  22. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  23. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  24. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  25. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  26. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  27. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  28. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  29. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  30. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  31. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  32. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  33. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  34. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  35. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  36. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  37. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  38. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  39. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  40. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  41. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  42. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  43. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  44. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  45. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  46. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  47. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  48. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  49. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  50. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  51. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  52. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  53. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  54. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
  55. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  56. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  57. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  58. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  59. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  60. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  61. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  62. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  63. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  64. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  65. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  66. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  67. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  68. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  69. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  70. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  71. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  72. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  73. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  74. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  75. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  76. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  77. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  78. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  79. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  80. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  81. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  82. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  83. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  84. {fal-1.26.0 → fal-1.26.1}/openapi-fal-rest/pyproject.toml +0 -0
  85. {fal-1.26.0 → fal-1.26.1}/openapi_rest.config.yaml +0 -0
  86. {fal-1.26.0 → fal-1.26.1}/pyproject.toml +0 -0
  87. {fal-1.26.0 → fal-1.26.1}/setup.cfg +0 -0
  88. {fal-1.26.0 → fal-1.26.1}/src/fal/__init__.py +0 -0
  89. {fal-1.26.0 → fal-1.26.1}/src/fal/__main__.py +0 -0
  90. {fal-1.26.0 → fal-1.26.1}/src/fal/_serialization.py +0 -0
  91. {fal-1.26.0 → fal-1.26.1}/src/fal/_version.py +0 -0
  92. {fal-1.26.0 → fal-1.26.1}/src/fal/apps.py +0 -0
  93. {fal-1.26.0 → fal-1.26.1}/src/fal/auth/__init__.py +0 -0
  94. {fal-1.26.0 → fal-1.26.1}/src/fal/auth/auth0.py +0 -0
  95. {fal-1.26.0 → fal-1.26.1}/src/fal/auth/local.py +0 -0
  96. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/__init__.py +0 -0
  97. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/_utils.py +0 -0
  98. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/api.py +0 -0
  99. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/auth.py +0 -0
  100. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/cli_nested_json.py +0 -0
  101. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/create.py +0 -0
  102. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/debug.py +0 -0
  103. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/deploy.py +0 -0
  104. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/doctor.py +0 -0
  105. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/files.py +0 -0
  106. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/keys.py +0 -0
  107. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/main.py +0 -0
  108. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/parser.py +0 -0
  109. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/profile.py +0 -0
  110. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/run.py +0 -0
  111. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/runners.py +0 -0
  112. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/secrets.py +0 -0
  113. {fal-1.26.0 → fal-1.26.1}/src/fal/cli/teams.py +0 -0
  114. {fal-1.26.0 → fal-1.26.1}/src/fal/config.py +0 -0
  115. {fal-1.26.0 → fal-1.26.1}/src/fal/console/__init__.py +0 -0
  116. {fal-1.26.0 → fal-1.26.1}/src/fal/console/icons.py +0 -0
  117. {fal-1.26.0 → fal-1.26.1}/src/fal/console/ux.py +0 -0
  118. {fal-1.26.0 → fal-1.26.1}/src/fal/container.py +0 -0
  119. {fal-1.26.0 → fal-1.26.1}/src/fal/exceptions/__init__.py +0 -0
  120. {fal-1.26.0 → fal-1.26.1}/src/fal/exceptions/_base.py +0 -0
  121. {fal-1.26.0 → fal-1.26.1}/src/fal/exceptions/_cuda.py +0 -0
  122. {fal-1.26.0 → fal-1.26.1}/src/fal/exceptions/auth.py +0 -0
  123. {fal-1.26.0 → fal-1.26.1}/src/fal/flags.py +0 -0
  124. {fal-1.26.0 → fal-1.26.1}/src/fal/logging/__init__.py +0 -0
  125. {fal-1.26.0 → fal-1.26.1}/src/fal/logging/isolate.py +0 -0
  126. {fal-1.26.0 → fal-1.26.1}/src/fal/logging/style.py +0 -0
  127. {fal-1.26.0 → fal-1.26.1}/src/fal/logging/trace.py +0 -0
  128. {fal-1.26.0 → fal-1.26.1}/src/fal/logging/user.py +0 -0
  129. {fal-1.26.0 → fal-1.26.1}/src/fal/project.py +0 -0
  130. {fal-1.26.0 → fal-1.26.1}/src/fal/py.typed +0 -0
  131. {fal-1.26.0 → fal-1.26.1}/src/fal/rest_client.py +0 -0
  132. {fal-1.26.0 → fal-1.26.1}/src/fal/sync.py +0 -0
  133. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/__init__.py +0 -0
  134. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/audio/__init__.py +0 -0
  135. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/audio/audio.py +0 -0
  136. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/exceptions.py +0 -0
  137. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/file/__init__.py +0 -0
  138. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/file/file.py +0 -0
  139. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/file/providers/fal.py +0 -0
  140. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/file/providers/gcp.py +0 -0
  141. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/file/providers/r2.py +0 -0
  142. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/file/providers/s3.py +0 -0
  143. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/file/types.py +0 -0
  144. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/image/__init__.py +0 -0
  145. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/image/image.py +0 -0
  146. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
  147. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
  148. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
  149. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
  150. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
  151. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/image/safety_checker.py +0 -0
  152. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/kv.py +0 -0
  153. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/optimize.py +0 -0
  154. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/types.py +0 -0
  155. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/utils/__init__.py +0 -0
  156. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/utils/download_utils.py +0 -0
  157. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/utils/endpoint.py +0 -0
  158. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/utils/retry.py +0 -0
  159. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/video/__init__.py +0 -0
  160. {fal-1.26.0 → fal-1.26.1}/src/fal/toolkit/video/video.py +0 -0
  161. {fal-1.26.0 → fal-1.26.1}/src/fal/utils.py +0 -0
  162. {fal-1.26.0 → fal-1.26.1}/src/fal/workflows.py +0 -0
  163. {fal-1.26.0 → fal-1.26.1}/tests/__init__.py +0 -0
  164. {fal-1.26.0 → fal-1.26.1}/tests/assets/cat.png +0 -0
  165. {fal-1.26.0 → fal-1.26.1}/tests/cli/__init__.py +0 -0
  166. {fal-1.26.0 → fal-1.26.1}/tests/cli/test_apps.py +0 -0
  167. {fal-1.26.0 → fal-1.26.1}/tests/cli/test_auth.py +0 -0
  168. {fal-1.26.0 → fal-1.26.1}/tests/cli/test_deploy.py +0 -0
  169. {fal-1.26.0 → fal-1.26.1}/tests/cli/test_keys.py +0 -0
  170. {fal-1.26.0 → fal-1.26.1}/tests/cli/test_run.py +0 -0
  171. {fal-1.26.0 → fal-1.26.1}/tests/cli/test_secrets.py +0 -0
  172. {fal-1.26.0 → fal-1.26.1}/tests/conftest.py +0 -0
  173. {fal-1.26.0 → fal-1.26.1}/tests/integration_test.py +0 -0
  174. {fal-1.26.0 → fal-1.26.1}/tests/mainify_package/__init__.py +0 -0
  175. {fal-1.26.0 → fal-1.26.1}/tests/mainify_package/impl.py +0 -0
  176. {fal-1.26.0 → fal-1.26.1}/tests/mainify_package/utils.py +0 -0
  177. {fal-1.26.0 → fal-1.26.1}/tests/mainify_target.py +0 -0
  178. {fal-1.26.0 → fal-1.26.1}/tests/test_kv.py +0 -0
  179. {fal-1.26.0 → fal-1.26.1}/tests/test_stability.py +0 -0
  180. {fal-1.26.0 → fal-1.26.1}/tests/toolkit/file/providers/test_fal_retry.py +0 -0
  181. {fal-1.26.0 → fal-1.26.1}/tests/toolkit/file_test.py +0 -0
  182. {fal-1.26.0 → fal-1.26.1}/tests/toolkit/image_test.py +0 -0
  183. {fal-1.26.0 → fal-1.26.1}/tests/toolkit/test_types.py +0 -0
  184. {fal-1.26.0 → fal-1.26.1}/tests/toolkit/utils/retry.py +0 -0
  185. {fal-1.26.0 → fal-1.26.1}/tools/demo_script.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fal
3
- Version: 1.26.0
3
+ Version: 1.26.1
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.26.0
3
+ Version: 1.26.1
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
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '1.26.0'
21
- __version_tuple__ = version_tuple = (1, 26, 0)
20
+ __version__ = version = '1.26.1'
21
+ __version_tuple__ = version_tuple = (1, 26, 1)
@@ -500,7 +500,7 @@ class FalServerlessHost(Host):
500
500
  partial_func,
501
501
  environments,
502
502
  application_name=application_name,
503
- application_auth_mode=application_auth_mode,
503
+ auth_mode=application_auth_mode,
504
504
  machine_requirements=machine_requirements,
505
505
  metadata=metadata,
506
506
  deployment_strategy=deployment_strategy,
@@ -197,7 +197,14 @@ class AppClient:
197
197
 
198
198
  @classmethod
199
199
  @contextmanager
200
- def connect(cls, app_cls):
200
+ def connect(
201
+ cls,
202
+ app_cls,
203
+ *,
204
+ health_request_timeout: int = 30,
205
+ startup_timeout: int = 60,
206
+ health_check_interval: float = 0.5,
207
+ ):
201
208
  app = wrap_app(app_cls)
202
209
  info = app.spawn()
203
210
  _shutdown_event = threading.Event()
@@ -214,20 +221,50 @@ class AppClient:
214
221
  _log_printer.start()
215
222
 
216
223
  try:
224
+ if info.url is None:
225
+ raise AppClientError(
226
+ "App spawn failed: no URL returned",
227
+ status_code=500,
228
+ )
229
+
230
+ start_time = time.perf_counter()
231
+ url = info.url + "/health"
232
+ last_error = None
233
+ attempt = 0
234
+
217
235
  with httpx.Client() as client:
218
- retries = 100
219
- for _ in range(retries):
220
- url = info.url + "/health"
221
- resp = client.get(url, timeout=60)
222
-
223
- if resp.is_success:
224
- break
225
- elif resp.status_code not in (500, 404):
226
- raise AppClientError(
227
- f"Failed to GET {url}: {resp.status_code} {resp.text}",
228
- status_code=resp.status_code,
236
+ while time.perf_counter() - start_time < startup_timeout:
237
+ attempt += 1
238
+
239
+ try:
240
+ resp = client.get(url, timeout=health_request_timeout)
241
+ except httpx.TimeoutException:
242
+ last_error = (
243
+ f"Request timed out after {health_request_timeout} seconds"
229
244
  )
230
- time.sleep(0.1)
245
+ except httpx.TransportError as e:
246
+ last_error = f"Network error: {e}"
247
+ else:
248
+ if resp.is_success:
249
+ break
250
+
251
+ if resp.status_code in (500, 404):
252
+ last_error = f"Server not ready (HTTP {resp.status_code})"
253
+ else:
254
+ raise AppClientError(
255
+ "Health check failed with non-retryable error: "
256
+ f"{resp.status_code} {resp.text}",
257
+ status_code=resp.status_code,
258
+ )
259
+
260
+ time.sleep(health_check_interval)
261
+ else:
262
+ # retry loop completed without success
263
+ raise AppClientError(
264
+ f"Health check failed after {startup_timeout}s "
265
+ f"({attempt} attempts). Last error: {last_error}",
266
+ status_code=500,
267
+ )
231
268
 
232
269
  client = cls(app_cls, info.url)
233
270
  yield client
@@ -270,7 +270,7 @@ def _add_set_rev_parser(subparsers, parents):
270
270
  parser.add_argument(
271
271
  "--auth",
272
272
  choices=ALIAS_AUTH_MODES,
273
- default="private",
273
+ default=None,
274
274
  help="Application authentication mode.",
275
275
  )
276
276
  parser.set_defaults(func=_set_rev)
@@ -1,5 +1,8 @@
1
+ import concurrent.futures
2
+ import math
1
3
  import os
2
4
  import posixpath
5
+ from concurrent.futures import ThreadPoolExecutor
3
6
  from functools import cached_property
4
7
  from typing import TYPE_CHECKING
5
8
 
@@ -9,6 +12,19 @@ if TYPE_CHECKING:
9
12
  import httpx
10
13
 
11
14
  USER_AGENT = "fal-sdk/1.14.0 (python)"
15
+ MULTIPART_THRESHOLD = 10 * 1024 * 1024 # 10MB
16
+ MULTIPART_CHUNK_SIZE = 10 * 1024 * 1024 # 10MB
17
+ MULTIPART_WORKERS = 2 # only 2 because our REST is currently struggling with more
18
+
19
+
20
+ def _compute_md5(lpath, chunk_size=8192):
21
+ import hashlib
22
+
23
+ hasher = hashlib.md5()
24
+ with open(lpath, "rb") as fobj:
25
+ for chunk in iter(lambda: fobj.read(chunk_size), b""):
26
+ hasher.update(chunk)
27
+ return hasher.hexdigest()
12
28
 
13
29
 
14
30
  class FalFileSystem(AbstractFileSystem):
@@ -95,16 +111,90 @@ class FalFileSystem(AbstractFileSystem):
95
111
  response = self._request("GET", f"/files/file/{rpath}")
96
112
  fobj.write(response.content)
97
113
 
114
+ def _put_file_part(self, rpath, lpath, upload_id, part_number, chunk_size):
115
+ offset = (part_number - 1) * chunk_size
116
+ with open(lpath, "rb") as fobj:
117
+ fobj.seek(offset)
118
+ chunk = fobj.read(chunk_size)
119
+ response = self._request(
120
+ "PUT",
121
+ f"/files/file/multipart/{rpath}/{upload_id}/{part_number}",
122
+ files={"file_upload": (posixpath.basename(lpath), chunk)},
123
+ )
124
+ data = response.json()
125
+ return {
126
+ "part_number": data["part_number"],
127
+ "etag": data["etag"],
128
+ }
129
+
130
+ def _put_file_multipart(self, lpath, rpath, size, progress):
131
+ response = self._request(
132
+ "POST",
133
+ f"/files/file/multipart/{rpath}/initiate",
134
+ )
135
+ upload_id = response.json()["upload_id"]
136
+
137
+ parts = []
138
+ num_parts = math.ceil(size / MULTIPART_CHUNK_SIZE)
139
+ md5 = _compute_md5(lpath)
140
+
141
+ task = progress.add_task(f"{os.path.basename(lpath)}", total=num_parts)
142
+
143
+ with ThreadPoolExecutor(max_workers=MULTIPART_WORKERS) as executor:
144
+ futures = []
145
+
146
+ for part_number in range(1, num_parts + 1):
147
+ futures.append(
148
+ executor.submit(
149
+ self._put_file_part,
150
+ rpath,
151
+ lpath,
152
+ upload_id,
153
+ part_number,
154
+ MULTIPART_CHUNK_SIZE,
155
+ )
156
+ )
157
+
158
+ for future in concurrent.futures.as_completed(futures):
159
+ parts.append(future.result())
160
+ progress.advance(task)
161
+
162
+ response = self._request(
163
+ "POST",
164
+ f"/files/file/multipart/{rpath}/{upload_id}/complete",
165
+ json={"parts": parts},
166
+ )
167
+ data = response.json()
168
+ if data["etag"] != md5:
169
+ raise RuntimeError(
170
+ f"MD5 mismatch on {rpath}: {data['etag']} != {md5}, "
171
+ "please contact support"
172
+ )
173
+
98
174
  def put_file(self, lpath, rpath, mode="overwrite", **kwargs):
175
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
176
+
99
177
  if os.path.isdir(lpath):
100
178
  return
101
179
 
102
- with open(lpath, "rb") as fobj:
103
- self._request(
104
- "POST",
105
- f"/files/file/local/{rpath}",
106
- files={"file_upload": (posixpath.basename(lpath), fobj, "text/plain")},
107
- )
180
+ size = os.path.getsize(lpath)
181
+ with Progress(
182
+ SpinnerColumn(),
183
+ TextColumn("[progress.description]{task.description}"),
184
+ BarColumn(),
185
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
186
+ ) as progress:
187
+ if size > MULTIPART_THRESHOLD:
188
+ self._put_file_multipart(lpath, rpath, size, progress)
189
+ else:
190
+ task = progress.add_task(f"{os.path.basename(lpath)}", total=1)
191
+ with open(lpath, "rb") as fobj:
192
+ self._request(
193
+ "POST",
194
+ f"/files/file/local/{rpath}",
195
+ files={"file_upload": (posixpath.basename(lpath), fobj)},
196
+ )
197
+ progress.advance(task)
108
198
  self.dircache.clear()
109
199
 
110
200
  def put_file_from_url(self, url, rpath, mode="overwrite", **kwargs):
@@ -45,6 +45,9 @@ logger = get_logger(__name__)
45
45
  patch_pickle()
46
46
 
47
47
 
48
+ AuthMode = Optional[Literal["public", "private", "shared"]]
49
+
50
+
48
51
  class ServerCredentials:
49
52
  def to_grpc(self) -> grpc.ChannelCredentials:
50
53
  raise NotImplementedError
@@ -565,7 +568,7 @@ class FalServerlessConnection:
565
568
  function: Callable[..., ResultT],
566
569
  environments: list[isolate_proto.EnvironmentDefinition],
567
570
  application_name: str | None = None,
568
- application_auth_mode: Literal["public", "private", "shared"] | None = None,
571
+ auth_mode: AuthMode = None,
569
572
  *,
570
573
  serialization_method: str = _DEFAULT_SERIALIZATION_METHOD,
571
574
  machine_requirements: MachineRequirements | None = None,
@@ -598,13 +601,14 @@ class FalServerlessConnection:
598
601
  else:
599
602
  wrapped_requirements = None
600
603
 
601
- auth_mode = None
602
- if application_auth_mode == "public":
603
- auth_mode = isolate_proto.ApplicationAuthMode.PUBLIC
604
- elif application_auth_mode == "shared":
605
- auth_mode = isolate_proto.ApplicationAuthMode.SHARED
606
- elif application_auth_mode == "private":
607
- auth_mode = isolate_proto.ApplicationAuthMode.PRIVATE
604
+ if auth_mode == "public":
605
+ auth = isolate_proto.ApplicationAuthMode.PUBLIC
606
+ elif auth_mode == "shared":
607
+ auth = isolate_proto.ApplicationAuthMode.SHARED
608
+ elif auth_mode == "private":
609
+ auth = isolate_proto.ApplicationAuthMode.PRIVATE
610
+ else:
611
+ auth = None
608
612
 
609
613
  struct_metadata = None
610
614
  if metadata:
@@ -620,7 +624,7 @@ class FalServerlessConnection:
620
624
  environments=environments,
621
625
  machine_requirements=wrapped_requirements,
622
626
  application_name=application_name,
623
- auth_mode=auth_mode,
627
+ auth_mode=auth,
624
628
  metadata=struct_metadata,
625
629
  deployment_strategy=deployment_strategy_proto,
626
630
  scale=scale,
@@ -730,14 +734,16 @@ class FalServerlessConnection:
730
734
  self,
731
735
  alias: str,
732
736
  revision: str,
733
- auth_mode: Literal["public", "private", "shared"],
737
+ auth_mode: AuthMode,
734
738
  ):
735
739
  if auth_mode == "public":
736
740
  auth = isolate_proto.ApplicationAuthMode.PUBLIC
737
741
  elif auth_mode == "shared":
738
742
  auth = isolate_proto.ApplicationAuthMode.SHARED
739
- else:
743
+ elif auth_mode == "private":
740
744
  auth = isolate_proto.ApplicationAuthMode.PRIVATE
745
+ else:
746
+ auth = None
741
747
 
742
748
  request = isolate_proto.SetAliasRequest(
743
749
  alias=alias,
@@ -235,7 +235,7 @@ class ExceptionApp(fal.App, keep_alive=300, max_concurrency=1):
235
235
  raise RuntimeError("cuDNN error: CUDNN_STATUS_INTERNAL_ERROR")
236
236
 
237
237
 
238
- class CancellableApp(fal.App, keep_alive=300, max_concurrency=1, request_timeout=10):
238
+ class CancellableApp(fal.App, keep_alive=300, max_concurrency=1, request_timeout=2):
239
239
  task = None
240
240
  running = 0
241
241
 
@@ -555,7 +555,7 @@ def test_app_disconnect_behavior(test_app: str, test_cancellable_app: str):
555
555
  with pytest.raises(HTTPStatusError) as e:
556
556
  apps.run(
557
557
  test_cancellable_app,
558
- arguments={"lhs": 1, "rhs": 2, "wait_time": 20},
558
+ arguments={"lhs": 1, "rhs": 2, "wait_time": 6},
559
559
  path="/well-handled",
560
560
  )
561
561
  assert (
@@ -575,7 +575,7 @@ def test_app_disconnect_behavior(test_app: str, test_cancellable_app: str):
575
575
  with pytest.raises(HTTPStatusError) as e:
576
576
  apps.run(
577
577
  test_cancellable_app,
578
- arguments={"lhs": 1, "rhs": 2, "wait_time": 20},
578
+ arguments={"lhs": 1, "rhs": 2, "wait_time": 6},
579
579
  )
580
580
  assert (
581
581
  e.value.response.status_code == 504
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes