fal 1.5.0__tar.gz → 1.5.2__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 (163) hide show
  1. {fal-1.5.0 → fal-1.5.2}/PKG-INFO +1 -1
  2. {fal-1.5.0 → fal-1.5.2}/fal.egg-info/PKG-INFO +1 -1
  3. {fal-1.5.0 → fal-1.5.2}/fal.egg-info/SOURCES.txt +1 -0
  4. {fal-1.5.0 → fal-1.5.2}/src/fal/_fal_version.py +2 -2
  5. {fal-1.5.0 → fal-1.5.2}/src/fal/api.py +7 -0
  6. {fal-1.5.0 → fal-1.5.2}/src/fal/app.py +4 -2
  7. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/file/file.py +33 -2
  8. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/file/providers/fal.py +60 -5
  9. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/file/types.py +11 -4
  10. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/image/image.py +5 -0
  11. fal-1.5.2/tests/assets/cat.png +0 -0
  12. {fal-1.5.0 → fal-1.5.2}/tests/integration_test.py +6 -7
  13. {fal-1.5.0 → fal-1.5.2}/tests/test_stability.py +1 -0
  14. {fal-1.5.0 → fal-1.5.2}/.gitignore +0 -0
  15. {fal-1.5.0 → fal-1.5.2}/README.md +0 -0
  16. {fal-1.5.0 → fal-1.5.2}/fal.egg-info/dependency_links.txt +0 -0
  17. {fal-1.5.0 → fal-1.5.2}/fal.egg-info/entry_points.txt +0 -0
  18. {fal-1.5.0 → fal-1.5.2}/fal.egg-info/requires.txt +0 -0
  19. {fal-1.5.0 → fal-1.5.2}/fal.egg-info/top_level.txt +0 -0
  20. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/README.md +0 -0
  21. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  22. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  23. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  24. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  25. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  26. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  27. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  28. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  29. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  30. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  31. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  32. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  33. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  34. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  35. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  36. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  37. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  38. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  39. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  40. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  41. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  42. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  43. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  44. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  45. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  46. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  47. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  48. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  49. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  50. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  51. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  52. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  53. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  54. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
  55. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  56. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  57. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  58. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  59. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  60. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  61. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  62. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  63. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  64. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  65. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  66. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  67. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  68. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  69. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  70. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  71. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  72. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  73. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  74. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  75. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  76. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  77. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  78. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  79. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  80. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  81. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  82. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  83. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  84. {fal-1.5.0 → fal-1.5.2}/openapi-fal-rest/pyproject.toml +0 -0
  85. {fal-1.5.0 → fal-1.5.2}/openapi_rest.config.yaml +0 -0
  86. {fal-1.5.0 → fal-1.5.2}/pyproject.toml +0 -0
  87. {fal-1.5.0 → fal-1.5.2}/setup.cfg +0 -0
  88. {fal-1.5.0 → fal-1.5.2}/src/fal/__init__.py +0 -0
  89. {fal-1.5.0 → fal-1.5.2}/src/fal/__main__.py +0 -0
  90. {fal-1.5.0 → fal-1.5.2}/src/fal/_serialization.py +0 -0
  91. {fal-1.5.0 → fal-1.5.2}/src/fal/_version.py +0 -0
  92. {fal-1.5.0 → fal-1.5.2}/src/fal/apps.py +0 -0
  93. {fal-1.5.0 → fal-1.5.2}/src/fal/auth/__init__.py +0 -0
  94. {fal-1.5.0 → fal-1.5.2}/src/fal/auth/auth0.py +0 -0
  95. {fal-1.5.0 → fal-1.5.2}/src/fal/auth/local.py +0 -0
  96. {fal-1.5.0 → fal-1.5.2}/src/fal/cli/__init__.py +0 -0
  97. {fal-1.5.0 → fal-1.5.2}/src/fal/cli/_utils.py +0 -0
  98. {fal-1.5.0 → fal-1.5.2}/src/fal/cli/apps.py +0 -0
  99. {fal-1.5.0 → fal-1.5.2}/src/fal/cli/auth.py +0 -0
  100. {fal-1.5.0 → fal-1.5.2}/src/fal/cli/create.py +0 -0
  101. {fal-1.5.0 → fal-1.5.2}/src/fal/cli/debug.py +0 -0
  102. {fal-1.5.0 → fal-1.5.2}/src/fal/cli/deploy.py +0 -0
  103. {fal-1.5.0 → fal-1.5.2}/src/fal/cli/doctor.py +0 -0
  104. {fal-1.5.0 → fal-1.5.2}/src/fal/cli/keys.py +0 -0
  105. {fal-1.5.0 → fal-1.5.2}/src/fal/cli/main.py +0 -0
  106. {fal-1.5.0 → fal-1.5.2}/src/fal/cli/parser.py +0 -0
  107. {fal-1.5.0 → fal-1.5.2}/src/fal/cli/run.py +0 -0
  108. {fal-1.5.0 → fal-1.5.2}/src/fal/cli/secrets.py +0 -0
  109. {fal-1.5.0 → fal-1.5.2}/src/fal/console/__init__.py +0 -0
  110. {fal-1.5.0 → fal-1.5.2}/src/fal/console/icons.py +0 -0
  111. {fal-1.5.0 → fal-1.5.2}/src/fal/console/ux.py +0 -0
  112. {fal-1.5.0 → fal-1.5.2}/src/fal/container.py +0 -0
  113. {fal-1.5.0 → fal-1.5.2}/src/fal/exceptions/__init__.py +0 -0
  114. {fal-1.5.0 → fal-1.5.2}/src/fal/exceptions/_base.py +0 -0
  115. {fal-1.5.0 → fal-1.5.2}/src/fal/exceptions/_cuda.py +0 -0
  116. {fal-1.5.0 → fal-1.5.2}/src/fal/exceptions/auth.py +0 -0
  117. {fal-1.5.0 → fal-1.5.2}/src/fal/files.py +0 -0
  118. {fal-1.5.0 → fal-1.5.2}/src/fal/flags.py +0 -0
  119. {fal-1.5.0 → fal-1.5.2}/src/fal/logging/__init__.py +0 -0
  120. {fal-1.5.0 → fal-1.5.2}/src/fal/logging/isolate.py +0 -0
  121. {fal-1.5.0 → fal-1.5.2}/src/fal/logging/style.py +0 -0
  122. {fal-1.5.0 → fal-1.5.2}/src/fal/logging/trace.py +0 -0
  123. {fal-1.5.0 → fal-1.5.2}/src/fal/logging/user.py +0 -0
  124. {fal-1.5.0 → fal-1.5.2}/src/fal/py.typed +0 -0
  125. {fal-1.5.0 → fal-1.5.2}/src/fal/rest_client.py +0 -0
  126. {fal-1.5.0 → fal-1.5.2}/src/fal/sdk.py +0 -0
  127. {fal-1.5.0 → fal-1.5.2}/src/fal/sync.py +0 -0
  128. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/__init__.py +0 -0
  129. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/exceptions.py +0 -0
  130. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/file/__init__.py +0 -0
  131. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/file/providers/gcp.py +0 -0
  132. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/file/providers/r2.py +0 -0
  133. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/image/__init__.py +0 -0
  134. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
  135. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
  136. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
  137. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
  138. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
  139. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/image/safety_checker.py +0 -0
  140. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/optimize.py +0 -0
  141. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/utils/__init__.py +0 -0
  142. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/utils/download_utils.py +0 -0
  143. {fal-1.5.0 → fal-1.5.2}/src/fal/toolkit/utils/retry.py +0 -0
  144. {fal-1.5.0 → fal-1.5.2}/src/fal/utils.py +0 -0
  145. {fal-1.5.0 → fal-1.5.2}/src/fal/workflows.py +0 -0
  146. {fal-1.5.0 → fal-1.5.2}/tests/__init__.py +0 -0
  147. {fal-1.5.0 → fal-1.5.2}/tests/cli/__init__.py +0 -0
  148. {fal-1.5.0 → fal-1.5.2}/tests/cli/test_apps.py +0 -0
  149. {fal-1.5.0 → fal-1.5.2}/tests/cli/test_auth.py +0 -0
  150. {fal-1.5.0 → fal-1.5.2}/tests/cli/test_deploy.py +0 -0
  151. {fal-1.5.0 → fal-1.5.2}/tests/cli/test_keys.py +0 -0
  152. {fal-1.5.0 → fal-1.5.2}/tests/cli/test_run.py +0 -0
  153. {fal-1.5.0 → fal-1.5.2}/tests/cli/test_secrets.py +0 -0
  154. {fal-1.5.0 → fal-1.5.2}/tests/conftest.py +0 -0
  155. {fal-1.5.0 → fal-1.5.2}/tests/mainify_package/__init__.py +0 -0
  156. {fal-1.5.0 → fal-1.5.2}/tests/mainify_package/impl.py +0 -0
  157. {fal-1.5.0 → fal-1.5.2}/tests/mainify_package/utils.py +0 -0
  158. {fal-1.5.0 → fal-1.5.2}/tests/mainify_target.py +0 -0
  159. {fal-1.5.0 → fal-1.5.2}/tests/test_apps.py +0 -0
  160. {fal-1.5.0 → fal-1.5.2}/tests/toolkit/file_test.py +0 -0
  161. {fal-1.5.0 → fal-1.5.2}/tests/toolkit/image_test.py +0 -0
  162. {fal-1.5.0 → fal-1.5.2}/tests/toolkit/utils/retry.py +0 -0
  163. {fal-1.5.0 → fal-1.5.2}/tools/demo_script.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.5.0
