patchtroy 0.5.0__tar.gz → 0.5.2__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.
- {patchtroy-0.5.0 → patchtroy-0.5.2}/Dockerfile +2 -2
- {patchtroy-0.5.0 → patchtroy-0.5.2}/PKG-INFO +8 -6
- {patchtroy-0.5.0 → patchtroy-0.5.2}/PRD.md +2 -2
- {patchtroy-0.5.0 → patchtroy-0.5.2}/README.md +4 -2
- patchtroy-0.5.2/VERSION +1 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/benchmarks/BENCHMARKS.md +1 -1
- {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/benchmarks.md +1 -1
- {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/getting-started.md +6 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/rest-api.md +1 -1
- {patchtroy-0.5.0 → patchtroy-0.5.2}/pyproject.toml +4 -4
- {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/__init__.py +2 -2
- {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/cli.py +21 -3
- {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/models.py +6 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/server.py +2 -2
- {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/utils.py +71 -0
- patchtroy-0.5.2/tests/test_cli.py +137 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/tests/test_server.py +1 -1
- {patchtroy-0.5.0 → patchtroy-0.5.2}/uv.lock +1 -1
- patchtroy-0.5.0/VERSION +0 -1
- patchtroy-0.5.0/tests/test_cli.py +0 -60
- {patchtroy-0.5.0 → patchtroy-0.5.2}/.dockerignore +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/.gitattributes +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/.githooks/pre-commit +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/.githooks/prepare-commit-msg +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/.githooks/versionlib.sh +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/.github/workflows/ci.yml +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/.github/workflows/docs.yml +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/.github/workflows/release.yml +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/.gitignore +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/LICENSE +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/benchmarks/benchmark_results.json +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/benchmarks/run_benchmark.py +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/docker-compose.yml +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/context-pool.md +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/index.md +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/llm-chunking.md +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/media-capture.md +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/proxy-rotation.md +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/stealth-architecture.md +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/mkdocs.yml +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/chunker.py +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/crawler.py +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/extractors.py +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/pool.py +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/proxy.py +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/tests/test_chunker.py +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/tests/test_crawler.py +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/tests/test_extractors.py +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/tests/test_media_capture.py +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/tests/test_models.py +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/tests/test_pool.py +0 -0
- {patchtroy-0.5.0 → patchtroy-0.5.2}/tests/test_proxy.py +0 -0
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
FROM python:3.11-slim
|
|
7
7
|
|
|
8
|
-
LABEL maintainer="
|
|
8
|
+
LABEL maintainer="marcuz-apl <support@alfazen.org>"
|
|
9
9
|
LABEL org.opencontainers.image.title="Patchtroy"
|
|
10
10
|
LABEL org.opencontainers.image.description="Undetected stealth web scraper & markdown extractor microservice"
|
|
11
|
-
LABEL org.opencontainers.image.version="0.5.
|
|
11
|
+
LABEL org.opencontainers.image.version="0.5.2"
|
|
12
12
|
|
|
13
13
|
# Python and environment settings
|
|
14
14
|
ENV PYTHONUNBUFFERED=1 \
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: patchtroy
|
|
3
|
-
Version: 0.5.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.5.2
|
|
4
|
+
Summary: Stealth web scraper & markdown extractor for LLMs, pairing Patchright with Trafilatura.
|
|
5
5
|
Project-URL: Homepage, https://github.com/marcuz-apl/patchtroy
|
|
6
6
|
Project-URL: Repository, https://github.com/marcuz-apl/patchtroy
|
|
7
7
|
Project-URL: Issues, https://github.com/marcuz-apl/patchtroy/issues
|
|
8
|
-
Author-email:
|
|
9
|
-
Maintainer-email: "Alfazen Inc." <support@alfazen.
|
|
8
|
+
Author-email: marcuz-apl <support@alfazen.org>
|
|
9
|
+
Maintainer-email: "Alfazen Inc." <support@alfazen.org>
|
|
10
10
|
License-Expression: Apache-2.0
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Keywords: crawler,llm,markdown,patchright,playwright,rag,scraper,stealth,trafilatura,web-scraping
|
|
@@ -117,8 +117,10 @@ print(result.markdown[:300])
|
|
|
117
117
|
# Scrape clean Markdown directly to stdout
|
|
118
118
|
patchtroy https://news.ycombinator.com
|
|
119
119
|
|
|
120
|
-
# Save to
|
|
120
|
+
# Save to Markdown, CSV, or JSON (auto-detected from file extension)
|
|
121
121
|
patchtroy https://example.com -o output.md
|
|
122
|
+
patchtroy https://example.com -o output.csv
|
|
123
|
+
patchtroy https://example.com -f json -o output.json
|
|
122
124
|
```
|
|
123
125
|
|
|
124
126
|
---
|
|
@@ -150,4 +152,4 @@ Full documentation, API references, architecture guides, and benchmarks are avai
|
|
|
150
152
|
|
|
151
153
|
## 📄 License
|
|
152
154
|
|
|
153
|
-
Patchtroy is open-source software created by
|
|
155
|
+
Patchtroy is open-source software created by [@marcuz-apl](https://github.com/marcuz-apl) and maintained by **Alfazen Inc.**, released under the **[Apache 2.0 License](LICENSE)**.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Document Version**: 1.0.0
|
|
4
4
|
**Status**: Active / Approved
|
|
5
|
-
**Author**:
|
|
5
|
+
**Author**: marcuz-apl / Patchtroy Project
|
|
6
6
|
**Target Category**: Developer Tools / AI & LLM Data Engineering
|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -152,7 +152,7 @@ Patchtroy occupies the unoccupied **"Lightweight Stealth Sweet Spot"**:
|
|
|
152
152
|
- [x] Token counter & LLM chunking utility (`result.chunk(max_tokens=2048)`).
|
|
153
153
|
- [x] Full test suite (32 tests passing).
|
|
154
154
|
|
|
155
|
-
### Phase 4 (v0.5.
|
|
155
|
+
### Phase 4 (v0.5.2 — Current)
|
|
156
156
|
- [x] PyPI public release workflows & multi-OS matrix CI (`.github/workflows/`).
|
|
157
157
|
- [x] Comprehensive benchmark publication vs Crawl4AI and Firecrawl (`benchmarks/BENCHMARKS.md`).
|
|
158
158
|
- [x] Official documentation portal (Material for MkDocs).
|
|
@@ -72,8 +72,10 @@ print(result.markdown[:300])
|
|
|
72
72
|
# Scrape clean Markdown directly to stdout
|
|
73
73
|
patchtroy https://news.ycombinator.com
|
|
74
74
|
|
|
75
|
-
# Save to
|
|
75
|
+
# Save to Markdown, CSV, or JSON (auto-detected from file extension)
|
|
76
76
|
patchtroy https://example.com -o output.md
|
|
77
|
+
patchtroy https://example.com -o output.csv
|
|
78
|
+
patchtroy https://example.com -f json -o output.json
|
|
77
79
|
```
|
|
78
80
|
|
|
79
81
|
---
|
|
@@ -105,4 +107,4 @@ Full documentation, API references, architecture guides, and benchmarks are avai
|
|
|
105
107
|
|
|
106
108
|
## 📄 License
|
|
107
109
|
|
|
108
|
-
Patchtroy is open-source software created by
|
|
110
|
+
Patchtroy is open-source software created by [@marcuz-apl](https://github.com/marcuz-apl) and maintained by **Alfazen Inc.**, released under the **[Apache 2.0 License](LICENSE)**.
|
patchtroy-0.5.2/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v0.5.2-2609033
|
|
@@ -19,7 +19,7 @@ Existing open-source solutions force developers to choose between:
|
|
|
19
19
|
|
|
20
20
|
## 2. Feature & Architecture Matrix
|
|
21
21
|
|
|
22
|
-
| Dimension | **Patchtroy** (v0.5.
|
|
22
|
+
| Dimension | **Patchtroy** (v0.5.2) | **Crawl4AI** (v0.9.3) | **Firecrawl** (Self-Hosted) |
|
|
23
23
|
| :--- | :---: | :---: | :---: |
|
|
24
24
|
| **Stealth Engine** | **✅ Native Patchright (C++ CDP masked)** | ❌ Standard Playwright (CDP leaked) | ⚠️ Standard Playwright / Puppeteer |
|
|
25
25
|
| **Dynamic Challenge Resilience** | **✅ Native driver protection** | ❌ Frequently flagged on automated checks | ❌ Blocked unless using paid SaaS proxy |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 📊 Benchmarks vs Crawl4AI and Firecrawl
|
|
2
2
|
|
|
3
|
-
A quantitative, reproducible benchmark comparing **Patchtroy (v0.5.
|
|
3
|
+
A quantitative, reproducible benchmark comparing **Patchtroy (v0.5.2)** against **Crawl4AI (v0.9.3)** and **Firecrawl (Self-Hosted)**.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -186,6 +186,12 @@ patchtroy https://example.com -o output.md
|
|
|
186
186
|
# Save complete JSON payload (markdown, metadata, structured items, links)
|
|
187
187
|
patchtroy https://example.com -f json -o output.json
|
|
188
188
|
|
|
189
|
+
# Export to standard CSV (auto-detected from file extension)
|
|
190
|
+
patchtroy https://example.com -o output.csv
|
|
191
|
+
|
|
192
|
+
# Batch crawl multiple URLs directly to CSV for data pipelines
|
|
193
|
+
patchtroy https://site1.com https://site2.com -f csv -o dataset.csv
|
|
194
|
+
|
|
189
195
|
# Capture screenshot and PDF
|
|
190
196
|
patchtroy https://example.com --screenshot page.png --pdf document.pdf
|
|
191
197
|
|
|
@@ -4,16 +4,16 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "patchtroy"
|
|
7
|
-
version = "0.5.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.5.2"
|
|
8
|
+
description = "Stealth web scraper & markdown extractor for LLMs, pairing Patchright with Trafilatura."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
12
|
authors = [
|
|
13
|
-
{ name = "
|
|
13
|
+
{ name = "marcuz-apl", email = "support@alfazen.org" }
|
|
14
14
|
]
|
|
15
15
|
maintainers = [
|
|
16
|
-
{ name = "Alfazen Inc.", email = "support@alfazen.
|
|
16
|
+
{ name = "Alfazen Inc.", email = "support@alfazen.org" }
|
|
17
17
|
]
|
|
18
18
|
keywords = [
|
|
19
19
|
"crawler",
|
|
@@ -6,8 +6,8 @@ from patchtroy.models import LinkItem, PatchtroyConfig, ScrapeResult
|
|
|
6
6
|
from patchtroy.pool import BrowserContextPool
|
|
7
7
|
from patchtroy.proxy import ProxyItem, ProxyManager
|
|
8
8
|
|
|
9
|
-
__version__ = "0.5.
|
|
10
|
-
__author__ = "
|
|
9
|
+
__version__ = "0.5.2"
|
|
10
|
+
__author__ = "marcuz-apl"
|
|
11
11
|
__license__ = "Apache-2.0"
|
|
12
12
|
__copyright__ = "Copyright 2026 Alfazen Inc."
|
|
13
13
|
|
|
@@ -11,7 +11,7 @@ from patchtroy.crawler import Patchtroy
|
|
|
11
11
|
from patchtroy.models import PatchtroyConfig
|
|
12
12
|
from patchtroy.utils import silence_windows_proactor_bug
|
|
13
13
|
|
|
14
|
-
__version__ = "0.5.
|
|
14
|
+
__version__ = "0.5.2"
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -42,9 +42,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
42
42
|
parser.add_argument(
|
|
43
43
|
"-f",
|
|
44
44
|
"--format",
|
|
45
|
-
choices=["markdown", "json", "html"],
|
|
45
|
+
choices=["markdown", "json", "html", "csv"],
|
|
46
46
|
default="markdown",
|
|
47
|
-
help="Output format (default: markdown)",
|
|
47
|
+
help="Output format (default: markdown, or inferred from -o extension)",
|
|
48
48
|
)
|
|
49
49
|
parser.add_argument(
|
|
50
50
|
"--wait-for",
|
|
@@ -101,6 +101,16 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
101
101
|
|
|
102
102
|
args = parser.parse_args(argv)
|
|
103
103
|
|
|
104
|
+
# Auto-detect format from output file extension if left at default markdown
|
|
105
|
+
if args.output and args.format == "markdown":
|
|
106
|
+
ext = Path(args.output).suffix.lower()
|
|
107
|
+
if ext == ".csv":
|
|
108
|
+
args.format = "csv"
|
|
109
|
+
elif ext == ".json":
|
|
110
|
+
args.format = "json"
|
|
111
|
+
elif ext in (".html", ".htm"):
|
|
112
|
+
args.format = "html"
|
|
113
|
+
|
|
104
114
|
# Launch REST microservice if requested
|
|
105
115
|
if args.serve or (args.urls and args.urls[0] == "serve"):
|
|
106
116
|
from patchtroy.server import run_server
|
|
@@ -149,6 +159,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
149
159
|
content = json.dumps(dump_data, indent=2, ensure_ascii=False)
|
|
150
160
|
elif args.format == "html":
|
|
151
161
|
content = result.html
|
|
162
|
+
elif args.format == "csv":
|
|
163
|
+
from patchtroy.utils import results_to_csv
|
|
164
|
+
content = results_to_csv(result)
|
|
152
165
|
else:
|
|
153
166
|
header = f"# {result.title}\n\nSource: {result.url}\n\n" if result.title else ""
|
|
154
167
|
content = header + result.markdown
|
|
@@ -176,6 +189,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
176
189
|
for r in results
|
|
177
190
|
]
|
|
178
191
|
content = json.dumps(dump_data, indent=2, ensure_ascii=False)
|
|
192
|
+
elif args.format == "csv":
|
|
193
|
+
from patchtroy.utils import results_to_csv
|
|
194
|
+
content = results_to_csv(results)
|
|
195
|
+
elif args.format == "html":
|
|
196
|
+
content = "\n\n<hr/>\n\n".join(r.html for r in results if r.html)
|
|
179
197
|
else:
|
|
180
198
|
combined = []
|
|
181
199
|
for r in results:
|
|
@@ -152,3 +152,9 @@ class ScrapeResult(BaseModel):
|
|
|
152
152
|
overlap_tokens=overlap_tokens,
|
|
153
153
|
metadata={"url": self.url, "title": self.title},
|
|
154
154
|
)
|
|
155
|
+
|
|
156
|
+
def to_csv(self) -> str:
|
|
157
|
+
"""Export this ScrapeResult as an RFC 4180 CSV string."""
|
|
158
|
+
from patchtroy.utils import results_to_csv
|
|
159
|
+
return results_to_csv(self)
|
|
160
|
+
|
|
@@ -90,7 +90,7 @@ def create_app() -> FastAPI:
|
|
|
90
90
|
app = FastAPI(
|
|
91
91
|
title="Patchtroy REST Microservice",
|
|
92
92
|
description="Undetected stealth web scraper & clean Markdown extractor for LLMs.",
|
|
93
|
-
version="0.5.
|
|
93
|
+
version="0.5.2",
|
|
94
94
|
lifespan=lifespan,
|
|
95
95
|
)
|
|
96
96
|
|
|
@@ -99,7 +99,7 @@ def create_app() -> FastAPI:
|
|
|
99
99
|
"""Microservice health and status check."""
|
|
100
100
|
return {
|
|
101
101
|
"status": "healthy",
|
|
102
|
-
"version": "0.5.
|
|
102
|
+
"version": "0.5.2",
|
|
103
103
|
"engine": "patchright",
|
|
104
104
|
"active": crawler_instance is not None and crawler_instance._pool.is_running,
|
|
105
105
|
}
|
|
@@ -103,3 +103,74 @@ def silence_subprocess_transport_bug() -> None:
|
|
|
103
103
|
|
|
104
104
|
# Backwards-compatible alias
|
|
105
105
|
silence_windows_proactor_bug = silence_subprocess_transport_bug
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def results_to_csv(results: Any) -> str:
|
|
109
|
+
"""Convert one or multiple ScrapeResult instances into standard RFC 4180 CSV text.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
results: A single ScrapeResult or an iterable/list of ScrapeResult instances.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
A valid CSV string containing header and data rows.
|
|
116
|
+
"""
|
|
117
|
+
import csv
|
|
118
|
+
import io
|
|
119
|
+
|
|
120
|
+
if not isinstance(results, (list, tuple)):
|
|
121
|
+
items = [results]
|
|
122
|
+
else:
|
|
123
|
+
items = list(results)
|
|
124
|
+
|
|
125
|
+
fieldnames = [
|
|
126
|
+
"url",
|
|
127
|
+
"success",
|
|
128
|
+
"status_code",
|
|
129
|
+
"title",
|
|
130
|
+
"author",
|
|
131
|
+
"date",
|
|
132
|
+
"token_count",
|
|
133
|
+
"elapsed_s",
|
|
134
|
+
"engine_used",
|
|
135
|
+
"markdown",
|
|
136
|
+
"error",
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
output = io.StringIO()
|
|
140
|
+
writer = csv.DictWriter(
|
|
141
|
+
output,
|
|
142
|
+
fieldnames=fieldnames,
|
|
143
|
+
quoting=csv.QUOTE_MINIMAL,
|
|
144
|
+
lineterminator="\n",
|
|
145
|
+
)
|
|
146
|
+
writer.writeheader()
|
|
147
|
+
|
|
148
|
+
for r in items:
|
|
149
|
+
meta = getattr(r, "metadata", {}) or {}
|
|
150
|
+
author = meta.get("author") or meta.get("byline") or ""
|
|
151
|
+
date = meta.get("date") or meta.get("published_time") or ""
|
|
152
|
+
token_count = meta.get("token_count", 0)
|
|
153
|
+
markdown_str = getattr(r, "markdown", "") or ""
|
|
154
|
+
if not token_count and markdown_str:
|
|
155
|
+
try:
|
|
156
|
+
from patchtroy.chunker import count_tokens
|
|
157
|
+
token_count = count_tokens(markdown_str)
|
|
158
|
+
except Exception:
|
|
159
|
+
token_count = len(markdown_str) // 4
|
|
160
|
+
|
|
161
|
+
writer.writerow({
|
|
162
|
+
"url": getattr(r, "url", ""),
|
|
163
|
+
"success": getattr(r, "success", True),
|
|
164
|
+
"status_code": getattr(r, "status_code", 200),
|
|
165
|
+
"title": getattr(r, "title", "") or "",
|
|
166
|
+
"author": author,
|
|
167
|
+
"date": date,
|
|
168
|
+
"token_count": token_count,
|
|
169
|
+
"elapsed_s": round(float(getattr(r, "elapsed_s", 0.0) or 0.0), 3),
|
|
170
|
+
"engine_used": getattr(r, "engine_used", "patchright"),
|
|
171
|
+
"markdown": markdown_str,
|
|
172
|
+
"error": getattr(r, "error", "") or "",
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
return output.getvalue()
|
|
176
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Tests for Patchtroy CLI."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
from patchtroy.cli import main
|
|
6
|
+
from patchtroy.models import ScrapeResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_cli_help(capsys):
|
|
10
|
+
ret = main([])
|
|
11
|
+
assert ret == 1
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_cli_single_url():
|
|
15
|
+
fake_result = ScrapeResult(
|
|
16
|
+
url="https://example.com",
|
|
17
|
+
title="Test Page",
|
|
18
|
+
markdown="This is test markdown.",
|
|
19
|
+
success=True,
|
|
20
|
+
)
|
|
21
|
+
with patch("patchtroy.cli.Patchtroy") as mock_cls:
|
|
22
|
+
mock_instance = MagicMock()
|
|
23
|
+
mock_instance.scrape.return_value = fake_result
|
|
24
|
+
mock_cls.return_value = mock_instance
|
|
25
|
+
|
|
26
|
+
ret = main(["https://example.com", "--screenshot", "out.png", "--pdf", "out.pdf"])
|
|
27
|
+
assert ret == 0
|
|
28
|
+
mock_cls.assert_called_once()
|
|
29
|
+
# Verify config created had screenshot and pdf enabled
|
|
30
|
+
config_arg = mock_cls.call_args[0][0]
|
|
31
|
+
assert config_arg.screenshot is True
|
|
32
|
+
assert config_arg.pdf is True
|
|
33
|
+
assert config_arg.screenshot_path == "out.png"
|
|
34
|
+
assert config_arg.pdf_path == "out.pdf"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_cli_batch_urls():
|
|
38
|
+
fake_result = ScrapeResult(
|
|
39
|
+
url="https://example.com",
|
|
40
|
+
title="Test Page",
|
|
41
|
+
markdown="Markdown content",
|
|
42
|
+
success=True,
|
|
43
|
+
)
|
|
44
|
+
with patch("patchtroy.cli.Patchtroy") as mock_cls:
|
|
45
|
+
mock_instance = MagicMock()
|
|
46
|
+
mock_instance.scrape_many.return_value = [fake_result, fake_result]
|
|
47
|
+
mock_cls.return_value = mock_instance
|
|
48
|
+
|
|
49
|
+
ret = main(["https://example.com/1", "https://example.com/2", "-c", "4"])
|
|
50
|
+
assert ret == 0
|
|
51
|
+
config_arg = mock_cls.call_args[0][0]
|
|
52
|
+
assert config_arg.max_concurrency == 4
|
|
53
|
+
mock_instance.scrape_many.assert_called_once_with(["https://example.com/1", "https://example.com/2"])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_cli_serve():
|
|
57
|
+
with patch("patchtroy.server.run_server") as mock_run:
|
|
58
|
+
ret = main(["serve", "--port", "9000", "--host", "127.0.0.1"])
|
|
59
|
+
assert ret == 0
|
|
60
|
+
mock_run.assert_called_once_with(host="127.0.0.1", port=9000)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_cli_single_url_csv(tmp_path):
|
|
64
|
+
csv_file = tmp_path / "result.csv"
|
|
65
|
+
fake_result = ScrapeResult(
|
|
66
|
+
url="https://example.com",
|
|
67
|
+
title="Test Page",
|
|
68
|
+
markdown="This is test markdown.",
|
|
69
|
+
success=True,
|
|
70
|
+
)
|
|
71
|
+
with patch("patchtroy.cli.Patchtroy") as mock_cls:
|
|
72
|
+
mock_instance = MagicMock()
|
|
73
|
+
mock_instance.scrape.return_value = fake_result
|
|
74
|
+
mock_cls.return_value = mock_instance
|
|
75
|
+
|
|
76
|
+
ret = main(["https://example.com", "-f", "csv", "-o", str(csv_file)])
|
|
77
|
+
assert ret == 0
|
|
78
|
+
assert csv_file.exists()
|
|
79
|
+
content = csv_file.read_text(encoding="utf-8")
|
|
80
|
+
assert "url,success,status_code,title" in content
|
|
81
|
+
assert "https://example.com,True,200,Test Page" in content
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_cli_csv_auto_detect(tmp_path):
|
|
85
|
+
csv_file = tmp_path / "autodetect.csv"
|
|
86
|
+
fake_result = ScrapeResult(
|
|
87
|
+
url="https://example.com/auto",
|
|
88
|
+
title="Auto CSV",
|
|
89
|
+
markdown="Auto CSV Content",
|
|
90
|
+
success=True,
|
|
91
|
+
)
|
|
92
|
+
with patch("patchtroy.cli.Patchtroy") as mock_cls:
|
|
93
|
+
mock_instance = MagicMock()
|
|
94
|
+
mock_instance.scrape.return_value = fake_result
|
|
95
|
+
mock_cls.return_value = mock_instance
|
|
96
|
+
|
|
97
|
+
ret = main(["https://example.com/auto", "-o", str(csv_file)])
|
|
98
|
+
assert ret == 0
|
|
99
|
+
assert csv_file.exists()
|
|
100
|
+
content = csv_file.read_text(encoding="utf-8")
|
|
101
|
+
assert "https://example.com/auto,True,200,Auto CSV" in content
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_cli_batch_csv(tmp_path):
|
|
105
|
+
csv_file = tmp_path / "batch.csv"
|
|
106
|
+
r1 = ScrapeResult(url="https://a.com", title="A", markdown="Doc A", success=True)
|
|
107
|
+
r2 = ScrapeResult(url="https://b.com", title="B", markdown="Doc B", success=False, error="Timeout")
|
|
108
|
+
|
|
109
|
+
with patch("patchtroy.cli.Patchtroy") as mock_cls:
|
|
110
|
+
mock_instance = MagicMock()
|
|
111
|
+
mock_instance.scrape_many.return_value = [r1, r2]
|
|
112
|
+
mock_cls.return_value = mock_instance
|
|
113
|
+
|
|
114
|
+
ret = main(["https://a.com", "https://b.com", "-f", "csv", "-o", str(csv_file)])
|
|
115
|
+
assert ret == 0
|
|
116
|
+
assert csv_file.exists()
|
|
117
|
+
lines = csv_file.read_text(encoding="utf-8").strip().splitlines()
|
|
118
|
+
assert len(lines) == 3 # Header + 2 data rows
|
|
119
|
+
assert "https://a.com" in lines[1]
|
|
120
|
+
assert "https://b.com" in lines[2]
|
|
121
|
+
assert "Timeout" in lines[2]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_scrape_result_to_csv():
|
|
125
|
+
res = ScrapeResult(
|
|
126
|
+
url="https://example.com/test",
|
|
127
|
+
title="Sample",
|
|
128
|
+
markdown="# Heading\nParagraph",
|
|
129
|
+
metadata={"author": "Marcus", "date": "2026-09-03"},
|
|
130
|
+
success=True,
|
|
131
|
+
)
|
|
132
|
+
csv_text = res.to_csv()
|
|
133
|
+
assert "url,success,status_code,title,author,date" in csv_text
|
|
134
|
+
assert "https://example.com/test" in csv_text
|
|
135
|
+
assert "Marcus" in csv_text
|
|
136
|
+
assert "2026-09-03" in csv_text
|
|
137
|
+
|
patchtroy-0.5.0/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
v0.5.0-2609031
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
"""Tests for Patchtroy CLI."""
|
|
2
|
-
|
|
3
|
-
from unittest.mock import MagicMock, patch
|
|
4
|
-
|
|
5
|
-
from patchtroy.cli import main
|
|
6
|
-
from patchtroy.models import ScrapeResult
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def test_cli_help(capsys):
|
|
10
|
-
ret = main([])
|
|
11
|
-
assert ret == 1
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_cli_single_url():
|
|
15
|
-
fake_result = ScrapeResult(
|
|
16
|
-
url="https://example.com",
|
|
17
|
-
title="Test Page",
|
|
18
|
-
markdown="This is test markdown.",
|
|
19
|
-
success=True,
|
|
20
|
-
)
|
|
21
|
-
with patch("patchtroy.cli.Patchtroy") as mock_cls:
|
|
22
|
-
mock_instance = MagicMock()
|
|
23
|
-
mock_instance.scrape.return_value = fake_result
|
|
24
|
-
mock_cls.return_value = mock_instance
|
|
25
|
-
|
|
26
|
-
ret = main(["https://example.com", "--screenshot", "out.png", "--pdf", "out.pdf"])
|
|
27
|
-
assert ret == 0
|
|
28
|
-
mock_cls.assert_called_once()
|
|
29
|
-
# Verify config created had screenshot and pdf enabled
|
|
30
|
-
config_arg = mock_cls.call_args[0][0]
|
|
31
|
-
assert config_arg.screenshot is True
|
|
32
|
-
assert config_arg.pdf is True
|
|
33
|
-
assert config_arg.screenshot_path == "out.png"
|
|
34
|
-
assert config_arg.pdf_path == "out.pdf"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def test_cli_batch_urls():
|
|
38
|
-
fake_result = ScrapeResult(
|
|
39
|
-
url="https://example.com",
|
|
40
|
-
title="Test Page",
|
|
41
|
-
markdown="Markdown content",
|
|
42
|
-
success=True,
|
|
43
|
-
)
|
|
44
|
-
with patch("patchtroy.cli.Patchtroy") as mock_cls:
|
|
45
|
-
mock_instance = MagicMock()
|
|
46
|
-
mock_instance.scrape_many.return_value = [fake_result, fake_result]
|
|
47
|
-
mock_cls.return_value = mock_instance
|
|
48
|
-
|
|
49
|
-
ret = main(["https://example.com/1", "https://example.com/2", "-c", "4"])
|
|
50
|
-
assert ret == 0
|
|
51
|
-
config_arg = mock_cls.call_args[0][0]
|
|
52
|
-
assert config_arg.max_concurrency == 4
|
|
53
|
-
mock_instance.scrape_many.assert_called_once_with(["https://example.com/1", "https://example.com/2"])
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def test_cli_serve():
|
|
57
|
-
with patch("patchtroy.server.run_server") as mock_run:
|
|
58
|
-
ret = main(["serve", "--port", "9000", "--host", "127.0.0.1"])
|
|
59
|
-
assert ret == 0
|
|
60
|
-
mock_run.assert_called_once_with(host="127.0.0.1", port=9000)
|
|
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
|