fal 1.6.2__tar.gz → 1.7.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 (171) hide show
  1. {fal-1.6.2/fal.egg-info → fal-1.7.1}/PKG-INFO +1 -1
  2. {fal-1.6.2 → fal-1.7.1/fal.egg-info}/PKG-INFO +1 -1
  3. {fal-1.6.2 → fal-1.7.1}/fal.egg-info/SOURCES.txt +3 -0
  4. {fal-1.6.2 → fal-1.7.1}/src/fal/_fal_version.py +2 -2
  5. {fal-1.6.2 → fal-1.7.1}/src/fal/app.py +2 -2
  6. {fal-1.6.2 → fal-1.7.1}/src/fal/auth/__init__.py +50 -2
  7. fal-1.7.1/src/fal/config.py +23 -0
  8. {fal-1.6.2 → fal-1.7.1}/src/fal/container.py +1 -1
  9. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/file/file.py +10 -22
  10. fal-1.7.1/src/fal/toolkit/types.py +140 -0
  11. {fal-1.6.2 → fal-1.7.1}/tests/integration_test.py +6 -6
  12. {fal-1.6.2 → fal-1.7.1}/tests/test_stability.py +3 -1
  13. {fal-1.6.2 → fal-1.7.1}/tests/toolkit/image_test.py +4 -4
  14. fal-1.7.1/tests/toolkit/test_types.py +99 -0
  15. {fal-1.6.2 → fal-1.7.1}/.gitignore +0 -0
  16. {fal-1.6.2 → fal-1.7.1}/Makefile +0 -0
  17. {fal-1.6.2 → fal-1.7.1}/README.md +0 -0
  18. {fal-1.6.2 → fal-1.7.1}/docs/conf.py +0 -0
  19. {fal-1.6.2 → fal-1.7.1}/docs/index.rst +0 -0
  20. {fal-1.6.2 → fal-1.7.1}/fal.egg-info/dependency_links.txt +0 -0
  21. {fal-1.6.2 → fal-1.7.1}/fal.egg-info/entry_points.txt +0 -0
  22. {fal-1.6.2 → fal-1.7.1}/fal.egg-info/requires.txt +0 -0
  23. {fal-1.6.2 → fal-1.7.1}/fal.egg-info/top_level.txt +0 -0
  24. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/README.md +0 -0
  25. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  26. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  27. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  28. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  29. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  30. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  31. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  32. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  33. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  34. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  35. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  36. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  37. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  38. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  39. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  40. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  41. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  42. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  43. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  44. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  45. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  46. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  47. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  48. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  49. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  50. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  51. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  52. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  53. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  54. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  55. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  56. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  57. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  58. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
  59. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  60. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  61. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  62. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  63. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  64. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  65. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  66. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  67. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  68. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  69. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  70. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  71. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  72. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  73. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  74. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  75. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  76. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  77. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  78. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  79. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  80. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  81. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  82. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  83. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  84. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  85. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  86. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  87. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  88. {fal-1.6.2 → fal-1.7.1}/openapi-fal-rest/pyproject.toml +0 -0
  89. {fal-1.6.2 → fal-1.7.1}/openapi_rest.config.yaml +0 -0
  90. {fal-1.6.2 → fal-1.7.1}/pyproject.toml +0 -0
  91. {fal-1.6.2 → fal-1.7.1}/setup.cfg +0 -0
  92. {fal-1.6.2 → fal-1.7.1}/src/fal/__init__.py +0 -0
  93. {fal-1.6.2 → fal-1.7.1}/src/fal/__main__.py +0 -0
  94. {fal-1.6.2 → fal-1.7.1}/src/fal/_serialization.py +0 -0
  95. {fal-1.6.2 → fal-1.7.1}/src/fal/_version.py +0 -0
  96. {fal-1.6.2 → fal-1.7.1}/src/fal/api.py +0 -0
  97. {fal-1.6.2 → fal-1.7.1}/src/fal/apps.py +0 -0
  98. {fal-1.6.2 → fal-1.7.1}/src/fal/auth/auth0.py +0 -0
  99. {fal-1.6.2 → fal-1.7.1}/src/fal/auth/local.py +0 -0
  100. {fal-1.6.2 → fal-1.7.1}/src/fal/cli/__init__.py +0 -0
  101. {fal-1.6.2 → fal-1.7.1}/src/fal/cli/_utils.py +0 -0
  102. {fal-1.6.2 → fal-1.7.1}/src/fal/cli/apps.py +0 -0
  103. {fal-1.6.2 → fal-1.7.1}/src/fal/cli/auth.py +0 -0
  104. {fal-1.6.2 → fal-1.7.1}/src/fal/cli/create.py +0 -0
  105. {fal-1.6.2 → fal-1.7.1}/src/fal/cli/debug.py +0 -0
  106. {fal-1.6.2 → fal-1.7.1}/src/fal/cli/deploy.py +0 -0
  107. {fal-1.6.2 → fal-1.7.1}/src/fal/cli/doctor.py +0 -0
  108. {fal-1.6.2 → fal-1.7.1}/src/fal/cli/keys.py +0 -0
  109. {fal-1.6.2 → fal-1.7.1}/src/fal/cli/main.py +0 -0
  110. {fal-1.6.2 → fal-1.7.1}/src/fal/cli/parser.py +0 -0
  111. {fal-1.6.2 → fal-1.7.1}/src/fal/cli/run.py +0 -0
  112. {fal-1.6.2 → fal-1.7.1}/src/fal/cli/runners.py +0 -0
  113. {fal-1.6.2 → fal-1.7.1}/src/fal/cli/secrets.py +0 -0
  114. {fal-1.6.2 → fal-1.7.1}/src/fal/console/__init__.py +0 -0
  115. {fal-1.6.2 → fal-1.7.1}/src/fal/console/icons.py +0 -0
  116. {fal-1.6.2 → fal-1.7.1}/src/fal/console/ux.py +0 -0
  117. {fal-1.6.2 → fal-1.7.1}/src/fal/exceptions/__init__.py +0 -0
  118. {fal-1.6.2 → fal-1.7.1}/src/fal/exceptions/_base.py +0 -0
  119. {fal-1.6.2 → fal-1.7.1}/src/fal/exceptions/_cuda.py +0 -0
  120. {fal-1.6.2 → fal-1.7.1}/src/fal/exceptions/auth.py +0 -0
  121. {fal-1.6.2 → fal-1.7.1}/src/fal/files.py +0 -0
  122. {fal-1.6.2 → fal-1.7.1}/src/fal/flags.py +0 -0
  123. {fal-1.6.2 → fal-1.7.1}/src/fal/logging/__init__.py +0 -0
  124. {fal-1.6.2 → fal-1.7.1}/src/fal/logging/isolate.py +0 -0
  125. {fal-1.6.2 → fal-1.7.1}/src/fal/logging/style.py +0 -0
  126. {fal-1.6.2 → fal-1.7.1}/src/fal/logging/trace.py +0 -0
  127. {fal-1.6.2 → fal-1.7.1}/src/fal/logging/user.py +0 -0
  128. {fal-1.6.2 → fal-1.7.1}/src/fal/py.typed +0 -0
  129. {fal-1.6.2 → fal-1.7.1}/src/fal/rest_client.py +0 -0
  130. {fal-1.6.2 → fal-1.7.1}/src/fal/sdk.py +0 -0
  131. {fal-1.6.2 → fal-1.7.1}/src/fal/sync.py +0 -0
  132. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/__init__.py +0 -0
  133. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/exceptions.py +0 -0
  134. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/file/__init__.py +0 -0
  135. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/file/providers/fal.py +0 -0
  136. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/file/providers/gcp.py +0 -0
  137. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/file/providers/r2.py +0 -0
  138. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/file/providers/s3.py +0 -0
  139. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/file/types.py +0 -0
  140. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/image/__init__.py +0 -0
  141. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/image/image.py +0 -0
  142. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
  143. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
  144. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
  145. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
  146. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
  147. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/image/safety_checker.py +0 -0
  148. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/optimize.py +0 -0
  149. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/utils/__init__.py +0 -0
  150. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/utils/download_utils.py +0 -0
  151. {fal-1.6.2 → fal-1.7.1}/src/fal/toolkit/utils/retry.py +0 -0
  152. {fal-1.6.2 → fal-1.7.1}/src/fal/utils.py +0 -0
  153. {fal-1.6.2 → fal-1.7.1}/src/fal/workflows.py +0 -0
  154. {fal-1.6.2 → fal-1.7.1}/tests/__init__.py +0 -0
  155. {fal-1.6.2 → fal-1.7.1}/tests/assets/cat.png +0 -0
  156. {fal-1.6.2 → fal-1.7.1}/tests/cli/__init__.py +0 -0
  157. {fal-1.6.2 → fal-1.7.1}/tests/cli/test_apps.py +0 -0
  158. {fal-1.6.2 → fal-1.7.1}/tests/cli/test_auth.py +0 -0
  159. {fal-1.6.2 → fal-1.7.1}/tests/cli/test_deploy.py +0 -0
  160. {fal-1.6.2 → fal-1.7.1}/tests/cli/test_keys.py +0 -0
  161. {fal-1.6.2 → fal-1.7.1}/tests/cli/test_run.py +0 -0
  162. {fal-1.6.2 → fal-1.7.1}/tests/cli/test_secrets.py +0 -0
  163. {fal-1.6.2 → fal-1.7.1}/tests/conftest.py +0 -0
  164. {fal-1.6.2 → fal-1.7.1}/tests/mainify_package/__init__.py +0 -0
  165. {fal-1.6.2 → fal-1.7.1}/tests/mainify_package/impl.py +0 -0
  166. {fal-1.6.2 → fal-1.7.1}/tests/mainify_package/utils.py +0 -0
  167. {fal-1.6.2 → fal-1.7.1}/tests/mainify_target.py +0 -0
  168. {fal-1.6.2 → fal-1.7.1}/tests/test_apps.py +0 -0
  169. {fal-1.6.2 → fal-1.7.1}/tests/toolkit/file_test.py +0 -0
  170. {fal-1.6.2 → fal-1.7.1}/tests/toolkit/utils/retry.py +0 -0
  171. {fal-1.6.2 → fal-1.7.1}/tools/demo_script.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: fal
