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.
Files changed (47) hide show
  1. dethrottled-0.1.1/LICENSE +21 -0
  2. dethrottled-0.1.1/PKG-INFO +267 -0
  3. dethrottled-0.1.1/README.md +211 -0
  4. dethrottled-0.1.1/pyproject.toml +138 -0
  5. dethrottled-0.1.1/setup.cfg +4 -0
  6. dethrottled-0.1.1/src/dethrottled/__init__.py +2 -0
  7. dethrottled-0.1.1/src/dethrottled/__main__.py +4 -0
  8. dethrottled-0.1.1/src/dethrottled/_quiet.py +122 -0
  9. dethrottled-0.1.1/src/dethrottled/cache.py +123 -0
  10. dethrottled-0.1.1/src/dethrottled/corpus.py +436 -0
  11. dethrottled-0.1.1/src/dethrottled/documents.py +372 -0
  12. dethrottled-0.1.1/src/dethrottled/domains.py +275 -0
  13. dethrottled-0.1.1/src/dethrottled/extract.py +258 -0
  14. dethrottled-0.1.1/src/dethrottled/fetch.py +1042 -0
  15. dethrottled-0.1.1/src/dethrottled/health.py +250 -0
  16. dethrottled-0.1.1/src/dethrottled/media.py +142 -0
  17. dethrottled-0.1.1/src/dethrottled/ocr.py +250 -0
  18. dethrottled-0.1.1/src/dethrottled/paths.py +58 -0
  19. dethrottled-0.1.1/src/dethrottled/rank.py +334 -0
  20. dethrottled-0.1.1/src/dethrottled/search.py +614 -0
  21. dethrottled-0.1.1/src/dethrottled/server.py +447 -0
  22. dethrottled-0.1.1/src/dethrottled.egg-info/PKG-INFO +267 -0
  23. dethrottled-0.1.1/src/dethrottled.egg-info/SOURCES.txt +45 -0
  24. dethrottled-0.1.1/src/dethrottled.egg-info/dependency_links.txt +1 -0
  25. dethrottled-0.1.1/src/dethrottled.egg-info/entry_points.txt +3 -0
  26. dethrottled-0.1.1/src/dethrottled.egg-info/requires.txt +40 -0
  27. dethrottled-0.1.1/src/dethrottled.egg-info/top_level.txt +1 -0
  28. dethrottled-0.1.1/tests/test_cache.py +110 -0
  29. dethrottled-0.1.1/tests/test_canonical.py +55 -0
  30. dethrottled-0.1.1/tests/test_concurrency.py +192 -0
  31. dethrottled-0.1.1/tests/test_contract.py +156 -0
  32. dethrottled-0.1.1/tests/test_cooldown.py +96 -0
  33. dethrottled-0.1.1/tests/test_corpus.py +247 -0
  34. dethrottled-0.1.1/tests/test_documents.py +94 -0
  35. dethrottled-0.1.1/tests/test_documents_odf.py +188 -0
  36. dethrottled-0.1.1/tests/test_documents_parsers.py +162 -0
  37. dethrottled-0.1.1/tests/test_domains.py +167 -0
  38. dethrottled-0.1.1/tests/test_endpoints.py +117 -0
  39. dethrottled-0.1.1/tests/test_extract.py +105 -0
  40. dethrottled-0.1.1/tests/test_fetch_internals.py +193 -0
  41. dethrottled-0.1.1/tests/test_health.py +176 -0
  42. dethrottled-0.1.1/tests/test_media.py +182 -0
  43. dethrottled-0.1.1/tests/test_ocr.py +187 -0
  44. dethrottled-0.1.1/tests/test_quiet.py +118 -0
  45. dethrottled-0.1.1/tests/test_rank.py +108 -0
  46. dethrottled-0.1.1/tests/test_search_internals.py +174 -0
  47. 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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ """Zero-API web search, fetch and extraction."""
2
+ __version__ = "0.1.1"
@@ -0,0 +1,4 @@
1
+ from .server import main
2
+
3
+ if __name__ == "__main__":
4
+ main()