dethrottled 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dethrottled-0.1.1/LICENSE +21 -0
- dethrottled-0.1.1/PKG-INFO +267 -0
- dethrottled-0.1.1/README.md +211 -0
- dethrottled-0.1.1/pyproject.toml +138 -0
- dethrottled-0.1.1/setup.cfg +4 -0
- dethrottled-0.1.1/src/dethrottled/__init__.py +2 -0
- dethrottled-0.1.1/src/dethrottled/__main__.py +4 -0
- dethrottled-0.1.1/src/dethrottled/_quiet.py +122 -0
- dethrottled-0.1.1/src/dethrottled/cache.py +123 -0
- dethrottled-0.1.1/src/dethrottled/corpus.py +436 -0
- dethrottled-0.1.1/src/dethrottled/documents.py +372 -0
- dethrottled-0.1.1/src/dethrottled/domains.py +275 -0
- dethrottled-0.1.1/src/dethrottled/extract.py +258 -0
- dethrottled-0.1.1/src/dethrottled/fetch.py +1042 -0
- dethrottled-0.1.1/src/dethrottled/health.py +250 -0
- dethrottled-0.1.1/src/dethrottled/media.py +142 -0
- dethrottled-0.1.1/src/dethrottled/ocr.py +250 -0
- dethrottled-0.1.1/src/dethrottled/paths.py +58 -0
- dethrottled-0.1.1/src/dethrottled/rank.py +334 -0
- dethrottled-0.1.1/src/dethrottled/search.py +614 -0
- dethrottled-0.1.1/src/dethrottled/server.py +447 -0
- dethrottled-0.1.1/src/dethrottled.egg-info/PKG-INFO +267 -0
- dethrottled-0.1.1/src/dethrottled.egg-info/SOURCES.txt +45 -0
- dethrottled-0.1.1/src/dethrottled.egg-info/dependency_links.txt +1 -0
- dethrottled-0.1.1/src/dethrottled.egg-info/entry_points.txt +3 -0
- dethrottled-0.1.1/src/dethrottled.egg-info/requires.txt +40 -0
- dethrottled-0.1.1/src/dethrottled.egg-info/top_level.txt +1 -0
- dethrottled-0.1.1/tests/test_cache.py +110 -0
- dethrottled-0.1.1/tests/test_canonical.py +55 -0
- dethrottled-0.1.1/tests/test_concurrency.py +192 -0
- dethrottled-0.1.1/tests/test_contract.py +156 -0
- dethrottled-0.1.1/tests/test_cooldown.py +96 -0
- dethrottled-0.1.1/tests/test_corpus.py +247 -0
- dethrottled-0.1.1/tests/test_documents.py +94 -0
- dethrottled-0.1.1/tests/test_documents_odf.py +188 -0
- dethrottled-0.1.1/tests/test_documents_parsers.py +162 -0
- dethrottled-0.1.1/tests/test_domains.py +167 -0
- dethrottled-0.1.1/tests/test_endpoints.py +117 -0
- dethrottled-0.1.1/tests/test_extract.py +105 -0
- dethrottled-0.1.1/tests/test_fetch_internals.py +193 -0
- dethrottled-0.1.1/tests/test_health.py +176 -0
- dethrottled-0.1.1/tests/test_media.py +182 -0
- dethrottled-0.1.1/tests/test_ocr.py +187 -0
- dethrottled-0.1.1/tests/test_quiet.py +118 -0
- dethrottled-0.1.1/tests/test_rank.py +108 -0
- dethrottled-0.1.1/tests/test_search_internals.py +174 -0
- dethrottled-0.1.1/tests/test_tier_matrix.py +103 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dethrottled contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dethrottled
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Zero-API web search, fetch and extraction. No keys, no quota, no metered pool.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/zataraine/dethrottled
|
|
7
|
+
Project-URL: Repository, https://github.com/zataraine/dethrottled
|
|
8
|
+
Project-URL: Issues, https://github.com/zataraine/dethrottled/issues
|
|
9
|
+
Keywords: search,scraping,extraction,rag,web,no-api-key
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
18
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: requests>=2.31
|
|
23
|
+
Requires-Dist: feedparser>=6.0
|
|
24
|
+
Requires-Dist: ddgs>=9.0
|
|
25
|
+
Requires-Dist: trafilatura>=1.8
|
|
26
|
+
Requires-Dist: resiliparse>=1.0
|
|
27
|
+
Requires-Dist: selectolax>=0.3.21
|
|
28
|
+
Requires-Dist: fastapi>=0.110
|
|
29
|
+
Requires-Dist: uvicorn>=0.27
|
|
30
|
+
Provides-Extra: documents
|
|
31
|
+
Requires-Dist: pymupdf>=1.24; extra == "documents"
|
|
32
|
+
Requires-Dist: openpyxl>=3.1; extra == "documents"
|
|
33
|
+
Requires-Dist: python-docx>=1.1; extra == "documents"
|
|
34
|
+
Requires-Dist: python-pptx>=0.6.23; extra == "documents"
|
|
35
|
+
Requires-Dist: xlrd>=2.0; extra == "documents"
|
|
36
|
+
Requires-Dist: odfpy>=1.4; extra == "documents"
|
|
37
|
+
Requires-Dist: ebooklib>=0.18; extra == "documents"
|
|
38
|
+
Requires-Dist: striprtf>=0.0.26; extra == "documents"
|
|
39
|
+
Provides-Extra: media
|
|
40
|
+
Requires-Dist: youtube-transcript-api>=1.0; extra == "media"
|
|
41
|
+
Provides-Extra: semantic
|
|
42
|
+
Requires-Dist: numpy>=1.24; extra == "semantic"
|
|
43
|
+
Requires-Dist: onnxruntime>=1.17; extra == "semantic"
|
|
44
|
+
Requires-Dist: transformers>=4.40; extra == "semantic"
|
|
45
|
+
Provides-Extra: rerank
|
|
46
|
+
Requires-Dist: flashrank>=0.2.9; extra == "rerank"
|
|
47
|
+
Provides-Extra: tls
|
|
48
|
+
Requires-Dist: curl_cffi>=0.7; extra == "tls"
|
|
49
|
+
Provides-Extra: all
|
|
50
|
+
Requires-Dist: dethrottled[documents,media,rerank,semantic,tls]; extra == "all"
|
|
51
|
+
Provides-Extra: dev
|
|
52
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
53
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
54
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
55
|
+
Dynamic: license-file
|
|
56
|
+
|
|
57
|
+
# dethrottled
|
|
58
|
+
|
|
59
|
+
**Web search, fetch and extraction with no API keys, no accounts, and no quota.**
|
|
60
|
+
|
|
61
|
+
Ask it a question and it returns results. Give it a URL and it returns the text
|
|
62
|
+
— from a web page, a PDF, a spreadsheet, a slide deck, an ebook, or a video's
|
|
63
|
+
captions. It runs on your machine, and there is nothing to sign up for.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install 'dethrottled[all]'
|
|
67
|
+
dethrottled
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
curl -X POST localhost:8787/search-and-fetch \
|
|
72
|
+
-H 'content-type: application/json' \
|
|
73
|
+
-d '{"query": "okapi bm25 ranking", "num_results": 3}'
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Or bring up the whole self-hosted stack — search engines, a JavaScript renderer
|
|
77
|
+
and the API — with nothing leaving your network:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
docker compose up -d
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
> Want the whole story? [**TLDREADME.md**](TLDREADME.md) documents every tier,
|
|
84
|
+
> every measurement, and every decision in full. This page is the short version.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Why it exists
|
|
89
|
+
|
|
90
|
+
Most search and scraping tools ask for an API key, meter you, and cut you off.
|
|
91
|
+
The ones that don't usually fall over on the pages that matter — the JavaScript
|
|
92
|
+
app, the PDF with no text layer, the spreadsheet where the actual numbers live.
|
|
93
|
+
|
|
94
|
+
dethrottled handles those, locally, for nothing. Every number below is measured
|
|
95
|
+
and reproducible from scripts in this repository.
|
|
96
|
+
|
|
97
|
+
## Three endpoints
|
|
98
|
+
|
|
99
|
+
| endpoint | what it does |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `POST /search` | a query → ranked results |
|
|
102
|
+
| `POST /fetch` | URLs → their text (`raw: true` for the source) |
|
|
103
|
+
| `POST /search-and-fetch` | both in one call, fetching only the winners |
|
|
104
|
+
|
|
105
|
+
Plus `/corpus/search` (query what you've already fetched, no network),
|
|
106
|
+
`/health`, `/v2/status`, `/v2/capabilities` and `/stats`.
|
|
107
|
+
|
|
108
|
+
There is deliberately **no `/crawl`**. Rendering is a strategy for obtaining a
|
|
109
|
+
page, not something a caller wants for its own sake — the ladder escalates by
|
|
110
|
+
itself, and a caller choosing the renderer by hand would spend four seconds of
|
|
111
|
+
Chromium on a page `direct` serves in two hundred milliseconds. Where forcing
|
|
112
|
+
it is genuinely useful, that's the `render` parameter.
|
|
113
|
+
|
|
114
|
+
`/extract` and `/search-and-extract` remain as aliases.
|
|
115
|
+
|
|
116
|
+
## The fetch ladder
|
|
117
|
+
|
|
118
|
+
Three tiers, tried in order, and **a tier only succeeds if it yields readable
|
|
119
|
+
prose**. That's the whole design, and it comes from a measurement:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
page direct tls crawl4ai
|
|
123
|
+
quotes-js 0 0 ✓ ← JavaScript-only page
|
|
124
|
+
indeed 0 2340 ✓ ← 403s an ordinary TLS handshake
|
|
125
|
+
wikipedia 26861 26861 ✓
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`quotes-js` returns **HTTP 200, a complete response body, and zero characters
|
|
129
|
+
of article text.** A ladder that escalates on failed fetches declares victory
|
|
130
|
+
there and hands you an empty shell. So escalation is driven by recovered text,
|
|
131
|
+
and anything under 600 characters counts as a miss.
|
|
132
|
+
|
|
133
|
+
| tier | speed | what it's for | needs |
|
|
134
|
+
| --- | --- | --- | --- |
|
|
135
|
+
| `direct` | ~2.1s | most pages. requests + trafilatura | nothing |
|
|
136
|
+
| `tls` | ~0.3s | a real Chrome TLS fingerprint, no browser | a library |
|
|
137
|
+
| `crawl4ai` | ~4.4s | renders JavaScript, locally | a container you run |
|
|
138
|
+
|
|
139
|
+
Only `direct` is required. `tls` is **faster than plain requests** (310ms vs
|
|
140
|
+
536ms median) because curl-impersonate is C — it's not a slow fallback, it's a
|
|
141
|
+
quick one. A shorter ladder solves fewer pages and nothing breaks.
|
|
142
|
+
|
|
143
|
+
There is deliberately **no relay tier**. Every option needs an account (a
|
|
144
|
+
Cloudflare Worker), a third party who then learns every URL you fetch (a hosted
|
|
145
|
+
reader, a CORS proxy), or an address range that anti-bot vendors blocklist on
|
|
146
|
+
sight (Tor publishes its exit list in real time).
|
|
147
|
+
|
|
148
|
+
## Everything it can read
|
|
149
|
+
|
|
150
|
+
Content type is decided from the **file signature**, never the server's header —
|
|
151
|
+
an HTML error page served as `application/vnd.ms-excel` is routine, and handing
|
|
152
|
+
that to a spreadsheet parser produces either a crash or, worse, nonsense that
|
|
153
|
+
looks like data.
|
|
154
|
+
|
|
155
|
+
| kind | formats |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| web | HTML, via trafilatura → resiliparse → selectolax |
|
|
158
|
+
| documents | PDF (+ OCR for scans), DOCX, XLSX, XLS, PPTX, CSV/TSV |
|
|
159
|
+
| open formats | ODT, ODS, ODP, EPUB, RTF |
|
|
160
|
+
| video | YouTube captions, from the URL |
|
|
161
|
+
|
|
162
|
+
Routing costs **0.6µs** for the common case. MarkItDown does this same job with
|
|
163
|
+
a neural network; this is forty lines of byte comparison.
|
|
164
|
+
|
|
165
|
+
## Ranking
|
|
166
|
+
|
|
167
|
+
Results arrive from three sources in no order at all. Up to three stages fix
|
|
168
|
+
that, each optional:
|
|
169
|
+
|
|
170
|
+
1. **BM25** — lexical, no model, microseconds
|
|
171
|
+
2. **Corpus merge** — passages you've already fetched, competing with the web
|
|
172
|
+
3. **Cross-encoder** — a real model, over a shortlist of 40 only
|
|
173
|
+
|
|
174
|
+
Measured on a pool where one document is right and the rest are plausible
|
|
175
|
+
neighbours: **MRR 0.646 → 0.833**. It costs 16ms and earns it.
|
|
176
|
+
|
|
177
|
+
Two orderings are deliberate: **rank before fetching**, because fetching is the
|
|
178
|
+
expensive step; and **merge the corpus before ranking**, so the reranker judges
|
|
179
|
+
web and local results on equal terms.
|
|
180
|
+
|
|
181
|
+
## The corpus
|
|
182
|
+
|
|
183
|
+
Every page fetched is split into passages, embedded and stored in SQLite, so
|
|
184
|
+
the cache quietly becomes searchable:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
curl -G localhost:8787/corpus/search --data-urlencode 'q=how are documents ranked'
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
No vector database. Brute-force cosine over 50,000 passages takes **2.5ms** —
|
|
191
|
+
an index would be machinery with nothing to do.
|
|
192
|
+
|
|
193
|
+
## It learns which sites are readable
|
|
194
|
+
|
|
195
|
+
Some domains reliably return nothing: single-page apps that render from a
|
|
196
|
+
private API, syndication aggregators, consent walls. Ranking answers *"is this
|
|
197
|
+
relevant?"* and has no idea whether a page can be **read**.
|
|
198
|
+
|
|
199
|
+
So dethrottled records per-domain extraction outcomes and uses them at exactly
|
|
200
|
+
one point — choosing which of the already-ranked results to spend a fetch on.
|
|
201
|
+
It never reorders by relevance and never drops a result.
|
|
202
|
+
|
|
203
|
+
Crucially it's **measured, not declared**. No blocklist ships. Evidence decays
|
|
204
|
+
with a 14-day half-life, so a site that starts working recovers on its own.
|
|
205
|
+
|
|
206
|
+
## Install
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
pip install 'dethrottled[all]' # everything below. Start here
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The base install is deliberately small — eight pure-Python dependencies — and
|
|
213
|
+
gives you search, the `direct` fetch tier and the extraction cascade. Every
|
|
214
|
+
other capability is an extra, because the heavy pieces are the ones many
|
|
215
|
+
callers never use:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
pip install dethrottled # search, direct fetch, extraction
|
|
219
|
+
pip install 'dethrottled[documents]' # + PDF, Office, ODF, EPUB, RTF, OCR
|
|
220
|
+
pip install 'dethrottled[tls]' # + the TLS tier
|
|
221
|
+
pip install 'dethrottled[semantic]' # + the corpus
|
|
222
|
+
pip install 'dethrottled[rerank]' # + the cross-encoder
|
|
223
|
+
pip install 'dethrottled[media]' # + video transcripts
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Every optional import is guarded: a missing extra removes a capability, it does
|
|
227
|
+
not break one, and `/v2/capabilities` reports exactly what this install can
|
|
228
|
+
actually do rather than what the code is capable of.
|
|
229
|
+
|
|
230
|
+
Runs on **x86-64 and ARM64**, Linux, macOS and Windows, Python 3.10–3.13. CI
|
|
231
|
+
tests both architectures. Nothing needs a compiler — **including on a Pi**.
|
|
232
|
+
|
|
233
|
+
One model, 87MB:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
./scripts/fetch-models.sh
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Politeness
|
|
240
|
+
|
|
241
|
+
This fetches other people's pages, so: `robots.txt` honoured at every tier and
|
|
242
|
+
cached; one request per domain at a time with a 1.5s floor; an honest,
|
|
243
|
+
contactable User-Agent; bounded retries; a 10MB response ceiling.
|
|
244
|
+
|
|
245
|
+
That 1.5s floor means **fetching many URLs from one site is slow by design** —
|
|
246
|
+
8 URLs from one domain take 12.7s, 8 URLs from eight domains take 3.3s. That's
|
|
247
|
+
the politeness working, not a defect.
|
|
248
|
+
|
|
249
|
+
## Licensing
|
|
250
|
+
|
|
251
|
+
MIT, and it ships no weights. Everything it downloads is permissive:
|
|
252
|
+
all-MiniLM-L6-v2 (Apache-2.0) and ms-marco-MiniLM-L-12-v2 (Apache-2.0).
|
|
253
|
+
|
|
254
|
+
There is deliberately **no non-commercially-licensed component in any code
|
|
255
|
+
path** — "optional" is not something a licence audit can rely on.
|
|
256
|
+
|
|
257
|
+
## Documentation
|
|
258
|
+
|
|
259
|
+
- [**TLDREADME.md**](TLDREADME.md) — everything, in depth. Every tier, every
|
|
260
|
+
measurement, every rejected alternative
|
|
261
|
+
- [USAGE.md](USAGE.md) — the API and every configuration knob
|
|
262
|
+
- [ARCHITECTURE.md](ARCHITECTURE.md) — how the pieces fit
|
|
263
|
+
- [SECURITY.md](SECURITY.md) — **read before exposing this to a network**
|
|
264
|
+
|
|
265
|
+
## Licence
|
|
266
|
+
|
|
267
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# dethrottled
|
|
2
|
+
|
|
3
|
+
**Web search, fetch and extraction with no API keys, no accounts, and no quota.**
|
|
4
|
+
|
|
5
|
+
Ask it a question and it returns results. Give it a URL and it returns the text
|
|
6
|
+
— from a web page, a PDF, a spreadsheet, a slide deck, an ebook, or a video's
|
|
7
|
+
captions. It runs on your machine, and there is nothing to sign up for.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install 'dethrottled[all]'
|
|
11
|
+
dethrottled
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
curl -X POST localhost:8787/search-and-fetch \
|
|
16
|
+
-H 'content-type: application/json' \
|
|
17
|
+
-d '{"query": "okapi bm25 ranking", "num_results": 3}'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or bring up the whole self-hosted stack — search engines, a JavaScript renderer
|
|
21
|
+
and the API — with nothing leaving your network:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
docker compose up -d
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> Want the whole story? [**TLDREADME.md**](TLDREADME.md) documents every tier,
|
|
28
|
+
> every measurement, and every decision in full. This page is the short version.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Why it exists
|
|
33
|
+
|
|
34
|
+
Most search and scraping tools ask for an API key, meter you, and cut you off.
|
|
35
|
+
The ones that don't usually fall over on the pages that matter — the JavaScript
|
|
36
|
+
app, the PDF with no text layer, the spreadsheet where the actual numbers live.
|
|
37
|
+
|
|
38
|
+
dethrottled handles those, locally, for nothing. Every number below is measured
|
|
39
|
+
and reproducible from scripts in this repository.
|
|
40
|
+
|
|
41
|
+
## Three endpoints
|
|
42
|
+
|
|
43
|
+
| endpoint | what it does |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `POST /search` | a query → ranked results |
|
|
46
|
+
| `POST /fetch` | URLs → their text (`raw: true` for the source) |
|
|
47
|
+
| `POST /search-and-fetch` | both in one call, fetching only the winners |
|
|
48
|
+
|
|
49
|
+
Plus `/corpus/search` (query what you've already fetched, no network),
|
|
50
|
+
`/health`, `/v2/status`, `/v2/capabilities` and `/stats`.
|
|
51
|
+
|
|
52
|
+
There is deliberately **no `/crawl`**. Rendering is a strategy for obtaining a
|
|
53
|
+
page, not something a caller wants for its own sake — the ladder escalates by
|
|
54
|
+
itself, and a caller choosing the renderer by hand would spend four seconds of
|
|
55
|
+
Chromium on a page `direct` serves in two hundred milliseconds. Where forcing
|
|
56
|
+
it is genuinely useful, that's the `render` parameter.
|
|
57
|
+
|
|
58
|
+
`/extract` and `/search-and-extract` remain as aliases.
|
|
59
|
+
|
|
60
|
+
## The fetch ladder
|
|
61
|
+
|
|
62
|
+
Three tiers, tried in order, and **a tier only succeeds if it yields readable
|
|
63
|
+
prose**. That's the whole design, and it comes from a measurement:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
page direct tls crawl4ai
|
|
67
|
+
quotes-js 0 0 ✓ ← JavaScript-only page
|
|
68
|
+
indeed 0 2340 ✓ ← 403s an ordinary TLS handshake
|
|
69
|
+
wikipedia 26861 26861 ✓
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`quotes-js` returns **HTTP 200, a complete response body, and zero characters
|
|
73
|
+
of article text.** A ladder that escalates on failed fetches declares victory
|
|
74
|
+
there and hands you an empty shell. So escalation is driven by recovered text,
|
|
75
|
+
and anything under 600 characters counts as a miss.
|
|
76
|
+
|
|
77
|
+
| tier | speed | what it's for | needs |
|
|
78
|
+
| --- | --- | --- | --- |
|
|
79
|
+
| `direct` | ~2.1s | most pages. requests + trafilatura | nothing |
|
|
80
|
+
| `tls` | ~0.3s | a real Chrome TLS fingerprint, no browser | a library |
|
|
81
|
+
| `crawl4ai` | ~4.4s | renders JavaScript, locally | a container you run |
|
|
82
|
+
|
|
83
|
+
Only `direct` is required. `tls` is **faster than plain requests** (310ms vs
|
|
84
|
+
536ms median) because curl-impersonate is C — it's not a slow fallback, it's a
|
|
85
|
+
quick one. A shorter ladder solves fewer pages and nothing breaks.
|
|
86
|
+
|
|
87
|
+
There is deliberately **no relay tier**. Every option needs an account (a
|
|
88
|
+
Cloudflare Worker), a third party who then learns every URL you fetch (a hosted
|
|
89
|
+
reader, a CORS proxy), or an address range that anti-bot vendors blocklist on
|
|
90
|
+
sight (Tor publishes its exit list in real time).
|
|
91
|
+
|
|
92
|
+
## Everything it can read
|
|
93
|
+
|
|
94
|
+
Content type is decided from the **file signature**, never the server's header —
|
|
95
|
+
an HTML error page served as `application/vnd.ms-excel` is routine, and handing
|
|
96
|
+
that to a spreadsheet parser produces either a crash or, worse, nonsense that
|
|
97
|
+
looks like data.
|
|
98
|
+
|
|
99
|
+
| kind | formats |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| web | HTML, via trafilatura → resiliparse → selectolax |
|
|
102
|
+
| documents | PDF (+ OCR for scans), DOCX, XLSX, XLS, PPTX, CSV/TSV |
|
|
103
|
+
| open formats | ODT, ODS, ODP, EPUB, RTF |
|
|
104
|
+
| video | YouTube captions, from the URL |
|
|
105
|
+
|
|
106
|
+
Routing costs **0.6µs** for the common case. MarkItDown does this same job with
|
|
107
|
+
a neural network; this is forty lines of byte comparison.
|
|
108
|
+
|
|
109
|
+
## Ranking
|
|
110
|
+
|
|
111
|
+
Results arrive from three sources in no order at all. Up to three stages fix
|
|
112
|
+
that, each optional:
|
|
113
|
+
|
|
114
|
+
1. **BM25** — lexical, no model, microseconds
|
|
115
|
+
2. **Corpus merge** — passages you've already fetched, competing with the web
|
|
116
|
+
3. **Cross-encoder** — a real model, over a shortlist of 40 only
|
|
117
|
+
|
|
118
|
+
Measured on a pool where one document is right and the rest are plausible
|
|
119
|
+
neighbours: **MRR 0.646 → 0.833**. It costs 16ms and earns it.
|
|
120
|
+
|
|
121
|
+
Two orderings are deliberate: **rank before fetching**, because fetching is the
|
|
122
|
+
expensive step; and **merge the corpus before ranking**, so the reranker judges
|
|
123
|
+
web and local results on equal terms.
|
|
124
|
+
|
|
125
|
+
## The corpus
|
|
126
|
+
|
|
127
|
+
Every page fetched is split into passages, embedded and stored in SQLite, so
|
|
128
|
+
the cache quietly becomes searchable:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
curl -G localhost:8787/corpus/search --data-urlencode 'q=how are documents ranked'
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
No vector database. Brute-force cosine over 50,000 passages takes **2.5ms** —
|
|
135
|
+
an index would be machinery with nothing to do.
|
|
136
|
+
|
|
137
|
+
## It learns which sites are readable
|
|
138
|
+
|
|
139
|
+
Some domains reliably return nothing: single-page apps that render from a
|
|
140
|
+
private API, syndication aggregators, consent walls. Ranking answers *"is this
|
|
141
|
+
relevant?"* and has no idea whether a page can be **read**.
|
|
142
|
+
|
|
143
|
+
So dethrottled records per-domain extraction outcomes and uses them at exactly
|
|
144
|
+
one point — choosing which of the already-ranked results to spend a fetch on.
|
|
145
|
+
It never reorders by relevance and never drops a result.
|
|
146
|
+
|
|
147
|
+
Crucially it's **measured, not declared**. No blocklist ships. Evidence decays
|
|
148
|
+
with a 14-day half-life, so a site that starts working recovers on its own.
|
|
149
|
+
|
|
150
|
+
## Install
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pip install 'dethrottled[all]' # everything below. Start here
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The base install is deliberately small — eight pure-Python dependencies — and
|
|
157
|
+
gives you search, the `direct` fetch tier and the extraction cascade. Every
|
|
158
|
+
other capability is an extra, because the heavy pieces are the ones many
|
|
159
|
+
callers never use:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
pip install dethrottled # search, direct fetch, extraction
|
|
163
|
+
pip install 'dethrottled[documents]' # + PDF, Office, ODF, EPUB, RTF, OCR
|
|
164
|
+
pip install 'dethrottled[tls]' # + the TLS tier
|
|
165
|
+
pip install 'dethrottled[semantic]' # + the corpus
|
|
166
|
+
pip install 'dethrottled[rerank]' # + the cross-encoder
|
|
167
|
+
pip install 'dethrottled[media]' # + video transcripts
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Every optional import is guarded: a missing extra removes a capability, it does
|
|
171
|
+
not break one, and `/v2/capabilities` reports exactly what this install can
|
|
172
|
+
actually do rather than what the code is capable of.
|
|
173
|
+
|
|
174
|
+
Runs on **x86-64 and ARM64**, Linux, macOS and Windows, Python 3.10–3.13. CI
|
|
175
|
+
tests both architectures. Nothing needs a compiler — **including on a Pi**.
|
|
176
|
+
|
|
177
|
+
One model, 87MB:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
./scripts/fetch-models.sh
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Politeness
|
|
184
|
+
|
|
185
|
+
This fetches other people's pages, so: `robots.txt` honoured at every tier and
|
|
186
|
+
cached; one request per domain at a time with a 1.5s floor; an honest,
|
|
187
|
+
contactable User-Agent; bounded retries; a 10MB response ceiling.
|
|
188
|
+
|
|
189
|
+
That 1.5s floor means **fetching many URLs from one site is slow by design** —
|
|
190
|
+
8 URLs from one domain take 12.7s, 8 URLs from eight domains take 3.3s. That's
|
|
191
|
+
the politeness working, not a defect.
|
|
192
|
+
|
|
193
|
+
## Licensing
|
|
194
|
+
|
|
195
|
+
MIT, and it ships no weights. Everything it downloads is permissive:
|
|
196
|
+
all-MiniLM-L6-v2 (Apache-2.0) and ms-marco-MiniLM-L-12-v2 (Apache-2.0).
|
|
197
|
+
|
|
198
|
+
There is deliberately **no non-commercially-licensed component in any code
|
|
199
|
+
path** — "optional" is not something a licence audit can rely on.
|
|
200
|
+
|
|
201
|
+
## Documentation
|
|
202
|
+
|
|
203
|
+
- [**TLDREADME.md**](TLDREADME.md) — everything, in depth. Every tier, every
|
|
204
|
+
measurement, every rejected alternative
|
|
205
|
+
- [USAGE.md](USAGE.md) — the API and every configuration knob
|
|
206
|
+
- [ARCHITECTURE.md](ARCHITECTURE.md) — how the pieces fit
|
|
207
|
+
- [SECURITY.md](SECURITY.md) — **read before exposing this to a network**
|
|
208
|
+
|
|
209
|
+
## Licence
|
|
210
|
+
|
|
211
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dethrottled"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Zero-API web search, fetch and extraction. No keys, no quota, no metered pool."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["search", "scraping", "extraction", "rag", "web", "no-api-key"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Internet :: WWW/HTTP :: Indexing/Search",
|
|
22
|
+
"Topic :: Text Processing :: Markup :: HTML",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# The base install fetches, extracts and searches. Everything past that is an
|
|
26
|
+
# extra, because the heavy pieces are exactly the ones most callers never use:
|
|
27
|
+
# onnxruntime and transformers are a few hundred megabytes to make a feature
|
|
28
|
+
# optional, and pymupdf is not free either. Every optional import in the source
|
|
29
|
+
# is guarded, so an absent extra removes a capability rather than breaking one.
|
|
30
|
+
dependencies = [
|
|
31
|
+
"requests>=2.31",
|
|
32
|
+
"feedparser>=6.0",
|
|
33
|
+
# Search: one pure-Python dependency covering a dozen keyless engines.
|
|
34
|
+
"ddgs>=9.0",
|
|
35
|
+
# Extraction cascade, in order. All three ship aarch64 wheels, so a
|
|
36
|
+
# Raspberry Pi installs them rather than compiling them.
|
|
37
|
+
"trafilatura>=1.8",
|
|
38
|
+
"resiliparse>=1.0",
|
|
39
|
+
"selectolax>=0.3.21",
|
|
40
|
+
"fastapi>=0.110",
|
|
41
|
+
"uvicorn>=0.27",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://github.com/zataraine/dethrottled"
|
|
46
|
+
Repository = "https://github.com/zataraine/dethrottled"
|
|
47
|
+
Issues = "https://github.com/zataraine/dethrottled/issues"
|
|
48
|
+
|
|
49
|
+
[project.optional-dependencies]
|
|
50
|
+
# Documents as text: PDF, the Office formats, OpenDocument, EPUB and RTF.
|
|
51
|
+
#
|
|
52
|
+
# Every one of these is a direct parser. MarkItDown was measured as the
|
|
53
|
+
# alternative and rejected: `markitdown[all]` is 375MB across 52 packages and
|
|
54
|
+
# pulls in four Azure SDKs and a neural network (magika) to identify file
|
|
55
|
+
# types -- in a tool whose entire claim is that it needs no account and no key.
|
|
56
|
+
# It also wraps these same libraries, so it would be an abstraction layer over
|
|
57
|
+
# dependencies we already have. File typing here is forty lines of signature
|
|
58
|
+
# comparison and costs under a microsecond on the common path.
|
|
59
|
+
documents = [
|
|
60
|
+
"pymupdf>=1.24",
|
|
61
|
+
"openpyxl>=3.1",
|
|
62
|
+
"python-docx>=1.1",
|
|
63
|
+
"python-pptx>=0.6.23",
|
|
64
|
+
"xlrd>=2.0",
|
|
65
|
+
# OpenDocument. Pure Python (an sdist, but nothing to compile) and what
|
|
66
|
+
# most European public bodies publish in.
|
|
67
|
+
"odfpy>=1.4",
|
|
68
|
+
# An EPUB is a zip of XHTML, read with the same extractor as a web page.
|
|
69
|
+
"ebooklib>=0.18",
|
|
70
|
+
# RTF, tiny and pure Python.
|
|
71
|
+
"striprtf>=0.0.26",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
# Video pages, read as their caption track.
|
|
75
|
+
#
|
|
76
|
+
# Keyless and accountless, but it talks to YouTube's internal endpoint and
|
|
77
|
+
# YouTube blocks datacentre address ranges hard -- so this works from a machine
|
|
78
|
+
# you own and often will not from a cloud host. Separated from `documents` so
|
|
79
|
+
# that limitation is an explicit choice rather than something that arrives
|
|
80
|
+
# with a spreadsheet parser.
|
|
81
|
+
media = ["youtube-transcript-api>=1.0"]
|
|
82
|
+
# The semantic corpus and the multilingual reranker. Also needs weights --
|
|
83
|
+
# see scripts/fetch-models.sh.
|
|
84
|
+
semantic = [
|
|
85
|
+
"numpy>=1.24",
|
|
86
|
+
"onnxruntime>=1.17",
|
|
87
|
+
"transformers>=4.40",
|
|
88
|
+
]
|
|
89
|
+
# The English cross-encoder. flashrank downloads its own model on first use.
|
|
90
|
+
rerank = ["flashrank>=0.2.9"]
|
|
91
|
+
# A real Chrome TLS fingerprint without a browser. Worth its place: it is the
|
|
92
|
+
# only thing that got past one host's blocking in testing, and it is FASTER
|
|
93
|
+
# than plain requests, not slower. aarch64 wheels available.
|
|
94
|
+
tls = ["curl_cffi>=0.7"]
|
|
95
|
+
all = ["dethrottled[documents,media,semantic,rerank,tls]"]
|
|
96
|
+
dev = ["pytest>=8.0", "httpx>=0.27", "ruff>=0.4"]
|
|
97
|
+
|
|
98
|
+
[project.scripts]
|
|
99
|
+
dethrottled = "dethrottled.server:main"
|
|
100
|
+
dethrottled-health = "dethrottled.health:main"
|
|
101
|
+
|
|
102
|
+
[tool.setuptools.packages.find]
|
|
103
|
+
where = ["src"]
|
|
104
|
+
|
|
105
|
+
[tool.pytest.ini_options]
|
|
106
|
+
testpaths = ["tests"]
|
|
107
|
+
markers = [
|
|
108
|
+
"network: hits the live internet; excluded from CI with -m 'not network'",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
[tool.ruff]
|
|
112
|
+
# 100, not 88. The bulk of this code sits comfortably under 80; the exceptions
|
|
113
|
+
# are all format strings that read worse broken across two lines than they do
|
|
114
|
+
# slightly wide. Picking a limit the code already meets beats reformatting
|
|
115
|
+
# working code to satisfy a number.
|
|
116
|
+
line-length = 100
|
|
117
|
+
target-version = "py310"
|
|
118
|
+
|
|
119
|
+
[tool.ruff.lint]
|
|
120
|
+
select = ["E", "W", "F", "I", "B", "PIE", "RUF"]
|
|
121
|
+
ignore = [
|
|
122
|
+
# This codebase uses %-formatting throughout, deliberately and
|
|
123
|
+
# consistently. Rewriting 52 call sites to f-strings would be churn, and a
|
|
124
|
+
# half-converted file is worse than either style applied whole.
|
|
125
|
+
"UP031",
|
|
126
|
+
# Broad excepts are the design here, not an oversight: every tier, every
|
|
127
|
+
# extractor and every optional model is meant to fail soft and degrade to
|
|
128
|
+
# the next thing. They are individually commented where the reason is not
|
|
129
|
+
# obvious.
|
|
130
|
+
"BLE001", "S110", "S112",
|
|
131
|
+
# zip() without strict= is fine where the two sequences are built together
|
|
132
|
+
# three lines above.
|
|
133
|
+
"B905",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
[tool.ruff.lint.per-file-ignores]
|
|
137
|
+
# Tests unpack tuples they only half-use, and say so.
|
|
138
|
+
"tests/*" = ["RUF059"]
|