fal 1.10.0__tar.gz → 1.11.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.
Files changed (174) hide show
  1. {fal-1.10.0/fal.egg-info → fal-1.11.1}/PKG-INFO +2 -2
  2. {fal-1.10.0 → fal-1.11.1/fal.egg-info}/PKG-INFO +2 -2
  3. {fal-1.10.0 → fal-1.11.1}/src/fal/_fal_version.py +2 -2
  4. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/_utils.py +7 -4
  5. {fal-1.10.0 → fal-1.11.1}/src/fal/container.py +6 -0
  6. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/utils/download_utils.py +38 -6
  7. {fal-1.10.0 → fal-1.11.1}/tests/cli/test_deploy.py +21 -0
  8. {fal-1.10.0 → fal-1.11.1}/tests/test_apps.py +43 -26
  9. {fal-1.10.0 → fal-1.11.1}/.gitignore +0 -0
  10. {fal-1.10.0 → fal-1.11.1}/Makefile +0 -0
  11. {fal-1.10.0 → fal-1.11.1}/README.md +0 -0
  12. {fal-1.10.0 → fal-1.11.1}/docs/conf.py +0 -0
  13. {fal-1.10.0 → fal-1.11.1}/docs/index.rst +0 -0
  14. {fal-1.10.0 → fal-1.11.1}/fal.egg-info/SOURCES.txt +0 -0
  15. {fal-1.10.0 → fal-1.11.1}/fal.egg-info/dependency_links.txt +0 -0
  16. {fal-1.10.0 → fal-1.11.1}/fal.egg-info/entry_points.txt +0 -0
  17. {fal-1.10.0 → fal-1.11.1}/fal.egg-info/requires.txt +0 -0
  18. {fal-1.10.0 → fal-1.11.1}/fal.egg-info/top_level.txt +0 -0
  19. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/README.md +0 -0
  20. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  21. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  22. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  23. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  24. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  25. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  26. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  27. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  28. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  29. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  30. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  31. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  32. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  33. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  34. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  35. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  36. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  37. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  38. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  39. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  40. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  41. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  42. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  43. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  44. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  45. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  46. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  47. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  48. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  49. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  50. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  51. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  52. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  53. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
  54. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  55. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  56. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  57. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  58. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  59. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  60. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  61. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  62. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  63. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  64. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  65. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  66. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  67. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  68. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  69. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  70. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  71. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  72. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  73. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  74. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  75. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  76. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  77. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  78. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  79. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  80. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  81. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  82. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  83. {fal-1.10.0 → fal-1.11.1}/openapi-fal-rest/pyproject.toml +0 -0
  84. {fal-1.10.0 → fal-1.11.1}/openapi_rest.config.yaml +0 -0
  85. {fal-1.10.0 → fal-1.11.1}/pyproject.toml +0 -0
  86. {fal-1.10.0 → fal-1.11.1}/setup.cfg +0 -0
  87. {fal-1.10.0 → fal-1.11.1}/src/fal/__init__.py +0 -0
  88. {fal-1.10.0 → fal-1.11.1}/src/fal/__main__.py +0 -0
  89. {fal-1.10.0 → fal-1.11.1}/src/fal/_serialization.py +0 -0
  90. {fal-1.10.0 → fal-1.11.1}/src/fal/_version.py +0 -0
  91. {fal-1.10.0 → fal-1.11.1}/src/fal/api.py +0 -0
  92. {fal-1.10.0 → fal-1.11.1}/src/fal/app.py +0 -0
  93. {fal-1.10.0 → fal-1.11.1}/src/fal/apps.py +0 -0
  94. {fal-1.10.0 → fal-1.11.1}/src/fal/auth/__init__.py +0 -0
  95. {fal-1.10.0 → fal-1.11.1}/src/fal/auth/auth0.py +0 -0
  96. {fal-1.10.0 → fal-1.11.1}/src/fal/auth/local.py +0 -0
  97. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/__init__.py +0 -0
  98. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/api.py +0 -0
  99. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/apps.py +0 -0
  100. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/auth.py +0 -0
  101. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/cli_nested_json.py +0 -0
  102. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/create.py +0 -0
  103. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/debug.py +0 -0
  104. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/deploy.py +0 -0
  105. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/doctor.py +0 -0
  106. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/keys.py +0 -0
  107. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/main.py +0 -0
  108. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/parser.py +0 -0
  109. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/profile.py +0 -0
  110. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/run.py +0 -0
  111. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/runners.py +0 -0
  112. {fal-1.10.0 → fal-1.11.1}/src/fal/cli/secrets.py +0 -0
  113. {fal-1.10.0 → fal-1.11.1}/src/fal/config.py +0 -0
  114. {fal-1.10.0 → fal-1.11.1}/src/fal/console/__init__.py +0 -0
  115. {fal-1.10.0 → fal-1.11.1}/src/fal/console/icons.py +0 -0
  116. {fal-1.10.0 → fal-1.11.1}/src/fal/console/ux.py +0 -0
  117. {fal-1.10.0 → fal-1.11.1}/src/fal/exceptions/__init__.py +0 -0
  118. {fal-1.10.0 → fal-1.11.1}/src/fal/exceptions/_base.py +0 -0
  119. {fal-1.10.0 → fal-1.11.1}/src/fal/exceptions/_cuda.py +0 -0
  120. {fal-1.10.0 → fal-1.11.1}/src/fal/exceptions/auth.py +0 -0
  121. {fal-1.10.0 → fal-1.11.1}/src/fal/files.py +0 -0
  122. {fal-1.10.0 → fal-1.11.1}/src/fal/flags.py +0 -0
  123. {fal-1.10.0 → fal-1.11.1}/src/fal/logging/__init__.py +0 -0
  124. {fal-1.10.0 → fal-1.11.1}/src/fal/logging/isolate.py +0 -0
  125. {fal-1.10.0 → fal-1.11.1}/src/fal/logging/style.py +0 -0
  126. {fal-1.10.0 → fal-1.11.1}/src/fal/logging/trace.py +0 -0
  127. {fal-1.10.0 → fal-1.11.1}/src/fal/logging/user.py +0 -0
  128. {fal-1.10.0 → fal-1.11.1}/src/fal/py.typed +0 -0
  129. {fal-1.10.0 → fal-1.11.1}/src/fal/rest_client.py +0 -0
  130. {fal-1.10.0 → fal-1.11.1}/src/fal/sdk.py +0 -0
  131. {fal-1.10.0 → fal-1.11.1}/src/fal/sync.py +0 -0
  132. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/__init__.py +0 -0
  133. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/exceptions.py +0 -0
  134. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/file/__init__.py +0 -0
  135. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/file/file.py +0 -0
  136. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/file/providers/fal.py +0 -0
  137. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/file/providers/gcp.py +0 -0
  138. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/file/providers/r2.py +0 -0
  139. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/file/providers/s3.py +0 -0
  140. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/file/types.py +0 -0
  141. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/image/__init__.py +0 -0
  142. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/image/image.py +0 -0
  143. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
  144. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
  145. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
  146. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
  147. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
  148. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/image/safety_checker.py +0 -0
  149. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/optimize.py +0 -0
  150. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/types.py +0 -0
  151. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/utils/__init__.py +0 -0
  152. {fal-1.10.0 → fal-1.11.1}/src/fal/toolkit/utils/retry.py +0 -0
  153. {fal-1.10.0 → fal-1.11.1}/src/fal/utils.py +0 -0
  154. {fal-1.10.0 → fal-1.11.1}/src/fal/workflows.py +0 -0
  155. {fal-1.10.0 → fal-1.11.1}/tests/__init__.py +0 -0
  156. {fal-1.10.0 → fal-1.11.1}/tests/assets/cat.png +0 -0
  157. {fal-1.10.0 → fal-1.11.1}/tests/cli/__init__.py +0 -0
  158. {fal-1.10.0 → fal-1.11.1}/tests/cli/test_apps.py +0 -0
  159. {fal-1.10.0 → fal-1.11.1}/tests/cli/test_auth.py +0 -0
  160. {fal-1.10.0 → fal-1.11.1}/tests/cli/test_keys.py +0 -0
  161. {fal-1.10.0 → fal-1.11.1}/tests/cli/test_run.py +0 -0
  162. {fal-1.10.0 → fal-1.11.1}/tests/cli/test_secrets.py +0 -0
  163. {fal-1.10.0 → fal-1.11.1}/tests/conftest.py +0 -0
  164. {fal-1.10.0 → fal-1.11.1}/tests/integration_test.py +0 -0
  165. {fal-1.10.0 → fal-1.11.1}/tests/mainify_package/__init__.py +0 -0
  166. {fal-1.10.0 → fal-1.11.1}/tests/mainify_package/impl.py +0 -0
  167. {fal-1.10.0 → fal-1.11.1}/tests/mainify_package/utils.py +0 -0
  168. {fal-1.10.0 → fal-1.11.1}/tests/mainify_target.py +0 -0
  169. {fal-1.10.0 → fal-1.11.1}/tests/test_stability.py +0 -0
  170. {fal-1.10.0 → fal-1.11.1}/tests/toolkit/file_test.py +0 -0
  171. {fal-1.10.0 → fal-1.11.1}/tests/toolkit/image_test.py +0 -0
  172. {fal-1.10.0 → fal-1.11.1}/tests/toolkit/test_types.py +0 -0
  173. {fal-1.10.0 → fal-1.11.1}/tests/toolkit/utils/retry.py +0 -0
  174. {fal-1.10.0 → fal-1.11.1}/tools/demo_script.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: fal
