SilentWraith commited on
Commit
6f8bc75
0 Parent(s):

Initial commit

Browse files
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11 AS builder
2
+
3
+ WORKDIR /app
4
+
5
+ RUN python3 -m venv venv
6
+ ENV VIRTUAL_ENV=/app/venv
7
+ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
8
+
9
+ COPY requirements.txt .
10
+ RUN pip install -r requirements.txt
11
+
12
+ # Stage 2
13
+ FROM python:3.11 AS runner
14
+
15
+ WORKDIR /app
16
+
17
+ COPY --from=builder /app/venv venv
18
+
19
+ ENV VIRTUAL_ENV=/app/venv
20
+ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
21
+
22
+ EXPOSE 8000
23
+
24
+ CMD [ "python screenshot/main.py" ]
pyproject.toml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.ruff]
2
+ line-length = 100
3
+
4
+ [tool.ruff.lint]
5
+ select = ["ALL"]
6
+ ignore = [
7
+ "CPY001", # copyright above code
8
+ "D", # sphinx not support
9
+ ]
10
+
11
+ [tool.mypy]
12
+ disallow_untyped_defs = true
13
+ show_error_codes = true
14
+ no_implicit_optional = true
15
+ warn_return_any = true
16
+ warn_unused_ignores = true
17
+ exclude = ["tests"]
18
+ python_version = "3.10"
requirements.txt ADDED
File without changes
screenshot/__init__.py ADDED
File without changes
screenshot/main.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import uvicorn
2
+ from fastapi import FastAPI
3
+ from routers.screenshot import routers as screenshot_router
4
+
5
+ app = FastAPI()
6
+
7
+ app.include_router(screenshot_router)
8
+
9
+ if __name__ == "__main__":
10
+ uvicorn.run(app, host="0.0.0.0", port=8000) # noqa: S104
screenshot/routers/__init__.py ADDED
File without changes
screenshot/routers/screenshot.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import IO, TYPE_CHECKING, AsyncContextManager, Literal
5
+
6
+ from fastapi import APIRouter, HTTPException
7
+ from fastapi.responses import Response
8
+ from playwright.async_api import BrowserContext, TimeoutError, async_playwright
9
+ from pydantic import BaseModel, HttpUrl
10
+
11
+ if TYPE_CHECKING:
12
+ from types import TracebackType
13
+
14
+
15
+ router = APIRouter()
16
+
17
+
18
+ class ViewPort(BaseModel):
19
+ width: int = 1280
20
+ height: int = 720
21
+
22
+
23
+ class ScreenshotItems(BaseModel):
24
+ url: HttpUrl
25
+ full_page: bool | None = False
26
+ query_selector: str | None = None
27
+
28
+ viewport: ViewPort | None = None
29
+ color_scheme: Literal["light", "dark", "no-preference"] | None = "no-preference"
30
+ bypass_csp: bool | None = False
31
+ java_script_enabled: bool | None = True
32
+ proxy: dict | None = None
33
+ is_mobile: bool | None = False
34
+ no_viewport: bool | None = False
35
+
36
+
37
+ class ScreenShot:
38
+ async def __aenter__(self) -> AsyncContextManager[ScreenShot]:
39
+ self.playwright = await async_playwright().start()
40
+ self.browser = await self.playwright.chromium.launch(
41
+ args=["--disable-extensions"],
42
+ chromium_sandbox=True,
43
+ )
44
+ return self
45
+
46
+ async def browser_context(self, items: ScreenshotItems) -> BrowserContext:
47
+ return await self.browser.new_context(
48
+ viewport=items.viewport.model_dump() if items.viewport else None,
49
+ color_scheme=items.color_scheme,
50
+ bypass_csp=items.bypass_csp,
51
+ java_script_enabled=items.java_script_enabled,
52
+ proxy=items.proxy.model_dump() if items.proxy else None,
53
+ is_mobile=items.is_mobile,
54
+ no_viewport=items.no_viewport,
55
+ )
56
+
57
+ async def capture(self, items: ScreenshotItems) -> IO[bytes]:
58
+ context: BrowserContext = await self.browser_context(items)
59
+ page = await context.new_page()
60
+ await page.goto(str(items.url))
61
+
62
+ if items.query_selector:
63
+ page = page.locator(items.query_selector)
64
+
65
+ screenshot_data = await page.screenshot(full_page=items.full_page)
66
+ await context.close()
67
+ return screenshot_data
68
+
69
+ async def __aexit__(
70
+ self,
71
+ typ: type[BaseException] | None,
72
+ exc: BaseException | None,
73
+ tb: TracebackType | None,
74
+ ) -> None:
75
+ if self.browser:
76
+ await self.browser.close()
77
+ if self.playwright:
78
+ await self.playwright.stop()
79
+
80
+
81
+ @router.post("/screenshot")
82
+ async def screenshot(data: ScreenshotItems) -> Response:
83
+ async with ScreenShot() as sc:
84
+ try:
85
+ response = await sc.capture(items=data)
86
+ return Response(content=response, media_type="image/png")
87
+ except TimeoutError as e:
88
+ raise HTTPException(
89
+ status_code=504,
90
+ detail=f"An error occurred while generating the screenshot: {e}",
91
+ ) from e
92
+ except Exception:
93
+ logging.exception("screenshot unhandled error")