fal 1.5.13__tar.gz → 1.5.15__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 (168) hide show
  1. {fal-1.5.13/fal.egg-info → fal-1.5.15}/PKG-INFO +1 -1
  2. {fal-1.5.13 → fal-1.5.15/fal.egg-info}/PKG-INFO +1 -1
  3. {fal-1.5.13 → fal-1.5.15}/fal.egg-info/SOURCES.txt +1 -0
  4. {fal-1.5.13 → fal-1.5.15}/src/fal/_fal_version.py +2 -2
  5. {fal-1.5.13 → fal-1.5.15}/src/fal/apps.py +129 -1
  6. fal-1.5.15/src/fal/cli/machine.py +43 -0
  7. {fal-1.5.13 → fal-1.5.15}/src/fal/cli/main.py +2 -2
  8. {fal-1.5.13 → fal-1.5.15}/src/fal/sdk.py +4 -0
  9. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/file/providers/fal.py +16 -1
  10. {fal-1.5.13 → fal-1.5.15}/tests/test_apps.py +71 -0
  11. {fal-1.5.13 → fal-1.5.15}/.gitignore +0 -0
  12. {fal-1.5.13 → fal-1.5.15}/Makefile +0 -0
  13. {fal-1.5.13 → fal-1.5.15}/README.md +0 -0
  14. {fal-1.5.13 → fal-1.5.15}/docs/conf.py +0 -0
  15. {fal-1.5.13 → fal-1.5.15}/docs/index.rst +0 -0
  16. {fal-1.5.13 → fal-1.5.15}/fal.egg-info/dependency_links.txt +0 -0
  17. {fal-1.5.13 → fal-1.5.15}/fal.egg-info/entry_points.txt +0 -0
  18. {fal-1.5.13 → fal-1.5.15}/fal.egg-info/requires.txt +0 -0
  19. {fal-1.5.13 → fal-1.5.15}/fal.egg-info/top_level.txt +0 -0
  20. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/README.md +0 -0
  21. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  22. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  23. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  24. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  25. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  26. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  27. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  28. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  29. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  30. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  31. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  32. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  33. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  34. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  35. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  36. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  37. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  38. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  39. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  40. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  41. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  42. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  43. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  44. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  45. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  46. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  47. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  48. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  49. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  50. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  51. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  52. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  53. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  54. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
  55. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  56. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  57. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  58. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  59. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  60. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  61. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  62. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  63. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  64. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  65. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  66. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  67. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  68. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  69. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  70. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  71. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  72. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  73. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  74. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  75. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  76. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  77. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  78. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  79. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  80. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  81. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  82. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  83. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  84. {fal-1.5.13 → fal-1.5.15}/openapi-fal-rest/pyproject.toml +0 -0
  85. {fal-1.5.13 → fal-1.5.15}/openapi_rest.config.yaml +0 -0
  86. {fal-1.5.13 → fal-1.5.15}/pyproject.toml +0 -0
  87. {fal-1.5.13 → fal-1.5.15}/setup.cfg +0 -0
  88. {fal-1.5.13 → fal-1.5.15}/src/fal/__init__.py +0 -0
  89. {fal-1.5.13 → fal-1.5.15}/src/fal/__main__.py +0 -0
  90. {fal-1.5.13 → fal-1.5.15}/src/fal/_serialization.py +0 -0
  91. {fal-1.5.13 → fal-1.5.15}/src/fal/_version.py +0 -0
  92. {fal-1.5.13 → fal-1.5.15}/src/fal/api.py +0 -0
  93. {fal-1.5.13 → fal-1.5.15}/src/fal/app.py +0 -0
  94. {fal-1.5.13 → fal-1.5.15}/src/fal/auth/__init__.py +0 -0
  95. {fal-1.5.13 → fal-1.5.15}/src/fal/auth/auth0.py +0 -0
  96. {fal-1.5.13 → fal-1.5.15}/src/fal/auth/local.py +0 -0
  97. {fal-1.5.13 → fal-1.5.15}/src/fal/cli/__init__.py +0 -0
  98. {fal-1.5.13 → fal-1.5.15}/src/fal/cli/_utils.py +0 -0
  99. {fal-1.5.13 → fal-1.5.15}/src/fal/cli/apps.py +0 -0
  100. {fal-1.5.13 → fal-1.5.15}/src/fal/cli/auth.py +0 -0
  101. {fal-1.5.13 → fal-1.5.15}/src/fal/cli/create.py +0 -0
  102. {fal-1.5.13 → fal-1.5.15}/src/fal/cli/debug.py +0 -0
  103. {fal-1.5.13 → fal-1.5.15}/src/fal/cli/deploy.py +0 -0
  104. {fal-1.5.13 → fal-1.5.15}/src/fal/cli/doctor.py +0 -0
  105. {fal-1.5.13 → fal-1.5.15}/src/fal/cli/keys.py +0 -0
  106. {fal-1.5.13 → fal-1.5.15}/src/fal/cli/parser.py +0 -0
  107. {fal-1.5.13 → fal-1.5.15}/src/fal/cli/run.py +0 -0
  108. {fal-1.5.13 → fal-1.5.15}/src/fal/cli/secrets.py +0 -0
  109. {fal-1.5.13 → fal-1.5.15}/src/fal/console/__init__.py +0 -0
  110. {fal-1.5.13 → fal-1.5.15}/src/fal/console/icons.py +0 -0
  111. {fal-1.5.13 → fal-1.5.15}/src/fal/console/ux.py +0 -0
  112. {fal-1.5.13 → fal-1.5.15}/src/fal/container.py +0 -0
  113. {fal-1.5.13 → fal-1.5.15}/src/fal/exceptions/__init__.py +0 -0
  114. {fal-1.5.13 → fal-1.5.15}/src/fal/exceptions/_base.py +0 -0
  115. {fal-1.5.13 → fal-1.5.15}/src/fal/exceptions/_cuda.py +0 -0
  116. {fal-1.5.13 → fal-1.5.15}/src/fal/exceptions/auth.py +0 -0
  117. {fal-1.5.13 → fal-1.5.15}/src/fal/files.py +0 -0
  118. {fal-1.5.13 → fal-1.5.15}/src/fal/flags.py +0 -0
  119. {fal-1.5.13 → fal-1.5.15}/src/fal/logging/__init__.py +0 -0
  120. {fal-1.5.13 → fal-1.5.15}/src/fal/logging/isolate.py +0 -0
  121. {fal-1.5.13 → fal-1.5.15}/src/fal/logging/style.py +0 -0
  122. {fal-1.5.13 → fal-1.5.15}/src/fal/logging/trace.py +0 -0
  123. {fal-1.5.13 → fal-1.5.15}/src/fal/logging/user.py +0 -0
  124. {fal-1.5.13 → fal-1.5.15}/src/fal/py.typed +0 -0
  125. {fal-1.5.13 → fal-1.5.15}/src/fal/rest_client.py +0 -0
  126. {fal-1.5.13 → fal-1.5.15}/src/fal/sync.py +0 -0
  127. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/__init__.py +0 -0
  128. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/exceptions.py +0 -0
  129. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/file/__init__.py +0 -0
  130. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/file/file.py +0 -0
  131. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/file/providers/gcp.py +0 -0
  132. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/file/providers/r2.py +0 -0
  133. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/file/providers/s3.py +0 -0
  134. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/file/types.py +0 -0
  135. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/image/__init__.py +0 -0
  136. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/image/image.py +0 -0
  137. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
  138. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
  139. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
  140. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
  141. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
  142. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/image/safety_checker.py +0 -0
  143. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/optimize.py +0 -0
  144. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/utils/__init__.py +0 -0
  145. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/utils/download_utils.py +0 -0
  146. {fal-1.5.13 → fal-1.5.15}/src/fal/toolkit/utils/retry.py +0 -0
  147. {fal-1.5.13 → fal-1.5.15}/src/fal/utils.py +0 -0
  148. {fal-1.5.13 → fal-1.5.15}/src/fal/workflows.py +0 -0
  149. {fal-1.5.13 → fal-1.5.15}/tests/__init__.py +0 -0
  150. {fal-1.5.13 → fal-1.5.15}/tests/assets/cat.png +0 -0
  151. {fal-1.5.13 → fal-1.5.15}/tests/cli/__init__.py +0 -0
  152. {fal-1.5.13 → fal-1.5.15}/tests/cli/test_apps.py +0 -0
  153. {fal-1.5.13 → fal-1.5.15}/tests/cli/test_auth.py +0 -0
  154. {fal-1.5.13 → fal-1.5.15}/tests/cli/test_deploy.py +0 -0
  155. {fal-1.5.13 → fal-1.5.15}/tests/cli/test_keys.py +0 -0
  156. {fal-1.5.13 → fal-1.5.15}/tests/cli/test_run.py +0 -0
  157. {fal-1.5.13 → fal-1.5.15}/tests/cli/test_secrets.py +0 -0
  158. {fal-1.5.13 → fal-1.5.15}/tests/conftest.py +0 -0
  159. {fal-1.5.13 → fal-1.5.15}/tests/integration_test.py +0 -0
  160. {fal-1.5.13 → fal-1.5.15}/tests/mainify_package/__init__.py +0 -0
  161. {fal-1.5.13 → fal-1.5.15}/tests/mainify_package/impl.py +0 -0
  162. {fal-1.5.13 → fal-1.5.15}/tests/mainify_package/utils.py +0 -0
  163. {fal-1.5.13 → fal-1.5.15}/tests/mainify_target.py +0 -0
  164. {fal-1.5.13 → fal-1.5.15}/tests/test_stability.py +0 -0
  165. {fal-1.5.13 → fal-1.5.15}/tests/toolkit/file_test.py +0 -0
  166. {fal-1.5.13 → fal-1.5.15}/tests/toolkit/image_test.py +0 -0
  167. {fal-1.5.13 → fal-1.5.15}/tests/toolkit/utils/retry.py +0 -0
  168. {fal-1.5.13 → fal-1.5.15}/tools/demo_script.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.5.13
