Coverage for src / quber / playground / metadata.py: 99%

75 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-09-23 22:14 -0400

1"""Filing metadata: what a document is, derived and displayed. 

2 

3A document carries filing_type, year, period and version as columns. Nothing 

4displayable is stored beyond them: the label a card, the workspace header or 

5an export filename shows is derived here at read time, so grooming the 

6metadata corrects every label at once. 

7 

8`infer` pre-fills the metadata from a filename. It is a convenience and never 

9authoritative — the upload form shows what it guessed and any field the user 

10touches is pinned. The same rules live in `ui/src/metadata.js` for the form; 

11`tests/playground/test_metadata.py` runs one case list through both so the 

12two implementations cannot drift apart silently. 

13 

14The identity tuple (folder, filing_type, year, period, version) is what the 

15overwrite flow keys on. It only exists when filing_type, year and period are 

16all present, and it compares case-insensitively; `tuple_key` is that rule in 

17one place. `overlap_key` is the same tuple without the version — two versions 

18of one filing share it, which is exactly what the library's possible-duplicate 

19flag reports. 

20""" 

21 

22from __future__ import annotations 

23 

24import re 

25from typing import Any, Dict, Iterable, Optional, Tuple 

26 

27#: The normalized identity tuple: (folder, filing_type, year, period, version). 

28TupleKey = Tuple[str, str, int, str, Optional[int]] 

29 

30PERIODS = ("Q1", "Q2", "Q3", "Q4", "H1", "H2", "FY") 

31 

32# (pattern, groups) pairs tried in order; the first hit decides the period. 

33# Compact quarter-year compounds come first so "q126" reads as Q1 2026 rather 

34# than as a bare Q1 with the year lost. 

35_PERIOD_PATTERNS: list[tuple[re.Pattern[str], str]] = [ 

36 (re.compile(r"(?<!\d)([1-4])\s*q\s*(20\d{2})(?!\d)", re.I), "q-year"), 

37 (re.compile(r"(?<!\d)(20\d{2})[-_. ]?q([1-4])(?!\d)", re.I), "year-q"), 

38 (re.compile(r"(?<![a-z0-9])q([1-4])[-_. ]?(\d{2})(?!\d)", re.I), "q-yy"), 

39 (re.compile(r"(?<![a-z0-9])q([1-4])(?![0-9])", re.I), "q"), 

40 (re.compile(r"(?<![a-z0-9])h([12])(?![0-9])", re.I), "h"), 

41 (re.compile(r"(first|second)[-_ ]half", re.I), "half"), 

42 (re.compile(r"(?<![a-z0-9])(fy|full[-_ ]?year|annual)(?![a-z0-9])", re.I), "fy"), 

43] 

44 

45_YEAR_RE = re.compile(r"(?<!\d)(20\d{2})(?!\d)") 

46 

47# Explicit form tokens first, then the press-release word, then the deck 

48# vocabulary the design names for 99-2. 

49_TYPE_PATTERNS: list[tuple[re.Pattern[str], str]] = [ 

50 (re.compile(r"(?<![a-z0-9])10[-_ ]?k(?![a-z0-9])", re.I), "10-K"), 

51 (re.compile(r"(?<![a-z0-9])10[-_ ]?q(?![a-z0-9])", re.I), "10-Q"), 

52 (re.compile(r"(?<![a-z0-9])99[-_. ]?1(?![0-9])", re.I), "99-1"), 

53 (re.compile(r"(?<![a-z0-9])99[-_. ]?2(?![0-9])", re.I), "99-2"), 

54 (re.compile(r"press[-_ ]?release", re.I), "99-1"), 

55 (re.compile(r"supplemental|presentation|deck|slides|earnings", re.I), "99-2"), 

56] 

57 

58 

59def infer(filename: str) -> Dict[str, Any]: 

60 """Filing metadata guessed from a filename: {filing_type, year, period}, 

61 each None when nothing in the name says. A 10-K with no explicit period 

62 defaults to FY — an annual report is annual.""" 

63 stem = filename.rsplit("/", 1)[-1] 

64 stem = re.sub(r"\.[^.]+$", "", stem) 

65 

66 filing_type = next((label for rx, label in _TYPE_PATTERNS if rx.search(stem)), None) 

67 

68 period: Optional[str] = None 

69 year: Optional[int] = None 

70 for rx, kind in _PERIOD_PATTERNS: 

71 m = rx.search(stem) 

72 if not m: 

73 continue 

74 if kind == "q-year": 

75 period, year = f"Q{m.group(1)}", int(m.group(2)) 

76 elif kind == "year-q": 

77 period, year = f"Q{m.group(2)}", int(m.group(1)) 

78 elif kind == "q-yy": 

79 period, year = f"Q{m.group(1)}", 2000 + int(m.group(2)) 

80 elif kind == "q": 

81 period = f"Q{m.group(1)}" 

82 elif kind == "h": 

83 period = f"H{m.group(1)}" 

84 elif kind == "half": 

85 period = "H1" if m.group(1).lower() == "first" else "H2" 

86 else: 

87 period = "FY" 

88 break 

89 

90 if year is None: 

91 m = _YEAR_RE.search(stem) 

92 if m: 

93 year = int(m.group(1)) 

94 if period is None and filing_type == "10-K": 

95 period = "FY" 

96 return {"filing_type": filing_type, "year": year, "period": period} 

97 

98 

99def filing_label( 

100 filing_type: Optional[str], 

101 year: Optional[int], 

102 period: Optional[str], 

103 version: Optional[int], 

104) -> str: 

105 """The display label: `99-2 · Q1 2026 · v2` with missing parts omitted. 

106 Empty when the metadata is entirely absent — the title then stands alone.""" 

107 parts: list[str] = [] 

108 if filing_type: 

109 parts.append(filing_type) 

110 py = " ".join(str(p) for p in (period, year) if p) 

111 if py: 

112 parts.append(py) 

113 if version: 

114 parts.append(f"v{version}") 

115 return " · ".join(parts) 

116 

117 

118def export_stem(*candidates: Optional[str]) -> str: 

119 """A filename-safe stem from the first non-empty candidate — label, then 

120 title, then original filename, per the label precedence.""" 

121 for c in candidates: 

122 if c and c.strip(): 

123 stem = re.sub(r"[^a-z0-9]+", "-", c.lower()).strip("-")[:60] 

124 if stem: 

125 return stem 

126 return "document" 

127 

128 

129def _norm(s: Optional[str]) -> str: 

130 return (s or "").strip().lower() 

131 

132 

133def tuple_key( 

134 folder: Optional[str], 

135 filing_type: Optional[str], 

136 year: Optional[int], 

137 period: Optional[str], 

138 version: Optional[int], 

139) -> Optional[TupleKey]: 

140 """The identity tuple, normalized for comparison — or None when it does 

141 not exist because filing_type, year or period is missing. Documents 

142 without a tuple never collide and never replace.""" 

143 if not (filing_type and year and period): 

144 return None 

145 return (_norm(folder), _norm(filing_type), int(year), _norm(period), version) 

146 

147 

148def overlap_key( 

149 folder: Optional[str], 

150 filing_type: Optional[str], 

151 year: Optional[int], 

152 period: Optional[str], 

153) -> Optional[Tuple[str, str, int, str]]: 

154 """The tuple without its version: what v1/v2 siblings share, and what the 

155 possible-duplicate flag groups by.""" 

156 key = tuple_key(folder, filing_type, year, period, version=None) 

157 return key[:4] if key else None 

158 

159 

160def annotate_overlaps(docs: Iterable[Dict[str, Any]]) -> None: 

161 """Set `overlap` on each document dict: True when another document shares 

162 its versionless tuple. Documents with incomplete metadata never flag.""" 

163 rows = list(docs) 

164 counts: Dict[Tuple[str, str, int, str], int] = {} 

165 for d in rows: 

166 key = overlap_key(d.get("folder"), d.get("filing_type"), d.get("year"), d.get("period")) 

167 if key is not None: 

168 counts[key] = counts.get(key, 0) + 1 

169 for d in rows: 

170 key = overlap_key(d.get("folder"), d.get("filing_type"), d.get("year"), d.get("period")) 

171 d["overlap"] = bool(key is not None and counts[key] > 1)