fal 1.6.2__tar.gz → 1.7.0__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 (170) hide show
  1. {fal-1.6.2/fal.egg-info → fal-1.7.0}/PKG-INFO +1 -1
  2. {fal-1.6.2 → fal-1.7.0/fal.egg-info}/PKG-INFO +1 -1
  3. {fal-1.6.2 → fal-1.7.0}/fal.egg-info/SOURCES.txt +2 -0
  4. {fal-1.6.2 → fal-1.7.0}/src/fal/_fal_version.py +2 -2
  5. {fal-1.6.2 → fal-1.7.0}/src/fal/app.py +2 -2
  6. {fal-1.6.2 → fal-1.7.0}/src/fal/auth/__init__.py +47 -2
  7. {fal-1.6.2 → fal-1.7.0}/src/fal/container.py +1 -1
  8. fal-1.7.0/src/fal/toolkit/types.py +140 -0
  9. fal-1.7.0/tests/toolkit/test_types.py +99 -0
  10. {fal-1.6.2 → fal-1.7.0}/.gitignore +0 -0
  11. {fal-1.6.2 → fal-1.7.0}/Makefile +0 -0
  12. {fal-1.6.2 → fal-1.7.0}/README.md +0 -0
  13. {fal-1.6.2 → fal-1.7.0}/docs/conf.py +0 -0
  14. {fal-1.6.2 → fal-1.7.0}/docs/index.rst +0 -0
  15. {fal-1.6.2 → fal-1.7.0}/fal.egg-info/dependency_links.txt +0 -0
  16. {fal-1.6.2 → fal-1.7.0}/fal.egg-info/entry_points.txt +0 -0
  17. {fal-1.6.2 → fal-1.7.0}/fal.egg-info/requires.txt +0 -0
  18. {fal-1.6.2 → fal-1.7.0}/fal.egg-info/top_level.txt +0 -0
  19. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/README.md +0 -0
  20. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
  21. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
  22. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
  23. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
  24. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
  25. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
  26. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
  27. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
  28. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
  29. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
  30. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
  31. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
  32. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
  33. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
  34. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
  35. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
  36. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
  37. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
  38. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
  39. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
  40. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
  41. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
  42. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
  43. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
  44. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
  45. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
  46. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
  47. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
  48. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
  49. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
  50. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
  51. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
  52. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
  53. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
  54. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
  55. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
  56. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
  57. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
  58. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
  59. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
  60. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
  61. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
  62. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
  63. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
  64. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
  65. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
  66. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
  67. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
  68. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
  69. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
  70. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
  71. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
  72. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
  73. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
  74. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
  75. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
  76. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
  77. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
  78. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
  79. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
  80. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
  81. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
  82. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
  83. {fal-1.6.2 → fal-1.7.0}/openapi-fal-rest/pyproject.toml +0 -0
  84. {fal-1.6.2 → fal-1.7.0}/openapi_rest.config.yaml +0 -0
  85. {fal-1.6.2 → fal-1.7.0}/pyproject.toml +0 -0
  86. {fal-1.6.2 → fal-1.7.0}/setup.cfg +0 -0
  87. {fal-1.6.2 → fal-1.7.0}/src/fal/__init__.py +0 -0
  88. {fal-1.6.2 → fal-1.7.0}/src/fal/__main__.py +0 -0
  89. {fal-1.6.2 → fal-1.7.0}/src/fal/_serialization.py +0 -0
  90. {fal-1.6.2 → fal-1.7.0}/src/fal/_version.py +0 -0
  91. {fal-1.6.2 → fal-1.7.0}/src/fal/api.py +0 -0
  92. {fal-1.6.2 → fal-1.7.0}/src/fal/apps.py +0 -0
  93. {fal-1.6.2 → fal-1.7.0}/src/fal/auth/auth0.py +0 -0
  94. {fal-1.6.2 → fal-1.7.0}/src/fal/auth/local.py +0 -0
  95. {fal-1.6.2 → fal-1.7.0}/src/fal/cli/__init__.py +0 -0
  96. {fal-1.6.2 → fal-1.7.0}/src/fal/cli/_utils.py +0 -0
  97. {fal-1.6.2 → fal-1.7.0}/src/fal/cli/apps.py +0 -0
  98. {fal-1.6.2 → fal-1.7.0}/src/fal/cli/auth.py +0 -0
  99. {fal-1.6.2 → fal-1.7.0}/src/fal/cli/create.py +0 -0
  100. {fal-1.6.2 → fal-1.7.0}/src/fal/cli/debug.py +0 -0
  101. {fal-1.6.2 → fal-1.7.0}/src/fal/cli/deploy.py +0 -0
  102. {fal-1.6.2 → fal-1.7.0}/src/fal/cli/doctor.py +0 -0
  103. {fal-1.6.2 → fal-1.7.0}/src/fal/cli/keys.py +0 -0
  104. {fal-1.6.2 → fal-1.7.0}/src/fal/cli/main.py +0 -0
  105. {fal-1.6.2 → fal-1.7.0}/src/fal/cli/parser.py +0 -0
  106. {fal-1.6.2 → fal-1.7.0}/src/fal/cli/run.py +0 -0
  107. {fal-1.6.2 → fal-1.7.0}/src/fal/cli/runners.py +0 -0
  108. {fal-1.6.2 → fal-1.7.0}/src/fal/cli/secrets.py +0 -0
  109. {fal-1.6.2 → fal-1.7.0}/src/fal/console/__init__.py +0 -0
  110. {fal-1.6.2 → fal-1.7.0}/src/fal/console/icons.py +0 -0
  111. {fal-1.6.2 → fal-1.7.0}/src/fal/console/ux.py +0 -0
  112. {fal-1.6.2 → fal-1.7.0}/src/fal/exceptions/__init__.py +0 -0
  113. {fal-1.6.2 → fal-1.7.0}/src/fal/exceptions/_base.py +0 -0
  114. {fal-1.6.2 → fal-1.7.0}/src/fal/exceptions/_cuda.py +0 -0
  115. {fal-1.6.2 → fal-1.7.0}/src/fal/exceptions/auth.py +0 -0
  116. {fal-1.6.2 → fal-1.7.0}/src/fal/files.py +0 -0
  117. {fal-1.6.2 → fal-1.7.0}/src/fal/flags.py +0 -0
  118. {fal-1.6.2 → fal-1.7.0}/src/fal/logging/__init__.py +0 -0
  119. {fal-1.6.2 → fal-1.7.0}/src/fal/logging/isolate.py +0 -0
  120. {fal-1.6.2 → fal-1.7.0}/src/fal/logging/style.py +0 -0
  121. {fal-1.6.2 → fal-1.7.0}/src/fal/logging/trace.py +0 -0
  122. {fal-1.6.2 → fal-1.7.0}/src/fal/logging/user.py +0 -0
  123. {fal-1.6.2 → fal-1.7.0}/src/fal/py.typed +0 -0
  124. {fal-1.6.2 → fal-1.7.0}/src/fal/rest_client.py +0 -0
  125. {fal-1.6.2 → fal-1.7.0}/src/fal/sdk.py +0 -0
  126. {fal-1.6.2 → fal-1.7.0}/src/fal/sync.py +0 -0
  127. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/__init__.py +0 -0
  128. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/exceptions.py +0 -0
  129. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/file/__init__.py +0 -0
  130. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/file/file.py +0 -0
  131. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/file/providers/fal.py +0 -0
  132. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/file/providers/gcp.py +0 -0
  133. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/file/providers/r2.py +0 -0
  134. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/file/providers/s3.py +0 -0
  135. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/file/types.py +0 -0
  136. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/image/__init__.py +0 -0
  137. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/image/image.py +0 -0
  138. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
  139. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
  140. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
  141. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
  142. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
  143. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/image/safety_checker.py +0 -0
  144. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/optimize.py +0 -0
  145. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/utils/__init__.py +0 -0
  146. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/utils/download_utils.py +0 -0
  147. {fal-1.6.2 → fal-1.7.0}/src/fal/toolkit/utils/retry.py +0 -0
  148. {fal-1.6.2 → fal-1.7.0}/src/fal/utils.py +0 -0
  149. {fal-1.6.2 → fal-1.7.0}/src/fal/workflows.py +0 -0
  150. {fal-1.6.2 → fal-1.7.0}/tests/__init__.py +0 -0
  151. {fal-1.6.2 → fal-1.7.0}/tests/assets/cat.png +0 -0
  152. {fal-1.6.2 → fal-1.7.0}/tests/cli/__init__.py +0 -0
  153. {fal-1.6.2 → fal-1.7.0}/tests/cli/test_apps.py +0 -0
  154. {fal-1.6.2 → fal-1.7.0}/tests/cli/test_auth.py +0 -0
  155. {fal-1.6.2 → fal-1.7.0}/tests/cli/test_deploy.py +0 -0
  156. {fal-1.6.2 → fal-1.7.0}/tests/cli/test_keys.py +0 -0
  157. {fal-1.6.2 → fal-1.7.0}/tests/cli/test_run.py +0 -0
  158. {fal-1.6.2 → fal-1.7.0}/tests/cli/test_secrets.py +0 -0
  159. {fal-1.6.2 → fal-1.7.0}/tests/conftest.py +0 -0
  160. {fal-1.6.2 → fal-1.7.0}/tests/integration_test.py +0 -0
  161. {fal-1.6.2 → fal-1.7.0}/tests/mainify_package/__init__.py +0 -0
  162. {fal-1.6.2 → fal-1.7.0}/tests/mainify_package/impl.py +0 -0
  163. {fal-1.6.2 → fal-1.7.0}/tests/mainify_package/utils.py +0 -0
  164. {fal-1.6.2 → fal-1.7.0}/tests/mainify_target.py +0 -0
  165. {fal-1.6.2 → fal-1.7.0}/tests/test_apps.py +0 -0
  166. {fal-1.6.2 → fal-1.7.0}/tests/test_stability.py +0 -0
  167. {fal-1.6.2 → fal-1.7.0}/tests/toolkit/file_test.py +0 -0
  168. {fal-1.6.2 → fal-1.7.0}/tests/toolkit/image_test.py +0 -0
  169. {fal-1.6.2 → fal-1.7.0}/tests/toolkit/utils/retry.py +0 -0
  170. {fal-1.6.2 → fal-1.7.0}/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.0
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.0
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
@@ -125,6 +125,7 @@ src/fal/logging/user.py
125
125
  src/fal/toolkit/__init__.py
