From af72c7137fde8b0ad0e6ebe570e6b6111a675331 Mon Sep 17 00:00:00 2001 From: kchawlani19 Date: Wed, 17 Jun 2026 11:48:43 +0530 Subject: [PATCH 1/4] fix(dbt): handle missing dbt-artifacts-parser import errors in CLI Catch ImportError in `feast dbt import` and `feast dbt list` so missing dbt parser dependency returns a clean CLI error. Add regression tests to verify both commands exit with actionable install guidance. Signed-off-by: kchawlani19 Co-authored-by: Cursor --- sdk/python/feast/cli/dbt_import.py | 7 +-- .../integration/dbt/test_dbt_integration.py | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/sdk/python/feast/cli/dbt_import.py b/sdk/python/feast/cli/dbt_import.py index c2e78b45c82..16bd7812101 100644 --- a/sdk/python/feast/cli/dbt_import.py +++ b/sdk/python/feast/cli/dbt_import.py @@ -130,10 +130,7 @@ def import_command( try: parser = DbtManifestParser(manifest_path) parser.parse() - except FileNotFoundError as e: - click.echo(f"{Fore.RED}Error: {e}{Style.RESET_ALL}", err=True) - raise SystemExit(1) - except ValueError as e: + except (FileNotFoundError, ValueError, ImportError) as e: click.echo(f"{Fore.RED}Error: {e}{Style.RESET_ALL}", err=True) raise SystemExit(1) @@ -374,7 +371,7 @@ def list_command( try: parser = DbtManifestParser(manifest_path) parser.parse() - except (FileNotFoundError, ValueError) as e: + except (FileNotFoundError, ValueError, ImportError) as e: click.echo(f"{Fore.RED}Error: {e}{Style.RESET_ALL}", err=True) raise SystemExit(1) diff --git a/sdk/python/tests/integration/dbt/test_dbt_integration.py b/sdk/python/tests/integration/dbt/test_dbt_integration.py index 7231f31427b..08ee21224cb 100644 --- a/sdk/python/tests/integration/dbt/test_dbt_integration.py +++ b/sdk/python/tests/integration/dbt/test_dbt_integration.py @@ -1061,6 +1061,26 @@ def test_dbt_list_show_columns(self, manifest_path): assert "driver_id" in result.output assert "conv_rate" in result.output + def test_dbt_list_import_error(self, manifest_path, monkeypatch): + from click.testing import CliRunner + + from feast.cli.dbt_import import list_command + from feast.dbt.parser import DbtManifestParser + + def _raise_import_error(self): # pragma: no cover - behavior tested via CLI + raise ImportError( + "dbt-artifacts-parser is required for dbt integration.\n" + "Install with: pip install 'feast[dbt]' or pip install dbt-artifacts-parser" + ) + + monkeypatch.setattr(DbtManifestParser, "parse", _raise_import_error) + + runner = CliRunner() + result = runner.invoke(list_command, ["-m", manifest_path]) + assert result.exit_code == 1 + assert "dbt-artifacts-parser is required for dbt integration" in result.output + assert "feast[dbt]" in result.output + def test_dbt_import_dry_run(self, manifest_path): from click.testing import CliRunner @@ -1086,6 +1106,36 @@ def test_dbt_import_dry_run(self, manifest_path): assert result.exit_code == 0 assert "Dry run" in result.output + def test_dbt_import_import_error(self, manifest_path, monkeypatch): + from click.testing import CliRunner + + from feast.cli.dbt_import import import_command + from feast.dbt.parser import DbtManifestParser + + def _raise_import_error(self): # pragma: no cover - behavior tested via CLI + raise ImportError( + "dbt-artifacts-parser is required for dbt integration.\n" + "Install with: pip install 'feast[dbt]' or pip install dbt-artifacts-parser" + ) + + monkeypatch.setattr(DbtManifestParser, "parse", _raise_import_error) + + runner = CliRunner() + result = runner.invoke( + import_command, + [ + "-m", + manifest_path, + "-e", + "driver_id", + "--dry-run", + ], + obj={"CHDIR": ".", "FS_YAML_FILE": "feature_store.yaml"}, + ) + assert result.exit_code == 1 + assert "dbt-artifacts-parser is required for dbt integration" in result.output + assert "feast[dbt]" in result.output + def test_dbt_import_output_codegen(self, manifest_path, tmp_path): from click.testing import CliRunner From b44c6fe7542481959f39c3a89f87d15e0ac4cb03 Mon Sep 17 00:00:00 2001 From: kchawlani19 Date: Wed, 17 Jun 2026 23:36:22 +0530 Subject: [PATCH 2/4] test(dbt): make import error CLI assertions robust Signed-off-by: kchawlani19 --- .../tests/integration/dbt/test_dbt_integration.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/sdk/python/tests/integration/dbt/test_dbt_integration.py b/sdk/python/tests/integration/dbt/test_dbt_integration.py index 08ee21224cb..8a72cbf6ff2 100644 --- a/sdk/python/tests/integration/dbt/test_dbt_integration.py +++ b/sdk/python/tests/integration/dbt/test_dbt_integration.py @@ -1077,9 +1077,10 @@ def _raise_import_error(self): # pragma: no cover - behavior tested via CLI runner = CliRunner() result = runner.invoke(list_command, ["-m", manifest_path]) + output = result.output + (result.stderr or "") assert result.exit_code == 1 - assert "dbt-artifacts-parser is required for dbt integration" in result.output - assert "feast[dbt]" in result.output + assert "dbt-artifacts-parser is required for dbt integration" in output + assert "feast[dbt]" in output def test_dbt_import_dry_run(self, manifest_path): from click.testing import CliRunner @@ -1131,10 +1132,12 @@ def _raise_import_error(self): # pragma: no cover - behavior tested via CLI "--dry-run", ], obj={"CHDIR": ".", "FS_YAML_FILE": "feature_store.yaml"}, + catch_exceptions=False, ) + output = result.output + (result.stderr or "") assert result.exit_code == 1 - assert "dbt-artifacts-parser is required for dbt integration" in result.output - assert "feast[dbt]" in result.output + assert "dbt-artifacts-parser is required for dbt integration" in output + assert "feast[dbt]" in output def test_dbt_import_output_codegen(self, manifest_path, tmp_path): from click.testing import CliRunner From e8de1864a801d75175a4ad17d84d6c2bf6880d50 Mon Sep 17 00:00:00 2001 From: kchawlani19 Date: Wed, 17 Jun 2026 23:46:30 +0530 Subject: [PATCH 3/4] test(dbt): make CLI error output assertions click-version safe Signed-off-by: kchawlani19 --- .../tests/integration/dbt/test_dbt_integration.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/sdk/python/tests/integration/dbt/test_dbt_integration.py b/sdk/python/tests/integration/dbt/test_dbt_integration.py index 8a72cbf6ff2..09a3cb5e123 100644 --- a/sdk/python/tests/integration/dbt/test_dbt_integration.py +++ b/sdk/python/tests/integration/dbt/test_dbt_integration.py @@ -1025,6 +1025,16 @@ def test_codegen_online_false(self, parser): class TestDbtCli: """Test the `feast dbt import` and `feast dbt list` CLI commands.""" + @staticmethod + def _get_cli_output(result) -> str: + """Return combined stdout/stderr across Click versions.""" + stderr = getattr(result, "stderr", "") + if not stderr: + stderr_bytes = getattr(result, "stderr_bytes", b"") + if stderr_bytes: + stderr = stderr_bytes.decode("utf-8", errors="replace") + return result.output + stderr + def test_dbt_list_command(self, manifest_path): from click.testing import CliRunner @@ -1077,7 +1087,7 @@ def _raise_import_error(self): # pragma: no cover - behavior tested via CLI runner = CliRunner() result = runner.invoke(list_command, ["-m", manifest_path]) - output = result.output + (result.stderr or "") + output = self._get_cli_output(result) assert result.exit_code == 1 assert "dbt-artifacts-parser is required for dbt integration" in output assert "feast[dbt]" in output @@ -1134,7 +1144,7 @@ def _raise_import_error(self): # pragma: no cover - behavior tested via CLI obj={"CHDIR": ".", "FS_YAML_FILE": "feature_store.yaml"}, catch_exceptions=False, ) - output = result.output + (result.stderr or "") + output = self._get_cli_output(result) assert result.exit_code == 1 assert "dbt-artifacts-parser is required for dbt integration" in output assert "feast[dbt]" in output From 1301a68217fd714591e6e1f840968e55b9567d24 Mon Sep 17 00:00:00 2001 From: kchawlani19 Date: Wed, 17 Jun 2026 23:59:43 +0530 Subject: [PATCH 4/4] fix(dbt): tolerate unsupported macro languages in dbt manifests Signed-off-by: kchawlani19 --- sdk/python/feast/dbt/parser.py | 39 +++++++++++++++++++- sdk/python/tests/unit/dbt/test_parser.py | 47 ++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/sdk/python/feast/dbt/parser.py b/sdk/python/feast/dbt/parser.py index 676a5a76f11..9314104c9c1 100644 --- a/sdk/python/feast/dbt/parser.py +++ b/sdk/python/feast/dbt/parser.py @@ -8,6 +8,7 @@ """ import json +from copy import deepcopy from dataclasses import dataclass, field from pathlib import Path from typing import Any, Dict, List, Optional @@ -79,6 +80,34 @@ def __init__(self, manifest_path: str): self._raw_manifest: Optional[Dict[str, Any]] = None self._parsed_manifest: Optional[Any] = None + @staticmethod + def _sanitize_supported_languages(manifest: Dict[str, Any]) -> Dict[str, Any]: + """ + Remove unsupported macro language values before typed parsing. + + dbt may emit values like "javascript" in `macros.*.supported_languages`, + while dbt-artifacts-parser currently accepts only "python" and "sql". + Since Feast only needs model metadata, dropping unknown values is safe. + """ + manifest_copy = deepcopy(manifest) + macros = manifest_copy.get("macros", {}) + if not isinstance(macros, dict): + return manifest_copy + + for macro in macros.values(): + if not isinstance(macro, dict): + continue + + supported_languages = macro.get("supported_languages") + if isinstance(supported_languages, list): + macro["supported_languages"] = [ + language + for language in supported_languages + if language in {"python", "sql"} + ] + + return manifest_copy + def parse(self) -> None: """ Load and parse the manifest.json file using dbt-artifacts-parser. @@ -108,7 +137,15 @@ def parse(self) -> None: from dbt_artifacts_parser.parser import parse_manifest assert self._raw_manifest is not None - self._parsed_manifest = parse_manifest(manifest=self._raw_manifest) + try: + self._parsed_manifest = parse_manifest(manifest=self._raw_manifest) + except Exception as e: + if "supported_languages" not in str(e): + raise + sanitized_manifest = self._sanitize_supported_languages( + self._raw_manifest + ) + self._parsed_manifest = parse_manifest(manifest=sanitized_manifest) except ImportError: raise ImportError( "dbt-artifacts-parser is required for dbt integration.\n" diff --git a/sdk/python/tests/unit/dbt/test_parser.py b/sdk/python/tests/unit/dbt/test_parser.py index 2e15f9863ee..af4a5c3c366 100644 --- a/sdk/python/tests/unit/dbt/test_parser.py +++ b/sdk/python/tests/unit/dbt/test_parser.py @@ -172,6 +172,53 @@ def test_parse_manifest_invalid_json(self, tmp_path): assert "Invalid JSON" in str(exc_info.value) + def test_parse_retries_with_sanitized_supported_languages( + self, tmp_path, monkeypatch + ): + """Test parser retries after removing unsupported macro languages.""" + manifest = { + "metadata": { + "dbt_schema_version": "https://schemas.getdbt.com/dbt/manifest/v12.json", + "dbt_version": "1.11.11", + }, + "nodes": {}, + "macros": { + "macro.dbt.materialization_function_default": { + "name": "materialization_function_default", + "supported_languages": ["python", "sql", "javascript"], + } + }, + } + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text(json.dumps(manifest)) + + class _ParsedManifest: + nodes = {} + metadata = None + + call_manifests = [] + + def _fake_parse_manifest(*, manifest): + call_manifests.append(manifest) + if len(call_manifests) == 1: + raise Exception("supported_languages Input should be 'python' or 'sql'") + return _ParsedManifest() + + monkeypatch.setattr( + "dbt_artifacts_parser.parser.parse_manifest", _fake_parse_manifest + ) + + parser = DbtManifestParser(str(manifest_path)) + parser.parse() + + assert len(call_manifests) == 2 + assert call_manifests[0]["macros"][ + "macro.dbt.materialization_function_default" + ]["supported_languages"] == ["python", "sql", "javascript"] + assert call_manifests[1]["macros"][ + "macro.dbt.materialization_function_default" + ]["supported_languages"] == ["python", "sql"] + def test_get_all_models(self, sample_manifest): """Test getting all models from manifest.""" parser = DbtManifestParser(str(sample_manifest))