fal 1.2.0__tar.gz → 1.2.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 (151) hide show
  1. {fal-1.2.0 → fal-1.2.2}/PKG-INFO +2 -1
  2. {fal-1.2.0 → fal-1.2.2}/fal.egg-info/PKG-INFO +2 -1
  3. {fal-1.2.0 → fal-1.2.2}/fal.egg-info/SOURCES.txt +1 -0
  4. {fal-1.2.0 → fal-1.2.2}/fal.egg-info/requires.txt +1 -0
  5. {fal-1.2.0 → fal-1.2.2}/pyproject.toml +1 -0
  6. {fal-1.2.0 → fal-1.2.2}/src/fal/_fal_version.py +2 -2
  7. {fal-1.2.0 → fal-1.2.2}/src/fal/api.py +3 -1
  8. {fal-1.2.0 → fal-1.2.2}/src/fal/app.py +2 -1
  9. {fal-1.2.0 → fal-1.2.2}/src/fal/cli/apps.py +4 -2
  10. fal-1.2.2/src/fal/cli/create.py +26 -0
  11. {fal-1.2.0 → fal-1.2.2}/src/fal/cli/deploy.py +3 -3
  12. {fal-1.2.0 → fal-1.2.2}/src/fal/cli/main.py +2 -2
  13. {fal-1.2.0 → fal-1.2.2}/src/fal/cli/run.py +1 -1
  14. {fal-1.2.0 → fal-1.2.2}/src/fal/logging/isolate.py +20 -0
  15. {fal-1.2.0 → fal-1.2.2}/src/fal/toolkit/utils/download_utils.py +48 -39
  16. {fal-1.2.0 → fal-1.2.2}/src/fal/utils.py +8 -4
  17. {fal-1.2.0 → fal-1.2.2}/tests/test_apps.py +4 -4
  18. {fal-1.2.0 → fal-1.2.2}/.gitignore +0 -0
  19. {fal-1.2.0 → fal-1.2.2}/README.md +0 -0
  20. {fal-1.2.0 → fal-1.2.2}/fal.egg-info/dependency_links.txt +0 -0
  21. {fal-1.2.0 → fal-1.2.2}/fal.egg-info/entry_points.txt +0 -0
  22. {fal-1.2.0 → fal-1.2.2}/fal.egg-info/top_level.txt +0 -0
  23. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/README.md +0 -0
  24. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  25. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  26. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  27. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  28. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  29. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  30. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  31. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  32. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  33. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  34. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  35. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  36. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  37. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  38. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  39. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  40. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  41. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  42. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  43. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  44. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  45. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  46. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  47. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  48. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  49. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  50. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  51. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  52. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  53. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  54. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  55. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  56. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  57. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
  58. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  59. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  60. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  61. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  62. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  63. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  64. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  65. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  66. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  67. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  68. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  69. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  70. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  71. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  72. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  73. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  74. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  75. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  76. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  77. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  78. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  79. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  80. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  81. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  82. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  83. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  84. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  85. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  86. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  87. {fal-1.2.0 → fal-1.2.2}/openapi-fal-rest/pyproject.toml +0 -0
  88. {fal-1.2.0 → fal-1.2.2}/openapi_rest.config.yaml +0 -0
  89. {fal-1.2.0 → fal-1.2.2}/setup.cfg +0 -0
  90. {fal-1.2.0 → fal-1.2.2}/src/fal/__init__.py +0 -0
  91. {fal-1.2.0 → fal-1.2.2}/src/fal/__main__.py +0 -0
  92. {fal-1.2.0 → fal-1.2.2}/src/fal/_serialization.py +0 -0
  93. {fal-1.2.0 → fal-1.2.2}/src/fal/_version.py +0 -0
  94. {fal-1.2.0 → fal-1.2.2}/src/fal/apps.py +0 -0
  95. {fal-1.2.0 → fal-1.2.2}/src/fal/auth/__init__.py +0 -0
  96. {fal-1.2.0 → fal-1.2.2}/src/fal/auth/auth0.py +0 -0
  97. {fal-1.2.0 → fal-1.2.2}/src/fal/auth/local.py +0 -0
  98. {fal-1.2.0 → fal-1.2.2}/src/fal/cli/__init__.py +0 -0
  99. {fal-1.2.0 → fal-1.2.2}/src/fal/cli/auth.py +0 -0
  100. {fal-1.2.0 → fal-1.2.2}/src/fal/cli/debug.py +0 -0
  101. {fal-1.2.0 → fal-1.2.2}/src/fal/cli/doctor.py +0 -0
  102. {fal-1.2.0 → fal-1.2.2}/src/fal/cli/keys.py +0 -0
  103. {fal-1.2.0 → fal-1.2.2}/src/fal/cli/parser.py +0 -0
  104. {fal-1.2.0 → fal-1.2.2}/src/fal/cli/secrets.py +0 -0
  105. {fal-1.2.0 → fal-1.2.2}/src/fal/console/__init__.py +0 -0
  106. {fal-1.2.0 → fal-1.2.2}/src/fal/console/icons.py +0 -0
  107. {fal-1.2.0 → fal-1.2.2}/src/fal/console/ux.py +0 -0
  108. {fal-1.2.0 → fal-1.2.2}/src/fal/container.py +0 -0
  109. {fal-1.2.0 → fal-1.2.2}/src/fal/exceptions/__init__.py +0 -0
  110. {fal-1.2.0 → fal-1.2.2}/src/fal/exceptions/_base.py +0 -0
  111. {fal-1.2.0 → fal-1.2.2}/src/fal/exceptions/auth.py +0 -0
  112. {fal-1.2.0 → fal-1.2.2}/src/fal/flags.py +0 -0
  113. {fal-1.2.0 → fal-1.2.2}/src/fal/logging/__init__.py +0 -0
  114. {fal-1.2.0 → fal-1.2.2}/src/fal/logging/style.py +0 -0
  115. {fal-1.2.0 → fal-1.2.2}/src/fal/logging/trace.py +0 -0
  116. {fal-1.2.0 → fal-1.2.2}/src/fal/logging/user.py +0 -0
  117. {fal-1.2.0 → fal-1.2.2}/src/fal/py.typed +0 -0
  118. {fal-1.2.0 → fal-1.2.2}/src/fal/rest_client.py +0 -0
  119. {fal-1.2.0 → fal-1.2.2}/src/fal/sdk.py +0 -0
  120. {fal-1.2.0 → fal-1.2.2}/src/fal/sync.py +0 -0
  121. {fal-1.2.0 → fal-1.2.2}/src/fal/toolkit/__init__.py +0 -0
  122. {fal-1.2.0 → fal-1.2.2}/src/fal/toolkit/exceptions.py +0 -0
  123. {fal-1.2.0 → fal-1.2.2}/src/fal/toolkit/file/__init__.py +0 -0
  124. {fal-1.2.0 → fal-1.2.2}/src/fal/toolkit/file/file.py +0 -0
  125. {fal-1.2.0 → fal-1.2.2}/src/fal/toolkit/file/providers/fal.py +0 -0
  126. {fal-1.2.0 → fal-1.2.2}/src/fal/toolkit/file/providers/gcp.py +0 -0
  127. {fal-1.2.0 → fal-1.2.2}/src/fal/toolkit/file/providers/r2.py +0 -0
  128. {fal-1.2.0 → fal-1.2.2}/src/fal/toolkit/file/types.py +0 -0
  129. {fal-1.2.0 → fal-1.2.2}/src/fal/toolkit/image/__init__.py +0 -0
  130. {fal-1.2.0 → fal-1.2.2}/src/fal/toolkit/image/image.py +0 -0
  131. {fal-1.2.0 → fal-1.2.2}/src/fal/toolkit/optimize.py +0 -0
  132. {fal-1.2.0 → fal-1.2.2}/src/fal/toolkit/utils/__init__.py +0 -0
  133. {fal-1.2.0 → fal-1.2.2}/src/fal/workflows.py +0 -0
  134. {fal-1.2.0 → fal-1.2.2}/tests/__init__.py +0 -0
  135. {fal-1.2.0 → fal-1.2.2}/tests/cli/__init__.py +0 -0
  136. {fal-1.2.0 → fal-1.2.2}/tests/cli/test_apps.py +0 -0
  137. {fal-1.2.0 → fal-1.2.2}/tests/cli/test_auth.py +0 -0
  138. {fal-1.2.0 → fal-1.2.2}/tests/cli/test_deploy.py +0 -0
  139. {fal-1.2.0 → fal-1.2.2}/tests/cli/test_keys.py +0 -0
  140. {fal-1.2.0 → fal-1.2.2}/tests/cli/test_run.py +0 -0
  141. {fal-1.2.0 → fal-1.2.2}/tests/cli/test_secrets.py +0 -0
  142. {fal-1.2.0 → fal-1.2.2}/tests/conftest.py +0 -0
  143. {fal-1.2.0 → fal-1.2.2}/tests/integration_test.py +0 -0
  144. {fal-1.2.0 → fal-1.2.2}/tests/mainify_package/__init__.py +0 -0
  145. {fal-1.2.0 → fal-1.2.2}/tests/mainify_package/impl.py +0 -0
  146. {fal-1.2.0 → fal-1.2.2}/tests/mainify_package/utils.py +0 -0
  147. {fal-1.2.0 → fal-1.2.2}/tests/mainify_target.py +0 -0
  148. {fal-1.2.0 → fal-1.2.2}/tests/test_stability.py +0 -0
  149. {fal-1.2.0 → fal-1.2.2}/tests/toolkit/file_test.py +0 -0
  150. {fal-1.2.0 → fal-1.2.2}/tests/toolkit/image_test.py +0 -0
  151. {fal-1.2.0 → fal-1.2.2}/tools/demo_script.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.2.0
