fal 1.16.0__tar.gz → 1.16.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.
- {fal-1.16.0/fal.egg-info → fal-1.16.1}/PKG-INFO +1 -1
- {fal-1.16.0 → fal-1.16.1/fal.egg-info}/PKG-INFO +1 -1
- {fal-1.16.0 → fal-1.16.1}/fal.egg-info/SOURCES.txt +1 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/_fal_version.py +2 -2
- fal-1.16.1/src/fal/_version.py +89 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/api.py +7 -7
- {fal-1.16.0 → fal-1.16.1}/src/fal/app.py +6 -9
- {fal-1.16.0 → fal-1.16.1}/src/fal/auth/__init__.py +7 -2
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/main.py +37 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/profile.py +1 -1
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/runners.py +6 -2
- {fal-1.16.0 → fal-1.16.1}/src/fal/config.py +1 -1
- fal-1.16.1/src/fal/toolkit/utils/endpoint.py +29 -0
- {fal-1.16.0 → fal-1.16.1}/tests/test_apps.py +82 -8
- fal-1.16.0/src/fal/_version.py +0 -6
- {fal-1.16.0 → fal-1.16.1}/.gitignore +0 -0
- {fal-1.16.0 → fal-1.16.1}/Makefile +0 -0
- {fal-1.16.0 → fal-1.16.1}/README.md +0 -0
- {fal-1.16.0 → fal-1.16.1}/docs/conf.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/docs/index.rst +0 -0
- {fal-1.16.0 → fal-1.16.1}/fal.egg-info/dependency_links.txt +0 -0
- {fal-1.16.0 → fal-1.16.1}/fal.egg-info/entry_points.txt +0 -0
- {fal-1.16.0 → fal-1.16.1}/fal.egg-info/requires.txt +0 -0
- {fal-1.16.0 → fal-1.16.1}/fal.egg-info/top_level.txt +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/README.md +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/pyproject.toml +0 -0
- {fal-1.16.0 → fal-1.16.1}/openapi_rest.config.yaml +0 -0
- {fal-1.16.0 → fal-1.16.1}/pyproject.toml +0 -0
- {fal-1.16.0 → fal-1.16.1}/setup.cfg +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/__main__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/_serialization.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/apps.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/auth/auth0.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/auth/local.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/_utils.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/api.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/apps.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/auth.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/cli_nested_json.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/create.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/debug.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/deploy.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/doctor.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/files.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/keys.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/parser.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/run.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/secrets.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/cli/teams.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/console/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/console/icons.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/console/ux.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/container.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/exceptions/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/exceptions/_base.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/exceptions/_cuda.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/exceptions/auth.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/files.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/flags.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/logging/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/logging/isolate.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/logging/style.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/logging/trace.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/logging/user.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/project.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/py.typed +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/rest_client.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/sdk.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/sync.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/exceptions.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/file/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/file/file.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/file/providers/fal.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/file/providers/gcp.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/file/providers/r2.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/file/providers/s3.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/file/types.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/image/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/image/image.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/image/nsfw_filter/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/image/nsfw_filter/env.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/image/nsfw_filter/inference.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/image/nsfw_filter/model.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/image/nsfw_filter/requirements.txt +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/image/safety_checker.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/optimize.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/types.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/utils/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/utils/download_utils.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/toolkit/utils/retry.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/utils.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/src/fal/workflows.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/assets/cat.png +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/cli/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/cli/test_apps.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/cli/test_auth.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/cli/test_deploy.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/cli/test_keys.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/cli/test_run.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/cli/test_secrets.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/conftest.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/integration_test.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/mainify_package/__init__.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/mainify_package/impl.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/mainify_package/utils.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/mainify_target.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/test_stability.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/toolkit/file_test.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/toolkit/image_test.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/toolkit/test_types.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tests/toolkit/utils/retry.py +0 -0
- {fal-1.16.0 → fal-1.16.1}/tools/demo_script.py +0 -0
|
@@ -150,6 +150,7 @@ src/fal/toolkit/image/nsfw_filter/model.py
|
|
|
150
150
|
src/fal/toolkit/image/nsfw_filter/requirements.txt
|
|
151
151
|
src/fal/toolkit/utils/__init__.py
|
|
152
152
|
src/fal/toolkit/utils/download_utils.py
|
|
153
|
+
src/fal/toolkit/utils/endpoint.py
|
|
153
154
|
src/fal/toolkit/utils/retry.py
|
|
154
155
|
tests/__init__.py
|
|
155
156
|
tests/conftest.py
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from ._fal_version import version as __version__ # type: ignore[import]
|
|
8
|
+
from ._fal_version import version_tuple # type: ignore[import]
|
|
9
|
+
except ImportError:
|
|
10
|
+
__version__ = "UNKNOWN"
|
|
11
|
+
version_tuple = (0, 0, __version__) # type: ignore[assignment]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_PYPI_URL = "https://pypi.org/pypi/fal/json"
|
|
15
|
+
_PYPI_CACHE_TTL = 60 * 60 # 1 hour
|
|
16
|
+
_PYPI_CACHE_PATH = os.path.expanduser("~/.fal/cache/pypi.json")
|
|
17
|
+
_URLOPEN_TIMEOUT = 1
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _write_pypi_cache(data: Dict[str, Any]) -> None:
|
|
21
|
+
cache_dir = os.path.dirname(_PYPI_CACHE_PATH)
|
|
22
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
23
|
+
prefix = os.path.basename(_PYPI_CACHE_PATH) + ".tmp."
|
|
24
|
+
with tempfile.NamedTemporaryFile(
|
|
25
|
+
mode="w",
|
|
26
|
+
dir=cache_dir,
|
|
27
|
+
prefix=prefix,
|
|
28
|
+
delete=False,
|
|
29
|
+
) as fobj:
|
|
30
|
+
fobj.write(json.dumps(data))
|
|
31
|
+
os.rename(fobj.name, _PYPI_CACHE_PATH)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_pypi_cache() -> Optional[Dict[str, Any]]:
|
|
35
|
+
import time
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
mtime = os.path.getmtime(_PYPI_CACHE_PATH)
|
|
39
|
+
except FileNotFoundError:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
if mtime + _PYPI_CACHE_TTL < time.time():
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
with open(_PYPI_CACHE_PATH) as fobj:
|
|
46
|
+
try:
|
|
47
|
+
return json.load(fobj)
|
|
48
|
+
except ValueError:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _fetch_pypi_data() -> Dict[str, Any]:
|
|
53
|
+
from urllib.request import urlopen
|
|
54
|
+
|
|
55
|
+
response = urlopen(_PYPI_URL, timeout=_URLOPEN_TIMEOUT)
|
|
56
|
+
if response.status != 200:
|
|
57
|
+
raise Exception(f"Failed to fetch {_PYPI_URL}")
|
|
58
|
+
|
|
59
|
+
data = response.read()
|
|
60
|
+
return json.loads(data)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_latest_version() -> str:
|
|
64
|
+
from fal.logging import get_logger
|
|
65
|
+
|
|
66
|
+
logger = get_logger(__name__)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
data = _get_pypi_cache()
|
|
70
|
+
except Exception:
|
|
71
|
+
logger.warning("Failed to get pypi cache", exc_info=True)
|
|
72
|
+
data = None
|
|
73
|
+
|
|
74
|
+
if data is None:
|
|
75
|
+
try:
|
|
76
|
+
data = _fetch_pypi_data()
|
|
77
|
+
except Exception:
|
|
78
|
+
logger.warning("Failed to get latest fal version", exc_info=True)
|
|
79
|
+
data = {}
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
_write_pypi_cache(data)
|
|
83
|
+
except Exception:
|
|
84
|
+
logger.warning("Failed to write pypi cache", exc_info=True)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
return data["info"]["version"]
|
|
88
|
+
except KeyError:
|
|
89
|
+
return "0.0.0"
|
|
@@ -501,12 +501,7 @@ class FalServerlessHost(Host):
|
|
|
501
501
|
if isinstance(func, ServeWrapper):
|
|
502
502
|
# Assigning in a separate property leaving a place for the user
|
|
503
503
|
# to add more metadata in the future
|
|
504
|
-
|
|
505
|
-
metadata["openapi"] = func.openapi()
|
|
506
|
-
except Exception as e:
|
|
507
|
-
print(
|
|
508
|
-
f"[warning] Failed to generate OpenAPI metadata for function: {e}"
|
|
509
|
-
)
|
|
504
|
+
metadata["openapi"] = func.openapi()
|
|
510
505
|
|
|
511
506
|
for partial_result in self._connection.register(
|
|
512
507
|
partial_func,
|
|
@@ -1169,7 +1164,12 @@ class BaseServable:
|
|
|
1169
1164
|
Build the OpenAPI specification for the served function.
|
|
1170
1165
|
Attach needed metadata for a better integration to fal.
|
|
1171
1166
|
"""
|
|
1172
|
-
|
|
1167
|
+
try:
|
|
1168
|
+
return self._build_app().openapi()
|
|
1169
|
+
except Exception as e:
|
|
1170
|
+
raise FalServerlessException(
|
|
1171
|
+
"Failed to generate OpenAPI metadata for function"
|
|
1172
|
+
) from e
|
|
1173
1173
|
|
|
1174
1174
|
def serve(self) -> None:
|
|
1175
1175
|
import asyncio
|
|
@@ -105,15 +105,12 @@ def wrap_app(cls: type[App], **kwargs) -> IsolatedFunction:
|
|
|
105
105
|
app.serve()
|
|
106
106
|
|
|
107
107
|
metadata = {}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
else:
|
|
115
|
-
routes = app.collect_routes()
|
|
116
|
-
realtime_app = any(route.is_websocket for route in routes)
|
|
108
|
+
app = cls(_allow_init=True)
|
|
109
|
+
|
|
110
|
+
metadata["openapi"] = app.openapi()
|
|
111
|
+
|
|
112
|
+
routes = app.collect_routes()
|
|
113
|
+
realtime_app = any(route.is_websocket for route in routes)
|
|
117
114
|
|
|
118
115
|
kind = cls.host_kwargs.pop("kind", "virtualenv")
|
|
119
116
|
if kind == "container":
|
|
@@ -63,8 +63,13 @@ def key_credentials() -> tuple[str, str] | None:
|
|
|
63
63
|
|
|
64
64
|
key = os.environ.get("FAL_KEY") or config.get("key") or get_colab_token()
|
|
65
65
|
if key:
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
try:
|
|
67
|
+
key_id, key_secret = key.split(":", 1)
|
|
68
|
+
return (key_id, key_secret)
|
|
69
|
+
except ValueError:
|
|
70
|
+
print(f"Invalid key format: {key}")
|
|
71
|
+
return None
|
|
72
|
+
|
|
68
73
|
elif "FAL_KEY_ID" in os.environ and "FAL_KEY_SECRET" in os.environ:
|
|
69
74
|
return (os.environ["FAL_KEY_ID"], os.environ["FAL_KEY_SECRET"])
|
|
70
75
|
else:
|
|
@@ -77,11 +77,48 @@ def _print_error(msg):
|
|
|
77
77
|
console.print(f"{CROSS_ICON} {msg}")
|
|
78
78
|
|
|
79
79
|
|
|
80
|
+
def _check_latest_version():
|
|
81
|
+
from packaging.version import parse
|
|
82
|
+
from rich.emoji import Emoji
|
|
83
|
+
from rich.panel import Panel
|
|
84
|
+
from rich.text import Text
|
|
85
|
+
|
|
86
|
+
from fal._version import get_latest_version, version_tuple
|
|
87
|
+
|
|
88
|
+
latest_version = get_latest_version()
|
|
89
|
+
parsed = parse(latest_version)
|
|
90
|
+
latest_version_tuple = (parsed.major, parsed.minor, parsed.micro)
|
|
91
|
+
if latest_version_tuple <= version_tuple:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
if not console.is_terminal:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
line1 = Text.assemble(
|
|
98
|
+
(Emoji.replace(":warning-emoji: "), "bold white"),
|
|
99
|
+
("A new version of fal is available: ", "bold white"),
|
|
100
|
+
(latest_version, "bold green"),
|
|
101
|
+
)
|
|
102
|
+
line2 = Text.assemble(("pip install --upgrade fal", "bold cyan"))
|
|
103
|
+
line2.align("center", width=len(line1))
|
|
104
|
+
|
|
105
|
+
panel = Panel(
|
|
106
|
+
line1 + "\n\n" + line2,
|
|
107
|
+
border_style="yellow",
|
|
108
|
+
padding=(1, 2),
|
|
109
|
+
highlight=True,
|
|
110
|
+
expand=False,
|
|
111
|
+
)
|
|
112
|
+
console.print(panel)
|
|
113
|
+
|
|
114
|
+
|
|
80
115
|
def main(argv=None) -> int:
|
|
81
116
|
import grpc
|
|
82
117
|
|
|
83
118
|
from fal.api import UserFunctionException
|
|
84
119
|
|
|
120
|
+
_check_latest_version()
|
|
121
|
+
|
|
85
122
|
ret = 1
|
|
86
123
|
try:
|
|
87
124
|
args = parse_args(argv)
|
|
@@ -21,17 +21,21 @@ def runners_table(runners: List[RunnerInfo]):
|
|
|
21
21
|
table.add_column("Revision")
|
|
22
22
|
|
|
23
23
|
for runner in runners:
|
|
24
|
+
external_metadata = runner.external_metadata
|
|
25
|
+
present = external_metadata.get("present_in_group", True)
|
|
26
|
+
|
|
24
27
|
num_leases_with_request = len(
|
|
25
28
|
[
|
|
26
29
|
lease
|
|
27
|
-
for lease in
|
|
30
|
+
for lease in external_metadata.get("leases", [])
|
|
28
31
|
if lease.get("request_id") is not None
|
|
29
32
|
]
|
|
30
33
|
)
|
|
31
34
|
|
|
32
35
|
table.add_row(
|
|
33
36
|
runner.alias,
|
|
34
|
-
|
|
37
|
+
# Mark lost runners in red
|
|
38
|
+
runner.runner_id if present else f"[red]{runner.runner_id}[/]",
|
|
35
39
|
str(runner.in_flight_requests),
|
|
36
40
|
str(runner.in_flight_requests - num_leases_with_request),
|
|
37
41
|
(
|
|
@@ -99,7 +99,7 @@ class Config:
|
|
|
99
99
|
def unset_internal(self, key: str) -> None:
|
|
100
100
|
self._config.get(SETTINGS_SECTION, {}).pop(key, None)
|
|
101
101
|
|
|
102
|
-
def
|
|
102
|
+
def delete_profile(self, profile: str) -> None:
|
|
103
103
|
del self._config[profile]
|
|
104
104
|
|
|
105
105
|
@contextmanager
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager
|
|
2
|
+
|
|
3
|
+
from anyio import create_task_group
|
|
4
|
+
from fastapi import Request
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@asynccontextmanager
|
|
8
|
+
async def cancel_on_disconnect(request: Request):
|
|
9
|
+
"""
|
|
10
|
+
Async context manager for async code that needs to be cancelled if client
|
|
11
|
+
disconnects prematurely.
|
|
12
|
+
The client disconnect is monitored through the Request object.
|
|
13
|
+
"""
|
|
14
|
+
async with create_task_group() as tg:
|
|
15
|
+
|
|
16
|
+
async def watch_disconnect():
|
|
17
|
+
while True:
|
|
18
|
+
message = await request.receive()
|
|
19
|
+
|
|
20
|
+
if message["type"] == "http.disconnect":
|
|
21
|
+
tg.cancel_scope.cancel()
|
|
22
|
+
break
|
|
23
|
+
|
|
24
|
+
tg.start_soon(watch_disconnect)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
yield
|
|
28
|
+
finally:
|
|
29
|
+
tg.cancel_scope.cancel()
|
|
@@ -10,7 +10,7 @@ from typing import Generator, List, Tuple
|
|
|
10
10
|
|
|
11
11
|
import httpx
|
|
12
12
|
import pytest
|
|
13
|
-
from fastapi import WebSocket
|
|
13
|
+
from fastapi import Request, WebSocket
|
|
14
14
|
from httpx import HTTPStatusError
|
|
15
15
|
from isolate.backends.common import active_python
|
|
16
16
|
from openapi_fal_rest.api.applications import app_metadata
|
|
@@ -23,9 +23,15 @@ from fal import apps
|
|
|
23
23
|
from fal.app import AppClient, AppClientError
|
|
24
24
|
from fal.cli.deploy import User, _get_user
|
|
25
25
|
from fal.container import ContainerImage
|
|
26
|
-
from fal.exceptions import
|
|
26
|
+
from fal.exceptions import (
|
|
27
|
+
AppException,
|
|
28
|
+
FalServerlessException,
|
|
29
|
+
FieldException,
|
|
30
|
+
RequestCancelledException,
|
|
31
|
+
)
|
|
27
32
|
from fal.exceptions._cuda import _CUDA_OOM_MESSAGE, _CUDA_OOM_STATUS_CODE
|
|
28
33
|
from fal.rest_client import REST_CLIENT
|
|
34
|
+
from fal.toolkit.utils.endpoint import cancel_on_disconnect
|
|
29
35
|
from fal.workflows import Workflow
|
|
30
36
|
|
|
31
37
|
|
|
@@ -228,13 +234,17 @@ class ExceptionApp(fal.App, keep_alive=300, max_concurrency=1):
|
|
|
228
234
|
raise RuntimeError("cuDNN error: CUDNN_STATUS_INTERNAL_ERROR")
|
|
229
235
|
|
|
230
236
|
|
|
231
|
-
class CancellableApp(fal.App, keep_alive=300, max_concurrency=1):
|
|
237
|
+
class CancellableApp(fal.App, keep_alive=300, max_concurrency=1, request_timeout=10):
|
|
232
238
|
task = None
|
|
239
|
+
running = 0
|
|
240
|
+
|
|
241
|
+
async def _sleep(self, input: Input):
|
|
242
|
+
if self.running > 0:
|
|
243
|
+
raise Exception("App is already running")
|
|
233
244
|
|
|
234
|
-
@fal.endpoint("/")
|
|
235
|
-
async def sleep(self, input: Input) -> Output:
|
|
236
245
|
self.task = asyncio.create_task(asyncio.sleep(input.wait_time))
|
|
237
246
|
try:
|
|
247
|
+
self.running += 1
|
|
238
248
|
await self.task
|
|
239
249
|
except asyncio.CancelledError:
|
|
240
250
|
print("Task was cancelled")
|
|
@@ -244,9 +254,20 @@ class CancellableApp(fal.App, keep_alive=300, max_concurrency=1):
|
|
|
244
254
|
await self.task
|
|
245
255
|
|
|
246
256
|
raise RequestCancelledException("Request cancelled by the client.")
|
|
247
|
-
|
|
257
|
+
finally:
|
|
258
|
+
self.task = None
|
|
259
|
+
self.running -= 1
|
|
248
260
|
return Output(result=input.lhs + input.rhs)
|
|
249
261
|
|
|
262
|
+
@fal.endpoint("/")
|
|
263
|
+
async def sleep(self, input: Input) -> Output:
|
|
264
|
+
return await self._sleep(input)
|
|
265
|
+
|
|
266
|
+
@fal.endpoint("/well-handled")
|
|
267
|
+
async def well_handled(self, input: Input, request: Request) -> Output:
|
|
268
|
+
async with cancel_on_disconnect(request):
|
|
269
|
+
return await self._sleep(input)
|
|
270
|
+
|
|
250
271
|
@fal.endpoint("/cancel")
|
|
251
272
|
async def cancel_handler(self) -> Output:
|
|
252
273
|
if self.task:
|
|
@@ -301,6 +322,14 @@ class RealtimeApp(fal.App, keep_alive=300, max_concurrency=1):
|
|
|
301
322
|
return RTOutputs(texts=[input.prompt] + [i.prompt for i in inputs])
|
|
302
323
|
|
|
303
324
|
|
|
325
|
+
class BrokenApp(fal.App, keep_alive=300, max_concurrency=1):
|
|
326
|
+
machine_type = "S"
|
|
327
|
+
|
|
328
|
+
@fal.endpoint("/")
|
|
329
|
+
def broken(self) -> Exception:
|
|
330
|
+
raise Exception("this app is designed to fail")
|
|
331
|
+
|
|
332
|
+
|
|
304
333
|
@pytest.fixture(scope="module")
|
|
305
334
|
def host() -> Generator[api.FalServerlessHost, None, None]:
|
|
306
335
|
yield addition_app.host
|
|
@@ -400,6 +429,13 @@ def test_realtime_app(host: api.FalServerlessHost, user: User):
|
|
|
400
429
|
yield f"{user.username}/{app_alias}"
|
|
401
430
|
|
|
402
431
|
|
|
432
|
+
def test_broken_app_failure(host: api.FalServerlessHost, user: User):
|
|
433
|
+
with pytest.raises(FalServerlessException) as e:
|
|
434
|
+
fal.wrap_app(BrokenApp)
|
|
435
|
+
|
|
436
|
+
assert "Failed to generate OpenAPI" in str(e)
|
|
437
|
+
|
|
438
|
+
|
|
403
439
|
def test_app_client(test_app: str, test_nomad_app: str):
|
|
404
440
|
response = apps.run(test_app, arguments={"lhs": 1, "rhs": 2})
|
|
405
441
|
assert response["result"] == 3
|
|
@@ -467,7 +503,7 @@ def test_stateful_app_client(test_stateful_app: str):
|
|
|
467
503
|
|
|
468
504
|
def test_app_cancellation(test_app: str, test_cancellable_app: str):
|
|
469
505
|
request_handle = apps.submit(
|
|
470
|
-
test_cancellable_app, arguments={"lhs": 1, "rhs": 2, "wait_time":
|
|
506
|
+
test_cancellable_app, arguments={"lhs": 1, "rhs": 2, "wait_time": 5}
|
|
471
507
|
)
|
|
472
508
|
|
|
473
509
|
while True:
|
|
@@ -487,7 +523,7 @@ def test_app_cancellation(test_app: str, test_cancellable_app: str):
|
|
|
487
523
|
|
|
488
524
|
# normal app should just ignore the cancellation
|
|
489
525
|
request_handle = apps.submit(
|
|
490
|
-
test_app, arguments={"lhs": 1, "rhs": 2, "wait_time":
|
|
526
|
+
test_app, arguments={"lhs": 1, "rhs": 2, "wait_time": 5}
|
|
491
527
|
)
|
|
492
528
|
|
|
493
529
|
while True:
|
|
@@ -504,6 +540,44 @@ def test_app_cancellation(test_app: str, test_cancellable_app: str):
|
|
|
504
540
|
assert response == {"result": 3}
|
|
505
541
|
|
|
506
542
|
|
|
543
|
+
def test_app_disconnect_behavior(test_app: str, test_cancellable_app: str):
|
|
544
|
+
with pytest.raises(HTTPStatusError) as e:
|
|
545
|
+
apps.run(
|
|
546
|
+
test_cancellable_app,
|
|
547
|
+
arguments={"lhs": 1, "rhs": 2, "wait_time": 20},
|
|
548
|
+
path="/well-handled",
|
|
549
|
+
)
|
|
550
|
+
assert (
|
|
551
|
+
e.value.response.status_code == 504
|
|
552
|
+
), "Expected Gateway Timeout even though the app handled it"
|
|
553
|
+
|
|
554
|
+
# and running it again shows the app "handled" it
|
|
555
|
+
response = apps.run(
|
|
556
|
+
test_cancellable_app,
|
|
557
|
+
arguments={"lhs": 1, "rhs": 2, "wait_time": 1},
|
|
558
|
+
path="/well-handled",
|
|
559
|
+
)
|
|
560
|
+
assert response == {"result": 3}
|
|
561
|
+
|
|
562
|
+
# vs on an unhandled one
|
|
563
|
+
|
|
564
|
+
with pytest.raises(HTTPStatusError) as e:
|
|
565
|
+
apps.run(
|
|
566
|
+
test_cancellable_app,
|
|
567
|
+
arguments={"lhs": 1, "rhs": 2, "wait_time": 20},
|
|
568
|
+
)
|
|
569
|
+
assert (
|
|
570
|
+
e.value.response.status_code == 504
|
|
571
|
+
), "Expected Gateway Timeout even though the app handled it"
|
|
572
|
+
|
|
573
|
+
with pytest.raises(HTTPStatusError) as e:
|
|
574
|
+
apps.run(
|
|
575
|
+
test_cancellable_app,
|
|
576
|
+
arguments={"lhs": 1, "rhs": 2, "wait_time": 1},
|
|
577
|
+
)
|
|
578
|
+
assert e.value.response.status_code == 500
|
|
579
|
+
|
|
580
|
+
|
|
507
581
|
@pytest.mark.xfail(
|
|
508
582
|
reason="Temporary disabled while investigating backend issue. Ping @efiop"
|
|
509
583
|
)
|
fal-1.16.0/src/fal/_version.py
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py
RENAMED
|
File without changes
|
{fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
{fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py
RENAMED
|
File without changes
|
{fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py
RENAMED
|
File without changes
|
{fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py
RENAMED
|
File without changes
|
{fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py
RENAMED
|
File without changes
|
|
File without changes
|
{fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.16.0 → fal-1.16.1}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|