126
126
  src/fal/toolkit/exceptions.py
127
127
  src/fal/toolkit/optimize.py
128
+ src/fal/toolkit/types.py
128
129
  src/fal/toolkit/file/__init__.py
129
130
  src/fal/toolkit/file/file.py
130
131
  src/fal/toolkit/file/types.py
@@ -162,5 +163,6 @@ tests/mainify_package/impl.py
162
163
  tests/mainify_package/utils.py
163
164
  tests/toolkit/file_test.py
164
165
  tests/toolkit/image_test.py
166
+ tests/toolkit/test_types.py
165
167
  tests/toolkit/utils/retry.py
166
168
  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.0'
16
+ __version_tuple__ = version_tuple = (1, 7, 0)
@@ -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,6 +2,8 @@ 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
 
@@ -11,13 +13,56 @@ from fal.console.icons import CHECK_ICON
11
13
  from fal.exceptions.auth import UnauthenticatedException
12
14
 
13
15
 
16
+ class GoogleColabState:
17
+ def __init__(self):
18
+ self.is_checked = False
19
+ self.lock = Lock()
20
+ self.secret: Optional[str] = None
21
+
22
+
23
+ _colab_state = GoogleColabState()
24
+
25
+
26
+ def is_google_colab() -> bool:
27
+ try:
28
+ from IPython import get_ipython
29
+
30
+ return "google.colab" in str(get_ipython())
31
+ except ModuleNotFoundError:
32
+ return False
33
+ except NameError:
34
+ return False
35
+
36
+
37
+ def get_colab_token() -> Optional[str]:
38
+ if not is_google_colab():
39
+ return None
40
+ with _colab_state.lock:
41
+ if _colab_state.is_checked: # request access only once
42
+ return _colab_state.secret
43
+
44
+ try:
45
+ from google.colab import userdata # noqa: I001
46
+ except ImportError:
47
+ return None
48
+
49
+ try:
50
+ token = userdata.get("FAL_KEY")
51
+ _colab_state.secret = token.strip()
52
+ except Exception:
53
+ _colab_state.secret = None
54
+
55
+ _colab_state.is_checked = True
56
+ return _colab_state.secret
57
+
58
+
14
59
  def key_credentials() -> tuple[str, str] | None:
15
60
  # Ignore key credentials when the user forces auth by user.
16
61
  if os.environ.get("FAL_FORCE_AUTH_BY_USER") == "1":
17
62
  return None
18
63
 
19
- if "FAL_KEY" in os.environ:
20
- key = os.environ["FAL_KEY"]
64
+ key = os.environ.get("FAL_KEY") or get_colab_token()
65
+ if key:
21
66
  key_id, key_secret = key.split(":", 1)
22
67
  return (key_id, key_secret)
23
68
  elif "FAL_KEY_ID" in os.environ and "FAL_KEY_SECRET" in os.environ:
@@ -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):
@@ -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]
@@ -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
File without changes
File without changes
File without changes
File without changes