fal 1.1.1__tar.gz → 1.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fal might be problematic. Click here for more details.

Files changed (150) hide show
  1. {fal-1.1.1 → fal-1.2.1}/PKG-INFO +1 -1
  2. {fal-1.1.1 → fal-1.2.1}/fal.egg-info/PKG-INFO +1 -1
  3. {fal-1.1.1 → fal-1.2.1}/src/fal/_fal_version.py +2 -2
  4. {fal-1.1.1 → fal-1.2.1}/src/fal/_serialization.py +3 -4
  5. {fal-1.1.1 → fal-1.2.1}/src/fal/apps.py +1 -2
  6. {fal-1.1.1 → fal-1.2.1}/src/fal/cli/apps.py +0 -1
  7. {fal-1.1.1 → fal-1.2.1}/src/fal/cli/debug.py +1 -5
  8. {fal-1.1.1 → fal-1.2.1}/src/fal/cli/deploy.py +8 -7
  9. {fal-1.1.1 → fal-1.2.1}/src/fal/cli/keys.py +4 -1
  10. {fal-1.1.1 → fal-1.2.1}/src/fal/cli/run.py +1 -4
  11. {fal-1.1.1 → fal-1.2.1}/src/fal/cli/secrets.py +3 -5
  12. {fal-1.1.1 → fal-1.2.1}/src/fal/exceptions/_base.py +1 -0
  13. {fal-1.1.1 → fal-1.2.1}/src/fal/logging/isolate.py +20 -1
  14. {fal-1.1.1 → fal-1.2.1}/src/fal/sdk.py +1 -1
  15. {fal-1.1.1 → fal-1.2.1}/src/fal/toolkit/file/file.py +1 -0
  16. {fal-1.1.1 → fal-1.2.1}/src/fal/toolkit/file/providers/fal.py +3 -1
  17. {fal-1.1.1 → fal-1.2.1}/src/fal/toolkit/file/providers/gcp.py +6 -1
  18. {fal-1.1.1 → fal-1.2.1}/src/fal/toolkit/file/providers/r2.py +6 -1
  19. {fal-1.1.1 → fal-1.2.1}/src/fal/toolkit/image/image.py +6 -7
  20. {fal-1.1.1 → fal-1.2.1}/src/fal/toolkit/utils/download_utils.py +48 -39
  21. {fal-1.1.1 → fal-1.2.1}/src/fal/utils.py +0 -1
  22. {fal-1.1.1 → fal-1.2.1}/tests/cli/test_apps.py +11 -6
  23. {fal-1.1.1 → fal-1.2.1}/tests/cli/test_keys.py +4 -2
  24. {fal-1.1.1 → fal-1.2.1}/tests/integration_test.py +0 -1
  25. {fal-1.1.1 → fal-1.2.1}/tests/test_stability.py +16 -7
  26. {fal-1.1.1 → fal-1.2.1}/tests/toolkit/file_test.py +1 -39
  27. {fal-1.1.1 → fal-1.2.1}/.gitignore +0 -0
  28. {fal-1.1.1 → fal-1.2.1}/README.md +0 -0
  29. {fal-1.1.1 → fal-1.2.1}/fal.egg-info/SOURCES.txt +0 -0
  30. {fal-1.1.1 → fal-1.2.1}/fal.egg-info/dependency_links.txt +0 -0
  31. {fal-1.1.1 → fal-1.2.1}/fal.egg-info/entry_points.txt +0 -0
  32. {fal-1.1.1 → fal-1.2.1}/fal.egg-info/requires.txt +0 -0
  33. {fal-1.1.1 → fal-1.2.1}/fal.egg-info/top_level.txt +0 -0
  34. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/README.md +0 -0
  35. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  36. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  37. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  38. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  39. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  40. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  41. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  42. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  43. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  44. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  45. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  46. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  47. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  48. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  49. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  50. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  51. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  52. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  53. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  54. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  55. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  56. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  57. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  58. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  59. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  60. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  61. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  62. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  63. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  64. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  65. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  66. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  67. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  68. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
  69. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  70. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  71. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  72. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  73. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  74. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  75. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  76. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  77. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  78. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  79. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  80. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  81. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  82. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  83. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  84. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  85. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  86. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  87. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  88. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  89. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  90. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  91. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  92. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  93. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  94. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  95. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  96. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  97. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  98. {fal-1.1.1 → fal-1.2.1}/openapi-fal-rest/pyproject.toml +0 -0
  99. {fal-1.1.1 → fal-1.2.1}/openapi_rest.config.yaml +0 -0
  100. {fal-1.1.1 → fal-1.2.1}/pyproject.toml +0 -0
  101. {fal-1.1.1 → fal-1.2.1}/setup.cfg +0 -0
  102. {fal-1.1.1 → fal-1.2.1}/src/fal/__init__.py +0 -0
  103. {fal-1.1.1 → fal-1.2.1}/src/fal/__main__.py +0 -0
  104. {fal-1.1.1 → fal-1.2.1}/src/fal/_version.py +0 -0
  105. {fal-1.1.1 → fal-1.2.1}/src/fal/api.py +0 -0
  106. {fal-1.1.1 → fal-1.2.1}/src/fal/app.py +0 -0
  107. {fal-1.1.1 → fal-1.2.1}/src/fal/auth/__init__.py +0 -0
  108. {fal-1.1.1 → fal-1.2.1}/src/fal/auth/auth0.py +0 -0
  109. {fal-1.1.1 → fal-1.2.1}/src/fal/auth/local.py +0 -0
  110. {fal-1.1.1 → fal-1.2.1}/src/fal/cli/__init__.py +0 -0
  111. {fal-1.1.1 → fal-1.2.1}/src/fal/cli/auth.py +0 -0
  112. {fal-1.1.1 → fal-1.2.1}/src/fal/cli/doctor.py +0 -0
  113. {fal-1.1.1 → fal-1.2.1}/src/fal/cli/main.py +0 -0
  114. {fal-1.1.1 → fal-1.2.1}/src/fal/cli/parser.py +0 -0
  115. {fal-1.1.1 → fal-1.2.1}/src/fal/console/__init__.py +0 -0
  116. {fal-1.1.1 → fal-1.2.1}/src/fal/console/icons.py +0 -0
  117. {fal-1.1.1 → fal-1.2.1}/src/fal/console/ux.py +0 -0
  118. {fal-1.1.1 → fal-1.2.1}/src/fal/container.py +0 -0
  119. {fal-1.1.1 → fal-1.2.1}/src/fal/exceptions/__init__.py +0 -0
  120. {fal-1.1.1 → fal-1.2.1}/src/fal/exceptions/auth.py +0 -0
  121. {fal-1.1.1 → fal-1.2.1}/src/fal/flags.py +0 -0
  122. {fal-1.1.1 → fal-1.2.1}/src/fal/logging/__init__.py +0 -0
  123. {fal-1.1.1 → fal-1.2.1}/src/fal/logging/style.py +0 -0
  124. {fal-1.1.1 → fal-1.2.1}/src/fal/logging/trace.py +0 -0
  125. {fal-1.1.1 → fal-1.2.1}/src/fal/logging/user.py +0 -0
  126. {fal-1.1.1 → fal-1.2.1}/src/fal/py.typed +0 -0
  127. {fal-1.1.1 → fal-1.2.1}/src/fal/rest_client.py +0 -0
  128. {fal-1.1.1 → fal-1.2.1}/src/fal/sync.py +0 -0
  129. {fal-1.1.1 → fal-1.2.1}/src/fal/toolkit/__init__.py +0 -0
  130. {fal-1.1.1 → fal-1.2.1}/src/fal/toolkit/exceptions.py +0 -0
  131. {fal-1.1.1 → fal-1.2.1}/src/fal/toolkit/file/__init__.py +0 -0
  132. {fal-1.1.1 → fal-1.2.1}/src/fal/toolkit/file/types.py +0 -0
  133. {fal-1.1.1 → fal-1.2.1}/src/fal/toolkit/image/__init__.py +0 -0
  134. {fal-1.1.1 → fal-1.2.1}/src/fal/toolkit/optimize.py +0 -0
  135. {fal-1.1.1 → fal-1.2.1}/src/fal/toolkit/utils/__init__.py +0 -0
  136. {fal-1.1.1 → fal-1.2.1}/src/fal/workflows.py +0 -0
  137. {fal-1.1.1 → fal-1.2.1}/tests/__init__.py +0 -0
  138. {fal-1.1.1 → fal-1.2.1}/tests/cli/__init__.py +0 -0
  139. {fal-1.1.1 → fal-1.2.1}/tests/cli/test_auth.py +1 -1
  140. {fal-1.1.1 → fal-1.2.1}/tests/cli/test_deploy.py +0 -0
  141. {fal-1.1.1 → fal-1.2.1}/tests/cli/test_run.py +0 -0
  142. {fal-1.1.1 → fal-1.2.1}/tests/cli/test_secrets.py +0 -0
  143. {fal-1.1.1 → fal-1.2.1}/tests/conftest.py +0 -0
  144. {fal-1.1.1 → fal-1.2.1}/tests/mainify_package/__init__.py +0 -0
  145. {fal-1.1.1 → fal-1.2.1}/tests/mainify_package/impl.py +0 -0
  146. {fal-1.1.1 → fal-1.2.1}/tests/mainify_package/utils.py +0 -0
  147. {fal-1.1.1 → fal-1.2.1}/tests/mainify_target.py +0 -0
  148. {fal-1.1.1 → fal-1.2.1}/tests/test_apps.py +0 -0
  149. {fal-1.1.1 → fal-1.2.1}/tests/toolkit/image_test.py +0 -0
  150. {fal-1.1.1 → fal-1.2.1}/tools/demo_script.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.1.1
