-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add new dbt artifacts support #89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| """Constants for the Altimate API client.""" | ||
|
|
||
| # Supported dbt artifact file types for onboarding | ||
| SUPPORTED_ARTIFACT_TYPES = { | ||
| "manifest", | ||
| "catalog", | ||
| "run_results", | ||
| "sources", | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| from requests import Response | ||
|
|
||
| from datapilot.clients.altimate.client import APIClient | ||
| from datapilot.clients.altimate.constants import SUPPORTED_ARTIFACT_TYPES | ||
|
|
||
|
|
||
| def check_token_and_instance( | ||
|
|
@@ -56,6 +57,27 @@ def validate_permissions( | |
|
|
||
|
|
||
| def onboard_file(api_token, tenant, dbt_core_integration_id, dbt_core_integration_environment, file_type, file_path, backend_url) -> Dict: | ||
| """ | ||
| Upload a dbt artifact file to the Altimate backend. | ||
|
|
||
| Args: | ||
| api_token: API authentication token | ||
| tenant: Tenant/instance name | ||
| dbt_core_integration_id: ID of the dbt integration | ||
| dbt_core_integration_environment: Environment type (e.g., PROD) | ||
| file_type: Type of artifact - one of: manifest, catalog, run_results, sources, semantic_manifest | ||
| file_path: Path to the artifact file | ||
| backend_url: URL of the Altimate backend | ||
|
|
||
| Returns: | ||
| Dict with 'ok' boolean and optional 'message' on failure | ||
| """ | ||
|
Comment on lines
+60
to
+74
|
||
| if file_type not in SUPPORTED_ARTIFACT_TYPES: | ||
| return { | ||
| "ok": False, | ||
| "message": f"Unsupported file type: {file_type}. Supported types: {', '.join(sorted(SUPPORTED_ARTIFACT_TYPES))}", | ||
| } | ||
|
|
||
| api_client = APIClient(api_token, base_url=backend_url, tenant=tenant) | ||
|
|
||
| params = { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| from typing import Union | ||
|
|
||
| from pydantic import ConfigDict | ||
|
|
||
| from vendor.dbt_artifacts_parser.parsers.run_results.run_results_v1 import RunResultsV1 as BaseRunResultsV1 | ||
| from vendor.dbt_artifacts_parser.parsers.run_results.run_results_v2 import RunResultsV2 as BaseRunResultsV2 | ||
| from vendor.dbt_artifacts_parser.parsers.run_results.run_results_v3 import RunResultsV3 as BaseRunResultsV3 | ||
| from vendor.dbt_artifacts_parser.parsers.run_results.run_results_v4 import RunResultsV4 as BaseRunResultsV4 | ||
| from vendor.dbt_artifacts_parser.parsers.run_results.run_results_v5 import RunResultsV5 as BaseRunResultsV5 | ||
| from vendor.dbt_artifacts_parser.parsers.run_results.run_results_v6 import RunResultsV6 as BaseRunResultsV6 | ||
|
|
||
|
|
||
| class RunResultsV1(BaseRunResultsV1): | ||
| model_config = ConfigDict(extra="allow") | ||
|
|
||
|
|
||
| class RunResultsV2(BaseRunResultsV2): | ||
| model_config = ConfigDict(extra="allow") | ||
|
|
||
|
|
||
| class RunResultsV3(BaseRunResultsV3): | ||
| model_config = ConfigDict(extra="allow") | ||
|
|
||
|
|
||
| class RunResultsV4(BaseRunResultsV4): | ||
| model_config = ConfigDict(extra="allow") | ||
|
|
||
|
|
||
| class RunResultsV5(BaseRunResultsV5): | ||
| model_config = ConfigDict(extra="allow") | ||
|
|
||
|
|
||
| class RunResultsV6(BaseRunResultsV6): | ||
| model_config = ConfigDict(extra="allow") | ||
|
||
|
|
||
|
|
||
| RunResults = Union[ | ||
| RunResultsV6, | ||
| RunResultsV5, | ||
| RunResultsV4, | ||
| RunResultsV3, | ||
| RunResultsV2, | ||
| RunResultsV1, | ||
| ] | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,26 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Union | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pydantic import ConfigDict | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from vendor.dbt_artifacts_parser.parsers.sources.sources_v1 import SourcesV1 as BaseSourcesV1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from vendor.dbt_artifacts_parser.parsers.sources.sources_v2 import SourcesV2 as BaseSourcesV2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from vendor.dbt_artifacts_parser.parsers.sources.sources_v3 import SourcesV3 as BaseSourcesV3 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class SourcesV1(BaseSourcesV1): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model_config = ConfigDict(extra="allow") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class SourcesV2(BaseSourcesV2): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model_config = ConfigDict(extra="allow") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class SourcesV3(BaseSourcesV3): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model_config = ConfigDict(extra="allow") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pydantic import ConfigDict | |
| from vendor.dbt_artifacts_parser.parsers.sources.sources_v1 import SourcesV1 as BaseSourcesV1 | |
| from vendor.dbt_artifacts_parser.parsers.sources.sources_v2 import SourcesV2 as BaseSourcesV2 | |
| from vendor.dbt_artifacts_parser.parsers.sources.sources_v3 import SourcesV3 as BaseSourcesV3 | |
| class SourcesV1(BaseSourcesV1): | |
| model_config = ConfigDict(extra="allow") | |
| class SourcesV2(BaseSourcesV2): | |
| model_config = ConfigDict(extra="allow") | |
| class SourcesV3(BaseSourcesV3): | |
| model_config = ConfigDict(extra="allow") | |
| from vendor.dbt_artifacts_parser.parsers.sources.sources_v1 import SourcesV1 as BaseSourcesV1 | |
| from vendor.dbt_artifacts_parser.parsers.sources.sources_v2 import SourcesV2 as BaseSourcesV2 | |
| from vendor.dbt_artifacts_parser.parsers.sources.sources_v3 import SourcesV3 as BaseSourcesV3 | |
| class SourcesV1(BaseSourcesV1): | |
| ... | |
| class SourcesV2(BaseSourcesV2): | |
| ... | |
| class SourcesV3(BaseSourcesV3): | |
| ... |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,13 +22,17 @@ | |
| from datapilot.core.platforms.dbt.schemas.manifest import AltimateManifestSourceNode | ||
| from datapilot.core.platforms.dbt.schemas.manifest import AltimateManifestTestNode | ||
| from datapilot.core.platforms.dbt.schemas.manifest import Manifest | ||
| from datapilot.core.platforms.dbt.schemas.run_results import RunResults | ||
| from datapilot.core.platforms.dbt.schemas.sources import Sources | ||
| from datapilot.exceptions.exceptions import AltimateFileNotFoundError | ||
| from datapilot.exceptions.exceptions import AltimateInvalidJSONError | ||
| from datapilot.utils.utils import extract_dir_name_from_file_path | ||
| from datapilot.utils.utils import extract_folders_in_path | ||
| from datapilot.utils.utils import is_superset_path | ||
| from datapilot.utils.utils import load_json | ||
| from vendor.dbt_artifacts_parser.parser import parse_manifest | ||
| from vendor.dbt_artifacts_parser.parser import parse_run_results | ||
| from vendor.dbt_artifacts_parser.parser import parse_sources | ||
|
|
||
| MODEL_TYPE_PATTERNS = { | ||
| STAGING: r"^stg_.*", # Example: models starting with 'stg_' | ||
|
|
@@ -94,8 +98,36 @@ def load_catalog(catalog_path: str) -> Catalog: | |
| return catalog | ||
|
|
||
|
|
||
| def load_run_results(run_results_path: str) -> Manifest: | ||
| raise NotImplementedError | ||
| def load_run_results(run_results_path: str) -> RunResults: | ||
| try: | ||
| run_results_dict = load_json(run_results_path) | ||
| except FileNotFoundError as e: | ||
| raise AltimateFileNotFoundError(f"Run results file not found: {run_results_path}. Error: {e}") from e | ||
| except ValueError as e: | ||
| raise AltimateInvalidJSONError(f"Invalid JSON file: {run_results_path}. Error: {e}") from e | ||
|
|
||
| try: | ||
| run_results: RunResults = parse_run_results(run_results_dict) | ||
| except ValueError as e: | ||
| raise AltimateInvalidManifestError(f"Invalid run results file: {run_results_path}. Error: {e}") from e | ||
|
Comment on lines
+110
to
+112
This comment was marked as outdated.
Sorry, something went wrong.
|
||
|
|
||
| return run_results | ||
|
|
||
|
|
||
| def load_sources(sources_path: str) -> Sources: | ||
| try: | ||
| sources_dict = load_json(sources_path) | ||
| except FileNotFoundError as e: | ||
| raise AltimateFileNotFoundError(f"Sources file not found: {sources_path}. Error: {e}") from e | ||
| except ValueError as e: | ||
| raise AltimateInvalidJSONError(f"Invalid JSON file: {sources_path}. Error: {e}") from e | ||
|
|
||
| try: | ||
| sources: Sources = parse_sources(sources_dict) | ||
| except ValueError as e: | ||
| raise AltimateInvalidManifestError(f"Invalid sources file: {sources_path}. Error: {e}") from e | ||
|
||
|
|
||
| return sources | ||
|
|
||
|
|
||
| # TODO: Add tests! | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| from datapilot.clients.altimate.constants import SUPPORTED_ARTIFACT_TYPES | ||
| from datapilot.clients.altimate.utils import onboard_file | ||
|
|
||
|
|
||
| class TestOnboardFile: | ||
| def test_supported_artifact_types(self): | ||
| """Test that all expected artifact types are supported.""" | ||
| expected_types = {"manifest", "catalog", "run_results", "sources"} | ||
| assert SUPPORTED_ARTIFACT_TYPES == expected_types | ||
|
|
||
| def test_unsupported_file_type_returns_error(self): | ||
| """Test that unsupported file types return an error without making API calls.""" | ||
| test_token = "test_token" # noqa: S105 | ||
| result = onboard_file( | ||
| api_token=test_token, | ||
| tenant="test_tenant", | ||
| dbt_core_integration_id="test_id", | ||
| dbt_core_integration_environment="PROD", | ||
| file_type="unsupported_type", | ||
| file_path="test_path.json", | ||
| backend_url="http://localhost", | ||
| ) | ||
|
|
||
| assert result["ok"] is False | ||
| assert "Unsupported file type" in result["message"] | ||
| assert "unsupported_type" in result["message"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import pytest | ||
|
|
||
| from datapilot.core.platforms.dbt.utils import load_run_results | ||
| from datapilot.core.platforms.dbt.utils import load_sources | ||
| from datapilot.exceptions.exceptions import AltimateFileNotFoundError | ||
|
|
||
|
|
||
| class TestLoadRunResults: | ||
| def test_load_run_results_v6(self): | ||
| run_results_path = "tests/data/run_results_v6.json" | ||
| run_results = load_run_results(run_results_path) | ||
|
|
||
| assert run_results is not None | ||
| assert run_results.metadata.dbt_schema_version == "https://schemas.getdbt.com/dbt/run-results/v6.json" | ||
| assert len(run_results.results) == 1 | ||
| assert run_results.results[0].status.value == "success" | ||
| assert run_results.results[0].unique_id == "model.jaffle_shop.stg_customers" | ||
|
|
||
| def test_load_run_results_file_not_found(self): | ||
| with pytest.raises(AltimateFileNotFoundError): | ||
| load_run_results("nonexistent_file.json") | ||
|
|
||
|
|
||
| class TestLoadSources: | ||
| def test_load_sources_v3(self): | ||
| sources_path = "tests/data/sources_v3.json" | ||
| sources = load_sources(sources_path) | ||
|
|
||
| assert sources is not None | ||
| assert sources.metadata.dbt_schema_version == "https://schemas.getdbt.com/dbt/sources/v3.json" | ||
| assert len(sources.results) == 1 | ||
| assert sources.results[0].unique_id == "source.jaffle_shop.raw.customers" | ||
|
|
||
| def test_load_sources_file_not_found(self): | ||
| with pytest.raises(AltimateFileNotFoundError): | ||
| load_sources("nonexistent_file.json") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also add semantic manifest, we can already collect it, we should not validate it.