fal 1.7.2__tar.gz → 1.7.4__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 (171) hide show
  1. {fal-1.7.2/fal.egg-info → fal-1.7.4}/PKG-INFO +1 -1
  2. {fal-1.7.2 → fal-1.7.4/fal.egg-info}/PKG-INFO +1 -1
  3. {fal-1.7.2 → fal-1.7.4}/src/fal/_fal_version.py +2 -2
  4. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/file/providers/fal.py +237 -121
  5. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/utils/download_utils.py +5 -1
  6. {fal-1.7.2 → fal-1.7.4}/tests/integration_test.py +22 -6
  7. {fal-1.7.2 → fal-1.7.4}/.gitignore +0 -0
  8. {fal-1.7.2 → fal-1.7.4}/Makefile +0 -0
  9. {fal-1.7.2 → fal-1.7.4}/README.md +0 -0
  10. {fal-1.7.2 → fal-1.7.4}/docs/conf.py +0 -0
  11. {fal-1.7.2 → fal-1.7.4}/docs/index.rst +0 -0
  12. {fal-1.7.2 → fal-1.7.4}/fal.egg-info/SOURCES.txt +0 -0
  13. {fal-1.7.2 → fal-1.7.4}/fal.egg-info/dependency_links.txt +0 -0
  14. {fal-1.7.2 → fal-1.7.4}/fal.egg-info/entry_points.txt +0 -0
  15. {fal-1.7.2 → fal-1.7.4}/fal.egg-info/requires.txt +0 -0
  16. {fal-1.7.2 → fal-1.7.4}/fal.egg-info/top_level.txt +0 -0
  17. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/README.md +0 -0
  18. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  19. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  20. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  21. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  22. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  23. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  24. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  25. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  26. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  27. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  28. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  29. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  30. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  31. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  32. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  33. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  34. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  35. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  36. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  37. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  38. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  39. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  40. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  41. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  42. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  43. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  44. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  45. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  46. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  47. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  48. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  49. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  50. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  51. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
  52. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  53. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  54. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  55. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  56. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  57. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  58. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  59. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  60. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  61. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  62. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  63. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  64. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  65. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  66. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  67. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  68. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  69. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  70. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  71. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  72. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  73. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  74. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  75. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  76. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  77. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  78. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  79. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  80. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  81. {fal-1.7.2 → fal-1.7.4}/openapi-fal-rest/pyproject.toml +0 -0
  82. {fal-1.7.2 → fal-1.7.4}/openapi_rest.config.yaml +0 -0
  83. {fal-1.7.2 → fal-1.7.4}/pyproject.toml +0 -0
  84. {fal-1.7.2 → fal-1.7.4}/setup.cfg +0 -0
  85. {fal-1.7.2 → fal-1.7.4}/src/fal/__init__.py +0 -0
  86. {fal-1.7.2 → fal-1.7.4}/src/fal/__main__.py +0 -0
  87. {fal-1.7.2 → fal-1.7.4}/src/fal/_serialization.py +0 -0
  88. {fal-1.7.2 → fal-1.7.4}/src/fal/_version.py +0 -0
  89. {fal-1.7.2 → fal-1.7.4}/src/fal/api.py +0 -0
  90. {fal-1.7.2 → fal-1.7.4}/src/fal/app.py +0 -0
  91. {fal-1.7.2 → fal-1.7.4}/src/fal/apps.py +0 -0
  92. {fal-1.7.2 → fal-1.7.4}/src/fal/auth/__init__.py +0 -0
  93. {fal-1.7.2 → fal-1.7.4}/src/fal/auth/auth0.py +0 -0
  94. {fal-1.7.2 → fal-1.7.4}/src/fal/auth/local.py +0 -0
  95. {fal-1.7.2 → fal-1.7.4}/src/fal/cli/__init__.py +0 -0
  96. {fal-1.7.2 → fal-1.7.4}/src/fal/cli/_utils.py +0 -0
  97. {fal-1.7.2 → fal-1.7.4}/src/fal/cli/apps.py +0 -0
  98. {fal-1.7.2 → fal-1.7.4}/src/fal/cli/auth.py +0 -0
  99. {fal-1.7.2 → fal-1.7.4}/src/fal/cli/create.py +0 -0
  100. {fal-1.7.2 → fal-1.7.4}/src/fal/cli/debug.py +0 -0
  101. {fal-1.7.2 → fal-1.7.4}/src/fal/cli/deploy.py +0 -0
  102. {fal-1.7.2 → fal-1.7.4}/src/fal/cli/doctor.py +0 -0
  103. {fal-1.7.2 → fal-1.7.4}/src/fal/cli/keys.py +0 -0
  104. {fal-1.7.2 → fal-1.7.4}/src/fal/cli/main.py +0 -0
  105. {fal-1.7.2 → fal-1.7.4}/src/fal/cli/parser.py +0 -0
  106. {fal-1.7.2 → fal-1.7.4}/src/fal/cli/run.py +0 -0
  107. {fal-1.7.2 → fal-1.7.4}/src/fal/cli/runners.py +0 -0
  108. {fal-1.7.2 → fal-1.7.4}/src/fal/cli/secrets.py +0 -0
  109. {fal-1.7.2 → fal-1.7.4}/src/fal/config.py +0 -0
  110. {fal-1.7.2 → fal-1.7.4}/src/fal/console/__init__.py +0 -0
  111. {fal-1.7.2 → fal-1.7.4}/src/fal/console/icons.py +0 -0
  112. {fal-1.7.2 → fal-1.7.4}/src/fal/console/ux.py +0 -0
  113. {fal-1.7.2 → fal-1.7.4}/src/fal/container.py +0 -0
  114. {fal-1.7.2 → fal-1.7.4}/src/fal/exceptions/__init__.py +0 -0
  115. {fal-1.7.2 → fal-1.7.4}/src/fal/exceptions/_base.py +0 -0
  116. {fal-1.7.2 → fal-1.7.4}/src/fal/exceptions/_cuda.py +0 -0
  117. {fal-1.7.2 → fal-1.7.4}/src/fal/exceptions/auth.py +0 -0
  118. {fal-1.7.2 → fal-1.7.4}/src/fal/files.py +0 -0
  119. {fal-1.7.2 → fal-1.7.4}/src/fal/flags.py +0 -0
  120. {fal-1.7.2 → fal-1.7.4}/src/fal/logging/__init__.py +0 -0
  121. {fal-1.7.2 → fal-1.7.4}/src/fal/logging/isolate.py +0 -0
  122. {fal-1.7.2 → fal-1.7.4}/src/fal/logging/style.py +0 -0
  123. {fal-1.7.2 → fal-1.7.4}/src/fal/logging/trace.py +0 -0
  124. {fal-1.7.2 → fal-1.7.4}/src/fal/logging/user.py +0 -0
  125. {fal-1.7.2 → fal-1.7.4}/src/fal/py.typed +0 -0
  126. {fal-1.7.2 → fal-1.7.4}/src/fal/rest_client.py +0 -0
  127. {fal-1.7.2 → fal-1.7.4}/src/fal/sdk.py +0 -0
  128. {fal-1.7.2 → fal-1.7.4}/src/fal/sync.py +0 -0
  129. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/__init__.py +0 -0
  130. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/exceptions.py +0 -0
  131. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/file/__init__.py +0 -0
  132. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/file/file.py +0 -0
  133. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/file/providers/gcp.py +0 -0
  134. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/file/providers/r2.py +0 -0
  135. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/file/providers/s3.py +0 -0
  136. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/file/types.py +0 -0
  137. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/image/__init__.py +0 -0
  138. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/image/image.py +0 -0
  139. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
  140. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
  141. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
  142. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
  143. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
  144. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/image/safety_checker.py +0 -0
  145. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/optimize.py +0 -0
  146. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/types.py +0 -0
  147. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/utils/__init__.py +0 -0
  148. {fal-1.7.2 → fal-1.7.4}/src/fal/toolkit/utils/retry.py +0 -0
  149. {fal-1.7.2 → fal-1.7.4}/src/fal/utils.py +0 -0
  150. {fal-1.7.2 → fal-1.7.4}/src/fal/workflows.py +0 -0
  151. {fal-1.7.2 → fal-1.7.4}/tests/__init__.py +0 -0
  152. {fal-1.7.2 → fal-1.7.4}/tests/assets/cat.png +0 -0
  153. {fal-1.7.2 → fal-1.7.4}/tests/cli/__init__.py +0 -0
  154. {fal-1.7.2 → fal-1.7.4}/tests/cli/test_apps.py +0 -0
  155. {fal-1.7.2 → fal-1.7.4}/tests/cli/test_auth.py +0 -0
  156. {fal-1.7.2 → fal-1.7.4}/tests/cli/test_deploy.py +0 -0
  157. {fal-1.7.2 → fal-1.7.4}/tests/cli/test_keys.py +0 -0
  158. {fal-1.7.2 → fal-1.7.4}/tests/cli/test_run.py +0 -0
  159. {fal-1.7.2 → fal-1.7.4}/tests/cli/test_secrets.py +0 -0
  160. {fal-1.7.2 → fal-1.7.4}/tests/conftest.py +0 -0
  161. {fal-1.7.2 → fal-1.7.4}/tests/mainify_package/__init__.py +0 -0
  162. {fal-1.7.2 → fal-1.7.4}/tests/mainify_package/impl.py +0 -0
  163. {fal-1.7.2 → fal-1.7.4}/tests/mainify_package/utils.py +0 -0
  164. {fal-1.7.2 → fal-1.7.4}/tests/mainify_target.py +0 -0
  165. {fal-1.7.2 → fal-1.7.4}/tests/test_apps.py +0 -0
  166. {fal-1.7.2 → fal-1.7.4}/tests/test_stability.py +0 -0
  167. {fal-1.7.2 → fal-1.7.4}/tests/toolkit/file_test.py +0 -0
  168. {fal-1.7.2 → fal-1.7.4}/tests/toolkit/image_test.py +0 -0
  169. {fal-1.7.2 → fal-1.7.4}/tests/toolkit/test_types.py +0 -0
  170. {fal-1.7.2 → fal-1.7.4}/tests/toolkit/utils/retry.py +0 -0
  171. {fal-1.7.2 → fal-1.7.4}/tools/demo_script.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: fal
