1 class ConfigValidator: 2 VALID_STRATEGY_TYPES = {"random", "stratified", "stratified-proportional"} 3 4 def __init__(self, config: dict): 5 self.config = config 6 7 def validate(self): 8 self.validate_customer_name() 9 self.validate_data_sources() 10 self.validate_filters() 11 self.validate_sampling_strategy() 12 self.validate_output() 13 self.validate_alerting() 14 15 def validate_customer_name(self): 16 if "customer_name" not in self.config: 17 raise ValueError("Missing 'customer_name' in config.") 18 if not isinstance(self.config["customer_name"], str): 19 raise ValueError("'customer_name' must be a string.") 20 21 def validate_data_sources(self): 22 if "data_sources" not in self.config: 23 raise ValueError("Missing 'data_sources' in config.") 24 25 for source in ["matches", "catalog"]: 26 if source not in self.config["data_sources"]: 27 raise ValueError(f"Missing '{source}' in data_sources.") 28 29 source_conf = self.config["data_sources"][source] 30 if "sql_file" not in source_conf: 31 raise ValueError(f"Missing 'sql_file' in {source} data source.") 32 if "connection" not in source_conf: 33 raise ValueError(f"Missing 'connection' details in {source} data source.") 34 if "secret_name" not in source_conf["connection"]: 35 raise ValueError(f"Missing 'secret_name' in {source} connection config.") 36 37 def validate_filters(self): 38 filters = self.config.get("filters", {}) 39 if not isinstance(filters, dict): 40 raise ValueError("'filters' section must be a dictionary if provided.") 41 42 def validate_sampling_strategy(self): 43 if "sampling_strategy" not in self.config: 44 raise ValueError("Missing 'sampling_strategy' in config.") 45 46 strategy = self.config["sampling_strategy"] 47 if "type" not in strategy: 48 raise ValueError("sampling_strategy must include a 'type' field.") 49 if strategy["type"] not in self.VALID_STRATEGY_TYPES: 50 raise ValueError(f"sampling_strategy.type must be one of {self.VALID_STRATEGY_TYPES}") 51 52 # Strategy-specific checks 53 if strategy["type"] == "random": 54 if "sample_size" not in strategy: 55 raise ValueError("random sampling_strategy must have 'sample_size' defined.") 56 57 elif strategy["type"] == "stratified": 58 if "by" not in strategy: 59 raise ValueError("stratified sampling_strategy must have 'by' field.") 60 # 'ratio' is optional — default to 10/group in Sampler 61 if "ratio" in strategy: 62 if not isinstance(strategy["ratio"], (dict, int)): 63 raise ValueError("'ratio' must be an int or dict in stratified strategy.") 64 65 elif strategy["type"] == "stratified-proportional": 66 if "by" not in strategy: 67 raise ValueError("stratified-proportional sampling_strategy must have 'by' field.") 68 # 'proportion' is optional — default to equal weight in Sampler 69 if "proportion" in strategy: 70 if not isinstance(strategy["proportion"], (dict, float, int)): 71 raise ValueError("'proportion' must be a float or dict in stratified-proportional strategy.") 72 73 def validate_output(self): 74 if "output" not in self.config: 75 raise ValueError("Missing 'output' section in config.") 76 77 output = self.config["output"] 78 if "bucket" not in output: 79 raise ValueError("output must include 'bucket'.") 80 if "prefix" not in output: 81 raise ValueError("output must include 'prefix'.") 82 if "file_format" not in output: 83 raise ValueError("output must include 'file_format' (csv or parquet).") 84 85 def validate_alerting(self): 86 alerting = self.config.get("alerting", {}) 87 if alerting and not isinstance(alerting, dict): 88 raise ValueError("'alerting' must be a dictionary if provided.") 89 90 if alerting: 91 if "slack_webhook_url" not in alerting and "email_list" not in alerting: 92 raise ValueError("alerting must have either 'slack_webhook_url' or 'email_list'.") 93