feat(bundles): consolidate FAISS, Notion into lfx-bundles (lowercase-slug support)

Uppercase source dirs need lowercase bundle slugs (BUNDLE_NAME_RE is
lowercase-only). Enhance consolidate_bundles.py with bundle_slug(provider) =
provider.lower(): the bundle dir, ext id, shim target, and extra key use the
lowercased slug, while the migration import_path keeps the historical
lfx.components.<Provider> casing so saved flows still resolve. Identity for
already-lowercase providers (no change to prior entries).

- FAISS  -> lfx_bundles/faiss  (faiss-cpu[py-split] + langchain-community)  1 component
- Notion -> lfx_bundles/notion (requests + Markdown)  8 components

test_bundle_shims.py: derive the expected bundle name by lowercasing the shim
dir name (FAISS shim aliases lfx_bundles.faiss) — 159 shim tests pass.
Relocate the lfx-isolated FAISS test to backend/vectorstores (repointed to
lfx_bundles.faiss.faiss). 36 migration entries (0 ambiguous); index 22->20
categories, 140->131 components. SKIP=detect-secrets: regenerated code_hash hex.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eric Hare
2026-06-16 13:03:59 -07:00
parent 594e4db2fc
commit f95fb0603d
21 changed files with 354 additions and 1652 deletions

View File

