fal 1.7.1__tar.gz → 1.7.3__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.1/fal.egg-info → fal-1.7.3}/PKG-INFO +1 -1
  2. {fal-1.7.1 → fal-1.7.3/fal.egg-info}/PKG-INFO +1 -1
  3. {fal-1.7.1 → fal-1.7.3}/src/fal/_fal_version.py +2 -2
  4. {fal-1.7.1 → fal-1.7.3}/src/fal/api.py +1 -0
  5. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/file/providers/fal.py +252 -133
  6. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/utils/retry.py +3 -0
  7. {fal-1.7.1 → fal-1.7.3}/.gitignore +0 -0
  8. {fal-1.7.1 → fal-1.7.3}/Makefile +0 -0
  9. {fal-1.7.1 → fal-1.7.3}/README.md +0 -0
  10. {fal-1.7.1 → fal-1.7.3}/docs/conf.py +0 -0
  11. {fal-1.7.1 → fal-1.7.3}/docs/index.rst +0 -0
  12. {fal-1.7.1 → fal-1.7.3}/fal.egg-info/SOURCES.txt +0 -0
  13. {fal-1.7.1 → fal-1.7.3}/fal.egg-info/dependency_links.txt +0 -0
  14. {fal-1.7.1 → fal-1.7.3}/fal.egg-info/entry_points.txt +0 -0
  15. {fal-1.7.1 → fal-1.7.3}/fal.egg-info/requires.txt +0 -0
  16. {fal-1.7.1 → fal-1.7.3}/fal.egg-info/top_level.txt +0 -0
  17. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/README.md +0 -0
  18. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  19. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  20. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  21. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  22. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  23. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  24. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  25. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  26. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  27. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  28. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  29. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  30. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  31. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  32. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  33. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  34. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  35. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  36. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  37. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  38. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  39. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  40. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  41. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  42. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  43. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  44. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  45. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  46. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  47. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  48. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  49. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  50. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  51. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
  52. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  53. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  54. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  55. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  56. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  57. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  58. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  59. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  60. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  61. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  62. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  63. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  64. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  65. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  66. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  67. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  68. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  69. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  70. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  71. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  72. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  73. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  74. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  75. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  76. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  77. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  78. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  79. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  80. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  81. {fal-1.7.1 → fal-1.7.3}/openapi-fal-rest/pyproject.toml +0 -0
  82. {fal-1.7.1 → fal-1.7.3}/openapi_rest.config.yaml +0 -0
  83. {fal-1.7.1 → fal-1.7.3}/pyproject.toml +0 -0
  84. {fal-1.7.1 → fal-1.7.3}/setup.cfg +0 -0
  85. {fal-1.7.1 → fal-1.7.3}/src/fal/__init__.py +0 -0
  86. {fal-1.7.1 → fal-1.7.3}/src/fal/__main__.py +0 -0
  87. {fal-1.7.1 → fal-1.7.3}/src/fal/_serialization.py +0 -0
  88. {fal-1.7.1 → fal-1.7.3}/src/fal/_version.py +0 -0
  89. {fal-1.7.1 → fal-1.7.3}/src/fal/app.py +0 -0
  90. {fal-1.7.1 → fal-1.7.3}/src/fal/apps.py +0 -0
  91. {fal-1.7.1 → fal-1.7.3}/src/fal/auth/__init__.py +0 -0
  92. {fal-1.7.1 → fal-1.7.3}/src/fal/auth/auth0.py +0 -0
  93. {fal-1.7.1 → fal-1.7.3}/src/fal/auth/local.py +0 -0
  94. {fal-1.7.1 → fal-1.7.3}/src/fal/cli/__init__.py +0 -0
  95. {fal-1.7.1 → fal-1.7.3}/src/fal/cli/_utils.py +0 -0
  96. {fal-1.7.1 → fal-1.7.3}/src/fal/cli/apps.py +0 -0
  97. {fal-1.7.1 → fal-1.7.3}/src/fal/cli/auth.py +0 -0
  98. {fal-1.7.1 → fal-1.7.3}/src/fal/cli/create.py +0 -0
  99. {fal-1.7.1 → fal-1.7.3}/src/fal/cli/debug.py +0 -0
  100. {fal-1.7.1 → fal-1.7.3}/src/fal/cli/deploy.py +0 -0
  101. {fal-1.7.1 → fal-1.7.3}/src/fal/cli/doctor.py +0 -0
  102. {fal-1.7.1 → fal-1.7.3}/src/fal/cli/keys.py +0 -0
  103. {fal-1.7.1 → fal-1.7.3}/src/fal/cli/main.py +0 -0
  104. {fal-1.7.1 → fal-1.7.3}/src/fal/cli/parser.py +0 -0
  105. {fal-1.7.1 → fal-1.7.3}/src/fal/cli/run.py +0 -0
  106. {fal-1.7.1 → fal-1.7.3}/src/fal/cli/runners.py +0 -0
  107. {fal-1.7.1 → fal-1.7.3}/src/fal/cli/secrets.py +0 -0
  108. {fal-1.7.1 → fal-1.7.3}/src/fal/config.py +0 -0
  109. {fal-1.7.1 → fal-1.7.3}/src/fal/console/__init__.py +0 -0
  110. {fal-1.7.1 → fal-1.7.3}/src/fal/console/icons.py +0 -0
  111. {fal-1.7.1 → fal-1.7.3}/src/fal/console/ux.py +0 -0
  112. {fal-1.7.1 → fal-1.7.3}/src/fal/container.py +0 -0
  113. {fal-1.7.1 → fal-1.7.3}/src/fal/exceptions/__init__.py +0 -0
  114. {fal-1.7.1 → fal-1.7.3}/src/fal/exceptions/_base.py +0 -0
  115. {fal-1.7.1 → fal-1.7.3}/src/fal/exceptions/_cuda.py +0 -0
  116. {fal-1.7.1 → fal-1.7.3}/src/fal/exceptions/auth.py +0 -0
  117. {fal-1.7.1 → fal-1.7.3}/src/fal/files.py +0 -0
  118. {fal-1.7.1 → fal-1.7.3}/src/fal/flags.py +0 -0
  119. {fal-1.7.1 → fal-1.7.3}/src/fal/logging/__init__.py +0 -0
  120. {fal-1.7.1 → fal-1.7.3}/src/fal/logging/isolate.py +0 -0
  121. {fal-1.7.1 → fal-1.7.3}/src/fal/logging/style.py +0 -0
  122. {fal-1.7.1 → fal-1.7.3}/src/fal/logging/trace.py +0 -0
  123. {fal-1.7.1 → fal-1.7.3}/src/fal/logging/user.py +0 -0
  124. {fal-1.7.1 → fal-1.7.3}/src/fal/py.typed +0 -0
  125. {fal-1.7.1 → fal-1.7.3}/src/fal/rest_client.py +0 -0
  126. {fal-1.7.1 → fal-1.7.3}/src/fal/sdk.py +0 -0
  127. {fal-1.7.1 → fal-1.7.3}/src/fal/sync.py +0 -0
  128. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/__init__.py +0 -0
  129. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/exceptions.py +0 -0
  130. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/file/__init__.py +0 -0
  131. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/file/file.py +0 -0
  132. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/file/providers/gcp.py +0 -0
  133. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/file/providers/r2.py +0 -0
  134. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/file/providers/s3.py +0 -0
  135. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/file/types.py +0 -0
  136. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/image/__init__.py +0 -0
  137. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/image/image.py +0 -0
  138. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
  139. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
  140. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
  141. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
  142. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
  143. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/image/safety_checker.py +0 -0
  144. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/optimize.py +0 -0
  145. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/types.py +0 -0
  146. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/utils/__init__.py +0 -0
  147. {fal-1.7.1 → fal-1.7.3}/src/fal/toolkit/utils/download_utils.py +0 -0
  148. {fal-1.7.1 → fal-1.7.3}/src/fal/utils.py +0 -0
  149. {fal-1.7.1 → fal-1.7.3}/src/fal/workflows.py +0 -0
  150. {fal-1.7.1 → fal-1.7.3}/tests/__init__.py +0 -0
  151. {fal-1.7.1 → fal-1.7.3}/tests/assets/cat.png +0 -0
  152. {fal-1.7.1 → fal-1.7.3}/tests/cli/__init__.py +0 -0
  153. {fal-1.7.1 → fal-1.7.3}/tests/cli/test_apps.py +0 -0
  154. {fal-1.7.1 → fal-1.7.3}/tests/cli/test_auth.py +0 -0
  155. {fal-1.7.1 → fal-1.7.3}/tests/cli/test_deploy.py +0 -0
  156. {fal-1.7.1 → fal-1.7.3}/tests/cli/test_keys.py +0 -0
  157. {fal-1.7.1 → fal-1.7.3}/tests/cli/test_run.py +0 -0
  158. {fal-1.7.1 → fal-1.7.3}/tests/cli/test_secrets.py +0 -0
  159. {fal-1.7.1 → fal-1.7.3}/tests/conftest.py +0 -0
  160. {fal-1.7.1 → fal-1.7.3}/tests/integration_test.py +0 -0
  161. {fal-1.7.1 → fal-1.7.3}/tests/mainify_package/__init__.py +0 -0
  162. {fal-1.7.1 → fal-1.7.3}/tests/mainify_package/impl.py +0 -0
  163. {fal-1.7.1 → fal-1.7.3}/tests/mainify_package/utils.py +0 -0
  164. {fal-1.7.1 → fal-1.7.3}/tests/mainify_target.py +0 -0
  165. {fal-1.7.1 → fal-1.7.3}/tests/test_apps.py +0 -0
  166. {fal-1.7.1 → fal-1.7.3}/tests/test_stability.py +0 -0
  167. {fal-1.7.1 → fal-1.7.3}/tests/toolkit/file_test.py +0 -0
  168. {fal-1.7.1 → fal-1.7.3}/tests/toolkit/image_test.py +0 -0
  169. {fal-1.7.1 → fal-1.7.3}/tests/toolkit/test_types.py +0 -0
  170. {fal-1.7.1 → fal-1.7.3}/tests/toolkit/utils/retry.py +0 -0
  171. {fal-1.7.1 → fal-1.7.3}/tools/demo_script.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: fal
3
- Version: 1.7.1
3
+ Version: 1.7.3
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.1
3
+ Version: 1.7.3
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.1'
16
- __version_tuple__ = version_tuple = (1, 7, 1)
15
+ __version__ = version = '1.7.3'
16
+ __version_tuple__ = version_tuple = (1, 7, 3)
@@ -77,6 +77,7 @@ SERVE_REQUIREMENTS = [
77
77
  "uvicorn",
78
78
  "starlette_exporter",
79
79
  "structlog",
80
+ "tomli",
80
81
  ]
81
82
 
82
83
 
@@ -159,24 +159,27 @@ class FalFileRepositoryBase(FileRepository):
159
159
  result = json.load(response)
160
160
 
161
161
  upload_url = result["upload_url"]
162
- self._upload_file(upload_url, file)
163
-
164
- return result["file_url"]
165
162
  except HTTPError as e:
166
163
  raise FileUploadException(
167
164
  f"Error initiating upload. Status {e.status}: {e.reason}"
168
165
  )
169
166
 
170
- def _upload_file(self, upload_url: str, file: FileData):
171
- req = Request(
172
- upload_url,
173
- method="PUT",
174
- data=file.data,
175
- headers={"Content-Type": file.content_type},
176
- )
167
+ try:
168
+ req = Request(
169
+ upload_url,
170
+ method="PUT",
171
+ data=file.data,
172
+ headers={"Content-Type": file.content_type},
173
+ )
177
174
 
