Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- Internal logging uses named module loggers instead of the root logger

## [1.1.3] - 2026-09-03

### Added
Expand Down
14 changes: 8 additions & 6 deletions datacontract/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
from datacontract.model.exceptions import DataContractException, DefinitionResolutionError
from datacontract.model.run import Check, ResultEnum, Run

logger = logging.getLogger(__name__)

DATA_CONTRACT_EXAMPLE_PAYLOAD = """apiVersion: v3.1.0
kind: DataContract
id: orders
Expand Down Expand Up @@ -665,21 +667,21 @@ def _reject_request_platform_host_with_environment_key(config) -> None:
def check_api_key(api_key_header: str | None):
correct_api_key = os.getenv("DATACONTRACT_CLI_API_KEY")
if correct_api_key is None or correct_api_key == "":
logging.info("Environment variable DATACONTRACT_CLI_API_KEY is not set. Skip API key check.")
logger.info("Environment variable DATACONTRACT_CLI_API_KEY is not set. Skip API key check.")
return
if api_key_header is None or api_key_header == "":
logging.info("The API key is missing.")
logger.info("The API key is missing.")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing API key. Use Header 'x-api-key' to provide the API key.",
)
if not secrets.compare_digest(api_key_header, correct_api_key):
logging.info("The provided API key is not correct.")
logger.info("The provided API key is not correct.")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="The provided API key is not correct.",
)
logging.info("Request authenticated with API key.")
logger.info("Request authenticated with API key.")
pass


Expand Down Expand Up @@ -777,8 +779,8 @@ async def test(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Use either the filter or the filters parameter, not both.",
)
logging.info("Testing data contract...")
logging.info(body)
logger.info("Testing data contract...")
logger.info(body)
config = config_from_headers(request.headers)
untrusted_contract = getattr(request.app.state, "untrusted_contracts", False)
if untrusted_contract:
Expand Down
4 changes: 3 additions & 1 deletion datacontract/catalog/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from datacontract.data_contract import DataContract
from datacontract.export.html_exporter import get_version

logger = logging.getLogger(__name__)


def _get_owner(odcs: OpenDataContractStandard) -> Optional[str]:
"""Get the owner from ODCS customProperties or team."""
Expand All @@ -25,7 +27,7 @@ def _get_owner(odcs: OpenDataContractStandard) -> Optional[str]:


def create_data_contract_html(contracts, file: Path, path: Path, schema: str):
logging.debug(f"Creating data contract html for file {file} and schema {schema}")
logger.debug(f"Creating data contract html for file {file} and schema {schema}")
data_contract = DataContract(
data_contract_file=f"{file.absolute()}", inline_references=True, schema_location=schema, config=cli_config()
)
Expand Down
4 changes: 3 additions & 1 deletion datacontract/data_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from datacontract.model.exceptions import DataContractException, DataContractValidationErrors
from datacontract.model.run import Check, ResultEnum, Run

logger = logging.getLogger(__name__)


class DataContract:
def __init__(
Expand Down Expand Up @@ -208,7 +210,7 @@ def test(self) -> Run:
engine="datacontract-cli",
)
)
logging.exception("Exception occurred")
logger.exception("Exception occurred")
run.log_error(str(e))

run.finish()
Expand Down
10 changes: 6 additions & 4 deletions datacontract/engines/fastjsonschema/check_jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from datacontract.model.exceptions import DataContractException
from datacontract.model.run import Check, ResultEnum, Run

logger = logging.getLogger(__name__)

# Thread-safe cache for primaryKey fields.
_primary_key_cache = {}
_cache_lock = threading.Lock()
Expand Down Expand Up @@ -92,13 +94,13 @@ def process_exceptions(run, exceptions: List[DataContractException], config: Con
def validate_json_stream(
schema: dict, model_name: str, validate: Callable, json_stream: Generator[Any, Any, None]
) -> List[DataContractException]:
logging.info(f"Validating JSON stream for model: '{model_name}'.")
logger.info(f"Validating JSON stream for model: '{model_name}'.")
exceptions: List[DataContractException] = []
for json_obj in json_stream:
try:
validate(json_obj)
except JsonSchemaValueException as e:
logging.warning(f"Validation failed for JSON object with type: '{model_name}'.")
logger.warning(f"Validation failed for JSON object with type: '{model_name}'.")
primary_key_value = get_primary_key_value(schema, model_name, json_obj)
exceptions.append(
DataContractException(
Expand All @@ -112,7 +114,7 @@ def validate_json_stream(
)
)
if not exceptions:
logging.info(f"All JSON objects in the stream passed validation for model: '{model_name}'.")
logger.info(f"All JSON objects in the stream passed validation for model: '{model_name}'.")
return exceptions


Expand Down Expand Up @@ -201,7 +203,7 @@ def process_local_file(run, server, schema, model_name, validate, config: Config
)

for file in all_files:
logging.info(f"Processing file: {file}")
logger.info(f"Processing file: {file}")
with open(file, "r") as f:
process_json_file(run, schema, model_name, validate, f, server.delimiter, config)

Expand Down
4 changes: 3 additions & 1 deletion datacontract/engines/fastjsonschema/s3/s3_read_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
from datacontract.model.exceptions import DataContractException
from datacontract.model.run import ResultEnum

logger = logging.getLogger(__name__)


def yield_s3_files(s3_endpoint_url, s3_location, config: Config | None = None):
fs = s3_fs(s3_endpoint_url, config)
files = fs.glob(s3_location)
for file in files:
with fs.open(file) as f:
logging.info(f"Downloading file {file}")
logger.info(f"Downloading file {file}")
yield f.read()


Expand Down
2 changes: 2 additions & 0 deletions datacontract/engines/ibis/connections/kafka.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@

_SASL_MECHANISMS = ("PLAIN", "SCRAM-SHA-256", "SCRAM-SHA-512")

logger = logging.getLogger(__name__)


def _import(module: str):
try:
Expand Down
4 changes: 3 additions & 1 deletion datacontract/export/html_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from datacontract.export.exporter import Exporter
from datacontract.export.mermaid_exporter import to_mermaid

logger = logging.getLogger(__name__)


class HtmlExporter(Exporter):
def export(self, data_contract, schema_name, server, sql_server_type, export_args) -> str:
Expand Down Expand Up @@ -66,5 +68,5 @@ def get_version() -> str:
try:
return version("datacontract_cli")
except Exception as e:
logging.debug("Ignoring exception", e)
logger.debug("Ignoring exception", e)
return ""
4 changes: 3 additions & 1 deletion datacontract/imports/powerbi_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from datacontract.imports.odcs_helper import create_odcs, create_property, create_schema_object, create_server
from datacontract.model.exceptions import DataContractException

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Power BI data type → (ODCS logical type, optional format)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -217,7 +219,7 @@ def _build_odcs(bim: dict[str, Any], model_name: str) -> OpenDataContractStandar
_apply_bim_relationships(bim_relationships, table_name_to_obj)

if not schema_objects:
logging.warning("Power BI import produced an empty contract: No tables were found in the semantic model.")
logger.warning("Power BI import produced an empty contract: No tables were found in the semantic model.")

schema_objects.sort(key=lambda s: s.name.lower())
odcs.schema_ = schema_objects
Expand Down
6 changes: 4 additions & 2 deletions datacontract/imports/sql_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from datacontract.model.exceptions import DataContractException
from datacontract.model.run import ResultEnum

logger = logging.getLogger(__name__)


class SqlDialect(str, Enum):
postgres = "postgres"
Expand Down Expand Up @@ -45,7 +47,7 @@ def import_sql(source: str, import_args: dict = None) -> OpenDataContractStandar
# not parse_one: sqlglot below 29 gives it only the first statement of a script
statements = [s for s in sqlglot.parse(sql=sql, read=dialect) if s is not None]
except Exception as e:
logging.error(f"Error sqlglot SQL: {str(e)}")
logger.error(f"Error sqlglot SQL: {str(e)}")
raise DataContractException(
type="import",
name=f"Reading source from {source}",
Expand All @@ -64,7 +66,7 @@ def import_sql(source: str, import_args: dict = None) -> OpenDataContractStandar
server_defaults.update(location)
odcs.servers = [create_server(name=server_type, server_type=server_type, **server_defaults)]
placeholders = ", ".join(field for field in server_defaults if field not in location)
logging.warning(
logger.warning(
f"SQL import generated a server block with placeholder connection values. "
f"Update the following values before use: {placeholders}"
)
Expand Down
4 changes: 3 additions & 1 deletion datacontract/init/init_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@

DEFAULT_DATA_CONTRACT_INIT_TEMPLATE = "odcs-3.1.0.init.yaml"

logger = logging.getLogger(__name__)


def get_init_template(location: str = None) -> str:
if location is None:
logging.info("Use default bundled template " + DEFAULT_DATA_CONTRACT_INIT_TEMPLATE)
logger.info("Use default bundled template " + DEFAULT_DATA_CONTRACT_INIT_TEMPLATE)
schemas = resources.files("datacontract")
template = schemas.joinpath("schemas", DEFAULT_DATA_CONTRACT_INIT_TEMPLATE)
with template.open("r") as file:
Expand Down
24 changes: 13 additions & 11 deletions datacontract/lint/resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from datacontract.model.odcs import is_open_data_contract_standard, is_open_data_product_standard
from datacontract.model.run import ResultEnum

logger = logging.getLogger(__name__)


class _LaxOpenDataContractStandard(OpenDataContractStandard):
"""ODCS variant that accepts unknown top-level fields.
Expand Down Expand Up @@ -72,7 +74,7 @@ def _resolve_jsonschema_compliance_error_message_path(yaml_str, message):
f"properties.{yaml_str['schema'][int(schema_index)]['properties'][int(property_index)]['name']}",
)
except Exception:
logging.warning("YAML doesn't conform to JSON schema. Could not resolve indexed schema or property names.")
logger.warning("YAML doesn't conform to JSON schema. Could not resolve indexed schema or property names.")
except_message = message

return except_message
Expand Down Expand Up @@ -599,7 +601,7 @@ def _resolve_data_contract_from_str(
)

if is_open_data_product_standard(yaml_dict):
logging.info("Cannot import ODPS, as not supported")
logger.info("Cannot import ODPS, as not supported")
raise DataContractException(
type="schema",
result=ResultEnum.failed,
Expand All @@ -609,7 +611,7 @@ def _resolve_data_contract_from_str(
)

if is_open_data_contract_standard(yaml_dict):
logging.info("Importing ODCS v3")
logger.info("Importing ODCS v3")
# When a custom JSON schema is provided, treat it as the source of
# truth and accept extra top-level fields the standard ODCS Pydantic
# class would reject.
Expand All @@ -626,7 +628,7 @@ def _resolve_data_contract_from_str(
return odcs

# For DCS format, we need to convert it to ODCS
logging.info("Importing DCS format - converting to ODCS")
logger.info("Importing DCS format - converting to ODCS")
from datacontract.imports.dcs_importer import convert_dcs_to_odcs, parse_dcs_from_dict

dcs = parse_dcs_from_dict(yaml_dict)
Expand Down Expand Up @@ -665,7 +667,7 @@ def _to_yaml(data_contract_str) -> dict:
try:
return yaml.load(data_contract_str, Loader=_SafeLoaderNoTimestamp)
except Exception as e:
logging.warning(f"Cannot parse YAML. Error: {str(e)}")
logger.warning(f"Cannot parse YAML. Error: {str(e)}")
raise DataContractException(
type="lint",
result="failed",
Expand All @@ -687,28 +689,28 @@ def _validation_error_to_exception(error_message: str, original_exception=None)


def _validate_json_schema(yaml_str, schema_location: str | Path = None, all_errors: bool = False):
logging.debug(f"Linting data contract with schema at {schema_location}")
logger.debug(f"Linting data contract with schema at {schema_location}")
schema = fetch_schema(schema_location)
if all_errors:
validator_cls = validators.validator_for(schema)
validator_cls.check_schema(schema)
validator = validator_cls(schema=schema)
errors = sorted(validator.iter_errors(yaml_str), key=lambda error: list(error.path))
if errors:
logging.warning(f"Data Contract YAML is invalid. Validation errors: {len(errors)}")
logger.warning(f"Data Contract YAML is invalid. Validation errors: {len(errors)}")
raise DataContractValidationErrors(
[_validation_error_to_exception(error.message, original_exception=error) for error in errors]
)
logging.debug("YAML data is valid.")
logger.debug("YAML data is valid.")
return
try:
fastjsonschema.validate(schema, yaml_str, use_default=False)
logging.debug("YAML data is valid.")
logger.debug("YAML data is valid.")
except JsonSchemaValueException as e:
except_message = _resolve_jsonschema_compliance_error_message_path(yaml_str, e.message)

logging.warning(f"Data Contract YAML is invalid. Validation error: {except_message}")
logger.warning(f"Data Contract YAML is invalid. Validation error: {except_message}")
raise _validation_error_to_exception(except_message, original_exception=e)
except Exception as e:
logging.warning(f"Data Contract YAML is invalid. Validation error: {str(e)}")
logger.warning(f"Data Contract YAML is invalid. Validation error: {str(e)}")
raise _validation_error_to_exception(str(e), original_exception=e)
8 changes: 5 additions & 3 deletions datacontract/lint/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

DEFAULT_DATA_CONTRACT_SCHEMA = "datacontract-1.2.1.schema.json"

logger = logging.getLogger(__name__)


def fetch_schema(location: str | Path = None) -> Dict[str, Any]:
"""
Expand All @@ -33,7 +35,7 @@ def fetch_schema(location: str | Path = None) -> Dict[str, Any]:

"""
if location is None:
logging.info("Use default bundled schema " + DEFAULT_DATA_CONTRACT_SCHEMA)
logger.info("Use default bundled schema " + DEFAULT_DATA_CONTRACT_SCHEMA)
schemas = resources.files("datacontract")
schema_file = schemas.joinpath("schemas", DEFAULT_DATA_CONTRACT_SCHEMA)
with schema_file.open("r") as file:
Expand All @@ -43,7 +45,7 @@ def fetch_schema(location: str | Path = None) -> Dict[str, Any]:
location_str = str(location)

if location_str.startswith("http://") or location_str.startswith("https://"):
logging.debug(f"Downloading schema from {location_str}")
logger.debug(f"Downloading schema from {location_str}")
response = requests.get(location_str)
schema = response.json()
else:
Expand All @@ -56,7 +58,7 @@ def fetch_schema(location: str | Path = None) -> Dict[str, Any]:
result=ResultEnum.error,
)

logging.debug(f"Loading JSON schema locally at {location}")
logger.debug(f"Loading JSON schema locally at {location}")
with open(location, "r") as file:
schema = json.load(file)

Expand Down
9 changes: 6 additions & 3 deletions datacontract/model/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ def setter(self, value):
return property(getter, setter, doc=message)


logger = logging.getLogger(__name__)


class ResultEnum(str, Enum):
"""The outcome of a check or of a whole test run."""

Expand Down Expand Up @@ -259,15 +262,15 @@ def calculate_result(self):
self.result = ResultEnum.unknown

def log_info(self, message: str):
logging.info(message)
logger.info(message)
self.logs.append(Log(level="INFO", message=message, timestamp=datetime.now(timezone.utc)))

def log_warn(self, message: str):
logging.warning(message)
logger.warning(message)
self.logs.append(Log(level="WARN", message=message, timestamp=datetime.now(timezone.utc)))

def log_error(self, message: str):
logging.error(message)
logger.error(message)
self.logs.append(Log(level="ERROR", message=message, timestamp=datetime.now(timezone.utc)))

def pretty(self):
Expand Down
1 change: 0 additions & 1 deletion tests/test_import_powerbi.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,6 @@ def test_import_bim_calculated_table_physical_type():


def test_import_pbit_from_zip(tmp_path):

result = import_powerbi_from_file(PBIT_FIXTURE)

assert result is not None
Expand Down