3
- Version: 1.10.0
3
+ Version: 1.11.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
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: fal
3
- Version: 1.10.0
3
+ Version: 1.11.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.10.0'
21
- __version_tuple__ = version_tuple = (1, 10, 0)
20
+ __version__ = version = '1.11.1'
21
+ __version_tuple__ = version_tuple = (1, 11, 1)
@@ -25,7 +25,7 @@ def get_app_data_from_toml(app_name):
25
25
  raise ValueError(f"App {app_name} not found in pyproject.toml")
26
26
 
27
27
  try:
28
- app_ref = app_data["ref"]
28
+ app_ref = app_data.pop("ref")
29
29
  except KeyError:
30
30
  raise ValueError(f"App {app_name} does not have a ref key in pyproject.toml")
31
31
 
@@ -33,8 +33,11 @@ def get_app_data_from_toml(app_name):
33
33
  project_root, _ = find_project_root(None)
34
34
  app_ref = str(project_root / app_ref)
35
35
 
36
- app_auth = app_data.get("auth", "private")
37
- app_deployment_strategy = app_data.get("deployment_strategy", "recreate")
38
- app_no_scale = app_data.get("no_scale", False)
36
+ app_auth = app_data.pop("auth", "private")
37
+ app_deployment_strategy = app_data.pop("deployment_strategy", "recreate")
38
+ app_no_scale = app_data.pop("no_scale", False)
39
+
40
+ if len(app_data) > 0:
41
+ raise ValueError(f"Found unexpected keys in pyproject.toml: {app_data}")
39
42
 