3
+ Version: 1.5.2
4
4
  Summary: fal is an easy-to-use Serverless Python Framework
5
5
  Author: Features & Labels <support@fal.ai>
6
6
  Requires-Python: >=3.8
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.5.0
3
+ Version: 1.5.2
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
@@ -144,6 +144,7 @@ tests/integration_test.py
144
144
  tests/mainify_target.py
145
145
  tests/test_apps.py
146
146
  tests/test_stability.py
147
+ tests/assets/cat.png
147
148
  tests/cli/__init__.py
148
149
  tests/cli/test_apps.py
149
150
  tests/cli/test_auth.py
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.5.0'
16
- __version_tuple__ = version_tuple = (1, 5, 0)
15
+ __version__ = version = '1.5.2'
16
+ __version_tuple__ = version_tuple = (1, 5, 2)
@@ -971,6 +971,8 @@ class RouteSignature(NamedTuple):
971
971
 
972
972
 
973
973
  class BaseServable:
974
+ version: ClassVar[str] = "unknown"
975
+
974
976
  def collect_routes(self) -> dict[RouteSignature, Callable[..., Any]]:
975
977
  raise NotImplementedError
976
978
 
@@ -1099,9 +1101,14 @@ class BaseServable:
1099
1101
  def serve(self) -> None:
