Metadata-Version: 2.4
Name: scholar-sidekick
Version: 0.1.0
Summary: Resolve, verify, format and audit scholarly citations — a thin Python client for the Scholar Sidekick API.
Project-URL: Homepage, https://scholar-sidekick.com
Project-URL: Documentation, https://scholar-sidekick.com/docs
Project-URL: Source, https://github.com/mlava/scholar-sidekick-python
Project-URL: Issues, https://github.com/mlava/scholar-sidekick-python/issues
Author: Scholar Sidekick
License: MIT
License-File: LICENSE
Keywords: academic,arxiv,bibliography,bibtex,citation,citation-integrity,crossref,csl,doi,fabricated-citations,hallucination,isbn,open-access,pmid,pubmed,reference-manager,retraction,ris,systematic-review,zotero
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Text Processing :: Markup
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.24
Provides-Extra: dev
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
Requires-Dist: pytest>=7.4; extra == 'dev'
Requires-Dist: respx>=0.20; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Provides-Extra: pandas
Requires-Dist: pandas>=1.3; extra == 'pandas'
Description-Content-Type: text/markdown

# scholar-sidekick

Resolve, verify, format and audit scholarly citations from Python.

A thin client for the [Scholar Sidekick](https://scholar-sidekick.com) REST API — citation
formatting in 10,000+ CSL styles, bibliography export, retraction and open-access checks, and
verification that a citation is real rather than fabricated.

```bash
pip install scholar-sidekick
```

## Quick start

```python
from scholar_sidekick import ScholarSidekick

client = ScholarSidekick()

# Format an identifier
print(client.format(text="10.1038/nphys1170", style="vancouver").text)

# Export a bibliography
print(client.export(text="10.1038/nphys1170", format="bibtex"))
```

## Verifying citations

The dominant AI-fabrication pattern is a **real, resolvable DOI paired with an invented title**.
Such citations resolve cleanly under `doi.org`, so "follow the link and see if it loads" does not
catch them. Comparing the claimed title against the resolved record does.

```python
result = client.verify(
    title="Quantum entanglement in biological systems",
    doi="10.1038/nphys1170",
)

print(result.verdict)  # matched | mismatch | ambiguous | not_found
print(result.confidence)  # high | medium | low
```

A `mismatch` is returned as a value, never raised as an exception — it is the expected outcome for
a fabricated citation, and the reason this method exists.

### Interpreting verdicts

| verdict | meaning |
|---|---|
| `matched` | the claimed citation agrees with the resolved record |
| `mismatch` | the identifier resolves to a **different** work — likely fabrication |
| `ambiguous` | a discrepancy that needs human eyes, not an accusation |
| `not_found` | no record found in the registries searched |

**`not_found` is not proof of fabrication.** Standards documents, software repositories, model
cards and institutional reports are frequently real but absent from the scholarly registries the
API searches. Treat it as "could not confirm", not "invented".

## Auditing a whole bibliography

The API audits up to 25 references per call and rate-limits those calls. `audit_bibliography()`
handles both for you: it chunks, paces itself from the server's rate-limit headers, and sends
chunks **serially** — the server already fans out internally, so parallelising here would compete
with it for the same upstream budget.

```python
report = client.audit_bibliography(
    references, progress=lambda done, total: print(f"{done}/{total}")
)

print(report.summary)  # verdict counts across every chunk
print(len(report.needs_review))  # entries a human should look at

for entry in report.needs_review:
    print(entry.input_index, entry.verdict, entry.title)
```

`entry.input_index` is the 0-based position in the list *you* passed in, so it always points at the
right reference regardless of how the work was chunked.

A chunk that fails is recorded rather than raised, so one transient upstream error does not discard
several minutes of completed work:

```python
if not report.complete:
    for failure in report.errors:
        print(f"entries {failure.start_index}-{failure.end_index} failed: {failure.error}")
```

With `pandas` installed (`pip install 'scholar-sidekick[pandas]'`):

```python
df = report.to_dataframe()
df[df.verdict != "matched"]
```

There is a runnable end-to-end example in
[`examples/systematic_review_audit.ipynb`](examples/systematic_review_audit.ipynb).

## Authentication

Every endpoint works **anonymously** — no key, no signup. An API key raises the rate limit (about
fivefold on the free tier) but is never required.

```python
client = ScholarSidekick(api_key="ssk_…")
```

Or set `SCHOLAR_SIDEKICK_API_KEY` in the environment. Precedence is the explicit argument, then the
environment variable, then anonymous. Free keys are issued at
[scholar-sidekick.com/account](https://scholar-sidekick.com/account).

## Async

Every method has an async equivalent:

```python
from scholar_sidekick import AsyncScholarSidekick

async with AsyncScholarSidekick() as client:
    result = await client.verify(title="…", doi="10.1038/nphys1170")
```

Note that `audit_bibliography()` still issues its chunks serially in the async client — the
constraint is the server's upstream budget, not this client's concurrency.

## Errors

Exceptions are selected by the API's stable error `code`, not by HTTP status (the contract
guarantees the code; statuses may change).

```python
from scholar_sidekick import APIError, RateLimitError, UpstreamError

try:
    client.format(text="10.1038/nphys1170")
except RateLimitError as exc:
    print("retry after", exc.retry_after)
except UpstreamError as exc:
    print("a data source failed:", exc.code)
except APIError as exc:
    print(exc.code, exc.message, exc.request_id)
```

| exception | when |
|---|---|
| `ValidationError` | the request was malformed or invalid |
| `AuthError` | authentication or entitlement was refused |
| `RateLimitError` | rate limit exceeded; carries `.retry_after` |
| `UpstreamError` | a scholarly data source failed after the API's own retries |
| `TransportError` | connection failure or timeout; no HTTP response |

Every `APIError` carries `.request_id` — include it in bug reports.

## API surface

| method | purpose |
|---|---|
| `format()` | resolve identifiers and format them |
| `format_items()` | format already-resolved CSL-JSON items |
| `export()` | export to RIS, BibTeX, CSV, EndNote XML, … |
| `verify()` | check one citation against the resolved record |
| `audit()` | audit up to 25 references in one call |
| `audit_bibliography()` | audit any number, chunked and paced |
| `health()` | service liveness |

`format()` returns whole-batch `text`/`html` strings covering every input, newline-joined — the API
has no per-item formatted field, and this client does not invent one by splitting that string.
`format_items()` names the same concept `output`. The difference is preserved deliberately rather
than papered over.

## Related

- [Scholar Sidekick API docs](https://scholar-sidekick.com/docs)
- [`scholar-sidekick-cli`](https://github.com/mlava/scholar-sidekick-cli) — the same API from the terminal
- [`scholar-sidekick-mcp`](https://github.com/mlava/scholar-sidekick-mcp) — MCP server for AI assistants

## Development

```bash
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,pandas]"
pytest --cov
```

## License

MIT
