Coverage for product_risk_suite/scraper/pathutils.py: 89%
35 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-16 14:44 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-16 14:44 +0000
1import re
3_TOKEN_RE = re.compile(r"^(\w+)((?:\[\d+\])*)$")
4_INDEX_RE = re.compile(r"\[(\d+)\]")
7def resolve_json_path(data, path):
8 """Resolve a dotted/indexed path like 'fields[0].assignee.name' against nested
9 dict/list data. Returns None (rather than raising) if the path doesn't match."""
10 value = data
11 for token in path.split("."):
12 match = _TOKEN_RE.match(token)
13 if not match:
14 return None
15 key, index_part = match.groups()
16 if not isinstance(value, dict) or key not in value:
17 return None
18 value = value[key]
19 for index in _INDEX_RE.findall(index_part):
20 try:
21 value = value[int(index)]
22 except (IndexError, TypeError):
23 return None
24 if value is None:
25 return None
26 return str(value)
29_ATTR_RE = re.compile(r"^(.*)::attr\(([^)]+)\)$")
32def resolve_html_value(soup, selector):
33 """Resolve a CSS selector against a BeautifulSoup document. A selector suffixed
34 with '::attr(name)' reads that attribute instead of the element's text."""
35 match = _ATTR_RE.match(selector.strip())
36 attr_name = None
37 css_selector = selector.strip()
38 if match:
39 css_selector, attr_name = match.group(1).strip(), match.group(2).strip()
41 element = soup.select_one(css_selector)
42 if element is None:
43 return None
44 if attr_name:
45 value = element.get(attr_name)
46 return str(value) if value is not None else None
47 return element.get_text(strip=True)