3
+ Version: 1.2.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
@@ -35,6 +35,7 @@ Requires-Dist: websockets<13,>=12.0
35
35
  Requires-Dist: pillow<11,>=10.2.0
36
36
  Requires-Dist: pyjwt[crypto]<3,>=2.8.0
37
37
  Requires-Dist: uvicorn<1,>=0.29.0
38
+ Requires-Dist: cookiecutter
38
39
  Provides-Extra: test
39
40
  Requires-Dist: pytest<8; extra == "test"
40
41
  Requires-Dist: pytest-asyncio; extra == "test"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fal
3
- Version: 1.2.0
3
+ Version: 1.2.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
@@ -35,6 +35,7 @@ Requires-Dist: websockets<13,>=12.0
35
35
  Requires-Dist: pillow<11,>=10.2.0
36
36
  Requires-Dist: pyjwt[crypto]<3,>=2.8.0
37
37
  Requires-Dist: uvicorn<1,>=0.29.0
38
+ Requires-Dist: cookiecutter
38
39
  Provides-Extra: test
39
40
  Requires-Dist: pytest<8; extra == "test"
40
41
  Requires-Dist: pytest-asyncio; extra == "test"
@@ -95,6 +95,7 @@ src/fal/auth/local.py
95
95
  src/fal/cli/__init__.py
