Spaces:
Sleeping
Sleeping
Commit
·
6f8bc75
0
Parent(s):
Initial commit
Browse files- Dockerfile +24 -0
- pyproject.toml +18 -0
- requirements.txt +0 -0
- screenshot/__init__.py +0 -0
- screenshot/main.py +10 -0
- screenshot/routers/__init__.py +0 -0
- screenshot/routers/screenshot.py +93 -0
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")
|