Repository: data61/blocklib (language: python)

Issue:
Serialize to a blocking schema
Related to #49, if we have an API to create and validate blocking schemas we need a method to get back to a JSON schema.


Discussion / hints:

Fix patch:
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index cca48f5..489eebd 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -46,14 +46,13 @@ stages:
     displayName: Linux
     strategy:
       matrix:
-        python36:
-          PYTHON_VERSION: '3.6'
         python37:
           PYTHON_VERSION: '3.7'
         python38:
           PYTHON_VERSION: '3.8'
         python39:
           PYTHON_VERSION: '3.9'
+
     pool:
       vmImage: 'ubuntu-18.04'
     steps:
diff --git a/blocklib/__init__.py b/blocklib/__init__.py
index 49c9d23..3b0757d 100644
--- a/blocklib/__init__.py
+++ b/blocklib/__init__.py
@@ -4,7 +4,7 @@
 from .pprllambdafold import PPRLIndexLambdaFold
 from .signature_generator import generate_signatures
 from .blocks_generator import generate_blocks, generate_reverse_blocks
-from .validation import validate_signature_config
+from .validation import validate_blocking_schema
 from .candidate_blocks_generator import generate_candidate_blocks
 from .encoding import generate_bloom_filter, flip_bloom_filter
 from .evaluation import assess_blocks_2party
diff --git a/blocklib/blocks_generator.py b/blocklib/blocks_generator.py
index cb02b94..f165626 100644
--- a/blocklib/blocks_generator.py
+++ b/blocklib/blocks_generator.py
@@ -1,4 +1,4 @@
-"""Module that implement final block generations."""
+"""Module that implements final block generations."""
 from collections import defaultdict
 from typing import Any, Dict, Sequence, Set, List, cast
 import ast
@@ -11,9 +11,10 @@
 
 def check_block_object(candidate_block_objs: Sequence[CandidateBlockingResult]):
     """
-    Check candidate block objects type and their states type. Raise TypeError if conditions aren't met.
+    Check candidate block objects type and their states type.
+
+    :raises TypeError: if conditions aren't met.
     :param candidate_block_objs: A list of candidate block result objects from 2 data providers
-    :return:
     """
     for obj in candidate_block_objs:
         if type(obj) != CandidateBlockingResult:
@@ -25,13 +26,13 @@ def check_block_object(candidate_block_objs: Sequence[CandidateBlockingResult]):
             raise TypeError('Unexpected state type found: {} where we expect: {}'.format(type(obj.state), state_type))
 
 
-def generate_blocks(candidate_block_objs: Sequence[CandidateBlockingResult], K: int):
+def generate_blocks(candidate_block_objs: Sequence[CandidateBlockingResult], K: int) -> List[Dict[Any, List[Any]]]:
     """
     Generate final blocks given list of candidate block objects from 2 or more than 2 data providers.
 
     :param candidate_block_objs: A list of CandidateBlockingResult from multiple data providers
     :param K: it specifies the minimum number of occurrence for records to be included in the final blocks
-    :return: filtered_reversed_indices: List of dictionaries, filter out records that appear in less than K parties
+    :return: List of dictionaries, filter out records that appear in less than K parties
     """
     check_block_object(candidate_block_objs)
     assert len(candidate_block_objs) >= K >= 2
@@ -61,10 +62,10 @@ def generate_blocks(candidate_block_objs: Sequence[CandidateBlockingResult], K:
 
 
 def generate_reverse_blocks(reversed_indices: Sequence[Dict]):
-    """
-    Return a list of dictionaries of record to block key mapping
+    """Invert a map from "blocks to records" to "records to blocks".
+
     :param reversed_indices: A list of dictionaries where key is the block key and value is a list of record IDs.
-    :return: rec_to_blockkey: A list of dictionaries where key is the record ID and value is a set of block key the record belongs to
+    :return: A list of dictionaries where key is the record ID and value is a set of blocking keys the record belongs to.
     """
     rec_to_blockkey = []
     for reversed_index in reversed_indices:
@@ -77,12 +78,12 @@ def generate_reverse_blocks(reversed_indices: Sequence[Dict]):
 
 
 def generate_blocks_psig(reversed_indices: Sequence[Dict], block_states: Sequence[PPRLIndexPSignature], threshold: int):
-    """
-    Generate final blocks for P-Sig.
+    """Generate blocks for P-Sig
+
     :param reversed_indices: A list of dictionaries where key is the block key and value is a list of record IDs.
     :param block_states: A list of PPRLIndex objects that hold configuration of the blocking job
     :param threshold: int which decides a pair when number of 1 bits in bloom filter is large than or equal to threshold
-    :return: reversed_indices: A list of dictionaries where blocks that don't contain any matches are deleted
+    :return: A list of dictionaries where blocks that don't contain any matches are deleted
     """
     # generate candidate bloom filters
     candidate_bloom_filters = []
@@ -92,7 +93,7 @@ def generate_blocks_psig(reversed_indices: Sequence[Dict], block_states: Sequenc
             bf_set = ast.literal_eval(bf_set)
             cbf = cbf.union(bf_set)
 
-        bf_len = int(block_states[0].blocking_config.get("bf-len", None))
+        bf_len = block_states[0].blocking_config.bloom_filter_length
         bf_vector = np.zeros(bf_len, dtype=bool)
         bf_vector[list(cbf)] = True
         candidate_bloom_filters.append(bf_vector)
diff --git a/blocklib/candidate_blocks_generator.py b/blocklib/candidate_blocks_generator.py
index f15b151..c78bbf0 100644
--- a/blocklib/candidate_blocks_generator.py
+++ b/blocklib/candidate_blocks_generator.py
@@ -1,12 +1,10 @@
-"""Class that implement candidate block generations."""
 import sys
 
 from typing import Dict, Sequence, Tuple, Type, List, Optional, TextIO
-from .configuration import get_config
 from .pprlindex import PPRLIndex, ReversedIndexResult
 from .pprlpsig import PPRLIndexPSignature
 from .pprllambdafold import PPRLIndexLambdaFold
-from .validation import validate_signature_config
+from .validation import validate_blocking_schema
 
 
 PPRLSTATES = {
@@ -56,30 +54,29 @@ def print_stats(stats: Dict, out: TextIO):
                 print_stats(stat, output)
 
 
-def generate_candidate_blocks(data: Sequence[Tuple[str, ...]], signature_config: Dict, header: Optional[List[str]] = None):
+def generate_candidate_blocks(data: Sequence[Tuple[str, ...]],
+                              blocking_schema: Dict,
+                              header: Optional[List[str]] = None) -> CandidateBlockingResult:
     """
     :param data: list of tuples E.g. ('0', 'Kenneth Bain', '1964/06/17', 'M')
-    :param signature_config:
+    :param blocking_schema:
         A description of how the signatures should be generated.
-        Schema for the signature config is found in
-        ``docs/schema/signature-config-schema.json``
+        See :ref:`blocking-schema`
     :param header: column names (optional)
         Program should throw exception if block features are string but header is None
 
     :return: A 2-tuple containing
         A list of "signatures" per record in data.
         Internal state object from the signature generation (or None).
-
     """
-    # validate config of blocking
-    validate_signature_config(signature_config)
+    blocking_model = validate_blocking_schema(blocking_schema)
 
     # extract algorithm and its config
-    algorithm = signature_config.get('type', 'not specified')
-    config = signature_config.get('config', 'not specified')
+    algorithm = blocking_model.type.value
+    config = blocking_model.config
 
     # check if blocking features are column index or feature name
-    blocking_features = get_config(config, 'blocking-features')
+    blocking_features = config.blocking_features
     feature_type = type(blocking_features[0])
     error_msg = 'All feature types should be the same - either feature name or feature index'
     assert all(type(x) == feature_type for x in blocking_features[1:]), error_msg
diff --git a/blocklib/pprlindex.py b/blocklib/pprlindex.py
index b3982d0..5b537ba 100644
--- a/blocklib/pprlindex.py
+++ b/blocklib/pprlindex.py
@@ -1,17 +1,23 @@
 import random
-from typing import Dict, List, Sequence, Optional
+from typing import Any, Dict, List, Sequence, Optional, Union, cast
+import logging
+from pydantic.tools import parse_obj_as
+
 from blocklib.configuration import get_config
 from blocklib.utils import check_header
+from blocklib.validation import PPRLIndexConfig
 
 
 class PPRLIndex:
     """Base class for PPRL indexing/blocking."""
 
-    def __init__(self, config: Dict = {}) -> None:
+    def __init__(self, config: PPRLIndexConfig) -> None:
         """Initialise base class."""
+
+        self.config: PPRLIndexConfig = cast(PPRLIndexConfig, config)
         self.rec_dict = None
         self.ent_id_col = None
-        self.rec_id_col = None
+        self.rec_id_col: Optional[int] = None
 
     def get_feature_to_index_map(self, data: Sequence[Sequence], header: Optional[List[str]] = None):
         """Return feature name to feature index mapping if there is a header and feature is of type string."""
@@ -49,7 +55,8 @@ def build_reversed_index(self, data: Sequence[Sequence],  header: Optional[List[
         """
         raise NotImplementedError("Derived class needs to implement")
 
-    def select_reference_value(self, reference_data: Sequence[Sequence], ref_data_config: Dict):
+    @classmethod
+    def select_reference_value(cls, reference_data: Sequence[Sequence], ref_data_config: Dict):
         """Load reference data for methods need reference."""
         # read configurations
         ref_default_features = get_config(ref_data_config, 'blocking-features')
@@ -63,7 +70,7 @@ def select_reference_value(self, reference_data: Sequence[Sequence], ref_data_co
         random.seed(ref_random_seed)
         ref_val_list = random.sample(rec_features, num_vals)
 
-        print('  Selected %d random reference values' % (len(ref_val_list)))
+        logging.info('Selected %d random reference values' % (len(ref_val_list)))
         return ref_val_list
 
 
diff --git a/blocklib/pprllambdafold.py b/blocklib/pprllambdafold.py
index 0aaa1e4..baacb93 100644
--- a/blocklib/pprllambdafold.py
+++ b/blocklib/pprllambdafold.py
@@ -1,12 +1,13 @@
 import random
 
 from collections import defaultdict
-from typing import Dict, Sequence, Any, List, Optional
-from blocklib.configuration import get_config
+from typing import Dict, Sequence, Any, List, Optional, Union, cast
+
 from .pprlindex import PPRLIndex, ReversedIndexResult
 from .encoding import generate_bloom_filter
 from .utils import deserialize_filters
 from .stats import reversed_index_stats
+from .validation import LambdaConfig
 
 
 class PPRLIndexLambdaFold(PPRLIndex):
@@ -17,27 +18,26 @@ class PPRLIndexLambdaFold(PPRLIndex):
         This class includes an implementation of Lambda-fold redundant blocking method.
     """
 
-    def __init__(self, config: Dict):
+    def __init__(self, config: Union[LambdaConfig, Dict]):
         """Initialize the class and set the required parameters.
 
-        Arguments:
-        - config: dict
-            Configuration for P-Sig reverted index.
-
+        :param config: Configuration for P-Sig reverted index.
         """
-        super().__init__()
-        self.blocking_features = get_config(config, "blocking-features")
+
+        if isinstance(config, dict):
+            config = LambdaConfig.parse_obj(config)
+        config = cast(LambdaConfig, config)
+        super().__init__(config)
+        self.blocking_features = config.blocking_features
         # Lambda: number of redundant tables
-        self.mylambda = int(get_config(config, "Lambda"))
-        # bf-len: length of bloom filter
-        self.bf_len = int(get_config(config, "bf-len"))
-        # num_hash_function
-        self.num_hash_function = int(get_config(config, "num-hash-funcs"))
+        self.mylambda = config.Lambda
+        self.bf_len = config.bloom_filter_length
+        self.num_hash_function = config.number_of_hash_functions
         # K: number of base Hamming LSH hashing functions
-        self.K = int(get_config(config, "K"))
-        self.input_clks = get_config(config, 'input-clks')
-        self.random_state = get_config(config, "random_state")
-        self.record_id_col = config.get("record-id-col", None)
+        self.K = config.K
+        self.input_clks = config.block_encodings
+        self.random_state = config.random_state
+        self.record_id_col = config.record_id_column
 
     def __record_to_bf__(self, record: Sequence, blocking_features_index: List[int]):
         """Convert a record to list of bigrams and then map to a bloom filter."""
@@ -50,6 +50,7 @@ def __record_to_bf__(self, record: Sequence, blocking_features_index: List[int])
 
     def build_reversed_index(self, data: Sequence[Any], header: Optional[List[str]] = None):
         """Build inverted index for PPRL Lambda-fold blocking method.
+
         :param data: list of lists
         :param header: file header, optional
         :return: reversed index as ReversedIndexResult
diff --git a/blocklib/pprlpsig.py b/blocklib/pprlpsig.py
index 1a5477a..e54a14e 100644
--- a/blocklib/pprlpsig.py
+++ b/blocklib/pprlpsig.py
@@ -1,40 +1,45 @@
 import logging
 from collections import defaultdict
-from typing import Dict, List, Sequence, Any, Optional
+from typing import Dict, List, Sequence, Any, Optional, Union, cast
 
-from .configuration import get_config
 from .encoding import flip_bloom_filter
 from .pprlindex import PPRLIndex, ReversedIndexResult
 from .signature_generator import generate_signatures
 from .stats import reversed_index_per_strategy_stats, reversed_index_stats
+from .validation import PSigConfig
 
 
 class PPRLIndexPSignature(PPRLIndex):
     """Class that implements the PPRL indexing technique:
 
-        Reference scalability entity resolution using probability signatures
-        on parallel databases.
+    Reference scalability entity resolution using probability signatures
+    on parallel databases.
 
-        This class includes an implementation of p-sig algorithm.
+    This class includes an implementation of p-sig algorithm.
     """
 
-    def __init__(self, config: Dict) -> None:
+    def __init__(self, config: Union[PSigConfig, Dict]) -> None:
         """Initialize the class and set the required parameters.
 
         Arguments:
-        - config: dict
-            Configuration for P-Sig reverted index.
+        - config: Configuration for P-Sig reverted index.
 
         """
-        super().__init__()
-        self.blocking_features = get_config(config, "blocking-features")
-        self.filter_config = get_config(config, "filter")
-        self.blocking_config = get_config(config, "blocking-filter")
-        self.signature_strategies = get_config(config, 'signatureSpecs')
-        self.rec_id_col = config.get("record-id-col", None)
+
+        if isinstance(config, dict):
+            config = PSigConfig.parse_obj(config)
+        config = cast(PSigConfig, config)
+        super().__init__(config)
+        self.blocking_features = config.blocking_features
+        self.filter_config = config.filter
+        self.blocking_config = config.blocking_filter
+        self.signature_strategies = config.signatures
+        self.rec_id_col = config.record_id_column
 
     def build_reversed_index(self, data: Sequence[Sequence], header: Optional[List[str]] = None):
-        """Build inverted index given P-Sig method."""
+        """Build inverted index given P-Sig method.
+
+        """
         feature_to_index = self.get_feature_to_index_map(data, header)
         self.set_blocking_features_index(self.blocking_features, feature_to_index)
 
@@ -83,8 +88,8 @@ def build_reversed_index(self, data: Sequence[Sequence], header: Optional[List[s
             )
 
         # map signatures in reversed_index into bloom filter
-        num_hash_func = int(self.blocking_config.get("number-hash-functions", None))
-        bf_len = int(self.blocking_config.get("bf-len", None))
+        num_hash_func = self.blocking_config.number_of_hash_functions
+        bf_len = self.blocking_config.bloom_filter_length
 
         reversed_index = {}  # type: Dict[str, List[Any]]
 
@@ -106,14 +111,14 @@ def filter_reversed_index(self, data: Sequence[Sequence], reversed_index: Dict):
         n = len(data)
 
         # filter blocks based on filter type
-        filter_type = get_config(self.filter_config, "type")
+        filter_type = self.filter_config.type
         if filter_type == "ratio":
-            min_occur_ratio = get_config(self.filter_config, 'min')
-            max_occur_ratio = get_config(self.filter_config, 'max')
+            min_occur_ratio = self.filter_config.min
+            max_occur_ratio = self.filter_config.max
             reversed_index = {k: v for k, v in reversed_index.items() if n * max_occur_ratio > len(v) > n * min_occur_ratio}
         elif filter_type == "count":
-            min_occur_count = get_config(self.filter_config, "min")
-            max_occur_count = get_config(self.filter_config, "max")
+            min_occur_count = self.filter_config.min
+            max_occur_count = self.filter_config.max
             reversed_index = {k: v for k, v in reversed_index.items() if max_occur_count > len(v) > min_occur_count}
         else:
             raise NotImplementedError("Don't support {} filter yet.".format(filter_type))
diff --git a/blocklib/signature_generator.py b/blocklib/signature_generator.py
index 04657f6..45727c3 100644
--- a/blocklib/signature_generator.py
+++ b/blocklib/signature_generator.py
@@ -1,7 +1,10 @@
-from typing import Any, Callable, Dict, List, Sequence, Optional
+from typing import Any, Callable, Dict, List, Sequence, Optional, cast
 
 from metaphone import doublemetaphone
 
+from blocklib.validation import PSigSignatureModel
+from blocklib.validation.psig_validation import PSigCharsAtSignatureSpec
+
 
 def generate_by_feature_value(attr_ind: int, dtuple: Sequence):
     """Generate signatures by simply return original feature at attr_ind."""
@@ -10,10 +13,12 @@ def generate_by_feature_value(attr_ind: int, dtuple: Sequence):
 
 def generate_by_char_at(attr_ind: int, dtuple: Sequence, pos: List[Any]):
     """ Generate signatures by select subset of characters in original features.
+
     >>> res = generate_by_char_at(2, ('harry potter', '4 Privet Drive', 'Little Whinging', 'Surrey'), [0, 3])
     >>> assert res == 'Lt'
     >>> res = generate_by_char_at(2, ('harry potter', '4 Privet Drive', 'Little Whinging', 'Surrey'), [":4"])
     >>> assert res == 'Litt'
+
     """
     sig = []
     feature = dtuple[attr_ind]
@@ -78,13 +83,13 @@ def generate_by_metaphone(attr_ind: int, dtuple: Sequence):
 }  # type: Dict[str, Callable[..., str]]
 
 
-def generate_signatures(signature_strategies: List[List],
+def generate_signatures(signature_strategies: List[PSigSignatureModel],
                         dtuple: Sequence,
                         feature_to_index: Optional[Dict[str, int]] = None):
     """Generate signatures for one record.
 
     :param signature_strategies:
-        A list of dicts each describing a strategy to generate signatures.
+        A list of PSigSignatureModel instances each describing a strategy to generate signatures.
 
     :param dtuple:
         Raw data to generate signatures from
@@ -103,26 +108,27 @@ def generate_signatures(signature_strategies: List[List],
         sig = []
         for spec in strategy:
             # arguments that we need to pass for any strategy
-            attr = spec.get("feature", -1)
+            attr = spec.feature
             if type(attr) == str:
-                assert feature_to_index
-                attr_ind = feature_to_index.get(attr, None)
+                attr_name: str = cast(str, attr)
+                assert feature_to_index, "Missing information to map from feature name to index"
+                attr_ind = feature_to_index.get(attr_name, None)
                 if attr_ind is None:
                     raise ValueError(f'Feature {attr} is not in the dataset')
             else:
-                attr_ind = attr
+                attr_ind = cast(int, attr)
             args = dict(attr_ind=attr_ind, dtuple=[str(x) for x in dtuple])
-            config = spec.get('config', {})
 
             # find the correct strategy function to call
-            func = SIGNATURE_STRATEGIES.get(spec['type'], None)
+            func = SIGNATURE_STRATEGIES.get(spec.type, None)
 
             if func is None:
-                strategy_type = spec['type']
-                raise NotImplementedError('Strategy {} is not implemented yet!'.format(strategy_type))
+                raise NotImplementedError(f'Strategy {spec.type} is not implemented yet!')
             else:
-                config.update(args)
-                s = func(**config)
+                if hasattr(spec, 'config'):
+                    # For now that means it must be a PSigCharsAtSignatureSpec
+                    args.update(cast(PSigCharsAtSignatureSpec, spec).config)
+                s = func(**args)
                 sig.append(s)
         signatures.append('{}_{}'.format(i, "_".join([x for x in sig if x is not None])))
 
diff --git a/blocklib/validation.py b/blocklib/validation.py
deleted file mode 100644
index e388951..0000000
--- a/blocklib/validation.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import json
-from typing import Dict
-import jsonschema
-import pathlib
-
-
-def load_schema(file_name: str):
-    path = pathlib.Path(__file__).parent / 'schemas' / file_name
-
-    # schema_bytes = pkgutil.get_data('anonlinkclient', 'docs/schemas/{}'.format(file_name))
-    with open(str(path.resolve()), 'rt') as schema_file:
-        try:
-            return json.load(schema_file)
-        except json.decoder.JSONDecodeError as e:
-            raise ValueError("Invalid schema") from e
-
-
-
-def validate_signature_config(config: Dict):
-    schema = load_schema('signature-config-schema.json')
-    try:
-        jsonschema.validate(config, schema)
-    except jsonschema.exceptions.ValidationError as e:
-        raise ValueError('The signature config is not valid.\n\n' + str(e)) from e
diff --git a/blocklib/validation/__init__.py b/blocklib/validation/__init__.py
new file mode 100644
index 0000000..b2d6c21
--- /dev/null
+++ b/blocklib/validation/__init__.py
@@ -0,0 +1,62 @@
+import json
+import pathlib
+from enum import Enum
+from typing import Dict, Union
+
+from pydantic import BaseModel, Field, validator, ValidationError
+
+from blocklib.validation.constrained_types import PositiveInt, UnitFloat
+from blocklib.validation.lambda_fold_validation import LambdaConfig
+from blocklib.validation.psig_validation import PSigConfig, PSigSignatureModel, PSigBlockingBFFilterConfig, \
+    PSigCharsAtSignatureSpec, PSigMetaphoneSignatureSpec
+
+
+class BlockingSchemaTypes(str, Enum):
+    psig = 'p-sig'
+    lambdafold = 'lambda-fold'
+
+
+PPRLIndexConfig = Union[PSigConfig, LambdaConfig]
+
+
+class BlockingSchemaModel(BaseModel):
+    version: int
+    type: BlockingSchemaTypes = Field(..., description="Identifier of the blocking type")
+    config: PPRLIndexConfig
+
+    @validator('config', pre=True, always=True, each_item=False)
+    def validate_config(cls, config_to_validate, values):
+        return cls.config_gen(config_to_validate, values)
+
+    @classmethod
+    def config_gen(cls, config_to_validate, values) -> Union[PSigConfig, LambdaConfig]:
+        # TODO rework after https://github.com/samuelcolvin/pydantic/issues/619
+        if not isinstance(config_to_validate, dict):
+            raise ValueError(f'invalid blocking config, not dict')
+        blocking_type = values.get('type')
+
+        if blocking_type == BlockingSchemaTypes.psig:
+            return PSigConfig(**config_to_validate)
+        elif blocking_type == BlockingSchemaTypes.lambdafold:
+            return LambdaConfig(**config_to_validate)
+        else:
+            raise ValueError("Unsupported blocking type")
+
+
+def load_schema(file_name: str):
+    path = pathlib.Path(__file__).parent / 'schemas' / file_name
+
+    # schema_bytes = pkgutil.get_data('anonlinkclient', 'docs/schemas/{}'.format(file_name))
+    with open(str(path.resolve()), 'rt') as schema_file:
+        try:
+            return json.load(schema_file)
+        except json.decoder.JSONDecodeError as e:
+            raise ValueError("Invalid schema") from e
+
+
+def validate_blocking_schema(config: Dict) -> BlockingSchemaModel:
+    """Validate blocking schema data with pydantic.
+
+    :raises ValueError: exceptions when passed an invalid config.
+    """
+    return BlockingSchemaModel.parse_obj(config)
diff --git a/blocklib/validation/constrained_types.py b/blocklib/validation/constrained_types.py
new file mode 100644
index 0000000..38d1dda
--- /dev/null
+++ b/blocklib/validation/constrained_types.py
@@ -0,0 +1,14 @@
+from pydantic.types import ConstrainedInt, ConstrainedFloat
+
+
+class PositiveInt(ConstrainedInt):
+    # These ensure mypy knows what is going on with constrained types
+    # Directly using confloat, conint works, but mypy gets unhappy.
+    # https://github.com/samuelcolvin/pydantic/issues/239
+    ge = 0
+
+
+class UnitFloat(ConstrainedFloat):
+    ge = 0.0
+    le = 1.0
+
diff --git a/blocklib/validation/lambda_fold_validation.py b/blocklib/validation/lambda_fold_validation.py
new file mode 100644
index 0000000..741701c
--- /dev/null
+++ b/blocklib/validation/lambda_fold_validation.py
@@ -0,0 +1,18 @@
+from typing import Optional
+
+from pydantic import Field
+
+from .shared_blocking_config import BlockingConfigBase
+
+
+class LambdaConfig(BlockingConfigBase):
+    Lambda: int = Field(..., description='the degree of redundancy')
+    bloom_filter_length: int = Field(..., alias='bf-len')
+    number_of_hash_functions: int = Field(..., alias='num-hash-funcs')
+    K: int
+    block_encodings: bool = Field(...,
+                                  alias='input-clks',
+                                  description='Input data is CLK rather than PII')
+    random_state: int
+
+    #random_state: Optional[int] = Field(None, alias='random-state')
\ No newline at end of file
diff --git a/blocklib/validation/psig_validation.py b/blocklib/validation/psig_validation.py
new file mode 100644
index 0000000..7abcf97
--- /dev/null
+++ b/blocklib/validation/psig_validation.py
@@ -0,0 +1,71 @@
+from enum import Enum
+from typing import cast, Union, List
+
+from typing_extensions import Literal
+from pydantic import BaseModel, Field
+
+from .constrained_types import UnitFloat, PositiveInt
+from .shared_blocking_config import BlockingConfigBase
+
+
+class PSigFilterConfigBase(BaseModel):
+    type: str
+
+
+class PSigFilterRatioConfig(PSigFilterConfigBase):
+    type: Literal['ratio']
+    max: UnitFloat
+    min: UnitFloat = cast(UnitFloat, 0.0)
+
+
+class PSigFilterCountConfig(PSigFilterConfigBase):
+    type: Literal['count']
+    max: PositiveInt
+    min: PositiveInt
+
+
+class PSigBlockingBFFilterConfig(BaseModel):
+    type: Literal['bloom filter']
+    number_of_hash_functions: int = Field(..., alias='number-hash-functions')
+    bloom_filter_length: int = Field(..., alias='bf-len')
+
+
+class PSigSignatureTypes(str, Enum):
+    chars_at = 'characters-at'
+    feature_value = 'feature-value'
+    metaphone = 'metaphone'
+
+
+class PSigSignatureSpecBase(BaseModel):
+    type: str
+    feature: Union[int, str]
+
+
+class PSigCharsAtSignatureConfig(BaseModel):
+    pos: List[Union[PositiveInt, str]]
+
+
+class PSigCharsAtSignatureSpec(PSigSignatureSpecBase):
+    type: Literal[PSigSignatureTypes.chars_at] = Field(..., name='type', alias='type')
+    config: PSigCharsAtSignatureConfig
+
+
+class PSigMetaphoneSignatureSpec(PSigSignatureSpecBase):
+    type: Literal[PSigSignatureTypes.metaphone]
+
+
+class PSigFeatureValueSignatureSpec(PSigSignatureSpecBase):
+    type: Literal[PSigSignatureTypes.feature_value] = Field(..., name='type', alias='type')
+
+
+PSigSignatureModel = List[Union[
+    PSigCharsAtSignatureSpec,
+    PSigMetaphoneSignatureSpec,
+    PSigFeatureValueSignatureSpec
+]]
+
+
+class PSigConfig(BlockingConfigBase):
+    filter: Union[PSigFilterRatioConfig, PSigFilterCountConfig]
+    blocking_filter: PSigBlockingBFFilterConfig = Field(..., alias='blocking-filter')
+    signatures: List[PSigSignatureModel] = Field(..., alias='signatureSpecs')
diff --git a/blocklib/validation/shared_blocking_config.py b/blocklib/validation/shared_blocking_config.py
new file mode 100644
index 0000000..be608dc
--- /dev/null
+++ b/blocklib/validation/shared_blocking_config.py
@@ -0,0 +1,12 @@
+from typing import Optional, List, Union
+
+from pydantic import BaseModel, Field
+
+
+class BlockingConfigBase(BaseModel):
+    record_id_column: Optional[int] = Field(alias='record-id-col')
+    blocking_features: Union[List[int], List[str]] = Field(
+        description='Features used for blocking',
+        name='blocking-features',
+        alias='blocking-features'
+    )
diff --git a/docs/blocking-schema.rst b/docs/blocking-schema.rst
index 9c93b82..413c2d5 100644
--- a/docs/blocking-schema.rst
+++ b/docs/blocking-schema.rst
@@ -40,7 +40,7 @@ Example Schema
       "type": "lambda-fold",
       "version": 1,
       "config": {
-        "blocking-features": [1, 2],
+        "blocking-features": ["name", "suburb"],
         "Lambda": 30,
         "bf-len": 2048,
         "num-hash-funcs": 5,
diff --git a/docs/index.rst b/docs/index.rst
index 1237e8a..072d524 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -33,6 +33,7 @@ Table of Contents
 
    tutorial/index
    blocking-schema
+   python-api
    development
    devops
    references
diff --git a/docs/python-api.rst b/docs/python-api.rst
new file mode 100644
index 0000000..6267149
--- /dev/null
+++ b/docs/python-api.rst
@@ -0,0 +1,89 @@
+Python API
+==========
+
+Block Generator
+---------------
+
+.. automodule:: blocklib.blocks_generator
+    :members:
+
+
+Blocks
+------
+
+.. automodule:: blocklib.candidate_blocks_generator
+    :members:
+
+
+Base PPRL Index
+---------------
+
+.. automodule:: blocklib.pprlindex
+    :members:
+
+
+Signature Generator
+-------------------
+
+.. automodule:: blocklib.signature_generator
+    :members:
+
+
+P-Sig
+-----
+
+
+.. automodule:: blocklib.pprlpsig
+    :members:
+
+
+Configuration
+~~~~~~~~~~~~~
+
+.. autoclass:: PSigConfig
+    :members:
+    :undoc-members:
+
+
+.. automodule:: blocklib.validation.psig_validation
+    :members:
+    :undoc-members:
+
+
+Lambda Fold
+-----------
+
+.. automodule:: blocklib.pprllambdafold
+    :members:
+
+
+Configuration
+~~~~~~~~~~~~~
+
+
+.. autoclass:: LambdaConfig
+    :members:
+    :undoc-members:
+
+
+.. automodule:: blocklib.validation.lambda_fold_validation
+    :members:
+    :undoc-members:
+
+
+Internal
+--------
+
+``blocklib`` uses pydantic for validation.
+
+.. automodule:: blocklib.validation
+    :members:
+    :undoc-members:
+
+
+Encoding
+--------
+
+.. automodule:: blocklib.encoding
+    :members:
+
diff --git a/docs/tutorial/tutorial_blocking.ipynb b/docs/tutorial/tutorial_blocking.ipynb
index 157053d..d31bc11 100644
--- a/docs/tutorial/tutorial_blocking.ipynb
+++ b/docs/tutorial/tutorial_blocking.ipynb
@@ -26,7 +26,7 @@
     "\n",
     "In this tutorial, we demonstrate how to use blocking in privacy preserving record linkage. \n",
     "\n",
-    "Load example Nothern Carolina voter registration dataset:"
+    "Load example Northern Carolina voter registration dataset:"
    ]
   },
   {
@@ -34,9 +34,18 @@
    "execution_count": 1,
    "metadata": {
     "pycharm": {
-     "is_executing": true
+     "name": "#%%\n"
     }
    },
+   "outputs": [],
+   "source": [
+    "import blocklib"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
    "outputs": [
     {
      "data": {
@@ -120,7 +129,7 @@
        "4  5971993      ingo  richardson  woolsthorpe  3276"
       ]
      },
-     "execution_count": 1,
+     "execution_count": 2,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -137,27 +146,29 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "In this dataset, `recid` is the voter registration number. So we are able to verify the quality of a linkage between snapshots of this dataset taken at different times.  `pc` refers to postcode.\n",
+    "In this dataset, `recid` is the voter registration number, with that we are able to verify the quality of a linkage\n",
+    "between snapshots of this dataset taken at different times. `pc` refers to postcode.\n",
     "\n",
-    "Next step is to config a blocking job. Before we do that, let's look at the blocking methods we are currently supporting:\n",
+    "The next step is to configure how to block the data. There are two privacy preserving blocking methods currently\n",
+    "supported by `blocklib`:\n",
     "\n",
     "1. Probabilistic signature (p-sig)\n",
     "2. LSH based $\\Lambda$-fold redundant (lambda-fold)\n",
     "\n",
-    "Let's firstly look at P-sig\n",
-    "\n",
-    "### Blocking Methods - Probabilistic signature (p-sig)\n",
+    "This tutorial will demonstrate using both of these, starting with probabilistic signatures.\n",
     "\n",
-    "The high level idea behind this blocking method is that it uses signatures as the blocking key and place only records having same signatures into the same block. You can find the original paper here: [Scalable Entity Resolution Using Probabilistic Signatures on Parallel Databases](https://arxiv.org/abs/1712.09691).\n",
+    "## Blocking Methods - Probabilistic signature (p-sig)\n",
     "\n",
-    "Detailed steps and explanations are in the following.\n",
+    "The high level idea behind this blocking method is that it uses signatures as the blocking key and places records\n",
+    "having the same signatures into the same block. You can find the original paper here:\n",
+    "[Scalable Entity Resolution Using Probabilistic Signatures on Parallel Databases](https://arxiv.org/abs/1712.09691).\n",
     "\n",
-    "Let's see an example of configuration for `p-sig`"
+    "An example blocking configuration using probabilistic signatures:"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 2,
+   "execution_count": 3,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -165,8 +176,7 @@
     "    \"type\": \"p-sig\",\n",
     "    \"version\": 1,\n",
     "    \"config\": {\n",
-    "        \"blocking-features\": [1, 2],\n",
-    "#         \"record-id-col\": 0,\n",
+    "        \"blocking-features\": ['givenname', 'surname'],\n",
     "        \"filter\": {\n",
     "            \"type\": \"ratio\",\n",
     "            \"max\": 0.02,\n",
@@ -174,100 +184,148 @@
     "        },\n",
     "        \"blocking-filter\": {\n",
     "            \"type\": \"bloom filter\",\n",
-    "            \"number-hash-functions\": 4,\n",
+    "            \"number-hash-functions\": 20,\n",
     "            \"bf-len\": 2048,\n",
     "        },\n",
     "        \"signatureSpecs\": [\n",
     "            [\n",
-    "                 {\"type\": \"characters-at\", \"config\": {\"pos\": [0]}, \"feature\": 1},\n",
-    "                 {\"type\": \"characters-at\", \"config\": {\"pos\": [0]}, \"feature\": 2},\n",
+    "                 {\"type\": \"characters-at\", \"config\": {\"pos\": [0]}, \"feature\": \"givenname\"},\n",
+    "                 {\"type\": \"characters-at\", \"config\": {\"pos\": [0]}, \"feature\": \"surname\"},\n",
     "            ],\n",
     "            [\n",
-    "                {\"type\": \"metaphone\", \"feature\": 1},\n",
-    "                {\"type\": \"metaphone\", \"feature\": 2},\n",
+    "                {\"type\": \"metaphone\", \"feature\": \"givenname\"},\n",
+    "                {\"type\": \"metaphone\", \"feature\": \"surname\"},\n",
     "            ]\n",
     "        ]\n",
     "    }\n",
-    "}"
+    "}\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The blocking config can be fully validated to ensure all required types are present."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {
+    "pycharm": {
+     "name": "#%%\n"
+    }
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "BlockingSchemaModel(version=1, type=<BlockingSchemaTypes.psig: 'p-sig'>, config=PSigConfig(record_id_column=None, blocking_features=['givenname', 'surname'], filter=PSigFilterRatioConfig(type='ratio', max=0.02, min=0.0), blocking_filter=PSigBlockingBFFilterConfig(type='bloom filter', number_of_hash_functions=20, bloom_filter_length=2048), signatures=[[PSigCharsAtSignatureSpec(type=<PSigSignatureTypes.chars_at: 'characters-at'>, feature='givenname', config=PSigCharsAtSignatureConfig(pos=[0])), PSigCharsAtSignatureSpec(type=<PSigSignatureTypes.chars_at: 'characters-at'>, feature='surname', config=PSigCharsAtSignatureConfig(pos=[0]))], [PSigMetaphoneSignatureSpec(type=<PSigSignatureTypes.metaphone: 'metaphone'>, feature='givenname'), PSigMetaphoneSignatureSpec(type=<PSigSignatureTypes.metaphone: 'metaphone'>, feature='surname')]]))"
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "blocklib.validation.validate_blocking_schema(blocking_config)"
    ]
   },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "**Step1 - Generate Signature**\n",
+    "### Step 1 - Generate Signatures\n",
     "\n",
-    "For a record `r`, a signature is a sub-record derived from record `r` with a signature strategy. An example signature strategy is to concatenate the initials of first and last name, e.g., the signature for record `\"John White\"` is `\"JW\"`.\n",
+    "For a record `r`, a signature is a sub-record derived from record `r` with a signature strategy. An example signature\n",
+    "strategy is to concatenate the initials of first and last name, e.g., the signature for record `\"John White\"` is `\"JW\"`.\n",
     "\n",
-    "We provide the following signature strategies:\n",
+    "`blocklib` provides the following signature strategies:\n",
     "\n",
-    "* feature-value: the signature is generated by returning the selected feature\n",
-    "* characters-at: the signature is generated by selecting a single character or a sequence of characters from selected feature\n",
-    "* metaphone: the signature is generated by phonetic encoding the selected feature using metaphone\n",
+    "* `feature-value`: the signature is generated by returning the selected feature\n",
+    "* `characters-at`: the signature is generated by selecting a single character or a sequence of characters from selected feature\n",
+    "* `metaphone`: the signature is generated by phonetic encoding the selected feature using metaphone\n",
     "\n",
-    "The output of this step is a reversed index where keys are generated signatures / blocking key and the values are list of corresponding record IDs. A record ID could be row index or the actual record identifier if it is available in the dataset.\n",
+    "The output of this step is a reversed index where keys are generated signatures / blocking key, and the values are\n",
+    "lists of corresponding record IDs. A record ID could be row index or the actual record identifier if it is available\n",
+    "in the dataset.\n",
     "\n",
-    "Signature strategies are defined in the `signatureSpecs` section. For example, in the above configuration, we are going to generate two signatures for each record. The first signature is a combination of 3 different signature strategies\n",
+    "Signature strategies are defined in the `signatureSpecs` section. For example, in the above configuration, we are\n",
+    "going to generate two signatures for each record. The first signature produces initials:\n",
     "\n",
+    "```json\n",
+    "[\n",
+    "     {\"type\": \"characters-at\", \"config\": {\"pos\": [0]}, \"feature\": \"givenname\"},\n",
+    "     {\"type\": \"characters-at\", \"config\": {\"pos\": [0]}, \"feature\": \"surname\"}\n",
+    "]\n",
     "```\n",
-    "     {\"type\": \"characters-at\", \"config\": {\"pos\": [0]}, \"feature\": 1},\n",
-    "     {\"type\": \"characters-at\", \"config\": {\"pos\": [0]}, \"feature\": 2},\n",
-    "     {\"type\": \"feature-value\", \"feature_idx\": 4}\n",
     "\n",
-    "```\n",
-    "It combines the initials of first and last name and postcode.\n",
+    "The second signature is generated by a combination of how the two components of a person's name sounds:\n",
     "\n",
-    "The second signature is generated by a combination of 2 signature strategies:\n",
-    "```\n",
-    "    {\"type\": \"metaphone\", \"feature\": 1},\n",
-    "    {\"type\": \"metaphone\", \"feature\": 2},\n",
+    "```json\n",
+    "[\n",
+    "    {\"type\": \"metaphone\", \"feature\": \"givenname\"},\n",
+    "    {\"type\": \"metaphone\", \"feature\": \"surname\"}\n",
+    "]\n",
     "```\n",
     "That is phonetic encoding of first name and last name.\n",
     "\n",
     "*One signature corresponds to one block. I will use signature and block interchangeably but they mean the same thing.*\n",
     "\n",
-    "**Step2 - Filter Too Frequent Signatures**\n",
+    "### Step 2 - Filter Signatures\n",
     "\n",
-    "A signature is assumed to identify a record as uniquely as possible. Therefore, we need to filter out some too frequent signatures since they can uniquely identify the record. On the otherside, we want to be resilient to frequency attack, so we need to filter out too rare signature that only contains very few records. The configuration of filtering is in the `filter` part. For example, in the above configuration, the filter section is configured as:\n",
-    "```\n",
-    "    \"filter\": {\n",
-    "        \"type\": \"ratio\",\n",
-    "        \"max\": 0.02,\n",
-    "        \"min\": 0.001,\n",
-    "    }\n",
-    "```\n",
-    "Then we will filter out all signatures / blocks whose number of records is greater than 2% of number of total records or is less than 0.1% of number of total records. \n",
+    "Signature strategies can create blocks with many records, and blocks with just one record. To impose limits\n",
+    "on the minimum and maximum block size `blocklib` provides configurable filtering.\n",
     "\n",
-    "Note that we also support absoulte filtering configuration i.e. filter by number of counts. For example:\n",
+    "For example, in the above configuration, the filter is configured as:\n",
     "\n",
-    "```\n",
-    "    \"filter\": {\n",
-    "        \"type\": \"count\",\n",
-    "        \"max\": 100,\n",
-    "        \"min\": 5,\n",
-    "    }\n",
+    "```json\n",
+    "{\n",
+    "    \"type\": \"ratio\",\n",
+    "    \"max\": 0.02,\n",
+    "    \"min\": 0.001\n",
+    "}\n",
     "```\n",
     "\n",
-    "**Step3 - Anonymization**\n",
+    "`blocklib` will filter out all signatures / blocks whose number of records is greater than 2% of number of total\n",
+    "records or is less than 0.1% of number of total records. Note these percentages are based on the data provided\n",
+    "to `blocklib` so only use on roughly symmetric sized record linkage.\n",
     "\n",
-    "Given we want to do privacy preserving record linkage, the signatures need to be hashed to avoid leaking of PII information. The most frequent used data structure of such encoding is Bloom Filter. Here we use one Bloom Filter and map all filtered signatures into that Bloom Filter. The configuration of Bloom Filter is in `block-filter` section:\n",
+    "Absolute filtering is also supported to filter by number of records. An example `filter` configuration:\n",
     "\n",
+    "```json\n",
+    "{\n",
+    "    \"type\": \"count\",\n",
+    "    \"max\": 100,\n",
+    "    \"min\": 5\n",
+    "}\n",
     "```\n",
-    "    \"blocking-filter\": {\n",
-    "        \"type\": \"bloom filter\",\n",
-    "        \"number-hash-functions\": 20,\n",
-    "        \"bf-len\": 2048,\n",
-    "    }\n",
     "\n",
+    "### Step 3 - Anonymization\n",
+    "\n",
+    "Given the aim of privacy preserving record linkage, the signatures themselves (e.g. `\"JW\"`) are not going to be\n",
+    "shared, instead following the `p-sig` paper, the signatures all get encoded into a Bloom Filter. Here we use one\n",
+    "Bloom Filter and map all filtered signatures into that Bloom Filter.\n",
+    "\n",
+    "```\n",
+    "\"blocking-filter\": {\n",
+    "    \"type\": \"bloom filter\",\n",
+    "    \"number-hash-functions\": 20,\n",
+    "    \"bf-len\": 2048,\n",
+    "}\n",
     "```\n",
     "\n",
-    "After anonymization, the signature becomes the set of indices of bits 1 in the bloom filter and hence can preseve the privacy of data for each data provider.\n",
+    "After anonymization, the signature becomes the set of indices of bits 1 in the bloom filter and hence can preserve\n",
+    "the privacy of data for each data provider.\n",
     "\n",
-    "### Carry out Blocking Job\n",
+    "### Blocking Data\n",
     "\n",
-    "Okay, once you have a good understanding of the P-Sig blocking, we can carry out our blocking job with `blocklib`. First, we need to process the data since `blocklib` only accept list of tuples or lists as input data. An example data input for blocklib is\n",
+    "Now that we have configured how the P-Sig blocking will work, we can carry out our blocking job with `blocklib`.\n",
+    "Note `blocklib` only accept list of tuples or lists as input data, so some pre-processing may be necessary. Example\n",
+    "data input for `blocklib`:\n",
     "\n",
-    "```\n",
+    "```python\n",
     "[\n",
     "    [761859, 'kate', 'chapman', 'brighton', 4017],\n",
     "    [1384455, 'lian', 'hurse', 'carisbrook', 3464],\n",
@@ -277,12 +335,12 @@
     "]\n",
     "```\n",
     "\n",
-    "**Step1 - Generate Candidate Blocks for Party A - Alice**"
+    "**Step 1 - Generate Candidate Blocks for Party A - Alice**"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 3,
+   "execution_count": 5,
    "metadata": {},
    "outputs": [
     {
@@ -294,13 +352,13 @@
     }
    ],
    "source": [
-    "data_alice = df_alice.to_dict(orient='split')['data']\n",
-    "print(\"Example PII\", data_alice[0])"
+    "alice = df_alice.to_dict(orient='split')\n",
+    "print(\"Example PII\", alice['data'][0])"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 6,
    "metadata": {},
    "outputs": [
     {
@@ -308,12 +366,12 @@
      "output_type": "stream",
      "text": [
       "Statistics for the generated blocks:\n",
-      "\tNumber of Blocks:   5029\n",
+      "\tNumber of Blocks:   5028\n",
       "\tMinimum Block Size: 1\n",
       "\tMaximum Block Size: 61\n",
-      "\tAverage Block Size: 1.8337641678266057\n",
+      "\tAverage Block Size: 1.834128878281623\n",
       "\tMedian Block Size:  1\n",
-      "\tStandard Deviation of Block Size:  3.8368431973204213\n",
+      "\tStandard Deviation of Block Size:  3.8371894627245102\n",
       "\tCoverage:           100.0%\n",
       "Individual statistics for each strategy:\n",
       "Strategy: 0\n",
@@ -338,7 +396,7 @@
    "source": [
     "from blocklib import generate_candidate_blocks\n",
     "\n",
-    "block_obj_alice = generate_candidate_blocks(data_alice, blocking_config)\n",
+    "block_obj_alice = generate_candidate_blocks(alice['data'], blocking_config, header=alice['columns'])\n",
     "block_obj_alice.print_summary_statistics()"
    ]
   },
@@ -346,23 +404,24 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "You can print the statistics of the blocks in order to inspect the block distribution and decide if this is a good blocking result. Here both average and median block sizes are 1 which is resilient to frequency attack. \n",
+    "You can print the statistics of the blocks in order to inspect the block distribution and decide if this is a good blocking result. \n",
     "\n",
-    "You can get the blocking instance and blocks/reversed indice in the `block_obj_alice`. Let's look at the first block in the reversed indcies:"
+    "`generate_candidate_blocks` returns a `CandidateBlockingResult`, the attribute we are most interested in is `blocks`,\n",
+    "a `dict` that maps signatures to lists of records.\n"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 7,
    "metadata": {},
    "outputs": [
     {
      "data": {
       "text/plain": [
-       "'(1560, 401, 491, 1470)'"
+       "'(1920, 1031, 142, 401, 1560, 671, 1830, 941, 52, 1211, 1470, 581, 1740, 851, 2010, 1121, 232, 491, 1650, 761)'"
       ]
      },
-     "execution_count": 5,
+     "execution_count": 7,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -375,14 +434,16 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "To protect the privacy of data, the signature / blocking key is not the original signature such as `JW`. Instead, it is a list of mapped indices of bits 1 in Bloom Filter of `JW`. Next we want to do the same thing for another party - Bob.\n",
+    "To protect privacy, the signature / blocking key is not the original signature such as `JW`. Instead, it is a list of\n",
+    "mapped indices of bits set to 1 in the Bloom Filter for the original signature. Next we want to do the same thing for\n",
+    "another party - _enter Bob_.\n",
     "\n",
     "**Step2 - Generate Candidate Blocks for Party B - Bob**"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 8,
    "metadata": {},
    "outputs": [
     {
@@ -395,7 +456,7 @@
       "\tMaximum Block Size: 59\n",
       "\tAverage Block Size: 1.8377839776803508\n",
       "\tMedian Block Size:  1\n",
-      "\tStandard Deviation of Block Size:  3.838423809405143\n",
+      "\tStandard Deviation of Block Size:  3.8382680217493017\n",
       "\tCoverage:           100.0%\n",
       "Individual statistics for each strategy:\n",
       "Strategy: 0\n",
@@ -420,8 +481,8 @@
    "source": [
     "# NBVAL_IGNORE_OUTPUT\n",
     "df_bob = pd.read_csv('data/bob.csv')\n",
-    "data_bob = df_bob.to_dict(orient='split')['data']\n",
-    "block_obj_bob = generate_candidate_blocks(data_bob, blocking_config)\n",
+    "bob = df_bob.to_dict(orient='split')\n",
+    "block_obj_bob = generate_candidate_blocks(bob['data'], blocking_config, header=bob['columns'])\n",
     "block_obj_bob.print_summary_statistics()\n"
    ]
   },
@@ -431,20 +492,23 @@
    "source": [
     "### Generate Final Blocks\n",
     "\n",
-    "Now we have candidate blocks from both parties, we can generate final blocks by only including signatures that appear in both parties. Instead of directly comparing signature, the algorithm will firstly map the list of signatures into a Bloom Filter for  for each party called the candidate blocking filter, and then creates the combined blocking filter by only retaining the bits that are present in all candidate filters."
+    "Now we have _candidate_ blocks from both parties, we can generate final blocks by only including signatures that appear\n",
+    "in both parties. Instead of directly comparing signatures, the algorithm maps the list of signatures into a\n",
+    "Bloom Filter for each party called the candidate blocking filter, and then creates the combined blocking filter by only\n",
+    "retaining the bits that are present in both candidate filters."
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 9,
    "metadata": {},
    "outputs": [
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Alice: 2793 out of 5029 blocks are in common\n",
-      "Bob:   2793 out of 5018 blocks are in common\n"
+      "Alice: 2794 out of 5028 blocks are in common\n",
+      "Bob:   2794 out of 5018 blocks are in common\n"
      ]
     }
    ],
@@ -462,7 +526,8 @@
    "source": [
     "### Assess Blocking\n",
     "\n",
-    "We can assess the blocking result when we know the ground truth. There are two main metrics to assess blocking result as we mentioned in the beginning of this tutorial. Here is a recap:\n",
+    "We can assess the blocking result when we have ground truth. There are two main metrics to assess blocking result as\n",
+    "mentioned in the beginning of this tutorial. Here is a recap:\n",
     "\n",
     "* reduction ratio: relative reduction in the number of record pairs to be compared.\n",
     "* pair completeness: the percentage of true matches after blocking\n"
@@ -470,14 +535,14 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": 10,
    "metadata": {},
    "outputs": [
     {
      "name": "stderr",
      "output_type": "stream",
      "text": [
-      "assessing blocks: 100%|██████████| 2793/2793 [00:00<00:00, 36770.31key/s]"
+      "assessing blocks: 100%|██████████| 2794/2794 [00:00<00:00, 144438.65key/s]"
      ]
     },
     {
@@ -500,9 +565,8 @@
     "# NBVAL_IGNORE_OUTPUT\n",
     "from blocklib.evaluation import assess_blocks_2party\n",
     "\n",
-    "\n",
-    "subdata1 = [x[0] for x in data_alice]\n",
-    "subdata2 = [x[0] for x in data_bob]\n",
+    "subdata1 = [x[0] for x in alice['data']]\n",
+    "subdata2 = [x[0] for x in bob['data']]\n",
     "\n",
     "rr, pc = assess_blocks_2party([filtered_blocks_alice, filtered_blocks_bob],\n",
     "                              [subdata1, subdata2])\n",
@@ -514,114 +578,33 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "### Feature Name are also Supported!\n",
+    "### Blocking Methods - LSH Based $\\Lambda$-fold Redundant\n",
     "\n",
-    "When there are many columns in the data, it is a bit inconvenient to use feature index. Luckily, blocklib also supports feature name in the blocking schema:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 9,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "blocking_config = {\n",
-    "    \"type\": \"p-sig\",\n",
-    "    \"version\": 1,\n",
-    "    \"config\": {\n",
-    "        \"blocking-features\": ['givenname', 'surname'],\n",
-    "        \"filter\": {\n",
-    "            \"type\": \"ratio\",\n",
-    "            \"max\": 0.02,\n",
-    "            \"min\": 0.00,\n",
-    "        },\n",
-    "        \"blocking-filter\": {\n",
-    "            \"type\": \"bloom filter\",\n",
-    "            \"number-hash-functions\": 4,\n",
-    "            \"bf-len\": 2048,\n",
-    "        },\n",
-    "        \"signatureSpecs\": [\n",
-    "            [\n",
-    "                 {\"type\": \"characters-at\", \"config\": {\"pos\": [0]}, \"feature\": 'givenname'},\n",
-    "                 {\"type\": \"characters-at\", \"config\": {\"pos\": [0]}, \"feature\": 'surname'},\n",
-    "            ],\n",
-    "            [\n",
-    "                {\"type\": \"metaphone\", \"feature\": 'givenname'},\n",
-    "                {\"type\": \"metaphone\", \"feature\": 'surname'},\n",
-    "            ]\n",
-    "        ]\n",
-    "    }\n",
-    "}"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "When generating candidate blocks, a header is required to pass through:"
+    "Now we look the other blocking method that we support - LSH Based $\\Lambda$-fold Redundant blocking. This blocking\n",
+    "method uses a list of selected bits selected randomly from Bloom Filter for each record as block keys.\n",
+    "$\\Lambda$ refers the degree of redundancy i.e. we will conduct LSH-based blocking $\\Lambda$ times, each forms a\n",
+    "blocking group. Then those blocking groups are combined into one blocking results. This will make a record\n",
+    "redundant $\\Lambda$ times but will increase the recall.\n",
+    "\n",
+    "Let's see an example config, this time selecting the blocking features using column indices instead of column names:"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 11,
    "metadata": {},
    "outputs": [
     {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Statistics for the generated blocks:\n",
-      "\tNumber of Blocks:   5029\n",
-      "\tMinimum Block Size: 1\n",
-      "\tMaximum Block Size: 61\n",
-      "\tAverage Block Size: 1.8337641678266057\n",
-      "\tMedian Block Size:  1\n",
-      "\tStandard Deviation of Block Size:  3.8368431973204213\n",
-      "\tCoverage:           100.0%\n",
-      "Individual statistics for each strategy:\n",
-      "Strategy: 0\n",
-      "\tNumber of Blocks:   503\n",
-      "\tMinimum Block Size: 1\n",
-      "\tMaximum Block Size: 61\n",
-      "\tAverage Block Size: 9.16699801192843\n",
-      "\tMedian Block Size:  6\n",
-      "\tStandard Deviation of Block Size:  9.342740344535663\n",
-      "\tCoverage:           100.0%\n",
-      "Strategy: 1\n",
-      "\tNumber of Blocks:   4534\n",
-      "\tMinimum Block Size: 1\n",
-      "\tMaximum Block Size: 7\n",
-      "\tAverage Block Size: 1.0169827966475518\n",
-      "\tMedian Block Size:  1\n",
-      "\tStandard Deviation of Block Size:  0.1583699259848952\n",
-      "\tCoverage:           100.0%\n"
-     ]
+     "data": {
+      "text/plain": [
+       "BlockingSchemaModel(version=1, type=<BlockingSchemaTypes.lambdafold: 'lambda-fold'>, config=LambdaConfig(record_id_column=None, blocking_features=[1, 2], Lambda=5, bloom_filter_length=2048, number_of_hash_functions=10, K=40, block_encodings=False, random_state=0))"
+      ]
+     },
+     "execution_count": 11,
+     "metadata": {},
+     "output_type": "execute_result"
     }
    ],
-   "source": [
-    "data_alice = df_alice.to_dict(orient='split')['data']\n",
-    "header = list(df_alice.columns)\n",
-    "\n",
-    "block_obj_alice = generate_candidate_blocks(data_alice, blocking_config, header=header)\n",
-    "block_obj_alice.print_summary_statistics()"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "### Blocking Methods - LSH Based $\\Lambda$-fold Redundant\n",
-    "\n",
-    "Now we look the other blocking method that we support - LSH Based $\\Lambda$-fold Redundant blocking.This blocking method uses the a list of selected bits selected randomly from Bloom Filter for each record as block keys. $\\Lambda$ refers the degree of redundancy i.e. we will conduct LSH-based blocking $\\Lambda$ times, each forms a blocking group. Then those blocking groups are combined into one blocking results. This will make a record redundant $\\Lambda$ times but will increase the recall.\n",
-    "\n",
-    "Let's see an example config of it:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 11,
-   "metadata": {},
-   "outputs": [],
    "source": [
     "blocking_config = {\n",
     "    \"type\": \"lambda-fold\",\n",
@@ -635,7 +618,9 @@
     "        \"random_state\": 0,\n",
     "        \"input-clks\": False\n",
     "    }\n",
-    "}"
+    "}\n",
+    "\n",
+    "blocklib.validation.validate_blocking_schema(blocking_config)"
    ]
   },
   {
@@ -687,11 +672,11 @@
    ],
    "source": [
     "print('Generating candidate blocks for Alice:')\n",
-    "block_obj_alice = generate_candidate_blocks(data_alice, blocking_config)\n",
+    "block_obj_alice = generate_candidate_blocks(alice['data'], blocking_config)\n",
     "block_obj_alice.print_summary_statistics()\n",
     "print()\n",
     "print('Generating candidate blocks for Bob: ')\n",
-    "block_obj_bob = generate_candidate_blocks(data_bob, blocking_config)\n",
+    "block_obj_bob = generate_candidate_blocks(bob['data'], blocking_config)\n",
     "block_obj_bob.print_summary_statistics()"
    ]
   },
@@ -724,7 +709,7 @@
      "name": "stderr",
      "output_type": "stream",
      "text": [
-      "assessing blocks: 100%|██████████| 4167/4167 [00:00<00:00, 7677.36key/s]\n"
+      "assessing blocks: 100%|██████████| 4167/4167 [00:00<00:00, 10001.17key/s]\n"
      ]
     },
     {
@@ -743,13 +728,6 @@
     "print(f'reduction ratio:   {round(rr, 3)}')\n",
     "print(f'pair completeness: {pc}')"
    ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": []
   }
  ],
  "metadata": {
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 0000000..ff37064
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,2 @@
+[mypy]
+plugins = pydantic.mypy
diff --git a/poetry.lock b/poetry.lock
index fa22f9a..91de70f 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -22,6 +22,23 @@ category = "main"
 optional = true
 python-versions = "*"
 
+[[package]]
+name = "argon2-cffi"
+version = "20.1.0"
+description = "The secure Argon2 password hashing algorithm."
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+cffi = ">=1.0.0"
+six = "*"
+
+[package.extras]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest", "sphinx", "wheel", "pre-commit"]
+docs = ["sphinx"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest"]
+
 [[package]]
 name = "async-generator"
 version = "1.10"
@@ -185,7 +202,7 @@ toml = ["toml"]
 name = "dataclasses"
 version = "0.8"
 description = "A backport of the dataclasses module for Python 3.6"
-category = "dev"
+category = "main"
 optional = false
 python-versions = ">=3.6, <3.7"
 
@@ -223,17 +240,17 @@ python-versions = ">=2.7"
 
 [[package]]
 name = "flake8"
-version = "3.8.4"
+version = "3.9.0"
 description = "the modular source code checker: pep8 pyflakes and co"
 category = "dev"
 optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
 
 [package.dependencies]
 importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
 mccabe = ">=0.6.0,<0.7.0"
-pycodestyle = ">=2.6.0a1,<2.7.0"
-pyflakes = ">=2.2.0,<2.3.0"
+pycodestyle = ">=2.7.0,<2.8.0"
+pyflakes = ">=2.3.0,<2.4.0"
 
 [[package]]
 name = "idna"
@@ -253,7 +270,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "importlib-metadata"
-version = "3.7.2"
+version = "3.7.3"
 description = "Read metadata from Python packages"
 category = "main"
 optional = false
@@ -381,7 +398,7 @@ format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator
 
 [[package]]
 name = "jupyter-client"
-version = "6.1.11"
+version = "6.1.12"
 description = "Jupyter protocol implementation and client libraries"
 category = "main"
 optional = true
@@ -396,7 +413,7 @@ traitlets = "*"
 
 [package.extras]
 doc = ["sphinx (>=1.3.6)", "sphinx-rtd-theme", "sphinxcontrib-github-alt"]
-test = ["jedi (<=0.17.2)", "ipykernel", "ipython", "mock", "pytest", "pytest-asyncio", "async-generator", "pytest-timeout"]
+test = ["async-generator", "ipykernel", "ipython", "mock", "pytest-asyncio", "pytest-timeout", "pytest", "jedi (<0.18)"]
 
 [[package]]
 name = "jupyter-core"
@@ -585,6 +602,35 @@ category = "main"
 optional = true
 python-versions = ">=3.5"
 
+[[package]]
+name = "notebook"
+version = "6.2.0"
+description = "A web-based notebook environment for interactive computing"
+category = "main"
+optional = true
+python-versions = ">=3.5"
+
+[package.dependencies]
+argon2-cffi = "*"
+ipykernel = "*"
+ipython-genutils = "*"
+jinja2 = "*"
+jupyter-client = ">=5.3.4"
+jupyter-core = ">=4.6.1"
+nbconvert = "*"
+nbformat = "*"
+prometheus-client = "*"
+pyzmq = ">=17"
+Send2Trash = ">=1.5.0"
+terminado = ">=0.8.3"
+tornado = ">=6.1"
+traitlets = ">=4.2.1"
+
+[package.extras]
+docs = ["sphinx", "nbsphinx", "sphinxcontrib-github-alt", "sphinx-rtd-theme"]
+json-logging = ["json-logging"]
+test = ["pytest", "coverage", "requests", "nbval", "selenium", "pytest-cov", "requests-unixsocket"]
+
 [[package]]
 name = "numpy"
 version = "1.19.5"
@@ -689,6 +735,17 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
 [package.extras]
 dev = ["pre-commit", "tox"]
 
+[[package]]
+name = "prometheus-client"
+version = "0.9.0"
+description = "Python client for the Prometheus monitoring system."
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.extras]
+twisted = ["twisted"]
+
 [[package]]
 name = "prompt-toolkit"
 version = "3.0.17"
@@ -718,7 +775,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "pycodestyle"
-version = "2.6.0"
+version = "2.7.0"
 description = "Python style guide checker"
 category = "dev"
 optional = false
@@ -732,9 +789,25 @@ category = "main"
 optional = true
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
+[[package]]
+name = "pydantic"
+version = "1.8.1"
+description = "Data validation and settings management using python 3.6 type hinting"
+category = "main"
+optional = false
+python-versions = ">=3.6.1"
+
+[package.dependencies]
+dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""}
+typing-extensions = ">=3.7.4.3"
+
+[package.extras]
+dotenv = ["python-dotenv (>=0.10.4)"]
+email = ["email-validator (>=1.0.3)"]
+
 [[package]]
 name = "pyflakes"
-version = "2.2.0"
+version = "2.3.0"
 description = "passive checker of Python programs"
 category = "dev"
 optional = false
@@ -839,6 +912,14 @@ category = "main"
 optional = true
 python-versions = "*"
 
+[[package]]
+name = "pywinpty"
+version = "0.5.7"
+description = "Python bindings for the winpty library"
+category = "main"
+optional = true
+python-versions = "*"
+
 [[package]]
 name = "pyzmq"
 version = "22.0.3"
@@ -853,7 +934,7 @@ py = {version = "*", markers = "implementation_name == \"pypy\""}
 
 [[package]]
 name = "regex"
-version = "2020.11.13"
+version = "2021.3.17"
 description = "Alternative regular expression module, to replace re."
 category = "dev"
 optional = false
@@ -877,6 +958,14 @@ urllib3 = ">=1.21.1,<1.27"
 security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
 socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
 
+[[package]]
+name = "send2trash"
+version = "1.5.0"
+description = "Send file to trash natively under Mac OS X, Windows and Linux."
+category = "main"
+optional = true
+python-versions = "*"
+
 [[package]]
 name = "six"
 version = "1.15.0"
@@ -995,6 +1084,19 @@ python-versions = ">=3.5"
 lint = ["flake8", "mypy", "docutils-stubs"]
 test = ["pytest"]
 
+[[package]]
+name = "terminado"
+version = "0.9.3"
+description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library."
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.dependencies]
+ptyprocess = {version = "*", markers = "os_name != \"nt\""}
+pywinpty = {version = ">=0.5", markers = "os_name == \"nt\""}
+tornado = ">=4"
+
 [[package]]
 name = "testpath"
 version = "0.4.4"
@@ -1069,16 +1171,16 @@ python-versions = "*"
 
 [[package]]
 name = "urllib3"
-version = "1.26.3"
+version = "1.26.4"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
 
 [package.extras]
-brotli = ["brotlipy (>=0.6.0)"]
 secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
 socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+brotli = ["brotlipy (>=0.6.0)"]
 
 [[package]]
 name = "wcwidth"
@@ -1109,12 +1211,12 @@ docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
 testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
 
 [extras]
-docs = ["Sphinx", "nbsphinx", "pandas", "ipython", "nbval"]
+docs = ["Sphinx", "nbsphinx", "pandas", "notebook", "nbval"]
 
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.6.1"
-content-hash = "4f4bc0e4d76789340563b2048d8d445855e14f7566b57966b68bf4ee5d9de0a2"
+content-hash = "a229a5184c4d53f607b1cf18be6eb9df751771b4cad8e7604c9a69ef625c232b"
 
 [metadata.files]
 alabaster = [
@@ -1129,6 +1231,26 @@ appnope = [
     {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"},
     {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"},
 ]
+argon2-cffi = [
+    {file = "argon2-cffi-20.1.0.tar.gz", hash = "sha256:d8029b2d3e4b4cea770e9e5a0104dd8fa185c1724a0f01528ae4826a6d25f97d"},
+    {file = "argon2_cffi-20.1.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:6ea92c980586931a816d61e4faf6c192b4abce89aa767ff6581e6ddc985ed003"},
+    {file = "argon2_cffi-20.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:05a8ac07c7026542377e38389638a8a1e9b78f1cd8439cd7493b39f08dd75fbf"},
+    {file = "argon2_cffi-20.1.0-cp27-cp27m-win32.whl", hash = "sha256:0bf066bc049332489bb2d75f69216416329d9dc65deee127152caeb16e5ce7d5"},
+    {file = "argon2_cffi-20.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:57358570592c46c420300ec94f2ff3b32cbccd10d38bdc12dc6979c4a8484fbc"},
+    {file = "argon2_cffi-20.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7d455c802727710e9dfa69b74ccaab04568386ca17b0ad36350b622cd34606fe"},
+    {file = "argon2_cffi-20.1.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:b160416adc0f012fb1f12588a5e6954889510f82f698e23ed4f4fa57f12a0647"},
+    {file = "argon2_cffi-20.1.0-cp35-cp35m-win32.whl", hash = "sha256:9bee3212ba4f560af397b6d7146848c32a800652301843df06b9e8f68f0f7361"},
+    {file = "argon2_cffi-20.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:392c3c2ef91d12da510cfb6f9bae52512a4552573a9e27600bdb800e05905d2b"},
+    {file = "argon2_cffi-20.1.0-cp36-cp36m-win32.whl", hash = "sha256:ba7209b608945b889457f949cc04c8e762bed4fe3fec88ae9a6b7765ae82e496"},
+    {file = "argon2_cffi-20.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:da7f0445b71db6d3a72462e04f36544b0de871289b0bc8a7cc87c0f5ec7079fa"},
+    {file = "argon2_cffi-20.1.0-cp37-abi3-macosx_10_6_intel.whl", hash = "sha256:cc0e028b209a5483b6846053d5fd7165f460a1f14774d79e632e75e7ae64b82b"},
+    {file = "argon2_cffi-20.1.0-cp37-cp37m-win32.whl", hash = "sha256:18dee20e25e4be86680b178b35ccfc5d495ebd5792cd00781548d50880fee5c5"},
+    {file = "argon2_cffi-20.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6678bb047373f52bcff02db8afab0d2a77d83bde61cfecea7c5c62e2335cb203"},
+    {file = "argon2_cffi-20.1.0-cp38-cp38-win32.whl", hash = "sha256:77e909cc756ef81d6abb60524d259d959bab384832f0c651ed7dcb6e5ccdbb78"},
+    {file = "argon2_cffi-20.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:9dfd5197852530294ecb5795c97a823839258dfd5eb9420233c7cfedec2058f2"},
+    {file = "argon2_cffi-20.1.0-cp39-cp39-win32.whl", hash = "sha256:e2db6e85c057c16d0bd3b4d2b04f270a7467c147381e8fd73cbbe5bc719832be"},
+    {file = "argon2_cffi-20.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8a84934bd818e14a17943de8099d41160da4a336bcc699bb4c394bbb9b94bd32"},
+]
 async-generator = [
     {file = "async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b"},
     {file = "async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"},
@@ -1333,8 +1455,8 @@ entrypoints = [
     {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"},
 ]
 flake8 = [
-    {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"},
-    {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"},
+    {file = "flake8-3.9.0-py2.py3-none-any.whl", hash = "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff"},
+    {file = "flake8-3.9.0.tar.gz", hash = "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0"},
 ]
 idna = [
     {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
@@ -1345,8 +1467,8 @@ imagesize = [
     {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
 ]
 importlib-metadata = [
-    {file = "importlib_metadata-3.7.2-py3-none-any.whl", hash = "sha256:407d13f55dc6f2a844e62325d18ad7019a436c4bfcaee34cda35f2be6e7c3e34"},
-    {file = "importlib_metadata-3.7.2.tar.gz", hash = "sha256:18d5ff601069f98d5d605b6a4b50c18a34811d655c55548adc833e687289acde"},
+    {file = "importlib_metadata-3.7.3-py3-none-any.whl", hash = "sha256:b74159469b464a99cb8cc3e21973e4d96e05d3024d337313fedb618a6e86e6f4"},
+    {file = "importlib_metadata-3.7.3.tar.gz", hash = "sha256:742add720a20d0467df2f444ae41704000f50e1234f46174b51f9c6031a1bd71"},
 ]
 iniconfig = [
     {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
@@ -1377,8 +1499,8 @@ jsonschema = [
     {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"},
 ]
 jupyter-client = [
-    {file = "jupyter_client-6.1.11-py3-none-any.whl", hash = "sha256:5eaaa41df449167ebba5e1cf6ca9b31f7fd4f71625069836e2e4fee07fe3cb13"},
-    {file = "jupyter_client-6.1.11.tar.gz", hash = "sha256:649ca3aca1e28f27d73ef15868a7c7f10d6e70f761514582accec3ca6bb13085"},
+    {file = "jupyter_client-6.1.12-py3-none-any.whl", hash = "sha256:e053a2c44b6fa597feebe2b3ecb5eea3e03d1d91cc94351a52931ee1426aecfc"},
+    {file = "jupyter_client-6.1.12.tar.gz", hash = "sha256:c4bca1d0846186ca8be97f4d2fa6d2bae889cce4892a167ffa1ba6bd1f73e782"},
 ]
 jupyter-core = [
     {file = "jupyter_core-4.7.1-py3-none-any.whl", hash = "sha256:8c6c0cac5c1b563622ad49321d5ec47017bd18b94facb381c6973a0486395f8e"},
@@ -1486,6 +1608,10 @@ nest-asyncio = [
     {file = "nest_asyncio-1.5.1-py3-none-any.whl", hash = "sha256:76d6e972265063fe92a90b9cc4fb82616e07d586b346ed9d2c89a4187acea39c"},
     {file = "nest_asyncio-1.5.1.tar.gz", hash = "sha256:afc5a1c515210a23c461932765691ad39e8eba6551c055ac8d5546e69250d0aa"},
 ]
+notebook = [
+    {file = "notebook-6.2.0-py3-none-any.whl", hash = "sha256:25ad93c982b623441b491e693ef400598d1a46cdf11b8c9c0b3be6c61ebbb6cd"},
+    {file = "notebook-6.2.0.tar.gz", hash = "sha256:0464b28e18e7a06cec37e6177546c2322739be07962dd13bf712bcb88361f013"},
+]
 numpy = [
     {file = "numpy-1.19.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc6bd4fd593cb261332568485e20a0712883cf631f6f5e8e86a52caa8b2b50ff"},
     {file = "numpy-1.19.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:aeb9ed923be74e659984e321f609b9ba54a48354bfd168d21a2b072ed1e833ea"},
@@ -1599,6 +1725,10 @@ pluggy = [
     {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
     {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
 ]
+prometheus-client = [
+    {file = "prometheus_client-0.9.0-py2.py3-none-any.whl", hash = "sha256:b08c34c328e1bf5961f0b4352668e6c8f145b4a087e09b7296ef62cbe4693d35"},
+    {file = "prometheus_client-0.9.0.tar.gz", hash = "sha256:9da7b32f02439d8c04f7777021c304ed51d9ec180604700c1ba72a4d44dceb03"},
+]
 prompt-toolkit = [
     {file = "prompt_toolkit-3.0.17-py3-none-any.whl", hash = "sha256:4cea7d09e46723885cb8bc54678175453e5071e9449821dce6f017b1d1fbfc1a"},
     {file = "prompt_toolkit-3.0.17.tar.gz", hash = "sha256:9397a7162cf45449147ad6042fa37983a081b8a73363a5253dd4072666333137"},
@@ -1612,16 +1742,40 @@ py = [
     {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
 ]
 pycodestyle = [
-    {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
-    {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},
+    {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
+    {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
 ]
 pycparser = [
     {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
     {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
 ]
+pydantic = [
+    {file = "pydantic-1.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0c40162796fc8d0aa744875b60e4dc36834db9f2a25dbf9ba9664b1915a23850"},
+    {file = "pydantic-1.8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fff29fe54ec419338c522b908154a2efabeee4f483e48990f87e189661f31ce3"},
+    {file = "pydantic-1.8.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:fbfb608febde1afd4743c6822c19060a8dbdd3eb30f98e36061ba4973308059e"},
+    {file = "pydantic-1.8.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:eb8ccf12295113ce0de38f80b25f736d62f0a8d87c6b88aca645f168f9c78771"},
+    {file = "pydantic-1.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:20d42f1be7c7acc352b3d09b0cf505a9fab9deb93125061b376fbe1f06a5459f"},
+    {file = "pydantic-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dde4ca368e82791de97c2ec019681ffb437728090c0ff0c3852708cf923e0c7d"},
+    {file = "pydantic-1.8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:3bbd023c981cbe26e6e21c8d2ce78485f85c2e77f7bab5ec15b7d2a1f491918f"},
+    {file = "pydantic-1.8.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:830ef1a148012b640186bf4d9789a206c56071ff38f2460a32ae67ca21880eb8"},
+    {file = "pydantic-1.8.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:fb77f7a7e111db1832ae3f8f44203691e15b1fa7e5a1cb9691d4e2659aee41c4"},
+    {file = "pydantic-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:3bcb9d7e1f9849a6bdbd027aabb3a06414abd6068cb3b21c49427956cce5038a"},
+    {file = "pydantic-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2287ebff0018eec3cc69b1d09d4b7cebf277726fa1bd96b45806283c1d808683"},
+    {file = "pydantic-1.8.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:4bbc47cf7925c86a345d03b07086696ed916c7663cb76aa409edaa54546e53e2"},
+    {file = "pydantic-1.8.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:6388ef4ef1435364c8cc9a8192238aed030595e873d8462447ccef2e17387125"},
+    {file = "pydantic-1.8.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:dd4888b300769ecec194ca8f2699415f5f7760365ddbe243d4fd6581485fa5f0"},
+    {file = "pydantic-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:8fbb677e4e89c8ab3d450df7b1d9caed23f254072e8597c33279460eeae59b99"},
+    {file = "pydantic-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2f2736d9a996b976cfdfe52455ad27462308c9d3d0ae21a2aa8b4cd1a78f47b9"},
+    {file = "pydantic-1.8.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3114d74329873af0a0e8004627f5389f3bb27f956b965ddd3e355fe984a1789c"},
+    {file = "pydantic-1.8.1-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:258576f2d997ee4573469633592e8b99aa13bda182fcc28e875f866016c8e07e"},
+    {file = "pydantic-1.8.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c17a0b35c854049e67c68b48d55e026c84f35593c66d69b278b8b49e2484346f"},
+    {file = "pydantic-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:e8bc082afef97c5fd3903d05c6f7bb3a6af9fc18631b4cc9fedeb4720efb0c58"},
+    {file = "pydantic-1.8.1-py3-none-any.whl", hash = "sha256:e3f8790c47ac42549dc8b045a67b0ca371c7f66e73040d0197ce6172b385e520"},
+    {file = "pydantic-1.8.1.tar.gz", hash = "sha256:26cf3cb2e68ec6c0cfcb6293e69fb3450c5fd1ace87f46b64f678b0d29eac4c3"},
+]
 pyflakes = [
-    {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},
-    {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
+    {file = "pyflakes-2.3.0-py2.py3-none-any.whl", hash = "sha256:910208209dcea632721cb58363d0f72913d9e8cf64dc6f8ae2e02a3609aba40d"},
+    {file = "pyflakes-2.3.0.tar.gz", hash = "sha256:e59fd8e750e588358f1b8885e5a4751203a0516e0ee6d34811089ac294c8806f"},
 ]
 pygments = [
     {file = "Pygments-2.8.1-py3-none-any.whl", hash = "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"},
@@ -1666,6 +1820,18 @@ pywin32 = [
     {file = "pywin32-300-cp39-cp39-win32.whl", hash = "sha256:638b68eea5cfc8def537e43e9554747f8dee786b090e47ead94bfdafdb0f2f50"},
     {file = "pywin32-300-cp39-cp39-win_amd64.whl", hash = "sha256:b1609ce9bd5c411b81f941b246d683d6508992093203d4eb7f278f4ed1085c3f"},
 ]
+pywinpty = [
+    {file = "pywinpty-0.5.7-cp27-cp27m-win32.whl", hash = "sha256:b358cb552c0f6baf790de375fab96524a0498c9df83489b8c23f7f08795e966b"},
+    {file = "pywinpty-0.5.7-cp27-cp27m-win_amd64.whl", hash = "sha256:1e525a4de05e72016a7af27836d512db67d06a015aeaf2fa0180f8e6a039b3c2"},
+    {file = "pywinpty-0.5.7-cp35-cp35m-win32.whl", hash = "sha256:2740eeeb59297593a0d3f762269b01d0285c1b829d6827445fcd348fb47f7e70"},
+    {file = "pywinpty-0.5.7-cp35-cp35m-win_amd64.whl", hash = "sha256:33df97f79843b2b8b8bc5c7aaf54adec08cc1bae94ee99dfb1a93c7a67704d95"},
+    {file = "pywinpty-0.5.7-cp36-cp36m-win32.whl", hash = "sha256:e854211df55d107f0edfda8a80b39dfc87015bef52a8fe6594eb379240d81df2"},
+    {file = "pywinpty-0.5.7-cp36-cp36m-win_amd64.whl", hash = "sha256:dbd838de92de1d4ebf0dce9d4d5e4fc38d0b7b1de837947a18b57a882f219139"},
+    {file = "pywinpty-0.5.7-cp37-cp37m-win32.whl", hash = "sha256:5fb2c6c6819491b216f78acc2c521b9df21e0f53b9a399d58a5c151a3c4e2a2d"},
+    {file = "pywinpty-0.5.7-cp37-cp37m-win_amd64.whl", hash = "sha256:dd22c8efacf600730abe4a46c1388355ce0d4ab75dc79b15d23a7bd87bf05b48"},
+    {file = "pywinpty-0.5.7-cp38-cp38-win_amd64.whl", hash = "sha256:8fc5019ff3efb4f13708bd3b5ad327589c1a554cb516d792527361525a7cb78c"},
+    {file = "pywinpty-0.5.7.tar.gz", hash = "sha256:2d7e9c881638a72ffdca3f5417dd1563b60f603e1b43e5895674c2a1b01f95a0"},
+]
 pyzmq = [
     {file = "pyzmq-22.0.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c0cde362075ee8f3d2b0353b283e203c2200243b5a15d5c5c03b78112a17e7d4"},
     {file = "pyzmq-22.0.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:ff1ea14075bbddd6f29bf6beb8a46d0db779bcec6b9820909584081ec119f8fd"},
@@ -1701,52 +1867,56 @@ pyzmq = [
     {file = "pyzmq-22.0.3.tar.gz", hash = "sha256:f7f63ce127980d40f3e6a5fdb87abf17ce1a7c2bd8bf2c7560e1bbce8ab1f92d"},
 ]
 regex = [
-    {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"},
-    {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"},
-    {file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"},
-    {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"},
-    {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"},
-    {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"},
-    {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"},
-    {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"},
-    {file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"},
-    {file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"},
-    {file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"},
-    {file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"},
-    {file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"},
-    {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"},
-    {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"},
-    {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"},
-    {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"},
-    {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"},
-    {file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"},
-    {file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"},
-    {file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"},
-    {file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"},
-    {file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"},
-    {file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"},
-    {file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"},
-    {file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"},
-    {file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"},
-    {file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"},
-    {file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"},
-    {file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"},
-    {file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"},
-    {file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"},
-    {file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"},
-    {file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"},
-    {file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"},
-    {file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"},
-    {file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"},
-    {file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"},
-    {file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"},
-    {file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"},
-    {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"},
+    {file = "regex-2021.3.17-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b97ec5d299c10d96617cc851b2e0f81ba5d9d6248413cd374ef7f3a8871ee4a6"},
+    {file = "regex-2021.3.17-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:cb4ee827857a5ad9b8ae34d3c8cc51151cb4a3fe082c12ec20ec73e63cc7c6f0"},
+    {file = "regex-2021.3.17-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:633497504e2a485a70a3268d4fc403fe3063a50a50eed1039083e9471ad0101c"},
+    {file = "regex-2021.3.17-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:a59a2ee329b3de764b21495d78c92ab00b4ea79acef0f7ae8c1067f773570afa"},
+    {file = "regex-2021.3.17-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f85d6f41e34f6a2d1607e312820971872944f1661a73d33e1e82d35ea3305e14"},
+    {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:4651f839dbde0816798e698626af6a2469eee6d9964824bb5386091255a1694f"},
+    {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:39c44532d0e4f1639a89e52355b949573e1e2c5116106a395642cbbae0ff9bcd"},
+    {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3d9a7e215e02bd7646a91fb8bcba30bc55fd42a719d6b35cf80e5bae31d9134e"},
+    {file = "regex-2021.3.17-cp36-cp36m-win32.whl", hash = "sha256:159fac1a4731409c830d32913f13f68346d6b8e39650ed5d704a9ce2f9ef9cb3"},
+    {file = "regex-2021.3.17-cp36-cp36m-win_amd64.whl", hash = "sha256:13f50969028e81765ed2a1c5fcfdc246c245cf8d47986d5172e82ab1a0c42ee5"},
+    {file = "regex-2021.3.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9d8d286c53fe0cbc6d20bf3d583cabcd1499d89034524e3b94c93a5ab85ca90"},
+    {file = "regex-2021.3.17-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:201e2619a77b21a7780580ab7b5ce43835e242d3e20fef50f66a8df0542e437f"},
+    {file = "regex-2021.3.17-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d47d359545b0ccad29d572ecd52c9da945de7cd6cf9c0cfcb0269f76d3555689"},
+    {file = "regex-2021.3.17-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ea2f41445852c660ba7c3ebf7d70b3779b20d9ca8ba54485a17740db49f46932"},
+    {file = "regex-2021.3.17-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:486a5f8e11e1f5bbfcad87f7c7745eb14796642323e7e1829a331f87a713daaa"},
+    {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:18e25e0afe1cf0f62781a150c1454b2113785401ba285c745acf10c8ca8917df"},
+    {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:a2ee026f4156789df8644d23ef423e6194fad0bc53575534101bb1de5d67e8ce"},
+    {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:4c0788010a93ace8a174d73e7c6c9d3e6e3b7ad99a453c8ee8c975ddd9965643"},
+    {file = "regex-2021.3.17-cp37-cp37m-win32.whl", hash = "sha256:575a832e09d237ae5fedb825a7a5bc6a116090dd57d6417d4f3b75121c73e3be"},
+    {file = "regex-2021.3.17-cp37-cp37m-win_amd64.whl", hash = "sha256:8e65e3e4c6feadf6770e2ad89ad3deb524bcb03d8dc679f381d0568c024e0deb"},
+    {file = "regex-2021.3.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a0df9a0ad2aad49ea3c7f65edd2ffb3d5c59589b85992a6006354f6fb109bb18"},
+    {file = "regex-2021.3.17-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b98bc9db003f1079caf07b610377ed1ac2e2c11acc2bea4892e28cc5b509d8d5"},
+    {file = "regex-2021.3.17-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:808404898e9a765e4058bf3d7607d0629000e0a14a6782ccbb089296b76fa8fe"},
+    {file = "regex-2021.3.17-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:5770a51180d85ea468234bc7987f5597803a4c3d7463e7323322fe4a1b181578"},
+    {file = "regex-2021.3.17-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:976a54d44fd043d958a69b18705a910a8376196c6b6ee5f2596ffc11bff4420d"},
+    {file = "regex-2021.3.17-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:63f3ca8451e5ff7133ffbec9eda641aeab2001be1a01878990f6c87e3c44b9d5"},
+    {file = "regex-2021.3.17-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bcd945175c29a672f13fce13a11893556cd440e37c1b643d6eeab1988c8b209c"},
+    {file = "regex-2021.3.17-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:3d9356add82cff75413bec360c1eca3e58db4a9f5dafa1f19650958a81e3249d"},
+    {file = "regex-2021.3.17-cp38-cp38-win32.whl", hash = "sha256:f5d0c921c99297354cecc5a416ee4280bd3f20fd81b9fb671ca6be71499c3fdf"},
+    {file = "regex-2021.3.17-cp38-cp38-win_amd64.whl", hash = "sha256:14de88eda0976020528efc92d0a1f8830e2fb0de2ae6005a6fc4e062553031fa"},
+    {file = "regex-2021.3.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4c2e364491406b7888c2ad4428245fc56c327e34a5dfe58fd40df272b3c3dab3"},
+    {file = "regex-2021.3.17-cp39-cp39-manylinux1_i686.whl", hash = "sha256:8bd4f91f3fb1c9b1380d6894bd5b4a519409135bec14c0c80151e58394a4e88a"},
+    {file = "regex-2021.3.17-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:882f53afe31ef0425b405a3f601c0009b44206ea7f55ee1c606aad3cc213a52c"},
+    {file = "regex-2021.3.17-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:07ef35301b4484bce843831e7039a84e19d8d33b3f8b2f9aab86c376813d0139"},
+    {file = "regex-2021.3.17-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:360a01b5fa2ad35b3113ae0c07fb544ad180603fa3b1f074f52d98c1096fa15e"},
+    {file = "regex-2021.3.17-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:709f65bb2fa9825f09892617d01246002097f8f9b6dde8d1bb4083cf554701ba"},
+    {file = "regex-2021.3.17-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:c66221e947d7207457f8b6f42b12f613b09efa9669f65a587a2a71f6a0e4d106"},
+    {file = "regex-2021.3.17-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c782da0e45aff131f0bed6e66fbcfa589ff2862fc719b83a88640daa01a5aff7"},
+    {file = "regex-2021.3.17-cp39-cp39-win32.whl", hash = "sha256:dc9963aacb7da5177e40874585d7407c0f93fb9d7518ec58b86e562f633f36cd"},
+    {file = "regex-2021.3.17-cp39-cp39-win_amd64.whl", hash = "sha256:a0d04128e005142260de3733591ddf476e4902c0c23c1af237d9acf3c96e1b38"},
+    {file = "regex-2021.3.17.tar.gz", hash = "sha256:4b8a1fb724904139149a43e172850f35aa6ea97fb0545244dc0b805e0154ed68"},
 ]
 requests = [
     {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
     {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
 ]
+send2trash = [
+    {file = "Send2Trash-1.5.0-py3-none-any.whl", hash = "sha256:f1691922577b6fa12821234aeb57599d887c4900b9ca537948d2dac34aea888b"},
+    {file = "Send2Trash-1.5.0.tar.gz", hash = "sha256:60001cc07d707fe247c94f74ca6ac0d3255aabcb930529690897ca2a39db28b2"},
+]
 six = [
     {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
     {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
@@ -1783,6 +1953,10 @@ sphinxcontrib-serializinghtml = [
     {file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"},
     {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"},
 ]
+terminado = [
+    {file = "terminado-0.9.3-py3-none-any.whl", hash = "sha256:430e876ec9d4d93a4fd8a49e82dcfae0c25f846540d0c5ca774b397533e237e8"},
+    {file = "terminado-0.9.3.tar.gz", hash = "sha256:261c0b7825fecf629666e1820b484a5380f7e54d6b8bd889fa482e99dcf9bde4"},
+]
 testpath = [
     {file = "testpath-0.4.4-py2.py3-none-any.whl", hash = "sha256:bfcf9411ef4bf3db7579063e0546938b1edda3d69f4e1fb8756991f5951f85d4"},
     {file = "testpath-0.4.4.tar.gz", hash = "sha256:60e0a3261c149755f4399a1fff7d37523179a70fdc3abdf78de9fc2604aeec7e"},
@@ -1880,8 +2054,8 @@ typing-extensions = [
     {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
 ]
 urllib3 = [
-    {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"},
-    {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"},
+    {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"},
+    {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"},
 ]
 wcwidth = [
     {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
diff --git a/pyproject.toml b/pyproject.toml
index a6d6e7a..51c178b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "blocklib"
-version = "0.1.7"
+version = "0.1.8-dev"
 description = "A library for blocking in record linkage"
 license = "Apache-2.0"
 authors = [
@@ -33,13 +33,15 @@ numpy = [
 ]
 metaphone = "^0.6"
 tqdm = "^4.36.1"
+pydantic = "^1.8"
+typing_extensions = "^3.7.4"
 
 # Optional dependencies, see extras section to toggle
 nbval = {version = "^0.9.6", optional = true}
 Sphinx = {version = "^3.5.2", optional = true}
 nbsphinx = {version = "^0.8.2", optional = true}
 pandas = {version = ">1.0", optional = true}
-ipython = {version = ">7.0", optional = true}
+notebook = {version = "^6.2.0", optional = true}
 
 [tool.poetry.dev-dependencies]
 pytest = "^6.2"
@@ -56,7 +58,7 @@ docs = [
     "sphinx",
     "nbsphinx",
     "pandas",
-    "ipython",
+    "notebook",
     "nbval"
 ]
 


Test patch:
diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml
index f83d6ed..0ec7226 100644
--- a/.github/workflows/python-test.yml
+++ b/.github/workflows/python-test.yml
@@ -3,15 +3,31 @@
 
 name: Python Unit Tests
 
-# Triggers the workflow on push or pull request events
+# Ref: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#on
+# Triggers the workflow on push pushed to any branch in a repository
 on: push
 
 jobs:
-  build:
-    runs-on: Ubuntu-20.04
+  pytest:
+    name: ${{ matrix.os }} / ${{ matrix.python-version }}
+    runs-on: ${{ matrix.os }}
+    # Ref: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idcontinue-on-error
+    continue-on-error: ${{ matrix.experimental }}
     strategy:
       matrix:
-        python-version: [3.9]
+        experimental: [false]
+        # Ref: https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json
+        os: [macos-latest, ubuntu-latest]
+        python-version:
+          - "3.7"
+          - "3.8"
+          - "3.9"
+# How to add an experimental job:
+#          - "3.10.0-alpha.6"
+#        include:
+#          - python-version: "3.10.0-alpha.6"
+#            os: ubuntu-latest
+#            experimental: true
 
     steps:
     - uses: actions/checkout@v2
@@ -32,7 +48,9 @@ jobs:
         echo "$HOME/.poetry/bin" >> $GITHUB_PATH
     - name: Configure poetry
       shell: bash
-      run: poetry config virtualenvs.in-project true
+      run: |
+        poetry --version
+        poetry config virtualenvs.in-project true
 
     - name: Set up cache
       uses: actions/cache@v2.1.4
@@ -43,7 +61,7 @@ jobs:
 
     - name: Install dependencies
       run: |
-        poetry install
+        poetry install --no-interaction
     - name: Test with pytest
       run: |
         poetry run pytest -q tests
diff --git a/pytest.ini b/pytest.ini
index df3eb51..e69de29 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,2 +0,0 @@
-[pytest]
-addopts = --doctest-modules
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..28dcbbf
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,24 @@
+import pytest
+
+
+@pytest.fixture(scope="module")
+def valid_psig_config():
+    yield {
+            "blocking-features": [1],
+            "record-id-col": 0,
+            "filter": {
+                "type": "ratio",
+                "max": 0.5,
+                "min": 0.2,
+            },
+            "blocking-filter": {
+                "type": "bloom filter",
+                "number-hash-functions": 20,
+                "bf-len": 2048,
+            },
+            "signatureSpecs": [
+                [
+                    {"type": "feature-value", "feature": 1}
+                ]
+            ]
+        }
\ No newline at end of file
diff --git a/tests/test_candidate_block_generator.py b/tests/test_candidate_block_generator.py
index 7572124..9d291cb 100644
--- a/tests/test_candidate_block_generator.py
+++ b/tests/test_candidate_block_generator.py
@@ -1,7 +1,7 @@
 import pytest
 import io
-from blocklib import generate_candidate_blocks
-from blocklib import PPRLIndexPSignature
+
+from blocklib import generate_candidate_blocks, validate_blocking_schema
 from blocklib import flip_bloom_filter
 
 data = [('id1', 'Joyce', 'Wang', 'Ashfield'),
@@ -23,7 +23,7 @@ def test_generate_candidate_blocks_assertion(self):
             generate_candidate_blocks(data, block_config)
 
         # test when type of blocking is not implemented
-        with pytest.raises(NotImplementedError):
+        with pytest.raises(ValueError):
             block_config = {'type': 'fancy-block', 'version': 1, 'config': {'blocking-features': [1, 2]}}
             generate_candidate_blocks(data, block_config)
 
@@ -33,12 +33,12 @@ def test_generate_candidate_blocks_assertion(self):
             generate_candidate_blocks(data, block_config)
 
         # test when blocking feature is a mix of integer and string
-        with pytest.raises(AssertionError):
+        with pytest.raises(ValueError):
             block_config = {'type': 'p-sig', 'version': 1, 'config': {'blocking-features': [1, 'name']}}
             generate_candidate_blocks(data, block_config)
 
         # test when blocking features are string but header is not given
-        with pytest.raises(AssertionError):
+        with pytest.raises(ValueError):
             block_config = {'type': 'p-sig', 'version': 1, 'config': {'blocking-features': ['name']}}
             generate_candidate_blocks(data, block_config)
 
@@ -70,6 +70,8 @@ def test_generate_candidate_blocks_psig(self):
         block_config = {'type': 'p-sig',
                         'version': 1,
                         'config': config}
+        print(validate_blocking_schema(block_config))
+
         candidate_block_obj = generate_candidate_blocks(data, block_config)
         bf_set_fred = str(tuple(flip_bloom_filter('0_Fred', bf_len, num_hash_funcs)))
         bf_set_lindsay = str(tuple(flip_bloom_filter('0_Lindsay', bf_len, num_hash_funcs)))
diff --git a/tests/test_pprlindex.py b/tests/test_pprlindex.py
index ea9ad2b..9287d1f 100644
--- a/tests/test_pprlindex.py
+++ b/tests/test_pprlindex.py
@@ -3,17 +3,18 @@
 import random
 
 
-def test_init():
+def test_init(valid_psig_config):
     """Test constructor for base class PPRLIndex."""
-    pprl = PPRLIndex()
+    pprl = PPRLIndex(config=valid_psig_config)
 
     assert pprl.rec_dict is None
     assert pprl.ent_id_col is None
     assert pprl.rec_id_col is None
 
 
-def test_summarize_reversed_index():
+def test_summarize_reversed_index(valid_psig_config):
     """Test summarize_reversed_index for base class PPRLIndex."""
+    pprl = PPRLIndex(config=valid_psig_config)
 
     reversed_index = {
         'Jo': ['id1', 'id2', 'id3'],
@@ -43,15 +44,15 @@ def test_select_reference_value():
         (4, 'Lindsay', 'Lin'),
         (5, 'Evelyn', 'Lai')
     ]
-    pprl = PPRLIndex()
-    ref_val_list = pprl.select_reference_value(reference_data, reference_config)
+
+    ref_val_list = PPRLIndex.select_reference_value(reference_data, reference_config)
     combined = [reference_data[i][1] + reference_data[i][2] for i in range(len(reference_data))]
     random.seed(0)
     expected = random.sample(combined, 3)
     assert ref_val_list == expected
 
 
-def test_get_feature_to_index_map():
-    pprl = PPRLIndex()
+def test_get_feature_to_index_map(valid_psig_config):
+    pprl = PPRLIndex(config=valid_psig_config)
     pprl.blocking_features = ['boo']
     assert pprl.get_feature_to_index_map([]) is None
diff --git a/tests/test_pprlpsig.py b/tests/test_pprlpsig.py
index 322afcb..85f5ae6 100644
--- a/tests/test_pprlpsig.py
+++ b/tests/test_pprlpsig.py
@@ -120,8 +120,11 @@ def test_build_reversed_index_feature_name(self):
 
         psig = PPRLIndexPSignature(config)
         reversed_index_result = psig.build_reversed_index(data, header=header)
-        bf_set = tuple(flip_bloom_filter("0_Fred", config['blocking-filter']['bf-len'],
-                                         config['blocking-filter']['number-hash-functions']))
+        bf_set = tuple(flip_bloom_filter(
+            "0_Fred",
+             config['blocking-filter']['bf-len'],
+             config['blocking-filter']['number-hash-functions']
+         ))
         assert reversed_index_result.reversed_index == {str(bf_set): ['id4', 'id5']}
 
         # test if results with column name and column index are the same
diff --git a/tests/test_signature_generator.py b/tests/test_signature_generator.py
index d4e1650..817fc91 100644
--- a/tests/test_signature_generator.py
+++ b/tests/test_signature_generator.py
@@ -1,5 +1,13 @@
+from typing import List
+
 import pytest
+from pydantic import ValidationError
+from pydantic.tools import parse_obj_as
+
 from blocklib import generate_signatures
+from blocklib.validation import PSigSignatureModel
+from blocklib.validation.psig_validation import PSigCharsAtSignatureSpec, PSigMetaphoneSignatureSpec, \
+    PSigFeatureValueSignatureSpec
 
 
 class TestPSig:
@@ -7,9 +15,14 @@ class TestPSig:
     def test_feature_value(self):
         """Test signatures generated by feature-value."""
         dtuple = ('Joyce', 'Wang', 2134)
-        signatures = [[{'type': 'feature-value', 'feature': 0},
-                      {'type': 'feature-value', 'feature': 1},
-                      ]]
+        signatures = [
+            parse_obj_as(PSigSignatureModel,
+                [
+                    {'type': 'feature-value', 'feature': 0},
+                    {'type': 'feature-value', 'feature': 1},
+                ]
+            )
+        ]
         signatures = generate_signatures(signatures, dtuple)
         assert signatures == ["0_Joyce_Wang"]
 
@@ -17,16 +30,22 @@ def test_char_at(self):
         """Test signatures generated by characters-at."""
         dtuple = ('Joyce', 'Wang', 2134)
         # test start_ind: end_ind
-        signatures = [[{'type': 'characters-at', 'feature': 0, 'config': {'pos': ["1:4"]}},
-                      {'type': 'characters-at', 'feature': 1, 'config': {'pos': ["1:4"]}}
-                      ]]
+        signatures = [
+            parse_obj_as(PSigSignatureModel, [
+                {'type': 'characters-at', 'feature': 0, 'config': {'pos': ["1:4"]}},
+                {'type': 'characters-at', 'feature': 1, 'config': {'pos': ["1:4"]}}
+            ])
+        ]
+
+        print(signatures)
+
         signatures = generate_signatures(signatures, dtuple)
         assert signatures == ["0_oyc_ang"]
 
         # test :end_ind
         strategy = [
             [
-                {'type': 'characters-at', 'feature': 0, 'config': {'pos': [':2', '-2:', 2, '2']}}
+                PSigCharsAtSignatureSpec(**{'type': 'characters-at', 'feature': 0, 'config': {'pos': [':2', '-2:', 2, '2']}})
             ]
         ]
         signature = generate_signatures(strategy, dtuple)
@@ -37,19 +56,19 @@ def test_char_at(self):
 
         invalid_strategy = [
             [
-                {'type': 'characters-at', 'feature': 0, 'config': {'pos': ['1-2', '-2:', 2, '2']}}
+                PSigCharsAtSignatureSpec(**{'type': 'characters-at', 'feature': 0, 'config': {'pos': ['1-2', '-2:', 2, '2']}})
             ]
         ]
         with pytest.raises(ValueError) as e:
             generate_signatures(invalid_strategy, dtuple)
             assert e == 'Invalid pos argument: 1-2'
 
-    def test_metaphonee(self):
+    def test_metaphone(self):
         """Test signatures generated by metaphone."""
         dtuple = ('Smith', 'Schmidt', 2134)
         signature_strategies = [
             [
-                {'type': 'metaphone', 'feature': 0}
+                PSigMetaphoneSignatureSpec(type='metaphone', feature=0)
             ]
         ]
         signatures = generate_signatures(signature_strategies, dtuple)
@@ -58,39 +77,41 @@ def test_metaphonee(self):
     def test_generate_signatures(self):
         """Test a multi-strategy signatures."""
         dtuple = ('Joyce', 'Wang', 2134)
-        signatures = [
+        signatures = parse_obj_as(
+            List[PSigSignatureModel],
             [
-                {'type': 'feature-value', 'feature': 0},
-                {'type': 'feature-value', 'feature': 1},
-            ],
-            [
-                {'type': 'metaphone', 'feature': 0},
-                {'type': 'metaphone', 'feature': 1},
+                [
+                    {'type': 'feature-value', 'feature': 0},
+                    {'type': 'feature-value', 'feature': 1},
+                ],
+                [
+                    {'type': 'metaphone', 'feature': 0},
+                    {'type': 'metaphone', 'feature': 1},
+                ]
             ]
-        ]
+        )
         signatures = generate_signatures(signatures, dtuple)
         assert signatures == ["0_Joyce_Wang", "1_JSAS_ANKFNK"]
 
     def test_invalid_signature_type(self):
-        dtuple = ('Joyce', 'Wang', 2134)
-        signatures = [
-            [
-                {'type': 'fuzzy matching'}
-            ]
-        ]
-        with pytest.raises(NotImplementedError) as e:
-            generate_signatures(signatures, dtuple)
-            assert e == 'Strategy fuzzy matching is not implemented yet!'
+        with pytest.raises(ValidationError) as e:
+            parse_obj_as(
+                PSigSignatureModel,
+                [
+                    {'type': 'fuzzy matching'}
+                ]
+            )
 
     def test_invalid_feature_to_index(self):
         dtuple = ('Joyce', 'Wang', 2134)
         signatures = [
             [
-                {'type': 'feature-value', 'feature': 'firstname'}
+                PSigFeatureValueSignatureSpec(type='feature-value', feature='firstname')
             ]
         ]
-        with pytest.raises(AssertionError):
+        with pytest.raises(AssertionError) as e:
             generate_signatures(signatures, dtuple)
+            assert e == 'Missing information to map from feature name to index'
 
         with pytest.raises(ValueError) as e:
             feature_to_index = {'name': 0}
diff --git a/tests/test_validation.py b/tests/test_validation.py
index 8b398ed..7f16d7b 100644
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -1,6 +1,6 @@
 import pytest
-from blocklib import validate_signature_config
-from blocklib.validation import load_schema
+from blocklib import validate_blocking_schema
+from blocklib.validation import load_schema, BlockingSchemaModel, BlockingSchemaTypes
 import tempfile
 import json
 
@@ -9,16 +9,59 @@ class TestValidation:
 
     def test_validate_signature_config(self):
         """Test validation on signature config."""
-        config = {"version": 1,
-                  "type": "p-sig",
-                  "config": {}}
-        validate_signature_config(config)
+        config = {
+            "type": "p-sig",
+            "version": 1,
+            "config": {
+                "blocking-features": [1],
+                "filter": {
+                    "type": "ratio",
+                    "max": 0.02,
+                    "min": 0.00,
+                },
+                "blocking-filter": {
+                    "type": "bloom filter",
+                    "number-hash-functions": 4,
+                    "bf-len": 2048,
+                },
+                "signatureSpecs": [
+                    [
+                        {"type": "characters-at", "config": {"pos": [0]}, "feature": 1},
+                        {"type": "characters-at", "config": {"pos": [0]}, "feature": 2},
+                    ],
+                    [
+                        {"type": "metaphone", "feature": 1},
+                        {"type": "metaphone", "feature": 2},
+                    ]
+                ]
+            }
+        }
+
+        validated_schema = validate_blocking_schema(config)
+        assert isinstance(validated_schema, BlockingSchemaModel)
+
+        # Check we can serialize the object
+        validated_json_data = validated_schema.dict()
+        # Note we use Python keys with underscores, rather than hyphens:
+        assert 'blocking_features' in validated_json_data['config']
+        assert 'blocking-features' not in validated_json_data['config']
+
+        # Note the type values are enums:
+        assert isinstance(validated_schema.type, BlockingSchemaTypes)
+        assert validated_schema.type == BlockingSchemaTypes.psig
+
+        # To get the original JSON keys use `by_alias=True`
+        assert 'blocking-features' in validated_schema.config.dict(by_alias=True)
+
+        # We can also export as json
+        json_str = validated_schema.json(indent=2, by_alias=True)
+        assert 'blocking-features' in json_str
 
     def test_validate_signature_assertion(self):
         """Test if validation will capture error and throw assertion."""
         config = {"type": "p-sig", "version": 1}
         with pytest.raises(ValueError):
-            validate_signature_config(config)
+            validate_blocking_schema(config)
 
     def test_load_schema(self):
         """Test exception with invalid schema."""