3
+ Version: 1.2.1
4
4
  Summary: fal is an easy-to-use Serverless Python Framework
5
5
  Author: Features & Labels <support@fal.ai>
6
6
  Requires-Python: >=3.8
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.1.1
3
+ Version: 1.2.1
4
4
  Summary: fal is an easy-to-use Serverless Python Framework
5
5
  Author: Features & Labels <support@fal.ai>
6
6
  Requires-Python: >=3.8
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.1.1'
16
- __version_tuple__ = version_tuple = (1, 1, 1)
15
+ __version__ = version = '1.2.1'
16
+ __version_tuple__ = version_tuple = (1, 2, 1)
@@ -186,8 +186,8 @@ def _patch_rlock() -> None:
186
186
 
187
187
  def pickle_rlock(obj: RLockType) -> tuple[Callable, tuple]:
188
188
  r = obj.__repr__()
189
- count = int(r.split('count=')[1].split()[0].rstrip('>'))
190
- owner = int(r.split('owner=')[1].split()[0])
189
+ count = int(r.split("count=")[1].split()[0].rstrip(">"))
190
+ owner = int(r.split("owner=")[1].split()[0])
191
191
 
192
192
  return create_rlock, (count, owner)
193
193
 
@@ -209,7 +209,7 @@ def _patch_console_thread_locals() -> None:
209
209
  "buffer": obj.buffer,