3
+ Version: 1.5.15
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.1
2
2
  Name: fal
3
- Version: 1.5.13
3
+ Version: 1.5.15
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
@@ -105,6 +105,7 @@ src/fal/cli/debug.py
105
105
  src/fal/cli/deploy.py
106
106
  src/fal/cli/doctor.py
107
107
  src/fal/cli/keys.py
108
+ src/fal/cli/machine.py
108
109
  src/fal/cli/main.py
109
110
  src/fal/cli/parser.py
110
111
  src/fal/cli/run.py
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.5.13'
16
- __version_tuple__ = version_tuple = (1, 5, 13)
15
+ __version__ = version = '1.5.15'
16
+ __version_tuple__ = version_tuple = (1, 5, 15)
@@ -4,15 +4,19 @@ import json
4
4
  import time
5
5
  from contextlib import contextmanager
6
6
  from dataclasses import dataclass, field
7
- from typing import Any, Iterator
7
+ from typing import TYPE_CHECKING, Any, Iterator
8
8
 
9
9
  import httpx
10
10
 
11
11
  from fal import flags
12
12
  from fal.sdk import Credentials, get_default_credentials
13
13
 
14
+ if TYPE_CHECKING:
15
+ from websockets.sync.connection import Connection
16
+
14
17
  _QUEUE_URL_FORMAT = f"https://queue.{flags.FAL_RUN_HOST}/{{app_id}}"