96
96
  src/fal/cli/apps.py
97
97
  src/fal/cli/auth.py
98
+ src/fal/cli/create.py
98
99
  src/fal/cli/debug.py
99
100
  src/fal/cli/deploy.py
100
101
  src/fal/cli/doctor.py
@@ -27,6 +27,7 @@ websockets<13,>=12.0
27
27
  pillow<11,>=10.2.0
28
28
  pyjwt[crypto]<3,>=2.8.0
29
29
  uvicorn<1,>=0.29.0
30
+ cookiecutter
30
31
 
31
32
  [:python_version < "3.10"]
32
33
  importlib-metadata>=4.4
@@ -55,6 +55,7 @@ dependencies = [
55
55
  "pillow>=10.2.0,<11",
56
56
  "pyjwt[crypto]>=2.8.0,<3",
57
57
  "uvicorn>=0.29.0,<1",
58
+ "cookiecutter",
58
59
  ]
59
60
 
60
61
  [project.optional-dependencies]
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.2.0'
16
- __version_tuple__ = version_tuple = (1, 2, 0)
15
+ __version__ = version = '1.2.2'
16
+ __version_tuple__ = version_tuple = (1, 2, 2)
@@ -1048,7 +1048,9 @@ class BaseServable:
1048
1048
  from uvicorn import Config
1049
1049
 
1050
1050
  app = self._build_app()
1051
- server = Server(config=Config(app, host="0.0.0.0", port=8080))
1051
+ server = Server(
1052
+ config=Config(app, host="0.0.0.0", port=8080, timeout_keep_alive=300)
1053
+ )
1052
1054
  metrics_app = FastAPI()
1053
1055
  metrics_app.add_route("/metrics", handle_metrics)
1054
1056
  metrics_server = Server(config=Config(metrics_app, host="0.0.0.0", port=9090))
@@ -7,7 +7,7 @@ import re
7
7
  import time
8
8
  import typing
9
9
  from contextlib import asynccontextmanager, contextmanager
10
- from typing import Any, Callable, ClassVar, TypeVar
10
+ from typing import Any, Callable, ClassVar, Literal, TypeVar
11
11
 
12
12
  import httpx