3
- Version: 1.7.2
3
+ Version: 1.7.4
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.2
2
2
  Name: fal
3
- Version: 1.7.2
3
+ Version: 1.7.4
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
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.7.2'
16
- __version_tuple__ = version_tuple = (1, 7, 2)
15
+ __version__ = version = '1.7.4'
16
+ __version_tuple__ = version_tuple = (1, 7, 4)
@@ -205,12 +205,12 @@ class MultipartUpload:
205
205
 
206
206
  def __init__(
207
207
  self,
208
- file_path: str | Path,
208
+ file_name: str,
209
209
  chunk_size: int | None = None,
210
210
  content_type: str | None = None,
211
211
  max_concurrency: int | None = None,
212
212
  ) -> None:
213
- self.file_path = file_path
213
+ self.file_name = file_name
214
214
  self.chunk_size = chunk_size or self.MULTIPART_CHUNK_SIZE
215
215
  self.content_type = content_type or "application/octet-stream"
216
216
  self.max_concurrency = max_concurrency or self.MULTIPART_MAX_CONCURRENCY
@@ -230,7 +230,7 @@ class MultipartUpload:
230
230
  },
231
231
  data=json.dumps(
232
232
  {
233
- "file_name": os.path.basename(self.file_path),
233
+ "file_name": self.file_name,
234
234
  "content_type": self.content_type,
235
235
  }
236
236
  ).encode(),
