-
Notifications
You must be signed in to change notification settings - Fork 1
Checks
The library exposes the DataCheck class (defined in data_checks/classes/data_check.py) which you can use to write your checks. Begin by subclassing the DataCheck class:
from data_checks.classes.data_check import DataCheck
class MyFirstDataCheck(DataCheck):
passThen define methods that check the data and will be executed when the check is run. These methods are called rules and are prefixed with rule_. For example:
from data_checks.classes.data_check import DataCheck
class MyFirstDataCheck(DataCheck):
def rule_my_first_successful_rule(self, data="Hello World"):
# Call functions to check the data
assert data == "Hello World"
# Return anything if the rule passes
def rule_my_first_failed_rule(self):
# Call functions to check the data
# Throw an exception if the rule fails
assert False, "This rule failed"
def my_first_helper_function(self):
# This function will not be run as a rule
raise Exception("This function will not be run as a rule")defines two rules: rule_my_first_successful_rule and rule_my_first_failed_rule. The my_first_helper_function function will not be run as a rule.
A rule is considered a failure when an exception is raised. Exceptions and tracebacks are stored in the database for failures. Successful rules should throw no exceptions and can return anything (this can be accessed in Pre- and Post- Steps). Logs are stored for all runs.
Like unit tests, you can define a series of optional pre- and post- steps that are run for setup and teardown of a check and before and after each rule. These steps are useful for doing actions based off a check and/or rule and whether or not they succeed. These methods are:
-
setup(self): This method is run before any rules are run. Use this method to load data, initialize models, etc. For exampleself.df = pd.read_csv("data.csv")sets a dataframe for all the rules to use. This method is run only once per check. -
before(self, context): This method is run before each rule. -
on_success(self, context): This method is run when a rule succeeds. -
on_failure(self, context): This method is run when a rule fails. -
after(self, context): This method is run after each rule. This runs regardless of success or failure of the rule. -
teardown(self): This method is run after all rules are run. Use this method to close connections, etc. This method is run only once per check.
The context parameter is a dictionary that is passed between the four pre- and post- rule steps. You can access a few system defined keys by calling context.get_sys(key). These keys are:
-
context.get_sys("rule"): The name of the rule being run -
context.get_sys("params"): The parameters of the rule being run as a dictionary. Forrule_my_first_successful_rulein the example above,context.get_sys("params")would return:{ "args": (), "kwargs": {"data": "Hello World"} } - If the rule succeeds,
context.get_sys("result")(when called insideon_successandafter) will contain the return value of the rule. If the rule fails,context.get_sys("exception")(when called insideon_failureandafter) will return the exception that was raised.
This context is shared between these four functions. Thus you can use it to pass information between these steps by setting and getting like you would a dictionary. For example:
from data_checks.classes.data_check import DataCheck
class MyFirstDataCheck(DataCheck):
def setup(self):
"""
Setup the check. Use this to load data, initialize models, etc.
For example `self.df = pd.read_csv("data.csv")` sets a dataframe
for all the rules to use.
"""
super().setup() # DON'T FORGET TO CALL SUPER
print("Setup")
self.content = "Apple"
def before(self, context):
"""
Run before each rule. If None, the rule will not be run
"""
super().before(context) # DON'T FORGET TO CALL SUPER
print("Running before each rule")
print(context.get_sys("rule")) # Prints the name of the rule being run
print(context.get_sys("params")) # Prints the parameters of the rule being run
context["favorite_fruit"] = ["Apple", "Banana", "Orange"]
def rule_my_first_successful_rule(self, data="Hello World"):
# Call functions to check the data
assert data == "Hello World"
assert self.content == "Apple"
# Return nothing if the rule passes
return "Big Success!"
def rule_my_first_failed_rule(self):
# Call functions to check the data
# Throw an exception if the rule fails
assert False, "This rule failed"
def on_success(self, context):
"""
Called when a rule succeeds
"""
super().on_success(context) # DON'T FORGET TO CALL SUPER
print("Rule succeeded")
print(context.get_sys("result")) # Prints "Big Success!"
print(context["favorite_fruit"]) # Prints ["Apple", "Banana", "Orange"]
def on_failure(self, context):
"""
Called when a rule fails
"""
super().on_failure(context) # DON'T FORGET TO CALL SUPER
print("Rule failed")
print(context.get_sys("exception")) # Prints the exception that was raised
def after(self, context):
"""
Runs after each rule
"""
super().after(context) # DON'T FORGET TO CALL SUPER
print("Running after each rule")
print(context)
def teardown(self):
"""
Teardown the check. Use this to close connections, etc.
"""
super().teardown() # DON'T FORGET TO CALL SUPER
print("Teardown")
def my_first_helper_function(self):
# This function will not be run as a rule
raise Exception("This function will not be run as a rule")Running this check will print:
[1/1 checks] MyFirstDataCheck
Setup
[1/2 Rules] rule_my_first_successful_rule
Running before each rule
rule_my_first_successful_rule
{'args': (), 'kwargs': {}}
rule_my_first_successful_rule took 1.1920928955078125e-06 seconds
Rule succeeded
Big Success!
['Apple', 'Banana', 'Orange']
Running after each rule
{'sys': {'rule': 'rule_my_first_successful_rule', 'params': {'args': (), 'kwargs': {}}, 'result': 'Big Success!'}, 'favorite_fruit': ['Apple', 'Banana', 'Orange']}
[2/2 Rules] rule_my_first_failed_rule
Running before each rule
rule_my_first_failed_rule
{'args': (), 'kwargs': {}}
This rule failed
Rule failed
DataCheckException(severity=1.0, exception=This rule failed, metadata={'rule': 'rule_my_first_failed_rule', 'params': {'args': (), 'kwargs': {}}})
Running after each rule
{'sys': {'rule': 'rule_my_first_failed_rule', 'params': {'args': (), 'kwargs': {}}, 'exception': DataCheckException()}, 'favorite_fruit': ['Apple', 'Banana', 'Orange']}
TeardownβIMPORTANT
Make sure to callsuper().setup(),super().before(context),super().on_success(context),super().on_failure(context),super().after(context), andsuper().teardown()in your methods. Their omission may break the scheduling and alerting features of this library.
In addition to these methods, you can also define these two additional methods:
from data_checks.classes.data_check import DataCheck
class MyFirstDataCheck(DataCheck):
...
@classmethod
def defined_rules(cls) -> list[str]:
return [
"rule_my_first_successful_rule"
]
@classmethod
def check_config(cls) -> dict:
return {
"param1": value1,
"param2": value2,
"rules_config": { # Configuration for each rule. If not provided, rule config will be empty dictionary.
"rule_1": {
"silenced_until": "2021-08-22 00:00:00.359828-00", # "%Y-%m-%d %H:%M:%S.%f%z" formatted date or datetime until which the rule will be silenced. If not provided, rule will not be silenced.
...
},
"rule_2": {
...
},
}
}
...-
defined_rules(cls) -> list[str]: This method returns a list of rules that should be run. If this method is not defined, all rules (with the prefix) will be run. This method is useful if you want to run only a subset of rules. For example, you can use this method to run only rules that are not silenced. You may also override this method in a subclass to exclude certain rules. -
check_config(cls) -> dict: This method returns a dictionary of configurations for the check. These configuration options are stored in the database and can be used to define additional features for your checks. For instance you may use a description field to describe the check. Note that therules_configare not stored with checks but instead with each respective rule (i.e.rule_1configuration options are stored in the Rules database table). For each rule, you can define asilenced_untilfield that silences the rule until that point.
Your check should be written inside the specified CHECKS_MODULE in your settings file. For example, if you set CHECKS_MODULE = "my_checks", then you should write your check in my_checks/my_first_data_check.py. Make sure that CHECKS_MODULE and any nested modules are properly defined as directories (i.e. have an __init__.py file). Doing this allows the library to automatically find and run checks.
DataCheck also defines additional methods as well as instance variables that can are used to instantiate and run checks. The DataCheck class exposes the following instance variables:
-
name: The name of the check. This is used to identify the check in the database. If not provided, the name of the class is used. -
rules_params: A dictionary of parameters for each rule. The keys are the names of the rules and the values are the parameters to be passed to that rule. If a rule does not have any required parameters or has default parameters values that work, you can omit it from the dictionary. If a rule has parameters, you must provide them in the dictionary. If a rule has parameters but you do not provide them, an exception will be thrown. If a rule does not have parameters and you provide them, an exception will be thrown. Assume you have the following rule:from data_checks.classes.data_check import DataCheck class MyFirstDataCheck(DataCheck): ... def rule_with_required_arguments(self, data): assert data == "Hello World", "This rule failed" ...
You can define the
dataparameter value ofrule_with_required_argumentsin several ways:- A dictionary of keyword arguments:
check = MyFirstDataCheck( rules_params={"rule_with_required_arguments": {"data": "Hello World"}} )
- A tuple of positional arguments
check = MyFirstDataCheck( rules_params={"rule_with_required_arguments": ("Hello World",)} )
- A dictionary with a
argskey for positional arguments and akwargskey for keyword argumentscheck = MyFirstDataCheck( rules_params={"rule_with_required_arguments": {"args": ("Hello World",), "kwargs": {}}} )
- A list of any of the three previous options. This runs the rule multiple times with each set of arguments.
check = MyFirstDataCheck( rules_params={ "rule_with_required_arguments": [ {"data": "Hello World"}, ("Hello World",), {"args": ("Hello World",), "kwargs": {}}, ] } )
- A callable that returns any of the four previous options. This allows you to dynamically define the arguments. The callable can have any arguments but should return one of the four options.
check = MyFirstDataCheck( rules_params={"rule_with_required_arguments": lambda: {"data": "Hello World"}} )
- A dictionary of keyword arguments:
-
excluded_rules: A list of rules to exclude from the check. This is useful if you want to run all rules except for a few. For example, you can use this to exclude rules that are silenced. -
only_run_specified_rules: A boolean that determines whether to run only the rules specified inrules_params. IfTrue, only the rules specified inrules_paramswill be run. IfFalse, all rules will be run except for those specified inexcluded_rules. -
Additional keyword arguments that are passed to the
Checkclass become instance fields of the check.
You can pass these arguments to the DataCheck constructor. For example:
from my_checks.my_first_data_check import MyFirstDataCheck
check = MyFirstDataCheck(
name="My First Data Check",
rules_params={"rule_with_required_arguments": {"data": "Hello World"}},
excluded_rules=["rule_my_first_failed_rule"],
only_run_specified_rules=True,
param1=value1,
param2=value2,
)creates a check with the name "My First Data Check" that runs only the rule_with_required_arguments rule with the data parameter set to "Hello World" and excludes the rule_my_first_failed_rule rule. The check also has the param1 and param2 instance fields set to value1 and value2 respectively that can be accessed via check.param1 and check.param2.
You can call a few built-in methods on the check. These are:
-
get_rules_to_run(): Returns a set of rules that will be run. This is useful if you want to know which rules will be run before running the check. -
run_all(): Runs all rules in the check. This is the default method that is called when you run a check. -
run_all_async(): Runs all rules in the check asynchronously. -
run(rule): Runs a specific rule. -
run_async(rule, wait_for_completion=True): Runs a specific rule asynchronously. Ifwait_for_completionisTrue, the method will wait for the rule to finish before returning. IfFalse, the method will return a list ofProcessobjects that can be used to wait for the rule to finish (i.e. by callingprocess.join()).
You can also define rules with arguments. For example:
from data_checks.classes.data_check import DataCheck
class MyFirstDataCheck(DataCheck):
...
# This rule has an argument
def rule_with_required_arguments(self, data):
assert data == "Hello World", "This rule failed"
...To run this check, you must pass in the arguments. For example:
# run_check.py
from my_checks.my_first_data_check import MyFirstDataCheck
check = MyFirstDataCheck(
rules_params={"rule_with_required_arguments": {"data": "Hello World1"}}
)
check.run_all() # check.run_all_async() to run all rules asynchronouslyRun this check with python run_check.py. The output should be:
[1/3 Rules] rule_my_first_successful_rule
rule_my_first_successful_rule took 9.5367431640625e-07 seconds
[2/3 Rules] rule_with_required_arguments
This rule failed
[3/3 Rules] rule_my_first_failed_rule
This rule failed
A similar process applies for checks that are run within a suite (see [Advanced] Suites). Note that checks with arguments cannot be run from the command line.
If you have existing check classes, you can still subclass DataCheck and use the library in the same manner noted above. Make sure that your class does not accidentally override any of the methods in Check (see data_checks/base/check.py)
The DataCheck class is a simplified and beginner friendly subclass of the base Check class (data_checks/base/check.py). The user can also directly subclass the Check class to create more advanced checks (see References).