15
18
  _REALTIME_URL_FORMAT = f"wss://{flags.FAL_RUN_HOST}/{{app_id}}"
19
+ _WS_URL_FORMAT = f"wss://ws.{flags.FAL_RUN_HOST}/{{app_id}}"
16
20
 
17
21
 
18
22
  def _backwards_compatible_app_id(app_id: str) -> str:
@@ -245,3 +249,127 @@ def _connect(app_id: str, *, path: str = "/realtime") -> Iterator[_RealtimeConne
245
249
  url, additional_headers=creds.to_headers(), open_timeout=90
246
250
  ) as ws:
247
251
  yield _RealtimeConnection(ws)
252
+
253
+
254
+ class _MetaMessageFound(Exception): ...
255
+
256
+
257
+ @dataclass
258
+ class _WSConnection:
259
+ """A WS connection to an HTTP Fal app."""
260
+
261
+ _ws: Connection
262
+ _buffer: str | bytes | None = None
263
+
264
+ def run(self, arguments: dict[str, Any]) -> dict[str, Any]:
265
+ """Run an inference task on the app and return the result."""
266
+ self.send(arguments)
267
+ return self.recv()
268
+
269
+ def send(self, arguments: dict[str, Any]) -> None:
270
+ import json
271
+
272
+ payload = json.dumps(arguments)
273
+ self._ws.send(payload)
274
+
275
+ def _peek(self) -> bytes | str:
276
+ if self._buffer is None:
277
+ self._buffer = self._ws.recv()
278
+
279
+ return self._buffer
280
+
281
+ def _consume(self) -> None:
282
+ if self._buffer is None:
283
+ raise ValueError("No data to consume")
284
+
285
+ self._buffer = None
286
+
287
+ @contextmanager
288
+ def _recv(self) -> Iterator[str | bytes]:
289
+ res = self._peek()
290
+
291
+ yield res
292
+
293
+ # Only consume if it went through the context manager without raising
294
+ self._consume()
295
+
296
+ def _is_meta(self, res: str | bytes) -> bool:
297
+ if not isinstance(res, str):
298
+ return False
299
+
300
+ try:
301
+ json_payload: Any = json.loads(res)
302
+ except json.JSONDecodeError:
303
+ return False
304
+
305
+ if not isinstance(json_payload, dict):
306
+ return False
307
+
308
+ return "type" in json_payload and "request_id" in json_payload
309
+
310
+ def _recv_meta(self, type: str) -> dict[str, Any]:
311
+ with self._recv() as res:
312
+ if not self._is_meta(res):
313
+ raise ValueError(f"Expected a {type} message")
314
+
315
+ json_payload: dict = json.loads(res)
316
+ if json_payload.get("type") != type:
317
+ raise ValueError(f"Expected a {type} message")
318
+
319
+ return json_payload
320
+
321
+ def _recv_response(self) -> Any:
322
+ import msgpack
323
+
324
+ body: bytes = b""
325
+ while True:
326
+ try:
327
+ with self._recv() as res:
328
+ if self._is_meta(res):
329
+ # Keep the meta message for later
330
+ raise _MetaMessageFound()
331
+
332
+ if isinstance(res, str):
333
+ return res
334
+ else:
335
+ body += res
336
+ except _MetaMessageFound:
337
+ break
338
+
339
+ if not body:
340
+ raise ValueError("Empty response body")
341
+
342
+ return msgpack.unpackb(body)
343
+
344
+ def recv(self) -> Any:
345
+ start = self._recv_meta("start")
346
+ request_id = start["request_id"]
347
+
348
+ response = self._recv_response()
349
+
350
+ end = self._recv_meta("end")
351
+ if end["request_id"] != request_id:
352
+ raise ValueError("Mismatched request_id in end message")
353
+
354
+ return response
355
+
356
+
357
+ @contextmanager
358
+ def ws(app_id: str, *, path: str = "") -> Iterator[_WSConnection]:
359
+ """Connect to a HTTP endpoint but with websocket protocol. This is an internal and
360
+ experimental API, use it at your own risk."""
361
+
362
+ from websockets.sync import client
363
+
364
+ app_id = _backwards_compatible_app_id(app_id)
365
+ url = _WS_URL_FORMAT.format(app_id=app_id)
366
+ if path:
367
+ _path = path[len("/") :] if path.startswith("/") else path
368
+ url += "/" + _path
369
+
370
+ creds = get_default_credentials()
371
+
372
+ with client.connect(
373
+ url, additional_headers=creds.to_headers(), open_timeout=90
374
+ ) as ws:
375
+ yield _WSConnection(ws)
@@ -0,0 +1,43 @@
1
+ from .parser import FalClientParser
2
+
3
+
4
+ def _kill(args):
5
+ from fal.sdk import FalServerlessClient
6
+
7
+ client = FalServerlessClient(args.host)
8
+ with client.connect() as connection:
9
+ connection.kill_runner(args.id)
10
+
11
+
12
+ def _add_kill_parser(subparsers, parents):
13
+ kill_help = "Kill a machine."
14
+ parser = subparsers.add_parser(
15
+ "kill",
16
+ description=kill_help,
17
+ help=kill_help,
18
+ parents=parents,
19
+ )
20
+ parser.add_argument(
21
+ "id",
22
+ help="Runner ID.",
23
+ )
24
+ parser.set_defaults(func=_kill)
25
+
26
+
27
+ def add_parser(main_subparsers, parents):
28
+ machine_help = "Manage fal machines."
29
+ parser = main_subparsers.add_parser(
30
+ "machine",
31
+ description=machine_help,
32
+ help=machine_help,
33
+ parents=parents,
34
+ )
35
+
36
+ subparsers = parser.add_subparsers(
37
+ title="Commands",
38
+ metavar="command",
39
+ required=True,
40
+ parser_class=FalClientParser,
41
+ )
42
+
43
+ _add_kill_parser(subparsers, parents)
@@ -6,7 +6,7 @@ from fal import __version__
6
6
  from fal.console import console
