* feat: v2.1.0 — fal platform utilities (async fan-out, cost tools, recovery) Six new nodes under FAL/Platform, built on fal's queue/request-id/pricing primitives: - Fal Submit + Fal Collect: queue jobs without blocking and collect results later — N generations run in parallel inside one graph - Fal Result by Request ID: re-fetch any past generation from fal's queue without re-generating or re-billing - Fal Cost Estimator: per-run cost report parsed from registry pricing before anything is queued - Fal Session Costs: in-memory ledger of every fal call this session (endpoint, duration, request id, est. cost) with running total - Fal Save Media from URL: persist expiring CDN results into the ComfyUI output directory Core: utils/pricing.py (pricing-text parser + registry lookup), utils/ledger.py (thread-safe session ledger), api.py gains submit_only, result_from_request_id, request-id capture via on_enqueue, and automatic ledger recording; any_endpoint helpers extracted for reuse. 16 new tests (61 total). * feat: v2.2.0 — persistent result cache, spend guard, URL passthrough Three fal-platform features on top of the 2.1 platform pack: - Persistent result cache (sqlite, [cache] config): identical fal calls are served free from disk across ComfyUI restarts; uploads are deduplicated by content hash so identical inputs upload once. Bypass per node via force_rerun. Cache hits are never blocked by the spend guard and never recorded as spend. - Spend Guard + billing ([spend_guard] config): preflight refuses to submit once the session's estimated spend reaches session_budget_usd or the account balance (fal Billing API) falls below min_balance_usd. New Fal Account Balance node; ledger reconciliation against the aggregated usage API (fal exposes no per-request billed amounts). - fal-CDN URL passthrough: every dynamic node's media inputs gain optional *_direct_url twins (URL sent directly, no download/re-upload; 1,405 twins across 992 nodes) and image-kind nodes output image_urls alongside IMAGE — fal→fal chains with zero local I/O. Review fixes: path traversal blocked in FalSaveMediaURL (confined to the output dir, atomic O_EXCL filename claim), FalAnyEndpoint force_rerun now bypasses the cache, cache lookup ordered before spend-guard preflight, result recovery no longer counts as new session spend. 14 new tests (76 total).
70 lines
1.6 KiB
Python
70 lines
1.6 KiB
Python
"""Shared fixtures: load the pack exactly like ComfyUI does (hyphenated dir)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import importlib.util
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
PKG = "ComfyUI_fal_API"
|
|
|
|
# keep the persistent result cache out of the user's real cache dir during tests
|
|
os.environ.setdefault(
|
|
"COMFYUI_FAL_API_CACHE_DB",
|
|
str(Path(tempfile.mkdtemp(prefix="fal-api-test-cache-")) / "cache.db"),
|
|
)
|
|
|
|
|
|
def _load_package():
|
|
if PKG in sys.modules:
|
|
return sys.modules[PKG]
|
|
spec = importlib.util.spec_from_file_location(
|
|
PKG, ROOT / "__init__.py", submodule_search_locations=[str(ROOT)]
|
|
)
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[PKG] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def pack():
|
|
"""The fully loaded node pack (static + dynamic mappings)."""
|
|
return _load_package()
|
|
|
|
|
|
def _submodule(name: str):
|
|
_load_package()
|
|
return importlib.import_module(f"{PKG}.{name}")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def schema_to_inputs():
|
|
return _submodule("nodes.dynamic.schema_to_inputs")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def arguments_mod():
|
|
return _submodule("nodes.dynamic.arguments")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def outputs_mod():
|
|
return _submodule("nodes.dynamic.outputs")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def factory_mod():
|
|
return _submodule("nodes.dynamic.factory")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def errors_mod():
|
|
return _submodule("nodes.utils.errors")
|