Repository navigation
Quick Start
English · 中文
Each Context is its own process with a persistent JavaScript world. Use it as a context manager so the process is always cleaned up.
from ruyijs import Context
with Context() as context:
context.eval("let count = 40")
print(context.eval("count += 2")) # 42
print(context.eval("navigator.userAgent"))Variables and functions persist across eval calls within the same context. Objects and arrays come back as Python dict and list; JavaScript null becomes None and undefined becomes ruyijs.UNDEFINED.
with Context() as context:
context.set("config", {"offset": 2})
add = context.eval("(value) => value + config.offset", to_py=False)
print(add(40)) # 42set() copies a Python value onto the page global. eval(..., to_py=False) keeps functions, DOM nodes and class instances as live JSFunction / JSObject handles that you can call or read from Python.
with Context() as context:
context.expose(lambda a, b: a + b, "add")
print(context.eval("add(20, 22)")) # 42The callback runs while the script waits for it. It must return within the context's timeout, and it must not call back into the same context (that raises ReentrantError).
from ruyijs import Context, JSError
with Context(timeout=10) as context:
try:
context.eval("throw new TypeError('bad input')", filename="demo.js")
except JSError as error:
print(error.name, error.message) # TypeError bad input
print(error.stack)A JSError leaves the context usable.
with Context(timeout=60) as context:
context.put_resource("https://smoke.test/app.js",
"window.loaded = document.title + ':' + location.host;")
context.load_page("https://smoke.test/",
"<!doctype html><title>smoke</title><script src='/app.js'></script>")
print(context.eval("window.loaded")) # smoke:smoke.testload_page(url, html) makes url the document address, so relative URLs resolve against it. Parser-inserted classic scripts run in order, then DOMContentLoaded and load fire. Scripts with src are read from the resource directory (put_resource) or from the network. If a page script throws, load_page raises the first error after the load finishes.
with Context() as context:
context.configure_clock("logical", epoch_ms=1700000000000)
context.eval("globalThis.result = 0; setTimeout(() => { result = 42 }, 10)")
context.advance(10)
print(context.eval("result")) # 42The host drives time. Timers, network callbacks and navigations make progress when you call advance(ms) (or tick() / drain_timers()). With the logical clock time moves forward instantly and runs are reproducible; with the system clock advance() waits in real time.
with Context(proxy="http://127.0.0.1:7890", timeout=60) as context:
context.configure_clock("system")
context.navigate("https://example.com/")
for _ in range(40): # up to ~10 s
context.advance(250, max_tasks=500)
if context.eval("location.host === 'example.com' && document.readyState === 'complete'"):
break
print(context.eval("document.title"))navigate() makes the top-level navigation the browser itself would make (no Referer, Sec-Fetch-Site: none, Sec-Fetch-User: ?1); advance() lets it fetch and commit. Leave out proxy= for a direct connection. The CreepJS and Akamai examples use the same pattern: system clock, navigate(), then a loop of advance() calls.
In-page requests work the same way — start them with eval, then advance:
with Context(proxy="http://127.0.0.1:7890", timeout=30, tls="120") as context:
context.load_page("https://example.com/", "")
context.eval("globalThis.r = null; fetch('/api').then(x => x.json()).then(v => r = v)")
context.advance(25) # drive the event loop until it settles
print(context.eval("r"))with Context() as context:
context.configure_fingerprint({
"navigator": {"language": "zh-CN", "hardwareConcurrency": 8},
"screen": {"width": 1920, "height": 1080},
"viewport": {"width": 1280, "height": 720},
"locale": {"timezone": "Asia/Shanghai"},
})
print(context.eval("[navigator.language, screen.width]")) # ['zh-CN', 1920]Apply the profile before the first page operation so every script observes the same values. See Fingerprint and Environment.
records = context.console_log() # [{"realm", "method", "level", "text", "indent", "stack"}, ...]
requests = context.net_log() # [{"id", "method", "url", "requestBody", "status", "matched"}, ...]- Prefer
with Context() as context:. Outside awithblock, callcontext.close(); repeated calls have no effect. - A timeout or a broken pipe closes the context and kills its process; any later call raises
RuntimeError: ruyijs Context is closed. Create a newContext. - Contexts share nothing: each has its own process, JavaScript world, cookies, resource directory and proxy.
Next: API Reference · Examples
ruyiJS · GitHub · PyPI · LinuxDO · License: AGPL-3.0-only or commercial