from __future__ import annotations import fnmatch import html import json import os import re from functools import lru_cache from html.parser import HTMLParser from pathlib import Path from urllib.parse import parse_qs, urlparse _MAX_READ_CHARS = 4000 _MAX_GREP_MATCHES = 20 _MAX_GLOB_MATCHES = 50 _MAX_ORACLE_PAGE_CHARS = 24000 _MAX_ORACLE_CONTEXT_CHARS = 80000 def _normalize_data_dirs(data_dirs: list[str] | tuple[str, ...] | str | None, project_root: Path) -> list[str]: if data_dirs is None: return [] if isinstance(data_dirs, str): items = [part.strip() for chunk in data_dirs.split(os.pathsep) for part in chunk.split(",")] else: items = [str(item).strip() for item in data_dirs] resolved: list[str] = [] for item in items: if not item: continue path = Path(item).expanduser() if not path.is_absolute(): path = project_root / path resolved.append(str(path)) return resolved def resolve_docs_roots(data_dirs: list[str] | tuple[str, ...] | str | None = None) -> list[str]: project_root = Path(__file__).resolve().parents[3] env_value = os.environ.get("OFFICEQA_DOCS_DIR", "").strip() candidates = _normalize_data_dirs(data_dirs, project_root) candidates.extend(_normalize_data_dirs(env_value, project_root)) candidates.extend([ str(project_root / "data" / "officeqa_docs_official"), str(project_root / "data" / "officeqa_smoke_docs"), os.path.expanduser("~/officeqa-sparse/treasury_bulletins_parsed"), os.path.expanduser("~/officeqa/treasury_bulletins_parsed"), ]) roots: list[str] = [] seen: set[str] = set() for candidate in candidates: path = Path(candidate).expanduser() if not path.is_dir(): continue transformed = path / "transformed" resolved = str((transformed if transformed.is_dir() else path).resolve()) if resolved in seen: continue seen.add(resolved) roots.append(resolved) if not roots: raise FileNotFoundError("OfficeQA docs directory not found. Set OFFICEQA_DOCS_DIR or env.data_dirs.") return roots def _is_allowed(path: str, allowed_roots: list[str], allowed_files: list[str]) -> bool: try: resolved = str(Path(path).resolve()) except FileNotFoundError: return False if not any(resolved.startswith(root + os.sep) or resolved == root for root in allowed_roots): return False if not allowed_files: return True base = os.path.basename(resolved) return base in allowed_files def resolve_candidate_files(source_files: list[str], allowed_roots: list[str]) -> list[str]: resolved: list[str] = [] seen: set[str] = set() for root in allowed_roots: for dirpath, _, filenames in os.walk(root): for filename in filenames: if source_files and filename not in source_files: continue full = str(Path(dirpath, filename).resolve()) if full in seen: continue seen.add(full) resolved.append(full) return resolved def _as_list(value: object) -> list[str]: if value is None: return [] if isinstance(value, list): return [str(item).strip() for item in value if str(item).strip()] text = str(value).strip() if not text: return [] try: loaded = json.loads(text) except json.JSONDecodeError: loaded = None if isinstance(loaded, list): return [str(item).strip() for item in loaded if str(item).strip()] if "\n" in text: return [part.strip() for part in text.splitlines() if part.strip()] return [text] def _extract_page_number(source_doc: str) -> int | None: text = str(source_doc or "").strip() if not text: return None parsed = urlparse(text) query = parse_qs(parsed.query) for key in ("page", "pagenum", "page_id"): for raw_value in query.get(key, []): try: return int(str(raw_value).strip()) except ValueError: continue match = re.search(r"(?:[?&]|^)page=(\d+)", text) if match: return int(match.group(1)) return None def _iter_oracle_refs(source_files: object, source_docs: object) -> list[tuple[str, int, str]]: files = _as_list(source_files) docs = _as_list(source_docs) refs: list[tuple[str, int, str]] = [] seen: set[tuple[str, int, str]] = set() if not files or not docs: return refs for index, source_doc in enumerate(docs): page_number = _extract_page_number(source_doc) if page_number is None: continue if index < len(files): source_file = files[index] elif len(files) == 1: source_file = files[0] else: continue key = (source_file, page_number, source_doc) if key in seen: continue seen.add(key) refs.append(key) return refs def _parsed_root_candidates(docs_roots: list[str]) -> list[Path]: candidates: list[Path] = [] seen: set[str] = set() for root in docs_roots: path = Path(root).expanduser() for candidate in ( path, path.parent, path / "treasury_bulletins_parsed", path.parent / "treasury_bulletins_parsed", ): resolved = str(candidate.resolve()) if candidate.exists() else str(candidate) if resolved in seen: continue seen.add(resolved) candidates.append(candidate) return candidates def _locate_parsed_json(source_file: str, docs_roots: list[str]) -> Path | None: source_path = Path(str(source_file).strip()) stem = source_path.stem if source_path.suffix else source_path.name if not stem: return None candidate_names = [stem + ".json"] if source_path.suffix == ".json": candidate_names.insert(0, source_path.name) for root in _parsed_root_candidates(docs_roots): for name in candidate_names: path = root / "jsons" / name if path.is_file(): return path return None class _TableMarkdownParser(HTMLParser): def __init__(self) -> None: super().__init__(convert_charrefs=True) self.rows: list[list[str]] = [] self._row: list[str] | None = None self._cell: list[str] | None = None def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: if tag.lower() == "tr": self._row = [] elif tag.lower() in {"td", "th"} and self._row is not None: self._cell = [] def handle_data(self, data: str) -> None: if self._cell is not None: self._cell.append(data) def handle_endtag(self, tag: str) -> None: normalized_tag = tag.lower() if normalized_tag in {"td", "th"} and self._cell is not None and self._row is not None: cell = re.sub(r"\s+", " ", "".join(self._cell)).strip() self._row.append(cell) self._cell = None elif normalized_tag == "tr" and self._row is not None: if any(cell for cell in self._row): self.rows.append(self._row) self._row = None self._cell = None def _escape_markdown_cell(value: str) -> str: return str(value).replace("\n", " ").replace("|", "\\|").strip() def _html_table_to_markdown(raw_html: str) -> str: parser = _TableMarkdownParser() try: parser.feed(raw_html) except Exception: # noqa: BLE001 parser.rows = [] rows = parser.rows if not rows: text = re.sub(r"(?is)<[^>]+>", " ", raw_html) return re.sub(r"\s+", " ", html.unescape(text)).strip() width = max(len(row) for row in rows) normalized_rows = [row + [""] * (width - len(row)) for row in rows] header = normalized_rows[0] body = normalized_rows[1:] lines = [ "| " + " | ".join(_escape_markdown_cell(cell) for cell in header) + " |", "| " + " | ".join(["---"] * width) + " |", ] lines.extend("| " + " | ".join(_escape_markdown_cell(cell) for cell in row) + " |" for row in body) return "\n".join(lines) def _render_parsed_content(content: str) -> str: text = content.strip() if not text: return "" if "