Asynchronous Playwright wrappers for browser-like HTTP scenarios, controlled render flow, and API autotest integration.
- Typed wrappers over Playwright primitives:
HumanBrowserHumanContextHumanPage
HumanPage.fetch(...): execute HTTP requests from page context and get structuredFetchResponse.HumanPage.goto_render(...): render already available response payloads without duplicate upstream request.- Storage helpers:
HumanContext.local_storage()for full context snapshotHumanPage.local_storage()for current page originHumanPage.cookies()convenience alias
- Fingerprint snapshot collection:
HumanContext.fingerprint(...). - Built-in pytest autotest plugin for API clients (
@autotest, hooks, params, dependencies).
Base package:
pip install human-requests
playwright install chromiumOptional autotest addon dependencies:
pip install human-requests[autotest] pytest pytest-anyio pytest-jsonschema-snapshot pytest-subtestsIf you run with Camoufox, install it separately:
pip install camoufox
camoufox fetchimport asyncio
from playwright.async_api import async_playwright
from human_requests import HumanBrowser
async def main() -> None:
async with async_playwright() as p:
pw_browser = await p.chromium.launch(headless=True)
browser = HumanBrowser.replace(pw_browser)
ctx = await browser.new_context()
page = await ctx.new_page()
await page.goto("https://httpbin.org/html", wait_until="domcontentloaded")
print(page.url)
await browser.close()
asyncio.run(main())resp = await page.fetch("https://httpbin.org/json")
print(resp.status_code)
print(resp.json())challenge = await page.fetch("https://example.com/challenge")
await page.goto_render(challenge, wait_until="networkidle")If you want a screenshot to be taken when errors like playwright.Error (including timeouts) occur,
you can set page.on_error_screenshot_path = "screenshot.png" (this setting is page-specific; by default, screenshots are disabled).
cookies = await page.cookies()
context_storage = await ctx.local_storage()
page_storage = await page.local_storage()
print(len(cookies), context_storage.keys(), page_storage.keys())fingerprint = await ctx.fingerprint(origin="https://example.com")
print(fingerprint.user_agent)
print(fingerprint.browser_name, fingerprint.browser_version)To avoid repetitive _parent and __post_init__ wiring in SDK-style clients
(like fixprice_api / perekrestok_api), use:
ApiChild[ParentType]ApiParentapi_child_field(...)
from dataclasses import dataclass
from human_requests import ApiChild, ApiParent, api_child_field
class ClassCatalog(ApiChild["ShopApi"]):
async def tree(self):
...
class ClassGeolocation(ApiChild["ShopApi"]):
async def cities_list(self):
...
@dataclass
class ShopApi(ApiParent):
Catalog: ClassCatalog = api_child_field(ClassCatalog)
Geolocation: ClassGeolocation = api_child_field(ClassGeolocation)ApiParent initializes all api_child_field(...) values in __post_init__
automatically, so manual assignments are no longer needed.
Nested chains are supported as well (Root -> Child -> Child):
@dataclass
class BranchApi(ApiChild["RootApi"], ApiParent):
Catalog: ClassCatalog = api_child_field(ClassCatalog)
@dataclass
class RootApi(ApiParent):
Branch: BranchApi = api_child_field(BranchApi)human-requests ships with a pytest plugin that can auto-run API methods marked with @autotest and validate payloads via schemashot from pytest-jsonschema-snapshot.
With pytest-subtests, each discovered @autotest method and @autotest_data
provider is shown as a separate subtest inside test_autotest_api_methods.
Minimal pytest.ini:
[pytest]
anyio_mode = auto
autotest_start_class = your_package.StartClass
autotest_typecheck = warnautotest_typecheck modes:
off(default): no runtime type checks for params provider argumentswarn: emitRuntimeWarningon annotation mismatchstrict: fail test case withTypeErroron mismatch
Minimal fixtures:
import pytest
from your_package import StartClass
@pytest.fixture(scope="session")
def anyio_backend() -> str:
return "asyncio"
@pytest.fixture(scope="session")
async def api() -> StartClass:
async with StartClass() as client:
yield clientBusiness code only marks methods:
from human_requests import autotest
class Catalog:
@autotest
async def tree(self):
...Test layer adds hooks and params:
from human_requests import autotest_depends_on, autotest_hook, autotest_params
from human_requests.autotest import AutotestCallContext, AutotestContext
@autotest_hook(target=Catalog.tree)
def _capture_category(_resp, data, ctx: AutotestContext) -> None:
ctx.state["category_id"] = data["items"][0]["id"]
@autotest_depends_on(Catalog.tree)
@autotest_params(target=Catalog.feed)
def _feed_params(ctx: AutotestCallContext) -> dict[str, int]:
return {"category_id": ctx.state["category_id"]}Parent-specific registration is supported:
@autotest_hook(target=Child.method, parent=ParentA)
def _only_for_parent_a(_resp, data, ctx):
...For a complete guide, see docs/source/autotest.rst.
Setup:
git clone https://github.com/Miskler/human-requests.git
cd human-requests
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
pip install -e .Commands:
pytest
make lint
make type-check
make format
make docs