13
13
  from fastapi import FastAPI
@@ -152,6 +152,7 @@ class App(fal.api.BaseServable):
152
152
  "keep_alive": 60,
153
153
  }
154
154
  app_name: ClassVar[str]
155
+ app_auth: ClassVar[Literal["private", "public", "shared"]] = "private"
155
156
 
156
157
  def __init_subclass__(cls, **kwargs):
157
158
  app_name = kwargs.pop("name", None) or _to_fal_app_name(cls.__name__)
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from typing import TYPE_CHECKING
2
4
 
3
5
  from .parser import FalClientParser
@@ -6,7 +8,7 @@ if TYPE_CHECKING:
6
8
  from fal.sdk import AliasInfo, ApplicationInfo
7
9
 
8
10
 
9
- def _apps_table(apps: list["AliasInfo"]):
11
+ def _apps_table(apps: list[AliasInfo]):
10
12
  from rich.table import Table
11
13
 
12
14
  table = Table()
@@ -56,7 +58,7 @@ def _add_list_parser(subparsers, parents):
56
58
  parser.set_defaults(func=_list)
57
59
 
58
60
 
59
- def _app_rev_table(revs: list["ApplicationInfo"]):
61
+ def _app_rev_table(revs: list[ApplicationInfo]):
60
62
  from rich.table import Table
61
63
 
62
64
  table = Table()
@@ -0,0 +1,26 @@
1
+ PROJECT_TYPES = ["app"]
2
+
3
+
4
+ def _create_project(project_type: str):
5
+ from cookiecutter.main import cookiecutter
6
+
7
+ cookiecutter("https://github.com/fal-ai/cookiecutter-fal.git")
8
+
9
+
10
+ def add_parser(main_subparsers, parents):
11
+ apps_help = "Create fal applications."
12
+ parser = main_subparsers.add_parser(
13
+ "create",
14
+ description=apps_help,
15
+ help=apps_help,
16
+ parents=parents,
17
+ )
18
+
19
+ parser.add_argument(
20
+ metavar="project_type",
21
+ choices=PROJECT_TYPES,
22
+ help="Type of project to create.",
23
+ dest="project_type",
24
+ )
25
+
26
+ parser.set_defaults(func=_create_project)
@@ -81,17 +81,18 @@ def _deploy(args):
81
81
 
82
82
  user = _get_user()
83
83
  host = FalServerlessHost(args.host)
84
- isolated_function, app_name = load_function_from(
84
+ isolated_function, app_name, app_auth = load_function_from(
85
85
  host,
86
86
  file_path,
87
87
  func_name,
88
88
  )
89
89
  app_name = args.app_name or app_name
90
+ app_auth = args.auth or app_auth or "private"
90
91
  app_id = host.register(
91
92
  func=isolated_function.func,
92
93
  options=isolated_function.options,
93
94
  application_name=app_name,
94
- application_auth_mode=args.auth,
95
+ application_auth_mode=app_auth,
95
96
  metadata=isolated_function.options.host.get("metadata", {}),
96
97
  )
97
98
 
@@ -151,7 +152,6 @@ def add_parser(main_subparsers, parents):
151
152
  parser.add_argument(
152
153
  "--auth",
153
154
  type=valid_auth_option,
154
- default="private",
155
155
  help="Application authentication mode (private, public).",
156
156
  )
157
157
  parser.set_defaults(func=_deploy)
@@ -6,7 +6,7 @@ from fal import __version__
6
6
  from fal.console import console
7
7
  from fal.console.icons import CROSS_ICON
8
8
 
9
- from . import apps, auth, deploy, doctor, keys, run, secrets
9
+ from . import apps, auth, create, deploy, doctor, keys, run, secrets
10
10
  from .debug import debugtools, get_debug_parser
11
11
  from .parser import FalParser, FalParserExit
12
12
 
@@ -31,7 +31,7 @@ def _get_main_parser() -> argparse.ArgumentParser:
31
31
  required=True,
32
32
  )
33
33
 
34
- for cmd in [auth, apps, deploy, run, keys, secrets, doctor]:
34
+ for cmd in [auth, apps, deploy, run, keys, secrets, doctor, create]:
35
35
  cmd.add_parser(subparsers, parents)
36
36
 
37
37
  return parser
@@ -6,7 +6,7 @@ def _run(args):
6
6
  from fal.utils import load_function_from
7
7
 
8
8
  host = FalServerlessHost(args.host)
