"""Typer CLI: turn federal/state regulation & statute citations into Cornell LII links. Links are built offline by default. ``--verify`` (CFR) and ``search`` (federal) are the only paths that reach the network, via the public eCFR API. """ from __future__ import annotations import typer from . import citations, ecfr, output, states, urls from .citations import Citation, CitationError from .client import HttpError, url_exists app = typer.Typer( add_completion=False, no_args_is_help=True, help="Query Cornell LII (law.cornell.edu) for federal & state regulation links.", ) # Reusable --json flag, shared across commands. JSON = typer.Option(False, "--json", "-j", help="Emit JSON instead of human-readable text.") def _fail(message: str) -> "typer.NoReturn": typer.secho(f"error: {message}", fg=typer.colors.RED, err=True) raise typer.Exit(1) def _parse(text: str | None, title: str | None, section: str | None, corpus: str) -> Citation: """Build a Citation from a free-form string or explicit --title/--section.""" if text: try: return citations.parse_cfr(text) if corpus == "cfr" else citations.parse_usc(text) except CitationError as e: _fail(str(e)) if title and section: return Citation(corpus, str(title), str(section)) word = "CFR" if corpus == "cfr" else "USC" _fail(f"Provide a citation (e.g. '29 {word} 1604.11') or both --title and --section.") # --- Federal CFR (regulations) ------------------------------------------------ @app.command() def cfr( citation: str = typer.Argument(None, help="e.g. '29 CFR 1604.11' or '29/1604.11'"), title: str = typer.Option(None, "--title", "-t", help="CFR title number."), section: str = typer.Option(None, "--section", "-s", help="Section number, e.g. 1604.11."), verify: bool = typer.Option(False, "--verify", help="Confirm the citation exists via the eCFR API."), json_out: bool = JSON, ) -> None: """Resolve a Code of Federal Regulations citation to its LII page.""" cit = _parse(citation, title, section, "cfr") result = { "corpus": "cfr", "citation": cit.label, "title": cit.title, "section": cit.section, "url": urls.cfr_section_url(cit.title, cit.section), } if verify: try: v = ecfr.verify_cfr(cit.title, cit.section) except HttpError as e: _fail(f"eCFR verification failed: {e}") result["verified"] = v["valid"] result["date"] = v["date"] if v.get("heading"): result["label"] = v["heading"] if v.get("detail"): result["detail"] = v["detail"] output.show(result, json_out) @app.command("cfr-part") def cfr_part( title: str = typer.Argument(..., help="CFR title number, e.g. 29."), part: str = typer.Argument(..., help="Part number, e.g. 1604."), json_out: bool = JSON, ) -> None: """Link to a CFR part's browse page (lists its sections).""" result = { "corpus": "cfr", "citation": f"{title} CFR Part {part}", "url": urls.cfr_part_url(title, part), } output.show(result, json_out) @app.command("cfr-title") def cfr_title( title: str = typer.Argument(..., help="CFR title number, e.g. 29."), json_out: bool = JSON, ) -> None: """Link to a CFR title's index page.""" result = {"corpus": "cfr", "citation": f"{title} CFR", "url": urls.cfr_title_url(title)} output.show(result, json_out) # --- Federal USC (statutes) --------------------------------------------------- @app.command() def usc( citation: str = typer.Argument(None, help="e.g. '42 USC 1983' or '42/1983'"), title: str = typer.Option(None, "--title", "-t", help="USC title number."), section: str = typer.Option(None, "--section", "-s", help="Section number, e.g. 1983."), verify: bool = typer.Option(False, "--verify", help="Confirm the LII page resolves (HTTP check)."), json_out: bool = JSON, ) -> None: """Resolve a US Code citation to its LII page.""" cit = _parse(citation, title, section, "usc") url = urls.usc_section_url(cit.title, cit.section) result = { "corpus": "usc", "citation": cit.label, "title": cit.title, "section": cit.section, "url": url, } if verify: ok = url_exists(url) result["verified"] = ok if not ok: result["detail"] = "LII page did not return HTTP 200." output.show(result, json_out) # --- State materials ---------------------------------------------------------- @app.command("state-regs") def state_regs( state: str = typer.Argument(..., help="State name or USPS abbreviation, e.g. 'maine' or 'ME'."), json_out: bool = JSON, ) -> None: """Link to a state's LII-hosted administrative code (regulations).""" try: slug = states.resolve_state(state) except ValueError as e: _fail(str(e)) result = { "corpus": "state-regs", "citation": f"{slug.replace('_', ' ').title()} regulations", "url": urls.state_regs_url(slug), } output.show(result, json_out) @app.command() def state( state: str = typer.Argument(..., help="State name or USPS abbreviation, e.g. 'new york' or 'NY'."), json_out: bool = JSON, ) -> None: """Link to a state's LII portal page (statutes link out to official sources).""" try: slug = states.resolve_state(state) except ValueError as e: _fail(str(e)) result = { "corpus": "state", "citation": f"{slug.replace('_', ' ').title()} legal materials", "url": urls.state_portal_url(slug), } output.show(result, json_out) @app.command("states") def states_cmd(json_out: bool = JSON) -> None: """Link to the LII states & regulations directories.""" result = { "links": [ {"citation": "All states", "url": urls.states_directory_url()}, {"citation": "All state regulations", "url": urls.regulations_directory_url()}, ] } output.show(result, json_out) # --- Search ------------------------------------------------------------------- @app.command() def search( query: str = typer.Argument(..., help="Search terms."), corpus: str = typer.Option("cfr", "--corpus", "-c", help="'cfr' = eCFR API deep links; 'lii' = LII site-search URL."), limit: int = typer.Option(10, "--limit", "-n", help="Max results (cfr corpus)."), json_out: bool = JSON, ) -> None: """Search federal regulations (eCFR -> LII links), or get the LII site-search URL.""" corpus = corpus.lower() if corpus == "cfr": try: hits = ecfr.search_federal(query, limit) except HttpError as e: _fail(f"eCFR search failed: {e}") output.show({"query": query, "corpus": "cfr", "links": hits}, json_out, attribution=True) elif corpus == "lii": result = { "query": query, "corpus": "lii", "links": [ { "citation": f"LII site search: {query}", "url": urls.lii_search_url(query), "note": "Open in a browser — LII search is not crawlable (robots.txt) and renders client-side.", } ], } output.show(result, json_out, attribution=True) else: _fail("--corpus must be 'cfr' or 'lii'.") if __name__ == "__main__": app()