@ -158,6 +158,14 @@ PROVIDER_DEPS: dict[str, list[str]] = {
# --- tranche 9: langwatch evaluator (pure httpx REST; the langwatch SDK extra
# is for the tracing service, not this component) ---
"langwatch": [],
# --- tranche 10: uppercase source dirs -> lowercase bundle slug (FAISS->faiss,
# Notion->notion via bundle_slug(); import paths keep historical casing) ---
"FAISS": [
"faiss-cpu==1.9.0.post1; python_version < '3.14'",
"faiss-cpu>=1.13.2; python_version >= '3.14'",
_LC_COMMUNITY,
],
"Notion": ["requests>=2.32.0", "Markdown>=3.8.0"],
}
_OPTIONAL_DEPS_HEADER = (
@ -201,7 +209,7 @@ def discover_component_classes(provider_dir: Path) -> list[tuple[str, str]]:
return found
def _shim_source(provider: str) -> str:
def _shim_source(provider: str, slug: str) -> str:
return (
"# lfx-bundles-shim\n"
f'"""Compatibility shim: lfx.components.{provider} moved to lfx-bundles.\n'
@ -215,7 +223,7 @@ def _shim_source(provider: str) -> str:
"import sys\n"
"\n"
"try:\n"
f' sys.modules[__name__] = importlib.import_module("lfx_bundles.{provider}")\n'
f' sys.modules[__name__] = importlib.import_module("lfx_bundles.{slug}")\n'
"except ModuleNotFoundError as exc:\n"
' if exc.name is not None and (exc.name == "lfx_bundles" or exc.name.startswith("lfx_bundles.")):\n'
" msg = (\n"
@ -228,15 +236,28 @@ def _shim_source(provider: str) -> str:
)
def _rewrite_self_imports(text: str, provider: str) -> str:
"""Rewrite absolute ``lfx.components.<provider>`` self-refs to ``lfx_bundles.<provider>``."""
return re.sub(rf"\blfx\.components\.{re.escape(provider)}\b", f"lfx_bundles.{provider}", text)
def _rewrite_self_imports(text: str, provider: str, slug: str) -> str:
"""Rewrite absolute ``lfx.components.<provider>`` self-refs to ``lfx_bundles.<slug>``."""
return re.sub(rf"\blfx\.components\.{re.escape(provider)}\b", f"lfx_bundles.{slug}", text)
def bundle_slug(provider: str) -> str:
"""Bundle directory / ext-slug name for a provider.
Bundle names must satisfy ``BUNDLE_NAME_RE`` (lowercase), so the in-tree
source dir name is lowercased -- e.g. ``FAISS`` -> ``faiss``, ``Notion`` ->
``notion``. Already-lowercase providers are unchanged. The historical
``lfx.components.<provider>`` import paths (migration table) keep the
original casing; only the bundle dir + ``ext:<slug>`` id are lowercased.
"""
return provider.lower()
def move_provider(provider: str, *, apply: bool) -> list[tuple[str, str]]:
"""Move one provider into the metapackage and leave a shim. Returns its classes."""
slug = bundle_slug(provider)
src = COMPONENTS_DIR / provider
dst = BUNDLES_PKG / provider
dst = BUNDLES_PKG / slug
if not src.is_dir():
msg = f"provider directory not found: {src}"
raise SystemExit(msg)
@ -248,19 +269,19 @@ def move_provider(provider: str, *, apply: bool) -> list[tuple[str, str]]:
py_files = sorted(src.glob("*.py"))
print(f" {provider}: {len(py_files)} file(s), {len(classes)} component class(es) -> {dst.relative_to(REPO_ROOT)}")
for module_stem, class_name in classes:
print(f" {module_stem}.{class_name} -> ext:{provider}:{class_name}@official")
print(f" {module_stem}.{class_name} -> ext:{slug}:{class_name}@official")
if not apply:
return classes
dst.mkdir(parents=True)
for py in py_files:
content = _rewrite_self_imports(py.read_text(encoding="utf-8"), provider)
content = _rewrite_self_imports(py.read_text(encoding="utf-8"), provider, slug)
(dst / py.name).write_text(content, encoding="utf-8")
# Remove the moved source, then replace the in-tree dir with a one-file shim.
shutil.rmtree(src)
src.mkdir()
(src / "__init__.py").write_text(_shim_source(provider), encoding="utf-8")
(src / "__init__.py").write_text(_shim_source(provider, slug), encoding="utf-8")
return classes
@ -324,8 +345,11 @@ def append_migration_entries(plan: dict[str, list[tuple[str, str]]], *, apply: b
new_entries: list[dict] = []
new_ambiguous: list[dict] = []
for provider, classes in plan.items():
# ext id uses the lowercase bundle slug; import paths keep the historical
# (possibly mixed-case) ``lfx.components.<provider>`` form for migration.
slug = bundle_slug(provider)
for module_stem, class_name in classes:
target = f"ext:{provider}:{class_name}@official"
target = f"ext:{slug}:{class_name}@official"
prior = existing_bare.get(class_name)
if class_name in already_ambiguous:
print(f" ! {class_name!r} already ambiguous; import-path entries only")
@ -337,7 +361,7 @@ def append_migration_entries(plan: dict[str, list[tuple[str, str]]], *, apply: b
existing_bare[class_name] = target
new_entries.append(_entry("import_path", f"lfx.components.{provider}.{module_stem}.{class_name}", target))
new_entries.append(_entry("import_path", f"lfx.components.{provider}.{class_name}", target))
new_entries.append(_entry("legacy_slot", f"ext:{provider}:{class_name}@official-pre-a", target))
new_entries.append(_entry("legacy_slot", f"ext:{slug}:{class_name}@official-pre-a", target))
# Idempotency guard: a partially-failed earlier run (provider dir moved
# but the table already written, or vice versa) must not duplicate rows on

View File

@ -4,8 +4,8 @@ import pytest
pytest.importorskip("langchain_community")
from lfx.components.FAISS.faiss import FaissVectorStoreComponent
from lfx.io import BoolInput
from lfx_bundles.faiss.faiss import FaissVectorStoreComponent
class TestFaissVectorStoreComponentDefaults:

View File

@ -47,6 +47,7 @@ cuga = ["cuga>=0.2.20,<0.3.0; sys_platform != 'darwin' and python_version < '3.1
deepseek = ["langchain-openai>=1.1.6", "openai>=1.68.2,<3.0.0", "requests>=2.32.0"]
elastic = ["elasticsearch~=8.19", "langchain-elasticsearch~=1.0.0", "opensearch-py==2.8.0"]
exa = ["metaphor-python==0.1.23"]
faiss = ["faiss-cpu==1.9.0.post1; python_version < '3.14'", "faiss-cpu>=1.13.2; python_version >= '3.14'", "langchain-community>=0.4.1,<1.0.0"]
firecrawl = ["firecrawl-py>=1.0.16,<2.0.0"]
git = ["GitPython>=3.1.50", "langchain-community>=0.4.1,<1.0.0"]
glean = []
@ -65,6 +66,7 @@ milvus = ["langchain-milvus~=0.3.2"]
mistral = ["langchain-mistralai~=1.1.1"]
mongodb = ["pymongo>=4.10.1", "langchain-mongodb>=0.11.0"]
needle = ["needle-python>=0.4.0", "langchain-community>=0.4.1,<1.0.0"]
notion = ["requests>=2.32.0", "Markdown>=3.8.0"]
novita = ["langchain-openai>=1.1.6", "requests>=2.32.0"]
nvidia = ["langchain-nvidia-ai-endpoints~=1.0.0", "nv-ingest-client>=26.1.0,<27.0.0 ; python_version >= '3.12'", "gassist>=0.0.1; sys_platform == 'win32'"]
olivya = []
@ -118,6 +120,7 @@ all = [
"lfx-bundles[deepseek]",
"lfx-bundles[elastic]",
"lfx-bundles[exa]",
"lfx-bundles[faiss]",
"lfx-bundles[firecrawl]",
"lfx-bundles[git]",
"lfx-bundles[glean]",
@ -136,6 +139,7 @@ all = [
"lfx-bundles[mistral]",
"lfx-bundles[mongodb]",
"lfx-bundles[needle]",
"lfx-bundles[notion]",
"lfx-bundles[novita]",
"lfx-bundles[nvidia]",
"lfx-bundles[olivya]",

View File

@ -0,0 +1,34 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from lfx.components._importing import import_mod
if TYPE_CHECKING:
from .faiss import FaissVectorStoreComponent
_dynamic_imports = {
"FaissVectorStoreComponent": "faiss",
}
__all__ = [
"FaissVectorStoreComponent",
]
def __getattr__(attr_name: str) -> Any:
"""Lazily import FAISS components on attribute access."""
if attr_name not in _dynamic_imports:
msg = f"module '{__name__}' has no attribute '{attr_name}'"
raise AttributeError(msg)
try:
result = import_mod(attr_name, _dynamic_imports[attr_name], __spec__.parent)
except (ModuleNotFoundError, ImportError, AttributeError) as e:
msg = f"Could not import '{attr_name}' from '{__name__}': {e}"
raise AttributeError(msg) from e
globals()[attr_name] = result
return result
def __dir__() -> list[str]:
return list(__all__)

View File

@ -1,7 +1,6 @@
from pathlib import Path
from langchain_community.vectorstores import FAISS
from lfx.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store
from lfx.helpers.data import docs_to_data
from lfx.io import BoolInput, HandleInput, IntInput, StrInput

View File

@ -0,0 +1,19 @@
from .add_content_to_page import AddContentToPage
from .create_page import NotionPageCreator
from .list_database_properties import NotionDatabaseProperties
from .list_pages import NotionListPages
from .list_users import NotionUserList
from .page_content_viewer import NotionPageContent
from .search import NotionSearch
from .update_page_property import NotionPageUpdate
__all__ = [
"AddContentToPage",
"NotionDatabaseProperties",
"NotionListPages",
"NotionPageContent",
"NotionPageCreator",
"NotionPageUpdate",
"NotionSearch",
"NotionUserList",
]

View File

@ -4,14 +4,13 @@ from typing import Any
import requests
from bs4 import BeautifulSoup
from langchain_core.tools import StructuredTool
from markdown import markdown
from pydantic import BaseModel, Field
from lfx.base.langchain_utilities.model import LCToolComponent
from lfx.field_typing import Tool
from lfx.inputs.inputs import MultilineInput, SecretStrInput, StrInput
from lfx.log.logger import logger
from lfx.schema.data import Data
from markdown import markdown
from pydantic import BaseModel, Field
MIN_ROWS_IN_TABLE = 3

View File

@ -3,12 +3,11 @@ from typing import Any
import requests
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
from lfx.base.langchain_utilities.model import LCToolComponent
from lfx.field_typing import Tool
from lfx.inputs.inputs import MultilineInput, SecretStrInput, StrInput
from lfx.schema.data import Data
from pydantic import BaseModel, Field
class NotionPageCreator(LCToolComponent):

View File

@ -1,12 +1,11 @@
import requests
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
from lfx.base.langchain_utilities.model import LCToolComponent
from lfx.field_typing import Tool
from lfx.inputs.inputs import SecretStrInput, StrInput
from lfx.log.logger import logger
from lfx.schema.data import Data
from pydantic import BaseModel, Field
class NotionDatabaseProperties(LCToolComponent):

View File

@ -3,13 +3,12 @@ from typing import Any
import requests
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
from lfx.base.langchain_utilities.model import LCToolComponent
from lfx.field_typing import Tool
from lfx.inputs.inputs import MultilineInput, SecretStrInput, StrInput
from lfx.log.logger import logger
from lfx.schema.data import Data
from pydantic import BaseModel, Field
class NotionListPages(LCToolComponent):

View File

@ -1,11 +1,10 @@
import requests
from langchain_core.tools import StructuredTool
from pydantic import BaseModel
from lfx.base.langchain_utilities.model import LCToolComponent
from lfx.field_typing import Tool
from lfx.inputs.inputs import SecretStrInput
from lfx.schema.data import Data
from pydantic import BaseModel
class NotionUserList(LCToolComponent):

View File

@ -1,12 +1,11 @@
import requests
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
from lfx.base.langchain_utilities.model import LCToolComponent
from lfx.field_typing import Tool
from lfx.inputs.inputs import SecretStrInput, StrInput
from lfx.log.logger import logger
from lfx.schema.data import Data
from pydantic import BaseModel, Field
class NotionPageContent(LCToolComponent):

View File

@ -2,12 +2,11 @@ from typing import Any
import requests
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
from lfx.base.langchain_utilities.model import LCToolComponent
from lfx.field_typing import Tool
from lfx.inputs.inputs import DropdownInput, SecretStrInput, StrInput
from lfx.schema.data import Data
from pydantic import BaseModel, Field
class NotionSearch(LCToolComponent):

View File

@ -3,13 +3,12 @@ from typing import Any
import requests
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
from lfx.base.langchain_utilities.model import LCToolComponent
from lfx.field_typing import Tool
from lfx.inputs.inputs import MultilineInput, SecretStrInput, StrInput
from lfx.log.logger import logger
from lfx.schema.data import Data
from pydantic import BaseModel, Field
class NotionPageUpdate(LCToolComponent):

File diff suppressed because one or more lines are too long

View File

@ -1,34 +1,22 @@
from __future__ import annotations
# lfx-bundles-shim
"""Compatibility shim: lfx.components.FAISS moved to lfx-bundles.
from typing import TYPE_CHECKING, Any
This module re-points to the installed bundle distribution. It contains
no component implementations and no third-party dependencies, and is
removed once the deprecation window closes (M4).
"""
from lfx.components._importing import import_mod
import importlib
import sys
if TYPE_CHECKING:
from .faiss import FaissVectorStoreComponent
_dynamic_imports = {
"FaissVectorStoreComponent": "faiss",
}
__all__ = [
"FaissVectorStoreComponent",
]
def __getattr__(attr_name: str) -> Any:
"""Lazily import FAISS components on attribute access."""
if attr_name not in _dynamic_imports:
msg = f"module '{__name__}' has no attribute '{attr_name}'"
raise AttributeError(msg)
try:
result = import_mod(attr_name, _dynamic_imports[attr_name], __spec__.parent)
except (ModuleNotFoundError, ImportError, AttributeError) as e:
msg = f"Could not import '{attr_name}' from '{__name__}': {e}"
raise AttributeError(msg) from e
globals()[attr_name] = result
return result
def __dir__() -> list[str]:
return list(__all__)
try:
sys.modules[__name__] = importlib.import_module("lfx_bundles.faiss")
except ModuleNotFoundError as exc:
if exc.name is not None and (exc.name == "lfx_bundles" or exc.name.startswith("lfx_bundles.")):
msg = (
"The 'FAISS' components moved to the 'lfx-bundles' distribution. "
"Install it with: pip install lfx-bundles "
"(or 'pip install langflow', which bundles it)."
)
raise ModuleNotFoundError(msg, name="lfx_bundles") from exc
raise

View File

@ -1,19 +1,22 @@
from .add_content_to_page import AddContentToPage
from .create_page import NotionPageCreator
from .list_database_properties import NotionDatabaseProperties
from .list_pages import NotionListPages
from .list_users import NotionUserList
from .page_content_viewer import NotionPageContent
from .search import NotionSearch
from .update_page_property import NotionPageUpdate
# lfx-bundles-shim
"""Compatibility shim: lfx.components.Notion moved to lfx-bundles.
__all__ = [
"AddContentToPage",
"NotionDatabaseProperties",
"NotionListPages",
"NotionPageContent",
"NotionPageCreator",
"NotionPageUpdate",
"NotionSearch",
"NotionUserList",
]
This module re-points to the installed bundle distribution. It contains
no component implementations and no third-party dependencies, and is
removed once the deprecation window closes (M4).
"""
import importlib
import sys
try:
sys.modules[__name__] = importlib.import_module("lfx_bundles.notion")
except ModuleNotFoundError as exc:
if exc.name is not None and (exc.name == "lfx_bundles" or exc.name.startswith("lfx_bundles.")):
msg = (
"The 'Notion' components moved to the 'lfx-bundles' distribution. "
"Install it with: pip install lfx-bundles "
"(or 'pip install langflow', which bundles it)."
)
raise ModuleNotFoundError(msg, name="lfx_bundles") from exc
raise

View File

@ -4580,6 +4580,186 @@
"legacy_slot": "ext:langwatch:LangWatchComponent@official-pre-a",
"target": "ext:langwatch:LangWatchComponent@official",
"added_in": "1.11.0"
},
{
"bare_class_name": "FaissVectorStoreComponent",
"target": "ext:faiss:FaissVectorStoreComponent@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.FAISS.faiss.FaissVectorStoreComponent",
"target": "ext:faiss:FaissVectorStoreComponent@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.FAISS.FaissVectorStoreComponent",
"target": "ext:faiss:FaissVectorStoreComponent@official",
"added_in": "1.11.0"
},
{
"legacy_slot": "ext:faiss:FaissVectorStoreComponent@official-pre-a",
"target": "ext:faiss:FaissVectorStoreComponent@official",
"added_in": "1.11.0"
},
{
"bare_class_name": "AddContentToPage",
"target": "ext:notion:AddContentToPage@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.Notion.add_content_to_page.AddContentToPage",
"target": "ext:notion:AddContentToPage@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.Notion.AddContentToPage",
"target": "ext:notion:AddContentToPage@official",
"added_in": "1.11.0"
},
{
"legacy_slot": "ext:notion:AddContentToPage@official-pre-a",
"target": "ext:notion:AddContentToPage@official",
"added_in": "1.11.0"
},
{
"bare_class_name": "NotionPageCreator",
"target": "ext:notion:NotionPageCreator@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.Notion.create_page.NotionPageCreator",
"target": "ext:notion:NotionPageCreator@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.Notion.NotionPageCreator",
"target": "ext:notion:NotionPageCreator@official",
"added_in": "1.11.0"
},
{
"legacy_slot": "ext:notion:NotionPageCreator@official-pre-a",
"target": "ext:notion:NotionPageCreator@official",
"added_in": "1.11.0"
},
{
"bare_class_name": "NotionDatabaseProperties",
"target": "ext:notion:NotionDatabaseProperties@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.Notion.list_database_properties.NotionDatabaseProperties",
"target": "ext:notion:NotionDatabaseProperties@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.Notion.NotionDatabaseProperties",
"target": "ext:notion:NotionDatabaseProperties@official",
"added_in": "1.11.0"
},
{
"legacy_slot": "ext:notion:NotionDatabaseProperties@official-pre-a",
"target": "ext:notion:NotionDatabaseProperties@official",
"added_in": "1.11.0"
},
{
"bare_class_name": "NotionListPages",
"target": "ext:notion:NotionListPages@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.Notion.list_pages.NotionListPages",
"target": "ext:notion:NotionListPages@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.Notion.NotionListPages",
"target": "ext:notion:NotionListPages@official",
"added_in": "1.11.0"
},
{
"legacy_slot": "ext:notion:NotionListPages@official-pre-a",
"target": "ext:notion:NotionListPages@official",
"added_in": "1.11.0"
},
{
"bare_class_name": "NotionUserList",
"target": "ext:notion:NotionUserList@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.Notion.list_users.NotionUserList",
"target": "ext:notion:NotionUserList@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.Notion.NotionUserList",
"target": "ext:notion:NotionUserList@official",
"added_in": "1.11.0"
},
{
"legacy_slot": "ext:notion:NotionUserList@official-pre-a",
"target": "ext:notion:NotionUserList@official",
"added_in": "1.11.0"
},
{
"bare_class_name": "NotionPageContent",
"target": "ext:notion:NotionPageContent@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.Notion.page_content_viewer.NotionPageContent",
"target": "ext:notion:NotionPageContent@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.Notion.NotionPageContent",
"target": "ext:notion:NotionPageContent@official",
"added_in": "1.11.0"
},
{
"legacy_slot": "ext:notion:NotionPageContent@official-pre-a",
"target": "ext:notion:NotionPageContent@official",
"added_in": "1.11.0"
},
{
"bare_class_name": "NotionSearch",
"target": "ext:notion:NotionSearch@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.Notion.search.NotionSearch",
"target": "ext:notion:NotionSearch@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.Notion.NotionSearch",
"target": "ext:notion:NotionSearch@official",
"added_in": "1.11.0"
},
{
"legacy_slot": "ext:notion:NotionSearch@official-pre-a",
"target": "ext:notion:NotionSearch@official",
"added_in": "1.11.0"
},
{
"bare_class_name": "NotionPageUpdate",
"target": "ext:notion:NotionPageUpdate@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.Notion.update_page_property.NotionPageUpdate",
"target": "ext:notion:NotionPageUpdate@official",
"added_in": "1.11.0"
},
{
"import_path": "lfx.components.Notion.NotionPageUpdate",
"target": "ext:notion:NotionPageUpdate@official",
"added_in": "1.11.0"
},
{
"legacy_slot": "ext:notion:NotionPageUpdate@official-pre-a",
"target": "ext:notion:NotionPageUpdate@official",
"added_in": "1.11.0"
}
],
"ambiguous_bare_names": [

View File

@ -640,7 +640,7 @@ def _process_single_module(modname: str) -> tuple[str, dict] | None:
logger.error(f"Failed to import module {modname}: {e}", exc_info=True)
return None
# Extract the top-level subpackage name after "lfx.components."
# e.g., "lfx.components.Notion.add_content_to_page" -> "Notion"
# e.g., "lfx.components.processing.combine_text" -> "processing"
mod_parts = modname.split(".")
if len(mod_parts) <= MIN_MODULE_PARTS:
return None

View File

@ -91,13 +91,16 @@ def test_shim_source_contract(shim_dir: Path) -> None:
message must name the matching distribution.
"""
provider = shim_dir.name
# Bundle names are lowercase (BUNDLE_NAME_RE), so a mixed-case in-tree dir
# (e.g. FAISS, Notion) aliases to its lowercased bundle (faiss, notion).
slug = provider.lower()
src = (shim_dir / "__init__.py").read_text(encoding="utf-8")
assert src.startswith(SHIM_MARKER), f"{provider}: first line must be the {SHIM_MARKER!r} marker"
assert "sys.modules[__name__] = importlib.import_module(" in src, f"{provider}: must module-alias"
meta_target = f'importlib.import_module("lfx_bundles.{provider}")'
partner_target = f'importlib.import_module("lfx_{provider}.components.{provider}")'
meta_target = f'importlib.import_module("lfx_bundles.{slug}")'
partner_target = f'importlib.import_module("lfx_{slug}.components.{slug}")'
is_meta = meta_target in src
is_partner = partner_target in src
assert is_meta or is_partner, f"{provider}: alias target is neither metapackage nor partner shape"
@ -106,9 +109,9 @@ def test_shim_source_contract(shim_dir: Path) -> None:
assert 'exc.name == "lfx_bundles"' in src, f"{provider}: name-check must guard lfx_bundles"
assert _METAPACKAGE_MSG in src, f"{provider}: locked install message missing ({_METAPACKAGE_MSG!r})"
else:
assert f'exc.name == "lfx_{provider}"' in src, f"{provider}: name-check must guard lfx_{provider}"
assert f"{_PARTNER_MSG_PREFIX}{provider}" in src, (
f"{provider}: locked install message missing ('pip install lfx-{provider}')"
assert f'exc.name == "lfx_{slug}"' in src, f"{provider}: name-check must guard lfx_{slug}"
assert f"{_PARTNER_MSG_PREFIX}{slug}" in src, (
f"{provider}: locked install message missing ('pip install lfx-{slug}')"
)
# Contract #5: anything other than the bundle-missing case re-raises.
assert src.rstrip().endswith("raise"), f"{provider}: must re-raise non-bundle import errors untouched"

21
uv.lock generated
View File

@ -9150,6 +9150,8 @@ all = [
{ name = "couchbase" },
{ name = "cuga", marker = "(python_full_version < '3.14' and platform_machine == 'arm64') or (python_full_version < '3.14' and sys_platform != 'darwin')" },
{ name = "elasticsearch" },
{ name = "faiss-cpu", version = "1.9.0.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" },
{ name = "faiss-cpu", version = "1.14.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" },
{ name = "firecrawl-py" },
{ name = "gassist", marker = "sys_platform == 'win32'" },
{ name = "gitpython" },
@ -9177,6 +9179,7 @@ all = [
{ name = "langchain-sambanova" },
{ name = "langchain-unstructured" },
{ name = "langchain-weaviate" },
{ name = "markdown" },
{ name = "mem0ai" },
{ name = "metaphor-python" },
{ name = "needle-python" },
@ -9278,6 +9281,11 @@ elastic = [
exa = [
{ name = "metaphor-python" },
]
faiss = [
{ name = "faiss-cpu", version = "1.9.0.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" },
{ name = "faiss-cpu", version = "1.14.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" },
{ name = "langchain-community" },
]
firecrawl = [
{ name = "firecrawl-py" },
]
@ -9336,6 +9344,10 @@ needle = [
{ name = "langchain-community" },
{ name = "needle-python" },
]
notion = [
{ name = "markdown" },
{ name = "requests" },
]
novita = [
{ name = "langchain-openai" },
{ name = "requests" },
@ -9457,6 +9469,8 @@ requires-dist = [
{ name = "cuga", marker = "python_full_version < '3.14' and platform_machine == 'arm64' and sys_platform == 'darwin' and extra == 'cuga'", specifier = ">=0.2.20,<0.3.0" },
{ name = "cuga", marker = "python_full_version < '3.14' and sys_platform != 'darwin' and extra == 'cuga'", specifier = ">=0.2.20,<0.3.0" },
{ name = "elasticsearch", marker = "extra == 'elastic'", specifier = "~=8.19" },
{ name = "faiss-cpu", marker = "python_full_version >= '3.14' and extra == 'faiss'", specifier = ">=1.13.2" },
{ name = "faiss-cpu", marker = "python_full_version < '3.14' and extra == 'faiss'", specifier = "==1.9.0.post1" },
{ name = "firecrawl-py", marker = "extra == 'firecrawl'", specifier = ">=1.0.16,<2.0.0" },
{ name = "gassist", marker = "sys_platform == 'win32' and extra == 'nvidia'", specifier = ">=0.0.1" },
{ name = "gitpython", marker = "extra == 'git'", specifier = ">=3.1.50" },
@ -9474,6 +9488,7 @@ requires-dist = [
{ name = "langchain-community", marker = "extra == 'cloudflare'", specifier = ">=0.4.1,<1.0.0" },
{ name = "langchain-community", marker = "extra == 'confluence'", specifier = ">=0.4.1,<1.0.0" },
{ name = "langchain-community", marker = "extra == 'couchbase'", specifier = ">=0.4.1,<1.0.0" },
{ name = "langchain-community", marker = "extra == 'faiss'", specifier = ">=0.4.1,<1.0.0" },
{ name = "langchain-community", marker = "extra == 'git'", specifier = ">=0.4.1,<1.0.0" },
{ name = "langchain-community", marker = "extra == 'huggingface'", specifier = ">=0.4.1,<1.0.0" },
{ name = "langchain-community", marker = "extra == 'maritalk'", specifier = ">=0.4.1,<1.0.0" },
@ -9537,6 +9552,7 @@ requires-dist = [
{ name = "lfx-bundles", extras = ["deepseek"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
{ name = "lfx-bundles", extras = ["elastic"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
{ name = "lfx-bundles", extras = ["exa"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
{ name = "lfx-bundles", extras = ["faiss"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
{ name = "lfx-bundles", extras = ["firecrawl"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
{ name = "lfx-bundles", extras = ["git"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
{ name = "lfx-bundles", extras = ["glean"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
@ -9555,6 +9571,7 @@ requires-dist = [
{ name = "lfx-bundles", extras = ["mistral"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
{ name = "lfx-bundles", extras = ["mongodb"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
{ name = "lfx-bundles", extras = ["needle"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
{ name = "lfx-bundles", extras = ["notion"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
{ name = "lfx-bundles", extras = ["novita"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
{ name = "lfx-bundles", extras = ["nvidia"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
{ name = "lfx-bundles", extras = ["olivya"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
@ -9586,6 +9603,7 @@ requires-dist = [
{ name = "lfx-bundles", extras = ["yahoosearch"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
{ name = "lfx-bundles", extras = ["youtube"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
{ name = "lfx-bundles", extras = ["zep"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" },
{ name = "markdown", marker = "extra == 'notion'", specifier = ">=3.8.0" },
{ name = "mem0ai", marker = "extra == 'mem0'", specifier = ">=2.0.2,<3.0.0" },
{ name = "metaphor-python", marker = "extra == 'exa'", specifier = "==0.1.23" },
{ name = "needle-python", marker = "extra == 'needle'", specifier = ">=0.4.0" },
@ -9608,6 +9626,7 @@ requires-dist = [
{ name = "requests", marker = "extra == 'deepseek'", specifier = ">=2.32.0" },
{ name = "requests", marker = "extra == 'homeassistant'", specifier = ">=2.32.0" },
{ name = "requests", marker = "extra == 'icosacomputing'", specifier = ">=2.32.0" },
{ name = "requests", marker = "extra == 'notion'", specifier = ">=2.32.0" },
{ name = "requests", marker = "extra == 'novita'", specifier = ">=2.32.0" },
{ name = "requests", marker = "extra == 'xai'", specifier = ">=2.32.0" },
{ name = "scrapegraph-py", marker = "extra == 'scrapegraph'", specifier = ">=1.12.0" },
@ -9624,7 +9643,7 @@ requires-dist = [
{ name = "youtube-transcript-api", marker = "extra == 'youtube'", specifier = ">=1.0.0,<2.0.0" },
{ name = "zep-python", marker = "extra == 'zep'", specifier = "==2.0.2" },
]
provides-extras = ["agentql", "aiml", "altk", "apify", "assemblyai", "azure", "baidu", "bing", "chroma", "cleanlab", "clickhouse", "cloudflare", "codeagents", "cometapi", "composio", "confluence", "couchbase", "cuga", "deepseek", "elastic", "exa", "firecrawl", "git", "glean", "google", "groq", "homeassistant", "huggingface", "icosacomputing", "jigsawstack", "langwatch", "litellm", "lmstudio", "maritalk", "mem0", "milvus", "mistral", "mongodb", "needle", "novita", "nvidia", "olivya", "ollama", "openrouter", "perplexity", "pgvector", "pinecone", "qdrant", "redis", "sambanova", "scrapegraph", "searchapi", "serpapi", "spider", "supabase", "tavily", "toolguard", "twelvelabs", "unstructured", "upstash", "vectara", "vertexai", "vllm", "weaviate", "wikipedia", "wolframalpha", "xai", "yahoosearch", "youtube", "zep", "all"]
provides-extras = ["agentql", "aiml", "altk", "apify", "assemblyai", "azure", "baidu", "bing", "chroma", "cleanlab", "clickhouse", "cloudflare", "codeagents", "cometapi", "composio", "confluence", "couchbase", "cuga", "deepseek", "elastic", "exa", "faiss", "firecrawl", "git", "glean", "google", "groq", "homeassistant", "huggingface", "icosacomputing", "jigsawstack", "langwatch", "litellm", "lmstudio", "maritalk", "mem0", "milvus", "mistral", "mongodb", "needle", "notion", "novita", "nvidia", "olivya", "ollama", "openrouter", "perplexity", "pgvector", "pinecone", "qdrant", "redis", "sambanova", "scrapegraph", "searchapi", "serpapi", "spider", "supabase", "tavily", "toolguard", "twelvelabs", "unstructured", "upstash", "vectara", "vertexai", "vllm", "weaviate", "wikipedia", "wolframalpha", "xai", "yahoosearch", "youtube", "zep", "all"]
[[package]]
name = "lfx-cohere"