Coverage for ramose/filters.py: 100%
63 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-24 21:59 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-24 21:59 +0000
1# SPDX-FileCopyrightText: 2026 Arcangelo Massari <arcangelo.massari@unibo.it>
2#
3# SPDX-License-Identifier: ISC
5from __future__ import annotations
7from collections.abc import Mapping
8from pathlib import Path
9from re import Match, findall, sub
11import yaml
13from ramose.escaping import escape_iri, escape_literal, escape_local_name
15FiltersConfig = Mapping[str, Mapping[str, "str | dict[str, str]"]]
17_VALUE_PATTERN = r"\{\{\s*value\s*\}\}"
18_QLEVER_HAS_WORD = ("ql:has-word", "<http://qlever.cs.uni-freiburg.de/builtin-functions/has-word>")
19_ALWAYS_EMPTY_FILTER = "FILTER(false)"
22def render(template: str, value: str) -> str:
23 def bind(placeholder: Match[str]) -> str:
24 before = template[placeholder.start() - 1 : placeholder.start()]
25 after = template[placeholder.end() : placeholder.end() + 1]
26 if before in ('"', "'") and after == before:
27 return escape_literal(value)
28 if before == "<" and after == ">":
29 return escape_iri(value)
30 return escape_local_name(value)
32 return sub(_VALUE_PATTERN, bind, template)
35def _select_template(key: str, spec: str | dict[str, str], value: str) -> str:
36 if isinstance(spec, str):
37 return spec
38 if value not in spec:
39 msg = f"The value '{value}' is not valid for filter '{key}', valid values are {', '.join(sorted(spec))}"
40 raise ValueError(msg)
41 return spec[value]
44def _render_fragments(template: str, value: str) -> list[str]:
45 if not any(predicate in template for predicate in _QLEVER_HAS_WORD):
46 return [render(template, value)]
47 words = findall(r"[^\W_]+", value.lower())
48 if not words:
49 return [_ALWAYS_EMPTY_FILTER]
50 return [render(template, word) for word in words]
53def _is_always_empty(fragment: str) -> bool:
54 return fragment.strip().upper() == _ALWAYS_EMPTY_FILTER.upper()
57def apply_filters(config: FiltersConfig, values: list[str]) -> dict[str, str]:
58 slots: list[str] = []
59 for slot_map in config.values():
60 for slot in slot_map:
61 if slot not in slots:
62 slots.append(slot)
63 result = dict.fromkeys(slots, "")
64 fragments: list[tuple[str, str]] = []
66 raw_pairs = (pair.strip() for value in values for pair in value.split(","))
67 for pair in (pair for pair in raw_pairs if pair):
68 key, value = pair.split(":", 1)
69 if key not in config:
70 msg = f"The filter '{key}' is not configured, configured filters are {', '.join(sorted(config))}"
71 raise ValueError(msg)
72 for slot, spec in config[key].items():
73 template = _select_template(key, spec, value)
74 fragments.extend((slot, fragment) for fragment in _render_fragments(template, value))
76 always_empty_slots = {slot for slot, fragment in fragments if _is_always_empty(fragment)}
77 if always_empty_slots:
78 for slot in always_empty_slots:
79 result[slot] = _ALWAYS_EMPTY_FILTER
80 return result
82 for slot, fragment in fragments:
83 result[slot] = f"{result[slot]}\n{fragment}" if result[slot] else fragment
84 return result
87def load_yaml_config(path: str) -> FiltersConfig:
88 return yaml.safe_load(Path(path).read_text(encoding="utf-8"))