3
- Version: 1.6.2
3
+ Version: 1.7.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.2
2
2
  Name: fal
3
- Version: 1.6.2
3
+ Version: 1.7.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
@@ -84,6 +84,7 @@ src/fal/_version.py
84
84
  src/fal/api.py
85
85
  src/fal/app.py
86
86
  src/fal/apps.py
87
+ src/fal/config.py
87
88
  src/fal/container.py
88
89
  src/fal/files.py
89
90
  src/fal/flags.py
@@ -125,6 +126,7 @@ src/fal/logging/user.py
125
126
  src/fal/toolkit/__init__.py
126
127
  src/fal/toolkit/exceptions.py
127
128
  src/fal/toolkit/optimize.py
129
+ src/fal/toolkit/types.py
128
130
  src/fal/toolkit/file/__init__.py
129
131
  src/fal/toolkit/file/file.py
130
132
  src/fal/toolkit/file/types.py
@@ -162,5 +164,6 @@ tests/mainify_package/impl.py
162
164
  tests/mainify_package/utils.py
163
165
  tests/toolkit/file_test.py
164
166
  tests/toolkit/image_test.py
167
+ tests/toolkit/test_types.py
165
168
  tests/toolkit/utils/retry.py
166
169
  tools/demo_script.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.6.2'