9
- isolated_function, _ = load_function_from(host, *args.func_ref)
9
+ isolated_function, _, _ = load_function_from(host, *args.func_ref)
10
10
  # let our exc handlers handle UserFunctionException
11
11
  isolated_function.reraise = False
12
12
  isolated_function()
@@ -17,11 +17,31 @@ class IsolateLogPrinter:
17
17
 
18
18
  def __init__(self, debug: bool = False) -> None:
19
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
20
38
 
21
39
  def print(self, log: Log):
22
40
  if log.level < LogLevel.INFO and not self.debug:
23
41
  return
24
42
 
43
+ self._maybe_print_header(log.source)
44
+
25
45
  if log.source == LogSource.USER:
26
46
  stream = sys.stderr if log.level == LogLevel.STDERR else sys.stdout
27
47
  print(log.message, file=stream)
@@ -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)
@@ -10,13 +10,13 @@ def load_function_from(
10
10
  host: FalServerlessHost,
11
11
  file_path: str,
12
12
  function_name: str | None = None,
13
- ) -> tuple[IsolatedFunction, str | None]:
13
+ ) -> tuple[IsolatedFunction, str | None, str | None]:
14
14
  import runpy
15
15
 
16
16
  module = runpy.run_path(file_path)
17
17
  if function_name is None:
18
18
  fal_objects = {
19
- obj.app_name: obj_name
19
+ obj_name: obj
20
20
  for obj_name, obj in module.items()
21
21
  if isinstance(obj, type)
22
22
  and issubclass(obj, fal.App)
@@ -30,9 +30,12 @@ def load_function_from(
30
30
  "Please specify the name of the app."
31
31
  )
32
32
 
33
- [(app_name, function_name)] = fal_objects.items()
33
+ [(function_name, obj)] = fal_objects.items()
34
+ app_name = obj.app_name
35
+ app_auth = obj.app_auth
34
36
  else:
35
37
  app_name = None
38
+ app_auth = None
36
39
 
37
40
  if function_name not in module:
38
41
  raise FalServerlessError(f"Function '{function_name}' not found in module")
@@ -44,10 +47,11 @@ def load_function_from(
44
47
  target = module[function_name]
45
48
  if isinstance(target, type) and issubclass(target, App):
46
49
  app_name = target.app_name
50
+ app_auth = target.app_auth
47
51
  target = wrap_app(target, host=host)
48
52
 
49
53
  if not isinstance(target, IsolatedFunction):
50
54
  raise FalServerlessError(
51
55
  f"Function '{function_name}' is not a fal.function or a fal.App"
52
56
  )
53
- return target, app_name
57
+ return target, app_name, app_auth
@@ -192,7 +192,7 @@ def aliased_app() -> Generator[tuple[str, str], None, None]:
192
192
 
193
193
  import uuid
194
194
 
195
- app_alias = str(uuid.uuid4()).replace("-", "")[:10]
195
+ app_alias = str(uuid.uuid4()) + "-alias"
196
196
  app_revision = addition_app.host.register(
197
197
  func=addition_app.func,
198
198
  options=addition_app.options,
@@ -482,15 +482,14 @@ def test_app_set_delete_alias(aliased_app: tuple[str, str]):
482
482
  assert found.revision == app_revision
483
483
  assert found.auth_mode == "private"
484
484
 
485
- new_app_alias = f"{app_alias}-new"
486
485
  with host._connection as client:
487
486
  # Get the registered values
488
- res = client.create_alias(new_app_alias, app_revision, "public")
487
+ res = client.create_alias(app_alias, app_revision, "public")
489
488
 
490
489
  with host._connection as client:
491
490
  # Get the registered values
492
491
  res = client.list_aliases()
493
- found = next(filter(lambda alias: alias.alias == new_app_alias, res), None)
492
+ found = next(filter(lambda alias: alias.alias == app_alias, res), None)
494
493
  assert found, f"Could not find app {app_alias} in {res}"
495
494
  assert found.revision == app_revision
496
495
  assert found.auth_mode == "public"
@@ -506,6 +505,7 @@ def test_app_set_delete_alias(aliased_app: tuple[str, str]):
506
505
  assert not found, f"Found app {app_alias} in {res} after deletion"
507
506
 
508
507
 
508
+ @pytest.mark.flaky(max_runs=3)
509
509
  def test_realtime_connection(test_realtime_app):
510
510
  response = apps.run(test_realtime_app, arguments={"prompt": "a cat"})
511
511
  assert response["text"] == "a cat"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes