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.
Files changed (52) hide show
  1. {patchtroy-0.5.0 → patchtroy-0.5.2}/Dockerfile +2 -2
  2. {patchtroy-0.5.0 → patchtroy-0.5.2}/PKG-INFO +8 -6
  3. {patchtroy-0.5.0 → patchtroy-0.5.2}/PRD.md +2 -2
  4. {patchtroy-0.5.0 → patchtroy-0.5.2}/README.md +4 -2
  5. patchtroy-0.5.2/VERSION +1 -0
  6. {patchtroy-0.5.0 → patchtroy-0.5.2}/benchmarks/BENCHMARKS.md +1 -1
  7. {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/benchmarks.md +1 -1
  8. {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/getting-started.md +6 -0
  9. {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/rest-api.md +1 -1
  10. {patchtroy-0.5.0 → patchtroy-0.5.2}/pyproject.toml +4 -4
  11. {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/__init__.py +2 -2
  12. {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/cli.py +21 -3
  13. {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/models.py +6 -0
  14. {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/server.py +2 -2
  15. {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/utils.py +71 -0
  16. patchtroy-0.5.2/tests/test_cli.py +137 -0
  17. {patchtroy-0.5.0 → patchtroy-0.5.2}/tests/test_server.py +1 -1
  18. {patchtroy-0.5.0 → patchtroy-0.5.2}/uv.lock +1 -1
  19. patchtroy-0.5.0/VERSION +0 -1
  20. patchtroy-0.5.0/tests/test_cli.py +0 -60
  21. {patchtroy-0.5.0 → patchtroy-0.5.2}/.dockerignore +0 -0
  22. {patchtroy-0.5.0 → patchtroy-0.5.2}/.gitattributes +0 -0
  23. {patchtroy-0.5.0 → patchtroy-0.5.2}/.githooks/pre-commit +0 -0
  24. {patchtroy-0.5.0 → patchtroy-0.5.2}/.githooks/prepare-commit-msg +0 -0
  25. {patchtroy-0.5.0 → patchtroy-0.5.2}/.githooks/versionlib.sh +0 -0
  26. {patchtroy-0.5.0 → patchtroy-0.5.2}/.github/workflows/ci.yml +0 -0
  27. {patchtroy-0.5.0 → patchtroy-0.5.2}/.github/workflows/docs.yml +0 -0
  28. {patchtroy-0.5.0 → patchtroy-0.5.2}/.github/workflows/release.yml +0 -0
  29. {patchtroy-0.5.0 → patchtroy-0.5.2}/.gitignore +0 -0
  30. {patchtroy-0.5.0 → patchtroy-0.5.2}/LICENSE +0 -0
  31. {patchtroy-0.5.0 → patchtroy-0.5.2}/benchmarks/benchmark_results.json +0 -0
  32. {patchtroy-0.5.0 → patchtroy-0.5.2}/benchmarks/run_benchmark.py +0 -0
  33. {patchtroy-0.5.0 → patchtroy-0.5.2}/docker-compose.yml +0 -0
  34. {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/context-pool.md +0 -0
  35. {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/index.md +0 -0
  36. {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/llm-chunking.md +0 -0
  37. {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/media-capture.md +0 -0
  38. {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/proxy-rotation.md +0 -0
  39. {patchtroy-0.5.0 → patchtroy-0.5.2}/docs/stealth-architecture.md +0 -0
  40. {patchtroy-0.5.0 → patchtroy-0.5.2}/mkdocs.yml +0 -0
  41. {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/chunker.py +0 -0
  42. {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/crawler.py +0 -0
  43. {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/extractors.py +0 -0
  44. {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/pool.py +0 -0
  45. {patchtroy-0.5.0 → patchtroy-0.5.2}/src/patchtroy/proxy.py +0 -0
  46. {patchtroy-0.5.0 → patchtroy-0.5.2}/tests/test_chunker.py +0 -0
  47. {patchtroy-0.5.0 → patchtroy-0.5.2}/tests/test_crawler.py +0 -0
  48. {patchtroy-0.5.0 → patchtroy-0.5.2}/tests/test_extractors.py +0 -0
  49. {patchtroy-0.5.0 → patchtroy-0.5.2}/tests/test_media_capture.py +0 -0
  50. {patchtroy-0.5.0 → patchtroy-0.5.2}/tests/test_models.py +0 -0
  51. {patchtroy-0.5.0 → patchtroy-0.5.2}/tests/test_pool.py +0 -0
  52. {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="Marcus Zou <marcus.zou@icloud.com>"
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.0"
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.0
4
- Summary: Undetected stealth web scraper & markdown extractor for LLMs, pairing Patchright with Trafilatura.
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: Marcus Zou <marcus.zou@icloud.com>
9
- Maintainer-email: "Alfazen Inc." <support@alfazen.io>
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 output file
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 **Marcus Zou** and maintained by **Alfazen Inc.**, released under the **[Apache 2.0 License](LICENSE)**.
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**: Marcus Zou / Patchtroy Project
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.0 — Current)
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 output file
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 **Marcus Zou** and maintained by **Alfazen Inc.**, released under the **[Apache 2.0 License](LICENSE)**.
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)**.
@@ -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.0) | **Crawl4AI** (v0.9.3) | **Firecrawl** (Self-Hosted) |
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.0)** against **Crawl4AI (v0.9.3)** and **Firecrawl (Self-Hosted)**.
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
 
@@ -53,7 +53,7 @@ curl http://localhost:4013/health
53
53
  ```json
54
54
  {
55
55
  "status": "healthy",
56
- "version": "0.5.0",
56
+ "version": "0.5.2",
57
57
  "engine": "patchright",
58
58
  "active": true
59
59
  }
@@ -4,16 +4,16 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "patchtroy"
7
- version = "0.5.0"
8
- description = "Undetected stealth web scraper & markdown extractor for LLMs, pairing Patchright with Trafilatura."
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 = "Marcus Zou", email = "marcus.zou@icloud.com" }
13
+ { name = "marcuz-apl", email = "support@alfazen.org" }
14
14
  ]
15
15
  maintainers = [
16
- { name = "Alfazen Inc.", email = "support@alfazen.io" }
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.0"
10
- __author__ = "Marcus Zou"
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.0"
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.0",
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.0",
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
+
@@ -17,7 +17,7 @@ async def test_health_check_endpoint():
17
17
  assert resp.status_code == 200
18
18
  data = resp.json()
19
19
  assert data["status"] == "healthy"
20
- assert data["version"] == "0.5.0"
20
+ assert data["version"] == "0.5.2"
21
21
  assert data["engine"] == "patchright"
22
22
 
23
23
 
@@ -900,7 +900,7 @@ wheels = [
900
900
 
901
901
  [[package]]
902
902
  name = "patchtroy"
903
- version = "0.5.0"
903
+ version = "0.5.2"
904
904
  source = { editable = "." }
905
905
  dependencies = [
906
906
  { name = "beautifulsoup4" },
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