Skip to main content

apollodorus-client

Python client for the Apollodorus paper-search API: FluxGate's cache of top research papers (quant finance, CS, statistics, economics), with search that reaches beyond it.

pip install apollodorus-client            # add [dotenv] to read a .env file
from apollodorus_client import Apollodorus

with Apollodorus.from_env() as ap:        # APOLLODORUS_URL + APOLLODORUS_API_KEY
    hits = ap.search("momentum crash", effort="medium")
    for hit in hits:
        print(f"{hit.score:8.3f}  {hit.id}  cached={hit.cached}  {hit.title}")
    print(hits.sources, "partial" if hits.partial else "complete")

Python 3.10 or later; the only dependency is httpx. The server also serves a guide for AI agents at {APOLLODORUS_URL}/llms.txt and its OpenAPI schema at {APOLLODORUS_URL}/openapi.json.

Configuration

Apollodorus.from_env(*, owner=None, timeout=60.0) reads three variables:

Variable Meaning
APOLLODORUS_URL the API root, e.g. https://<your-host>/apollodorus/api (required)
APOLLODORUS_API_KEY the API key, sent as the X-API-Key header (required)
APOLLODORUS_BOOKMARK_OWNER optional: the name your bookmarks are kept under (owner overrides it)

With the [dotenv] extra installed, a .env file in the working directory, or in a directory above it, supplies what the environment lacks; a real environment variable wins over the file. Only those three names are read, from either. from_env() raises RuntimeError when the URL or the key is missing.

Or build the client yourself:

import os

from apollodorus_client import Apollodorus

ap = Apollodorus(
    "https://<your-host>/apollodorus/api",
    os.environ["APOLLODORUS_API_KEY"],
    owner="my-project",  # optional: sent as X-Bookmark-Owner
    timeout=60.0,        # seconds per request; see "Papers, bookmarks, and the corpus" for the exceptions
)
  • base_url must be an absolute http:// or https:// URL with a host.
  • api_key must be visible ASCII: no spaces, control characters, or curly quotes. Whitespace around it, such as a trailing newline from a secrets file, is dropped. A refused key is never repeated in the error.
  • owner is 1 to 64 lowercase letters, digits, ., _, or -, starting with a letter or digit. Without one, the server files your bookmarks under the shared owner api-key.
  • http_client (optional) is an httpx.Client to send through. It is used as it is: never modified, never closed, and never given the key or the base URL, which go only on this client's own requests. Its own timeout then applies in place of timeout.

Keep the key in the environment or a .env file, never in code or URLs, and use an https:// URL. A plain http:// base_url to any host but localhost, 127.0.0.0/8, or ::1 makes the constructor issue a UserWarning, because the key would travel in cleartext.

Effort levels

effort Matches by Searches Server time budget
low (default) keywords the cache: titles, abstracts, full text 10 s
medium keywords the cache + Semantic Scholar, OpenAlex, CORE, arXiv 30 s
high meaning the cache + Semantic Scholar 60 s
extra meaning the cache + Semantic Scholar, OpenAlex, CORE, arXiv 120 s
max meaning, over full text extra, then reads the top top_n papers in full none (minutes)

Keyword levels need every word, in any order; "quoted words" must appear as a phrase. Meaning levels find papers about the query even when they share none of its words. A level that runs out of time answers with what it found, and hits.partial is True. Start at low, and go up when the results fall short. EFFORTS and DEADLINES (seconds, None for max) are importable from the package.

Searching

from apollodorus_client import Apollodorus, JobStatus


def show(status: JobStatus) -> None:  # called with each poll's answer
    print(f"{status.status:<8} {status.stage}  {status.elapsed_s:.0f} s")


with Apollodorus.from_env() as ap:
    hits = ap.search(
        "regime switching asset allocation",
        effort="high",
        limit=50,                # 1-100 results, default 20
        since=2015, until=2024,  # publication years
        field="q-fin",           # a coarse field group: q-fin, econ, cs, stat, ...
        timeout=120,             # seconds the search may run; None waits as long as it takes
        on_progress=show,
    )
    print(hits.job_id, hits.partial, hits.sources, hits.elapsed_s)

    deep = ap.search("intraday momentum and liquidity", effort="max", top_n=10)  # minutes
    for hit in deep[:10]:
        print(hit.fulltext, hit.match, (hit.passage or "")[:200])

search(query, *, effort="low", limit=20, since=None, until=None, field=None, top_n=20, timeout=..., on_progress=None) submits the search as a job on the server and long-polls it until it is done. It returns SearchResults: a list of SearchResult, best first, that also carries .partial, .sources (each source's ok, failed, skipped, or timeout), .job_id, and .elapsed_s. top_n (1–50) is read at max only: the papers whose full text is read. Out-of-range values (a limit of 500, say) are refused by the server, as an ApollodorusError with status 422. on_progress receives a JobStatus (id, status, stage, elapsed_s, heartbeat_age_s, partial, sources) for every answer the server gives.

Each SearchResult has:

Field Meaning
id what fetch() takes: a paper id (2301.12345, ssrn-…, journal-W…), doi:… for a paper known only by its DOI, or None when no source we can fetch from knows it
title, authors, year, source, citation_count, url the paper
cached whether the cache holds it
score BM25 at the keyword levels, cosine similarity otherwise: compare it only within one search
match keyword, meaning, or fulltext
exact every query word (and quoted phrase) occurs in its text
passage the matching snippet (at max, the best full-text chunk), or None
fulltext at max: whether its full text was read; None otherwise
found_by the searches that found it: index (the cache), s2, openalex, core, arxiv

A SearchResult is immutable: authors and found_by are tuples, so a result can go in a set or key a dict. A result only CORE found has found_by == ("core",).

How long a search may run

  • By default timeout is the level's server budget plus 30 s: 40 s at low, 60 s at medium, 90 s at high, and 150 s at extra. max has no limit. Pass a number of seconds to set one, or None for none.
  • The clock starts once the server has taken the search. Time spent queued behind other searches doesn't count: the clock restarts on every poll that finds the search still queued.
  • A resubmitted search (see Retries) gets the whole timeout again, and the pause before it isn't charged to it.
  • Each poll waits on the server for up to 25 s, and never past the time left.
  • Past the limit, search() raises SearchTimeout.

Results the cache doesn't hold

Results with cached=False can still be read: when hit.id isn't None, fetch(hit.id) caches the paper now. Do it only for papers whose text you need now. The server caches uncached results that clear its citation bar in the background, when its auto-cache is on, unless only CORE found them: CORE isn't limited to the project's fields, so those wait for a fetch.

Papers, bookmarks, and the corpus

from apollodorus_client import Apollodorus

with Apollodorus.from_env(owner="my-project") as ap:
    hits = ap.search("volatility targeting", limit=5)
    meta = ap.paper(hits[0].id)  # dict: title, abstract, authors, status, text_source, citation_count, ...
    body = ap.text(hits[0].id)   # str: the full text (just the abstract for abstract-only papers)

    uncached = next((hit for hit in hits if not hit.cached and hit.id), None)
    if uncached is not None:              # only when you need its text now
        meta = ap.fetch(uncached.id)      # slow (tens of seconds); the id may be "doi:..."
        body = ap.text(meta["arxiv_id"])  # read it under the id the fetch returns

    paper_id = meta["arxiv_id"]
    ap.bookmark(paper_id, note="hedging section")  # pins the paper against eviction
    ap.bookmark(paper_id)                          # no note given: the note stays
    ap.bookmark(paper_id, note=None)               # clears the note
    page = ap.bookmarks(limit=20, offset=0)        # yours, newest first: {"results", "total", "limit", "offset"}
    ap.unbookmark(paper_id)                        # {"removed": True, "published": ...}

    print(ap.stats())   # corpus counts and footprint, from the nightly snapshot
    print(ap.health())  # {"status": "ok"}
  • paper(id): one paper's metadata, whatever its status (active, evicted, or parse_failed), including whether you bookmarked it.
  • text(id): a cached paper's full text.
  • fetch(id): caches a paper now, from any source, and returns its metadata. The id may be a search result's doi:…. Use the arxiv_id it returns from then on: it can differ from the id you sent (a version suffix dropped, a merged work, the same paper under another source). Fetching a paper the cache already holds returns it as it is.
  • bookmark(id, note=...): bookmarks a paper for your owner, fetching it first if it isn't cached. The answer's published is False while the change is still on its way to the shared index: in flight, never lost.
  • unbookmark(id): removes it. Removing a bookmark that isn't there is not an error.
  • bookmarks(*, limit=20, offset=0): one page of your bookmarks.
  • stats() and health(): the corpus's counts and footprint, and a liveness check.

fetch and bookmark allow 300 s, since they can download and parse a paper, and each of search()'s polls allows its wait plus 30 s. Every other request uses the client's timeout.

Retries

  • search() and the reads (paper, text, bookmarks, stats, health) retry a 429, a 502, 503, or 504, and a dropped connection: 5 attempts in all, about 1, 2, 4, then 8 s apart. Each pause varies by up to 25% either way, so clients that failed together don't come back together.
  • A Retry-After header on a 429 or 503, in seconds or as an HTTP date, is waited for when it asks for longer than that pause, for at most 30 s. The server sends Retry-After: 5 when its search queue is full or it is restarting.
  • A search the server lost (a poll answered 404, after a restart, say) is submitted again at once. One that failed, or that stalled (running, but silent for over 60 s), is submitted again after a pause of about 1, 2, then 4 s. A search is resubmitted 3 times at most, whatever the causes.
  • The writes (fetch, bookmark, unbookmark) make one attempt, since a write whose answer was lost may still have landed.
  • A request this client built wrong (httpx.LocalProtocolError) is never retried.

Errors

  • ApollodorusError (.status, .detail): the API answered with an error, after any retries. It is also raised when a search failed or stalled through 3 resubmits, or reported a status this client doesn't know, and when the answer can't be the API's:
    • A redirect is never followed, so the key can't be forwarded to another host. Use the final https:// address.
    • A web page means base_url is likely the web UI's address: the API is under it at /api.
  • SearchTimeout (a TimeoutError): the search ran past search()'s timeout.
  • httpx.TransportError: the server stayed unreachable through every attempt.
  • ValueError: a bad argument, such as an unknown effort, a malformed owner or api_key, or a base_url that isn't an absolute http(s):// URL with a host.
  • RuntimeError: from_env() found no URL or no key.

Both exception types pickle and copy cleanly, so they cross process boundaries.

Threads and lifetime

One client per process is enough: it pools connections. It keeps no per-call state, and the httpx.Client it wraps can be shared between threads, so every thread can use the same client. search() blocks the calling thread until the results arrive. Use the client as a context manager, or call close() when you're done with it. Closing never closes an http_client you passed in.

Metadata

Release files for apollodorus-client 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for apollodorus-client 0.1.0
File Size Uploaded Provenance
apollodorus_client-0.1.0.tar.gz 28.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for apollodorus-client 0.1.0
File Interpreter ABI Platform
apollodorus_client-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 43.9 kB

Release files / apollodorus_client-0.1.0.tar.gz

Download URL apollodorus_client-0.1.0.tar.gz
Size 28.5 kB
Tags Source
SHA-256 checksum
How to use checksums
b7ea87e2939b25040c890d0706c933b0e290c1dd6ed6f96956701971db8d5ddf
BLAKE2b-256 checksum
How to use checksums
7691c4cf92515cb4bb4801e40498ce58584b3ad10f9fa8ee2cb126b225f7bbe5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / apollodorus_client-0.1.0-py3-none-any.whl

Download URL apollodorus_client-0.1.0-py3-none-any.whl
Size 15.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8cefad41d50d5ff381f658d7664582108dbf5fadb2dea3558965f2f166dc609e
BLAKE2b-256 checksum
How to use checksums
4970ef39c017fb5427c8e4bd3817f3defb9d450eca641c92bc1dcfc3e58fbd94
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for apollodorus-client 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page