210
210
  "buffer_index": obj.buffer_index,
211
211
  }
212
- return create_locals, (kwargs, )
212
+ return create_locals, (kwargs,)
213
213
 
214
214
  _register(ConsoleThreadLocals, pickle_locals)
215
215
 
@@ -231,4 +231,3 @@ def patch_pickle() -> None:
231
231
  _patch_exceptions()
232
232
 
233
233
  _register_pickle_by_value("fal")
234
-
@@ -24,8 +24,7 @@ def _backwards_compatible_app_id(app_id: str) -> str:
24
24
 
25
25
 
26
26
  @dataclass
27
- class _Status:
28
- ...
27
+ class _Status: ...
29
28
 
30
29
 
31
30
  @dataclass
@@ -203,7 +203,6 @@ def _runners(args):
203
203
 
204
204
  from fal.sdk import FalServerlessClient
205
205
 
206
-
207
206
  client = FalServerlessClient(args.host)
208
207
  with client.connect() as connection:
209
208
  runners = connection.list_alias_runners(alias=args.app_name)
@@ -47,11 +47,7 @@ def debugtools(args):
47
47
  def get_debug_parser():
48
48
  parser = FalParser(add_help=False)
49
49
  group = parser.add_argument_group(title="Debug")
50
- group.add_argument(
51
- "--debug",
52
- action="store_true",
53
- help="Show verbose errors."
54
- )
50
+ group.add_argument("--debug", action="store_true", help="Show verbose errors.")
55
51
  group.add_argument(
56
52
  "--pdb",
57
53
  action="store_true",
@@ -69,9 +69,7 @@ def _deploy(args):
69
69
  # Try to find a python file in the current directory
70
70
  options = list(Path(".").glob("*.py"))
71
71
  if len(options) == 0:
72
- raise FalServerlessError(
73
- "No python files found in the current directory"
74
- )
72
+ raise FalServerlessError("No python files found in the current directory")
75
73
  elif len(options) > 1:
76
74
  raise FalServerlessError(
77
75
  "Multiple python files found in the current directory. "
@@ -107,8 +105,12 @@ def _deploy(args):
107
105
  "Registered a new revision for function "
108
106
  f"'{app_name}' (revision='{app_id}')."
109
107
  )
110
- args.console.print(f"Playground: https://fal.ai/models/{user.username}/{app_name}")
111
- args.console.print(f"Endpoint: https://{gateway_host}/{user.username}/{app_name}")
108
+ args.console.print(
109
+ f"Playground: https://fal.ai/models/{user.username}/{app_name}"
110
+ )
111
+ args.console.print(
112
+ f"Endpoint: https://{gateway_host}/{user.username}/{app_name}"
113
+ )
112
114
 
113
115
 
114
116
  def add_parser(main_subparsers, parents):
@@ -139,8 +141,7 @@ def add_parser(main_subparsers, parents):
139
141
  nargs="?",
140
142
  action=RefAction,
141
143
  help=(
142
- "Application reference. "
143
- "For example: `myfile.py::MyApp`, `myfile.py`."
144
+ "Application reference. " "For example: `myfile.py::MyApp`, `myfile.py`."
144
145
  ),
145
146
  )
