Skip to content
54 changes: 54 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
.PHONY: help venv install test test-vendor test-cov test-all clean lint format

## help - Display help about make targets for this Makefile
help:
@cat Makefile | grep '^## ' --color=never | cut -c4- | sed -e "`printf 's/ - /\t- /;'`" | column -s "`printf '\t'`" -t

## venv - Create virtual environment
venv:
python3 -m venv .venv
.venv/bin/pip install --upgrade pip
@echo ""
@echo "Virtual environment created. Activate with:"
@echo " source .venv/bin/activate"

## install - Install package and dependencies in development mode
install:
pip install -e .
pip install pytest pytest-cov tox pre-commit ruff

## test - Run tests quickly
test:
python -m pytest tests/ -v

## test-vendor - Run catalog vendor tests
test-vendor:
python -m pytest tests/test_vendor/test_catalog_v1.py -v

## test-cov - Run tests with coverage report
test-cov:
python -m pytest --cov=src --cov-report=term-missing --cov-report=html tests/ -v

## test-all - Run full test suite with tox (all Python/Pydantic versions)
test-all:
tox

## lint - Run code quality checks
lint:
pre-commit run --all-files

## format - Format code with ruff
format:
ruff format src/ tests/

## clean - Remove build artifacts and cache
clean:
rm -rf build/
rm -rf dist/
rm -rf *.egg-info
rm -rf .pytest_cache/
rm -rf .tox/
rm -rf htmlcov/
rm -rf .coverage
find . -type d -name __pycache__ -exec rm -rf {} +
find . -type f -name '*.pyc' -delete
19 changes: 19 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Root pytest configuration."""
import sys
from pathlib import Path

# Add src directory to Python path for all tests
# This needs to run at import time, before pytest collects tests
src_path = Path(__file__).parent.parent / "src"
if str(src_path) not in sys.path:
sys.path.insert(0, str(src_path))


def pytest_configure(config):
"""
Hook that runs before test collection.
Ensures src directory is in path before pytest imports test modules.
"""
# Double-check src is in path
if str(src_path) not in sys.path:
sys.path.insert(0, str(src_path))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this file is not required.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rationale for having the file: #87 (comment)

Empty file added tests/test_vendor/__init__.py
Empty file.
113 changes: 113 additions & 0 deletions tests/test_vendor/test_catalog_v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""Tests for catalog v1 parser, specifically testing extra fields handling."""
import pytest

from vendor.dbt_artifacts_parser.parsers.catalog.catalog_v1 import Metadata


class TestMetadataExtraFields:
"""Test that Metadata class accepts extra fields from dbt."""

def test_metadata_accepts_extra_fields(self):
"""Test that metadata accepts fields not explicitly defined in the model."""
# Test with a new field that dbt might add in the future
data = {
"dbt_schema_version": "https://schemas.getdbt.com/dbt/catalog/v1.json",
"dbt_version": "1.9.0",
"generated_at": "2025-11-05T10:00:00Z",
"invocation_id": "test-invocation-123",
"invocation_started_at": "2025-11-05T09:59:00Z", # New field
"new_future_field": "some_value", # Another potential future field
}

# This should not raise a validation error
metadata = Metadata(**data)

# Verify that known fields are accessible normally
assert metadata.dbt_schema_version == "https://schemas.getdbt.com/dbt/catalog/v1.json"
assert metadata.dbt_version == "1.9.0"
assert metadata.generated_at == "2025-11-05T10:00:00Z"
assert metadata.invocation_id == "test-invocation-123"

def test_metadata_extra_fields_in_pydantic_extra(self):
"""Test that extra fields are stored in __pydantic_extra__."""
data = {
"dbt_version": "1.9.0",
"invocation_started_at": "2025-11-05T09:59:00Z",
"new_field_1": "value1",
"new_field_2": 123,
}

metadata = Metadata(**data)

# Extra fields should be stored in __pydantic_extra__
assert metadata.__pydantic_extra__ is not None
assert "invocation_started_at" in metadata.__pydantic_extra__
assert "new_field_1" in metadata.__pydantic_extra__
assert "new_field_2" in metadata.__pydantic_extra__
assert metadata.__pydantic_extra__["invocation_started_at"] == "2025-11-05T09:59:00Z"
assert metadata.__pydantic_extra__["new_field_1"] == "value1"
assert metadata.__pydantic_extra__["new_field_2"] == 123

def test_metadata_model_dump_includes_extra_fields(self):
"""Test that model_dump() includes extra fields."""
data = {
"dbt_version": "1.9.0",
"invocation_id": "test-123",
"invocation_started_at": "2025-11-05T09:59:00Z",
"future_field": "future_value",
}

metadata = Metadata(**data)
dumped = metadata.model_dump()

# All fields including extra should be in the dump
assert dumped["dbt_version"] == "1.9.0"
assert dumped["invocation_id"] == "test-123"
assert dumped["invocation_started_at"] == "2025-11-05T09:59:00Z"
assert dumped["future_field"] == "future_value"

def test_metadata_with_no_extra_fields(self):
"""Test that metadata works normally when no extra fields are provided."""
data = {
"dbt_version": "1.9.0",
"generated_at": "2025-11-05T10:00:00Z",
}

metadata = Metadata(**data)

assert metadata.dbt_version == "1.9.0"
assert metadata.generated_at == "2025-11-05T10:00:00Z"

def test_metadata_with_only_extra_fields(self):
"""Test that metadata accepts data with only extra fields (all known fields are Optional)."""
data = {
"some_new_field": "value",
"another_new_field": 42,
}

# This should work since all defined fields are Optional
metadata = Metadata(**data)

assert metadata.__pydantic_extra__["some_new_field"] == "value"
assert metadata.__pydantic_extra__["another_new_field"] == 42

def test_invocation_started_at_as_extra_field(self):
"""Test the specific case of invocation_started_at being handled as an extra field."""
# This is the real-world scenario: dbt adds invocation_started_at
data = {
"dbt_schema_version": "https://schemas.getdbt.com/dbt/catalog/v1.json",
"dbt_version": "1.9.0",
"generated_at": "2025-11-05T10:00:00Z",
"invocation_id": "abc-123-def-456",
"invocation_started_at": "2025-11-05T09:55:30.123456Z",
}

# Should not raise ValidationError
metadata = Metadata(**data)

# The field should be accessible via __pydantic_extra__
assert metadata.__pydantic_extra__["invocation_started_at"] == "2025-11-05T09:55:30.123456Z"

# And should be included in model_dump()
dumped = metadata.model_dump()
assert dumped["invocation_started_at"] == "2025-11-05T09:55:30.123456Z"
Loading