webless 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,241 @@
1
+ Metadata-Version: 2.4
2
+ Name: webless
3
+ Version: 0.1.0
4
+ Summary: Web search and page fetching with no API keys: multi-engine search with rank fusion, and HTML to Markdown
5
+ Keywords: search,web-search,scraping,duckduckgo,markdown,html-to-markdown,no-api-key
6
+ Author: Ahmed Saqr
7
+ Author-email: Ahmed Saqr <ahmedhassansaqr28@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Internet :: WWW/HTTP
18
+ Classifier: Topic :: Text Processing :: Markup :: HTML
19
+ Classifier: Typing :: Typed
20
+ Requires-Dist: httpx>=0.27
21
+ Requires-Python: >=3.10
22
+ Project-URL: Homepage, https://github.com/ahmedhassan456/webless
23
+ Project-URL: Repository, https://github.com/ahmedhassan456/webless
24
+ Project-URL: Issues, https://github.com/ahmedhassan456/webless/issues
25
+ Description-Content-Type: text/markdown
26
+
27
+ # webless
28
+
29
+ Web search and page fetching for Python, with **no API keys**.
30
+
31
+ `pip install webless` and the first call works. There is no account to
32
+ create, no key to configure, no quota to track, and no paid tier — every
33
+ back end is a public endpoint.
34
+
35
+ ```python
36
+ import asyncio
37
+ from webless import search, fetch
38
+
39
+ async def main():
40
+ result = await search("reciprocal rank fusion")
41
+ for hit in result[:3]:
42
+ print(hit.title, "—", hit.url)
43
+
44
+ page = await fetch(result[0].url)
45
+ print(page.content[:500])
46
+
47
+ asyncio.run(main())
48
+ ```
49
+
50
+ No event loop of your own? Use `search_sync` and `fetch_sync`, which take the
51
+ same arguments.
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install webless
57
+ ```
58
+
59
+ The only dependency is `httpx`. HTML parsing, content extraction, and Markdown
60
+ rendering are all written against the standard library, so there is no
61
+ `lxml`, no `beautifulsoup4`, and no compiler needed to install.
62
+
63
+ ## `search`
64
+
65
+ ```python
66
+ result = await search(
67
+ "sqlite write-ahead logging",
68
+ limit=10,
69
+ wide=False,
70
+ allowed_domains=["sqlite.org"],
71
+ blocked_domains=["pinterest.com"],
72
+ timeout=12.0,
73
+ )
74
+ ```
75
+
76
+ `search` queries several engines **concurrently** and fuses their rankings.
77
+ The general set is DuckDuckGo, Mojeek, Bing, and Wikipedia; `wide=True` adds
78
+ Marginalia's small-web index and Hacker News, which helps on niche or
79
+ discussion-oriented queries and costs some latency on ordinary ones.
80
+
81
+ Fusion is Reciprocal Rank Fusion: a result scores `Σ 1 / (60 + rank)` over the
82
+ engines that returned it. Rank is all it needs, which is the point — the
83
+ engines disagree about scoring and none exposes a comparable number, but they
84
+ all produce an ordered list, and a page several independent indexes rank
85
+ highly is a better answer than any one index's favourite. The fused ranking is
86
+ then reweighted by how much of the query actually appears in each result, so
87
+ an engine answering loosely cannot outrank one answering the question.
88
+
89
+ `allowed_domains` and `blocked_domains` filter on host, and an entry covers
90
+ its subdomains: `python.org` keeps `docs.python.org`.
91
+
92
+ ### Results
93
+
94
+ `SearchResult` iterates, indexes, and measures like the list of hits it
95
+ carries, and also tells you what happened:
96
+
97
+ ```python
98
+ result.hits # list[SearchHit]
99
+ result.succeeded # ["duckduckgo", "bing"]
100
+ result.failed # {"mojeek": "returned a page with no results, likely a soft block."}
101
+ result.ok # at least one engine answered
102
+
103
+ hit.title, hit.url, hit.snippet, hit.host, hit.engines, hit.score
104
+ ```
105
+
106
+ **A blocked engine is reported, not raised.** Public endpoints rate-limit,
107
+ soft-block, and serve challenge pages, and they do it differently from
108
+ different IPs. Every engine is given the same deadline and none can hold up
109
+ the others: a failure is recorded against its name and the rest of the ranking
110
+ stands. Only when all of them fail do you get an empty ranking — with the
111
+ reasons attached.
112
+
113
+ A soft block is detected rather than mistaken for an empty index. A results
114
+ page that returns HTTP 200 and parses to zero rows, or answers with a
115
+ challenge status, is reported as that engine failing.
116
+
117
+ ## `fetch`
118
+
119
+ ```python
120
+ page = await fetch(
121
+ "https://docs.python.org/3/library/asyncio.html",
122
+ format="markdown",
123
+ full_page=False,
124
+ max_chars=None,
125
+ offset=0,
126
+ )
127
+ ```
128
+
129
+ `fetch` returns the page's readable content, not its markup. A page's HTML is
130
+ mostly navigation, scripts, and layout, so the default trims to the article
131
+ body and renders headings, lists, code blocks, tables, and links as Markdown —
132
+ small enough to read or feed to a model, structured enough to still quote from
133
+ and follow links out of. Relative links are absolutised against the final URL.
134
+
135
+ - `format="markdown"` — main content as Markdown (default)
136
+ - `format="text"` — plain text, no markup
137
+ - `format="raw"` — the untouched response body
138
+ - `full_page=True` — keep navigation, sidebars, and footers, for when the part
139
+ you want *is* the chrome (a docs index, a link list)
140
+ - `max_chars` / `offset` — read a long page in sections; `page.truncated` says
141
+ whether anything was left behind
142
+
143
+ JSON responses are returned pretty-printed.
144
+
145
+ ```python
146
+ page.content # the rendered text
147
+ page.title # <og:title>, else <title>
148
+ page.description # meta description
149
+ page.url # what you asked for
150
+ page.final_url # where you ended up
151
+ page.redirected # whether those differ
152
+ page.status, page.content_type, page.host, page.truncated
153
+ ```
154
+
155
+ Already holding a response from your own client or a cache? `render(response,
156
+ format=..., full_page=...)` does the same conversion with no network call.
157
+
158
+ ## Not blocking
159
+
160
+ Two senses, both deliberate.
161
+
162
+ **Nothing blocks anything else.** Every call is async with a hard timeout. A
163
+ search is several concurrent requests, so a slow engine delays its own result
164
+ and nothing more.
165
+
166
+ **Requests are hard to block.** Hosts refuse on fingerprint reputation, so a
167
+ retry after a 403 or 429 presents a *different* browser user agent rather than
168
+ repeating the one just refused — which clears most transient blocks on its
169
+ own. Retries back off exponentially with jitter.
170
+
171
+ ## Not reachable inward
172
+
173
+ A URL that resolves to a private, loopback, link-local, or reserved address is
174
+ refused before the request is sent — and again on every redirect hop, since
175
+ redirects are followed by hand for exactly that reason. A URL from a search
176
+ result, a user, or a model cannot be steered into your own network.
177
+
178
+ ```python
179
+ await fetch("http://169.254.169.254/latest/meta-data/")
180
+ # WebError: ... resolves to a private or loopback address; refusing to fetch it
181
+ ```
182
+
183
+ Response bodies are capped, and redirect chains are limited to five hops.
184
+
185
+ ## Command line
186
+
187
+ ```bash
188
+ webless search "structured concurrency python" -n 5
189
+ webless search "rust async traits" --wide --allow rust-lang.org
190
+ webless fetch https://peps.python.org/pep-3156/ > pep.md
191
+ webless fetch https://api.github.com/repos/python/cpython --json | jq .status
192
+ ```
193
+
194
+ `--json` on either subcommand prints the full record, so it composes with
195
+ `jq`. `search` exits non-zero when every engine failed.
196
+
197
+ ## Engines
198
+
199
+ | engine | how | notes |
200
+ |---|---|---|
201
+ | DuckDuckGo | scrape | lite view, falls back to the html view |
202
+ | Mojeek | scrape | independent index |
203
+ | Bing | RSS, then scrape | tracker URLs decoded |
204
+ | Wikipedia | OpenSearch API | |
205
+ | Marginalia | public API | `wide` only; small-web index |
206
+ | Hacker News | Algolia API | `wide` only; discussion |
207
+
208
+ Pick your own set with `engines=`:
209
+
210
+ ```python
211
+ from webless import search, DuckDuckGoEngine, MojeekEngine
212
+
213
+ result = await search("query", engines=(DuckDuckGoEngine, MojeekEngine))
214
+ ```
215
+
216
+ A custom engine is any subclass of `Engine` with a `name` and an
217
+ `async search(query, limit, timeout) -> list[SearchHit]`.
218
+
219
+ ## Honest caveats
220
+
221
+ These are public endpoints being read by a program, which has consequences
222
+ worth knowing before you build on them:
223
+
224
+ - **Availability varies by IP.** Mojeek soft-blocks some address ranges
225
+ outright; Marginalia's public key is shared and rate-limits. This is why
226
+ failures are per-engine and reported rather than fatal.
227
+ - **Bing occasionally serves results for an unrelated query** — the same
228
+ response carries the right query in its metadata and the wrong results in
229
+ its body. The relevance reweighting demotes them, but on a query where Bing
230
+ is the only engine answering you may see them.
231
+ - **Scrapers track markup.** When an engine changes its results page, its
232
+ parser needs updating; the soft-block detection turns that into a reported
233
+ failure rather than silence.
234
+
235
+ ## Requirements
236
+
237
+ Python 3.10+. `httpx` 0.27+.
238
+
239
+ ## License
240
+
241
+ MIT
@@ -0,0 +1,14 @@
1
+ webless/__init__.py,sha256=4Kva_7KtdQJ5yiehKZQU4OnoziQSbDhQsWLGPZpL9Z8,2056
2
+ webless/_engines.py,sha256=PHNXWxifdwYb3q219DSpWWi8-5a5-lbGoeRbGonbbS4,21497
3
+ webless/_fetch.py,sha256=A2vnwKJAzzapZETGbUJlYSW8XjX6UJBANRKawdZHXRs,4501
4
+ webless/_html.py,sha256=Lj3bVVWsV2PPDfJpvaw7h2vlAePVU17lPUjwFt1q1Jw,16995
5
+ webless/_http.py,sha256=GJ8gd05xXuqo9NGdQQBYpBASS5apW49iFF5KLN_-FLk,9568
6
+ webless/_search.py,sha256=nL19gri01A9lWbUMSBRt9PZ0zIMM574YIm8RkU-pRlU,2771
7
+ webless/_sync.py,sha256=OsioPbu1koSWzHlxk1Li4D8kQkW1u99GDql5LZWtI5k,1404
8
+ webless/cli.py,sha256=HH_3GJ9iGq3E-TEJ05rbmbixC_4mSexYOdMfdCXcJfw,4128
9
+ webless/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ webless-0.1.0.dist-info/licenses/LICENSE,sha256=oAwCYlBHADbqgD77B-tz1XlWRsrvn_0qcSBHCkZROjg,1067
11
+ webless-0.1.0.dist-info/WHEEL,sha256=0ksFqNncQTsCUyOQQNfyYN3tci86LvOLl92eMRFTR70,81
12
+ webless-0.1.0.dist-info/entry_points.txt,sha256=KiV26Yf63G_wDh80NkTxDQ48UDKH6Fy1tQU2FfEz38Y,46
13
+ webless-0.1.0.dist-info/METADATA,sha256=wFTcd-sFaEedA05aM69WMh0BXAH3TyPYpo2niBmdBaU,8725
14
+ webless-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.22
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ webless = webless.cli:main
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ahmed Saqr
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.