patchtroy 0.4.5__tar.gz → 0.4.6__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 (51) hide show
  1. {patchtroy-0.4.5 → patchtroy-0.4.6}/Dockerfile +1 -1
  2. {patchtroy-0.4.5 → patchtroy-0.4.6}/PKG-INFO +1 -1
  3. {patchtroy-0.4.5 → patchtroy-0.4.6}/PRD.md +1 -1
  4. patchtroy-0.4.6/VERSION +1 -0
  5. {patchtroy-0.4.5 → patchtroy-0.4.6}/benchmarks/BENCHMARKS.md +1 -1
  6. {patchtroy-0.4.5 → patchtroy-0.4.6}/docs/benchmarks.md +1 -1
  7. {patchtroy-0.4.5 → patchtroy-0.4.6}/docs/rest-api.md +1 -1
  8. {patchtroy-0.4.5 → patchtroy-0.4.6}/pyproject.toml +1 -1
  9. {patchtroy-0.4.5 → patchtroy-0.4.6}/src/patchtroy/__init__.py +1 -1
  10. {patchtroy-0.4.5 → patchtroy-0.4.6}/src/patchtroy/cli.py +1 -1
  11. {patchtroy-0.4.5 → patchtroy-0.4.6}/src/patchtroy/pool.py +2 -3
  12. {patchtroy-0.4.5 → patchtroy-0.4.6}/src/patchtroy/server.py +2 -2
  13. {patchtroy-0.4.5 → patchtroy-0.4.6}/src/patchtroy/utils.py +28 -26
  14. {patchtroy-0.4.5 → patchtroy-0.4.6}/tests/test_server.py +1 -1
  15. {patchtroy-0.4.5 → patchtroy-0.4.6}/uv.lock +1 -1
  16. patchtroy-0.4.5/VERSION +0 -1
  17. {patchtroy-0.4.5 → patchtroy-0.4.6}/.dockerignore +0 -0
  18. {patchtroy-0.4.5 → patchtroy-0.4.6}/.gitattributes +0 -0
  19. {patchtroy-0.4.5 → patchtroy-0.4.6}/.githooks/pre-commit +0 -0
  20. {patchtroy-0.4.5 → patchtroy-0.4.6}/.githooks/prepare-commit-msg +0 -0
  21. {patchtroy-0.4.5 → patchtroy-0.4.6}/.githooks/versionlib.sh +0 -0
  22. {patchtroy-0.4.5 → patchtroy-0.4.6}/.github/workflows/ci.yml +0 -0
  23. {patchtroy-0.4.5 → patchtroy-0.4.6}/.github/workflows/docs.yml +0 -0
  24. {patchtroy-0.4.5 → patchtroy-0.4.6}/.github/workflows/release.yml +0 -0
  25. {patchtroy-0.4.5 → patchtroy-0.4.6}/.gitignore +0 -0
  26. {patchtroy-0.4.5 → patchtroy-0.4.6}/LICENSE +0 -0
  27. {patchtroy-0.4.5 → patchtroy-0.4.6}/README.md +0 -0
  28. {patchtroy-0.4.5 → patchtroy-0.4.6}/benchmarks/benchmark_results.json +0 -0
  29. {patchtroy-0.4.5 → patchtroy-0.4.6}/benchmarks/run_benchmark.py +0 -0
  30. {patchtroy-0.4.5 → patchtroy-0.4.6}/docker-compose.yml +0 -0
  31. {patchtroy-0.4.5 → patchtroy-0.4.6}/docs/context-pool.md +0 -0
  32. {patchtroy-0.4.5 → patchtroy-0.4.6}/docs/getting-started.md +0 -0
  33. {patchtroy-0.4.5 → patchtroy-0.4.6}/docs/index.md +0 -0
  34. {patchtroy-0.4.5 → patchtroy-0.4.6}/docs/llm-chunking.md +0 -0
  35. {patchtroy-0.4.5 → patchtroy-0.4.6}/docs/media-capture.md +0 -0
  36. {patchtroy-0.4.5 → patchtroy-0.4.6}/docs/proxy-rotation.md +0 -0
  37. {patchtroy-0.4.5 → patchtroy-0.4.6}/docs/stealth-architecture.md +0 -0
  38. {patchtroy-0.4.5 → patchtroy-0.4.6}/mkdocs.yml +0 -0
  39. {patchtroy-0.4.5 → patchtroy-0.4.6}/src/patchtroy/chunker.py +0 -0
  40. {patchtroy-0.4.5 → patchtroy-0.4.6}/src/patchtroy/crawler.py +0 -0
  41. {patchtroy-0.4.5 → patchtroy-0.4.6}/src/patchtroy/extractors.py +0 -0
  42. {patchtroy-0.4.5 → patchtroy-0.4.6}/src/patchtroy/models.py +0 -0
  43. {patchtroy-0.4.5 → patchtroy-0.4.6}/src/patchtroy/proxy.py +0 -0
  44. {patchtroy-0.4.5 → patchtroy-0.4.6}/tests/test_chunker.py +0 -0
  45. {patchtroy-0.4.5 → patchtroy-0.4.6}/tests/test_cli.py +0 -0
  46. {patchtroy-0.4.5 → patchtroy-0.4.6}/tests/test_crawler.py +0 -0
  47. {patchtroy-0.4.5 → patchtroy-0.4.6}/tests/test_extractors.py +0 -0
  48. {patchtroy-0.4.5 → patchtroy-0.4.6}/tests/test_media_capture.py +0 -0
  49. {patchtroy-0.4.5 → patchtroy-0.4.6}/tests/test_models.py +0 -0
  50. {patchtroy-0.4.5 → patchtroy-0.4.6}/tests/test_pool.py +0 -0
  51. {patchtroy-0.4.5 → patchtroy-0.4.6}/tests/test_proxy.py +0 -0
@@ -8,7 +8,7 @@ FROM python:3.11-slim
8
8
  LABEL maintainer="Marcus Zou <marcus.zou@icloud.com>"
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.4.5"
11
+ LABEL org.opencontainers.image.version="0.4.6"
12
12
 
13
13
  # Python and environment settings
14
14
  ENV PYTHONUNBUFFERED=1 \
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: patchtroy
3
- Version: 0.4.5
3
+ Version: 0.4.6
4
4
  Summary: Undetected 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
@@ -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.4.5 — Current)
155
+ ### Phase 4 (v0.4.6 — 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).
@@ -0,0 +1 @@
1
+ v0.4.7-260902d
@@ -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.4.5) | **Crawl4AI** (v0.9.3) | **Firecrawl** (Self-Hosted) |
22
+ | Dimension | **Patchtroy** (v0.4.6) | **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.4.5)** against **Crawl4AI (v0.9.3)** and **Firecrawl (Self-Hosted)**.
3
+ A quantitative, reproducible benchmark comparing **Patchtroy (v0.4.6)** against **Crawl4AI (v0.9.3)** and **Firecrawl (Self-Hosted)**.
4
4
 
5
5
  ---
6
6
 
@@ -53,7 +53,7 @@ curl http://localhost:4013/health
53
53
  ```json
54
54
  {
55
55
  "status": "healthy",
56
- "version": "0.4.5",
56
+ "version": "0.4.6",
57
57
  "engine": "patchright",
58
58
  "active": true
59
59
  }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "patchtroy"
7
- version = "0.4.5"
7
+ version = "0.4.6"
8
8
  description = "Undetected stealth web scraper & markdown extractor for LLMs, pairing Patchright with Trafilatura."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -6,7 +6,7 @@ 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.4.5"
9
+ __version__ = "0.4.6"
10
10
  __author__ = "Marcus Zou"
11
11
  __license__ = "Apache-2.0"
12
12
  __copyright__ = "Copyright 2026 Alfazen Inc."
@@ -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.4.5"
14
+ __version__ = "0.4.6"
15
15
 
16
16
 
17
17
  def main(argv: list[str] | None = None) -> int:
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import logging
7
- import sys
8
7
  from collections.abc import AsyncIterator
9
8
  from contextlib import asynccontextmanager
10
9
  from typing import Any
@@ -90,8 +89,8 @@ class BrowserContextPool:
90
89
  pass
91
90
  self._playwright = None
92
91
 
93
- if sys.platform == "win32":
94
- await asyncio.sleep(0.05)
92
+ # Allow event loop to process subprocess termination callbacks before shutdown
93
+ await asyncio.sleep(0.05)
95
94
 
96
95
  @asynccontextmanager
97
96
  async def acquire_context(
@@ -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.4.5",
93
+ version="0.4.6",
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.4.5",
102
+ "version": "0.4.6",
103
103
  "engine": "patchright",
104
104
  "active": crawler_instance is not None and crawler_instance._pool.is_running,
105
105
  }
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import random
6
- import sys
7
6
  from typing import Any
8
7
  from urllib.parse import urlparse
9
8
 
@@ -56,17 +55,33 @@ def is_valid_url(url: str) -> bool:
56
55
  return False
57
56
 
58
57
 
59
- def silence_windows_proactor_bug() -> None:
60
- """Silence known Python Windows asyncio proactor pipe transport bug.
58
+ def silence_subprocess_transport_bug() -> None:
59
+ """Silence Python asyncio BaseSubprocessTransport and pipe transport destructor errors.
61
60
 
62
- On Windows, when an asyncio subprocess transport is garbage collected after
63
- loop shutdown, _ProactorBasePipeTransport.__del__ and BaseSubprocessTransport.__del__
64
- call __repr__, which calls fileno() on a closed pipe handle and raises
65
- ValueError: I/O operation on closed pipe. This wraps both destructors to cleanly
66
- catch and ignore closed pipe errors during interpreter teardown.
61
+ When an event loop finishes and closes (via asyncio.run), underlying child process
62
+ transports may remain in the garbage collector until interpreter exit. During GC,
63
+ their __del__ destructors attempt post-loop cleanup:
64
+ - On Unix/Linux: calls self.close() -> loop.call_soon() -> RuntimeError('Event loop is closed')
65
+ - On Windows: calls _warn() -> __repr__() -> fileno() -> ValueError('I/O operation on closed pipe')
66
+ Wrapping both destructors safely catches and suppresses these post-loop cleanup errors across
67
+ all platforms (Linux, macOS, Windows).
67
68
  """
68
- if sys.platform != "win32":
69
- return
69
+ try:
70
+ from asyncio.base_subprocess import BaseSubprocessTransport
71
+
72
+ orig_sub_del = getattr(BaseSubprocessTransport, "__del__", None)
73
+ if orig_sub_del and not getattr(BaseSubprocessTransport, "_patchtroy_safe", False):
74
+
75
+ def _safe_sub_del(self: Any, *args: Any, **kwargs: Any) -> None:
76
+ try:
77
+ orig_sub_del(self, *args, **kwargs)
78
+ except (RuntimeError, ValueError, OSError):
79
+ pass
80
+
81
+ BaseSubprocessTransport.__del__ = _safe_sub_del # type: ignore[method-assign]
82
+ BaseSubprocessTransport._patchtroy_safe = True # type: ignore[attr-defined]
83
+ except Exception:
84
+ pass
70
85
 
71
86
  try:
72
87
  from asyncio.proactor_events import _ProactorBasePipeTransport
@@ -77,7 +92,7 @@ def silence_windows_proactor_bug() -> None:
77
92
  def _safe_pipe_del(self: Any, *args: Any, **kwargs: Any) -> None:
78
93
  try:
79
94
  orig_pipe_del(self, *args, **kwargs)
80
- except (ValueError, OSError):
95
+ except (RuntimeError, ValueError, OSError):
81
96
  pass
82
97
 
83
98
  _ProactorBasePipeTransport.__del__ = _safe_pipe_del # type: ignore[method-assign]
@@ -85,19 +100,6 @@ def silence_windows_proactor_bug() -> None:
85
100
  except Exception:
86
101
  pass
87
102
 
88
- try:
89
- from asyncio.base_subprocess import BaseSubprocessTransport
90
-
91
- orig_sub_del = getattr(BaseSubprocessTransport, "__del__", None)
92
- if orig_sub_del and not getattr(BaseSubprocessTransport, "_patchtroy_safe", False):
93
-
94
- def _safe_sub_del(self: Any, *args: Any, **kwargs: Any) -> None:
95
- try:
96
- orig_sub_del(self, *args, **kwargs)
97
- except (ValueError, OSError):
98
- pass
99
103
 
100
- BaseSubprocessTransport.__del__ = _safe_sub_del # type: ignore[method-assign]
101
- BaseSubprocessTransport._patchtroy_safe = True # type: ignore[attr-defined]
102
- except Exception:
103
- pass
104
+ # Backwards-compatible alias
105
+ silence_windows_proactor_bug = silence_subprocess_transport_bug
@@ -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.4.5"
20
+ assert data["version"] == "0.4.6"
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.4.5"
903
+ version = "0.4.6"
904
904
  source = { editable = "." }
905
905
  dependencies = [
906
906
  { name = "beautifulsoup4" },
patchtroy-0.4.5/VERSION DELETED
@@ -1 +0,0 @@
1
- v0.4.5-260902b
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