Work · Repository
patent-fetch
Find existing patents through swappable providers, and keep a deduplicated library of the ones you read, with provenance.
- Python
- MIT licence
- 0 merged PRs
README
Find existing patents, and keep a deduplicated, private library of the ones you read. Give it some words, or a publication number; it asks a set of swappable patent services, merges what they answer, and says by name which of them answered and which did not. A patent retrieved once is stored with its description, its claims and a provenance record, and is never retrieved again.
It ships as a Python library and a command line (patent-fetch). It runs no language model:
searching, merging and bookkeeping are deterministic.
$ patent-fetch search "cochlear implant electrode array" -n 5
europepmc ok 5 hits
epo-ops skipped 0 hits needs EPO_OPS_KEY, EPO_OPS_SECRET
lens skipped 0 hits needs LENS_API_TOKEN
5 distinct patents
US20110264183 2011 PRE-CURVED COCHLEAR IMPLANT ELECTRODE ARRAY (GIBSON PETER) [europepmc]
US7272449 2007 Cochlear implant electrode array (DADD FYSH) [europepmc]
...
$ patent-fetch fetch US7861317B2
✓ US7861317B2 [retrieved] 2011 Nose cover
source=google-patents full text status=Expired - Fee Related
$ patent-fetch fetch "US 7,861,317" # second time: no network at all
✓ US7861317B2 [library] 2011 Nose cover
$ patent-fetch claims US7861317
I claim:
1. A flexible nose cover configured to attach to eyewear and shield the nose from frostbite ...
What it is for, and what it is not
Two jobs: a one-off search (“is there a patent on this?”), and keeping track of the patents in an area you work in.
-
A collection is a named area: the queries that define it, the patents you decided belong to it, and every patent its searches have ever shown. A sweep runs the queries again and reports only what was not shown before, so an area can be re-checked without re-reading it.
$ patent-fetch collection hearing --create --query "cochlear implant electrode array" $ patent-fetch sweep hearing # everything is new the first time $ patent-fetch collect hearing US7272449 # this one matters to us $ patent-fetch sweep hearing # next month: only what has appeared since - One entry per invention. An invention is usually published several times: an application,
its grant, the same filing at other offices. Publications that share a priority are folded into
one hit, with the others listed under it (
also published as ...), so one patent family does not fill the results.--all-publicationslists each separately. - It is not a legal opinion. A search shows what the enabled providers matched for these words on this day. No hits is not evidence that no patent exists, and nothing here says whether you are free to operate, whether a patent is valid, or whether it is in force: those turn on the claims as a patent attorney reads them. A legal status, an assignee or a priority date is passed on in the provider’s own words, and several providers say of their own data that it is not a legal conclusion.
- Loud, per-provider failure, of two kinds. A provider that is down, rate-limited or out of
allowance is reported by name as
unavailable, the other providers answer, and it is left alone for a while. A provider that fails because of a defect (it rejected our request, or its answer changed shape) is reported aserror, named in the search’sbrokenlist and logged with its traceback (patent-fetch problems). Neither is ever folded into “no results”. - Private. A patent is public, but a copy came from a service with terms of its own, and a
record of what you searched for says what you are working on. Files are
0600in directories0700; S3 objects never get an ACL and are checked with an anonymous request.
Providers
| name | search | lookup | account | what it gives |
|---|---|---|---|---|
europepmc |
yes | yes | none | Europe PMC’s patent records: life-science patents only, and only up to 2012 (4.2 million, from the EPO’s data; nothing later has been added). Abstract and classes, no claims. |
google-patents |
yes | none | One patent’s page on Google Patents, by number: description and claims for 100+ offices. Lookup only; see below. | |
epo-ops |
yes | yes | free registration | EPO Open Patent Services: bibliographic data for 100+ offices, full text for EP, WO and some others. 4 GB a week free. |
lens |
yes | yes | token | The Lens: 150M+ records with claims. Free for trial and non-commercial use; paid otherwise. |
serpapi |
yes | paid (free plan: 250 searches a month) | Google Patents’ search, through SerpApi: full text, so broader than a title-and-abstract search. Opt-in per search (--also serpapi). |
europepmc, google-patents, epo-ops and serpapi have been run against their services.
lens has not: it is written from the service’s documentation and marked NOT YET RUN LIVE
by patent-fetch providers until someone with an account runs it.
With no account at all you can read any patent by its number, but you cannot search for recent
ones: the only keyless search is an archive of life-science patents that ends in 2012. To search
what exists today, in any field, you need at least one keyed provider. epo-ops is the one to
get first: free, official and worldwide. Until then a sweep can never report anything new.
Google Patents has no API. Its robots.txt allows /patent/ pages and disallows everything
else, including its search. So google-patents requests one document page per lookup, two seconds
apart, and never searches. Searching Google Patents from a program means a paid intermediary
(serpapi) or Google’s own BigQuery patents dataset.
Services looked at and not built
| service | why not (as of October 2026) |
|---|---|
| USPTO PatentsView | Its API was shut down on 20 March 2026; the data moved to the USPTO Open Data Portal as bulk downloads. |
| USPTO Open Data Portal | Free key, but it needs a USPTO.gov account verified through ID.me, and its search is over application file wrappers rather than patent text. The next provider to add for US prosecution history. |
Google BigQuery patents-public-data |
Full-text SQL over 90M+ publications, 1 TB of queries a month free. Needs a Google Cloud project; a good fit for a bulk landscape, not for a search box. |
| WIPO PATENTSCOPE | Its web service is a paid subscription. |
| EPO linked open data | A keyless SPARQL endpoint over EP publications answered. A candidate for keyless EP search; not designed yet. |
| SureChEMBL | Serves a patent’s contents by number without a key (chemistry-annotated patents); its search endpoint answered 500. A candidate for a second keyless lookup. |
| PQAI | Natural-language prior-art search; its API now needs a token (401 without one) and its current terms were not checked. |
| Patent MCP servers | Commercial connectors exist (Patlytics, Solve Intelligence, PatSnap) and need their own subscriptions; community servers wrap the same USPTO, EPO and BigQuery sources listed here. |
Install
Python 3.11+. Not on PyPI; install from this repository:
pip install "patent-fetch @ git+https://github.com/polarizetech/patent-fetch"
pip install "patent-fetch[s3] @ git+https://github.com/polarizetech/patent-fetch" # plus the S3-compatible store (boto3)
# or, from a checkout:
uv sync && uv run patent-fetch --help
The core depends only on the standard library. boto3 is needed only for the S3 store.
Configure
Everything is configured with environment variables; none is required. patent-fetch also reads
KEY=value lines from *.env files in ~/.config/patent-fetch/ (or $PATENT_FETCH_ENV_DIR),
taking only the variables below and never overriding the environment. Keep those files out of
repositories (chmod 600).
| Variable | Purpose |
|---|---|
EPO_OPS_KEY, EPO_OPS_SECRET |
EPO Open Patent Services consumer key and secret (register, free). |
LENS_API_TOKEN |
The Lens patent API token (request). |
SERPAPI_API_KEY |
SerpApi key. Paid per search, so asked only when named. |
PATENT_FETCH_EMAIL |
Optional contact address, added to the User-Agent. Never sent unless set. |
PATENT_FETCH_PROVIDERS |
Comma-separated lookup order. Default epo-ops,lens,google-patents,europepmc. |
PATENT_FETCH_SEARCH_PROVIDERS |
Comma-separated search set. Default europepmc,epo-ops,lens. |
PATENT_FETCH_STORE |
local (default), s3 or memory. |
PATENT_FETCH_DATA_DIR |
Local store directory. Default ~/.local/share/patent-fetch ($XDG_DATA_HOME/patent-fetch). |
PATENT_FETCH_CACHE |
Read-through cache for the S3 store. Default ~/.cache/patent-fetch. |
PATENT_FETCH_S3_BUCKET |
S3 store: bucket (required when PATENT_FETCH_STORE=s3). |
PATENT_FETCH_S3_ENDPOINT_URL, PATENT_FETCH_S3_REGION |
S3 store: endpoint for non-AWS services (DigitalOcean Spaces, MinIO, R2, …) and region. |
PATENT_FETCH_S3_ACCESS_KEY_ID, PATENT_FETCH_S3_SECRET_ACCESS_KEY |
S3 store credentials; if unset, boto3’s usual credential chain applies. |
PATENT_FETCH_S3_PUBLIC_URL |
S3 store: the anonymous base URL used to prove objects are not public. Default https://<bucket>.<endpoint host>. |
PATENT_FETCH_PROBLEMS |
Where provider defects are logged, one JSON object per line. Default provider-problems.jsonl in the local store’s directory. Never in the store. |
Everything is stored under the prefix patents/, so one bucket can hold this library beside
another that keeps to its own prefix.
Numbers
A patent is named by its publication number, in any common spelling: US7861317B2,
US 7,861,317, EP 2264377 A2, WO 2012/016009, US 2012/0029596 A1, or a Google Patents URL.
It is stored under country code plus number without the kind code (US7861317). A US
application number (US 10/962,874) is refused, as is a number with no country code.
Python
from patent_fetch import Library
from patent_fetch.config import load_env
load_env() # keys from ~/.config/patent-fetch/*.env, if you keep them there
lib = Library.default()
res = lib.search("cochlear implant electrode array", since=2015, countries=["US", "EP", "WO"])
for report in res["providers"]: # read these before reading "no hits"
print(report["provider"], report["status"])
for hit in res["hits"]:
print(hit["id"], hit["year"], hit["title"], hit["found_by"])
rec = lib.fetch("US7861317B2") # the library first, then the providers
print(lib.claims(rec["id"]))
print(lib.record(rec["id"])["provenance"]) # which provider, when, every route tried
More in INTEGRATION.md.
Development
uv sync
uv run pytest # offline: no credentials, no network
uv run ruff check . && uv run ruff format --check .
uv run pyright
The suite never opens a socket: HTTP is a fake that serves tests/fixtures/. CLAUDE.md
records what was measured against each live service and how to add a provider.
Licence
MIT. See LICENSE.
Changelog
No merged pull requests yet.