40
43
  return app_ref, app_auth, app_deployment_strategy, app_no_scale
@@ -4,6 +4,8 @@ from typing import Dict, Literal
4
4
  Builder = Literal["depot", "service", "worker"]
5
5
  BUILDERS = {"depot", "service", "worker"}
6
6
  DEFAULT_BUILDER: Builder = "depot"
7
+ DEFAULT_COMPRESSION: str = "gzip"
8
+ DEFAULT_FORCE_COMPRESSION: bool = False
7
9
 
8
10
 
9
11
  @dataclass
@@ -16,6 +18,8 @@ class ContainerImage:
16
18
  build_args: Dict[str, str] = field(default_factory=dict)
17
19
  registries: Dict[str, Dict[str, str]] = field(default_factory=dict)
18
20
  builder: Builder = field(default=DEFAULT_BUILDER)
21
+ compression: str = DEFAULT_COMPRESSION
22
+ force_compression: bool = DEFAULT_FORCE_COMPRESSION
19
23
 
20
24
  def __post_init__(self) -> None:
21
25
  if self.registries:
@@ -46,4 +50,6 @@ class ContainerImage:
46
50
  "build_args": self.build_args,
47
51
  "registries": self.registries,
48
52
  "builder": self.builder,
53
+ "compression": self.compression,
54
+ "force_compression": self.force_compression,
49
55
  }
@@ -126,11 +126,12 @@ def download_file(
126
126
  *,
127
127
  force: bool = False,
128
128
  request_headers: dict[str, str] | None = None,
129
+ filesize_limit: int | None = None,
129
130
  ) -> Path:
130
131
  """Downloads a file from the specified URL to the target directory.
131
132
 
132
133
  The function downloads the file from the given URL and saves it in the specified
133
- target directory.
134
+ target directory, provided it is below the given filesize limit.
134
135
 
135
136
  It also checks whether the local file already exists and whether its content length
136
137
  matches the expected content length from the remote file. If the local file already
@@ -151,6 +152,8 @@ def download_file(
151
152
  Defaults to `False`.
152
153
  request_headers: A dictionary containing additional headers to be included in
153
154
  the HTTP request. Defaults to `None`.
155
+ filesize_limit: An integer specifying the maximum downloadable size,
156
+ in megabytes. Defaults to `None`.
154
157
 
155
158
 
156
159
  Returns:
@@ -160,12 +163,22 @@ def download_file(
160
163
  ValueError: If the provided `file_name` contains a forward slash ('/').
161
164
  DownloadError: If an error occurs during the download process.
162
165
  """
166
+ ONE_MB = 1024**2
167
+
163
168
  try:
164
- file_name = _get_remote_file_properties(url, request_headers)[0]
169
+ file_name, expected_filesize = _get_remote_file_properties(url, request_headers)
165
170
  except Exception as e:
166
- print(f"GOt error: {e}")
171
+ print(f"Got error: {e}")
167
172
  raise DownloadError(f"Failed to get remote file properties for {url}") from e
168
173
 
174
+ expected_filesize_mb = expected_filesize / ONE_MB
175
+
176
+ if filesize_limit is not None and expected_filesize_mb > filesize_limit:
177
+ raise DownloadError(
178
+ f"""File to be downloaded is of size {expected_filesize_mb},
179
+ which is over the limit of {filesize_limit}"""
180
+ )
181
+
169
182
  if "/" in file_name:
170
183
  raise ValueError(f"File name '{file_name}' cannot contain a slash.")
171
184
 
@@ -194,7 +207,10 @@ def download_file(
194
207
 
195
208
  try:
196
209
  _download_file_python(
197
- url=url, target_path=target_path, request_headers=request_headers
210
+ url=url,
211
+ target_path=target_path,
212
+ request_headers=request_headers,
213
+ filesize_limit=filesize_limit,
198
214
  )
199
215
  except Exception as e:
200
216
  msg = f"Failed to download {url} to {target_path}"
@@ -207,7 +223,10 @@ def download_file(
207
223
 
208
224
 
209
225
  def _download_file_python(
210
- url: str, target_path: Path | str, request_headers: dict[str, str] | None = None
226
+ url: str,
227
+ target_path: Path | str,
228
+ request_headers: dict[str, str] | None = None,
229
+ filesize_limit: int | None = None,
211
230
  ) -> Path:
212
231
  """Download a file from a given URL and save it to a specified path using a
213
232
  Python interface.
@@ -217,6 +236,8 @@ def _download_file_python(
217
236
  target_path: The path where the downloaded file will be saved.
218
237
  request_headers: A dictionary containing additional headers to be included in
219
238
  the HTTP request. Defaults to `None`.
239
+ filesize_limit: A integer value specifying how many megabytes can be
240
+ downloaded at maximum. Defaults to `None`.
220
241
 
221
242
  Returns:
222
243
  The path where the downloaded file has been saved.
@@ -233,7 +254,10 @@ def _download_file_python(
233
254
  file_path = temp_file.name
234
255
 
235
256
  for progress, total_size in _stream_url_data_to_file(
236
- url, temp_file.name, request_headers=request_headers
257
+ url,
258
+ temp_file.name,
259
+ request_headers=request_headers,
260
+ filesize_limit=filesize_limit,
237
261
  ):
238
262
  if total_size:
239
263
  progress_msg = f"Downloading {url} ... {progress:.2%}"
@@ -261,6 +285,7 @@ def _stream_url_data_to_file(
261
285
  file_path: str,
262
286
  chunk_size_in_mb: int = 64,
263
287
  request_headers: dict[str, str] | None = None,
288
+ filesize_limit: int | None = None,
264
289
  ):
265
290
  """Download data from a URL and stream it to a file.
266
291
 
@@ -277,6 +302,8 @@ def _stream_url_data_to_file(
277
302
  Defaults to 64.
278
303
  request_headers: A dictionary containing additional headers to be included in
279
304
  the HTTP request. Defaults to `None`.
305
+ filesize_limit: An integer specifying how many megabytes can be
306
+ downloaded at maximum. Defaults to `None`.
280
307
 
281
308
  Yields:
282
309
  A tuple containing two elements:
@@ -300,6 +327,11 @@ def _stream_url_data_to_file(
300
327
  f_stream.write(data)
301
328
 
302
329
  received_size = f_stream.tell()
330
+ if filesize_limit is not None and received_size > filesize_limit:
331
+ raise DownloadError(
332
+ f"""Attempted to download more data {received_size}
333
+ than the set limit of {filesize_limit}"""
334
+ )
303
335
 
304
336
  if total_size:
305
337
  progress = received_size / total_size
@@ -26,6 +26,10 @@ def mock_parse_pyproject_toml():
26
26
  "another-app": {
27
27
  "ref": "src/another_app/inference.py::AnotherApp",
28
28
  },
29
+ "app-with-extras": {
30
+ "ref": "src/app_with_extras/inference.py::AppWithExtras",
31
+ "extra_key": "extra_value",
32
+ },
29
33
  }
30
34
  }
31
35
 
@@ -143,6 +147,23 @@ def test_deploy_with_toml_missing_ref_key(
143
147
  _deploy(args)
144
148
 
145
149
 
150
+ @patch("fal.cli._utils.find_pyproject_toml", return_value="pyproject.toml")
151
+ @patch("fal.cli._utils.parse_pyproject_toml")
152
+ @patch("fal.cli.deploy._deploy_from_reference")
153
+ def test_deploy_with_toml_extra_keys_in_toml(
154
+ mock_deploy_ref, mock_parse_toml, mock_find_toml, mock_parse_pyproject_toml
155
+ ):
156
+ mock_parse_toml.return_value = mock_parse_pyproject_toml
157
+
158
+ args = mock_args(app_ref=("app-with-extras", None))
159
+
160
+ with pytest.raises(
161
+ ValueError,
162
+ match="Found unexpected keys in pyproject.toml: {'extra_key': 'extra_value'}",
163
+ ):
164
+ _deploy(args)
165
+
166
+
146
167
  @patch("fal.cli._utils.find_pyproject_toml", return_value=None)
147
168
  def test_deploy_with_toml_file_not_found(mock_find_toml):
148
169
  args = mock_args(app_ref=("my-app", None))
@@ -3,8 +3,9 @@ import json
3
3
  import secrets
4
4
  import subprocess
5
5
  import time
6
+ import uuid
6
7
  from contextlib import contextmanager, suppress
7
- from datetime import datetime
8
+ from datetime import datetime, timedelta
8
9
  from typing import Generator, List, Tuple
9
10
 
10
11
  import httpx
@@ -295,8 +296,6 @@ class RealtimeApp(fal.App, keep_alive=300, max_concurrency=1):
295
296
  def aliased_app() -> Generator[Tuple[str, str], None, None]:
296
297
  # Create a temporary app, register it, and return the ID of it.
297
298
 
298
- import uuid
299
-
300
299
  app_alias = str(uuid.uuid4()) + "-alias"
301
300
  app_revision = addition_app.host.register(
302
301
  func=addition_app.func,
@@ -485,13 +484,19 @@ def test_stateful_app_client(test_stateful_app: str):
485
484
  assert response["result"] == 0
486
485
 
487
486
 
488
- @pytest.mark.flaky(max_runs=3)
489
487
  def test_app_cancellation(test_app: str, test_cancellable_app: str):
490
488
  request_handle = apps.submit(
491
489
  test_cancellable_app, arguments={"lhs": 1, "rhs": 2, "wait_time": 10}
492
490
  )
493
- # enough time for it to start
494
- time.sleep(8)
491
+
492
+ while True:
493
+ status = request_handle.status()
494
+ time.sleep(0.5)
495
+ if isinstance(status, apps.InProgress):
496
+ # The app is running
497
+ break
498
+
499
+ # cancel the request
495
500
  request_handle.cancel()
496
501
 
497
502
  # should still finish successfully and return 499
@@ -504,8 +509,14 @@ def test_app_cancellation(test_app: str, test_cancellable_app: str):
504
509
  test_app, arguments={"lhs": 1, "rhs": 2, "wait_time": 10}
505
510
  )
506
511
 
507
- # enough time for it to start
508
- time.sleep(8)
512
+ while True:
513
+ status = request_handle.status()
514
+ time.sleep(0.5)
515
+ if isinstance(status, apps.InProgress):
516
+ # The app is running
517
+ break
518
+
519
+ # cancel the request
509
520
  request_handle.cancel()
510
521
 
511
522
  response = request_handle.get()
@@ -514,8 +525,6 @@ def test_app_cancellation(test_app: str, test_cancellable_app: str):
514
525
 
515
526
  @pytest.mark.flaky(max_runs=3)
516
527
  def test_app_client_async():
517
- import uuid
518
-
519
528
  app_alias = str(uuid.uuid4()) + "-client-async-alias"
520
529
 
521
530
  app = fal.wrap_app(SleepApp)
@@ -548,11 +557,11 @@ def test_app_client_async():
548
557
  elif isinstance(event, apps.Queued):
549
558
  assert event.position == 0
550
559
 
551
- status = handle.status(logs=True)
552
- assert isinstance(status, apps.Completed)
553
- if not status.logs:
554
- # Sometimes fetching logs times out to get faster results
560
+ for _ in range(10):
555
561
  status = handle.status(logs=True)
562
+ assert isinstance(status, apps.Completed)
563
+ if status.logs:
564
+ break
556
565
 
557
566
  assert status.logs, "Logs missing from Completed status"
558
567
  assert any("sleeping..." in log["message"] for log in status.logs)
@@ -609,7 +618,6 @@ def test_404_response(test_app: str, request: pytest.FixtureRequest):
609
618
 
610
619
 
611
620
  def test_app_deploy_scale(aliased_app: Tuple[str, str]):
612
- import uuid
613
621
  from dataclasses import replace
614
622
 
615
623
  app_alias = str(uuid.uuid4()) + "-alias"
@@ -823,8 +831,10 @@ def test_workflows(test_app: str):
823
831
  assert data["result"] == 10
824
832
 
825
833
 
834
+ # If the logging subsystem is not working for some nodes, this test will flake
835
+ @pytest.mark.flaky(max_runs=5)
826
836
  def test_traceback_logs(test_exception_app: AppClient):
827
- date = datetime.utcnow().isoformat()
837
+ date = (datetime.utcnow() - timedelta(seconds=1)).isoformat()
828
838
 
829
839
  with pytest.raises(AppClientError):
830
840
  test_exception_app.fail({})
@@ -835,16 +845,25 @@ def test_traceback_logs(test_exception_app: AppClient):
835
845
  timeout=300,
836
846
  ) as client:
837
847
  # Give some time for logs to propagate through the logging subsystem.
838
- time.sleep(5)
839
- response = client.get(
840
- REST_CLIENT.base_url + f"/logs/?traceback=true&since={date}"
841
- )
842
- for log in json.loads(response.text):
843
- assert log["message"].count("\n") > 1, "Logs are multi-line"
844
- assert '{"traceback":' not in log["message"], "Logs are not JSON-wrapped"
848
+ for _ in range(10):
849
+ time.sleep(2)
850
+ response = client.get(
851
+ REST_CLIENT.base_url + f"/logs/?traceback=true&since={date}"
852
+ )
853
+
854
+ logs = response.json()
855
+ if len(logs) > 0:
856
+ break
857
+
858
+ assert len(logs) > 0
859
+ for log in logs:
860
+ assert log["message"].count("\n") > 1, "Logs should be multi-line"
861
+ assert (
862
+ '{"traceback":' not in log["message"]
863
+ ), "Logs should not be JSON-wrapped"
845
864
  assert (
846
865
  "this app is designed to fail" in log["message"]
847
- ), "Logs contain the traceback message"
866
+ ), "Logs should contain the traceback message"
848
867
 
849
868
 
850
869
  def test_app_exceptions(test_exception_app: AppClient):
@@ -866,8 +885,6 @@ def test_app_exceptions(test_exception_app: AppClient):
866
885
 
867
886
 
868
887
  def test_kill_runner():
869
- import uuid
870
-
871
888
  app_alias = str(uuid.uuid4()) + "-sleep-alias"
872
889
  app = fal.wrap_app(SleepApp)
873
890
  app_revision = app.host.register(
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