178
- with urlopen(req):
179
- return
175
+ with urlopen(req):
176
+ pass
177
+
178
+ return result["file_url"]
179
+ except HTTPError as e:
180
+ raise FileUploadException(
181
+ f"Error uploading file. Status {e.status}: {e.reason}"
182
+ )
180
183
 
181
184
 
182
185
  @dataclass
@@ -202,12 +205,12 @@ class MultipartUpload:
202
205
 
203
206
  def __init__(
204
207
  self,
205
- file_path: str | Path,
208
+ file_name: str,
206
209
  chunk_size: int | None = None,
207
210
  content_type: str | None = None,
208
211
  max_concurrency: int | None = None,
209
212
  ) -> None:
210
- self.file_path = file_path
213
+ self.file_name = file_name
211
214
  self.chunk_size = chunk_size or self.MULTIPART_CHUNK_SIZE
212
215
  self.content_type = content_type or "application/octet-stream"
213
216
  self.max_concurrency = max_concurrency or self.MULTIPART_MAX_CONCURRENCY
@@ -227,7 +230,7 @@ class MultipartUpload:
227
230
  },
228
231
  data=json.dumps(
229
232
  {
230
- "file_name": os.path.basename(self.file_path),
233
+ "file_name": self.file_name,
231
234
  "content_type": self.content_type,
232
235
  }
233
236
  ).encode(),
@@ -241,47 +244,29 @@ class MultipartUpload:
241
244
  f"Error initiating upload. Status {exc.status}: {exc.reason}"
242
245
  )
243
246
 
244
- def _upload_part(self, url: str, part_number: int) -> dict:
245
- with open(self.file_path, "rb") as f:
246
- start = (part_number - 1) * self.chunk_size
247
- f.seek(start)
248
- data = f.read(self.chunk_size)
249
- req = Request(
250
- url,
251
- method="PUT",
252
- headers={"Content-Type": self.content_type},
253
- data=data,
254
- )
247
+ def upload_part(self, part_number: int, data: bytes) -> None:
248
+ url = f"{self._upload_url}&part_number={part_number}"
255
249
 
256
- try:
257
- with urlopen(req) as resp:
258
- return {
250
+ req = Request(
251
+ url,
252
+ method="PUT",
253
+ headers={"Content-Type": self.content_type},
254
+ data=data,
255
+ )
256
+
257
+ try:
258
+ with urlopen(req) as resp:
259
+ self._parts.append(
260
+ {
259
261
  "part_number": part_number,
260
262
  "etag": resp.headers["ETag"],
261
263
  }
262
- except HTTPError as exc:
263
- raise FileUploadException(
264
- f"Error uploading part {part_number} to {url}. "
265
- f"Status {exc.status}: {exc.reason}"
266
264
  )
267
-
268
- def upload(self) -> None:
269
- import concurrent.futures
270
-
271
- parts = math.ceil(os.path.getsize(self.file_path) / self.chunk_size)
272
- with concurrent.futures.ThreadPoolExecutor(
273
- max_workers=self.max_concurrency
274
- ) as executor:
275
- futures = []
276
- for part_number in range(1, parts + 1):
277
- upload_url = f"{self._upload_url}&part_number={part_number}"
278
- futures.append(
279
- executor.submit(self._upload_part, upload_url, part_number)
280
- )
281
-
282
- for future in concurrent.futures.as_completed(futures):
283
- entry = future.result()
284
- 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
+ )
285
270
 
286
271
  def complete(self):
287
272
  url = self._upload_url
@@ -304,6 +289,82 @@ class MultipartUpload:
304
289
 
305
290
  return self._file_url
306
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
+
307
368
 
308
369
  class InternalMultipartUploadV3:
309
370
  MULTIPART_THRESHOLD = 100 * 1024 * 1024
@@ -312,12 +373,12 @@ class InternalMultipartUploadV3:
312
373
 
313
374
  def __init__(
314
375
  self,
315
- file_path: str | Path,
376
+ file_name: str,
316
377
  chunk_size: int | None = None,
317
378
  content_type: str | None = None,
318
379
  max_concurrency: int | None = None,
319
380
  ) -> None:
320
- self.file_path = file_path
381
+ self.file_name = file_name
321
382
  self.chunk_size = chunk_size or self.MULTIPART_CHUNK_SIZE
322
383
  self.content_type = content_type or "application/octet-stream"
323
384
  self.max_concurrency = max_concurrency or self.MULTIPART_MAX_CONCURRENCY
@@ -356,7 +417,7 @@ class InternalMultipartUploadV3:
356
417
  **self.auth_headers,
357
418
  "Accept": "application/json",
358
419
  "Content-Type": self.content_type,
359
- "X-Fal-File-Name": os.path.basename(self.file_path),
420
+ "X-Fal-File-Name": self.file_name,
360
421
  },
361
422
  )
362
423
  with urlopen(req) as response:
@@ -370,52 +431,32 @@ class InternalMultipartUploadV3:
370
431
  )
371
432
 
372
433
  @retry(max_retries=5, base_delay=1, backoff_type="exponential", jitter=True)
373
- def _upload_part(self, url: str, part_number: int) -> dict:
374
- with open(self.file_path, "rb") as f:
375
- start = (part_number - 1) * self.chunk_size
376
- f.seek(start)
377
- data = f.read(self.chunk_size)
378
- req = Request(
379
- url,
380
- method="PUT",
381
- headers={
382
- **self.auth_headers,
383
- "Content-Type": self.content_type,
384
- },
385
- data=data,
386
- )
434
+ def upload_part(self, part_number: int, data: bytes) -> None:
435
+ url = f"{self.access_url}/multipart/{self.upload_id}/{part_number}"
387
436
 
388
- try:
389
- with urlopen(req) as resp:
390
- 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
+ {
391
451
  "partNumber": part_number,
392
452
  "etag": resp.headers["ETag"],
393
453
  }
394
- except HTTPError as exc:
395
- raise FileUploadException(
396
- f"Error uploading part {part_number} to {url}. "
397
- f"Status {exc.status}: {exc.reason}"
398
454
  )
399
-
400
- def upload(self) -> None:
401
- import concurrent.futures
402
-
403
- parts = math.ceil(os.path.getsize(self.file_path) / self.chunk_size)
404
- with concurrent.futures.ThreadPoolExecutor(
405
- max_workers=self.max_concurrency
406
- ) as executor:
407
- futures = []
408
- for part_number in range(1, parts + 1):
409
- upload_url = (
410
- f"{self.access_url}/multipart/{self.upload_id}/{part_number}"
411
- )
412
- futures.append(
413
- executor.submit(self._upload_part, upload_url, part_number)
414
- )
415
-
416
- for future in concurrent.futures.as_completed(futures):
417
- entry = future.result()
418
- 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
+ )
419
460
 
420
461
  def complete(self) -> str:
421
462
  url = f"{self.access_url}/multipart/{self.upload_id}/complete"
@@ -439,13 +480,106 @@ class InternalMultipartUploadV3:
439
480
 
440
481
  return self.access_url
441
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
+
442
559
 
443
560
  @dataclass
444
561
  class FalFileRepositoryV2(FalFileRepositoryBase):
445
562
  @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
446
563
  def save(
447
- 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,
448
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
+
449
583
  token = fal_v2_token_manager.get_token()
450
584
  headers = {
451
585
  "Authorization": f"{token.token_type} {token.token}",
@@ -472,23 +606,6 @@ class FalFileRepositoryV2(FalFileRepositoryBase):
472
606
  f"Error initiating upload. Status {e.status}: {e.reason}"
473
607
  )
474
608
 
475
- def _save_multipart(
476
- self,
477
- file_path: str | Path,
478
- chunk_size: int | None = None,
479
- content_type: str | None = None,
480
- max_concurrency: int | None = None,
481
- ) -> str:
482
- multipart = MultipartUpload(
483
- file_path,
484
- chunk_size=chunk_size,
485
- content_type=content_type,
486
- max_concurrency=max_concurrency,
487
- )
488
- multipart.create()
489
- multipart.upload()
490
- return multipart.complete()
491
-
492
609
  def save_file(
493
610
  self,
494
611
  file_path: str | Path,
@@ -504,7 +621,7 @@ class FalFileRepositoryV2(FalFileRepositoryBase):
504
621
  multipart = os.path.getsize(file_path) > threshold
505
622
 
506
623
  if multipart:
507
- url = self._save_multipart(
624
+ url = MultipartUpload.save_file(
508
625
  file_path,
509
626
  chunk_size=multipart_chunk_size,
510
627
  content_type=content_type,
@@ -605,8 +722,27 @@ class InternalFalFileRepositoryV3(FileRepository):
605
722
 
606
723
  @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
607
724
  def save(
608
- 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,
609
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
+
610
746
  headers = {
611
747
  **self.auth_headers,
612
748
  "Accept": "application/json",
@@ -637,23 +773,6 @@ class InternalFalFileRepositoryV3(FileRepository):
637
773
  "User-Agent": "fal/0.1.0",
638
774
  }
639
775
 
640
- def _save_multipart(
641
- self,
642
- file_path: str | Path,
643
- chunk_size: int | None = None,
644
- content_type: str | None = None,
645
- max_concurrency: int | None = None,
646
- ) -> str:
647
- multipart = InternalMultipartUploadV3(
648
- file_path,
649
- chunk_size=chunk_size,
650
- content_type=content_type,
651
- max_concurrency=max_concurrency,
652
- )
653
- multipart.create()
654
- multipart.upload()
655
- return multipart.complete()
656
-
657
776
  def save_file(
658
777
  self,
659
778
  file_path: str | Path,
@@ -669,7 +788,7 @@ class InternalFalFileRepositoryV3(FileRepository):
669
788
  multipart = os.path.getsize(file_path) > threshold
670
789
 
671
790
  if multipart:
672
- url = self._save_multipart(
791
+ url = MultipartUpload.save_file(
673
792
  file_path,
674
793
  chunk_size=multipart_chunk_size,
675
794
  content_type=content_type,
@@ -1,6 +1,7 @@
1
1
  import functools
2
2
  import random
3
3
  import time
4
+ import traceback
4
5
  from typing import Any, Callable, Literal
5
6
 
6
7
  BackoffType = Literal["exponential", "fixed"]
@@ -25,6 +26,8 @@ def retry(
25
26
  print(f"Retrying {retries} of {max_retries}...")
26
27
  if retries == max_retries:
27
28
  print(f"Max retries reached. Raising exception: {e}")
29
+ traceback.print_exc()
30
+
28
31
  raise e
29
32
 
30
33
  if backoff_type == "exponential":
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes