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

1import re 

2 

3_TOKEN_RE = re.compile(r"^(\w+)((?:\[\d+\])*)$") 

4_INDEX_RE = re.compile(r"\[(\d+)\]") 

5 

6 

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) 

27 

28 

29_ATTR_RE = re.compile(r"^(.*)::attr\(([^)]+)\)$") 

30 

31 

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() 

40 

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)