""" Command-line interface for citation verification. """ import sys from pathlib import Path import click from colorama import init, Fore, Style from .parser import parse_bibtex_file, format_entry_summary from .verifier import CitationVerifier def validate_bibtex_file(ctx, param, value): """Validate that the BibTeX file exists.""" if not Path(value).exists(): raise click.BadParameter( f"File not found: {value}\n" f"Tip: Specify a BibTeX file or ensure 'references.bib' exists in the current directory" ) return value @click.command() @click.option('--bibtex-file', default='references.bib', type=click.Path(), callback=validate_bibtex_file, help='BibTeX file to verify (default: references.bib)') @click.option('--timeout', default=10, help='Request timeout in seconds') @click.option('--max-retries', default=3, help='Maximum retries for 429 rate limit errors') @click.option('--verbose', '-v', is_flag=True, help='Show detailed output') @click.option('--summary-only', '-s', is_flag=True, help='Show only summary') @click.option('--crossref-mailto', default='', help='Email for CrossRef polite pool (improves rate limits)') def main(bibtex_file, timeout, max_retries, verbose, summary_only, crossref_mailto): """ Verify citations in a BibTeX file. This tool checks if citations are valid by: 1. Verifying the paper can be found online with a quick search 2. Validating provided URLs are correct and accessible 3. Checking title and author metadata 4. Identifying version information (arXiv, published, etc.) Example: verify-citations --bibtex-file references.bib verify-citations --bibtex-file sample.bib --verbose verify-citations --max-retries 5 verify-citations (uses default references.bib) """ # Initialize colorama for cross-platform colored output init(autoreset=True) click.echo(f"{Fore.CYAN}=== Citation Verification Tool ==={Style.RESET_ALL}\n") click.echo(f"Processing: {bibtex_file}\n") try: # Parse BibTeX file entries = parse_bibtex_file(bibtex_file) click.echo(f"Found {len(entries)} citation(s) to verify\n") if not entries: click.echo(f"{Fore.YELLOW}No citations found in file{Style.RESET_ALL}") return # Verify each citation verifier = CitationVerifier(timeout=timeout, max_retries=max_retries, crossref_mailto=crossref_mailto) results = [] for i, entry in enumerate(entries, 1): if not summary_only: click.echo(f"{Fore.CYAN}[{i}/{len(entries)}] Verifying:{Style.RESET_ALL}") click.echo(f" {format_entry_summary(entry)}") result = verifier.verify_citation(entry) results.append(result) if not summary_only: _print_result(result, verbose) click.echo() # Blank line between entries # Print summary _print_summary(results) except Exception as e: click.echo(f"{Fore.RED}Error: {str(e)}{Style.RESET_ALL}", err=True) if verbose: import traceback traceback.print_exc() sys.exit(1) def _print_result(result: dict, verbose: bool): """Print verification result for a single citation.""" status = result['status'] # Status indicator if status == 'verified': status_msg = f"{Fore.GREEN}✓ VERIFIED{Style.RESET_ALL}" elif status == 'issues_found': status_msg = f"{Fore.RED}✗ ISSUES FOUND{Style.RESET_ALL}" else: status_msg = f"{Fore.YELLOW}⚠ INCOMPLETE{Style.RESET_ALL}" click.echo(f" Status: {status_msg}") # Print verbose logs if in verbose mode if verbose and result.get('verbose_logs'): for log in result['verbose_logs']: click.echo(f" {Fore.CYAN}{log}{Style.RESET_ALL}") # Print messages if verbose or result['status'] != 'verified': for msg in result['messages']: # Color code messages if msg.startswith('✓'): colored_msg = f"{Fore.GREEN}{msg}{Style.RESET_ALL}" elif msg.startswith('✗'): # Red for critical errors (URL invalid, paper not found) colored_msg = f"{Fore.RED}{msg}{Style.RESET_ALL}" elif msg.startswith('⚠'): # Yellow for warnings (metadata mismatches) colored_msg = f"{Fore.YELLOW}{msg}{Style.RESET_ALL}" elif msg.startswith('ℹ'): colored_msg = f"{Fore.CYAN}{msg}{Style.RESET_ALL}" else: colored_msg = msg click.echo(f" {colored_msg}") def _print_summary(results: list): """Print summary of all verification results.""" click.echo(f"{Fore.CYAN}{'='*60}{Style.RESET_ALL}") click.echo(f"{Fore.CYAN}SUMMARY{Style.RESET_ALL}\n") verified = sum(1 for r in results if r['status'] == 'verified') issues = sum(1 for r in results if r['status'] == 'issues_found') incomplete = sum(1 for r in results if r['status'] == 'incomplete') # Count citations with actual 403 errors (where has_403 flag is True) citations_with_403 = [r for r in results if r.get('has_403', False)] # Count citations where metadata could not be verified automatically # This matches the count of "Could not verify metadata automatically" messages citations_no_metadata_verification = [ r for r in results if r.get('metadata_not_verified', False) ] total = len(results) click.echo(f"Total citations: {total}") click.echo(f"{Fore.GREEN}Verified: {verified}{Style.RESET_ALL}") click.echo(f"{Fore.RED}Issues found: {issues}{Style.RESET_ALL}") click.echo(f"{Fore.YELLOW}Incomplete: {incomplete}{Style.RESET_ALL}") # Show citations with issues (excluding those that only have 403 errors) if issues > 0: click.echo(f"\n{Fore.YELLOW}Citations with issues:{Style.RESET_ALL}") for result in results: if result['status'] == 'issues_found': click.echo(f"- {result['id']}: {result['title']}") # Show citations with 403 errors separately if citations_with_403: click.echo(f"\n{Fore.YELLOW}Citations where you should manually check the links due to a 403 error:{Style.RESET_ALL}") for result in citations_with_403: click.echo(f"- {result['id']}: {result['title']}") # Show citations where authors could not be verified if citations_no_metadata_verification: click.echo(f"\n{Fore.YELLOW}Citations where author list could not be verified:{Style.RESET_ALL}") click.echo(f"Count: {len(citations_no_metadata_verification)}\n") for result in citations_no_metadata_verification: click.echo(f"- {result['id']}: {result['title']}") # DOI verification via CrossRef doi_checked = [r for r in results if r['checks'].get('doi_correct') is not None] if doi_checked: doi_ok = [r for r in doi_checked if r['checks']['doi_correct'] is True] doi_bad = [r for r in doi_checked if r['checks']['doi_correct'] is False] click.echo(f"\n{Fore.CYAN}DOI verification via CrossRef:{Style.RESET_ALL}") click.echo(f" Checked: {len(doi_checked)}, Verified: {len(doi_ok)}, Issues: {len(doi_bad)}") if doi_bad: click.echo(f"\n{Fore.YELLOW}Citations with DOI issues:{Style.RESET_ALL}") for result in doi_bad: click.echo(f"- {result['id']}: {result['title']}") if __name__ == '__main__': main()