@@ -244,47 +244,29 @@ class MultipartUpload:
244
244
  f"Error initiating upload. Status {exc.status}: {exc.reason}"
245
245
  )
246
246
 
247
- def _upload_part(self, url: str, part_number: int) -> dict:
248
- with open(self.file_path, "rb") as f:
249
- start = (part_number - 1) * self.chunk_size
250
- f.seek(start)
251
- data = f.read(self.chunk_size)
252
- req = Request(
253
- url,
254
- method="PUT",
255
- headers={"Content-Type": self.content_type},
256
- data=data,
257
- )
247
+ def upload_part(self, part_number: int, data: bytes) -> None:
248
+ url = f"{self._upload_url}&part_number={part_number}"
249
+
250
+ req = Request(
251
+ url,
252
+ method="PUT",
253
+ headers={"Content-Type": self.content_type},
254
+ data=data,
255
+ )
258
256
 
259
- try:
260
- with urlopen(req) as resp:
261
- return {
257
+ try:
258
+ with urlopen(req) as resp:
259
+ self._parts.append(
260
+ {
262
261
  "part_number": part_number,
263
262
  "etag": resp.headers["ETag"],
264
263
  }
265
- except HTTPError as exc:
266
- raise FileUploadException(
267
- f"Error uploading part {part_number} to {url}. "
268
- f"Status {exc.status}: {exc.reason}"
269
264
  )
270
-
271
- def upload(self) -> None:
272
- import concurrent.futures
273
-
274
- parts = math.ceil(os.path.getsize(self.file_path) / self.chunk_size)
275
- with concurrent.futures.ThreadPoolExecutor(
276
- max_workers=self.max_concurrency
277
- ) as executor:
278
- futures = []
279
- for part_number in range(1, parts + 1):
280
- upload_url = f"{self._upload_url}&part_number={part_number}"
281
- futures.append(
282
- executor.submit(self._upload_part, upload_url, part_number)
283
- )
284
-
285
- for future in concurrent.futures.as_completed(futures):
286
- entry = future.result()
287
- self._parts.append(entry)
265
+ except HTTPError as exc:
266
+ raise FileUploadException(
267
+ f"Error uploading part {part_number} to {url}. "
268
+ f"Status {exc.status}: {exc.reason}"
269
+ )
288
270
 
289
271
  def complete(self):
290
272
  url = self._upload_url
@@ -307,6 +289,82 @@ class MultipartUpload:
307
289
 
308
290
  return self._file_url
309
291
 
292
+ @classmethod
293
+ def save(
294
+ cls,
295
+ file: FileData,
296
+ chunk_size: int | None = None,
297
+ max_concurrency: int | None = None,
298
+ ):
299
+ import concurrent.futures
300
+
301
+ multipart = cls(
302
+ file.file_name,
303
+ chunk_size=chunk_size,
304
+ content_type=file.content_type,
305
+ max_concurrency=max_concurrency,
306
+ )
307
+ multipart.create()
308
+
309
+ parts = math.ceil(len(file.data) / multipart.chunk_size)
310
+ with concurrent.futures.ThreadPoolExecutor(
311
+ max_workers=multipart.max_concurrency
312
+ ) as executor:
313
+ futures = []
314
+ for part_number in range(1, parts + 1):
315
+ start = (part_number - 1) * multipart.chunk_size
316
+ data = file.data[start : start + multipart.chunk_size]
317
+ futures.append(
318
+ executor.submit(multipart.upload_part, part_number, data)
319
+ )
320
+
321
+ for future in concurrent.futures.as_completed(futures):
322
+ future.result()
323
+
324
+ return multipart.complete()
325
+
326
+ @classmethod
327
+ def save_file(
328
+ cls,
329
+ file_path: str | Path,
330
+ chunk_size: int | None = None,
331
+ content_type: str | None = None,
332
+ max_concurrency: int | None = None,
333
+ ) -> str:
334
+ import concurrent.futures
335
+
336
+ file_name = os.path.basename(file_path)
337
+ size = os.path.getsize(file_path)
338
+
339
+ multipart = cls(
340
+ file_name,
341
+ chunk_size=chunk_size,
342
+ content_type=content_type,
343
+ max_concurrency=max_concurrency,
344
+ )
345
+ multipart.create()
346
+
347
+ parts = math.ceil(size / multipart.chunk_size)
348
+ with concurrent.futures.ThreadPoolExecutor(
349
+ max_workers=multipart.max_concurrency
350
+ ) as executor:
351
+ futures = []
352
+ for part_number in range(1, parts + 1):
353
+
354
+ def _upload_part(pn: int) -> None:
355
+ with open(file_path, "rb") as f:
356
+ start = (pn - 1) * multipart.chunk_size
357
+ f.seek(start)
358
+ data = f.read(multipart.chunk_size)
359
+ multipart.upload_part(pn, data)
360
+
361
+ futures.append(executor.submit(_upload_part, part_number))
362
+
363
+ for future in concurrent.futures.as_completed(futures):
364
+ future.result()
365
+
366
+ return multipart.complete()
367
+
310
368
 
311
369
  class InternalMultipartUploadV3:
312
370
  MULTIPART_THRESHOLD = 100 * 1024 * 1024
@@ -315,12 +373,12 @@ class InternalMultipartUploadV3:
315
373
 
316
374
  def __init__(
317
375
  self,
318
- file_path: str | Path,
376
+ file_name: str,
319
377
  chunk_size: int | None = None,
320
378
  content_type: str | None = None,
321
379
  max_concurrency: int | None = None,
322
380
  ) -> None:
323
- self.file_path = file_path
381
+ self.file_name = file_name
324
382
  self.chunk_size = chunk_size or self.MULTIPART_CHUNK_SIZE
325
383
  self.content_type = content_type or "application/octet-stream"
326
384
  self.max_concurrency = max_concurrency or self.MULTIPART_MAX_CONCURRENCY
@@ -359,7 +417,7 @@ class InternalMultipartUploadV3:
359
417
  **self.auth_headers,
360
418
  "Accept": "application/json",
361
419
  "Content-Type": self.content_type,
362
- "X-Fal-File-Name": os.path.basename(self.file_path),
420
+ "X-Fal-File-Name": self.file_name,
363
421
  },
364
422
  )
365
423
  with urlopen(req) as response:
@@ -373,52 +431,32 @@ class InternalMultipartUploadV3:
373
431
  )
374
432
 
375
433
  @retry(max_retries=5, base_delay=1, backoff_type="exponential", jitter=True)
376
- def _upload_part(self, url: str, part_number: int) -> dict:
377
- with open(self.file_path, "rb") as f:
378
- start = (part_number - 1) * self.chunk_size
379
- f.seek(start)
380
- data = f.read(self.chunk_size)
381
- req = Request(
382
- url,
383
- method="PUT",
384
- headers={
385
- **self.auth_headers,
386
- "Content-Type": self.content_type,
387
- },
388
- data=data,
389
- )
434
+ def upload_part(self, part_number: int, data: bytes) -> None:
435
+ url = f"{self.access_url}/multipart/{self.upload_id}/{part_number}"
390
436
 
391
- try:
392
- with urlopen(req) as resp:
393
- return {
437
+ req = Request(
438
+ url,
439
+ method="PUT",
440
+ headers={
441
+ **self.auth_headers,
442
+ "Content-Type": self.content_type,
443
+ },
444
+ data=data,
445
+ )
446
+
447
+ try:
448
+ with urlopen(req) as resp:
449
+ self._parts.append(
450
+ {
394
451
  "partNumber": part_number,
395
452
  "etag": resp.headers["ETag"],
396
453
  }
397
- except HTTPError as exc:
398
- raise FileUploadException(
399
- f"Error uploading part {part_number} to {url}. "
400
- f"Status {exc.status}: {exc.reason}"
401
- )
402
-
403
- def upload(self) -> None:
404
- import concurrent.futures
405
-
406
- parts = math.ceil(os.path.getsize(self.file_path) / self.chunk_size)
407
- with concurrent.futures.ThreadPoolExecutor(
408
- max_workers=self.max_concurrency
409
- ) as executor:
410
- futures = []
411
- for part_number in range(1, parts + 1):
412
- upload_url = (
413
- f"{self.access_url}/multipart/{self.upload_id}/{part_number}"
414
- )
415
- futures.append(
416
- executor.submit(self._upload_part, upload_url, part_number)
417
454
  )
418
-
419
- for future in concurrent.futures.as_completed(futures):
420
- entry = future.result()
421
- self._parts.append(entry)
455
+ except HTTPError as exc:
456
+ raise FileUploadException(
457
+ f"Error uploading part {part_number} to {url}. "
458
+ f"Status {exc.status}: {exc.reason}"
459
+ )
422
460
 
423
461
  def complete(self) -> str:
424
462
  url = f"{self.access_url}/multipart/{self.upload_id}/complete"
@@ -442,13 +480,106 @@ class InternalMultipartUploadV3:
442
480
 
443
481
  return self.access_url
444
482
 
483
+ @classmethod
484
+ def save(
485
+ cls,
486
+ file: FileData,
487
+ chunk_size: int | None = None,
488
+ max_concurrency: int | None = None,
489
+ ):
490
+ import concurrent.futures
491
+
492
+ multipart = cls(
493
+ file.file_name,
494
+ chunk_size=chunk_size,
495
+ content_type=file.content_type,
496
+ max_concurrency=max_concurrency,
497
+ )
498
+ multipart.create()
499
+
500
+ parts = math.ceil(len(file.data) / multipart.chunk_size)
501
+ with concurrent.futures.ThreadPoolExecutor(
502
+ max_workers=multipart.max_concurrency
503
+ ) as executor:
504
+ futures = []
505
+ for part_number in range(1, parts + 1):
506
+ start = (part_number - 1) * multipart.chunk_size
507
+ data = file.data[start : start + multipart.chunk_size]
508
+ futures.append(
509
+ executor.submit(multipart.upload_part, part_number, data)
510
+ )
511
+
512
+ for future in concurrent.futures.as_completed(futures):
513
+ future.result()
514
+
515
+ return multipart.complete()
516
+
517
+ @classmethod
518
+ def save_file(
519
+ cls,
520
+ file_path: str | Path,
521
+ chunk_size: int | None = None,
522
+ content_type: str | None = None,
523
+ max_concurrency: int | None = None,
524
+ ) -> str:
525
+ import concurrent.futures
526
+
527
+ file_name = os.path.basename(file_path)
528
+ size = os.path.getsize(file_path)
529
+
530
+ multipart = cls(
531
+ file_name,
532
+ chunk_size=chunk_size,
533
+ content_type=content_type,
534
+ max_concurrency=max_concurrency,
535
+ )
536
+ multipart.create()
537
+
538
+ parts = math.ceil(size / multipart.chunk_size)
539
+ with concurrent.futures.ThreadPoolExecutor(
540
+ max_workers=multipart.max_concurrency
541
+ ) as executor:
542
+ futures = []
543
+ for part_number in range(1, parts + 1):
544
+
545
+ def _upload_part(pn: int) -> None:
546
+ with open(file_path, "rb") as f:
547
+ start = (pn - 1) * multipart.chunk_size
548
+ f.seek(start)
549
+ data = f.read(multipart.chunk_size)
550
+ multipart.upload_part(pn, data)
551
+
552
+ futures.append(executor.submit(_upload_part, part_number))
553
+
554
+ for future in concurrent.futures.as_completed(futures):
555
+ future.result()
556
+
557
+ return multipart.complete()
558
+
445
559
 
446
560
  @dataclass
447
561
  class FalFileRepositoryV2(FalFileRepositoryBase):
448
562
  @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
449
563
  def save(
450
- self, file: FileData, object_lifecycle_preference: dict[str, str] | None = None
564
+ self,
565
+ file: FileData,
566
+ multipart: bool | None = None,
567
+ multipart_threshold: int | None = None,
568
+ multipart_chunk_size: int | None = None,
569
+ multipart_max_concurrency: int | None = None,
570
+ object_lifecycle_preference: dict[str, str] | None = None,
451
571
  ) -> str:
572
+ if multipart is None:
573
+ threshold = multipart_threshold or MultipartUpload.MULTIPART_THRESHOLD
574
+ multipart = len(file.data) > threshold
575
+
576
+ if multipart:
577
+ return MultipartUpload.save(
578
+ file,
579
+ chunk_size=multipart_chunk_size,
580
+ max_concurrency=multipart_max_concurrency,
581
+ )
582
+
452
583
  token = fal_v2_token_manager.get_token()
453
584
  headers = {
454
585
  "Authorization": f"{token.token_type} {token.token}",
@@ -475,23 +606,6 @@ class FalFileRepositoryV2(FalFileRepositoryBase):
475
606
  f"Error initiating upload. Status {e.status}: {e.reason}"
476
607
  )
477
608
 
478
- def _save_multipart(
479
- self,
480
- file_path: str | Path,
481
- chunk_size: int | None = None,
482
- content_type: str | None = None,
483
- max_concurrency: int | None = None,
484
- ) -> str:
485
- multipart = MultipartUpload(
486
- file_path,
487
- chunk_size=chunk_size,
488
- content_type=content_type,
489
- max_concurrency=max_concurrency,
490
- )
491
- multipart.create()
492
- multipart.upload()
493
- return multipart.complete()
494
-
495
609
  def save_file(
496
610
  self,
497
611
  file_path: str | Path,
@@ -507,7 +621,7 @@ class FalFileRepositoryV2(FalFileRepositoryBase):
507
621
  multipart = os.path.getsize(file_path) > threshold
508
622
 
509
623
  if multipart:
510
- url = self._save_multipart(
624
+ url = MultipartUpload.save_file(
511
625
  file_path,
512
626
  chunk_size=multipart_chunk_size,
513
627
  content_type=content_type,
@@ -608,8 +722,27 @@ class InternalFalFileRepositoryV3(FileRepository):
608
722
 
609
723
  @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
610
724
  def save(
611
- self, file: FileData, object_lifecycle_preference: dict[str, str] | None
725
+ self,
726
+ file: FileData,
727
+ multipart: bool | None = None,
728
+ multipart_threshold: int | None = None,
729
+ multipart_chunk_size: int | None = None,
730
+ multipart_max_concurrency: int | None = None,
731
+ object_lifecycle_preference: dict[str, str] | None = None,
612
732
  ) -> str:
733
+ if multipart is None:
734
+ threshold = (
735
+ multipart_threshold or InternalMultipartUploadV3.MULTIPART_THRESHOLD
736
+ )
737
+ multipart = len(file.data) > threshold
738
+
739
+ if multipart:
740
+ return InternalMultipartUploadV3.save(
741
+ file,
742
+ chunk_size=multipart_chunk_size,
743
+ max_concurrency=multipart_max_concurrency,
744
+ )
745
+
613
746
  headers = {
614
747
  **self.auth_headers,
615
748
  "Accept": "application/json",
@@ -640,23 +773,6 @@ class InternalFalFileRepositoryV3(FileRepository):
640
773
  "User-Agent": "fal/0.1.0",
641
774
  }
642
775
 
643
- def _save_multipart(
644
- self,
645
- file_path: str | Path,
646
- chunk_size: int | None = None,
647
- content_type: str | None = None,
648
- max_concurrency: int | None = None,
649
- ) -> str:
650
- multipart = InternalMultipartUploadV3(
651
- file_path,
652
- chunk_size=chunk_size,
653
- content_type=content_type,
654
- max_concurrency=max_concurrency,
655
- )
656
- multipart.create()
657
- multipart.upload()
658
- return multipart.complete()
659
-
660
776
  def save_file(
661
777
  self,
662
778
  file_path: str | Path,
@@ -672,7 +788,7 @@ class InternalFalFileRepositoryV3(FileRepository):
672
788
  multipart = os.path.getsize(file_path) > threshold
673
789
 
674
790
  if multipart:
675
- url = self._save_multipart(
791
+ url = MultipartUpload.save_file(
676
792
  file_path,
677
793
  chunk_size=multipart_chunk_size,
678
794
  content_type=content_type,
@@ -389,7 +389,11 @@ def clone_repository(
389
389
  A Path object representing the full path to the cloned Git repository.
390
390
  """
391
391
  target_dir = target_dir or FAL_REPOSITORY_DIR # type: ignore[assignment]
392
- repo_name = repo_name or Path(https_url).stem
392
+
393
+ if repo_name is None:
394
+ repo_name = Path(https_url).stem
395
+ if commit_hash:
396
+ repo_name += f"-{commit_hash[:8]}"
393
397
 
394
398
  local_repo_path = Path(target_dir) / repo_name # type: ignore[arg-type]
395
399
 
@@ -355,6 +355,12 @@ def test_clone_repository(isolated_client, mock_fal_persistent_dirs):
355
355
  EXAMPLE_REPO_FIRST_COMMIT = "64b0a89c8391bd2cb3ca23cdeae01779e11aee05"
356
356
  EXAMPLE_REPO_SECOND_COMMIT = "34ecbca8cc7b64719d2a5c40dd3272f8d13bc1d2"
357
357
  expected_path = FAL_REPOSITORY_DIR / "isolate"
358
+ first_expected_path = (
359
+ FAL_REPOSITORY_DIR / f"isolate-{EXAMPLE_REPO_FIRST_COMMIT[:8]}"
360
+ )
361
+ second_expected_path = (
362
+ FAL_REPOSITORY_DIR / f"isolate-{EXAMPLE_REPO_SECOND_COMMIT[:8]}"
363
+ )
358
364
 
359
365
  @isolated_client()
360
366
  def clone_without_commit_hash():
@@ -376,7 +382,7 @@ def test_clone_repository(isolated_client, mock_fal_persistent_dirs):
376
382
  EXAMPLE_REPO_URL, commit_hash=EXAMPLE_REPO_SECOND_COMMIT
377
383
  )
378
384
 
379
- second_repo_hash = _get_git_revision_hash(repo_path)
385
+ second_repo_hash = _get_git_revision_hash(second_path)
380
386
 
381
387
  return first_path, first_repo_hash, second_path, second_repo_hash
382
388
 
@@ -387,8 +393,12 @@ def test_clone_repository(isolated_client, mock_fal_persistent_dirs):
387
393
  second_repo_hash,
388
394
  ) = clone_with_commit_hash()
389
395
 
390
- assert str(expected_path) == str(first_path), "Path should be the target location"
391
- assert str(expected_path) == str(second_path), "Path should be the target location"
396
+ assert str(first_expected_path) == str(
397
+ first_path
398
+ ), "Path should be the target location"
399
+ assert str(second_expected_path) == str(
400
+ second_path
401
+ ), "Path should be the target location"
392
402
 
393
403
  assert (
394
404
  first_repo_hash == EXAMPLE_REPO_FIRST_COMMIT
@@ -432,9 +442,15 @@ def test_clone_repository(isolated_client, mock_fal_persistent_dirs):
432
442
  third_repo_stat,
433
443
  ) = clone_with_force()
434
444
 
435
- assert str(expected_path) == str(first_path), "Path should be the target location"
436
- assert str(expected_path) == str(second_path), "Path should be the target location"
437
- assert str(expected_path) == str(third_path), "Path should be the target location"
445
+ assert str(first_expected_path) == str(
446
+ first_path
447
+ ), "Path should be the target location"
448
+ assert str(first_expected_path) == str(
449
+ second_path
450
+ ), "Path should be the target location"
451
+ assert str(first_expected_path) == str(
452
+ third_path
453
+ ), "Path should be the target location"
438
454
 
439
455
  assert (
440
456
  first_repo_stat.st_mtime == second_repo_stat.st_mtime
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes