Spaces:
Sleeping
Sleeping
Alexey Minabutdinov
feat: add CrossRef DOI verification (Check 3b) Resolve each DOI via CrossRef API and compare returned metadata
1e0ae88 | """ | |
| 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 | |
| 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() | |