7
7
  from fal.console.icons import CROSS_ICON
8
8
 
9
- from . import apps, auth, create, deploy, doctor, keys, run, secrets
9
+ from . import apps, auth, create, deploy, doctor, keys, machine, run, secrets
10
10
  from .debug import debugtools, get_debug_parser
11
11
  from .parser import FalParser, FalParserExit
12
12
 
@@ -31,7 +31,7 @@ def _get_main_parser() -> argparse.ArgumentParser:
31
31
  required=True,
32
32
  )
33
33
 
34
- for cmd in [auth, apps, deploy, run, keys, secrets, doctor, create]:
34
+ for cmd in [auth, apps, deploy, run, keys, secrets, doctor, create, machine]:
35
35
  cmd.add_parser(subparsers, parents)
36
36
 
37
37
  return parser
@@ -686,3 +686,7 @@ class FalServerlessConnection:
686
686
  )
687
687
  for secret in response.secrets
688
688
  ]
689
+
690
+ def kill_runner(self, runner_id: str) -> None:
691
+ request = isolate_proto.KillRunnerRequest(runner_id=runner_id)
692
+ self.stub.KillRunner(request)
@@ -180,6 +180,14 @@ class FalFileRepository(FalFileRepositoryBase):
180
180
  return self._save(file, "gcs")
181
181
 
182
182
 
183
+ @dataclass
184
+ class FalFileRepositoryV3(FalFileRepositoryBase):
185
+ def save(
186
+ self, file: FileData, object_lifecycle_preference: dict[str, str] | None = None
187
+ ) -> str:
188
+ return self._save(file, "fal-cdn-v3")
189
+
190
+
183
191
  class MultipartUpload:
184
192
  MULTIPART_THRESHOLD = 100 * 1024 * 1024
185
193
  MULTIPART_CHUNK_SIZE = 100 * 1024 * 1024
@@ -548,8 +556,15 @@ class FalCDNFileRepository(FileRepository):
548
556
  }
549
557
 
550
558
 
559
+ # This is only available for internal users to have long-lived access tokens
551
560
  @dataclass
552
- class FalFileRepositoryV3(FileRepository):
561
+ class InternalFalFileRepositoryV3(FileRepository):
562
+ """
563
+ InternalFalFileRepositoryV3 is a file repository that uses the FAL CDN V3.
564
+ But generates and uses long-lived access tokens.
565
+ That way it can avoid the need to refresh the token for every upload.
566
+ """
567
+
553
568
  @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
554
569
  def save(
555
570
  self, file: FileData, object_lifecycle_preference: dict[str, str] | None
@@ -154,6 +154,23 @@ class StatefulAdditionApp(fal.App, keep_alive=300, max_concurrency=1):
154
154
  return Output(result=self.counter)
155
155
 
156
156
 
157
+ class SleepInput(BaseModel):
158
+ wait_time: int
159
+
160
+
161
+ class SleepOutput(BaseModel):
162
+ pass
163
+
164
+
165
+ class SleepApp(fal.App, keep_alive=300, max_concurrency=1):
166
+ machine_type = "XS"
167
+
168
+ @fal.endpoint("/")
169
+ async def sleep(self, input: SleepInput) -> SleepOutput:
170
+ await asyncio.sleep(input.wait_time)
171
+ return SleepOutput()
172
+
173
+
157
174
  class ExceptionApp(fal.App, keep_alive=300, max_concurrency=1):
158
175
  machine_type = "XS"
159
176
 
@@ -378,6 +395,21 @@ def test_app_client(test_app: str, test_nomad_app: str):
378
395
  assert response["result"] == 5
379
396
 
380
397
 
398
+ def test_ws_client(test_app: str):
399
+ with apps.ws(test_app) as connection:
400
+ for i in range(3):
401
+ response = json.loads(connection.run({"lhs": 1, "rhs": i}))
402
+ assert response["result"] == 1 + i
403
+
404
+ for i in range(3):
405
+ connection.send({"lhs": 2, "rhs": i})
406
+
407
+ for i in range(3):
408
+ # they should be in order
409
+ response = json.loads(connection.recv())
410
+ assert response["result"] == 2 + i
411
+
412
+
381
413
  def test_app_client_old_format(test_app: str):
382
414
  assert test_app.count("/") == 1, "Test app should be in new format"
383
415
  old_format = test_app.replace("/", "-")
@@ -772,3 +804,42 @@ def test_app_exceptions(test_exception_app: AppClient):
772
804
 
773
805
  assert cuda_exc.value.status_code == _CUDA_OOM_STATUS_CODE
774
806
  assert _CUDA_OOM_MESSAGE in cuda_exc.value.message
807
+
808
+
809
+ def test_kill_runner():
810
+ import uuid
811
+
812
+ app_alias = str(uuid.uuid4()) + "-sleep-alias"
813
+ app = fal.wrap_app(SleepApp)
814
+ app_revision = app.host.register(
815
+ func=app.func,
816
+ options=app.options,
817
+ application_name=app_alias,
818
+ application_auth_mode="private",
819
+ )
820
+
821
+ host: api.FalServerlessHost = app.host # type: ignore
822
+
823
+ user = _get_user()
824
+
825
+ handle = apps.submit(f"{user.user_id}/{app_revision}", arguments={"wait_time": 10})
826
+
827
+ while True:
828
+ status = handle.status()
829
+ if isinstance(status, apps.InProgress):
830
+ break
831
+ elif isinstance(status, apps.Queued):
832
+ time.sleep(1)
833
+ else:
834
+ raise Exception(f"Failed to start the app: {status}")
835
+
836
+ with host._connection as client:
837
+ try:
838
+ client.kill_runner("1234567890")
839
+ except Exception as e:
840
+ assert "not found" in str(e).lower()
841
+
842
+ runners = client.list_alias_runners(app_alias)
843
+ assert len(runners) == 1
844
+
845
+ client.kill_runner(runners[0].runner_id)
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