Skip to content

Commit

Permalink
Updated contract language
Browse files Browse the repository at this point in the history
  • Loading branch information
tombaeyens committed Feb 26, 2025
1 parent d9ad1bc commit 02373ca
Show file tree
Hide file tree
Showing 21 changed files with 283 additions and 192 deletions.
10 changes: 7 additions & 3 deletions soda-core/src/soda_core/cli/soda.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,18 @@ def verify_contract(
for contract_file_path in contract_file_paths:
contract_verification_builder.with_contract_yaml_file(contract_file_path)

if not use_agent and data_source_file_path:
if data_source_file_path:
contract_verification_builder.with_data_source_yaml_file(data_source_file_path)

if use_agent:
contract_verification_builder.with_execution_on_soda_agent()

if soda_cloud_file_path:
if skip_publish:
contract_verification_builder.with_soda_cloud_skip_publish()
contract_verification_builder.with_soda_cloud_yaml_file(soda_cloud_file_path)

if skip_publish:
contract_verification_builder.with_soda_cloud_skip_publish()

contract_verification_result: ContractVerificationResult = contract_verification_builder.execute()
if contract_verification_result.has_failures():
exit(2)
Expand Down
1 change: 1 addition & 0 deletions soda-core/src/soda_core/common/logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Emoticons:
EXPLODING_HEAD: str = "\U0001F92F"
POLICE_CAR_LIGHT: str = "\U0001F6A8"
SEE_NO_EVIL: str = "\U0001F648"
PINCHED_FINGERS: str = "\U0001F90C"


class Location:
Expand Down
5 changes: 2 additions & 3 deletions soda-core/src/soda_core/common/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,12 +264,12 @@ def read_object(self, key: str) -> YamlObject | None:
"""
return self.read_value(key=key, expected_type=dict, required=True, default_value=None)

def read_object_opt(self, key: str) -> YamlObject | None:
def read_object_opt(self, key: str, default_value: Optional[dict] = None) -> YamlObject | None:
"""
An error is generated if the value is present and not a YAML object.
:return: a dict if the value for the key is a YAML object, otherwise None.
"""
return self.read_value(key=key, expected_type=dict, required=False, default_value=None)
return self.read_value(key=key, expected_type=dict, required=False, default_value=default_value)

def read_list(self, key: str, expected_element_type: type | None = None, required: bool = True) -> YamlList | None:
"""
Expand Down Expand Up @@ -384,7 +384,6 @@ def read_value(
elif key in self.yaml_dict:
value = self.yaml_dict.get(key)
else:

if required:
self.logs.error(
message=f"{Emoticons.POLICE_CAR_LIGHT} {key_description} is required",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from typing import Optional

from soda_core.common.data_source import DataSource
from soda_core.common.data_source_results import QueryResult
from soda_core.common.sql_dialect import *
Expand All @@ -17,13 +19,13 @@
class InvalidCheckParser(CheckParser):

def get_check_type_names(self) -> list[str]:
return ['invalid_count', 'invalid_percent']
return ['invalid']

def parse_check(
self,
contract_impl: ContractImpl,
column_impl: ColumnImpl | None,
check_yaml: MissingCheckYaml,
check_yaml: InvalidCheckYaml,
) -> CheckImpl | None:
return InvalidCheck(
contract_impl=contract_impl,
Expand Down Expand Up @@ -51,7 +53,10 @@ def __init__(
)

# TODO create better support in class hierarchy for common vs specific stuff. name is common. see other check type impls
metric_name: str = ThresholdImpl.get_metric_name(check_yaml.type_name, column_impl=column_impl)

self.metric_name = "invalid_percent" if check_yaml.metric == "percent" else "invalid_count"
metric_name: str = ThresholdImpl.get_metric_name(self.metric_name, column_impl=column_impl)

self.name = check_yaml.name if check_yaml.name else (
self.threshold.get_assertion_summary(metric_name=metric_name) if self.threshold
else f"{check_yaml.type_name} (invalid threshold)"
Expand All @@ -76,16 +81,15 @@ def __init__(
check_impl=self
))

if self.type == "invalid_percent":
self.row_count_metric = self._resolve_metric(RowCountMetric(
contract_impl=contract_impl,
))
self.row_count_metric = self._resolve_metric(RowCountMetric(
contract_impl=contract_impl,
))

self.invalid_percent_metric = self._resolve_metric(DerivedPercentageMetricImpl(
metric_type="invalid_percent",
fraction_metric_impl=self.invalid_count_metric_impl,
total_metric_impl=self.row_count_metric
))
self.invalid_percent_metric = self._resolve_metric(DerivedPercentageMetricImpl(
metric_type="invalid_percent",
fraction_metric_impl=self.invalid_count_metric_impl,
total_metric_impl=self.row_count_metric
))

def evaluate(self, measurement_values: MeasurementValues, contract_info: Contract) -> CheckResult:
outcome: CheckOutcome = CheckOutcome.NOT_EVALUATED
Expand All @@ -95,16 +99,15 @@ def evaluate(self, measurement_values: MeasurementValues, contract_info: Contrac
NumericDiagnostic(name="invalid_count", value=invalid_count)
]

threshold_value: Number | None = None
if self.type == "invalid_count":
threshold_value = invalid_count
else:
row_count: int = measurement_values.get_value(self.row_count_metric)
diagnostics.append(NumericDiagnostic(name="row_count", value=row_count))
if row_count > 0:
invalid_percent: float = measurement_values.get_value(self.invalid_percent_metric)
diagnostics.append(NumericDiagnostic(name="invalid_percent", value=invalid_percent))
threshold_value = invalid_percent
row_count: int = measurement_values.get_value(self.row_count_metric)
diagnostics.append(NumericDiagnostic(name="row_count", value=row_count))

invalid_percent: float = 0
if row_count > 0:
invalid_percent = measurement_values.get_value(self.invalid_percent_metric)
diagnostics.append(NumericDiagnostic(name="invalid_percent", value=invalid_percent))

threshold_value: Optional[Number] = invalid_percent if self.metric_name == "invalid_percent" else invalid_count

if self.threshold and isinstance(threshold_value, Number):
if self.threshold.passes(threshold_value):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
from __future__ import annotations

from soda_core.common.logs import Logs
from soda_core.common.yaml import YamlObject
from soda_core.contracts.impl.contract_yaml import CheckYaml, ColumnYaml, CheckYamlParser, MissingAncValidityCheckYaml


class InvalidCheckYamlParser(CheckYamlParser):

def get_check_type_names(self) -> list[str]:
return ['invalid_count', 'invalid_percent']
return ['invalid']

def parse_check_yaml(
self,
check_type_name: str,
check_yaml_object: YamlObject,
column_yaml: ColumnYaml | None,
logs: Logs
) -> CheckYaml | None:
return InvalidCheckYaml(
type_name=check_type_name,
check_yaml_object=check_yaml_object,
logs=logs
)


class InvalidCheckYaml(MissingAncValidityCheckYaml):

def __init__(self, check_yaml_object: YamlObject):
super().__init__(check_yaml_object)
def __init__(self, type_name: str, check_yaml_object: YamlObject, logs: Logs):
super().__init__(type_name=type_name, check_yaml_object=check_yaml_object, logs=logs)
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from typing import Optional

from soda_core.common.sql_dialect import *
from soda_core.contracts.contract_verification import CheckResult, CheckOutcome, Contract, Diagnostic, NumericDiagnostic
from soda_core.contracts.impl.check_types.missing_check_yaml import MissingCheckYaml
Expand Down Expand Up @@ -46,7 +48,9 @@ def __init__(
)

# TODO create better support in class hierarchy for common vs specific stuff. name is common. see other check type impls
metric_name: str = ThresholdImpl.get_metric_name(check_yaml.type_name, column_impl=column_impl)

self.metric_name = "missing_percent" if check_yaml.metric == "percent" else "missing_count"
metric_name: str = ThresholdImpl.get_metric_name(self.metric_name, column_impl=column_impl)
self.name = check_yaml.name if check_yaml.name else (
self.threshold.get_assertion_summary(metric_name=metric_name) if self.threshold
else f"{check_yaml.type_name} (invalid threshold)"
Expand Down Expand Up @@ -78,12 +82,12 @@ def evaluate(self, measurement_values: MeasurementValues, contract_info: Contrac
NumericDiagnostic(name="missing_count", value=missing_count)
]

threshold_value: Number | None = None
row_count: int = measurement_values.get_value(self.row_count_metric_impl)
diagnostics.append(NumericDiagnostic(name="row_count", value=row_count))
missing_percent: float = measurement_values.get_value(self.missing_percent_metric_impl)
diagnostics.append(NumericDiagnostic(name="missing_percent", value=missing_percent))
threshold_value = missing_percent

threshold_value: Optional[Number] = missing_percent if self.metric_name == "missing_percent" else missing_count

if self.threshold and isinstance(threshold_value, Number):
if self.threshold.passes(threshold_value):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
from __future__ import annotations

from typing import Optional

from soda_core.common.logs import Logs
from soda_core.common.yaml import YamlObject
from soda_core.contracts.impl.contract_yaml import CheckYaml, ColumnYaml, CheckYamlParser, MissingAncValidityCheckYaml


class MissingCheckYamlParser(CheckYamlParser):

def get_check_type_names(self) -> list[str]:
return ['missing_count', 'missing_percent']
return ['missing']

def parse_check_yaml(
self,
check_type_name: str,
check_yaml_object: YamlObject,
column_yaml: ColumnYaml | None,
logs: Logs
) -> CheckYaml | None:
return MissingCheckYaml(
type_name=check_type_name,
check_yaml_object=check_yaml_object,
logs=logs
)


class MissingCheckYaml(MissingAncValidityCheckYaml):

def __init__(self, type_name: str, check_yaml_object: YamlObject):
super().__init__(type_name=type_name, check_yaml_object=check_yaml_object)
def __init__(self, type_name: str, check_yaml_object: YamlObject, logs: Logs):
super().__init__(type_name=type_name, check_yaml_object=check_yaml_object, logs=logs)
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from __future__ import annotations

from typing import Optional

from soda_core.common.logs import Logs
from soda_core.common.yaml import YamlObject
from soda_core.contracts.impl.contract_yaml import CheckYamlParser, ColumnYaml, CheckYaml, ThresholdCheckYaml

Expand All @@ -11,20 +14,19 @@ def get_check_type_names(self) -> list[str]:

def parse_check_yaml(
self,
check_type_name: str,
check_yaml_object: YamlObject,
column_yaml: ColumnYaml | None,
logs: Logs
) -> CheckYaml | None:
return RowCountCheckYaml(
type_name=check_type_name,
check_yaml_object=check_yaml_object,
logs=logs
)


class RowCountCheckYaml(ThresholdCheckYaml):

def __init__(
self,
check_yaml_object: YamlObject,
):
super().__init__(
check_yaml_object=check_yaml_object
)
def __init__(self, type_name: str, check_yaml_object: YamlObject, logs: Logs):
super().__init__(type_name=type_name, check_yaml_object=check_yaml_object, logs=logs)
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from __future__ import annotations

from typing import Optional

from soda_core.common.logs import Logs
from soda_core.common.yaml import YamlObject
from soda_core.contracts.impl.contract_yaml import CheckYaml, ColumnYaml, CheckYamlParser

Expand All @@ -11,20 +14,19 @@ def get_check_type_names(self) -> list[str]:

def parse_check_yaml(
self,
check_type_name: str,
check_yaml_object: YamlObject,
column_yaml: ColumnYaml | None,
logs: Logs
) -> CheckYaml | None:
return SchemaCheckYaml(
type_name=check_type_name,
check_yaml_object=check_yaml_object,
logs=logs
)


class SchemaCheckYaml(CheckYaml):

def __init__(
self,
check_yaml_object: YamlObject,
):
super().__init__(
check_yaml_object=check_yaml_object,
)
def __init__(self, type_name: str, check_yaml_object: YamlObject, logs: Logs):
super().__init__(type_name=type_name, check_yaml_object=check_yaml_object, logs=logs)
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,12 @@ def verify_contracts_locally(
for contract_impl in contract_impls:
contract_result: ContractResult = contract_impl.verify()
contract_results.append(contract_result)
self._log_summary(contract_result)
if self.soda_cloud:
self.soda_cloud.send_contract_result(contract_result, self.skip_publish)
else:
self.logs.debug(f"Not sending results to Soda Cloud {Emoticons.CROSS_MARK}")

self._log_summary(contract_result)
finally:
if open_close:
data_source.close_connection()
Expand Down
Loading

0 comments on commit 02373ca

Please sign in to comment.