1100
1102
  import asyncio
1101
1103
 
1104
+ from prometheus_client import Gauge
1102
1105
  from starlette_exporter import handle_metrics
1103
1106
  from uvicorn import Config
1104
1107
 
1108
+ # NOTE: this uses the global prometheus registry
1109
+ app_info = Gauge("fal_app_info", "Fal application information", ["version"])
1110
+ app_info.labels(version=self.version).set(1)
1111
+
1105
1112
  app = self._build_app()
1106
1113
  server = Server(
1107
1114
  config=Config(app, host="0.0.0.0", port=8080, timeout_keep_alive=300)
@@ -189,7 +189,6 @@ class App(fal.api.BaseServable):
189
189
  }
190
190
  app_name: ClassVar[str]
191
191
  app_auth: ClassVar[Literal["private", "public", "shared"]] = "private"
192
- version: ClassVar[str] = "unknown"
193
192
  request_timeout: ClassVar[int | None] = None
194
193
 
195
194
  def __init_subclass__(cls, **kwargs):
@@ -403,7 +402,10 @@ def _fal_websocket_template(
403
402
  batch.append(next_input)
404
403
 
405
404
  t0 = loop.time()
406
- output = await loop.run_in_executor(None, func, self, *batch) # type: ignore
405
+ if inspect.iscoroutinefunction(func):
406
+ output = await func(self, *batch)
407
+ else:
408
+ output = await loop.run_in_executor(None, func, self, *batch) # type: ignore
407
409
  total_time = loop.time() - t0
408
410
  if not isinstance(output, dict):
409
411
  # Handle pydantic output modal
@@ -8,6 +8,7 @@ from urllib.parse import urlparse
8
8
  from zipfile import ZipFile
9
9
 
10
10
  import pydantic
11
+ from fastapi import Request
11
12
 
12
13
  # https://github.com/pydantic/pydantic/pull/2573
13
14
  if not hasattr(pydantic, "__version__") or pydantic.__version__.startswith("1."):
@@ -24,6 +25,7 @@ from fal.toolkit.file.providers.fal import (
24
25
  FalCDNFileRepository,
25
26
  FalFileRepository,
26
27
  FalFileRepositoryV2,
28
+ FalFileRepositoryV3,
27
29
  InMemoryRepository,
28
30
  )
29
31
  from fal.toolkit.file.providers.gcp import GoogleStorageRepository
@@ -36,6 +38,7 @@ FileRepositoryFactory = Callable[[], FileRepository]
36
38
  BUILT_IN_REPOSITORIES: dict[RepositoryId, FileRepositoryFactory] = {
37
39
  "fal": lambda: FalFileRepository(),
38
40
  "fal_v2": lambda: FalFileRepositoryV2(),
41
+ "fal_v3": lambda: FalFileRepositoryV3(),
39
42
  "in_memory": lambda: InMemoryRepository(),
40
43
  "gcp_storage": lambda: GoogleStorageRepository(),
41
44
  "r2": lambda: R2Repository(),
@@ -53,6 +56,7 @@ get_builtin_repository.__module__ = "__main__"
53
56
 
54
57
  DEFAULT_REPOSITORY: FileRepository | RepositoryId = "fal_v2"
55
58
  FALLBACK_REPOSITORY: FileRepository | RepositoryId = "cdn"
59
+ OBJECT_LIFECYCLE_PREFERENCE_KEY = "x-fal-object-lifecycle-preference"
56
60
 
57
61
 
58
62
  class File(BaseModel):
@@ -130,6 +134,7 @@ class File(BaseModel):
130
134
  fallback_repository: Optional[
131
135
  FileRepository | RepositoryId
132
136
  ] = FALLBACK_REPOSITORY,
137
+ request: Optional[Request] = None,
133
138
  ) -> File:
134
139
  repo = (
135
140
  repository
@@ -139,8 +144,10 @@ class File(BaseModel):
139
144
 
140
145
  fdata = FileData(data, content_type, file_name)
141
146
 
147
+ object_lifecycle_preference = _get_lifecycle_preference(request)
148
+
142
149
  try:
143
- url = repo.save(fdata)
150
+ url = repo.save(fdata, object_lifecycle_preference)
144
151
  except Exception:
145
152
  if not fallback_repository:
146
153
  raise
@@ -151,7 +158,7 @@ class File(BaseModel):
151
158
  else get_builtin_repository(fallback_repository)
152
159
  )
153
160
 
154
- url = fallback_repo.save(fdata)
161
+ url = fallback_repo.save(fdata, object_lifecycle_preference)
155
162
 
156
163
  return cls(
157
164
  url=url,
@@ -171,6 +178,7 @@ class File(BaseModel):
171
178
  fallback_repository: Optional[
172
179
  FileRepository | RepositoryId
173
180
  ] = FALLBACK_REPOSITORY,
181
+ request: Optional[Request] = None,
174
182
  ) -> File:
175
183
  file_path = Path(path)
176
184
  if not file_path.exists():
@@ -183,12 +191,14 @@ class File(BaseModel):
183
191
  )
184
192
 
185
193
  content_type = content_type or "application/octet-stream"
194
+ object_lifecycle_preference = _get_lifecycle_preference(request)
186
195
 
187
196
  try:
188
197
  url, data = repo.save_file(
189
198
  file_path,
190
199
  content_type=content_type,
191
200
  multipart=multipart,
201
+ object_lifecycle_preference=object_lifecycle_preference,
192
202
  )
193
203
  except Exception:
194
204
  if not fallback_repository:
@@ -204,6 +214,7 @@ class File(BaseModel):
204
214
  file_path,
205
215
  content_type=content_type,
206
216
  multipart=multipart,
217
+ object_lifecycle_preference=object_lifecycle_preference,
207
218
  )
208
219
 
209
220
  return cls(
@@ -261,3 +272,23 @@ class CompressedFile(File):
261
272
  def __del__(self):
262
273
  if self.extract_dir:
263
274
  shutil.rmtree(self.extract_dir)
275
+
276
+
277
+ def _get_lifecycle_preference(request: Request) -> dict[str, str] | None:
278
+ import json
279
+
280
+ preference_str = (
281
+ request.headers.get(OBJECT_LIFECYCLE_PREFERENCE_KEY)
282
+ if request is not None
283
+ else None
284
+ )
285
+ if preference_str is None:
286
+ return None
287
+
288
+ object_lifecycle_preference = {}
289
+ try:
290
+ object_lifecycle_preference = json.loads(preference_str)
291
+ return object_lifecycle_preference
292
+ except Exception as e:
293
+ print(f"Failed to parse object lifecycle preference: {e}")
294
+ return None
@@ -19,6 +19,7 @@ from fal.toolkit.file.types import FileData, FileRepository
19
19
  from fal.toolkit.utils.retry import retry
20
20
 
21
21
  _FAL_CDN = "https://fal.media"
22
+ _FAL_CDN_V3 = "https://v3.fal.media"
22
23
 
23
24
 
24
25
  @dataclass
@@ -91,11 +92,11 @@ fal_v2_token_manager = FalV2TokenManager()
91
92
 
92
93
  @dataclass
93
94
  class ObjectLifecyclePreference:
94
- expriation_duration_seconds: int
95
+ expiration_duration_seconds: int
95
96
 
96
97
 
97
98
  GLOBAL_LIFECYCLE_PREFERENCE = ObjectLifecyclePreference(
98
- expriation_duration_seconds=86400
99
+ expiration_duration_seconds=86400
99
100
  )
100
101
 
101
102
 
@@ -158,7 +159,9 @@ class FalFileRepositoryBase(FileRepository):
158
159
 
159
160
  @dataclass
160
161
  class FalFileRepository(FalFileRepositoryBase):
161
- def save(self, file: FileData) -> str:
162
+ def save(
163
+ self, file: FileData, object_lifecycle_preference: dict[str, str] | None = None
164
+ ) -> str:
162
165
  return self._save(file, "gcs")
163
166
 
164
167
 
@@ -275,7 +278,9 @@ class MultipartUpload:
275
278
  @dataclass
276
279
  class FalFileRepositoryV2(FalFileRepositoryBase):
277
280
  @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
278
- def save(self, file: FileData) -> str:
281
+ def save(
282
+ self, file: FileData, object_lifecycle_preference: dict[str, str] | None = None
283
+ ) -> str:
279
284
  token = fal_v2_token_manager.get_token()
280
285
  headers = {
281
286
  "Authorization": f"{token.token_type} {token.token}",
@@ -327,6 +332,7 @@ class FalFileRepositoryV2(FalFileRepositoryBase):
327
332
  multipart_threshold: int | None = None,
328
333
  multipart_chunk_size: int | None = None,
329
334
  multipart_max_concurrency: int | None = None,
335
+ object_lifecycle_preference: dict[str, str] | None = None,
330
336
  ) -> tuple[str, FileData | None]:
331
337
  if multipart is None:
332
338
  threshold = multipart_threshold or MultipartUpload.MULTIPART_THRESHOLD
@@ -347,7 +353,7 @@ class FalFileRepositoryV2(FalFileRepositoryBase):
347
353
  content_type=content_type,
348
354
  file_name=os.path.basename(file_path),
349
355
  )
350
- url = self.save(data)
356
+ url = self.save(data, object_lifecycle_preference)
351
357
 
352
358
  return url, data
353
359
 
@@ -357,6 +363,7 @@ class InMemoryRepository(FileRepository):
357
363
  def save(
358
364
  self,
359
365
  file: FileData,
366
+ object_lifecycle_preference: dict[str, str] | None = None,
360
367
  ) -> str:
361
368
  return f'data:{file.content_type};base64,{b64encode(file.data).decode("utf-8")}'
362
369
 
@@ -367,6 +374,7 @@ class FalCDNFileRepository(FileRepository):
367
374
  def save(
368
375
  self,
369
376
  file: FileData,
377
+ object_lifecycle_preference: dict[str, str] | None = None,
370
378
  ) -> str:
371
379
  headers = {
372
380
  **self.auth_headers,
@@ -401,3 +409,50 @@ class FalCDNFileRepository(FileRepository):
401
409
  "Authorization": f"Bearer {key_id}:{key_secret}",
402
410
  "User-Agent": "fal/0.1.0",
403
411
  }
412
+
413
+
414
+ @dataclass
415
+ class FalFileRepositoryV3(FileRepository):
416
+ @retry(max_retries=3, base_delay=1, backoff_type="exponential", jitter=True)
417
+ def save(
418
+ self, file: FileData, user_lifecycle_preference: dict[str, str] | None
419
+ ) -> str:
420
+ object_lifecycle_preference = dataclasses.asdict(GLOBAL_LIFECYCLE_PREFERENCE)
421
+
422
+ if user_lifecycle_preference is not None:
423
+ object_lifecycle_preference = {
424
+ key: user_lifecycle_preference[key]
425
+ if key in user_lifecycle_preference
426
+ else value
427
+ for key, value in object_lifecycle_preference.items()
428
+ }
429
+
430
+ headers = {
431
+ **self.auth_headers,
432
+ "Accept": "application/json",
433
+ "Content-Type": file.content_type,
434
+ "X-Fal-File-Name": file.file_name,
435
+ "X-Fal-Object-Lifecycle-Preference": json.dumps(
436
+ object_lifecycle_preference
437
+ ),
438
+ }
439
+ url = os.getenv("FAL_CDN_V3_HOST", _FAL_CDN_V3) + "/files/upload"
440
+ request = Request(url, headers=headers, method="POST", data=file.data)
441
+ try:
442
+ with urlopen(request) as response:
443
+ result = json.load(response)
444
+ except HTTPError as e:
445
+ raise FileUploadException(
446
+ f"Error initiating upload. Status {e.status}: {e.reason}"
447
+ )
448
+
449
+ access_url = result["access_url"]
450
+ return access_url
451
+
452
+ @property
453
+ def auth_headers(self) -> dict[str, str]:
454
+ token = fal_v2_token_manager.get_token()
455
+ return {
456
+ "Authorization": f"{token.token_type} {token.token}",
457
+ "User-Agent": "fal/0.1.0",
458
+ }
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from mimetypes import guess_extension, guess_type
5
5
  from pathlib import Path
6
- from typing import Literal
6
+ from typing import Literal, Optional
7
7
  from uuid import uuid4
8
8
 
9
9
 
@@ -29,12 +29,18 @@ class FileData:
29
29
  self.file_name = file_name
30
30
 
31
31
 
32
- RepositoryId = Literal["fal", "fal_v2", "in_memory", "gcp_storage", "r2", "cdn"]
32
+ RepositoryId = Literal[
33
+ "fal", "fal_v2", "fal_v3", "in_memory", "gcp_storage", "r2", "cdn"
34
+ ]
33
35
 
34
36
 
35
37
  @dataclass
36
38
  class FileRepository:
37
- def save(self, data: FileData) -> str:
39
+ def save(
40
+ self,
41
+ data: FileData,
42
+ object_lifecycle_preference: Optional[dict[str, str]] = None,
43
+ ) -> str:
38
44
  raise NotImplementedError()
39
45
 
40
46
  def save_file(
@@ -45,6 +51,7 @@ class FileRepository:
45
51
  multipart_threshold: int | None = None,
46
52
  multipart_chunk_size: int | None = None,
47
53
  multipart_max_concurrency: int | None = None,
54
+ object_lifecycle_preference: Optional[dict[str, str]] = None,
48
55
  ) -> tuple[str, FileData | None]:
49
56
  if multipart:
50
57
  raise NotImplementedError()
@@ -52,4 +59,4 @@ class FileRepository:
52
59
  with open(file_path, "rb") as fobj:
53
60
  data = FileData(fobj.read(), content_type, Path(file_path).name)
54
61
 
55
- return self.save(data), data
62
+ return self.save(data, object_lifecycle_preference), data
@@ -4,6 +4,7 @@ import io
4
4
  from tempfile import NamedTemporaryFile
5
5
  from typing import TYPE_CHECKING, Literal, Optional, Union
6
6
 
7
+ from fastapi import Request
7
8
  from pydantic import BaseModel, Field
8
9
 
9
10
  from fal.toolkit.file.file import DEFAULT_REPOSITORY, FALLBACK_REPOSITORY, File
@@ -82,6 +83,7 @@ class Image(File):
82
83
  fallback_repository: Optional[
83
84
  FileRepository | RepositoryId
84
85
  ] = FALLBACK_REPOSITORY,
86
+ request: Optional[Request] = None,
85
87
  ) -> Image:
86
88
  obj = super().from_bytes(
87
89
  data,
@@ -89,6 +91,7 @@ class Image(File):
89
91
  file_name=file_name,
90
92
  repository=repository,
91
93
  fallback_repository=fallback_repository,
94
+ request=request,
92
95
  )
93
96
  obj.width = size.width if size else None
94
97
  obj.height = size.height if size else None
@@ -104,6 +107,7 @@ class Image(File):
104
107
  fallback_repository: Optional[
105
108
  FileRepository | RepositoryId
106
109
  ] = FALLBACK_REPOSITORY,
110
+ request: Optional[Request] = None,
107
111
  ) -> Image:
108
112
  size = ImageSize(width=pil_image.width, height=pil_image.height)
109
113
  if format is None:
@@ -133,6 +137,7 @@ class Image(File):
133
137
  file_name,
134
138
  repository,
135
139
  fallback_repository=fallback_repository,
140
+ request=request,
136
141
  )
137
142
 
138
143
  def to_pil(self, mode: str = "RGB") -> PILImage.Image:
Binary file
@@ -23,6 +23,8 @@ from fal.toolkit.utils.download_utils import _get_git_revision_hash, _hash_url
23
23
  from pydantic import BaseModel, Field
24
24
  from pydantic import __version__ as pydantic_version
25
25
 
26
+ EXAMPLE_FILE_URL = "https://raw.githubusercontent.com/fal-ai/fal/main/projects/fal/tests/assets/cat.png"
27
+
26
28
 
27
29
  @pytest.mark.flaky(max_runs=3)
28
30
  def test_isolated(isolated_client: Callable[..., Callable[..., IsolatedFunction]]):
@@ -197,11 +199,9 @@ def mock_fal_persistent_dirs(monkeypatch):
197
199
  def test_download_file(isolated_client, mock_fal_persistent_dirs):
198
200
  from fal.toolkit.utils.download_utils import FAL_PERSISTENT_DIR
199
201
 
200
- EXAMPLE_FILE_URL = "https://raw.githubusercontent.com/fal-ai/isolate/d553f927348206530208442556f481f39b161732/README.md"
201
-
202
202
  relative_directory = "test"
203
203
  output_directory = FAL_PERSISTENT_DIR / relative_directory
204
- expected_path = output_directory / "README.md"
204
+ expected_path = output_directory / "cat.png"
205
205
 
206
206
  @isolated_client()
207
207
  def absolute_path_persistent_dir():
@@ -301,8 +301,7 @@ def test_download_model_weights(isolated_client, mock_fal_persistent_dirs):
301
301
 
302
302
  print(FAL_MODEL_WEIGHTS_DIR)
303
303
 
304
- EXAMPLE_FILE_URL = "https://raw.githubusercontent.com/fal-ai/isolate/d553f927348206530208442556f481f39b161732/README.md"
305
- expected_path = FAL_MODEL_WEIGHTS_DIR / _hash_url(EXAMPLE_FILE_URL) / "README.md"
304
+ expected_path = FAL_MODEL_WEIGHTS_DIR / _hash_url(EXAMPLE_FILE_URL) / "cat.png"
306
305
 
307
306
  @isolated_client()
308
307
  def download_weights():
@@ -510,8 +509,8 @@ def test_fal_file_save(isolated_client):
510
509
  "file_url, expected_content",
511
510
  [
512
511
  (
513
- "https://raw.githubusercontent.com/fal-ai/fal/fe0e2a1aa4b46a42a93bad0fbd9aca4aefcb4296/README.md",
514
- "projects/fal/README.md",
512
+ EXAMPLE_FILE_URL,
513
+ "projects/fal/cat.png",
515
514
  ),
516
515
  ("data:text/plain;charset=UTF-8,fal", "fal"),
517
516
  ],
@@ -580,6 +580,7 @@ def test_worker_env_vars(isolated_client):
580
580
  "https://storage.googleapis.com/isolate-dev-smiling-shark_toolkit_bucket/",
581
581
  ),
582
582
  ("fal_v2", "https://v2.fal.media/files"),
583
+ ("fal_v3", "https://v3.fal.media/files"),
583
584
  ],
584
585
  )
585
586
  def test_fal_storage(isolated_client, repo_type, url_prefix):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes