fal 1.3.4__tar.gz → 1.4.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 (162) hide show
  1. {fal-1.3.4 → fal-1.4.1}/PKG-INFO +1 -1
  2. {fal-1.3.4 → fal-1.4.1}/fal.egg-info/PKG-INFO +1 -1
  3. {fal-1.3.4 → fal-1.4.1}/fal.egg-info/SOURCES.txt +2 -0
  4. {fal-1.3.4 → fal-1.4.1}/src/fal/_fal_version.py +2 -2
  5. {fal-1.3.4 → fal-1.4.1}/src/fal/api.py +20 -10
  6. {fal-1.3.4 → fal-1.4.1}/src/fal/app.py +2 -0
  7. {fal-1.3.4 → fal-1.4.1}/src/fal/cli/_utils.py +5 -1
  8. {fal-1.3.4 → fal-1.4.1}/src/fal/sdk.py +20 -3
  9. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/file/file.py +45 -7
  10. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/file/providers/fal.py +106 -22
  11. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/file/providers/gcp.py +2 -0
  12. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/file/providers/r2.py +2 -0
  13. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/image/image.py +16 -2
  14. fal-1.4.1/src/fal/toolkit/utils/retry.py +42 -0
  15. {fal-1.3.4 → fal-1.4.1}/tests/cli/test_deploy.py +13 -2
  16. {fal-1.3.4 → fal-1.4.1}/tests/cli/test_run.py +5 -2
  17. {fal-1.3.4 → fal-1.4.1}/tests/test_apps.py +1 -1
  18. {fal-1.3.4 → fal-1.4.1}/tests/test_stability.py +8 -2
  19. fal-1.4.1/tests/toolkit/utils/retry.py +88 -0
  20. {fal-1.3.4 → fal-1.4.1}/.gitignore +0 -0
  21. {fal-1.3.4 → fal-1.4.1}/README.md +0 -0
  22. {fal-1.3.4 → fal-1.4.1}/fal.egg-info/dependency_links.txt +0 -0
  23. {fal-1.3.4 → fal-1.4.1}/fal.egg-info/entry_points.txt +0 -0
  24. {fal-1.3.4 → fal-1.4.1}/fal.egg-info/requires.txt +0 -0
  25. {fal-1.3.4 → fal-1.4.1}/fal.egg-info/top_level.txt +0 -0
  26. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/README.md +0 -0
  27. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  28. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  29. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  30. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  31. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  32. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  33. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  34. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  35. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  36. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  37. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  38. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  39. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  40. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  41. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  42. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  43. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  44. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  45. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  46. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  47. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  48. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  49. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  50. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  51. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  52. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  53. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  54. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  55. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  56. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  57. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  58. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  59. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  60. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
  61. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  62. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  63. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  64. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  65. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  66. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  67. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  68. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  69. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  70. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  71. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  72. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  73. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  74. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  75. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  76. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  77. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  78. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  79. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  80. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  81. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  82. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  83. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  84. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  85. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  86. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  87. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  88. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  89. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  90. {fal-1.3.4 → fal-1.4.1}/openapi-fal-rest/pyproject.toml +0 -0
  91. {fal-1.3.4 → fal-1.4.1}/openapi_rest.config.yaml +0 -0
  92. {fal-1.3.4 → fal-1.4.1}/pyproject.toml +0 -0
  93. {fal-1.3.4 → fal-1.4.1}/setup.cfg +0 -0
  94. {fal-1.3.4 → fal-1.4.1}/src/fal/__init__.py +0 -0
  95. {fal-1.3.4 → fal-1.4.1}/src/fal/__main__.py +0 -0
  96. {fal-1.3.4 → fal-1.4.1}/src/fal/_serialization.py +0 -0
  97. {fal-1.3.4 → fal-1.4.1}/src/fal/_version.py +0 -0
  98. {fal-1.3.4 → fal-1.4.1}/src/fal/apps.py +0 -0
  99. {fal-1.3.4 → fal-1.4.1}/src/fal/auth/__init__.py +0 -0
  100. {fal-1.3.4 → fal-1.4.1}/src/fal/auth/auth0.py +0 -0
  101. {fal-1.3.4 → fal-1.4.1}/src/fal/auth/local.py +0 -0
  102. {fal-1.3.4 → fal-1.4.1}/src/fal/cli/__init__.py +0 -0
  103. {fal-1.3.4 → fal-1.4.1}/src/fal/cli/apps.py +0 -0
  104. {fal-1.3.4 → fal-1.4.1}/src/fal/cli/auth.py +0 -0
  105. {fal-1.3.4 → fal-1.4.1}/src/fal/cli/create.py +0 -0
  106. {fal-1.3.4 → fal-1.4.1}/src/fal/cli/debug.py +0 -0
  107. {fal-1.3.4 → fal-1.4.1}/src/fal/cli/deploy.py +0 -0
  108. {fal-1.3.4 → fal-1.4.1}/src/fal/cli/doctor.py +0 -0
  109. {fal-1.3.4 → fal-1.4.1}/src/fal/cli/keys.py +0 -0
  110. {fal-1.3.4 → fal-1.4.1}/src/fal/cli/main.py +0 -0
  111. {fal-1.3.4 → fal-1.4.1}/src/fal/cli/parser.py +0 -0
  112. {fal-1.3.4 → fal-1.4.1}/src/fal/cli/run.py +0 -0
  113. {fal-1.3.4 → fal-1.4.1}/src/fal/cli/secrets.py +0 -0
  114. {fal-1.3.4 → fal-1.4.1}/src/fal/console/__init__.py +0 -0
  115. {fal-1.3.4 → fal-1.4.1}/src/fal/console/icons.py +0 -0
  116. {fal-1.3.4 → fal-1.4.1}/src/fal/console/ux.py +0 -0
  117. {fal-1.3.4 → fal-1.4.1}/src/fal/container.py +0 -0
  118. {fal-1.3.4 → fal-1.4.1}/src/fal/exceptions/__init__.py +0 -0
  119. {fal-1.3.4 → fal-1.4.1}/src/fal/exceptions/_base.py +0 -0
  120. {fal-1.3.4 → fal-1.4.1}/src/fal/exceptions/_cuda.py +0 -0
  121. {fal-1.3.4 → fal-1.4.1}/src/fal/exceptions/auth.py +0 -0
  122. {fal-1.3.4 → fal-1.4.1}/src/fal/files.py +0 -0
  123. {fal-1.3.4 → fal-1.4.1}/src/fal/flags.py +0 -0
  124. {fal-1.3.4 → fal-1.4.1}/src/fal/logging/__init__.py +0 -0
  125. {fal-1.3.4 → fal-1.4.1}/src/fal/logging/isolate.py +0 -0
  126. {fal-1.3.4 → fal-1.4.1}/src/fal/logging/style.py +0 -0
  127. {fal-1.3.4 → fal-1.4.1}/src/fal/logging/trace.py +0 -0
  128. {fal-1.3.4 → fal-1.4.1}/src/fal/logging/user.py +0 -0
  129. {fal-1.3.4 → fal-1.4.1}/src/fal/py.typed +0 -0
  130. {fal-1.3.4 → fal-1.4.1}/src/fal/rest_client.py +0 -0
  131. {fal-1.3.4 → fal-1.4.1}/src/fal/sync.py +0 -0
  132. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/__init__.py +0 -0
  133. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/exceptions.py +0 -0
  134. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/file/__init__.py +0 -0
  135. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/file/types.py +0 -0
  136. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/image/__init__.py +0 -0
  137. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
  138. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
  139. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
  140. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
  141. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
  142. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/image/safety_checker.py +0 -0
  143. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/optimize.py +0 -0
  144. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/utils/__init__.py +0 -0
  145. {fal-1.3.4 → fal-1.4.1}/src/fal/toolkit/utils/download_utils.py +0 -0
  146. {fal-1.3.4 → fal-1.4.1}/src/fal/utils.py +0 -0
  147. {fal-1.3.4 → fal-1.4.1}/src/fal/workflows.py +0 -0
  148. {fal-1.3.4 → fal-1.4.1}/tests/__init__.py +0 -0
  149. {fal-1.3.4 → fal-1.4.1}/tests/cli/__init__.py +0 -0
  150. {fal-1.3.4 → fal-1.4.1}/tests/cli/test_apps.py +0 -0
  151. {fal-1.3.4 → fal-1.4.1}/tests/cli/test_auth.py +0 -0
  152. {fal-1.3.4 → fal-1.4.1}/tests/cli/test_keys.py +0 -0
  153. {fal-1.3.4 → fal-1.4.1}/tests/cli/test_secrets.py +0 -0
  154. {fal-1.3.4 → fal-1.4.1}/tests/conftest.py +0 -0
  155. {fal-1.3.4 → fal-1.4.1}/tests/integration_test.py +0 -0
  156. {fal-1.3.4 → fal-1.4.1}/tests/mainify_package/__init__.py +0 -0
  157. {fal-1.3.4 → fal-1.4.1}/tests/mainify_package/impl.py +0 -0
  158. {fal-1.3.4 → fal-1.4.1}/tests/mainify_package/utils.py +0 -0
  159. {fal-1.3.4 → fal-1.4.1}/tests/mainify_target.py +0 -0
  160. {fal-1.3.4 → fal-1.4.1}/tests/toolkit/file_test.py +0 -0
  161. {fal-1.3.4 → fal-1.4.1}/tests/toolkit/image_test.py +0 -0
  162. {fal-1.3.4 → fal-1.4.1}/tools/demo_script.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.3.4
3
+ Version: 1.4.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.1
2
2
  Name: fal
3
- Version: 1.3.4
3
+ Version: 1.4.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
@@ -137,6 +137,7 @@ src/fal/toolkit/image/nsfw_filter/model.py
137
137
  src/fal/toolkit/image/nsfw_filter/requirements.txt
138
138
  src/fal/toolkit/utils/__init__.py
139
139
  src/fal/toolkit/utils/download_utils.py
140
+ src/fal/toolkit/utils/retry.py
140
141
  tests/__init__.py
141
142
  tests/conftest.py
142
143
  tests/integration_test.py
@@ -155,4 +156,5 @@ tests/mainify_package/impl.py
155
156
  tests/mainify_package/utils.py
156
157
  tests/toolkit/file_test.py
157
158
  tests/toolkit/image_test.py
159
+ tests/toolkit/utils/retry.py
158
160
  tools/demo_script.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.3.4'
16
- __version_tuple__ = version_tuple = (1, 3, 4)
15
+ __version__ = version = '1.4.1'
16
+ __version_tuple__ = version_tuple = (1, 4, 1)
@@ -389,6 +389,8 @@ class FalServerlessHost(Host):
389
389
  _SUPPORTED_KEYS = frozenset(
390
390
  {
391
391
  "machine_type",
392
+ "machine_types",
393
+ "num_gpus",
392
394
  "keep_alive",
393
395
  "max_concurrency",
394
396
  "min_concurrency",
@@ -431,7 +433,7 @@ class FalServerlessHost(Host):
431
433
  environment_options.setdefault("python_version", active_python())
432
434
  environments = [self._connection.define_environment(**environment_options)]
433
435
 
434
- machine_type = options.host.get(
436
+ machine_type: list[str] | str = options.host.get(
435
437
  "machine_type", FAL_SERVERLESS_DEFAULT_MACHINE_TYPE
436
438
  )
437
439
  keep_alive = options.host.get("keep_alive", FAL_SERVERLESS_DEFAULT_KEEP_ALIVE)
@@ -444,7 +446,8 @@ class FalServerlessHost(Host):
444
446
  exposed_port = options.get_exposed_port()
445
447
 
446
448
  machine_requirements = MachineRequirements(
447
- machine_type=machine_type,
449
+ machine_types=machine_type, # type: ignore
450
+ num_gpus=options.host.get("num_gpus"),
448
451
  keep_alive=keep_alive,
449
452
  base_image=base_image,
450
453
  exposed_port=exposed_port,
@@ -501,7 +504,7 @@ class FalServerlessHost(Host):
501
504
  environment_options.setdefault("python_version", active_python())
502
505
  environments = [self._connection.define_environment(**environment_options)]
503
506
 
504
- machine_type = options.host.get(
507
+ machine_type: list[str] | str = options.host.get(
505
508
  "machine_type", FAL_SERVERLESS_DEFAULT_MACHINE_TYPE
506
509
  )
507
510
  keep_alive = options.host.get("keep_alive", FAL_SERVERLESS_DEFAULT_KEEP_ALIVE)
@@ -515,7 +518,8 @@ class FalServerlessHost(Host):
515
518
  setup_function = options.host.get("setup_function", None)
516
519
 
517
520
  machine_requirements = MachineRequirements(
518
- machine_type=machine_type,
521
+ machine_types=machine_type, # type: ignore
522
+ num_gpus=options.host.get("num_gpus"),
519
523
  keep_alive=keep_alive,
520
524
  base_image=base_image,
521
525
  exposed_port=exposed_port,
@@ -684,7 +688,8 @@ def function(
684
688
  max_concurrency: int | None = None,
685
689
  # FalServerlessHost options
686
690
  metadata: dict[str, Any] | None = None,
687
- machine_type: str = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
691
+ machine_type: str | list[str] = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
692
+ num_gpus: int | None = None,
688
693
  keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
689
694
  max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
690
695
  min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
@@ -709,7 +714,8 @@ def function(
709
714
  max_concurrency: int | None = None,
710
715
  # FalServerlessHost options
711
716
  metadata: dict[str, Any] | None = None,
712
- machine_type: str = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
717
+ machine_type: str | list[str] = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
718
+ num_gpus: int | None = None,
713
719
  keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
714
720
  max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
715
721
  min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
@@ -784,7 +790,8 @@ def function(
784
790
  max_concurrency: int | None = None,
785
791
  # FalServerlessHost options
786
792
  metadata: dict[str, Any] | None = None,
787
- machine_type: str = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
793
+ machine_type: str | list[str] = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
794
+ num_gpus: int | None = None,
788
795
  keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
789
796
  max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
790
797
  min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
@@ -814,7 +821,8 @@ def function(
814
821
  max_concurrency: int | None = None,
815
822
  # FalServerlessHost options
816
823
  metadata: dict[str, Any] | None = None,
817
- machine_type: str = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
824
+ machine_type: str | list[str] = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
825
+ num_gpus: int | None = None,
818
826
  keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
819
827
  max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
820
828
  min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
@@ -838,7 +846,8 @@ def function(
838
846
  max_concurrency: int | None = None,
839
847
  # FalServerlessHost options
840
848
  metadata: dict[str, Any] | None = None,
841
- machine_type: str = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
849
+ machine_type: str | list[str] = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
850
+ num_gpus: int | None = None,
842
851
  keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
843
852
  max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
844
853
  min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
@@ -862,7 +871,8 @@ def function(
862
871
  max_concurrency: int | None = None,
863
872
  # FalServerlessHost options
864
873
  metadata: dict[str, Any] | None = None,
865
- machine_type: str = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
874
+ machine_type: str | list[str] = FAL_SERVERLESS_DEFAULT_MACHINE_TYPE,
875
+ num_gpus: int | None = None,
866
876
  keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE,
867
877
  max_multiplexing: int = FAL_SERVERLESS_DEFAULT_MAX_MULTIPLEXING,
868
878
  min_concurrency: int = FAL_SERVERLESS_DEFAULT_MIN_CONCURRENCY,
@@ -60,6 +60,7 @@ def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
60
60
  kind,
61
61
  requirements=cls.requirements,
62
62
  machine_type=cls.machine_type,
63
+ num_gpus=cls.num_gpus,
63
64
  **cls.host_kwargs,
64
65
  **kwargs,
65
66
  metadata=metadata,
@@ -177,6 +178,7 @@ def _to_fal_app_name(name: str) -> str:
177
178
  class App(fal.api.BaseServable):
178
179
  requirements: ClassVar[list[str]] = []
179
180
  machine_type: ClassVar[str] = "S"
181
+ num_gpus: ClassVar[int | None] = None
180
182
  host_kwargs: ClassVar[dict[str, Any]] = {
181
183
  "_scheduler": "nomad",
182
184
  "_scheduler_options": {
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from fal.files import find_pyproject_toml, parse_pyproject_toml
3
+ from fal.files import find_project_root, find_pyproject_toml, parse_pyproject_toml
4
4
 
5
5
 
6
6
  def is_app_name(app_ref: tuple[str, str | None]) -> bool:
@@ -29,6 +29,10 @@ def get_app_data_from_toml(app_name):
29
29
  except KeyError:
30
30
  raise ValueError(f"App {app_name} does not have a ref key in pyproject.toml")
31
31
 
32
+ # Convert the app_ref to a path relative to the project root
33
+ project_root, _ = find_project_root(None)
34
+ app_ref = str(project_root / app_ref)
35
+
32
36
  app_auth = app_data.get("auth", "private")
33
37
 
34
38
  return app_ref, app_auth
@@ -389,7 +389,8 @@ def _from_grpc_hosted_run_result(
389
389
 
390
390
  @dataclass
391
391
  class MachineRequirements:
392
- machine_type: str
392
+ machine_types: list[str]
393
+ num_gpus: int | None = field(default=None)
393
394
  keep_alive: int = FAL_SERVERLESS_DEFAULT_KEEP_ALIVE
394
395
  base_image: str | None = None
395
396
  exposed_port: int | None = None
@@ -399,6 +400,16 @@ class MachineRequirements:
399
400
  max_multiplexing: int | None = None
400
401
  min_concurrency: int | None = None
401
402
 
403
+ def __post_init__(self):
404
+ if isinstance(self.machine_types, str):
405
+ self.machine_types = [self.machine_types]
406
+
407
+ if not isinstance(self.machine_types, list):
408
+ raise ValueError("machine_types must be a list of strings.")
409
+
410
+ if not self.machine_types:
411
+ raise ValueError("No machine type provided.")
412
+
402
413
 
403
414
  @dataclass
404
415
  class FalServerlessConnection:
@@ -489,7 +500,10 @@ class FalServerlessConnection:
489
500
  wrapped_function = to_serialized_object(function, serialization_method)
490
501
  if machine_requirements:
491
502
  wrapped_requirements = isolate_proto.MachineRequirements(
492
- machine_type=machine_requirements.machine_type,
503
+ # NOTE: backwards compatibility with old API
504
+ machine_type=machine_requirements.machine_types[0],
505
+ machine_types=machine_requirements.machine_types,
506
+ num_gpus=machine_requirements.num_gpus,
493
507
  keep_alive=machine_requirements.keep_alive,
494
508
  base_image=machine_requirements.base_image,
495
509
  exposed_port=machine_requirements.exposed_port,
@@ -579,7 +593,10 @@ class FalServerlessConnection:
579
593
  wrapped_function = to_serialized_object(function, serialization_method)
580
594
  if machine_requirements:
581
595
  wrapped_requirements = isolate_proto.MachineRequirements(
582
- machine_type=machine_requirements.machine_type,
596
+ # NOTE: backwards compatibility with old API
597
+ machine_type=machine_requirements.machine_types[0],
598
+ machine_types=machine_requirements.machine_types,
599
+ num_gpus=machine_requirements.num_gpus,
583
600
  keep_alive=machine_requirements.keep_alive,
584
601
  base_image=machine_requirements.base_image,
585
602
  exposed_port=machine_requirements.exposed_port,
@@ -51,7 +51,8 @@ def get_builtin_repository(id: RepositoryId) -> FileRepository:
51
51
 
52
52
  get_builtin_repository.__module__ = "__main__"
53
53
 
54
- DEFAULT_REPOSITORY: FileRepository | RepositoryId = "fal"
54
+ DEFAULT_REPOSITORY: FileRepository | RepositoryId = "fal_v2"
55
+ FALLBACK_REPOSITORY: FileRepository | RepositoryId = "cdn"
55
56
 
56
57
 
57
58
  class File(BaseModel):
@@ -126,6 +127,9 @@ class File(BaseModel):
126
127
  content_type: Optional[str] = None,
127
128
  file_name: Optional[str] = None,
128
129
  repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
130
+ fallback_repository: Optional[
131
+ FileRepository | RepositoryId
132
+ ] = FALLBACK_REPOSITORY,
129
133
  ) -> File:
130
134
  repo = (
131
135
  repository
@@ -135,8 +139,22 @@ class File(BaseModel):
135
139
 
136
140
  fdata = FileData(data, content_type, file_name)
137
141
 
142
+ try:
143
+ url = repo.save(fdata)
144
+ except Exception:
145
+ if not fallback_repository:
146
+ raise
147
+
148
+ fallback_repo = (
149
+ fallback_repository
150
+ if isinstance(fallback_repository, FileRepository)
151
+ else get_builtin_repository(fallback_repository)
152
+ )
153
+
154
+ url = fallback_repo.save(fdata)
155
+
138
156
  return cls(
139
- url=repo.save(fdata),
157
+ url=url,
140
158
  content_type=fdata.content_type,
141
159
  file_name=fdata.file_name,
142
160
  file_size=len(data),
@@ -150,6 +168,9 @@ class File(BaseModel):
150
168
  content_type: Optional[str] = None,
151
169
  repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
152
170
  multipart: bool | None = None,
171
+ fallback_repository: Optional[
172
+ FileRepository | RepositoryId
173
+ ] = FALLBACK_REPOSITORY,
153
174
  ) -> File:
154
175
  file_path = Path(path)
155
176
  if not file_path.exists():
@@ -163,11 +184,28 @@ class File(BaseModel):
163
184
 
164
185
  content_type = content_type or "application/octet-stream"
165
186
 
166
- url, data = repo.save_file(
167
- file_path,
168
- content_type=content_type,
169
- multipart=multipart,
170
- )
187
+ try:
188
+ url, data = repo.save_file(
189
+ file_path,
190
+ content_type=content_type,
191
+ multipart=multipart,
192
+ )
193
+ except Exception:
194
+ if not fallback_repository:
195
+ raise
196
+
197
+ fallback_repo = (
198
+ fallback_repository
199
+ if isinstance(fallback_repository, FileRepository)
200
+ else get_builtin_repository(fallback_repository)
201
+ )
202
+
203
+ url, data = fallback_repo.save_file(
204
+ file_path,
205
+ content_type=content_type,
206
+ multipart=multipart,
207
+ )
208
+
171
209
  return cls(
172
210
  url=url,
173
211
  file_data=data.data if data else None,
@@ -4,19 +4,91 @@ import dataclasses
4
4
  import json
5
5
  import math
6
6
  import os
7
+ import threading
7
8
  from base64 import b64encode
8
9
  from dataclasses import dataclass
10
+ from datetime import datetime, timezone
9
11
  from pathlib import Path
10
12
  from urllib.error import HTTPError
13
+ from urllib.parse import urlparse, urlunparse
11
14
  from urllib.request import Request, urlopen
12
15
 
13
16
  from fal.auth import key_credentials
14
17
  from fal.toolkit.exceptions import FileUploadException
15
18
  from fal.toolkit.file.types import FileData, FileRepository
19
+ from fal.toolkit.utils.retry import retry
16
20
 
17
21
  _FAL_CDN = "https://fal.media"
18
22
 
19
23
 
24
+ @dataclass
25
+ class FalV2Token:
26
+ token: str
27
+ token_type: str
28
+ base_upload_url: str
29
+ expires_at: datetime
30
+
31
+ def is_expired(self) -> bool:
32
+ return datetime.now(timezone.utc) >= self.expires_at
33
+
34
+
35
+ class FalV2TokenManager:
36
+ def __init__(self):
37
+ self._token: FalV2Token = FalV2Token(
38
+ token="",
39
+ token_type="",
40
+ base_upload_url="",
41
+ expires_at=datetime.min.replace(tzinfo=timezone.utc),
42
+ )
43
+ self._lock: threading.Lock = threading.Lock()
44
+
45
+ def get_token(self) -> FalV2Token:
46
+ with self._lock:
47
+ if self._token.is_expired():
48
+ self._refresh_token()
49
+ return self._token
50
+
51
+ def _refresh_token(self) -> None:
52
+ key_creds = key_credentials()
53
+ if not key_creds:
54
+ raise FileUploadException("FAL_KEY must be set")
55
+
56
+ key_id, key_secret = key_creds
57
+ headers = {
58
+ "Authorization": f"Key {key_id}:{key_secret}",
59
+ "Accept": "application/json",
60
+ "Content-Type": "application/json",
61
+ }
62
+
63
+ grpc_host = os.environ.get("FAL_HOST", "api.alpha.fal.ai")
64
+ rest_host = grpc_host.replace("api", "rest", 1)
65
+ url = f"https://{rest_host}/storage/auth/token"
66
+
67
+ req = Request(
68
+ url,
69
+ headers=headers,
70
+ data=b"{}",
71
+ method="POST",
72
+ )
73
+ with urlopen(req) as response:
74
+ result = json.load(response)
75
+
76
+ parsed_base_url = urlparse(result["base_url"])
77
+ base_upload_url = urlunparse(
78
+ parsed_base_url._replace(netloc="upload." + parsed_base_url.netloc)
79
+ )
80
+
81
+ self._token = FalV2Token(
82
+ token=result["token"],
83
+ token_type=result["token_type"],
84
+ base_upload_url=base_upload_url,
85
+ expires_at=datetime.fromisoformat(result["expires_at"]),
86
+ )
87
+
88
+
89
+ fal_v2_token_manager = FalV2TokenManager()
90
+
91
+
20
92
  @dataclass
21
93
  class ObjectLifecyclePreference:
22
94
  expriation_duration_seconds: int
@@ -29,6 +101,7 @@ GLOBAL_LIFECYCLE_PREFERENCE = ObjectLifecyclePreference(
29
101
 
30
102
  @dataclass
31
103
  class FalFileRepositoryBase(FileRepository):
104
+ @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
32
105
  def _save(self, file: FileData, storage_type: str) -> str:
33
106
  key_creds = key_credentials()
34
107
  if not key_creds:
@@ -108,26 +181,14 @@ class MultipartUpload:
108
181
 
109
182
  self._parts: list[dict] = []
110
183
 
111
- key_creds = key_credentials()
112
- if not key_creds:
113
- raise FileUploadException("FAL_KEY must be set")
114
-
115
- key_id, key_secret = key_creds
116
-
117
- self._auth_headers = {
118
- "Authorization": f"Key {key_id}:{key_secret}",
119
- }
120
- grpc_host = os.environ.get("FAL_HOST", "api.alpha.fal.ai")
121
- rest_host = grpc_host.replace("api", "rest", 1)
122
- self._storage_upload_url = f"https://{rest_host}/storage/upload"
123
-
124
184
  def create(self):
185
+ token = fal_v2_token_manager.get_token()
125
186
  try:
126
187
  req = Request(
127
- f"{self._storage_upload_url}/initiate-multipart",
188
+ f"{token.base_upload_url}/upload/initiate-multipart",
128
189
  method="POST",
129
190
  headers={
130
- **self._auth_headers,
191
+ "Authorization": f"{token.token_type} {token.token}",
131
192
  "Accept": "application/json",
132
193
  "Content-Type": "application/json",
133
194
  },
@@ -140,7 +201,7 @@ class MultipartUpload:
140
201
  )
141
202
  with urlopen(req) as response:
142
203
  result = json.load(response)
143
- self._upload_id = result["upload_id"]
204
+ self._upload_url = result["upload_url"]
144
205
  self._file_url = result["file_url"]
145
206
  except HTTPError as exc:
146
207
  raise FileUploadException(
@@ -180,10 +241,7 @@ class MultipartUpload:
180
241
  ) as executor:
181
242
  futures = []
182
243
  for part_number in range(1, parts + 1):
183
- upload_url = (
184
- f"{self._file_url}?upload_id={self._upload_id}"
185
- f"&part_number={part_number}"
186
- )
244
+ upload_url = f"{self._upload_url}&part_number={part_number}"
187
245
  futures.append(
188
246
  executor.submit(self._upload_part, upload_url, part_number)
189
247
  )
@@ -193,7 +251,7 @@ class MultipartUpload:
193
251
  self._parts.append(entry)
194
252
 
195
253
  def complete(self):
196
- url = f"{self._file_url}?upload_id={self._upload_id}"
254
+ url = self._upload_url
197
255
  try:
198
256
  req = Request(
199
257
  url,
@@ -216,8 +274,33 @@ class MultipartUpload:
216
274
 
217
275
  @dataclass
218
276
  class FalFileRepositoryV2(FalFileRepositoryBase):
277
+ @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
219
278
  def save(self, file: FileData) -> str:
220
- return self._save(file, "fal-cdn")
279
+ token = fal_v2_token_manager.get_token()
280
+ headers = {
281
+ "Authorization": f"{token.token_type} {token.token}",
282
+ "Accept": "application/json",
283
+ "X-Fal-File-Name": file.file_name,
284
+ "Content-Type": file.content_type,
285
+ }
286
+
287
+ storage_url = f"{token.base_upload_url}/upload"
288
+
289
+ try:
290
+ req = Request(
291
+ storage_url,
292
+ data=file.data,
293
+ headers=headers,
294
+ method="PUT",
295
+ )
296
+ with urlopen(req) as response:
297
+ result = json.load(response)
298
+
299
+ return result["file_url"]
300
+ except HTTPError as e:
301
+ raise FileUploadException(
302
+ f"Error initiating upload. Status {e.status}: {e.reason}"
303
+ )
221
304
 
222
305
  def _save_multipart(
223
306
  self,
@@ -280,6 +363,7 @@ class InMemoryRepository(FileRepository):
280
363
 
281
364
  @dataclass
282
365
  class FalCDNFileRepository(FileRepository):
366
+ @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
283
367
  def save(
284
368
  self,
285
369
  file: FileData,
@@ -8,6 +8,7 @@ import uuid
8
8
  from dataclasses import dataclass
9
9
 
10
10
  from fal.toolkit.file.types import FileData, FileRepository
11
+ from fal.toolkit.utils.retry import retry
11
12
 
12
13
  DEFAULT_URL_TIMEOUT = 60 * 15 # 15 minutes
13
14
 
@@ -50,6 +51,7 @@ class GoogleStorageRepository(FileRepository):
50
51
 
51
52
  return self._bucket
52
53
 
54
+ @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
53
55
  def save(self, data: FileData) -> str:
54
56
  destination_path = posixpath.join(
55
57
  self.folder,
@@ -8,6 +8,7 @@ from dataclasses import dataclass
8
8
  from io import BytesIO
9
9
 
10
10
  from fal.toolkit.file.types import FileData, FileRepository
11
+ from fal.toolkit.utils.retry import retry
11
12
 
12
13
  DEFAULT_URL_TIMEOUT = 60 * 15 # 15 minutes
13
14
 
@@ -67,6 +68,7 @@ class R2Repository(FileRepository):
67
68
 
68
69
  return self._bucket
69
70
 
71
+ @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
70
72
  def save(self, data: FileData) -> str:
71
73
  destination_path = posixpath.join(
72
74
  self.key,
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Literal, Optional, Union
6
6
 
7
7
  from pydantic import BaseModel, Field
8
8
 
9
- from fal.toolkit.file.file import DEFAULT_REPOSITORY, File
9
+ from fal.toolkit.file.file import DEFAULT_REPOSITORY, FALLBACK_REPOSITORY, File
10
10
  from fal.toolkit.file.types import FileRepository, RepositoryId
11
11
  from fal.toolkit.utils.download_utils import _download_file_python
12
12
 
@@ -79,12 +79,16 @@ class Image(File):
79
79
  size: ImageSize | None = None,
80
80
  file_name: str | None = None,
81
81
  repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
82
+ fallback_repository: Optional[
83
+ FileRepository | RepositoryId
84
+ ] = FALLBACK_REPOSITORY,
82
85
  ) -> Image:
83
86
  obj = super().from_bytes(
84
87
  data,
85
88
  content_type=f"image/{format}",
86
89
  file_name=file_name,
87
90
  repository=repository,
91
+ fallback_repository=fallback_repository,
88
92
  )
89
93
  obj.width = size.width if size else None
90
94
  obj.height = size.height if size else None
@@ -97,6 +101,9 @@ class Image(File):
97
101
  format: ImageFormat | None = None,
98
102
  file_name: str | None = None,
99
103
  repository: FileRepository | RepositoryId = DEFAULT_REPOSITORY,
104
+ fallback_repository: Optional[
105
+ FileRepository | RepositoryId
106
+ ] = FALLBACK_REPOSITORY,
100
107
  ) -> Image:
101
108
  size = ImageSize(width=pil_image.width, height=pil_image.height)
102
109
  if format is None:
@@ -119,7 +126,14 @@ class Image(File):
119
126
  pil_image.save(f, format=format, **saving_options)
120
127
  raw_image = f.getvalue()
121
128
 
122
- return cls.from_bytes(raw_image, format, size, file_name, repository)
129
+ return cls.from_bytes(
130
+ raw_image,
131
+ format,
132
+ size,
133
+ file_name,
134
+ repository,
135
+ fallback_repository=fallback_repository,
136
+ )
123
137
 
124
138
  def to_pil(self, mode: str = "RGB") -> PILImage.Image:
125
139
  try:
@@ -0,0 +1,42 @@
1
+ import functools
2
+ import random
3
+ import time
4
+ from typing import Any, Callable, Literal
5
+
6
+ BackoffType = Literal["exponential", "fixed"]
7
+
8
+
9
+ def retry(
10
+ max_retries: int = 3,
11
+ base_delay: float = 1.0,
12
+ max_delay: float = 60.0,
13
+ backoff_type: BackoffType = "exponential",
14
+ jitter: bool = False,
15
+ ) -> Callable:
16
+ def decorator(func: Callable) -> Callable:
17
+ @functools.wraps(func)
18
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
19
+ retries = 0
20
+ while retries < max_retries:
21
+ try:
22
+ return func(*args, **kwargs)
23
+ except Exception as e:
24
+ retries += 1
25
+ print(f"Retrying {retries} of {max_retries}...")
26
+ if retries == max_retries:
27
+ print(f"Max retries reached. Raising exception: {e}")
28
+ raise e
29
+
30
+ if backoff_type == "exponential":
31
+ delay = min(base_delay * (2 ** (retries - 1)), max_delay)
32
+ else: # fixed
33
+ delay = min(base_delay, max_delay)
34
+
35
+ if jitter:
36
+ delay *= random.uniform(0.5, 1.5)
37
+
38
+ time.sleep(delay)
39
+
40
+ return wrapper
41
+
42
+ return decorator