fal 1.49.4__tar.gz → 1.50.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 (201) hide show
  1. {fal-1.49.4/fal.egg-info → fal-1.50.1}/PKG-INFO +2 -2
  2. {fal-1.49.4 → fal-1.50.1/fal.egg-info}/PKG-INFO +2 -2
  3. {fal-1.49.4 → fal-1.50.1}/fal.egg-info/requires.txt +1 -1
  4. {fal-1.49.4 → fal-1.50.1}/pyproject.toml +1 -1
  5. {fal-1.49.4 → fal-1.50.1}/src/fal/_fal_version.py +2 -2
  6. {fal-1.49.4 → fal-1.50.1}/src/fal/api/client.py +6 -0
  7. {fal-1.49.4 → fal-1.50.1}/src/fal/api/runners.py +10 -0
  8. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/runners.py +140 -5
  9. {fal-1.49.4 → fal-1.50.1}/src/fal/sdk.py +4 -0
  10. {fal-1.49.4 → fal-1.50.1}/tests/test_apps.py +106 -3
  11. {fal-1.49.4 → fal-1.50.1}/.gitignore +0 -0
  12. {fal-1.49.4 → fal-1.50.1}/Makefile +0 -0
  13. {fal-1.49.4 → fal-1.50.1}/README.md +0 -0
  14. {fal-1.49.4 → fal-1.50.1}/docs/conf.py +0 -0
  15. {fal-1.49.4 → fal-1.50.1}/docs/index.rst +0 -0
  16. {fal-1.49.4 → fal-1.50.1}/fal.egg-info/SOURCES.txt +0 -0
  17. {fal-1.49.4 → fal-1.50.1}/fal.egg-info/dependency_links.txt +0 -0
  18. {fal-1.49.4 → fal-1.50.1}/fal.egg-info/entry_points.txt +0 -0
  19. {fal-1.49.4 → fal-1.50.1}/fal.egg-info/top_level.txt +0 -0
  20. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/README.md +0 -0
  21. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  22. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  23. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  24. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  25. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  26. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  27. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  28. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  29. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  30. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  31. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  32. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  33. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  34. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  35. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  36. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  37. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  38. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  39. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  40. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  41. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  42. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  43. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  44. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  45. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  46. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  47. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  48. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  49. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  50. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  51. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  52. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  53. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  54. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
  55. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  56. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  57. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  58. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  59. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  60. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  61. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  62. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  63. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  64. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  65. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  66. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  67. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  68. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  69. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  70. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  71. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  72. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  73. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  74. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  75. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  76. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  77. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  78. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  79. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  80. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  81. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  82. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  83. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  84. {fal-1.49.4 → fal-1.50.1}/openapi-fal-rest/pyproject.toml +0 -0
  85. {fal-1.49.4 → fal-1.50.1}/openapi_rest.config.yaml +0 -0
  86. {fal-1.49.4 → fal-1.50.1}/setup.cfg +0 -0
  87. {fal-1.49.4 → fal-1.50.1}/src/fal/__init__.py +0 -0
  88. {fal-1.49.4 → fal-1.50.1}/src/fal/__main__.py +0 -0
  89. {fal-1.49.4 → fal-1.50.1}/src/fal/_serialization.py +0 -0
  90. {fal-1.49.4 → fal-1.50.1}/src/fal/_version.py +0 -0
  91. {fal-1.49.4 → fal-1.50.1}/src/fal/api/__init__.py +0 -0
  92. {fal-1.49.4 → fal-1.50.1}/src/fal/api/api.py +0 -0
  93. {fal-1.49.4 → fal-1.50.1}/src/fal/api/apps.py +0 -0
  94. {fal-1.49.4 → fal-1.50.1}/src/fal/api/deploy.py +0 -0
  95. {fal-1.49.4 → fal-1.50.1}/src/fal/app.py +0 -0
  96. {fal-1.49.4 → fal-1.50.1}/src/fal/apps.py +0 -0
  97. {fal-1.49.4 → fal-1.50.1}/src/fal/auth/__init__.py +0 -0
  98. {fal-1.49.4 → fal-1.50.1}/src/fal/auth/auth0.py +0 -0
  99. {fal-1.49.4 → fal-1.50.1}/src/fal/auth/local.py +0 -0
  100. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/__init__.py +0 -0
  101. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/_utils.py +0 -0
  102. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/api.py +0 -0
  103. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/apps.py +0 -0
  104. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/auth.py +0 -0
  105. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/cli_nested_json.py +0 -0
  106. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/create.py +0 -0
  107. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/debug.py +0 -0
  108. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/deploy.py +0 -0
  109. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/doctor.py +0 -0
  110. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/files.py +0 -0
  111. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/keys.py +0 -0
  112. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/main.py +0 -0
  113. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/parser.py +0 -0
  114. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/profile.py +0 -0
  115. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/queue.py +0 -0
  116. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/run.py +0 -0
  117. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/secrets.py +0 -0
  118. {fal-1.49.4 → fal-1.50.1}/src/fal/cli/teams.py +0 -0
  119. {fal-1.49.4 → fal-1.50.1}/src/fal/config.py +0 -0
  120. {fal-1.49.4 → fal-1.50.1}/src/fal/console/__init__.py +0 -0
  121. {fal-1.49.4 → fal-1.50.1}/src/fal/console/icons.py +0 -0
  122. {fal-1.49.4 → fal-1.50.1}/src/fal/console/ux.py +0 -0
  123. {fal-1.49.4 → fal-1.50.1}/src/fal/container.py +0 -0
  124. {fal-1.49.4 → fal-1.50.1}/src/fal/distributed/__init__.py +0 -0
  125. {fal-1.49.4 → fal-1.50.1}/src/fal/distributed/utils.py +0 -0
  126. {fal-1.49.4 → fal-1.50.1}/src/fal/distributed/worker.py +0 -0
  127. {fal-1.49.4 → fal-1.50.1}/src/fal/exceptions/__init__.py +0 -0
  128. {fal-1.49.4 → fal-1.50.1}/src/fal/exceptions/_base.py +0 -0
  129. {fal-1.49.4 → fal-1.50.1}/src/fal/exceptions/_cuda.py +0 -0
  130. {fal-1.49.4 → fal-1.50.1}/src/fal/exceptions/auth.py +0 -0
  131. {fal-1.49.4 → fal-1.50.1}/src/fal/file_sync.py +0 -0
  132. {fal-1.49.4 → fal-1.50.1}/src/fal/files.py +0 -0
  133. {fal-1.49.4 → fal-1.50.1}/src/fal/flags.py +0 -0
  134. {fal-1.49.4 → fal-1.50.1}/src/fal/logging/__init__.py +0 -0
  135. {fal-1.49.4 → fal-1.50.1}/src/fal/logging/isolate.py +0 -0
  136. {fal-1.49.4 → fal-1.50.1}/src/fal/logging/style.py +0 -0
  137. {fal-1.49.4 → fal-1.50.1}/src/fal/logging/trace.py +0 -0
  138. {fal-1.49.4 → fal-1.50.1}/src/fal/project.py +0 -0
  139. {fal-1.49.4 → fal-1.50.1}/src/fal/py.typed +0 -0
  140. {fal-1.49.4 → fal-1.50.1}/src/fal/rest_client.py +0 -0
  141. {fal-1.49.4 → fal-1.50.1}/src/fal/sync.py +0 -0
  142. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/__init__.py +0 -0
  143. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/audio/__init__.py +0 -0
  144. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/audio/audio.py +0 -0
  145. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/exceptions.py +0 -0
  146. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/file/__init__.py +0 -0
  147. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/file/file.py +0 -0
  148. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/file/providers/fal.py +0 -0
  149. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/file/providers/gcp.py +0 -0
  150. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/file/providers/r2.py +0 -0
  151. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/file/providers/s3.py +0 -0
  152. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/file/types.py +0 -0
  153. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/image/__init__.py +0 -0
  154. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/image/image.py +0 -0
  155. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
  156. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
  157. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
  158. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
  159. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
  160. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/image/safety_checker.py +0 -0
  161. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/kv.py +0 -0
  162. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/optimize.py +0 -0
  163. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/types.py +0 -0
  164. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/utils/__init__.py +0 -0
  165. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/utils/download_utils.py +0 -0
  166. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/utils/endpoint.py +0 -0
  167. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/utils/retry.py +0 -0
  168. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/utils/setup_utils.py +0 -0
  169. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/video/__init__.py +0 -0
  170. {fal-1.49.4 → fal-1.50.1}/src/fal/toolkit/video/video.py +0 -0
  171. {fal-1.49.4 → fal-1.50.1}/src/fal/utils.py +0 -0
  172. {fal-1.49.4 → fal-1.50.1}/src/fal/workflows.py +0 -0
  173. {fal-1.49.4 → fal-1.50.1}/tests/__init__.py +0 -0
  174. {fal-1.49.4 → fal-1.50.1}/tests/assets/cat.png +0 -0
  175. {fal-1.49.4 → fal-1.50.1}/tests/cli/__init__.py +0 -0
  176. {fal-1.49.4 → fal-1.50.1}/tests/cli/test_apps.py +0 -0
  177. {fal-1.49.4 → fal-1.50.1}/tests/cli/test_auth.py +0 -0
  178. {fal-1.49.4 → fal-1.50.1}/tests/cli/test_deploy.py +0 -0
  179. {fal-1.49.4 → fal-1.50.1}/tests/cli/test_keys.py +0 -0
  180. {fal-1.49.4 → fal-1.50.1}/tests/cli/test_run.py +0 -0
  181. {fal-1.49.4 → fal-1.50.1}/tests/cli/test_secrets.py +0 -0
  182. {fal-1.49.4 → fal-1.50.1}/tests/conftest.py +0 -0
  183. {fal-1.49.4 → fal-1.50.1}/tests/integration_test.py +0 -0
  184. {fal-1.49.4 → fal-1.50.1}/tests/mainify_package/__init__.py +0 -0
  185. {fal-1.49.4 → fal-1.50.1}/tests/mainify_package/impl.py +0 -0
  186. {fal-1.49.4 → fal-1.50.1}/tests/mainify_package/utils.py +0 -0
  187. {fal-1.49.4 → fal-1.50.1}/tests/mainify_target.py +0 -0
  188. {fal-1.49.4 → fal-1.50.1}/tests/test_file_sync.py +0 -0
  189. {fal-1.49.4 → fal-1.50.1}/tests/test_files.py +0 -0
  190. {fal-1.49.4 → fal-1.50.1}/tests/test_kv.py +0 -0
  191. {fal-1.49.4 → fal-1.50.1}/tests/test_stability.py +0 -0
  192. {fal-1.49.4 → fal-1.50.1}/tests/toolkit/file/providers/test_fal_retry.py +0 -0
  193. {fal-1.49.4 → fal-1.50.1}/tests/toolkit/file_test.py +0 -0
  194. {fal-1.49.4 → fal-1.50.1}/tests/toolkit/image_test.py +0 -0
  195. {fal-1.49.4 → fal-1.50.1}/tests/toolkit/test_types.py +0 -0
  196. {fal-1.49.4 → fal-1.50.1}/tests/toolkit/utils/retry.py +0 -0
  197. {fal-1.49.4 → fal-1.50.1}/tests/unit/distributed/test_integration.py +0 -0
  198. {fal-1.49.4 → fal-1.50.1}/tests/unit/distributed/test_utils.py +0 -0
  199. {fal-1.49.4 → fal-1.50.1}/tests/unit/distributed/test_worker.py +0 -0
  200. {fal-1.49.4 → fal-1.50.1}/tests/unit/test_app.py +0 -0
  201. {fal-1.49.4 → fal-1.50.1}/tools/demo_script.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fal
3
- Version: 1.49.4
3
+ Version: 1.50.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
7
7
  Description-Content-Type: text/markdown
8
8
  Requires-Dist: isolate[build]<0.21.0,>=0.18.0
9
- Requires-Dist: isolate-proto<0.20.0,>=0.19.0
9
+ Requires-Dist: isolate-proto<0.23.0,>=0.22.0
10
10
  Requires-Dist: grpcio<2,>=1.64.0
11
11
  Requires-Dist: dill==0.3.7
12
12
  Requires-Dist: cloudpickle==3.0.0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fal
3
- Version: 1.49.4
3
+ Version: 1.50.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
7
7
  Description-Content-Type: text/markdown
8
8
  Requires-Dist: isolate[build]<0.21.0,>=0.18.0
9
- Requires-Dist: isolate-proto<0.20.0,>=0.19.0
9
+ Requires-Dist: isolate-proto<0.23.0,>=0.22.0
10
10
  Requires-Dist: grpcio<2,>=1.64.0
11
11
  Requires-Dist: dill==0.3.7
12
12
  Requires-Dist: cloudpickle==3.0.0
@@ -1,5 +1,5 @@
1
1
  isolate[build]<0.21.0,>=0.18.0
2
- isolate-proto<0.20.0,>=0.19.0
2
+ isolate-proto<0.23.0,>=0.22.0
3
3
  grpcio<2,>=1.64.0
4
4
  dill==0.3.7
5
5
  cloudpickle==3.0.0
@@ -23,7 +23,7 @@ readme = "README.md"
23
23
  requires-python = ">=3.8"
24
24
  dependencies = [
25
25
  "isolate[build]>=0.18.0,<0.21.0",
26
- "isolate-proto>=0.19.0,<0.20.0",
26
+ "isolate-proto>=0.22.0,<0.23.0",
27
27
  "grpcio>=1.64.0,<2",
28
28
  "dill==0.3.7",
29
29
  "cloudpickle==3.0.0",
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.49.4'
32
- __version_tuple__ = version_tuple = (1, 49, 4)
31
+ __version__ = version = '1.50.1'
32
+ __version_tuple__ = version_tuple = (1, 50, 1)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -66,6 +66,12 @@ class _RunnersNamespace:
66
66
  def list(self, *, since=None) -> List[RunnerInfo]:
67
67
  return runners_api.list_runners(self.client, since=since)
68
68
 
69
+ def stop(self, runner_id: str) -> None:
70
+ return runners_api.stop_runner(self.client, runner_id)
71
+
72
+ def kill(self, runner_id: str) -> None:
73
+ return runners_api.kill_runner(self.client, runner_id)
74
+
69
75
 
70
76
  @dataclass
71
77
  class SyncServerlessClient:
@@ -14,3 +14,13 @@ def list_runners(
14
14
  ) -> List[RunnerInfo]:
15
15
  with FalServerlessClient(client._grpc_host, client._credentials).connect() as conn:
16
16
  return conn.list_runners(start_time=since)
17
+
18
+
19
+ def stop_runner(client: SyncServerlessClient, runner_id: str) -> None:
20
+ with FalServerlessClient(client._grpc_host, client._credentials).connect() as conn:
21
+ conn.stop_runner(runner_id)
22
+
23
+
24
+ def kill_runner(client: SyncServerlessClient, runner_id: str) -> None:
25
+ with FalServerlessClient(client._grpc_host, client._credentials).connect() as conn:
26
+ conn.kill_runner(runner_id)
@@ -1,12 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import argparse
4
+ import fcntl
3
5
  import json
6
+ import os
7
+ import signal
8
+ import struct
9
+ import sys
10
+ import termios
11
+ import tty
4
12
  from collections import deque
5
13
  from dataclasses import dataclass
6
14
  from datetime import datetime, timedelta, timezone
7
15
  from http import HTTPStatus
16
+ from queue import Empty, Queue
17
+ from threading import Thread
8
18
  from typing import Iterator, List
9
19
 
20
+ import grpc
10
21
  import httpx
11
22
  from httpx_sse import connect_sse
12
23
  from rich.console import Console
@@ -14,7 +25,7 @@ from structlog.typing import EventDict
14
25
 
15
26
  from fal.api.client import SyncServerlessClient
16
27
  from fal.rest_client import REST_CLIENT
17
- from fal.sdk import FalServerlessClient, RunnerInfo, RunnerState
28
+ from fal.sdk import RunnerInfo, RunnerState
18
29
 
19
30
  from .parser import FalClientParser, SinceAction, get_output_parser
20
31
 
@@ -95,12 +106,108 @@ def runners_requests_table(runners: list[RunnerInfo]):
95
106
  return table
96
107
 
97
108
 
109
+ def _get_tty_size():
110
+ """Get current terminal dimensions."""
111
+ try:
112
+ h, w = struct.unpack("HH", fcntl.ioctl(0, termios.TIOCGWINSZ, b"\0" * 4))[:2]
113
+ return h, w
114
+ except (OSError, ValueError):
115
+ return 24, 80 # Fallback to standard size
116
+
117
+
118
+ def _shell(args):
119
+ """Execute interactive shell in runner."""
120
+ import isolate_proto
121
+
122
+ client = SyncServerlessClient(host=args.host, team=args.team)
123
+ stub = client._create_host()._connection.stub
124
+ runner_id = args.id
125
+
126
+ # Setup terminal for raw mode
127
+ fd = sys.stdin.fileno()
128
+ old_settings = termios.tcgetattr(fd)
129
+ tty.setraw(fd)
130
+
131
+ # Message queue for stdin data and resize events
132
+ messages = Queue() # type: ignore
133
+ stop_flag = False
134
+
135
+ def handle_resize(*_):
136
+ messages.put(("resize", None))
137
+
138
+ signal.signal(signal.SIGWINCH, handle_resize)
139
+
140
+ def read_stdin():
141
+ """Read stdin in a background thread."""
142
+ nonlocal stop_flag
143
+ while not stop_flag:
144
+ try:
145
+ data = os.read(fd, 4096)
146
+ if not data:
147
+ break
148
+ messages.put(("data", data))
149
+ except OSError:
150
+ break
151
+
152
+ reader = Thread(target=read_stdin, daemon=True)
153
+ reader.start()
154
+
155
+ def stream_inputs():
156
+ """Generate input stream for gRPC."""
157
+ # Send initial message with runner_id and terminal size
158
+ msg = isolate_proto.ShellRunnerInput(runner_id=runner_id)
159
+ h, w = _get_tty_size()
160
+ msg.tty_size.height = h
161
+ msg.tty_size.width = w
162
+ yield msg
163
+
164
+ # Stream stdin data and resize events
165
+ while True:
166
+ try:
167
+ msg_type, data = messages.get(timeout=0.1)
168
+ except Empty:
169
+ continue
170
+
171
+ if msg_type == "data":
172
+ yield isolate_proto.ShellRunnerInput(data=data)
173
+ elif msg_type == "resize":
174
+ msg = isolate_proto.ShellRunnerInput()
175
+ h, w = _get_tty_size()
176
+ msg.tty_size.height = h
177
+ msg.tty_size.width = w
178
+ yield msg
179
+
180
+ exit_code = 1
181
+ try:
182
+ for output in stub.ShellRunner(stream_inputs()):
183
+ if output.HasField("exit_code"):
184
+ exit_code = output.exit_code
185
+ break
186
+ if output.data:
187
+ sys.stdout.buffer.write(output.data)
188
+ sys.stdout.buffer.flush()
189
+ if output.close:
190
+ break
191
+ exit_code = exit_code or 0
192
+ except grpc.RpcError as exc:
193
+ args.console.print(f"\n[red]Connection error:[/] {exc.details()}")
194
+ except Exception as exc:
195
+ args.console.print(f"\n[red]Error:[/] {exc}")
196
+ finally:
197
+ stop_flag = True
198
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
199
+
200
+ return exit_code
201
+
202
+
203
+ def _stop(args):
204
+ client = SyncServerlessClient(host=args.host, team=args.team)
205
+ client.runners.stop(args.id)
206
+
207
+
98
208
  def _kill(args):
99
209
  client = SyncServerlessClient(host=args.host, team=args.team)
100
- with FalServerlessClient(
101
- client._grpc_host, client._credentials
102
- ).connect() as connection:
103
- connection.kill_runner(args.id)
210
+ client.runners.kill(args.id)
104
211
 
105
212
 
106
213
  def _list_json(args, runners: list[RunnerInfo]):
@@ -161,6 +268,21 @@ def _list(args):
161
268
  raise AssertionError(f"Invalid output format: {args.output}")
162
269
 
163
270
 
271
+ def _add_stop_parser(subparsers, parents):
272
+ stop_help = "Stop a runner gracefully."
273
+ parser = subparsers.add_parser(
274
+ "stop",
275
+ description=stop_help,
276
+ help=stop_help,
277
+ parents=parents,
278
+ )
279
+ parser.add_argument(
280
+ "id",
281
+ help="Runner ID.",
282
+ )
283
+ parser.set_defaults(func=_stop)
284
+
285
+
164
286
  def _add_kill_parser(subparsers, parents):
165
287
  kill_help = "Kill a runner."
166
288
  parser = subparsers.add_parser(
@@ -546,6 +668,17 @@ def _add_logs_parser(subparsers, parents):
546
668
  parser.set_defaults(func=_logs)
547
669
 
548
670
 
671
+ def _add_shell_parser(subparsers, parents):
672
+ """Add hidden shell command parser."""
673
+ parser = subparsers.add_parser(
674
+ "shell",
675
+ help=argparse.SUPPRESS,
676
+ parents=parents,
677
+ )
678
+ parser.add_argument("id", help="Runner ID.")
679
+ parser.set_defaults(func=_shell)
680
+
681
+
549
682
  def add_parser(main_subparsers, parents):
550
683
  runners_help = "Manage fal runners."
551
684
  parser = main_subparsers.add_parser(
@@ -563,6 +696,8 @@ def add_parser(main_subparsers, parents):
563
696
  parser_class=FalClientParser,
564
697
  )
565
698
 
699
+ _add_stop_parser(subparsers, parents)
566
700
  _add_kill_parser(subparsers, parents)
567
701
  _add_list_parser(subparsers, parents)
568
702
  _add_logs_parser(subparsers, parents)
703
+ _add_shell_parser(subparsers, parents)
@@ -884,6 +884,10 @@ class FalServerlessConnection:
884
884
  for secret in response.secrets
885
885
  ]
886
886
 
887
+ def stop_runner(self, runner_id: str) -> None:
888
+ request = isolate_proto.StopRunnerRequest(runner_id=runner_id)
889
+ self.stub.StopRunner(request)
890
+
887
891
  def kill_runner(self, runner_id: str) -> None:
888
892
  request = isolate_proto.KillRunnerRequest(runner_id=runner_id)
889
893
  self.stub.KillRunner(request)
@@ -31,6 +31,7 @@ from fal.exceptions import (
31
31
  )
32
32
  from fal.exceptions._cuda import _CUDA_OOM_MESSAGE, _CUDA_OOM_STATUS_CODE
33
33
  from fal.rest_client import REST_CLIENT
34
+ from fal.sdk import RunnerState
34
35
  from fal.toolkit.utils.endpoint import cancel_on_disconnect
35
36
  from fal.workflows import Workflow
36
37
 
@@ -1004,7 +1005,70 @@ def test_field_exception_billing(test_exception_app: AppClient):
1004
1005
  assert not hasattr(response.headers, "x-fal-billable-units")
1005
1006
 
1006
1007
 
1008
+ def test_stop_runner(host: api.FalServerlessHost, test_sleep_app: str):
1009
+ def submit_and_wait_for_runner():
1010
+ handle = apps.submit(test_sleep_app, arguments={"wait_time": 1})
1011
+
1012
+ while True:
1013
+ status = handle.status()
1014
+ if isinstance(status, apps.InProgress):
1015
+ break
1016
+ elif isinstance(status, apps.Queued):
1017
+ time.sleep(1)
1018
+ else:
1019
+ raise Exception(f"Failed to start the app: {status}")
1020
+
1021
+ return handle
1022
+
1023
+ # Submit a runner and wait for it to be idle
1024
+ submit_and_wait_for_runner()
1025
+
1026
+ with host._connection as client:
1027
+ timeout = 10
1028
+ start_time = time.time()
1029
+ while True:
1030
+ _, _, app_alias = test_sleep_app.partition("/")
1031
+ runners = client.list_alias_runners(app_alias)
1032
+ assert len(runners) == 1
1033
+
1034
+ if runners[0].in_flight_requests == 0:
1035
+ break
1036
+ elif time.time() - start_time > timeout:
1037
+ raise Exception(f"Timeout waiting for runner to be idle: {runners[0]}")
1038
+ time.sleep(1)
1039
+
1040
+ # Because the runner is not requested to be stopped, it should be reused
1041
+ submit_and_wait_for_runner()
1042
+
1043
+ with host._connection as client:
1044
+ _, _, app_alias = test_sleep_app.partition("/")
1045
+ runners = client.list_alias_runners(app_alias)
1046
+ assert len(runners) == 1
1047
+
1048
+ # Request to stop the runner
1049
+ with host._connection as client:
1050
+ with pytest.raises(Exception) as e:
1051
+ client.stop_runner("1234567890")
1052
+
1053
+ assert "not found" in str(e).lower()
1054
+
1055
+ _, _, app_alias = test_sleep_app.partition("/")
1056
+ runners = client.list_alias_runners(app_alias)
1057
+ assert len(runners) == 1
1058
+
1059
+ client.stop_runner(runners[0].runner_id)
1060
+
1061
+ # Because the runner is requested to be stopped,
1062
+ # it should not be reused and a new runner should be created
1063
+ submit_and_wait_for_runner()
1064
+
1065
+ with host._connection as client:
1066
+ runners = client.list_alias_runners(app_alias)
1067
+ assert len(runners) == 2
1068
+
1069
+
1007
1070
  def test_kill_runner(host: api.FalServerlessHost, test_sleep_app: str):
1071
+ # Kill all the replicas of the app that is already running
1008
1072
  handle = apps.submit(test_sleep_app, arguments={"wait_time": 10})
1009
1073
 
1010
1074
  while True:
@@ -1024,13 +1088,52 @@ def test_kill_runner(host: api.FalServerlessHost, test_sleep_app: str):
1024
1088
 
1025
1089
  _, _, app_alias = test_sleep_app.partition("/")
1026
1090
  runners = client.list_alias_runners(app_alias)
1027
- assert len(runners) == 1
1091
+ existing_runners = len(
1092
+ [runner for runner in runners if runner.state == RunnerState.RUNNING]
1093
+ )
1028
1094
 
1029
1095
  client.kill_runner(runners[0].runner_id)
1030
1096
 
1031
1097
  runners = client.list_alias_runners(app_alias)
1032
- num_runners = len([runner for runner in runners if runner.state == "running"])
1033
- assert num_runners == 0
1098
+ num_runners = len(
1099
+ [runner for runner in runners if runner.state == RunnerState.RUNNING]
1100
+ )
1101
+ assert num_runners == existing_runners - 1
1102
+
1103
+
1104
+ def test_shell_runner(host: api.FalServerlessHost, test_sleep_app: str):
1105
+ handle = apps.submit(test_sleep_app, arguments={"wait_time": 30})
1106
+
1107
+ while True:
1108
+ status = handle.status()
1109
+ if isinstance(status, apps.InProgress):
1110
+ break
1111
+ elif isinstance(status, apps.Queued):
1112
+ time.sleep(1)
1113
+ else:
1114
+ raise Exception(f"Failed to start the app: {status}")
1115
+
1116
+ with host._connection as client:
1117
+ _, _, app_alias = test_sleep_app.partition("/")
1118
+ runners = client.list_alias_runners(app_alias)
1119
+ assert len(runners) == 1
1120
+ runner_id = runners[0].runner_id
1121
+
1122
+ proc = subprocess.Popen(
1123
+ ["python", "-m", "fal", "runners", "shell", runner_id],
1124
+ stdin=subprocess.PIPE,
1125
+ stdout=subprocess.PIPE,
1126
+ stderr=subprocess.PIPE,
1127
+ )
1128
+
1129
+ try:
1130
+ commands = b"echo 'a' > t.txt\ncat t.txt\nexit\n"
1131
+ stdout, stderr = proc.communicate(input=commands, timeout=10)
1132
+ assert b"a" in stdout, f"Expected 'a' in output, got: {stdout.decode()}"
1133
+ finally:
1134
+ if proc.poll() is None:
1135
+ proc.kill()
1136
+ proc.wait()
1034
1137
 
1035
1138
 
1036
1139
  def test_container_app_client(test_container_app: str):
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
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