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