146
147
  parser.add_argument(
@@ -56,7 +56,10 @@ def _list(args):
56
56
  keys = connection.list_user_keys()
57
57
  for key in keys:
58
58
  table.add_row(
59
- key.key_id, str(key.created_at), str(key.scope.value), key.alias,
59
+ key.key_id,
60
+ str(key.created_at),
61
+ str(key.scope.value),
62
+ key.alias,
60
63
  )
61
64
 
62
65
  args.console.print(table)
@@ -14,10 +14,7 @@ def _run(args):
14
14
 
15
15
  def add_parser(main_subparsers, parents):
16
16
  run_help = "Run fal function."
17
- epilog = (
18
- "Examples:\n"
19
- " fal run path/to/myfile.py::myfunc"
20
- )
17
+ epilog = "Examples:\n" " fal run path/to/myfile.py::myfunc"
21
18
  parser = main_subparsers.add_parser(
22
19
  "run",
23
20
  description=run_help,
@@ -1,9 +1,9 @@
1
-
2
1
  from .parser import DictAction, FalClientParser
3
2
 
4
3
 
5
4
  def _set(args):
6
5
  from fal.sdk import FalServerlessClient
6
+
7
7
  client = FalServerlessClient(args.host)
8
8
  with client.connect() as connection:
9
9
  for name, value in args.secrets.items():
@@ -12,10 +12,7 @@ def _set(args):
12
12
 
13
13
  def _add_set_parser(subparsers, parents):
14
14
  set_help = "Set a secret."
15
- epilog = (
16
- "Examples:\n"
17
- " fal secrets set HF_TOKEN=hf_***"
18
- )
15
+ epilog = "Examples:\n" " fal secrets set HF_TOKEN=hf_***"
19
16
 
20
17
  parser = subparsers.add_parser(
21
18
  "set",
@@ -64,6 +61,7 @@ def _add_list_parser(subparsers, parents):
64
61
 
65
62
  def _unset(args):
66
63
  from fal.sdk import FalServerlessClient
64
+
67
65
  client = FalServerlessClient(args.host)
68
66
  with client.connect() as connection:
69
67
  connection.delete_secret(args.secret)
@@ -3,4 +3,5 @@ from __future__ import annotations
3
3
 
4
4
  class FalServerlessException(Exception):
5
5
  """Base exception type for fal Serverless related flows and APIs."""
6
+
6
7
  pass
@@ -13,16 +13,35 @@ _renderer = ConsoleRenderer(level_styles=LEVEL_STYLES)
13
13
 
14
14
 
15
15
  class IsolateLogPrinter:
16
-
17
16
  debug: bool
18
17
 
19
18
  def __init__(self, debug: bool = False) -> None:
20
19
  self.debug = debug
20
+ self._current_source: LogSource | None = None
21
+
22
+ def _maybe_print_header(self, source: LogSource):
23
+ from fal.console import console
24
+
25
+ if source == self._current_source:
26
+ return
27
+
28
+ msg = {
29
+ LogSource.BUILDER: "Building the environment",
30
+ LogSource.BRIDGE: "Unpacking user code",
31
+ LogSource.USER: "Running",
32
+ }.get(source)
33
+
34
+ if msg:
35
+ console.print(f"==> {msg}", style="bold green")
36
+
37
+ self._current_source = source
21
38
 
22
39
  def print(self, log: Log):
23
40
  if log.level < LogLevel.INFO and not self.debug:
24
41
  return
25
42
 
43
+ self._maybe_print_header(log.source)
44
+
26
45
  if log.source == LogSource.USER:
27
46
  stream = sys.stderr if log.level == LogLevel.STDERR else sys.stdout
28
47
  print(log.message, file=stream)
@@ -277,7 +277,7 @@ class KeyScope(enum.Enum):
277
277
 
278
278
  @from_grpc.register(isolate_proto.ApplicationInfo)
279
279
  def _from_grpc_application_info(
280
- message: isolate_proto.ApplicationInfo
280
+ message: isolate_proto.ApplicationInfo,
281
281
  ) -> ApplicationInfo:
282
282
  return ApplicationInfo(
283
283
  application_id=message.application_id,
@@ -15,6 +15,7 @@ if not hasattr(pydantic, "__version__") or pydantic.__version__.startswith("1.")
15
15
  else:
16
16
  from pydantic import GetCoreSchemaHandler
17
17
  from pydantic_core import CoreSchema, core_schema
18
+
18
19
  IS_PYDANTIC_V2 = True
19
20
 
20
21
  from pydantic import BaseModel, Field
@@ -41,7 +41,9 @@ class FalFileRepositoryBase(FileRepository):
41
41
 
42
42
  grpc_host = os.environ.get("FAL_HOST", "api.alpha.fal.ai")
43
43
  rest_host = grpc_host.replace("api", "rest", 1)
44
- storage_url = f"https://{rest_host}/storage/upload/initiate?storage_type={storage_type}"
44
+ storage_url = (
45
+ f"https://{rest_host}/storage/upload/initiate?storage_type={storage_type}"
46
+ )
45
47
 
46
48
  try:
47
49
  req = Request(
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  import datetime
4
4
  import json
5
5
  import os
6
+ import posixpath
7
+ import uuid
6
8
  from dataclasses import dataclass
7
9
 
8
10
  from fal.toolkit.file.types import FileData, FileRepository
@@ -49,7 +51,10 @@ class GoogleStorageRepository(FileRepository):
49
51
  return self._bucket
50
52
 
51
53
  def save(self, data: FileData) -> str:
52
- destination_path = os.path.join(self.folder, data.file_name)
54
+ destination_path = posixpath.join(
55
+ self.folder,
56
+ f"{uuid.uuid4().hex}_{data.file_name}",
57
+ )
53
58
 
54
59
  gcp_blob = self.bucket.blob(destination_path)
55
60
  gcp_blob.upload_from_string(data.data, content_type=data.content_type)
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import os
5
+ import posixpath
6
+ import uuid
5
7
  from dataclasses import dataclass
6
8
  from io import BytesIO
7
9
 
@@ -66,7 +68,10 @@ class R2Repository(FileRepository):
66
68
  return self._bucket
67
69
 
68
70
  def save(self, data: FileData) -> str:
69
- destination_path = os.path.join(self.key, data.file_name)
71
+ destination_path = posixpath.join(
72
+ self.key,
73
+ f"{uuid.uuid4().hex}_{data.file_name}",
74
+ )
70
75
 
71
76
  s3_object = self.bucket.Object(destination_path)
72
77
  s3_object.upload_fileobj(
@@ -44,6 +44,7 @@ IMAGE_SIZE_PRESETS: dict[ImageSizePreset, ImageSize] = {
44
44
 
45
45
  ImageSizeInput = Union[ImageSize, ImageSizePreset]
46
46
 
47
+
47
48
  def get_image_size(source: ImageSizeInput) -> ImageSize:
48
49
  if isinstance(source, ImageSize):
49
50
  return source
@@ -62,7 +63,8 @@ class Image(File):
62
63
  """
63
64
 
64
65
  width: Optional[int] = Field(
65
- None, description="The width of the image in pixels.",
66
+ None,
67
+ description="The width of the image in pixels.",
66
68
  examples=[1024],
67
69
  )
68
70
  height: Optional[int] = Field(
@@ -84,8 +86,8 @@ class Image(File):
84
86
  file_name=file_name,
85
87
  repository=repository,
86
88
  )
87
- obj.width=size.width if size else None
88
- obj.height=size.height if size else None
89
+ obj.width = size.width if size else None
90
+ obj.height = size.height if size else None
89
91
  return obj
90
92
 
91
93
  @classmethod
@@ -120,9 +122,7 @@ class Image(File):
120
122
  from PIL import Image as PILImage
121
123
  from PIL import ImageOps
122
124
  except ImportError:
123
- raise ImportError(
124
- "The PIL package is required to use Image.to_pil()."
125
- )
125
+ raise ImportError("The PIL package is required to use Image.to_pil().")
126
126
 
127
127
  # Stream the image data from url to a temp file and convert it to a PIL image
128
128
  with NamedTemporaryFile() as temp_file:
@@ -134,4 +134,3 @@ class Image(File):
134
134
  img = ImageOps.exif_transpose(img)
135
135
 
136
136
  return img
137
-
@@ -1,11 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import hashlib
4
+ import os
4
5
  import shutil
5
6
  import subprocess
6
7
  import sys
7
8
  from pathlib import Path, PurePath
8
- from tempfile import TemporaryDirectory
9
+ from tempfile import NamedTemporaryFile, TemporaryDirectory
9
10
  from urllib.parse import urlparse
10
11
  from urllib.request import Request, urlopen
11
12
 
@@ -215,10 +216,14 @@ def _download_file_python(
215
216
  Returns:
216
217
  The path where the downloaded file has been saved.
217
218
  """
218
- import shutil
219
- import tempfile
220
-
221
- with tempfile.NamedTemporaryFile(delete=False) as temp_file:
219
+ basename = os.path.basename(target_path)
220
+ # NOTE: using the same directory to avoid potential copies across temp fs and target
221
+ # fs, and also to be able to atomically rename a downloaded file into place.
222
+ with NamedTemporaryFile(
223
+ delete=False,
224
+ dir=os.path.dirname(target_path),
225
+ prefix=f"{basename}.tmp",
226
+ ) as temp_file:
222
227
  try:
223
228
  file_path = temp_file.name
224
229
 
@@ -232,13 +237,14 @@ def _download_file_python(
232
237
 
233
238
  print(progress_msg, end="\r\n")
234
239
 
235
- # Move the file when the file is downloaded completely. Since the
236
- # file used is temporary, in a case of an interruption, the downloaded
237
- # content will be lost. So, it is safe to redownload the file in such cases.
238
- shutil.move(file_path, target_path)
240
+ # NOTE: Atomically renaming the file into place when the file is downloaded
241
+ # completely.
242
+ #
243
+ # Since the file used is temporary, in a case of an interruption, the
244
+ # downloaded content will be lost. So, it is safe to redownload the file in
245
+ # such cases.
246
+ os.rename(file_path, target_path)
239
247
 
240
- except Exception as error:
241
- raise error
242
248
  finally:
243
249
  Path(temp_file.name).unlink(missing_ok=True)
244
250
 
@@ -403,35 +409,38 @@ def clone_repository(
403
409
  print(f"Removing the existing repository: {local_repo_path} ")
404
410
  shutil.rmtree(local_repo_path)
405
411
 
406
- # Temporary directory to download the repo into.
407
- temp_dir = TemporaryDirectory()
408
- temp_dir_path = temp_dir.name
409
-
410
- try:
411
- print(f"Cloning the repository '{https_url}' .")
412
-
413
- # Clone with disabling the logs and advices for detached HEAD state.
414
- clone_command = [
415
- "git",
416
- "clone",
417
- "--recursive",
418
- https_url,
419
- temp_dir_path,
420
- ]
421
- subprocess.check_call(clone_command)
422
-
423
- if commit_hash:
424
- checkout_command = ["git", "checkout", commit_hash]
425
- subprocess.check_call(checkout_command, cwd=temp_dir_path)
426
-
427
- # Move the repository when the clone and checkout is finished.
428
- shutil.move(temp_dir_path, local_repo_path)
429
-
430
- except Exception as error:
431
- print(f"{error}\nFailed to clone repository '{https_url}' .")
432
- temp_dir.cleanup()
412
+ # NOTE: using the target_dir to be able to avoid potential copies across temp fs
413
+ # and target fs, and also to be able to atomically rename repo_name dir into place
414
+ # when we are done setting it up.
415
+ os.makedirs(target_dir, exist_ok=True) # type: ignore[arg-type]
416
+ with TemporaryDirectory(
417
+ dir=target_dir,
418
+ suffix=f"{local_repo_path.name}.tmp",
419
+ ) as temp_dir:
420
+ try:
421
+ print(f"Cloning the repository '{https_url}' .")
422
+
423
+ # Clone with disabling the logs and advices for detached HEAD state.
424
+ clone_command = [
425
+ "git",
426
+ "clone",
427
+ "--recursive",
428
+ https_url,
429
+ temp_dir,
430
+ ]
431
+ subprocess.check_call(clone_command)
432
+
433
+ if commit_hash:
434
+ checkout_command = ["git", "checkout", commit_hash]
435
+ subprocess.check_call(checkout_command, cwd=temp_dir)
436
+
437
+ # NOTE: Atomically renaming the repository directory into place when the
438
+ # clone and checkout are done.
439
+ os.rename(temp_dir, local_repo_path)
433
440
 
434
- raise error
441
+ except Exception as error:
442
+ print(f"{error}\nFailed to clone repository '{https_url}' .")
443
+ raise error
435
444
 
436
445
  if include_to_path:
437
446
  __add_local_path_to_sys_path(local_repo_path)
@@ -51,4 +51,3 @@ def load_function_from(
51
51
  f"Function '{function_name}' is not a fal.function or a fal.App"
52
52
  )
53
53
  return target, app_name
54
-
@@ -31,11 +31,17 @@ def test_set_rev():
31
31
  def test_scale():
32
32
  args = parse_args(
33
33
  [
34
- "apps", "scale", "myapp",
35
- "--keep-alive", "123",
36
- "--max-multiplexing", "321",
37
- "--min-concurrency", "7",
38
- "--max-concurrency", "10",
34
+ "apps",
35
+ "scale",
36
+ "myapp",
37
+ "--keep-alive",
38
+ "123",
39
+ "--max-multiplexing",
40
+ "321",
41
+ "--min-concurrency",
42
+ "7",
43
+ "--max-concurrency",
44
+ "10",
39
45
  ]
40
46
  )
41
47
  assert args.func == _scale
@@ -61,4 +67,3 @@ def test_delete_rev():
61
67
  args = parse_args(["apps", "delete-rev", "myrev"])
62
68
  assert args.func == _delete_rev
63
69
  assert args.app_rev == "myrev"
64
-
@@ -7,8 +7,10 @@ def test_create():
7
7
  [
8
8
  "keys",
9
9
  "create",
10
- "--scope", "API",
11
- "--desc", "My test key",
10
+ "--scope",
11
+ "API",
12
+ "--desc",
13
+ "My test key",
12
14
  ]
13
15
  )
14
16
  assert args.func == _create
@@ -556,7 +556,6 @@ def test_fal_compressed_file(isolated_client):
556
556
 
557
557
 
558
558
  def test_fal_cdn(isolated_client):
559
-
560
559
  @isolated_client(requirements=[f"pydantic=={pydantic_version}"])
561
560
  def upload_to_fal_cdn() -> FalImage:
562
561
  return FalImage.from_bytes(b"0", "jpeg", repository="cdn")
@@ -56,7 +56,10 @@ def test_regular_function_on_nomad(isolated_client):
56
56
 
57
57
  assert mult(5, 2) == 10
58
58
 
59
- @pytest.mark.xfail(reason="The support needs to be deployed. See https://github.com/fal-ai/isolate-cloud/pull/1809")
59
+
60
+ @pytest.mark.xfail(
61
+ reason="The support needs to be deployed. See https://github.com/fal-ai/isolate-cloud/pull/1809"
62
+ )
60
63
  def test_regular_function_in_a_container(isolated_client):
61
64
  @isolated_client("container")
62
65
  def regular_function():
@@ -70,7 +73,10 @@ def test_regular_function_in_a_container(isolated_client):
70
73
 
71
74
  assert mult(5, 2) == 10
72
75
 
73
- @pytest.mark.xfail(reason="The support needs to be deployed. See https://github.com/fal-ai/isolate-cloud/pull/1809")
76
+
77
+ @pytest.mark.xfail(
78
+ reason="The support needs to be deployed. See https://github.com/fal-ai/isolate-cloud/pull/1809"
79
+ )
74
80
  def test_regular_function_in_a_container_with_custom_image(isolated_client):
75
81
  @isolated_client(
76
82
  "container",
@@ -557,11 +563,14 @@ def test_worker_env_vars(isolated_client):
557
563
 
558
564
 
559
565
  @pytest.mark.parametrize(
560
- "repo_type, url_prefix",
561
- [
562
- ("fal", "https://storage.googleapis.com/isolate-dev-smiling-shark_toolkit_bucket/"),
563
- ("fal_v2", "https://v2.fal.media/files"),
564
- ]
566
+ "repo_type, url_prefix",
567
+ [
568
+ (
569
+ "fal",
570
+ "https://storage.googleapis.com/isolate-dev-smiling-shark_toolkit_bucket/",
571
+ ),
572
+ ("fal_v2", "https://v2.fal.media/files"),
573
+ ],
565
574
  )
566
575
  def test_fal_storage(isolated_client, repo_type, url_prefix):
567
576
  file = File.from_bytes(b"Hello fal storage from local", repository=repo_type)
@@ -4,8 +4,7 @@ import os
4
4
  from base64 import b64encode
5
5
 
6
6
  import pytest
7
- from fal.toolkit.file.file import BUILT_IN_REPOSITORIES, File, GoogleStorageRepository
8
- from fal.toolkit.file.types import FileRepository, RepositoryId
7
+ from fal.toolkit.file.file import File, GoogleStorageRepository
9
8
 
10
9
 
11
10
  def test_binary_content_matches():
@@ -65,40 +64,3 @@ def test_gcp_storage_if_available():
65
64
  assert file.url.startswith(
66
65
  "https://storage.googleapis.com/fal_registry_image_results/"
67
66
  )
68
-
69
-
70
- @pytest.mark.xfail(reason="fal cdn is temporarily broken")
71
- @pytest.mark.parametrize(
72
- "repo",
73
- BUILT_IN_REPOSITORIES.keys(),
74
- )
75
- def test_uniqueness_of_file_name(repo: RepositoryId | FileRepository):
76
- if repo == "in_memory":
77
- return
78
-
79
- if repo == "gcp_storage":
80
- gcp_sa_json = os.environ.get("GCLOUD_SA_JSON")
81
- if gcp_sa_json is None:
82
- pytest.skip(reason="GCLOUD_SA_JSON environment variable is not set")
83
- repo = GoogleStorageRepository(bucket_name="fal_registry_image_results")
84
-
85
- if repo == "r2":
86
- r2_account_json = os.environ.get("R2_CREDS_JSON")
87
- if r2_account_json is None:
88
- pytest.skip(reason="R2_CREDS_JSON environment variable is not set")
89
-
90
- if repo == "fal":
91
- fal_key = os.environ.get("FAL_KEY")
92
- if fal_key is None:
93
- pytest.skip(reason="FAL_KEY environment variable is not set")
94
-
95
- file = File.from_bytes(b"print('Hello!')", repository=repo, file_name="hello.py")
96
-
97
- host_and_path = file.url.split("?")[0]
98
- last_path = host_and_path.split("/")[-1]
99
- assert (
100
- last_path.endswith(".py")
101
- ), f"The file name {last_path} should end with the same extension"
102
- assert (
103
- len(last_path) > 10
104
- ), f"There should be a long enough random string in the file name {last_path}"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -6,6 +6,7 @@ def test_login():
6
6
  args = parse_args(["auth", "login"])
7
7
  assert args.func == _login
8
8
 
9
+
9
10
  def test_logout():
10
11
  args = parse_args(["auth", "logout"])
11
12
  assert args.func == _logout
@@ -14,4 +15,3 @@ def test_logout():
14
15
  def test_whoami():
15
16
  args = parse_args(["auth", "whoami"])
16
17
  assert args.func == _whoami
17
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes