|
| 1 | +from pathlib import Path |
| 2 | +from structlog.stdlib import BoundLogger |
| 3 | + |
| 4 | +from bento_lib._internal import internal_logger |
| 5 | +from .exceptions import DiscoveryValidationError |
| 6 | +from .models.config import DiscoveryConfig |
| 7 | + |
| 8 | +__all__ = [ |
| 9 | + "load_discovery_config", |
| 10 | +] |
| 11 | + |
| 12 | + |
| 13 | +FIELD_DEF_NOT_FOUND = "field definition not found" |
| 14 | +FIELD_ALREADY_SEEN = "field already seen" |
| 15 | + |
| 16 | + |
| 17 | +def _load_discovery_config_json(config_path: Path | str) -> DiscoveryConfig: |
| 18 | + with open(config_path, "r") as fh: |
| 19 | + return DiscoveryConfig.model_validate_json(fh.read()) |
| 20 | + |
| 21 | + |
| 22 | +def _validate_references_and_duplicates(cfg: DiscoveryConfig, logger: BoundLogger) -> None: |
| 23 | + fields = cfg.fields |
| 24 | + |
| 25 | + # validate overview and check for chart duplicates: |
| 26 | + seen_chart_fields: set[str] = set() |
| 27 | + for s_idx, section in enumerate(cfg.overview): |
| 28 | + for c_idx, chart in enumerate(section.charts): |
| 29 | + exc_path = ( |
| 30 | + f"overview > section {section.section_title} [{s_idx}] > {chart.field} {chart.chart_type} [{c_idx}]" |
| 31 | + ) |
| 32 | + if chart.field not in fields: |
| 33 | + logger.error( |
| 34 | + f"overview {FIELD_DEF_NOT_FOUND}", section=section.section_title, field=chart.field, chart_idx=c_idx |
| 35 | + ) |
| 36 | + raise DiscoveryValidationError(f"{exc_path}: {FIELD_DEF_NOT_FOUND}") |
| 37 | + if chart.field in seen_chart_fields: |
| 38 | + logger.error( |
| 39 | + f"overview {FIELD_ALREADY_SEEN}", section=section.section_title, field=chart.field, chart_idx=c_idx |
| 40 | + ) |
| 41 | + raise DiscoveryValidationError(f"{exc_path}: {FIELD_ALREADY_SEEN}") |
| 42 | + seen_chart_fields.add(chart.field) |
| 43 | + |
| 44 | + # validate search: |
| 45 | + seen_search_fields: set[str] = set() |
| 46 | + for s_idx, section in enumerate(cfg.search): |
| 47 | + for f_idx, f in enumerate(section.fields): |
| 48 | + exc_path = f"search > section {section.section_title} [{s_idx}] > {f} [{f_idx}]" |
| 49 | + if f not in fields: |
| 50 | + logger.error(f"search {FIELD_DEF_NOT_FOUND}", section=section.section_title, field=f) |
| 51 | + raise DiscoveryValidationError(f"{exc_path}: {FIELD_DEF_NOT_FOUND}") |
| 52 | + if f in seen_search_fields: |
| 53 | + logger.error(f"search {FIELD_ALREADY_SEEN}", section=section.section_title, field=f) |
| 54 | + raise DiscoveryValidationError(f"{exc_path}: {FIELD_ALREADY_SEEN}") |
| 55 | + seen_search_fields.add(f) |
| 56 | + |
| 57 | + # issue warnings if there are fields defined that the config doesn't reference: |
| 58 | + referenced_fields = seen_chart_fields | seen_search_fields |
| 59 | + for fi, f in enumerate(fields.keys()): |
| 60 | + if f not in referenced_fields: |
| 61 | + logger.warning("field not referenced", field=f, field_idx=fi) |
| 62 | + |
| 63 | + |
| 64 | +def load_discovery_config(config_path: Path | str, logger: BoundLogger | None = None) -> DiscoveryConfig: |
| 65 | + # 1. load the config object (or raise a Pydantic validation error if the config is in the wrong format) |
| 66 | + cfg = _load_discovery_config_json(config_path) |
| 67 | + |
| 68 | + # 2. validate the config's internal references and overview chart/search field entries |
| 69 | + # a) make sure all fields in overview and search are defined |
| 70 | + # b) make sure fields are not listed more than once as a chart or as a search filter |
| 71 | + # c) issue warnings if any fields are defined that the config doesn't reference anywhere |
| 72 | + _validate_references_and_duplicates(cfg, logger or internal_logger) |
| 73 | + |
| 74 | + # now that we've validated references, return the config |
| 75 | + return cfg |
0 commit comments