16
- __version_tuple__ = version_tuple = (1, 6, 2)
15
+ __version__ = version = '1.7.1'
16
+ __version_tuple__ = version_tuple = (1, 7, 1)
@@ -249,9 +249,9 @@ def _to_fal_app_name(name: str) -> str:
249
249
 
250
250
 
251
251
  def _print_python_packages() -> None:
252
- from pkg_resources import working_set
252
+ from importlib.metadata import distributions
253
253
 
254
- packages = [f"{package.key}=={package.version}" for package in working_set]
254
+ packages = [f"{dist.metadata['Name']}=={dist.version}" for dist in distributions()]
255
255
 
256
256
  print("[debug] Python packages installed:", ", ".join(packages))
257
257
 
@@ -2,22 +2,70 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  from dataclasses import dataclass, field
5
+ from threading import Lock
6
+ from typing import Optional
5
7
 
6
8
  import click
7
9
 
8
10
  from fal.auth import auth0, local
11
+ from fal.config import Config
9
12
  from fal.console import console
10
13
  from fal.console.icons import CHECK_ICON
11
14
  from fal.exceptions.auth import UnauthenticatedException
12
15
 
13
16
 
17
+ class GoogleColabState:
18
+ def __init__(self):
19
+ self.is_checked = False
20
+ self.lock = Lock()
21
+ self.secret: Optional[str] = None
22
+
23
+
24
+ _colab_state = GoogleColabState()
25
+
26
+
27
+ def is_google_colab() -> bool:
28
+ try:
29
+ from IPython import get_ipython
30
+
31
+ return "google.colab" in str(get_ipython())
32
+ except ModuleNotFoundError:
33
+ return False
34
+ except NameError:
35
+ return False
36
+
37
+
38
+ def get_colab_token() -> Optional[str]:
39
+ if not is_google_colab():
40
+ return None
41
+ with _colab_state.lock:
42
+ if _colab_state.is_checked: # request access only once
43
+ return _colab_state.secret
44
+
45
+ try:
46
+ from google.colab import userdata # noqa: I001
47
+ except ImportError:
48
+ return None
49
+
50
+ try:
51
+ token = userdata.get("FAL_KEY")
52
+ _colab_state.secret = token.strip()
53
+ except Exception:
54
+ _colab_state.secret = None
55
+
56
+ _colab_state.is_checked = True
57
+ return _colab_state.secret
58
+
59
+
14
60
  def key_credentials() -> tuple[str, str] | None:
15
61
  # Ignore key credentials when the user forces auth by user.
16
62
  if os.environ.get("FAL_FORCE_AUTH_BY_USER") == "1":
17
63
  return None
18
64
 
19
- if "FAL_KEY" in os.environ:
20
- key = os.environ["FAL_KEY"]
65
+ config = Config()
66
+
67
+ key = os.environ.get("FAL_KEY") or config.get("key") or get_colab_token()
68
+ if key:
21
69
  key_id, key_secret = key.split(":", 1)
22
70
  return (key_id, key_secret)
23
71
  elif "FAL_KEY_ID" in os.environ and "FAL_KEY_SECRET" in os.environ:
@@ -0,0 +1,23 @@
1
+ import os
2
+
3
+ import tomli
4
+
5
+
6
+ class Config:
7
+ DEFAULT_CONFIG_PATH = "~/.fal/config.toml"
8
+ DEFAULT_PROFILE = "default"
9
+
10
+ def __init__(self):
11
+ self.config_path = os.path.expanduser(
12
+ os.getenv("FAL_CONFIG_PATH", self.DEFAULT_CONFIG_PATH)
13
+ )
14
+ self.profile = os.getenv("FAL_PROFILE", self.DEFAULT_PROFILE)
15
+
16
+ try:
17
+ with open(self.config_path, "rb") as file:
18
+ self.config = tomli.load(file)
19
+ except FileNotFoundError:
20
+ self.config = {}
21
+
22
+ def get(self, key):
23
+ return self.config.get(self.profile, {}).get(key)
@@ -3,7 +3,7 @@ class ContainerImage:
3
3
  from a Dockerfile.
