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_urlmust be an absolutehttp://orhttps://URL with a host.api_keymust 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.owneris 1 to 64 lowercase letters, digits,.,_, or-, starting with a letter or digit. Without one, the server files your bookmarks under the shared ownerapi-key.http_client(optional) is anhttpx.Clientto 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 oftimeout.
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
timeoutis the level's server budget plus 30 s: 40 s atlow, 60 s atmedium, 90 s athigh, and 150 s atextra.maxhas no limit. Pass a number of seconds to set one, orNonefor 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
timeoutagain, 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()raisesSearchTimeout.
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 itsstatus(active,evicted, orparse_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'sdoi:…. Use thearxiv_idit 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'spublishedisFalsewhile 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()andhealth(): 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-Afterheader 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 sendsRetry-After: 5when 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_urlis likely the web UI's address: the API is under it at/api.
- A redirect is never followed, so the key can't be forwarded to another host. Use the
final
SearchTimeout(aTimeoutError): the search ran pastsearch()'stimeout.httpx.TransportError: the server stayed unreachable through every attempt.ValueError: a bad argument, such as an unknowneffort, a malformedownerorapi_key, or abase_urlthat isn't an absolutehttp(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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| apollodorus_client-0.1.0.tar.gz | 28.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.