---------------------------------------------------------------------- This is the API documentation for the pointblank library. ---------------------------------------------------------------------- ## Validate When performing data validation, use the `Validate` class to get the process started. It takes the target table and options for metadata and failure thresholds (using the `Thresholds` class or shorthands). The `Validate` class has numerous methods for defining validation steps and for obtaining post-interrogation metrics and data. Validate(data: 'IntoDataFrame', reference: 'IntoFrame | None' = None, tbl_name: 'str | None' = None, label: 'str | None' = None, thresholds: 'int | float | bool | tuple | dict | Thresholds | None' = None, actions: 'Actions | None' = None, final_actions: 'FinalActions | None' = None, brief: 'str | bool | None' = None, lang: 'str | None' = None, locale: 'str | None' = None, owner: 'str | None' = None, consumers: 'str | list[str] | None' = None, version: 'str | None' = None) -> None Workflow for defining a set of validations on a table and interrogating for results. The `Validate` class is used for defining a set of validation steps on a table and interrogating the table with the *validation plan*. This class is the main entry point for the *data quality reporting* workflow. The overall aim of this workflow is to generate comprehensive reporting information to assess the level of data quality for a target table. We can supply as many validation steps as needed, and having a large number of them should increase the validation coverage for a given table. The validation methods (e.g., [`col_vals_gt()`](`pointblank.Validate.col_vals_gt`), [`col_vals_between()`](`pointblank.Validate.col_vals_between`), etc.) translate to discrete validation steps, where each step will be sequentially numbered (useful when viewing the reporting data). This process of calling validation methods is known as developing a *validation plan*. The validation methods, when called, are merely instructions up to the point the concluding [`interrogate()`](`pointblank.Validate.interrogate`) method is called. That kicks off the process of acting on the *validation plan* by querying the target table getting reporting results for each step. Once the interrogation process is complete, we can say that the workflow now has reporting information. We can then extract useful information from the reporting data to understand the quality of the table. Printing the `Validate` object (or using the [`get_tabular_report()`](`pointblank.Validate.get_tabular_report`) method) will return a table with the results of the interrogation and [`get_sundered_data()`](`pointblank.Validate.get_sundered_data`) allows for the splitting of the table based on passing and failing rows. Parameters ---------- data The table to validate, which could be a DataFrame object, an Ibis table object, a CSV file path, a Parquet file path, a GitHub URL pointing to a CSV or Parquet file, or a database connection string. When providing a CSV or Parquet file path (as a string or `pathlib.Path` object), the file will be automatically loaded using an available DataFrame library (Polars or Pandas). Parquet input also supports glob patterns, directories containing .parquet files, and Spark-style partitioned datasets. GitHub URLs are automatically transformed to raw content URLs and downloaded. Connection strings enable direct database access via Ibis with optional table specification using the `::table_name` suffix. Read the *Supported Input Table Types* section for details on the supported table types. reference An optional second table to compare the target table against, such as an earlier extract of the same data or a trusted baseline. It can be any of the input types accepted by `data=`. Reference data is used by the aggregate validation methods (the `col_sum_*`, `col_avg_*`, and `col_sd_*` families, such as [`col_sum_eq()`](`pointblank.Validate.col_sum_eq`)): a `value=` given with [`ref()`](`pointblank.ref`) takes the aggregate of a column in this table, and leaving out `value=` compares against the same column here. A file path or connection string given here is kept, so that [`to_code()`](`pointblank.Validate.to_code`) and [`to_yaml()`](`pointblank.Validate.to_yaml`) can write it back out. tbl_name An optional name to assign to the input table object. If no value is provided, a name will be generated based on whatever information is available. This table name will be displayed in the header area of the tabular report. label An optional label for the validation plan. If no value is provided, a label will be generated based on the current system date and time. Markdown can be used here to make the label more visually appealing (it will appear in the header area of the tabular report). thresholds Generate threshold failure levels so that all validation steps can report and react accordingly when exceeding the set levels. The thresholds are set at the global level and can be overridden at the validation step level (each validation step has its own `thresholds=` parameter). The default is `None`, which means that no thresholds will be set. Look at the *Thresholds* section for information on how to set threshold levels. actions The actions to take when validation steps meet or exceed any set threshold levels. These actions are paired with the threshold levels and are executed during the interrogation process when there are exceedances. The actions are executed right after each step is evaluated. Such actions should be provided in the form of an `Actions` object. If `None` then no global actions will be set. View the *Actions* section for information on how to set actions. final_actions The actions to take when the validation process is complete and the final results are available. This is useful for sending notifications or reporting the overall status of the validation process. The final actions are executed after all validation steps have been processed and the results have been collected. The final actions are not tied to any threshold levels, they are executed regardless of the validation results. Such actions should be provided in the form of a `FinalActions` object. If `None` then no finalizing actions will be set. Please see the *Actions* section for information on how to set final actions. brief A global setting for briefs, which are optional brief descriptions for validation steps (they be displayed in the reporting table). For such a global setting, templating elements like `"{step}"` (to insert the step number) or `"{auto}"` (to include an automatically generated brief) are useful. If `True` then each brief will be automatically generated. If `None` (the default) then briefs aren't globally set. lang The language to use for various reporting elements. By default, `None` will select English (`"en"`) as the but other options include French (`"fr"`), German (`"de"`), Italian (`"it"`), Spanish (`"es"`), and several more. Have a look at the *Reporting Languages* section for the full list of supported languages and information on how the language setting is utilized. locale An optional locale ID to use for formatting values in the reporting table according the locale's rules. Examples include `"en-US"` for English (United States) and `"fr-FR"` for French (France). More simply, this can be a language identifier without a designation of territory, like `"es"` for Spanish. owner An optional string identifying the owner of the data being validated. This is useful for governance purposes, indicating who is responsible for the quality and maintenance of the data. For example, `"data-platform-team"` or `"analytics-engineering"`. consumers An optional string or list of strings identifying who depends on or consumes this data. This helps document data dependencies and can be useful for impact analysis when data quality issues are detected. For example, `"ml-team"` or `["ml-team", "analytics"]`. version An optional string representing the version of the validation plan or data contract. This supports semantic versioning (e.g., `"1.0.0"`, `"2.1.0"`) and is useful for tracking changes to validation rules over time and for organizational governance. Returns ------- Validate A `Validate` object with the table and validations to be performed. Supported Input Table Types --------------------------- The `data=` parameter can be given any of the following table types: - Polars DataFrame (`"polars"`) - Pandas DataFrame (`"pandas"`) - PySpark table (`"pyspark"`) - DuckDB table (`"duckdb"`)* - MySQL table (`"mysql"`)* - PostgreSQL table (`"postgresql"`)* - SQLite table (`"sqlite"`)* - Microsoft SQL Server table (`"mssql"`)* - Snowflake table (`"snowflake"`)* - Databricks table (`"databricks"`)* - BigQuery table (`"bigquery"`)* - Parquet table (`"parquet"`)* - CSV files (string path or `pathlib.Path` object with `.csv` extension) - Parquet files (string path, `pathlib.Path` object, glob pattern, directory with `.parquet` extension, or partitioned dataset) - Database connection strings (URI format with optional table specification) The table types marked with an asterisk need to be prepared as Ibis tables (with type of `ibis.expr.types.relations.Table`). Furthermore, the use of `Validate` with such tables requires the Ibis library v9.5.0 and above to be installed. If the input table is a Polars or Pandas DataFrame, the Ibis library is not required. To use a CSV file, ensure that a string or `pathlib.Path` object with a `.csv` extension is provided. The file will be automatically detected and loaded using the best available DataFrame library. The loading preference is Polars first, then Pandas as a fallback. Connection strings follow database URL formats and must also specify a table using the `::table_name` suffix. Examples include: ``` "duckdb:///path/to/database.ddb::table_name" "sqlite:///path/to/database.db::table_name" "postgresql://user:password@localhost:5432/database::table_name" "mysql://user:password@localhost:3306/database::table_name" "bigquery://project/dataset::table_name" "snowflake://user:password@account/database/schema::table_name" ``` When using connection strings, the Ibis library with the appropriate backend driver is required. Thresholds ---------- The `thresholds=` parameter is used to set the failure-condition levels for all validation steps. They are set here at the global level but can be overridden at the validation step level (each validation step has its own local `thresholds=` parameter). There are three threshold levels: 'warning', 'error', and 'critical'. The threshold values can either be set as a proportion failing of all test units (a value between `0` to `1`), or, the absolute number of failing test units (as integer that's `1` or greater). Thresholds can be defined using one of these input schemes: 1. use the [`Thresholds`](`pointblank.Thresholds`) class (the most direct way to create thresholds) 2. provide a tuple of 1-3 values, where position `0` is the 'warning' level, position `1` is the 'error' level, and position `2` is the 'critical' level 3. create a dictionary of 1-3 value entries; the valid keys: are 'warning', 'error', and 'critical' 4. a single integer/float value denoting absolute number or fraction of failing test units for the 'warning' level only If the number of failing test units for a validation step exceeds set thresholds, the validation step will be marked as 'warning', 'error', or 'critical'. All of the threshold levels don't need to be set, you're free to set any combination of them. Aside from reporting failure conditions, thresholds can be used to determine the actions to take for each level of failure (using the `actions=` parameter). Actions ------- The `actions=` and `final_actions=` parameters provide mechanisms to respond to validation results. These actions can be used to notify users of validation failures, log issues, or trigger other processes when problems are detected. *Step Actions* The `actions=` parameter allows you to define actions that are triggered when validation steps exceed specific threshold levels (warning, error, or critical). These actions are executed during the interrogation process, right after each step is evaluated. Step actions should be provided using the [`Actions`](`pointblank.Actions`) class, which lets you specify different actions for different severity levels: ```python # Define an action that logs a message when warning threshold is exceeded def log_warning(): metadata = pb.get_action_metadata() print(f"WARNING: Step {metadata['step']} failed with type {metadata['type']}") # Define actions for different threshold levels actions = pb.Actions( warning = log_warning, error = lambda: send_email("Error in validation"), critical = "CRITICAL FAILURE DETECTED" ) # Use in Validate validation = pb.Validate( data=my_data, actions=actions # Global actions for all steps ) ``` You can also provide step-specific actions in individual validation methods: ```python validation.col_vals_gt( columns="revenue", value=0, actions=pb.Actions(warning=log_warning) # Only applies to this step ) ``` Step actions have access to step-specific context through the [`get_action_metadata()`](`pointblank.get_action_metadata`) function, which provides details about the current validation step that triggered the action. *Final Actions* The `final_actions=` parameter lets you define actions that execute after all validation steps have completed. These are useful for providing summaries, sending notifications based on overall validation status, or performing cleanup operations. Final actions should be provided using the [`FinalActions`](`pointblank.FinalActions`) class: ```python def send_report(): summary = pb.get_validation_summary() if summary["status"] == "CRITICAL": send_alert_email( subject=f"CRITICAL validation failures in {summary['tbl_name']}", body=f"{summary['critical_steps']} steps failed with critical severity." ) validation = pb.Validate( data=my_data, final_actions=pb.FinalActions(send_report) ) ``` Final actions have access to validation-wide summary information through the [`get_validation_summary()`](`pointblank.get_validation_summary`) function, which provides a comprehensive overview of the entire validation process. The combination of step actions and final actions provides a flexible system for responding to data quality issues at both the individual step level and the overall validation level. Reporting Languages ------------------- Various pieces of reporting in Pointblank can be localized to a specific language. This is done by setting the `lang=` parameter in `Validate`. Any of the following languages can be used (just provide the language code): - English (`"en"`) - French (`"fr"`) - German (`"de"`) - Italian (`"it"`) - Spanish (`"es"`) - Portuguese (`"pt"`) - Dutch (`"nl"`) - Swedish (`"sv"`) - Danish (`"da"`) - Norwegian Bokmål (`"nb"`) - Icelandic (`"is"`) - Finnish (`"fi"`) - Polish (`"pl"`) - Czech (`"cs"`) - Romanian (`"ro"`) - Greek (`"el"`) - Russian (`"ru"`) - Turkish (`"tr"`) - Arabic (`"ar"`) - Hindi (`"hi"`) - Simplified Chinese (`"zh-Hans"`) - Traditional Chinese (`"zh-Hant"`) - Japanese (`"ja"`) - Korean (`"ko"`) - Vietnamese (`"vi"`) - Indonesian (`"id"`) - Ukrainian (`"uk"`) - Bulgarian (`"bg"`) - Croatian (`"hr"`) - Estonian (`"et"`) - Hungarian (`"hu"`) - Irish (`"ga"`) - Latvian (`"lv"`) - Lithuanian (`"lt"`) - Maltese (`"mt"`) - Slovak (`"sk"`) - Slovenian (`"sl"`) - Hebrew (`"he"`) - Thai (`"th"`) - Persian (`"fa"`) Automatically generated briefs (produced by using `brief=True` or `brief="...{auto}..."`) will be written in the selected language. The language setting will also used when generating the validation report table through [`get_tabular_report()`](`pointblank.Validate.get_tabular_report`) (or printing the `Validate` object in a notebook environment). Examples -------- ### Creating a validation plan and interrogating Let's walk through a data quality analysis of an extremely small table. It's actually called `"small_table"` and it's accessible through the [`load_dataset()`](`pointblank.load_dataset`) function. ```{python} import pointblank as pb # Load the `small_table` dataset small_table = pb.load_dataset(dataset="small_table", tbl_type="polars") # Preview the table pb.preview(small_table) ``` We ought to think about what's tolerable in terms of data quality so let's designate proportional failure thresholds to the 'warning', 'error', and 'critical' states. This can be done by using the [`Thresholds`](`pointblank.Thresholds`) class. ```{python} thresholds = pb.Thresholds(warning=0.10, error=0.25, critical=0.35) ``` Now, we use the `Validate` class and give it the `thresholds` object (which serves as a default for all validation steps but can be overridden). The static thresholds provided in `thresholds=` will make the reporting a bit more useful. We also need to provide a target table and we'll use `small_table` for this. ```{python} validation = ( pb.Validate( data=small_table, tbl_name="small_table", label="`Validate` example.", thresholds=thresholds ) ) ``` Then, as with any `Validate` object, we can add steps to the validation plan by using as many validation methods as we want. To conclude the process (and actually query the data table), we use the [`interrogate()`](`pointblank.Validate.interrogate`) method. ```{python} validation = ( validation .col_vals_gt(columns="d", value=100) .col_vals_le(columns="c", value=5) .col_vals_between(columns="c", left=3, right=10, na_pass=True) .col_vals_regex(columns="b", pattern=r"[0-9]-[a-z]{3}-[0-9]{3}") .col_exists(columns=["date", "date_time"]) .interrogate() ) ``` The `validation` object can be printed as a reporting table. ```{python} validation ``` The report could be further customized by using the [`get_tabular_report()`](`pointblank.Validate.get_tabular_report`) method, which contains options for modifying the display of the table. ### Adding briefs Briefs are short descriptions of the validation steps. While they can be set for each step individually, they can also be set globally. The global setting is done by using the `brief=` argument in `Validate`. The global setting can be as simple as `True` to have automatically-generated briefs for each step. Alternatively, we can use templating elements like `"{step}"` (to insert the step number) or `"{auto}"` (to include an automatically generated brief). Here's an example of a global setting for briefs: ```{python} validation_2 = ( pb.Validate( data=pb.load_dataset(), tbl_name="small_table", label="Validation example with briefs", brief="Step {step}: {auto}", ) .col_vals_gt(columns="d", value=100) .col_vals_between(columns="c", left=3, right=10, na_pass=True) .col_vals_regex( columns="b", pattern=r"[0-9]-[a-z]{3}-[0-9]{3}", brief="Regex check for column {col}" ) .interrogate() ) validation_2 ``` We see the text of the briefs appear in the `STEP` column of the reporting table. Furthermore, the global brief's template (`"Step {step}: {auto}"`) is applied to all steps except for the final step, where the step-level `brief=` argument provided an override. If you should want to cancel the globally-defined brief for one or more validation steps, you can set `brief=False` in those particular steps. ### Post-interrogation methods The `Validate` class has a number of post-interrogation methods that can be used to extract useful information from the validation results. For example, the [`get_data_extracts()`](`pointblank.Validate.get_data_extracts`) method can be used to get the data extracts for each validation step. ```{python} validation_2.get_data_extracts() ``` We can also view step reports for each validation step using the [`get_step_report()`](`pointblank.Validate.get_step_report`) method. This method adapts to the type of validation step and shows the relevant information for a step's validation. ```{python} validation_2.get_step_report(i=2) ``` The `Validate` class also has a method for getting the sundered data, which is the data that passed or failed the validation steps. This can be done using the [`get_sundered_data()`](`pointblank.Validate.get_sundered_data`) method. ```{python} pb.preview(validation_2.get_sundered_data()) ``` The sundered data is a DataFrame that contains the rows that passed or failed the validation. The default behavior is to return the rows that failed the validation, as shown above. ### Working with CSV Files The `Validate` class can directly accept CSV file paths, making it easy to validate data stored in CSV files without manual loading: ```{python} # Get a path to a CSV file from the package data csv_path = pb.get_data_path("global_sales", "csv") validation_3 = ( pb.Validate( data=csv_path, label="CSV validation example" ) .col_exists(["customer_id", "product_id", "revenue"]) .col_vals_not_null(["customer_id", "product_id"]) .col_vals_gt(columns="revenue", value=0) .interrogate() ) validation_3 ``` You can also use a Path object to specify the CSV file. Here's an example of how to do that: ```{python} from pathlib import Path csv_file = Path(pb.get_data_path("game_revenue", "csv")) validation_4 = ( pb.Validate(data=csv_file, label="Game Revenue Validation") .col_exists(["player_id", "session_id", "item_name"]) .col_vals_regex( columns="session_id", pattern=r"[A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12}" ) .col_vals_gt(columns="item_revenue", value=0, na_pass=True) .interrogate() ) validation_4 ``` The CSV loading is automatic, so when a string or Path with a `.csv` extension is provided, Pointblank will automatically load the file using the best available DataFrame library (Polars preferred, Pandas as fallback). The loaded data can then be used with all validation methods just like any other supported table type. ### Working with Parquet Files The `Validate` class can directly accept Parquet files and datasets in various formats. The following examples illustrate how to validate Parquet files: ```{python} # Single Parquet file from package data parquet_path = pb.get_data_path("nycflights", "parquet") validation_5 = ( pb.Validate( data=parquet_path, tbl_name="NYC Flights Data" ) .col_vals_not_null(["carrier", "origin", "dest"]) .col_vals_gt(columns="distance", value=0) .interrogate() ) validation_5 ``` You can also use glob patterns and directories. Here are some examples for how to: 1. load multiple Parquet files 2. load a Parquet-containing directory 3. load a partitioned Parquet dataset ```python # Multiple Parquet files with glob patterns validation_6 = pb.Validate(data="data/sales_*.parquet") # Directory containing Parquet files validation_7 = pb.Validate(data="parquet_data/") # Partitioned Parquet dataset validation_8 = ( pb.Validate(data="sales_data/") # Contains year=2023/quarter=Q1/region=US/sales.parquet .col_exists(["transaction_id", "amount", "year", "quarter", "region"]) .interrogate() ) ``` When you point to a directory that contains a partitioned Parquet dataset (with subdirectories like `year=2023/quarter=Q1/region=US/`), Pointblank will automatically: - discover all Parquet files recursively - extract partition column values from directory paths - add partition columns to the final DataFrame - combine all partitions into a single table for validation Both Polars and Pandas handle partitioned datasets natively, so this works seamlessly with either DataFrame library. The loading preference is Polars first, then Pandas as a fallback. ### Working with Database Connection Strings The `Validate` class supports database connection strings for direct validation of database tables. Connection strings must specify a table using the `::table_name` suffix: ```{python} # Get path to a DuckDB database file from package data duckdb_path = pb.get_data_path("game_revenue", "duckdb") validation_9 = ( pb.Validate( data=f"duckdb:///{duckdb_path}::game_revenue", label="DuckDB Game Revenue Validation" ) .col_exists(["player_id", "session_id", "item_revenue"]) .col_vals_gt(columns="item_revenue", value=0) .interrogate() ) validation_9 ``` For comprehensive documentation on supported connection string formats, error handling, and installation requirements, see the [`connect_to_table()`](`pointblank.connect_to_table`) function. This function handles all the connection logic and provides helpful error messages when table specifications are missing or backend dependencies are not installed. Thresholds(warning: 'int | float | bool | None' = None, error: 'int | float | bool | None' = None, critical: 'int | float | bool | None' = None) -> None Definition of threshold values. Thresholds are used to set limits on the number of failing test units at different levels. The levels are 'warning', 'error', and 'critical'. These levels correspond to different levels of severity when a threshold is reached. The threshold values can be set as absolute counts or as fractions of the total number of test units. When a threshold is reached, an action can be taken (e.g., displaying a message or calling a function) if there is an associated action defined for that level (defined through the [`Actions`](`pointblank.Actions`) class). Parameters ---------- warning The threshold for the 'warning' level. This can be an absolute count or a fraction of the total. Using `True` will set this threshold value to `1`. error The threshold for the 'error' level. This can be an absolute count or a fraction of the total. Using `True` will set this threshold value to `1`. critical The threshold for the 'critical' level. This can be an absolute count or a fraction of the total. Using `True` will set this threshold value to `1`. Returns ------- Thresholds A `Thresholds` object. This can be used when using the [`Validate`](`pointblank.Validate`) class (to set thresholds globally) or when defining validation steps like [`col_vals_gt()`](`pointblank.Validate.col_vals_gt`) (so that threshold values are scoped to individual validation steps, overriding any global thresholds). Examples -------- ```{python} #| echo: false #| output: false import pointblank as pb pb.config(report_incl_footer_timings=False) ``` In a data validation workflow, you can set thresholds for the number of failing test units at different levels. For example, you can set a threshold for the 'warning' level when the number of failing test units exceeds 10% of the total number of test units: ```{python} thresholds_1 = pb.Thresholds(warning=0.1) ``` You can also set thresholds for the 'error' and 'critical' levels: ```{python} thresholds_2 = pb.Thresholds(warning=0.1, error=0.2, critical=0.05) ``` Thresholds can also be set as absolute counts. Here's an example where the 'warning' level is set to `5` failing test units: ```{python} thresholds_3 = pb.Thresholds(warning=5) ``` The `thresholds` object can be used to set global thresholds for all validation steps. Or, you can set thresholds for individual validation steps, which will override the global thresholds. Here's a data validation workflow example where we set global thresholds and then override with different thresholds at the [`col_vals_gt()`](`pointblank.Validate.col_vals_gt`) step: ```{python} validation = ( pb.Validate( data=pb.load_dataset(dataset="small_table"), label="Example Validation", thresholds=pb.Thresholds(warning=0.1, error=0.2, critical=0.3) ) .col_vals_not_null(columns=["c", "d"]) .col_vals_gt(columns="a", value=3, thresholds=pb.Thresholds(warning=5)) .interrogate() ) validation ``` As can be seen, the last step ([`col_vals_gt()`](`pointblank.Validate.col_vals_gt`)) has its own thresholds, which override the global thresholds set at the beginning of the validation workflow (in the [`Validate`](`pointblank.Validate`) class). Actions(warning: 'str | Callable | list[str | Callable] | None' = None, error: 'str | Callable | list[str | Callable] | None' = None, critical: 'str | Callable | list[str | Callable] | None' = None, default: 'str | Callable | list[str | Callable] | None' = None, highest_only: 'bool' = True) -> None Definition of action values. Actions complement threshold values by defining what action should be taken when a threshold level is reached. The action can be a string or a `Callable`. When a string is used, it is interpreted as a message to be displayed. When a `Callable` is used, it will be invoked at interrogation time if the threshold level is met or exceeded. There are three threshold levels: 'warning', 'error', and 'critical'. These levels correspond to different levels of severity when a threshold is reached. Those thresholds can be defined using the [`Thresholds`](`pointblank.Thresholds`) class or various shorthand forms. Actions don't have to be defined for all threshold levels; if an action is not defined for a level in exceedance, no action will be taken. Likewise, there is no negative consequence (other than a no-op) for defining actions for thresholds that don't exist (e.g., setting an action for the 'critical' level when no corresponding 'critical' threshold has been set). Parameters ---------- warning A string, `Callable`, or list of `Callable`/string values for the 'warning' level. Using `None` means no action should be performed at the 'warning' level. error A string, `Callable`, or list of `Callable`/string values for the 'error' level. Using `None` means no action should be performed at the 'error' level. critical A string, `Callable`, or list of `Callable`/string values for the 'critical' level. Using `None` means no action should be performed at the 'critical' level. default A string, `Callable`, or list of `Callable`/string values for all threshold levels. This parameter can be used to set the same action for all threshold levels. If an action is defined for a specific threshold level, it will override the action set for all levels. highest_only A boolean value that, when set to `True` (the default), results in executing only the action for the highest threshold level that is exceeded. Useful when you want to ensure that only the most severe action is taken when multiple threshold levels are exceeded. Returns ------- Actions An `Actions` object. This can be used when using the [`Validate`](`pointblank.Validate`) class (to set actions for meeting different threshold levels globally) or when defining validation steps like [`col_vals_gt()`](`pointblank.Validate.col_vals_gt`) (so that actions are scoped to individual validation steps, overriding any globally set actions). Types of Actions ---------------- Actions can be defined in different ways: 1. **String**: A message to be displayed when the threshold level is met or exceeded. 2. **Callable**: A function that is called when the threshold level is met or exceeded. 3. **List of Strings/Callables**: Multiple messages or functions to be called when the threshold level is met or exceeded. The actions are executed at interrogation time when the threshold level assigned to the action is exceeded by the number or proportion of failing test units. When providing a string, it will simply be printed to the console. A callable will also be executed at the time of interrogation. If providing a list of strings or callables, each item in the list will be executed in order. Such a list can contain a mix of strings and callables. String Templating ----------------- When using a string as an action, you can include placeholders for the following variables: - `{type}`: The validation step type where the action is executed (e.g., 'col_vals_gt', 'col_vals_lt', etc.) - `{level}`: The threshold level where the action is executed ('warning', 'error', or 'critical') - `{step}` or `{i}`: The step number in the validation workflow where the action is executed - `{col}` or `{column}`: The column name where the action is executed - `{val}` or `{value}`: An associated value for the validation method (e.g., the value to compare against in a 'col_vals_gt' validation step) - `{time}`: A datetime value for when the action was executed The first two placeholders can also be used in uppercase (e.g., `{TYPE}` or `{LEVEL}`) and the corresponding values will be displayed in uppercase. The placeholders are replaced with the actual values during interrogation. For example, the string `"{LEVEL}: '{type}' threshold exceeded for column {col}."` will be displayed as `"WARNING: 'col_vals_gt' threshold exceeded for column a."` when the 'warning' threshold is exceeded in a 'col_vals_gt' validation step involving column `a`. Crafting Callables with `get_action_metadata()` ----------------------------------------------- When creating a callable function to be used as an action, you can use the [`get_action_metadata()`](`pointblank.get_action_metadata`) function to retrieve metadata about the step where the action is executed. This metadata contains information about the validation step, including the step type, level, step number, column name, and associated value. You can use this information to craft your action message or to take specific actions based on the metadata provided. Examples -------- ```{python} #| echo: false #| output: false import pointblank as pb pb.config(report_incl_footer_timings=False) ``` Let's define both threshold values and actions for a data validation workflow. We'll set these thresholds and actions globally for all validation steps. In this specific example, the only actions we'll define are for the 'critical' level: ```{python} import pointblank as pb validation = ( pb.Validate( data=pb.load_dataset(dataset="game_revenue", tbl_type="duckdb"), thresholds=pb.Thresholds(warning=0.05, error=0.10, critical=0.15), actions=pb.Actions(critical="Major data quality issue found in step {step}."), ) .col_vals_regex(columns="player_id", pattern=r"[A-Z]{12}[0-9]{3}") .col_vals_gt(columns="item_revenue", value=0.05) .col_vals_gt(columns="session_duration", value=15) .interrogate() ) validation ``` Because we set the 'critical' action to display `"Major data quality issue found."` in the console, this message will be displayed if the number of failing test units exceeds the 'critical' threshold (set to 15% of the total number of test units). In step 3 of the validation workflow, the 'critical' threshold is exceeded, so the message is displayed in the console. Actions can be defined locally for individual validation steps, which will override any global actions set at the beginning of the validation workflow. Here's a variation of the above example where we set global threshold values but assign an action only for an individual validation step: ```{python} def dq_issue(): from datetime import datetime print(f"Data quality issue found ({datetime.now()}).") validation = ( pb.Validate( data=pb.load_dataset(dataset="game_revenue", tbl_type="duckdb"), thresholds=pb.Thresholds(warning=0.05, error=0.10, critical=0.15), ) .col_vals_regex(columns="player_id", pattern=r"[A-Z]{12}[0-9]{3}") .col_vals_gt(columns="item_revenue", value=0.05) .col_vals_gt( columns="session_duration", value=15, actions=pb.Actions(warning=dq_issue), ) .interrogate() ) validation ``` In this case, the 'warning' action is set to call the `dq_issue()` function. This action is only executed when the 'warning' threshold is exceeded in the 'session_duration' column. Because all three thresholds are exceeded in step 3, the 'warning' action of executing the function occurs (resulting in a message being printed to the console). If actions were set for the other two threshold levels, they would also be executed. See Also -------- get_action_metadata : Get metadata about the step where an action is executed. FinalActions : Define actions that run after all validation steps are done. FinalActions(*actions) -> 'None' Define actions to be taken after validation is complete. Final actions are executed after all validation steps have been completed. They provide a mechanism to respond to the overall validation results, such as sending alerts when critical failures are detected or generating summary reports. Parameters ---------- *actions One or more actions to execute after validation. An action can be (1) a callable function that will be executed with no arguments, or (2) a string message that will be printed to the console. Returns ------- FinalActions An `FinalActions` object. This can be used when using the [`Validate`](`pointblank.Validate`) class (to set final actions for the validation workflow). Types of Actions ---------------- Final actions can be defined in two different ways: 1. **String**: A message to be displayed when the validation is complete. 2. **Callable**: A function that is called when the validation is complete. The actions are executed at the end of the validation workflow. When providing a string, it will simply be printed to the console. A callable will also be executed at the time of validation completion. Several strings and callables can be provided to the `FinalActions` class, and they will be executed in the order they are provided. Crafting Callables with `get_validation_summary()` ------------------------------------------------- When creating a callable function to be used as a final action, you can use the [`get_validation_summary()`](`pointblank.get_validation_summary`) function to retrieve the summary of the validation results. This summary contains information about the validation workflow, including the number of test units, the number of failing test units, and the threshold levels that were exceeded. You can use this information to craft your final action message or to take specific actions based on the validation results. Examples -------- Final actions provide a powerful way to respond to the overall results of a validation workflow. They're especially useful for sending notifications, generating reports, or taking corrective actions based on the complete validation outcome. The following example shows how to create a final action that checks for critical failures and sends an alert. The `d` column of the `small_table` dataset has many values that aren't greater than `1000`, so the step reaches the 'critical' threshold: ```{python} import pointblank as pb def send_alert(): summary = pb.get_validation_summary() if summary["highest_severity"] == "critical": print(f"ALERT: Critical validation failures found in {summary['tbl_name']}") validation = ( pb.Validate( data=pb.load_dataset("small_table"), tbl_name="small_table", thresholds=(1, 2, 3), final_actions=pb.FinalActions(send_alert), ) .col_vals_gt(columns="d", value=1000) .interrogate() ) ``` In this example, the `send_alert()` function is defined to check the validation summary for critical failures. Since some were found, the alert message was printed. The function is passed to the `FinalActions` class, which ensures it will be executed after all validation steps are complete. Note that we used the [`get_validation_summary()`](`pointblank.get_validation_summary`) function to retrieve the summary of the validation results to help craft the alert message. Multiple final actions can be provided in a sequence. They will be executed in the order they are specified after all validation steps have completed. Here, a string message is followed by the `send_alert()` function and a second function that prints a count of the failing steps: ```{python} def log_counts(): summary = pb.get_validation_summary() print(f"{summary['n_failing_steps']} of {summary['n_steps']} steps had failures") validation = ( pb.Validate( data=pb.load_dataset("small_table"), tbl_name="small_table", thresholds=(1, 2, 3), final_actions=pb.FinalActions( "Validation complete.", # a string message send_alert, # a callable function log_counts, # another callable function ), ) .col_vals_gt(columns="d", value=1000) .col_vals_not_null(columns="c") .col_exists(columns="a") .interrogate() ) ``` The three outputs appear in the order the actions were given. A string action is printed as is, while the functions can use the summary to decide what to do. Two of the three steps have failures (`d` has values of `1000` or less and `c` has missing values), and only the `col_exists()` step passes. See Also -------- get_validation_summary : Get a summary of the validation results inside a final action. Actions : Define actions that run when a step exceeds a threshold. Schema(columns: 'str | list[str] | list[tuple[str, str]] | list[tuple[str]] | dict[str, str] | None' = None, tbl: 'Any | None' = None, **kwargs) -> 'None' Definition of a schema object. The schema object defines the structure of a table. Once it is defined, the object can be used in a validation workflow, using `Validate` and its methods, to ensure that the structure of a table matches the expected schema. The validation method that works with the schema object is called [`col_schema_match()`](`pointblank.Validate.col_schema_match`). A schema for a table can be constructed with the `Schema` class in a number of ways: 1. providing a list of column names to `columns=` (to check only the column names) 2. using a list of one- or two-element tuples in `columns=` (to check both column names and optionally dtypes, should be in the form of `[(column_name, dtype), ...]`) 3. providing a dictionary to `columns=`, where the keys are column names and the values are dtypes 4. providing individual column arguments in the form of keyword arguments (constructed as `column_name=dtype`) The schema object can also be constructed by providing a DataFrame or Ibis table object (using the `tbl=` parameter) and the schema will be collected from either type of object. The schema object can be printed to display the column names and dtypes. Note that if `tbl=` is provided then there shouldn't be any other inputs provided through either `columns=` or `**kwargs`. Parameters ---------- columns A list of strings (representing column names), a list of tuples (for column names and column dtypes), or a dictionary containing column and dtype information. If any of these inputs are provided here, it will take precedence over any column arguments provided via `**kwargs`. tbl A DataFrame (Polars or Pandas) or an Ibis table object from which the schema will be collected. Read the *Supported Input Table Types* section for details on the supported table types. **kwargs Individual column arguments that are in the form of `column=dtype` or `column=[dtype1, dtype2, ...]`. These will be ignored if the `columns=` parameter is not `None`. Returns ------- Schema A schema object. Supported Input Table Types --------------------------- The `tbl=` parameter, if used, can be given any of the following table types: - Polars DataFrame (`"polars"`) - Pandas DataFrame (`"pandas"`) - PySpark table (`"pyspark"`) - DuckDB table (`"duckdb"`)* - MySQL table (`"mysql"`)* - PostgreSQL table (`"postgresql"`)* - SQLite table (`"sqlite"`)* - Microsoft SQL Server table (`"mssql"`)* - Snowflake table (`"snowflake"`)* - Databricks table (`"databricks"`)* - BigQuery table (`"bigquery"`)* - Parquet table (`"parquet"`)* The table types marked with an asterisk need to be prepared as Ibis tables (with type of `ibis.expr.types.relations.Table`). Furthermore, using `Schema(tbl=)` with these types of tables requires the Ibis library (`v9.5.0` or above) to be installed. If the input table is a Polars or Pandas DataFrame, the availability of Ibis is not needed. Additional Notes on Schema Construction --------------------------------------- While there is flexibility in how a schema can be constructed, there is the potential for some confusion. So let's go through each of the methods of constructing a schema in more detail and single out some important points. When providing a list of column names to `columns=`, a [`col_schema_match()`](`pointblank.Validate.col_schema_match`) validation step will only check the column names. Any arguments pertaining to dtypes will be ignored. When using a list of tuples in `columns=`, the tuples could contain the column name and dtype or just the column name. This construction allows for more flexibility in constructing the schema as some columns will be checked for dtypes and others will not. This method is the only way to have mixed checks of column names and dtypes in [`col_schema_match()`](`pointblank.Validate.col_schema_match`). When providing a dictionary to `columns=`, the keys are the column names and the values are the dtypes. This method of input is useful in those cases where you might already have a dictionary of column names and dtypes that you want to use as the schema. If using individual column arguments in the form of keyword arguments, the column names are the keyword arguments and the dtypes are the values. This method emphasizes readability and is perhaps more convenient when manually constructing a schema with a small number of columns. Finally, multiple dtypes can be provided for a single column by providing a list or tuple of dtypes in place of a scalar string value. Having multiple dtypes for a column allows for the dtype check via [`col_schema_match()`](`pointblank.Validate.col_schema_match`) to make multiple attempts at matching the column dtype. Should any of the dtypes match the column dtype, that part of the schema check will pass. Here are some examples of how you could provide single and multiple dtypes for a column: ```python # list of tuples schema_1 = pb.Schema(columns=[("name", "String"), ("age", ["Float64", "Int64"])]) # dictionary schema_2 = pb.Schema(columns={"name": "String", "age": ["Float64", "Int64"]}) # keyword arguments schema_3 = pb.Schema(name="String", age=["Float64", "Int64"]) ``` All of the above examples will construct the same schema object. Examples -------- ```{python} #| echo: false #| output: false import pointblank as pb pb.config(report_incl_header=False, report_incl_footer_timings=False, preview_incl_header=False) ``` A schema can be constructed via the `Schema` class in multiple ways. Let's use the following Polars DataFrame as a basis for constructing a schema: ```{python} import pointblank as pb import polars as pl df = pl.DataFrame({ "name": ["Alice", "Bob", "Charlie"], "age": [25, 30, 35], "height": [5.6, 6.0, 5.8] }) ``` You could provide `Schema(columns=)` a list of tuples containing column names and data types: ```{python} schema = pb.Schema(columns=[("name", "String"), ("age", "Int64"), ("height", "Float64")]) ``` Alternatively, a dictionary containing column names and dtypes also works: ```{python} schema = pb.Schema(columns={"name": "String", "age": "Int64", "height": "Float64"}) ``` Another input method involves using individual column arguments in the form of keyword arguments: ```{python} schema = pb.Schema(name="String", age="Int64", height="Float64") ``` Finally, could also provide a DataFrame (Polars and Pandas) or an Ibis table object to `tbl=` and the schema will be collected: ```python schema = pb.Schema(tbl=df) ``` Whichever method you choose, you can verify the schema inputs by printing the `schema` object: ```{python} print(schema) ``` The `Schema` object can be used to validate the structure of a table against the schema. The relevant `Validate` method for this is [`col_schema_match()`](`pointblank.Validate.col_schema_match`). In a validation workflow, you'll have a target table (defined at the beginning of the workflow) and you might want to ensure that your expectations of the table structure are met. The [`col_schema_match()`](`pointblank.Validate.col_schema_match`) method works with a `Schema` object to validate the structure of the table. Here's an example of how you could use [`col_schema_match()`](`pointblank.Validate.col_schema_match`) in a validation workflow: ```{python} # Define the schema schema = pb.Schema(name="String", age="Int64", height="Float64") # Define a validation that checks the schema against the table (`df`) validation = ( pb.Validate(data=df) .col_schema_match(schema=schema) .interrogate() ) # Display the validation results validation ``` The [`col_schema_match()`](`pointblank.Validate.col_schema_match`) validation method will validate the structure of the table against the schema during interrogation. If the structure of the table does not match the schema, the single test unit will fail. In this case, the defined schema matched the structure of the table, so the validation passed. We can also choose to check only the column names of the target table. This can be done by providing a simplified `Schema` object, which is given a list of column names: ```{python} schema = pb.Schema(columns=["name", "age", "height"]) validation = ( pb.Validate(data=df) .col_schema_match(schema=schema) .interrogate() ) validation ``` In this case, the schema only checks the column names of the table against the schema during interrogation. If the column names of the table do not match the schema, the single test unit will fail. In this case, the defined schema matched the column names of the table, so the validation passed. If you wanted to check column names and dtypes only for a subset of columns (and just the column names for the rest), you could use a list of mixed one- or two-item tuples in `columns=`: ```{python} schema = pb.Schema(columns=[("name", "String"), ("age", ), ("height", )]) validation = ( pb.Validate(data=df) .col_schema_match(schema=schema) .interrogate() ) validation ``` Not specifying a dtype for a column (as is the case for the `age` and `height` columns in the above example) will only check the column name. There may also be the case where you want to check the column names and specify multiple dtypes for a column to have several attempts at matching the dtype. This can be done by providing a list of dtypes where there would normally be a single dtype: ```{python} schema = pb.Schema( columns=[("name", "String"), ("age", ["Float64", "Int64"]), ("height", "Float64")] ) validation = ( pb.Validate(data=df) .col_schema_match(schema=schema) .interrogate() ) validation ``` For the `age` column, the schema will check for both `Float64` and `Int64` dtypes. If either of these dtypes is found in the column, the portion of the schema check will succeed. See Also -------- Validate.col_schema_match : The validation step that checks a table against a `Schema`. DraftValidation(data: 'Any', model: 'str', api_key: 'str | None' = None, verify_ssl: 'bool' = True, max_reprompts: 'int' = 1) -> None Draft a validation plan for a given table using an LLM. By using a large language model (LLM) to draft a validation plan, you can quickly generate a starting point for validating a table. This can be useful when you have a new table and you want to get a sense of how to validate it (and adjustments could always be made later). The `DraftValidation` class uses the `chatlas` package to draft a validation plan for a given table using an LLM from the `"anthropic"`, `"openai"`, `"ollama"`, `"bedrock"`, or `"azure-openai"` provider. You can install all requirements for the class through an optional 'generate' install of Pointblank via `pip install pointblank[generate]`. :::{.callout-warning} The `DraftValidation` class is still experimental. Please report any issues you encounter in the [Pointblank issue tracker](https://github.com/posit-dev/pointblank/issues). ::: Parameters ---------- data The data to be used for drafting a validation plan. model The model to be used. This should be in the form of `provider:model` (e.g., `"anthropic:claude-opus-4-6"`). Supported providers are `"anthropic"`, `"openai"`, `"ollama"`, `"bedrock"`, and `"azure-openai"`. For `"azure-openai"`, the value after the colon is the Azure *deployment id*, not an OpenAI model id. api_key The API key to be used for the model. verify_ssl Whether to verify SSL certificates when making requests to the LLM provider. Set to `False` to disable SSL verification (e.g., when behind a corporate firewall with self-signed certificates). Defaults to `True`. Use with caution as disabling SSL verification can pose security risks. max_reprompts The maximum number of times to ask the model for a corrected plan when the one it returned doesn't pass a basic check: the code must parse as Python, and every method that looks like a validation method (a name starting with `col_`, `row_`, or `rows_`) must exist, since models sometimes invent method names. The retry tells the model what the problem was. Each retry is another request to the provider, so a higher value can cost more. Set to `0` to accept the first reply as is, and check it yourself with the object's `validate_syntax()` method. Defaults to `1`. Returns ------- str The drafted validation plan. Constructing the `model` Argument --------------------------------- The `model=` argument should be constructed using the provider and model name separated by a colon (`provider:model`). The provider text can any of: - `"anthropic"` (Anthropic) - `"openai"` (OpenAI) - `"ollama"` (Ollama) - `"bedrock"` (Amazon Bedrock) - `"azure-openai"` (Azure OpenAI) The model name should be the specific model to be used from the provider. Model names are subject to change so consult the provider's documentation for the most up-to-date model names. For `"azure-openai"`, the value after the colon is the Azure *deployment id* (the name you assigned when deploying the model in your Azure OpenAI resource). It also requires the environment variables `AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_ENDPOINT` (e.g., `https://.openai.azure.com`), and `OPENAI_API_VERSION` (e.g., `"2024-06-01"`) to be set. Notes on Authentication ----------------------- Providing a valid API key as a string in the `api_key` argument is adequate for getting started but you should consider using a more secure method for handling API keys. One way to do this is to load the API key from an environent variable and retrieve it using the `os` module (specifically the `os.getenv()` function). Places to store the API key might include `.bashrc`, `.bash_profile`, `.zshrc`, or `.zsh_profile`. Another solution is to store one or more model provider API keys in an `.env` file (in the root of your project). If the API keys have correct names (e.g., `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`) then DraftValidation will automatically load the API key from the `.env` file and there's no need to provide the `api_key` argument. An `.env` file might look like this: ```plaintext ANTHROPIC_API_KEY="your_anthropic_api_key_here" OPENAI_API_KEY="your_openai_api_key_here" ``` There's no need to have the `python-dotenv` package installed when using `.env` files in this way. Notes on SSL Certificate Verification -------------------------------------- By default, SSL certificate verification is enabled for all requests to LLM providers. However, in certain network environments (such as corporate networks with self-signed certificates or firewall proxies), you may encounter SSL certificate verification errors. To disable SSL verification, set the `verify_ssl` parameter to `False`: ```python import pointblank as pb data = pb.load_dataset(dataset="nycflights", tbl_type="duckdb") # Disable SSL verification for networks with self-signed certificates pb.DraftValidation( data=data, model="anthropic:claude-opus-4-6", verify_ssl=False ) ``` :::{.callout-warning} Disabling SSL verification (through `verify_ssl=False`) can expose your API keys and data to man-in-the-middle attacks. Only use this option in trusted network environments and when absolutely necessary. ::: Notes on Data Sent to the Model Provider ---------------------------------------- The data sent to the model provider is a JSON summary of the table. This data summary is generated internally by `DraftValidation` using the `DataScan` class. The summary includes the following information: - the number of rows and columns in the table - the type of dataset (e.g., Polars, DuckDB, Pandas, etc.) - the column names and their types - column level statistics such as the number of missing values, min, max, mean, and median, etc. - a short list of data values in each column The JSON summary is used to provide the model with the necessary information to draft a validation plan. As such, even very large tables can be used with the `DraftValidation` class since the contents of the table are not sent to the model provider. The Amazon Bedrock is a special case since it is a self-hosted model and security controls are in place to ensure that data is kept within the user's AWS environment. If using an Ollama model all data is handled locally, though only a few models are capable enough to perform the task of drafting a validation plan. Examples -------- Let's look at how the `DraftValidation` class can be used to draft a validation plan for a table. The table to be used is `"nycflights"`, which is available here via the [`load_dataset()`](`pointblank.load_dataset`) function. The model to be used is `"anthropic:claude-opus-4-6"` (which performs very well compared to other LLMs). The example assumes that the API key is stored in an `.env` file as `ANTHROPIC_API_KEY`. ```python import pointblank as pb # Load the "nycflights" dataset as a DuckDB table data = pb.load_dataset(dataset="nycflights", tbl_type="duckdb") # Draft a validation plan for the "nycflights" table pb.DraftValidation(data=data, model="anthropic:claude-opus-4-6") ``` The output will be a drafted validation plan for the `"nycflights"` table and this will appear in the console. ````plaintext ```python import pointblank as pb # Define schema based on column names and dtypes schema = pb.Schema(columns=[ ("year", "int64"), ("month", "int64"), ("day", "int64"), ("dep_time", "int64"), ("sched_dep_time", "int64"), ("dep_delay", "int64"), ("arr_time", "int64"), ("sched_arr_time", "int64"), ("arr_delay", "int64"), ("carrier", "string"), ("flight", "int64"), ("tailnum", "string"), ("origin", "string"), ("dest", "string"), ("air_time", "int64"), ("distance", "int64"), ("hour", "int64"), ("minute", "int64") ]) # The validation plan validation = ( pb.Validate( data=your_data, # Replace your_data with the actual data variable label="Draft Validation", thresholds=pb.Thresholds(warning=0.10, error=0.25, critical=0.35) ) .col_schema_match(schema=schema) .col_vals_not_null(columns=[ "year", "month", "day", "sched_dep_time", "carrier", "flight", "origin", "dest", "distance", "hour", "minute" ]) .col_vals_between(columns="month", left=1, right=12) .col_vals_between(columns="day", left=1, right=31) .col_vals_between(columns="sched_dep_time", left=106, right=2359) .col_vals_between(columns="dep_delay", left=-43, right=1301, na_pass=True) .col_vals_between(columns="air_time", left=20, right=695, na_pass=True) .col_vals_between(columns="distance", left=17, right=4983) .col_vals_between(columns="hour", left=1, right=23) .col_vals_between(columns="minute", left=0, right=59) .col_vals_in_set(columns="origin", set=["EWR", "LGA", "JFK"]) .col_count_match(count=18) .row_count_match(count=336776) .rows_distinct() .interrogate() ) validation ``` ```` The drafted validation plan can be copied and pasted into a Python script or notebook for further use. In other words, the generated plan can be adjusted as needed to suit the specific requirements of the table being validated. Note that the output does not know how the data was obtained, so it uses the placeholder `your_data` in the `data=` argument of the `Validate` class. When adapted for use, this should be replaced with the actual data variable. EditValidation(validation: 'Any', instruction: 'str', model: 'str', data: 'Any' = None, api_key: 'str | None' = None, verify_ssl: 'bool' = True, max_reprompts: 'int' = 1) -> None Edit an existing validation plan with a plain-English instruction using an LLM. While [`DraftValidation`](`pointblank.DraftValidation`) generates a validation plan from scratch by profiling a table, `EditValidation` takes an *existing* plan plus a natural-language instruction (e.g., "add a freshness check on `updated_at`, tighten the email regex, and drop the `price > 0` check") and produces a revised plan. You review the change as a diff and explicitly [`accept()`](`pointblank.EditValidation.accept`) it before anything runs, keeping a human in control. `EditValidation` reuses the same provider abstraction as `DraftValidation` (via the `chatlas` package), supporting the `"anthropic"`, `"openai"`, `"ollama"`, `"bedrock"`, and `"azure-openai"` providers. Install the optional requirements with `pip install pointblank[generate]`. :::{.callout-warning} The `EditValidation` class is experimental. Please report any issues you encounter in the [Pointblank issue tracker](https://github.com/posit-dev/pointblank/issues). ::: Parameters ---------- validation The existing validation plan to edit. This can be a [`Validate`](`pointblank.Validate`) object, a Python code string (such as the output of [`Validate.to_code()`](`pointblank.Validate.to_code`)), a YAML configuration string, or a path to a `.py` or `.yaml`/`.yml` file. All inputs are normalized to Pointblank code before being sent to the model. instruction A plain-English description of the changes to make to the plan. model The model to be used. This should be in the form of `provider:model` (e.g., `"anthropic:claude-opus-4-8"`). Supported providers are `"anthropic"`, `"openai"`, `"ollama"`, `"bedrock"`, and `"azure-openai"`. data Optional data table. When provided, a [`DataScan`](`pointblank.DataScan`) summary of the table is included in the prompt so the model can make *informed* edits (e.g., tightening a range using observed min/max values). It is also used as the default data source when building a [`Validate`](`pointblank.Validate`) via [`accept()`](`pointblank.EditValidation.accept`). api_key The API key to be used for the model. verify_ssl Whether to verify SSL certificates when making requests to the LLM provider. Defaults to `True`. Use with caution as disabling SSL verification can pose security risks. max_reprompts The maximum number of times to automatically re-prompt the model when the returned code fails the syntax/lint check, before surfacing the result to the user. Defaults to `1`. Returns ------- EditValidation An `EditValidation` object exposing the revised plan through [`to_code()`](`pointblank.EditValidation.to_code`), [`diff()`](`pointblank.EditValidation.diff`), [`changed_steps()`](`pointblank.EditValidation.changed_steps`), and [`accept()`](`pointblank.EditValidation.accept`). Notes on Data Sent to the Model Provider ---------------------------------------- As with `DraftValidation`, only a JSON summary of the table (generated by `DataScan`) is ever sent to the model, never the underlying rows. If `data=` is omitted, no table information is sent at all; the model edits the plan using only the plan code and your instruction. Examples -------- ```{python} #| echo: false #| output: false import pointblank as pb ``` Editing a plan with a model needs the `chatlas` package and an API key, so this first example isn't run here. We start with a plan for the `small_table` dataset and ask for two changes: ```python import pointblank as pb # An existing plan validation = ( pb.Validate(data=pb.load_dataset("small_table")) .col_vals_gt(columns="d", value=100) .col_vals_regex(columns="b", pattern="[0-9]-[a-z]{3}-[0-9]{3}") ) # Edit it with a plain-English instruction edited = pb.EditValidation( validation=validation, instruction="Loosen the `d` check to value=50 and add a not-null check on column `a`.", model="anthropic:claude-opus-4-8", data=pb.load_dataset("small_table"), ) print(edited.diff()) # review the change plan = edited.accept() # a Validate you can .interrogate() ``` Nothing runs when the `EditValidation` object is created, apart from the call to the model. The diff shows the revised plan line by line against the original, and only `accept()` turns the revised code into a `Validate` object, using the `data=` given here because none is passed to `accept()`. Because `data=` was given, the model also saw a summary of the table, which helps with edits like choosing a new bound. The review methods don't depend on the model, and [`from_plans()`](`pointblank.EditValidation.from_plans`) builds an `EditValidation` from two plans you already have. Here the revised plan makes the same changes that the instruction above asked for: ```{python} small_table = pb.load_dataset("small_table") original = ( pb.Validate(data=small_table) .col_vals_gt(columns="d", value=100) .col_vals_regex(columns="b", pattern="[0-9]-[a-z]{3}-[0-9]{3}") ) revised = ( pb.Validate(data=small_table) .col_vals_gt(columns="d", value=50) .col_vals_regex(columns="b", pattern="[0-9]-[a-z]{3}-[0-9]{3}") .col_vals_not_null(columns="a") ) edit = pb.EditValidation.from_plans(original, revised, instruction="Loosen d; require a") edit.review() ``` The review table lists one row per changed step: the `col_vals_gt()` step is modified (its `value=` went from `100` to `50`), the `col_vals_not_null()` step is added, and the unchanged `col_vals_regex()` step isn't shown. Once the changes look right, [`accept()`](`pointblank.EditValidation.accept`) gives the revised plan as a `Validate` object that's ready to interrogate. See Also -------- DraftValidation : Draft a new validation plan for a table with an LLM. Validate.to_code : Get a validation plan as Python code. MissingSpec(reasons: 'dict[Any, str]', categories: 'dict[str, list[str]] | None' = None, null_is_missing: 'bool' = True, null_reason: 'str' = 'unknown', description: 'str | None' = None) -> None Specification for structured missing values in a column. Real-world data rarely encodes missingness as a single `null` value. Survey data distinguishes *refused* from *don't know* from *not applicable*; clinical data uses codes like `"NOT DONE"`; statistical packages use sentinel values such as `-99`, `".A"`, or `""`. A `MissingSpec` captures these sentinel values, the *reason* each one represents, and how they should be handled during validation and analysis. This brings the idea of *structured missingness* (a missing value carries a reason for its absence) into Pointblank's runtime validation layer. Without a spec, a sentinel like `-99` is just a number: a range check on an `age` column fails it, and a null check passes it, so both checks give the wrong answer. Declaring the sentinels once and passing the spec to each check keeps the codes out of the value checks, while still letting you check the codes themselves. A spec can be used in these places: - the `missing=` parameter of the `col_vals_*()` methods (such as [`Validate.col_vals_between()`](`pointblank.Validate.col_vals_between`)), where values that the spec treats as missing pass the check, so only the real values are tested - [`Validate.col_pct_missing()`](`pointblank.Validate.col_pct_missing`), to limit how much of a column is missing, overall or for one reason or category - [`Validate.col_missing_coded()`](`pointblank.Validate.col_missing_coded`), [`Validate.col_missing_only_coded()`](`pointblank.Validate.col_missing_only_coded`), and [`Validate.col_missing_consistent()`](`pointblank.Validate.col_missing_consistent`), to check how the missing values are coded - [`missing_vals_tbl()`](`pointblank.missing_vals_tbl`), whose `missing=` mapping of column names to specs gives a breakdown of each column's missing values by reason - YAML configurations, through a top-level `missing_specs:` block or an inline `missing:` mapping on a step The factory methods build common specs: [`from_sas()`](`pointblank.MissingSpec.from_sas`) and [`from_spss()`](`pointblank.MissingSpec.from_spss`) for statistical packages, [`from_cdisc_null_flavors()`](`pointblank.MissingSpec.from_cdisc_null_flavors`) for clinical data, and [`from_variable_metadata()`](`pointblank.MissingSpec.from_variable_metadata`) for variables read with [`import_metadata()`](`pointblank.import_metadata`). Parameters ---------- reasons A dictionary mapping sentinel values to reason labels. Keys are the actual values present in the data (e.g., `-99`, `"NA"`, `".A"`), and they must have the same type as the column: use `"-99"` rather than `-99` for a string column. Values are reason identifiers (e.g., `"refused"`, `"not_asked"`). Several sentinel values can share a reason. The dictionary can be empty if `null_is_missing=True`. categories Optional grouping of reasons into categories (e.g., an `"item_nonresponse"` category that groups `"refused"` and `"dont_know"`). Useful for aggregate reporting and for checking missingness rates by category. Each value is a list of reason labels that appear in `reasons` (or `null_reason`, when `null_is_missing=True`); any other label raises a `ValueError`. Default is `None`. null_is_missing Whether actual null values (`None`, and `NaN` in floating-point columns) should also be treated as missing, with the reason given by `null_reason`. Default is `True`. With `False`, nulls are left to the method's usual null handling (such as `na_pass=` in the `col_vals_*()` methods). null_reason The reason label assigned to actual null values when `null_is_missing=True`. Default is `"unknown"`. description Optional human-readable description of the overall missingness pattern. Default is `None`. Returns ------- MissingSpec A missing-value specification that can be passed to validation methods (via `missing=`) and to [`missing_vals_tbl()`](`pointblank.missing_vals_tbl`). Examples -------- ```{python} #| echo: false #| output: false import pointblank as pb pb.config(report_incl_header=False, report_incl_footer_timings=False, preview_incl_header=False) ``` Suppose a survey records the respondent's age, with three codes for an age that wasn't given: `-99` when the question wasn't asked, `-98` when the respondent refused, and `-97` when they didn't know. The last two are both *item nonresponse* (the question was asked but not answered), so we'll group them in a category: ```{python} import pointblank as pb import polars as pl age_missing = pb.MissingSpec( reasons={-99: "not_asked", -98: "refused", -97: "dont_know"}, categories={"item_nonresponse": ["refused", "dont_know"]}, ) print(age_missing.sentinel_values()) print(age_missing.reason_for(-98)) print(age_missing.values_for_category("item_nonresponse")) ``` The spec lists its sentinel values, gives the reason for any of them, and finds the values in a category. Here's some survey data that uses these codes, along with a null and an age of `150` that's a genuine error: ```{python} survey = pl.DataFrame({"age": [34, -98, 51, -99, None, 27, 150, -97, 45, 62]}) pb.preview(survey) ``` We'll check the ages with and without the spec, and then limit the item nonresponse to 10% of the rows: ```{python} validation = ( pb.Validate(data=survey) .col_vals_between(columns="age", left=0, right=120) .col_vals_between(columns="age", left=0, right=120, missing=age_missing) .col_pct_missing( columns="age", missing=age_missing, category="item_nonresponse", max_pct=0.1 ) .interrogate() ) validation ``` Step 1 has 5 failing rows: the three codes and the null fail the range check along with the `150`, so the real error is lost among the codes. With `missing=age_missing`, step 2 lets the codes and the null pass and only the `150` fails. Step 3 looks at the codes instead of the ages: 2 of the 10 rows (`-98` and `-97`) are item nonresponse, which is 20% and over the 10% limit, so it fails. Together the steps separate bad values from values that are missing for a known reason, and check each on its own terms. See Also -------- Validate.col_pct_missing : Limit the share of a column that is missing. Validate.col_missing_coded : Check that every missing value has a declared code. missing_vals_tbl : Show each column's missing values by reason. import_metadata : Read variable metadata, including missing-value codes, from a file. ## Contracts and Pipelines Use `Contract` and `Step` to define declarative data quality contracts that specify what valid data looks like. Use `Steps` to build reusable step libraries with a fluent API and compose them into `Validate` plans via `add_steps()`. Use `Pipeline` to enforce contracts at both boundaries of a data transformation (source and target), producing a `PipelineResult` with full introspection into what passed and what failed. Contract(name: 'str', direction: "Literal['source', 'target']" = 'source', schema: 'Schema | None' = None, steps: 'list[Step]' = , version: 'str | None' = None, owner: 'str | None' = None, consumers: 'str | list[str] | None' = None, description: 'str | None' = None, thresholds: 'Thresholds | None' = None, on_violation: "Literal['warn', 'raise', 'log']" = 'warn') -> None A declarative boundary contract for pipeline data. A `Contract` is a written-down agreement about what a table must look like at a specific point in a data pipeline. It bundles three things into one object: a [`Schema`](`pointblank.Schema`) with the expected column names and types (the structural expectations), a list of [`Step`](`pointblank.Step`) objects with the rules the values must follow (the semantic expectations), and metadata saying who owns the data, who depends on it, and which version of the agreement is in force. With a plain [`Validate`](`pointblank.Validate`) chain, the rules exist only as code that runs against one table. A contract holds the same rules as data. It isn't tied to any table, so you can apply it to each day's delivery of a feed, check it into version control, review changes to it like any other change, and share it as a YAML file. Teams on both sides of a handoff can then refer to the same definition of "valid". The producer of a table can check its output against the contract before publishing, and the consumer can check its input against the same contract on arrival. Use a contract when the expectations matter more than any one validation run: data crossing a team or system boundary, inputs and outputs of a scheduled job, or a dataset with named owners and downstream consumers. For a one-off look at a table, a `Validate` chain is simpler. Validating data against a contract gives you an ordinary interrogated `Validate` object. That means everything you already use with validations works here: the tabular report, step reports, [`all_passed()`](`pointblank.Validate.all_passed`), [`assert_passing()`](`pointblank.Validate.assert_passing`), data extracts, and so on. Parameters ---------- name A short, unique name for the contract (e.g., `"raw_clickstream_feed"`). It becomes the table name in the validation report and is part of the report label. It must be a non-empty string. direction Where the contract sits relative to a transformation: `"source"` (the default) for data flowing in, or `"target"` for the data product flowing out. The direction is descriptive metadata that appears in violation messages from [`Pipeline`](`pointblank.Pipeline`). It doesn't change what gets checked. schema An optional [`Schema`](`pointblank.Schema`) giving the expected column names and data types. When a schema is given, it's checked first, with [`col_schema_match()`](`pointblank.Validate.col_schema_match`) and its default settings. These defaults are strict: the table must have exactly the schema's columns, in the same order, with matching types. steps A list of [`Step`](`pointblank.Step`) objects, each naming a validation method and its arguments (e.g., `pb.Step("col_vals_gt", columns="amount", value=0)`). They run in list order after the schema check. Only a subset of validation methods can be used. See the *Supported Step Methods* section below. version A version string for the contract, such as `"1.2.0"`. Semantic versioning is a good convention, but any string is accepted. The version is added to the report label (e.g., `"Contract: orders v1.2.0"`) and recorded in the report's governance metadata. owner The person or team responsible for the contract and the data it describes. It's recorded in the report's governance metadata. consumers Who depends on data meeting this contract, as a string or a list of strings. It's recorded in the report's governance metadata. description An optional longer description of the contract's purpose. It's kept with the contract and serialized with it, but doesn't appear in validation reports. thresholds Default [`Thresholds`](`pointblank.Thresholds`) for the validation built from this contract. These set when a failing step is considered a 'warning', 'error', or 'critical' issue. If the contract is used in a `Pipeline` that has its own thresholds, the pipeline's thresholds take precedence. on_violation What a [`Pipeline`](`pointblank.Pipeline`) should do when data fails this contract: `"warn"` (the default) issues a `UserWarning`, `"raise"` raises a [`ContractViolationError`](`pointblank.ContractViolationError`) (a `RuntimeError`), and `"log"` writes a warning to the `"pointblank.contract"` logger. This setting applies only when the contract is enforced through a `Pipeline`. Calling [`validate()`](`pointblank.Contract.validate`) directly always returns the validation without warning or raising, so check the result yourself (e.g., with [`assert_passing()`](`pointblank.Validate.assert_passing`)). Returns ------- Contract A `Contract` object. Apply it to data with [`validate()`](`pointblank.Contract.validate`), or with [`to_validate()`](`pointblank.Contract.to_validate`) when you want to add more steps before interrogating. How a Contract Becomes a Validation ----------------------------------- A contract holds no data and runs nothing until it's applied to a table. Calling [`to_validate()`](`pointblank.Contract.to_validate`) (or [`validate()`](`pointblank.Contract.validate`), which also interrogates) compiles the contract into a `Validate` object: 1. a `Validate` is created for the table, with `tbl_name=` set to the contract name, a label of the form `"Contract: v"`, and the contract's `thresholds=`, `owner=`, `consumers=`, and `version=` 2. if the contract has a schema, a `col_schema_match()` step is added first 3. each [`Step`](`pointblank.Step`) is added in order, by calling the validation method it names with its stored arguments A step that lists several columns expands into one validation step per column, just as it would in a `Validate` chain. Mistakes in a step's arguments (a misspelled parameter name, for example) surface when the contract is compiled, not when it's created. Supported Step Methods ---------------------- These validation methods can be used in a contract's steps: - column value checks: `col_vals_gt`, `col_vals_lt`, `col_vals_ge`, `col_vals_le`, `col_vals_eq`, `col_vals_ne`, `col_vals_between`, `col_vals_outside`, `col_vals_in_set`, `col_vals_not_in_set`, `col_vals_increasing`, `col_vals_decreasing`, `col_vals_null`, `col_vals_not_null`, `col_vals_regex`, `col_vals_within_spec`, and `col_vals_expr` - column-level checks: `col_exists` and `col_pct_null` - table-level checks: `col_schema_match`, `col_count_match`, `row_count_match`, `rows_distinct`, and `rows_complete` - multi-condition checks: `conjointly`, with its expressions given as a list in `exprs=` Each takes the same arguments as the [`Validate`](`pointblank.Validate`) method of the same name. A step that names any other method is rejected with a `ValueError` when the contract is created. For steps that a contract can't express, compile the contract with [`to_validate()`](`pointblank.Contract.to_validate`) and chain the extra methods onto the result. Serialization ------------- Contracts are meant to live outside your code, so they round-trip through YAML and plain dictionaries. [`to_yaml()`](`pointblank.Contract.to_yaml`) and [`from_yaml()`](`pointblank.Contract.from_yaml`) work with YAML files, and [`to_dict()`](`pointblank.Contract.to_dict`) and [`from_dict()`](`pointblank.Contract.from_dict`) work with dictionaries (useful for JSON or a configuration system). The schema is stored as a mapping of column names to type strings, the thresholds as a mapping of levels to values, and each step as a single-key mapping from method name to arguments. Step arguments must be plain data (strings, numbers, booleans, dates, lists, and dictionaries) for a contract to be written to YAML, although step-level `Thresholds` objects and tuples are converted to dictionaries automatically. A contract with other objects in its steps, such as a column selector (e.g., `pb.starts_with("amt_")`), a Polars expression, or a function, can't be written: `to_yaml()` raises a `ValueError` that says where the object is. Keep contracts like that in Python. Working With Other Pointblank Tools ----------------------------------- A contract is the building block for several other parts of the API: - [`Pipeline`](`pointblank.Pipeline`) pairs a source contract and a target contract around a transformation function. It validates the input, runs the transformation, validates the output, and applies each contract's `on_violation=` setting. The results come back in a [`PipelineResult`](`pointblank.PipelineResult`). - [`export_contract()`](`pointblank.export_contract`) converts a contract into an external format (JSON Schema, Frictionless Table Schema, dbt, or the Open Data Contract Standard), and [`import_contract()`](`pointblank.import_contract`) reads one of those formats. Call [`to_contract()`](`pointblank.ContractImport.to_contract`) on the result to get a `Contract`. Each format supports a different set of rules, so checks with no equivalent in the target format are left out of the export. - Because [`to_validate()`](`pointblank.Contract.to_validate`) returns an ordinary `Validate` object, a contract can be combined with reusable [`Steps`](`pointblank.Steps`) libraries through [`Validate.add_steps()`](`pointblank.Validate.add_steps`). Examples -------- ```{python} #| echo: false #| output: false import pointblank as pb pb.config(report_incl_footer_timings=False, preview_incl_header=False) ``` Let's write a contract for a daily orders extract. The schema fixes the four columns and their types, and the steps say that IDs must be present, each order must appear once, amounts must be positive, and the status must be one of three known values. Ownership and versioning go in the metadata, and the thresholds flag any failing test unit as a warning and a failure rate of 10% or more as an error. ```{python} import pointblank as pb import polars as pl orders_contract = pb.Contract( name="orders", version="1.2.0", owner="sales-data-team", consumers=["finance", "analytics"], description="Daily order extract delivered by the sales platform.", schema=pb.Schema( order_id="String", customer_id="Int64", amount="Float64", status="String", ), steps=[ pb.Step("col_vals_not_null", columns=["order_id", "customer_id"]), pb.Step("rows_distinct", columns_subset=["order_id"]), pb.Step("col_vals_gt", columns="amount", value=0), pb.Step("col_vals_in_set", columns="status", set=["pending", "shipped", "delivered"]), ], thresholds=pb.Thresholds(warning=1, error=0.10), ) orders_contract ``` The contract itself is just a definition. Printing it shows a short summary: the name, direction, version, whether a schema is set, and the number of steps. Now let's apply the contract to a day's extract that meets every expectation. The [`validate()`](`pointblank.Contract.validate`) method compiles the contract against the table and interrogates it, returning a `Validate` object that displays as a validation report. ```{python} orders_day_1 = pl.DataFrame( { "order_id": ["A1", "A2", "A3", "A4"], "customer_id": [101, 102, 101, 103], "amount": [25.00, 110.50, 42.00, 8.99], "status": ["pending", "shipped", "delivered", "shipped"], } ) orders_contract.validate(orders_day_1) ``` The report header shows the label built from the contract (`Contract: orders v1.2.0`) along with the owner, consumers, and version. The first step is the schema check, followed by one step per contract rule. The `col_vals_not_null` step names two columns, so it becomes two validation steps. All six steps pass. The next day's extract has problems: a missing customer ID, a repeated order ID, a negative amount, and a status the contract doesn't allow. The same contract catches all of them. ```{python} orders_day_2 = pl.DataFrame( { "order_id": ["A5", "A6", "A6", "A8"], "customer_id": [104, None, 101, 105], "amount": [25.00, -5.00, 42.00, 8.99], "status": ["pending", "shipped", "returned", "shipped"], } ) validation = orders_contract.validate(orders_day_2) validation ``` The schema still matches, but four of the rule steps have failing test units. With only four rows, one failure is already 25% of the table, so those steps exceed both the warning and the error thresholds. The `rows_distinct` step counts two failures because both rows with order `"A6"` are duplicates. Since the result is an ordinary `Validate` object, you can check it in code, for example to stop a scheduled job: ```{python} validation.all_passed() ``` To share the contract with another team or keep it under version control, write it out as YAML. Called without a path, [`to_yaml()`](`pointblank.Contract.to_yaml`) returns the YAML text. ```{python} print(orders_contract.to_yaml()) ``` The YAML lists the metadata, schema, steps, and thresholds in a form that's easy to read and review. [`from_yaml()`](`pointblank.Contract.from_yaml`) reads it back into an equivalent `Contract`. Finally, a contract can be the starting point for a stricter, local validation. Here, [`to_validate()`](`pointblank.Contract.to_validate`) compiles the contract without interrogating, and we add one more check (orders of 100 or less) before calling [`interrogate()`](`pointblank.Validate.interrogate`). ```{python} ( orders_contract.to_validate(orders_day_1) .col_vals_le(columns="amount", value=100) .interrogate() ) ``` The six contract steps pass as before, and the added seventh step flags the order of 110.50. The contract itself is unchanged, so other users of it aren't affected by the extra rule. See Also -------- Step : A single declarative validation step, used in a contract's `steps=`. Schema : The column names and types used in a contract's `schema=`. Pipeline : Enforce source and target contracts around a transformation. pointblank.export_contract : Convert a contract to JSON Schema, Frictionless, dbt, or ODCS. pointblank.import_contract : Read an external format, then call `to_contract()` on it. Step(method: 'str', **kwargs: 'Any') -> 'None' A single validation step, stored as data. A `Step` records one validation rule: the name of a [`Validate`](`pointblank.Validate`) validation method and the keyword arguments to call it with. `pb.Step("col_vals_gt", columns="amount", value=0)` describes the same check as `Validate.col_vals_gt(columns="amount", value=0)`, but nothing runs when the step is created. The step is a small, inert value that's applied later, when it's compiled into a validation for a specific table. Holding a rule as data rather than as a method call makes it possible to keep rules apart from the code that runs them. Steps can be collected into a [`Contract`](`pointblank.Contract`), written to YAML or JSON and read back, compared with each other, or built from configuration files and external schema formats. The steps in a contract imported with [`import_contract()`](`pointblank.import_contract`), for example, are `Step` objects generated from the source format. The step's parts are available as the `method` attribute (the method name) and the `kwargs` attribute (a dictionary of arguments). Two steps are equal when both their method names and arguments are equal. Parameters ---------- method The name of the validation method to call, such as `"col_vals_gt"` or `"rows_distinct"`. A `Step` doesn't check the name itself. A [`Contract`](`pointblank.Contract`) accepts only its supported methods (listed in its *Supported Step Methods* section) and raises a `ValueError` for any other name. Steps used through [`Steps`](`pointblank.Steps`) and [`Validate.add_steps()`](`pointblank.Validate.add_steps`) can name any validation method. **kwargs The arguments for the validation method, given by keyword (e.g., `columns="amount"`, `value=0`). They're stored as given and passed to the method when the step is compiled, so a misspelled or missing argument raises a `TypeError` at that point rather than when the step is created. Positional arguments aren't supported. The one validation method that takes positional arguments, `conjointly()`, receives its expressions as a list in `exprs=` (e.g., `pb.Step("conjointly", exprs=[lambda df: pl.col("a") > 0])`). Returns ------- Step A `Step` object. Writing Steps That Can Be Saved ------------------------------- A contract's steps are written to YAML as a mapping from the method name to its arguments, so a step can be saved only if its arguments are plain data: strings, numbers, booleans, dates, lists, and dictionaries. Step-level thresholds are the exception, since a [`Thresholds`](`pointblank.Thresholds`) object or a tuple is converted to a dictionary of levels automatically. Other objects can't be saved, and [`Contract.to_yaml()`](`pointblank.Contract.to_yaml`) raises a `ValueError` that names the step and argument holding one. Keep these in mind for steps you plan to save: - name columns with strings or lists of strings, rather than with selectors like `pb.starts_with()` or `pb.col()` - steps that take an expression or a function, such as `col_vals_expr` and `conjointly`, can only be used in contracts kept in Python All of these work when the step is used directly in Python. Steps and the Steps Class ------------------------- [`Steps`](`pointblank.Steps`) is a fluent builder for lists of `Step` objects. It has a method for each validation method (`pb.Steps().col_vals_gt(columns="amount", value=0)`), so you get the real method signatures, with argument names and defaults, while writing the steps. Use `Step` when the method name and arguments are data (when they come from a configuration file, for example) or when building a contract's `steps=` list. A list of `Step` objects can also be turned into a `Steps` object with `pb.Steps(steps=...)` and added to any validation with [`Validate.add_steps()`](`pointblank.Validate.add_steps`). Examples -------- ```{python} #| echo: false #| output: false import pointblank as pb pb.config(report_incl_footer_timings=False, preview_incl_header=False) ``` Let's create a step that checks that ages fall between 0 and 120. Printing it shows the method name and the stored arguments. ```{python} import pointblank as pb age_step = pb.Step("col_vals_between", columns="age", left=0, right=120) age_step ``` The two parts of the step are available as attributes: ```{python} print(age_step.method) print(age_step.kwargs) ``` Steps are normally used in groups. Here are three more rules for a table of patients: each patient ID must appear only once, IDs must follow a pattern, and the `visit` column must be present. Note that `rows_distinct` takes `columns_subset=` rather than `columns=`, because each step's arguments are exactly those of its `Validate` method. ```{python} patient_steps = [ pb.Step("rows_distinct", columns_subset="patient_id"), pb.Step("col_vals_regex", columns="patient_id", pattern=r"^PT-\d{3}$"), pb.Step("col_exists", columns="visit"), age_step, ] ``` Placing the steps in a [`Contract`](`pointblank.Contract`) lets us apply them to a table. The steps run in the order they're listed. ```{python} import polars as pl patients = pl.DataFrame( { "patient_id": ["PT-001", "PT-002", "PT-003", "PT-03"], "age": [34, 51, 130, 28], "visit": [1, 1, 2, 1], } ) patients_contract = pb.Contract(name="patients", steps=patient_steps) patients_contract.validate(patients) ``` The ID check flags `"PT-03"`, which is missing a digit, and the age check flags the value `130`. The other two steps pass. The same steps can be added to an ordinary validation by wrapping them in a [`Steps`](`pointblank.Steps`) object. That route also accepts validation methods that contracts don't support, such as [`col_vals_str_len()`](`pointblank.Validate.col_vals_str_len`): ```{python} id_length_step = pb.Step("col_vals_str_len", columns="patient_id", min_val=6, max_val=6) ( pb.Validate(data=patients, tbl_name="patients") .add_steps(pb.Steps(steps=patient_steps + [id_length_step])) .interrogate() ) ``` The first four steps give the same results as in the contract. The new length check, in step 5, also catches `"PT-03"`, which has five characters instead of six. Defining the steps once as a list lets a contract and an ordinary validation share them. See Also -------- Contract : Collect steps, a schema, and metadata into a reusable contract. Steps : Build lists of steps with the signatures of the validation methods. Validate.add_steps : Add steps from a `Steps` object to a validation. Steps(steps: 'list[Step] | None' = None) -> 'None' A reusable collection of validation step definitions. A `Steps` object is a list of validation steps that isn't attached to any table. You build it with the same methods you'd call on a [`Validate`](`pointblank.Validate`) object (`col_vals_not_null()`, `rows_distinct()`, `col_vals_regex()`, and so on), but each call only records the step. Nothing is checked until the collection is added to a validation with [`Validate.add_steps()`](`pointblank.Validate.add_steps`) and that validation is interrogated. This makes it possible to write a group of checks once and use it in many places. Many tables share the same rules: every table with a customer ID needs that ID to be present and unique, and every table with an email column needs valid addresses. Without `Steps`, those rules get copied into each validation and drift apart over time. With `Steps`, you define them in one place (a shared module, for example), name them for what they check (`id_checks`, `contact_checks`), and add them to whichever validations need them. Each validation keeps its own table, thresholds, actions, and metadata, and can mix shared steps with its own. Since the methods have the same signatures as the `Validate` methods they mirror, you get the same argument names, defaults, and editor completion, and a misspelled argument raises a `TypeError` as soon as the step is recorded. Parameters ---------- steps An optional list of [`Step`](`pointblank.Step`) objects to start the collection with. This is useful when step definitions come from data (a configuration file, for example) rather than from method calls. Returns ------- Steps A `Steps` object. Add steps to it with its validation methods, and apply it with [`Validate.add_steps()`](`pointblank.Validate.add_steps`). Recording Steps --------------- Each validation method records one step and returns the same `Steps` object, so calls can be chained. All of the row-based and table-level validation methods are available, from `col_vals_gt()` to `tbl_reconcile()`. The aggregation methods (`col_sum_*()`, `col_avg_*()`, and `col_sd_*()`), `specially()`, and `prompt()` aren't, so add those directly to the validation. Because each method adds to the object it's called on, a collection isn't copied when you add to it. Calling a method on a shared collection (e.g., `extended = id_checks.col_exists(...)`) adds the step to `id_checks` itself, and `extended` is the same object. To build variations, keep the shared parts in separate collections and add several of them to a validation. Printing a `Steps` object lists its steps with the arguments that differ from the defaults, and `len()` gives the number of steps. Applying Steps to a Validation ------------------------------ [`Validate.add_steps()`](`pointblank.Validate.add_steps`) appends the recorded steps to a validation, in order, as if each method had been called on it directly. It takes any number of `Steps` objects (and also `Validate` objects, whose steps it copies), and has options for adapting the steps to the validation at hand: - `columns_map=` renames columns in the steps, so that the same collection works on tables that name a column differently (e.g., `{"customer_id": "cust_id"}`) - `thresholds=` and `active=` replace the thresholds and active state of every added step - `exclude=` skips steps by method name or by their 1-based position in a collection Steps, Step, and Contract ------------------------- A [`Step`](`pointblank.Step`) is a single step stored as data: a method name and a dictionary of arguments. A `Steps` object holds a list of them, built with method calls instead. A [`Contract`](`pointblank.Contract`) also holds a list of `Step` objects, together with a schema and metadata (an owner, a version, consumers), and can be saved as YAML. Use `Steps` for reusable building blocks inside Python code, and a `Contract` for an agreement about a table that's shared, versioned, and enforced at a pipeline boundary. The two work together: a contract compiled with [`to_validate()`](`pointblank.Contract.to_validate`) is an ordinary validation, so collections can be added to it with `add_steps()`. Examples -------- ```{python} #| echo: false #| output: false import pointblank as pb pb.config(report_incl_footer_timings=False, preview_incl_header=False) ``` Let's build two collections of checks for customer data. The first covers identifiers (each customer ID must be present and appear only once), and the second covers contact details (email addresses must look valid, and the country must be one of three codes). ```{python} import pointblank as pb import polars as pl id_checks = ( pb.Steps() .col_vals_not_null(columns="customer_id") .rows_distinct(columns_subset="customer_id") ) contact_checks = ( pb.Steps() .col_vals_regex(columns="email", pattern=r"^[^@]+@[^@]+\.[a-z]+$") .col_vals_in_set(columns="country", set=["CA", "MX", "US"]) ) id_checks ``` Displaying a collection lists its steps and their arguments. No table is involved yet. Now let's apply both collections to a table of customers from a CRM system. The validation is created as usual, and `add_steps()` appends the four recorded steps before interrogation. ```{python} crm_customers = pl.DataFrame( { "customer_id": [1, 2, 3, 3, 5], "email": ["a@x.com", "b@y.org", "c-at-z.com", "d@w.net", "e@v.io"], "country": ["US", "CA", "US", "MX", "BR"], } ) ( pb.Validate(data=crm_customers, tbl_name="crm_customers") .add_steps(id_checks, contact_checks) .interrogate() ) ``` The report has one step per recorded step, in the order they were added. Customer ID `3` appears twice, so both of its rows fail the `rows_distinct` step. The address `"c-at-z.com"` fails the email pattern, and the country `"BR"` isn't in the allowed set. The same collections can check a table from a billing system, where the ID column is called `cust_id`. The `columns_map=` argument renames the column in the added steps, and `thresholds=` gives every added step a warning threshold of one failing test unit. ```{python} billing_accounts = pl.DataFrame( { "cust_id": [10, 11, None, 13], "email": ["f@x.com", "g@y.com", "h@z.com", "i@w"], "country": ["US", "US", "CA", "US"], } ) ( pb.Validate(data=billing_accounts, tbl_name="billing_accounts") .add_steps( id_checks, contact_checks, columns_map={"customer_id": "cust_id"}, thresholds=pb.Thresholds(warning=1), ) .interrogate() ) ``` The ID steps now check `cust_id`, and one account is missing its ID. The address `"i@w"` has no domain suffix, so it fails the email pattern. Both failing steps reach the warning threshold. See Also -------- Validate.add_steps : Add the steps in one or more collections to a validation. Step : A single validation step stored as data. Contract : A schema, steps, and metadata that make up an agreement about a table. add_steps(self, *steps: 'Steps | Validate', active: 'bool | Callable | None' = None, thresholds: 'int | float | bool | tuple | dict | Thresholds | None' = None, exclude: 'list[str | int] | None' = None, columns_map: 'dict[str, str] | None' = None) -> 'Validate' Add the steps from one or more `Steps` collections to the validation. This method appends previously recorded validation steps to the validation plan. The main source of steps is a [`Steps`](`pointblank.Steps`) collection: a reusable group of steps built with the same methods as `Validate` but not attached to any table. Each recorded step is applied to this validation as if its method had been called directly, in the order the steps were recorded. Several collections can be added in one call, and the result can be chained with other validation methods, so shared steps and steps specific to this table can be combined in one plan. A `Validate` object can also be given, in which case its steps are copied into this validation as they were defined, whether or not it has been interrogated (a step that interrogation split into one step per column or segment is copied once). Steps written for one table don't always fit another one exactly. The optional arguments adapt the added steps without changing the collections themselves: `columns_map=` renames columns, `thresholds=` and `active=` replace those settings on every added step, and `exclude=` leaves out steps that don't apply. Parameters ---------- *steps One or more [`Steps`](`pointblank.Steps`) or `Validate` objects whose steps should be added, in the order given. active Replace the `active=` setting of every added step. If `None` (the default), each step keeps its own setting. Use `False` to add the steps in an inactive state, or a callable (such as [`has_columns()`](`pointblank.has_columns`)) to decide at interrogation time. thresholds Replace the `thresholds=` setting of every added step. If `None` (the default), each step keeps its own thresholds and otherwise uses the thresholds of this validation. Accepts the same values as the `thresholds=` argument of `Validate`. exclude Steps to leave out, as a list of method names (e.g., `["col_vals_regex"]` skips every regex step) or of 1-based positions (e.g., `[2]` skips the second step). Positions count within each collection, so `[2]` skips the second step of every collection given. columns_map A dictionary that renames columns in the added steps, mapping the column names used in the collection to the names in this table (e.g., `{"customer_id": "cust_id"}`). It applies to column names given as strings or lists of strings in a step's `columns=`, `column=`, or `columns_subset=` argument. Column selectors such as [`starts_with()`](`pointblank.starts_with`) are left as they are. Returns ------- Validate The `Validate` object with the steps added, for further method chaining. Raises ------ ValueError If no `Steps` or `Validate` objects are given. TypeError If an argument in `*steps` isn't a `Steps` or `Validate` object, if an item in `exclude=` isn't a string or an integer, or if a step's arguments don't fit its validation method. Examples -------- ```{python} #| echo: false #| output: false import pointblank as pb pb.config(report_incl_footer_timings=False, preview_incl_header=False) ``` Let's define two collections of steps for order data: one for completeness and one for valid amounts. ```{python} import pointblank as pb import polars as pl completeness = ( pb.Steps() .col_vals_not_null(columns="order_id") .col_vals_not_null(columns="email") ) valid_amounts = ( pb.Steps() .col_vals_gt(columns="amount", value=0) .col_vals_le(columns="amount", value=10_000) ) ``` Both collections can be added to a validation of an orders table, along with a check that only applies to this table (the `region` values): ```{python} orders = pl.DataFrame( { "order_id": ["A1", "A2", None, "A4"], "email": ["a@x.com", None, "c@z.com", "d@w.net"], "amount": [25.0, 120.0, 0.0, 18_500.0], "region": ["east", "west", "east", "north"], } ) ( pb.Validate(data=orders, tbl_name="orders") .add_steps(completeness, valid_amounts) .col_vals_in_set(columns="region", set=["east", "west"]) .interrogate() ) ``` The first four steps come from the two collections, in order, and the fifth is the table-specific check. Each step finds one failing row. A table of refunds uses the same rules, but with different column names, no email column, and a stricter tolerance for failures. Here, `columns_map=` renames the columns, `exclude=` skips the second completeness step (the email check), and `thresholds=` makes any failure an error. ```{python} refunds = pl.DataFrame( {"refund_ref": ["R1", "R2", "R3"], "refund_amt": [15.0, -4.0, 60.0]} ) ( pb.Validate(data=refunds, tbl_name="refunds") .add_steps( completeness, valid_amounts, columns_map={"order_id": "refund_ref", "amount": "refund_amt"}, exclude=[2], thresholds=pb.Thresholds(error=1), ) .interrogate() ) ``` Because positions count within each collection, `exclude=[2]` also skips the second step of `valid_amounts` (the upper limit on the amount). The negative refund fails the remaining amount check and reaches the error threshold. Pipeline(source: 'Contract | None' = None, target: 'Contract | None' = None, thresholds: 'Thresholds | None' = None, actions: 'Actions | None' = None, final_actions: 'FinalActions | None' = None, label: 'str | None' = None, short_circuit: 'bool' = True) -> None Enforce data contracts at both ends of a data transformation. Most data problems are introduced at a boundary: an upstream system starts sending nulls or a new category, or a transformation step drops, duplicates, or miscalculates rows. A `Pipeline` guards those boundaries. It pairs a *source* [`Contract`](`pointblank.Contract`), describing the data a transformation expects to receive, with a *target* contract, describing the data it promises to produce. Data is validated against the source contract on the way in and against the target contract on the way out, so a failure tells you whether the input was bad or the transformation went wrong. The pipeline doesn't run or schedule your transformation logic beyond calling the function you give it. The transformation can use any DataFrame library, and the pipeline can sit inside any orchestration tool. You can also check just one boundary with [`validate_source()`](`pointblank.Pipeline.validate_source`) or [`validate_target()`](`pointblank.Pipeline.validate_target`), for example when the transformation runs somewhere else. Only one of the two contracts is required, so a pipeline can also act as an ingestion guard (source only) or an output guard (target only). Parameters ---------- source The [`Contract`](`pointblank.Contract`) that incoming data must meet. Leave it as `None` for a pipeline that only checks output. target The [`Contract`](`pointblank.Contract`) that the transformed data must meet. Leave it as `None` for a pipeline that only checks input. At least one of `source=` and `target=` must be given. thresholds [`Thresholds`](`pointblank.Thresholds`) to use for both boundary validations. When given, they replace each contract's own `thresholds=` (they aren't merged), which lets the same contracts be applied more or less strictly in different settings. Thresholds set on individual steps still take precedence. Thresholds determine the warning, error, and critical levels shown in the reports and the [`Actions`](`pointblank.Actions`) they trigger, but not whether a boundary passes (see *Passing and Failing* below). actions [`Actions`](`pointblank.Actions`) to take when a step in either boundary validation reaches a threshold level. The same actions are used for both boundaries. final_actions [`FinalActions`](`pointblank.FinalActions`) to run when a boundary validation finishes. They run once for each boundary that's validated, so twice in a full run. label A label for the pipeline. Each boundary validation is labeled `"