4
4
  """
5
5
 
6
- _known_keys = {"dockerfile_str", "build_args", "registries"}
6
+ _known_keys = {"dockerfile_str", "build_args", "registries", "builder"}
7
7
 
8
8
  @classmethod
9
9
  def from_dockerfile_str(cls, text: str, **kwargs):
@@ -47,7 +47,10 @@ BUILT_IN_REPOSITORIES: dict[RepositoryId, FileRepositoryFactory] = {
47
47
  }
48
48
 
49
49
 
50
- def get_builtin_repository(id: RepositoryId) -> FileRepository:
50
+ def get_builtin_repository(id: RepositoryId | FileRepository) -> FileRepository:
51
+ if isinstance(id, FileRepository):
52
+ return id
53
+
51
54
  if id not in BUILT_IN_REPOSITORIES.keys():
52
55
  raise ValueError(f'"{id}" is not a valid built-in file repository')
53
56
  return BUILT_IN_REPOSITORIES[id]()
@@ -122,7 +125,8 @@ class File(BaseModel):
122
125
  url=url,
123
126
  content_type=None,
124
127
  file_name=None,
125
- repository=DEFAULT_REPOSITORY,
128
+ file_size=None,
129
+ file_data=None,
126
130
  )
127
131
 
128
132
  @classmethod
@@ -139,11 +143,7 @@ class File(BaseModel):
139
143
  save_kwargs: Optional[dict] = None,
140
144
  fallback_save_kwargs: Optional[dict] = None,
141
145
  ) -> File:
142
- repo = (
143
- repository
144
- if isinstance(repository, FileRepository)
145
- else get_builtin_repository(repository)
146
- )
146
+ repo = get_builtin_repository(repository)
147
147
 
148
148
  save_kwargs = save_kwargs or {}
149
149
  fallback_save_kwargs = fallback_save_kwargs or {}
@@ -160,11 +160,7 @@ class File(BaseModel):
160
160
  if not fallback_repository:
161
161
  raise
162
162
 
163
- fallback_repo = (
164
- fallback_repository
165
- if isinstance(fallback_repository, FileRepository)
166
- else get_builtin_repository(fallback_repository)
167
- )
163
+ fallback_repo = get_builtin_repository(fallback_repository)
168
164
 
169
165
  url = fallback_repo.save(
170
166
  fdata, object_lifecycle_preference, **fallback_save_kwargs
@@ -196,11 +192,7 @@ class File(BaseModel):
196
192
  if not file_path.exists():
197
193
  raise FileNotFoundError(f"File {file_path} does not exist")
198
194
 
199
- repo = (
200
- repository
201
- if isinstance(repository, FileRepository)
202
- else get_builtin_repository(repository)
203
- )
195
+ repo = get_builtin_repository(repository)
204
196
 
205
197
  save_kwargs = save_kwargs or {}
206
198
  fallback_save_kwargs = fallback_save_kwargs or {}
@@ -222,11 +214,7 @@ class File(BaseModel):
222
214
  if not fallback_repository:
223
215
  raise
224
216
 
225
- fallback_repo = (
226
- fallback_repository
227
- if isinstance(fallback_repository, FileRepository)
228
- else get_builtin_repository(fallback_repository)
229
- )
217
+ fallback_repo = get_builtin_repository(fallback_repository)
230
218
 
231
219
  url, data = fallback_repo.save_file(
232
220
  file_path,
@@ -0,0 +1,140 @@
1
+ import re
2
+ import tempfile
3
+ from contextlib import contextmanager
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Generator, Union
6
+
7
+ import pydantic
8
+ from pydantic.utils import update_not_none
9
+
10
+ from fal.toolkit.image import read_image_from_url
11
+ from fal.toolkit.utils.download_utils import download_file
12
+
13
+ # https://github.com/pydantic/pydantic/pull/2573
14
+ if not hasattr(pydantic, "__version__") or pydantic.__version__.startswith("1."):
15
+ IS_PYDANTIC_V2 = False
16
+ else:
17
+ IS_PYDANTIC_V2 = True
18
+
19
+ MAX_DATA_URI_LENGTH = 10 * 1024 * 1024
20
+ MAX_HTTPS_URL_LENGTH = 2048
21
+
22
+ HTTP_URL_REGEX = (
23
+ r"^https:\/\/(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?::\d{1,5})?(?:\/[^\s]*)?$"
24
+ )
25
+
26
+
27
+ class DownloadFileMixin:
28
+ @contextmanager
29
+ def as_temp_file(self) -> Generator[Path, None, None]:
30
+ with tempfile.TemporaryDirectory() as temp_dir:
31
+ yield download_file(str(self), temp_dir)
32
+
33
+
34
+ class DownloadImageMixin:
35
+ def to_pil(self):
36
+ return read_image_from_url(str(self))
37
+
38
+
39
+ class DataUri(DownloadFileMixin, str):
40
+ if IS_PYDANTIC_V2:
41
+
42
+ @classmethod
43
+ def __get_pydantic_core_schema__(cls, source_type: Any, handler) -> Any:
44
+ return {
45
+ "type": "str",
46
+ "pattern": "^data:",
47
+ "max_length": MAX_DATA_URI_LENGTH,
48
+ "strip_whitespace": True,
49
+ }
50
+
51
+ def __get_pydantic_json_schema__(cls, core_schema, handler) -> Dict[str, Any]:
52
+ json_schema = handler(core_schema)
53
+ json_schema.update(format="data-uri")
54
+ return json_schema
55
+ else:
56
+
57
+ @classmethod
58
+ def __get_validators__(cls):
59
+ yield cls.validate
60
+
61
+ @classmethod
62
+ def validate(cls, value: Any) -> "DataUri":
63
+ from pydantic.validators import str_validator
64
+
65
+ value = str_validator(value)
66
+ value = value.strip()
67
+
68
+ if not value.startswith("data:"):
69
+ raise ValueError("Data URI must start with 'data:'")
70
+
71
+ if len(value) > MAX_DATA_URI_LENGTH:
72
+ raise ValueError(
73
+ f"Data URI is too long. Max length is {MAX_DATA_URI_LENGTH} bytes."
74
+ )
75
+
76
+ return cls(value)
77
+
78
+ @classmethod
79
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
80
+ update_not_none(field_schema, format="data-uri")
81
+
82
+
83
+ class HttpsUrl(DownloadFileMixin, str):
84
+ if IS_PYDANTIC_V2:
85
+
86
+ @classmethod
87
+ def __get_pydantic_core_schema__(cls, source_type: Any, handler) -> Any:
88
+ return {
89
+ "type": "str",
90
+ "pattern": HTTP_URL_REGEX,
91
+ "max_length": MAX_HTTPS_URL_LENGTH,
92
+ "strip_whitespace": True,
93
+ }
94
+
95
+ def __get_pydantic_json_schema__(cls, core_schema, handler) -> Dict[str, Any]:
96
+ json_schema = handler(core_schema)
97
+ json_schema.update(format="https-url")
98
+ return json_schema
99
+
100
+ else:
101
+
102
+ @classmethod
103
+ def __get_validators__(cls):
104
+ yield cls.validate
105
+
106
+ @classmethod
107
+ def validate(cls, value: Any) -> "HttpsUrl":
108
+ from pydantic.validators import str_validator
109
+
110
+ value = str_validator(value)
111
+ value = value.strip()
112
+
113
+ if not re.match(HTTP_URL_REGEX, value):
114
+ raise ValueError(
115
+ "URL must start with 'https://' and follow the correct format."
116
+ )
117
+
118
+ if len(value) > MAX_HTTPS_URL_LENGTH:
119
+ raise ValueError(
120
+ f"HTTPS URL is too long. Max length is "
121
+ f"{MAX_HTTPS_URL_LENGTH} characters."
122
+ )
123
+
124
+ return cls(value)
125
+
126
+ @classmethod
127
+ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
128
+ update_not_none(field_schema, format="https-url")
129
+
130
+
131
+ class ImageHttpsUrl(DownloadImageMixin, HttpsUrl):
132
+ pass
133
+
134
+
135
+ class ImageDataUri(DownloadImageMixin, DataUri):
136
+ pass
137
+
138
+
139
+ FileInput = Union[HttpsUrl, DataUri]
140
+ ImageInput = Union[ImageHttpsUrl, ImageDataUri]
@@ -458,7 +458,7 @@ def fal_file_content_matches(file: File, content: str):
458
458
 
459
459
 
460
460
  def test_fal_file_from_path(isolated_client):
461
- @isolated_client(requirements=[f"pydantic=={pydantic_version}"])
461
+ @isolated_client(requirements=[f"pydantic=={pydantic_version}", "tomli"])
462
462
  def fal_file_from_temp(content: str):
463
463
  with tempfile.NamedTemporaryFile() as temp_file:
464
464
  file_path = temp_file.name
@@ -475,7 +475,7 @@ def test_fal_file_from_path(isolated_client):
475
475
 
476
476
 
477
477
  def test_fal_file_from_bytes(isolated_client):
478
- @isolated_client(requirements=[f"pydantic=={pydantic_version}"])
478
+ @isolated_client(requirements=[f"pydantic=={pydantic_version}", "tomli"])
479
479
  def fal_file_from_bytes(content: str):
480
480
  return File.from_bytes(content.encode(), repository="in_memory")
481
481
 
@@ -486,7 +486,7 @@ def test_fal_file_from_bytes(isolated_client):
486
486
 
487
487
 
488
488
  def test_fal_file_save(isolated_client):
489
- @isolated_client(requirements=[f"pydantic=={pydantic_version}"])
489
+ @isolated_client(requirements=[f"pydantic=={pydantic_version}", "tomli"])
490
490
  def fal_file_to_local_file(content: str):
491
491
  file = File.from_bytes(content.encode(), repository="in_memory")
492
492
 
@@ -521,7 +521,7 @@ def test_fal_file_input(isolated_client, file_url: str, expected_content: str):
521
521
  class TestInput(BaseModel):
522
522
  file: File = Field()
523
523
 
524
- @isolated_client(requirements=[f"pydantic=={pydantic_version}"])
524
+ @isolated_client(requirements=[f"pydantic=={pydantic_version}", "tomli"])
525
525
  def init_file_on_fal(input: TestInput) -> File:
526
526
  return input.file
527
527
 
@@ -542,7 +542,7 @@ def test_fal_compressed_file(isolated_client):
542
542
  class TestInput(BaseModel):
543
543
  files: CompressedFile
544
544
 
545
- @isolated_client(requirements=[f"pydantic=={pydantic_version}"])
545
+ @isolated_client(requirements=[f"pydantic=={pydantic_version}", "tomli"])
546
546
  def init_compressed_file_on_fal(input: TestInput) -> int:
547
547
  extracted_file_paths = [file for file in input.files]
548
548
  return extracted_file_paths
@@ -557,7 +557,7 @@ def test_fal_compressed_file(isolated_client):
557
557
 
558
558
 
559
559
  def test_fal_cdn(isolated_client):
560
- @isolated_client(requirements=[f"pydantic=={pydantic_version}"])
560
+ @isolated_client(requirements=[f"pydantic=={pydantic_version}", "tomli"])
561
561
  def upload_to_fal_cdn() -> FalImage:
562
562
  return FalImage.from_bytes(b"0", "jpeg", repository="cdn")
563
563
 
@@ -595,7 +595,9 @@ def test_fal_storage(isolated_client, repo_type, url_prefix):
595
595
  assert file.url.startswith(url_prefix)
596
596
  assert file.as_bytes().decode().endswith("local")
597
597
 
598
- @isolated_client(serve=True, requirements=[f"pydantic=={pydantic_version}"])
598
+ @isolated_client(
599
+ serve=True, requirements=[f"pydantic=={pydantic_version}", "tomli"]
600
+ )
599
601
  def hello_file():
600
602
  # Run in the isolated environment
601
603
  return File.from_bytes(
@@ -77,7 +77,7 @@ def assert_fal_images_equals(fal_image_1: Image, fal_image_2: Image):
77
77
 
78
78
 
79
79
  def test_fal_image_from_pil(isolated_client):
80
- @isolated_client(requirements=["pillow", f"pydantic=={pydantic_version}"])
80
+ @isolated_client(requirements=["pillow", f"pydantic=={pydantic_version}", "tomli"])
81
81
  def fal_image_from_bytes_remote():
82
82
  pil_image = get_image()
83
83
  return Image.from_pil(pil_image, repository="in_memory")
@@ -87,7 +87,7 @@ def test_fal_image_from_pil(isolated_client):
87
87
 
88
88
 
89
89
  def test_fal_image_from_bytes(isolated_client):
90
- @isolated_client(requirements=["pillow", f"pydantic=={pydantic_version}"])
90
+ @isolated_client(requirements=["pillow", f"pydantic=={pydantic_version}", "tomli"])
91
91
  def fal_image_from_bytes_remote():
92
92
  image_bytes = get_image(as_bytes=True)
93
93
  return Image.from_bytes(image_bytes, repository="in_memory", format="png")
@@ -107,7 +107,7 @@ def test_fal_image_input(isolated_client, image_url):
107
107
  class TestInput(BaseModel):
108
108
  image: Image = Field()
109
109
 
110
- @isolated_client(requirements=["pillow", f"pydantic=={pydantic_version}"])
110
+ @isolated_client(requirements=["pillow", f"pydantic=={pydantic_version}", "tomli"])
111
111
  def init_image_on_fal(input: TestInput) -> Image:
112
112
  return TestInput(image=input.image).image
113
113
 
@@ -127,7 +127,7 @@ def test_fal_image_input_to_pil(isolated_client):
127
127
  class TestInput(BaseModel):
128
128
  image: Image = Field()
129
129
 
130
- @isolated_client(requirements=["pillow", f"pydantic=={pydantic_version}"])
130
+ @isolated_client(requirements=["pillow", f"pydantic=={pydantic_version}", "tomli"])
131
131
  def init_image_on_fal(input: TestInput) -> bytes:
132
132
  input_image = TestInput(image=input.image).image
133
133
  pil_image = input_image.to_pil()
@@ -0,0 +1,99 @@
1
+ import pytest
2
+ from pydantic import BaseModel, ValidationError
3
+
4
+ from fal.toolkit.types import MAX_DATA_URI_LENGTH, MAX_HTTPS_URL_LENGTH, FileInput
5
+
6
+
7
+ class DummyModel(BaseModel):
8
+ url: FileInput
9
+
10
+
11
+ class TestFileInput:
12
+ def test_valid_https_urls(self):
13
+ # Test basic HTTPS URL
14
+ model = DummyModel(url="https://example.com")
15
+ assert model.url == "https://example.com"
16
+
17
+ # Test HTTPS URL with path
18
+ model = DummyModel(url="https://example.com/path/to/resource")
19
+ assert model.url == "https://example.com/path/to/resource"
20
+
21
+ # Test HTTPS URL with query parameters
22
+ model = DummyModel(url="https://example.com/search?q=test&page=1")
23
+ assert model.url == "https://example.com/search?q=test&page=1"
24
+
25
+ # Test HTTPS URL with subdomain
26
+ model = DummyModel(url="https://sub.example.com")
27
+ assert model.url == "https://sub.example.com"
28
+
29
+ # Test HTTPS URL with port
30
+ model = DummyModel(url="https://example.com:8443")
31
+ assert model.url == "https://example.com:8443"
32
+
33
+ # Test HTTPS URL with whitespace
34
+ model = DummyModel(url=" https://example.com ")
35
+ assert model.url == "https://example.com"
36
+
37
+ # TODO: should we even allow this?
38
+ # Test HTTPS URL with port
39
+ model = DummyModel(url="https://example.com:8443")
40
+ assert model.url == "https://example.com:8443"
41
+
42
+ def test_valid_data_uris(self):
43
+ # Test basic data URI
44
+ model = DummyModel(url="data:text/plain;base64,SGVsbG8gV29ybGQ=")
45
+ assert model.url == "data:text/plain;base64,SGVsbG8gV29ybGQ="
46
+
47
+ # Test data URI with image
48
+ image_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" # noqa: E501
49
+ model = DummyModel(url=image_uri)
50
+ assert model.url == image_uri
51
+
52
+ # Test data URI with whitespace
53
+ model = DummyModel(url=" data:text/plain,Hello World ")
54
+ assert model.url == "data:text/plain,Hello World"
55
+
56
+ def test_invalid_inputs(self):
57
+ # Test HTTP URL (non-HTTPS)
58
+ with pytest.raises(ValueError):
59
+ DummyModel(url="http://example.com")
60
+
61
+ # Test malformed URL
62
+ with pytest.raises(ValueError):
63
+ DummyModel(url="not-a-url")
64
+
65
+ # Test invalid data URI
66
+ with pytest.raises(ValueError):
67
+ DummyModel(url="invalid-data-uri")
68
+
69
+ # Test empty string
70
+ with pytest.raises(ValueError):
71
+ DummyModel(url="")
72
+
73
+ # Test None value
74
+ with pytest.raises(ValueError):
75
+ DummyModel(url=None)
76
+
77
+ def test_length_limits(self):
78
+ # Test HTTPS URL at max length
79
+ domain = "example.com"
80
+ path_length = MAX_HTTPS_URL_LENGTH - len(f"https://{domain}/")
81
+ long_url = f"https://{domain}/{'a' * path_length}"
82
+ model = DummyModel(url=long_url)
83
+ assert model.url == long_url
84
+
85
+ # Test HTTPS URL exceeding max length
86
+ too_long_url = f"https://example.com/{'a' * MAX_HTTPS_URL_LENGTH}"
87
+ with pytest.raises(ValidationError):
88
+ DummyModel(url=too_long_url)
89
+
90
+ # Test data URI at max length
91
+ uri_prefix = "data:text/plain,"
92
+ long_uri = f"{uri_prefix}{'a' * (MAX_DATA_URI_LENGTH - len(uri_prefix))}"
93
+ model = DummyModel(url=long_uri)
94
+ assert model.url == long_uri
95
+
96
+ # Test data URI exceeding max length
97
+ too_long_uri = f"data:text/plain,{'a' * MAX_DATA_URI_LENGTH}"
98
+ with pytest.raises(ValueError):
99
+ DummyModel(url=too_long_uri)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes