webget-cli 0.12.0__tar.gz → 0.12.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.
- {webget_cli-0.12.0 → webget_cli-0.12.1}/PKG-INFO +1 -1
- {webget_cli-0.12.0 → webget_cli-0.12.1}/pyproject.toml +1 -1
- webget_cli-0.12.1/tests/test_base64_strip.py +55 -0
- webget_cli-0.12.1/tests/test_mcp_metadata.py +35 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_metadata.py +2 -28
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget/http.py +25 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget_cli.egg-info/PKG-INFO +1 -1
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget_cli.egg-info/SOURCES.txt +2 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/LICENSE +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/README.md +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/setup.cfg +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_adversarial_auth.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_adversarial_cache.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_adversarial_concurrency.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_adversarial_http.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_adversarial_mcp.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_adversarial_ssrf.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_auth_review.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_browser_ssrf.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_cache_review.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_concurrency_review.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_discovery_map.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_extraction_markdown.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_firecrawl_policy.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_integration_ladder.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_ladder_retry.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_login_flow.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_mcp_leak_review.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_mcp_map.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_mcp_profile.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_mcp_server.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_mcp_smoke.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_nonhtml.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_security_review.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_size_review.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_ssrf_dual_dns.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/tests/test_webget.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget/__init__.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget/cache.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget/cli.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget/discovery.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget/firecrawl.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget/ladder.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget/profile.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget/search.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget/ssrf.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget_cli.egg-info/dependency_links.txt +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget_cli.egg-info/entry_points.txt +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget_cli.egg-info/requires.txt +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget_cli.egg-info/top_level.txt +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget_cli.py +0 -0
- {webget_cli-0.12.0 → webget_cli-0.12.1}/webget_mcp.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: webget-cli
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.1
|
|
4
4
|
Summary: Local search + scrape CLI with an HTTP fast path, optional browser fallback, and authenticated session profiles. Zero API keys.
|
|
5
5
|
Author-email: David Tarigan <tarigansdavid@gmail.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "webget-cli"
|
|
7
|
-
version = "0.12.
|
|
7
|
+
version = "0.12.1"
|
|
8
8
|
description = "Local search + scrape CLI with an HTTP fast path, optional browser fallback, and authenticated session profiles. Zero API keys."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Ide #3: oversized base64 image payloads are stripped from extracted markdown.
|
|
2
|
+
|
|
3
|
+
Bug scope (probed 2026-09-16, see .hermes/plans/2026-09-16_webget-ide03-base64-strip.md):
|
|
4
|
+
the markdownify fallback keeps inline `data:image/...;base64,...` URLs verbatim,
|
|
5
|
+
so a single hero image can carry hundreds of KB into the output. The trafilatura
|
|
6
|
+
path drops images entirely, so only the fallback needs the strip.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import webget_cli as webget
|
|
10
|
+
|
|
11
|
+
BIG_PAYLOAD = "iVBORw0KGgo" + "A" * 800 # > _BASE64_PAYLOAD_MIN
|
|
12
|
+
SMALL_PAYLOAD = "B" * 120 # <= threshold, must survive
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _page(payload):
|
|
16
|
+
return (
|
|
17
|
+
"<html><head><title>T</title></head><body><article><h1>T</h1>"
|
|
18
|
+
f'<p><img alt="hero shot" src="data:image/png;base64,{payload}"></p>'
|
|
19
|
+
"<p>" + ("Body text filler words here to satisfy the length floor. " * 6) + "</p>"
|
|
20
|
+
"</article></body></html>"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _fallback_only(monkeypatch):
|
|
25
|
+
"""Force the markdownify path (trafilatura returns None)."""
|
|
26
|
+
import trafilatura
|
|
27
|
+
|
|
28
|
+
monkeypatch.setattr(trafilatura, "extract", lambda *a, **k: None)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestBase64Strip:
|
|
32
|
+
def test_big_payload_stripped_alt_and_mime_survive(self, monkeypatch):
|
|
33
|
+
_fallback_only(monkeypatch)
|
|
34
|
+
text, _ = webget._extract_with_metadata(_page(BIG_PAYLOAD))
|
|
35
|
+
assert BIG_PAYLOAD not in text
|
|
36
|
+
assert "data:image/png;base64,stripped" in text
|
|
37
|
+
assert "hero shot" in text
|
|
38
|
+
|
|
39
|
+
def test_small_payload_preserved(self, monkeypatch):
|
|
40
|
+
_fallback_only(monkeypatch)
|
|
41
|
+
text, _ = webget._extract_with_metadata(_page(SMALL_PAYLOAD))
|
|
42
|
+
assert SMALL_PAYLOAD in text
|
|
43
|
+
|
|
44
|
+
def test_threshold_boundary(self, monkeypatch):
|
|
45
|
+
_fallback_only(monkeypatch)
|
|
46
|
+
at_limit = "C" * 200
|
|
47
|
+
just_over = "D" * 201
|
|
48
|
+
assert at_limit in webget._extract_with_metadata(_page(at_limit))[0]
|
|
49
|
+
assert just_over not in webget._extract_with_metadata(_page(just_over))[0]
|
|
50
|
+
|
|
51
|
+
def test_extract_markdown_contract_unchanged(self, monkeypatch):
|
|
52
|
+
_fallback_only(monkeypatch)
|
|
53
|
+
out = webget._extract_markdown(_page(BIG_PAYLOAD))
|
|
54
|
+
assert isinstance(out, str)
|
|
55
|
+
assert BIG_PAYLOAD not in out
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Ide #1: metadata passthrough in the MCP tool layer.
|
|
2
|
+
|
|
3
|
+
Moved out of tests/test_metadata.py: these import webget_mcp, which needs
|
|
4
|
+
fastmcp/mcp, so they run in the mcp-test CI job (unit job has no fastmcp).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestMcpMetadataExposure:
|
|
9
|
+
def test_mcp_fetch_passthrough_has_metadata(self, fresh_cache):
|
|
10
|
+
import asyncio
|
|
11
|
+
|
|
12
|
+
import webget_mcp
|
|
13
|
+
|
|
14
|
+
res = asyncio.run(
|
|
15
|
+
webget_mcp.fetch(fresh_cache.url("/long"), strategy="http", no_cache=True)
|
|
16
|
+
)
|
|
17
|
+
assert set(res["metadata"]) == {"author", "published_at", "site_name", "language"}
|
|
18
|
+
|
|
19
|
+
def test_mcp_search_fetch_rebuild_has_metadata(self, fresh_cache, monkeypatch):
|
|
20
|
+
import asyncio
|
|
21
|
+
|
|
22
|
+
import webget_mcp
|
|
23
|
+
|
|
24
|
+
monkeypatch.setattr(
|
|
25
|
+
webget_mcp.wg,
|
|
26
|
+
"search",
|
|
27
|
+
lambda *a, **k: [{"title": "T", "url": fresh_cache.url("/long"), "snippet": "s"}],
|
|
28
|
+
)
|
|
29
|
+
out = asyncio.run(webget_mcp.search_fetch("q", n=1, no_cache=True))
|
|
30
|
+
assert set(out[0]["metadata"]) == {
|
|
31
|
+
"author",
|
|
32
|
+
"published_at",
|
|
33
|
+
"site_name",
|
|
34
|
+
"language",
|
|
35
|
+
}
|
|
@@ -76,35 +76,9 @@ class TestFetchHttpMetadata:
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
# NOTE: MCP-layer metadata passthrough tests live in tests/test_mcp_metadata.py
|
|
80
|
+
# (they import webget_mcp, which needs fastmcp; that file runs in the mcp-test CI job).
|
|
79
81
|
class TestOutputExposure:
|
|
80
|
-
def test_mcp_fetch_passthrough_has_metadata(self, fresh_cache):
|
|
81
|
-
import asyncio
|
|
82
|
-
|
|
83
|
-
import webget_mcp
|
|
84
|
-
|
|
85
|
-
res = asyncio.run(
|
|
86
|
-
webget_mcp.fetch(fresh_cache.url("/long"), strategy="http", no_cache=True)
|
|
87
|
-
)
|
|
88
|
-
assert set(res["metadata"]) == {"author", "published_at", "site_name", "language"}
|
|
89
|
-
|
|
90
|
-
def test_mcp_search_fetch_rebuild_has_metadata(self, fresh_cache, monkeypatch):
|
|
91
|
-
import asyncio
|
|
92
|
-
|
|
93
|
-
import webget_mcp
|
|
94
|
-
|
|
95
|
-
monkeypatch.setattr(
|
|
96
|
-
webget_mcp.wg,
|
|
97
|
-
"search",
|
|
98
|
-
lambda *a, **k: [{"title": "T", "url": fresh_cache.url("/long"), "snippet": "s"}],
|
|
99
|
-
)
|
|
100
|
-
out = asyncio.run(webget_mcp.search_fetch("q", n=1, no_cache=True))
|
|
101
|
-
assert set(out[0]["metadata"]) == {
|
|
102
|
-
"author",
|
|
103
|
-
"published_at",
|
|
104
|
-
"site_name",
|
|
105
|
-
"language",
|
|
106
|
-
}
|
|
107
|
-
|
|
108
82
|
def test_cli_su_json_rebuild_has_metadata(self, fresh_cache, monkeypatch, capsys):
|
|
109
83
|
import webget_cli as wgcli
|
|
110
84
|
from webget import cli as cli_mod
|
|
@@ -35,6 +35,30 @@ class ResponseTooLarge(Exception):
|
|
|
35
35
|
|
|
36
36
|
_EMPTY_META = {"author": None, "published_at": None, "site_name": None, "language": None}
|
|
37
37
|
|
|
38
|
+
# Inline data:image payloads longer than this (base64 chars) are replaced
|
|
39
|
+
# in extracted markdown; shorter ones are left alone since they cost little
|
|
40
|
+
# and may carry meaningful thumbnails.
|
|
41
|
+
_BASE64_PAYLOAD_MIN = 200
|
|
42
|
+
_DATA_URL_IMAGE_RE = re.compile(r"(data:image/[a-z0-9.+-]+;base64,)([A-Za-z0-9+/=]+)")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _strip_base64_images(text):
|
|
46
|
+
"""Replace oversized inline base64 image payloads with 'stripped'.
|
|
47
|
+
|
|
48
|
+
Pages routinely inline images as data URLs; a single hero image can
|
|
49
|
+
carry hundreds of KB of base64, which the markdownify fallback would
|
|
50
|
+
otherwise pass through verbatim as token-burning noise. Keeps the
|
|
51
|
+
mime prefix and the markdown around the URL (alt text lives outside
|
|
52
|
+
the URL), drops only the payload.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def _repl(m):
|
|
56
|
+
if len(m.group(2)) <= _BASE64_PAYLOAD_MIN:
|
|
57
|
+
return m.group(0)
|
|
58
|
+
return m.group(1) + "stripped"
|
|
59
|
+
|
|
60
|
+
return _DATA_URL_IMAGE_RE.sub(_repl, text)
|
|
61
|
+
|
|
38
62
|
|
|
39
63
|
def _extract_with_metadata(html):
|
|
40
64
|
"""Extract (text, metadata) from HTML.
|
|
@@ -86,6 +110,7 @@ def _extract_with_metadata(html):
|
|
|
86
110
|
# output style (verified differential 2026-08-08) so the fallback
|
|
87
111
|
# stays close to 0.7.2 (semantic parity).
|
|
88
112
|
converted = md(html, bullets="*", heading_style="ATX").strip()
|
|
113
|
+
converted = _strip_base64_images(converted)
|
|
89
114
|
if len(converted) > 50:
|
|
90
115
|
return converted, dict(_EMPTY_META)
|
|
91
116
|
return "", dict(_EMPTY_META)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: webget-cli
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.1
|
|
4
4
|
Summary: Local search + scrape CLI with an HTTP fast path, optional browser fallback, and authenticated session profiles. Zero API keys.
|
|
5
5
|
Author-email: David Tarigan <tarigansdavid@gmail.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -10,6 +10,7 @@ tests/test_adversarial_http.py
|
|
|
10
10
|
tests/test_adversarial_mcp.py
|
|
11
11
|
tests/test_adversarial_ssrf.py
|
|
12
12
|
tests/test_auth_review.py
|
|
13
|
+
tests/test_base64_strip.py
|
|
13
14
|
tests/test_browser_ssrf.py
|
|
14
15
|
tests/test_cache_review.py
|
|
15
16
|
tests/test_concurrency_review.py
|
|
@@ -21,6 +22,7 @@ tests/test_ladder_retry.py
|
|
|
21
22
|
tests/test_login_flow.py
|
|
22
23
|
tests/test_mcp_leak_review.py
|
|
23
24
|
tests/test_mcp_map.py
|
|
25
|
+
tests/test_mcp_metadata.py
|
|
24
26
|
tests/test_mcp_profile.py
|
|
25
27
|
tests/test_mcp_server.py
|
|
26
28
|
tests/test_mcp_smoke.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|