veilrender 0.1.0__tar.gz → 0.2.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.
Files changed (38) hide show
  1. {veilrender-0.1.0 → veilrender-0.2.0}/PKG-INFO +13 -5
  2. {veilrender-0.1.0 → veilrender-0.2.0}/README.md +12 -4
  3. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/__init__.py +1 -1
  4. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/app.py +2 -1
  5. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/browser.py +10 -0
  6. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/config.py +1 -1
  7. veilrender-0.2.0/src/veilrender/routes/dashboard.py +237 -0
  8. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/routes/health.py +0 -4
  9. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/routes/render.py +8 -0
  10. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/routes/screenshot.py +8 -0
  11. veilrender-0.2.0/src/veilrender/stats.py +83 -0
  12. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender.egg-info/PKG-INFO +13 -5
  13. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender.egg-info/SOURCES.txt +2 -0
  14. {veilrender-0.1.0 → veilrender-0.2.0}/pyproject.toml +0 -0
  15. {veilrender-0.1.0 → veilrender-0.2.0}/setup.cfg +0 -0
  16. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/__main__.py +0 -0
  17. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/_vendor/__init__.py +0 -0
  18. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/_vendor/benchmark_compare.py +0 -0
  19. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/_vendor/cache.py +0 -0
  20. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/_vendor/config.py +0 -0
  21. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/_vendor/dotenv.py +0 -0
  22. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/_vendor/httpserver.py +0 -0
  23. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/_vendor/jsonc.py +0 -0
  24. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/_vendor/markdown.py +0 -0
  25. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/_vendor/readability.py +0 -0
  26. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/_vendor/retry.py +0 -0
  27. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/_vendor/soup.py +0 -0
  28. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/_vendor/structlog.py +0 -0
  29. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/_vendor/useragent.py +0 -0
  30. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/_vendor/yaml.py +0 -0
  31. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/auth.py +0 -0
  32. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/cdp_proxy.py +0 -0
  33. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/models.py +0 -0
  34. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender/routes/__init__.py +0 -0
  35. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender.egg-info/dependency_links.txt +0 -0
  36. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender.egg-info/entry_points.txt +0 -0
  37. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender.egg-info/requires.txt +0 -0
  38. {veilrender-0.1.0 → veilrender-0.2.0}/src/veilrender.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: veilrender
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Headless browser rendering API — self-hostable on HF Spaces, Docker, or bare metal
5
5
  Author: Peng Ding
6
6
  License-Expression: MIT
@@ -23,15 +23,23 @@ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
23
23
  [![PyPI version](https://img.shields.io/pypi/v/veilrender?color=green)](https://pypi.org/project/veilrender/)
24
24
  [![GitHub release](https://img.shields.io/github/v/release/Oaklight/veilrender?color=green)](https://github.com/Oaklight/veilrender/releases/latest)
25
25
  [![CI](https://github.com/Oaklight/veilrender/actions/workflows/ci.yml/badge.svg)](https://github.com/Oaklight/veilrender/actions/workflows/ci.yml)
26
- [![Docker Pulls](https://img.shields.io/docker/pulls/oaklight/veilrender)](https://hub.docker.com/r/oaklight/veilrender)
27
- [![Docker Image](https://img.shields.io/docker/v/oaklight/veilrender?sort=semver&label=docker)](https://hub.docker.com/r/oaklight/veilrender)
26
+ [![Docker Pulls](https://img.shields.io/docker/pulls/oaklight/veilrender?color=blue)](https://hub.docker.com/r/oaklight/veilrender)
27
+ [![Docker Image](https://img.shields.io/docker/v/oaklight/veilrender?sort=semver&label=docker&color=blue)](https://hub.docker.com/r/oaklight/veilrender)
28
28
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
29
- [![HF Spaces](https://img.shields.io/badge/%F0%9F%A4%97-Spaces-blue)](https://huggingface.co/spaces/oaklight/veilrender)
29
+ [![Live Demo](https://img.shields.io/badge/%F0%9F%A4%97-Live_Demo-blue)](https://oaklight-veilrender-public.hf.space)
30
30
 
31
31
  [中文](README_zh.md) | **English**
32
32
 
33
33
  Headless browser rendering API — self-hostable on HF Spaces, Docker, or bare metal.
34
34
 
35
+ ### Try it now (no auth needed)
36
+
37
+ ```bash
38
+ curl -X POST https://oaklight-veilrender-public.hf.space/render \
39
+ -H "Content-Type: application/json" \
40
+ -d '{"url": "https://example.com"}'
41
+ ```
42
+
35
43
  VeilRender accepts a URL and returns the fully rendered page content (HTML, Markdown, readability-extracted article) using a headless Chromium browser. Designed as a fallback for fetch tools that fail on JavaScript-rendered pages.
36
44
 
37
45
  ## Quick Start
@@ -39,7 +47,7 @@ VeilRender accepts a URL and returns the fully rendered page content (HTML, Mark
39
47
  ### Docker
40
48
 
41
49
  ```bash
42
- docker run -p 7860:7860 -e VEILRENDER_API_TOKEN=your-secret ghcr.io/oaklight/veilrender
50
+ docker run -p 7860:7860 -e VEILRENDER_API_TOKEN=your-secret oaklight/veilrender
43
51
  ```
44
52
 
45
53
  ### Local Development
@@ -3,15 +3,23 @@
3
3
  [![PyPI version](https://img.shields.io/pypi/v/veilrender?color=green)](https://pypi.org/project/veilrender/)
4
4
  [![GitHub release](https://img.shields.io/github/v/release/Oaklight/veilrender?color=green)](https://github.com/Oaklight/veilrender/releases/latest)
5
5
  [![CI](https://github.com/Oaklight/veilrender/actions/workflows/ci.yml/badge.svg)](https://github.com/Oaklight/veilrender/actions/workflows/ci.yml)
6
- [![Docker Pulls](https://img.shields.io/docker/pulls/oaklight/veilrender)](https://hub.docker.com/r/oaklight/veilrender)
7
- [![Docker Image](https://img.shields.io/docker/v/oaklight/veilrender?sort=semver&label=docker)](https://hub.docker.com/r/oaklight/veilrender)
6
+ [![Docker Pulls](https://img.shields.io/docker/pulls/oaklight/veilrender?color=blue)](https://hub.docker.com/r/oaklight/veilrender)
7
+ [![Docker Image](https://img.shields.io/docker/v/oaklight/veilrender?sort=semver&label=docker&color=blue)](https://hub.docker.com/r/oaklight/veilrender)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
9
- [![HF Spaces](https://img.shields.io/badge/%F0%9F%A4%97-Spaces-blue)](https://huggingface.co/spaces/oaklight/veilrender)
9
+ [![Live Demo](https://img.shields.io/badge/%F0%9F%A4%97-Live_Demo-blue)](https://oaklight-veilrender-public.hf.space)
10
10
 
11
11
  [中文](README_zh.md) | **English**
12
12
 
13
13
  Headless browser rendering API — self-hostable on HF Spaces, Docker, or bare metal.
14
14
 
15
+ ### Try it now (no auth needed)
16
+
17
+ ```bash
18
+ curl -X POST https://oaklight-veilrender-public.hf.space/render \
19
+ -H "Content-Type: application/json" \
20
+ -d '{"url": "https://example.com"}'
21
+ ```
22
+
15
23
  VeilRender accepts a URL and returns the fully rendered page content (HTML, Markdown, readability-extracted article) using a headless Chromium browser. Designed as a fallback for fetch tools that fail on JavaScript-rendered pages.
16
24
 
17
25
  ## Quick Start
@@ -19,7 +27,7 @@ VeilRender accepts a URL and returns the fully rendered page content (HTML, Mark
19
27
  ### Docker
20
28
 
21
29
  ```bash
22
- docker run -p 7860:7860 -e VEILRENDER_API_TOKEN=your-secret ghcr.io/oaklight/veilrender
30
+ docker run -p 7860:7860 -e VEILRENDER_API_TOKEN=your-secret oaklight/veilrender
23
31
  ```
24
32
 
25
33
  ### Local Development
@@ -1,3 +1,3 @@
1
1
  """VeilRender — headless browser rendering API."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
@@ -11,7 +11,7 @@ from veilrender._vendor.httpserver import App
11
11
  from veilrender.browser import browser_manager
12
12
  from veilrender.cdp_proxy import handle_cdp_upgrade, is_websocket_upgrade
13
13
  from veilrender.config import settings
14
- from veilrender.routes import health, render, screenshot
14
+ from veilrender.routes import dashboard, health, render, screenshot
15
15
 
16
16
  logging.basicConfig(
17
17
  level=logging.INFO,
@@ -40,6 +40,7 @@ def create_app() -> App:
40
40
 
41
41
  # Register routes
42
42
  health.register(app)
43
+ dashboard.register(app)
43
44
  render.register(app)
44
45
  screenshot.register(app)
45
46
 
@@ -139,6 +139,16 @@ class BrowserManager:
139
139
  assert self._browser is not None
140
140
  return self._browser
141
141
 
142
+ @property
143
+ def active_pages(self) -> int:
144
+ """Number of browser pages currently in use."""
145
+ return settings.max_concurrent - self._semaphore._value
146
+
147
+ @property
148
+ def is_browser_alive(self) -> bool:
149
+ """Whether the browser process is connected."""
150
+ return self._browser is not None and self._browser.is_connected()
151
+
142
152
  @asynccontextmanager
143
153
  async def get_page(
144
154
  self,
@@ -19,7 +19,7 @@ class Settings:
19
19
  self.viewport_height: int = int(
20
20
  os.environ.get("VEILRENDER_VIEWPORT_HEIGHT", "720")
21
21
  )
22
- self.max_concurrent: int = int(os.environ.get("VEILRENDER_MAX_CONCURRENT", "3"))
22
+ self.max_concurrent: int = int(os.environ.get("VEILRENDER_MAX_CONCURRENT", "5"))
23
23
 
24
24
 
25
25
  settings = Settings()
@@ -0,0 +1,237 @@
1
+ """Dashboard endpoint — GET / stats page."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from veilrender import stats
6
+ from veilrender._vendor.httpserver import App, Request, Response
7
+ from veilrender.browser import browser_manager
8
+ from veilrender.config import settings
9
+
10
+ _BADGES_HTML = """
11
+ <a href="https://github.com/Oaklight/veilrender" target="_blank" rel="noopener">
12
+ <img src="https://img.shields.io/github/stars/Oaklight/veilrender?style=flat&amp;logo=github&amp;label=GitHub" alt="GitHub">
13
+ </a>
14
+ <a href="https://pypi.org/project/veilrender/" target="_blank" rel="noopener">
15
+ <img src="https://img.shields.io/pypi/v/veilrender?style=flat&amp;logo=pypi&amp;logoColor=white&amp;label=PyPI" alt="PyPI">
16
+ </a>
17
+ <a href="https://hub.docker.com/r/oaklight/veilrender" target="_blank" rel="noopener">
18
+ <img src="https://img.shields.io/docker/pulls/oaklight/veilrender?style=flat&amp;logo=docker&amp;logoColor=white&amp;label=Docker" alt="Docker Hub">
19
+ </a>
20
+ <a href="https://huggingface.co/spaces/oaklight/veilrender-public" target="_blank" rel="noopener">
21
+ <img src="https://img.shields.io/badge/%F0%9F%A4%97_HF_Spaces-demo-yellow?style=flat" alt="HF Spaces">
22
+ </a>
23
+ """
24
+
25
+
26
+ def _stats_row(name: str, ep: stats.EndpointStats) -> str:
27
+ """Build an HTML table row for one endpoint."""
28
+ rate_class = (
29
+ "green"
30
+ if ep.success_rate >= 95
31
+ else "yellow"
32
+ if ep.success_rate >= 80
33
+ else "red"
34
+ )
35
+ return f"""
36
+ <tr>
37
+ <td><code>{name}</code></td>
38
+ <td class="mono">{ep.requests}</td>
39
+ <td class="mono green">{ep.successes}</td>
40
+ <td class="mono red">{ep.failures}</td>
41
+ <td class="mono {rate_class}">{ep.success_rate:.1f}%</td>
42
+ <td class="mono">{ep.avg_ms:.0f} ms</td>
43
+ <td class="mono">{ep.p95_ms:.0f} ms</td>
44
+ </tr>"""
45
+
46
+
47
+ def _build_html() -> str:
48
+ """Build the dashboard HTML."""
49
+ browser_status = "🟢 alive" if browser_manager.is_browser_alive else "🔴 dead"
50
+ active = browser_manager.active_pages
51
+ max_conc = settings.max_concurrent
52
+
53
+ total_req = stats.render.requests + stats.screenshot.requests
54
+ total_ok = stats.render.successes + stats.screenshot.successes
55
+ total_fail = stats.render.failures + stats.screenshot.failures
56
+
57
+ utilization = (active / max_conc * 100) if max_conc > 0 else 0
58
+ util_class = (
59
+ "green" if utilization < 70 else "yellow" if utilization < 90 else "red"
60
+ )
61
+
62
+ return f"""<!DOCTYPE html>
63
+ <html lang="en">
64
+ <head>
65
+ <meta charset="utf-8">
66
+ <meta name="viewport" content="width=device-width, initial-scale=1">
67
+ <meta http-equiv="refresh" content="5">
68
+ <title>VeilRender Dashboard</title>
69
+ <style>
70
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
71
+ body {{
72
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
73
+ background: #0f172a; color: #e2e8f0; padding: 2rem;
74
+ min-height: 100vh;
75
+ }}
76
+
77
+ /* Header */
78
+ .header {{
79
+ display: flex; align-items: center; justify-content: space-between;
80
+ flex-wrap: wrap; gap: 1rem; margin-bottom: 2rem;
81
+ padding-bottom: 1.5rem; border-bottom: 1px solid #1e293b;
82
+ }}
83
+ .header h1 {{
84
+ font-size: 1.5rem; font-weight: 700; color: #f8fafc;
85
+ letter-spacing: -0.02em;
86
+ }}
87
+ .header h1 span {{ color: #818cf8; }}
88
+ .badges {{ display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }}
89
+ .badges a {{ text-decoration: none; transition: opacity 0.15s; }}
90
+ .badges a:hover {{ opacity: 0.8; }}
91
+ .badges img {{ height: 20px; vertical-align: middle; }}
92
+
93
+ /* Section labels */
94
+ .section-label {{
95
+ font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
96
+ letter-spacing: 0.08em; color: #64748b; margin-bottom: 0.75rem;
97
+ }}
98
+
99
+ /* Cards */
100
+ .grid {{
101
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
102
+ gap: 0.75rem; margin-bottom: 2rem;
103
+ }}
104
+ .card {{
105
+ background: #1e293b; border-radius: 10px; padding: 1.2rem;
106
+ border: 1px solid #334155; transition: border-color 0.2s;
107
+ }}
108
+ .card:hover {{ border-color: #475569; }}
109
+ .card .label {{
110
+ font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em;
111
+ color: #94a3b8; margin-bottom: 0.4rem;
112
+ }}
113
+ .card .value {{ font-size: 1.6rem; font-weight: 700; font-variant-numeric: tabular-nums; }}
114
+
115
+ /* Table */
116
+ .table-wrap {{
117
+ background: #1e293b; border-radius: 10px; border: 1px solid #334155;
118
+ overflow: hidden; margin-bottom: 2rem;
119
+ }}
120
+ table {{ width: 100%; border-collapse: collapse; }}
121
+ th {{
122
+ padding: 0.7rem 1rem; text-align: left; font-size: 0.7rem;
123
+ text-transform: uppercase; letter-spacing: 0.06em; color: #94a3b8;
124
+ background: #1e293b; border-bottom: 1px solid #334155;
125
+ }}
126
+ td {{
127
+ padding: 0.7rem 1rem; border-top: 1px solid rgba(51, 65, 85, 0.5);
128
+ }}
129
+ tr:hover td {{ background: rgba(51, 65, 85, 0.3); }}
130
+ code {{ font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace; font-size: 0.9em; }}
131
+ .mono {{ font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace; font-variant-numeric: tabular-nums; }}
132
+
133
+ /* Colors */
134
+ .green {{ color: #4ade80; }}
135
+ .yellow {{ color: #facc15; }}
136
+ .red {{ color: #f87171; }}
137
+ .blue {{ color: #60a5fa; }}
138
+ .indigo {{ color: #818cf8; }}
139
+
140
+ /* Capacity bar */
141
+ .bar-track {{
142
+ height: 6px; background: #334155; border-radius: 3px;
143
+ margin-top: 0.5rem; overflow: hidden;
144
+ }}
145
+ .bar-fill {{ height: 100%; border-radius: 3px; transition: width 0.3s; }}
146
+
147
+ /* Footer */
148
+ .footer {{
149
+ font-size: 0.7rem; color: #475569; display: flex;
150
+ justify-content: space-between; flex-wrap: wrap; gap: 0.5rem;
151
+ }}
152
+
153
+ @media (max-width: 640px) {{
154
+ body {{ padding: 1rem; }}
155
+ .header {{ flex-direction: column; align-items: flex-start; }}
156
+ .grid {{ grid-template-columns: repeat(2, 1fr); }}
157
+ }}
158
+ </style>
159
+ </head>
160
+ <body>
161
+ <div class="header">
162
+ <h1>👻 <span>Veil</span>Render</h1>
163
+ <div class="badges">
164
+ {_BADGES_HTML}
165
+ </div>
166
+ </div>
167
+
168
+ <div class="section-label">Overview</div>
169
+ <div class="grid">
170
+ <div class="card">
171
+ <div class="label">Uptime</div>
172
+ <div class="value">{stats.format_uptime()}</div>
173
+ </div>
174
+ <div class="card">
175
+ <div class="label">Browser</div>
176
+ <div class="value">{browser_status}</div>
177
+ </div>
178
+ <div class="card">
179
+ <div class="label">Capacity</div>
180
+ <div class="value {util_class}">{active} <span style="font-size:0.8rem;color:#94a3b8">/ {max_conc}</span></div>
181
+ <div class="bar-track">
182
+ <div class="bar-fill" style="width:{utilization:.0f}%;background:{"#4ade80" if utilization < 70 else "#facc15" if utilization < 90 else "#f87171"}"></div>
183
+ </div>
184
+ </div>
185
+ <div class="card">
186
+ <div class="label">Total Requests</div>
187
+ <div class="value indigo">{total_req}</div>
188
+ </div>
189
+ <div class="card">
190
+ <div class="label">Successes</div>
191
+ <div class="value green">{total_ok}</div>
192
+ </div>
193
+ <div class="card">
194
+ <div class="label">Failures</div>
195
+ <div class="value {"red" if total_fail > 0 else "green"}">{total_fail}</div>
196
+ </div>
197
+ </div>
198
+
199
+ <div class="section-label">Endpoints</div>
200
+ <div class="table-wrap">
201
+ <table>
202
+ <thead>
203
+ <tr>
204
+ <th>Endpoint</th>
205
+ <th>Requests</th>
206
+ <th>Success</th>
207
+ <th>Failure</th>
208
+ <th>Rate</th>
209
+ <th>Avg Latency</th>
210
+ <th>P95 Latency</th>
211
+ </tr>
212
+ </thead>
213
+ <tbody>
214
+ {_stats_row("/render", stats.render)}
215
+ {_stats_row("/screenshot", stats.screenshot)}
216
+ </tbody>
217
+ </table>
218
+ </div>
219
+
220
+ <div class="footer">
221
+ <span>Auto-refreshes every 5s &middot; Stats are in-memory, reset on restart</span>
222
+ <span>VeilRender &middot; Headless browser rendering API</span>
223
+ </div>
224
+ </body>
225
+ </html>"""
226
+
227
+
228
+ def register(app: App) -> None:
229
+ """Register dashboard route on the app."""
230
+
231
+ @app.get("/")
232
+ async def dashboard(request: Request) -> Response:
233
+ return Response(
234
+ body=_build_html(),
235
+ status_code=200,
236
+ content_type="text/html; charset=utf-8",
237
+ )
@@ -11,7 +11,3 @@ def register(app: App) -> None:
11
11
  @app.get("/health")
12
12
  async def health(request: Request) -> JSONResponse:
13
13
  return JSONResponse({"status": "ok"})
14
-
15
- @app.get("/")
16
- async def root(request: Request) -> JSONResponse:
17
- return JSONResponse({"service": "veilrender", "status": "ok"})
@@ -4,10 +4,12 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  import re
7
+ import time
7
8
 
8
9
  from veilrender._vendor.httpserver import App, JSONResponse, Request
9
10
  from veilrender._vendor.readability import extract as readability_extract
10
11
  from veilrender._vendor.soup import Soup
12
+ from veilrender import stats
11
13
  from veilrender.auth import verify_token
12
14
  from veilrender.browser import browser_manager
13
15
  from veilrender.config import settings
@@ -70,6 +72,8 @@ def register(app: App) -> None:
70
72
 
71
73
  req = RenderRequest.from_dict(data)
72
74
  timeout = req.timeout or settings.timeout
75
+ stats.render.requests += 1
76
+ t0 = time.monotonic()
73
77
 
74
78
  try:
75
79
  async with browser_manager.get_page() as (ctx, page):
@@ -83,6 +87,8 @@ def register(app: App) -> None:
83
87
  final_url = page.url
84
88
  html = await page.content()
85
89
  except Exception as exc:
90
+ elapsed = (time.monotonic() - t0) * 1000
91
+ stats.render.record_failure(elapsed)
86
92
  logger.error("Render failed for %s: %s", req.url, exc)
87
93
  return JSONResponse(
88
94
  {"error": f"Render failed: {exc!s}"},
@@ -119,4 +125,6 @@ def register(app: App) -> None:
119
125
  links=links,
120
126
  )
121
127
 
128
+ elapsed = (time.monotonic() - t0) * 1000
129
+ stats.render.record_success(elapsed)
122
130
  return JSONResponse(result.to_dict())
@@ -3,8 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ import time
6
7
 
7
8
  from veilrender._vendor.httpserver import App, Request, Response
9
+ from veilrender import stats
8
10
  from veilrender.auth import verify_token
9
11
  from veilrender.browser import browser_manager
10
12
  from veilrender.config import settings
@@ -38,6 +40,8 @@ def register(app: App) -> None:
38
40
 
39
41
  req = ScreenshotRequest.from_dict(data)
40
42
  timeout = req.timeout or settings.timeout
43
+ stats.screenshot.requests += 1
44
+ t0 = time.monotonic()
41
45
 
42
46
  try:
43
47
  async with browser_manager.get_page(
@@ -51,6 +55,8 @@ def register(app: App) -> None:
51
55
  )
52
56
  png_bytes = await page.screenshot(full_page=req.full_page)
53
57
  except Exception as exc:
58
+ elapsed = (time.monotonic() - t0) * 1000
59
+ stats.screenshot.record_failure(elapsed)
54
60
  logger.error("Screenshot failed for %s: %s", req.url, exc)
55
61
  return Response(
56
62
  body=f'{{"error": "Screenshot failed: {exc!s}"}}'.encode(),
@@ -58,6 +64,8 @@ def register(app: App) -> None:
58
64
  content_type="application/json",
59
65
  )
60
66
 
67
+ elapsed = (time.monotonic() - t0) * 1000
68
+ stats.screenshot.record_success(elapsed)
61
69
  return Response(
62
70
  body=png_bytes,
63
71
  status_code=200,
@@ -0,0 +1,83 @@
1
+ """In-memory request statistics.
2
+
3
+ All counters are safe to mutate without locks because asyncio
4
+ is single-threaded cooperative multitasking.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from collections import deque
11
+ from dataclasses import dataclass, field
12
+
13
+ _start_time: float = time.monotonic()
14
+
15
+
16
+ @dataclass
17
+ class EndpointStats:
18
+ """Per-endpoint request counters and latency tracking."""
19
+
20
+ requests: int = 0
21
+ successes: int = 0
22
+ failures: int = 0
23
+ total_ms: float = 0.0
24
+ _latencies: deque[float] = field(default_factory=lambda: deque(maxlen=1000))
25
+
26
+ def record_success(self, elapsed_ms: float) -> None:
27
+ """Record a successful request."""
28
+ self.successes += 1
29
+ self.total_ms += elapsed_ms
30
+ self._latencies.append(elapsed_ms)
31
+
32
+ def record_failure(self, elapsed_ms: float) -> None:
33
+ """Record a failed request."""
34
+ self.failures += 1
35
+ self.total_ms += elapsed_ms
36
+ self._latencies.append(elapsed_ms)
37
+
38
+ @property
39
+ def avg_ms(self) -> float:
40
+ """Average latency in milliseconds."""
41
+ total = self.successes + self.failures
42
+ return self.total_ms / total if total > 0 else 0.0
43
+
44
+ @property
45
+ def p95_ms(self) -> float:
46
+ """95th percentile latency in milliseconds."""
47
+ if not self._latencies:
48
+ return 0.0
49
+ sorted_lat = sorted(self._latencies)
50
+ idx = int(len(sorted_lat) * 0.95)
51
+ return sorted_lat[min(idx, len(sorted_lat) - 1)]
52
+
53
+ @property
54
+ def success_rate(self) -> float:
55
+ """Success rate as a percentage."""
56
+ total = self.successes + self.failures
57
+ return (self.successes / total * 100) if total > 0 else 0.0
58
+
59
+
60
+ render = EndpointStats()
61
+ screenshot = EndpointStats()
62
+
63
+
64
+ def uptime_seconds() -> float:
65
+ """Seconds since the process started."""
66
+ return time.monotonic() - _start_time
67
+
68
+
69
+ def format_uptime() -> str:
70
+ """Human-readable uptime string."""
71
+ secs = int(uptime_seconds())
72
+ days, secs = divmod(secs, 86400)
73
+ hours, secs = divmod(secs, 3600)
74
+ mins, secs = divmod(secs, 60)
75
+ parts = []
76
+ if days:
77
+ parts.append(f"{days}d")
78
+ if hours:
79
+ parts.append(f"{hours}h")
80
+ if mins:
81
+ parts.append(f"{mins}m")
82
+ parts.append(f"{secs}s")
83
+ return " ".join(parts)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: veilrender
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Headless browser rendering API — self-hostable on HF Spaces, Docker, or bare metal
5
5
  Author: Peng Ding
6
6
  License-Expression: MIT
@@ -23,15 +23,23 @@ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
23
23
  [![PyPI version](https://img.shields.io/pypi/v/veilrender?color=green)](https://pypi.org/project/veilrender/)
24
24
  [![GitHub release](https://img.shields.io/github/v/release/Oaklight/veilrender?color=green)](https://github.com/Oaklight/veilrender/releases/latest)
25
25
  [![CI](https://github.com/Oaklight/veilrender/actions/workflows/ci.yml/badge.svg)](https://github.com/Oaklight/veilrender/actions/workflows/ci.yml)
26
- [![Docker Pulls](https://img.shields.io/docker/pulls/oaklight/veilrender)](https://hub.docker.com/r/oaklight/veilrender)
27
- [![Docker Image](https://img.shields.io/docker/v/oaklight/veilrender?sort=semver&label=docker)](https://hub.docker.com/r/oaklight/veilrender)
26
+ [![Docker Pulls](https://img.shields.io/docker/pulls/oaklight/veilrender?color=blue)](https://hub.docker.com/r/oaklight/veilrender)
27
+ [![Docker Image](https://img.shields.io/docker/v/oaklight/veilrender?sort=semver&label=docker&color=blue)](https://hub.docker.com/r/oaklight/veilrender)
28
28
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
29
- [![HF Spaces](https://img.shields.io/badge/%F0%9F%A4%97-Spaces-blue)](https://huggingface.co/spaces/oaklight/veilrender)
29
+ [![Live Demo](https://img.shields.io/badge/%F0%9F%A4%97-Live_Demo-blue)](https://oaklight-veilrender-public.hf.space)
30
30
 
31
31
  [中文](README_zh.md) | **English**
32
32
 
33
33
  Headless browser rendering API — self-hostable on HF Spaces, Docker, or bare metal.
34
34
 
35
+ ### Try it now (no auth needed)
36
+
37
+ ```bash
38
+ curl -X POST https://oaklight-veilrender-public.hf.space/render \
39
+ -H "Content-Type: application/json" \
40
+ -d '{"url": "https://example.com"}'
41
+ ```
42
+
35
43
  VeilRender accepts a URL and returns the fully rendered page content (HTML, Markdown, readability-extracted article) using a headless Chromium browser. Designed as a fallback for fetch tools that fail on JavaScript-rendered pages.
36
44
 
37
45
  ## Quick Start
@@ -39,7 +47,7 @@ VeilRender accepts a URL and returns the fully rendered page content (HTML, Mark
39
47
  ### Docker
40
48
 
41
49
  ```bash
42
- docker run -p 7860:7860 -e VEILRENDER_API_TOKEN=your-secret ghcr.io/oaklight/veilrender
50
+ docker run -p 7860:7860 -e VEILRENDER_API_TOKEN=your-secret oaklight/veilrender
43
51
  ```
44
52
 
45
53
  ### Local Development
@@ -8,6 +8,7 @@ src/veilrender/browser.py
8
8
  src/veilrender/cdp_proxy.py
9
9
  src/veilrender/config.py
10
10
  src/veilrender/models.py
11
+ src/veilrender/stats.py
11
12
  src/veilrender.egg-info/PKG-INFO
12
13
  src/veilrender.egg-info/SOURCES.txt
13
14
  src/veilrender.egg-info/dependency_links.txt
@@ -29,6 +30,7 @@ src/veilrender/_vendor/structlog.py
29
30
  src/veilrender/_vendor/useragent.py
30
31
  src/veilrender/_vendor/yaml.py
31
32
  src/veilrender/routes/__init__.py
33
+ src/veilrender/routes/dashboard.py
32
34
  src/veilrender/routes/health.py
33
35
  src/veilrender/routes/render.py
34
36
  src/veilrender/routes/screenshot.py
File without changes
File without changes