API Reference¶
cookietuner can also be used as a Python library.
Models¶
Cookie¶
from cookietuner.models import Cookie
cookie = Cookie(
domain=".example.com",
name="session",
value="abc123",
path="/",
expires=datetime(2025, 12, 31),
is_secure=True,
is_httponly=True,
same_site="lax",
)
# Computed property
cookie.is_expired # False
Fields:
| Field | Type | Description |
|---|---|---|
domain |
str |
Cookie domain |
name |
str |
Cookie name |
value |
str |
Cookie value |
path |
str |
Cookie path |
expires |
datetime \| None |
Expiration time (None for session cookies) |
is_secure |
bool |
Secure flag |
is_httponly |
bool |
HttpOnly flag |
same_site |
str \| None |
SameSite policy (none, lax, strict) |
is_expired |
bool |
Computed: whether the cookie has expired |
CapturedSession¶
What capture_session returns: the cookies, and the browser that signed in.
from cookietuner import capture_session
session = capture_session(url="https://example.com/login", until={"session_id"})
session.headers() # {"User-Agent": ..., "Accept-Language": ..., "sec-ch-ua": ...}
session.cookies # the same list[Cookie] capture() returns
Fields:
| Field | Type | Description |
|---|---|---|
cookies |
list[Cookie] |
Exactly what capture would have returned |
user_agent |
str |
The user agent string this browser sends |
client_hints |
dict[str, str] |
sec-ch-ua, sec-ch-ua-mobile and sec-ch-ua-platform, as this Chrome sends them |
high_entropy_hints |
dict[str, str] |
sec-ch-ua-full-version-list, sec-ch-ua-platform-version, sec-ch-ua-arch, sec-ch-ua-bitness and sec-ch-ua-model |
accept_language |
str \| None |
The browser's languages, rendered as it renders them: en-GB,en-US;q=0.9,en;q=0.8 |
final_url |
str \| None |
The page the capture ended on, which can carry tokens in its query string |
headers() returns the headers this browser sends without being asked: the user
agent, the language and the three low-entropy sec-ch-ua* hints. The
high-entropy ones are left out, because a site only receives those when it asks
for them with Accept-CH; send them yourself if it does.
The hints are read from navigator.userAgentData, which only exists in a secure
context. A sign-in page on plain http has none to give, so both hint
dictionaries come back empty there and headers() leaves them out, as does a
page that replaces the API with one that throws. The user agent and the language
come from the browser itself and are always there.
Chrome¶
get_cookies¶
from cookietuner.chrome import get_cookies
# Get all cookies
cookies = get_cookies()
# Filter by domain
cookies = get_cookies(domain="github.com")
# Specific profile
cookies = get_cookies(profile="Profile 1")
list_profiles¶
from cookietuner.chrome import list_profiles
profiles = list_profiles()
for p in profiles:
print(f"{p.profile_name}: {p.path}")
Capture¶
capture opens Google Chrome on a throwaway profile, waits while a person signs
in, and returns the cookies that session set. It reaches session and HttpOnly
cookies that never reach disk. The temporary profile is deleted when the capture
ends, including on timeout, cancellation or error.
capture¶
from cookietuner import Cookie, capture
cookies: list[Cookie] = capture(
url: str,
domains: Iterable[str] | None = None,
until: Iterable[str] | Callable[[list[Cookie]], bool] | None = None,
until_values: Iterable[str] | None = None,
until_url: str | None = None,
settle: float = 0.0,
timeout: float = 300.0,
passwords: bool = False,
proxy: str | None = None,
on_progress: Callable[[list[str]], None] | None = None,
)
All arguments are keyword-only.
| Argument | Type | Default | Description |
|---|---|---|---|
url |
str |
required | Page to open, usually a sign-in form |
domains |
Iterable[str] \| None |
None |
Keep only cookies for exactly these domains (leading dot ignored, subdomains don't match). Every domain is kept when empty |
until |
Iterable[str] \| Callable \| None |
None |
Cookie names or globs to wait for, or a predicate |
until_values |
Iterable[str] \| None |
None |
Value conditions, each name=glob or name!=glob |
until_url |
str \| None |
None |
Glob an open page's URL must match, or must not when prefixed with ! |
settle |
float |
0.0 |
Seconds to keep polling after the conditions first hold |
timeout |
float |
300.0 |
Seconds before giving up |
passwords |
bool |
False |
Load Apple's iCloud Passwords extension, copied from your own Chrome, to sign in from the Passwords app. Pairing is asked for on every capture, because the profile is new. This is the only option that needs access to your Chrome profile folder |
proxy |
str \| None |
None |
Proxy for the browser, e.g. socks5://127.0.0.1:1080 |
on_progress |
Callable[[list[str]], None] \| None |
None |
Called with the conditions still outstanding whenever that list changes |
Returns the cookies in effect when the wait finished, exactly as the browser had them: a chunked cookie comes back in chunks, and nothing is rewritten.
When a capture is done¶
A capture finishes on the first poll where every condition given holds at once. The browser is asked for its cookies and its open pages four times a second.
- Names (
until) match a cookie name, a glob (*,?,[seq], matched case-sensitively), or a complete chunk set:name.0,name.1, … running from.0with no gap. Nothing in a cookie says how many chunks there will be, so a set looks complete as soon as it has.0, which is whatsettleis for. Chunks are only ever completed within one domain. - Values (
until_values) need the cookie to exist and its value to match, or, with!=, not to match. The cookie existing is part of the condition either way, souid!=anon_*is not satisfied byuidbeing absent. The name side may be a glob too; the split is on the first=, so a pattern may contain more of them. - URL (
until_url) matches any openhttp/httpspage. Prefixed with!, it waits until none of them matches, which is how you wait to leave a sign-in page. Pages such asabout:blankand Chrome's own tabs are ignored, so they can't stand in for having arrived or left. - A predicate (
untilas a callable) is called on each poll with the cookies that would be returned, and covers anything the rest can't express. settlekeeps polling for that many seconds after the conditions first hold, then returns the last snapshot. Late cookies, trailing chunks and post-login redirects land in that window. Once the conditions have held, the capture is done settling even if they stop holding, and iftimeoutruns out or the browser ends while it is settling it returns what it has rather than failing.
Given no conditions at all, the capture ends when the browser does, and settle
does nothing: there is nothing for it to settle after.
Malformed conditions raise ValueError before Chrome starts, so a typo costs
nothing.
When the browser ends¶
Closing the last window, quitting Chrome (Cmd+Q) and Chrome exiting on its own
are all the same thing: the capture answers with the last snapshot it took
before the end. If conditions were given and that snapshot doesn't satisfy them,
it raises CaptureIncomplete rather than returning a partial session. The
temporary profile is removed in every case.
capture_async¶
The same thing for an async application, with the same arguments. capture is a
thin wrapper around it, so both share one implementation:
from cookietuner import capture_async
cookies = await capture_async(
url="https://example.com/login",
domains={"example.com", "auth.example.com"},
until={"sb-*-auth-token"},
until_url="*/dashboard*",
settle=2,
)
capture_session¶
The same capture as capture, returning the browser's identity alongside the
cookies. A session replayed from a browser other than the one that signed in is
what a site sees as a stolen one, so this is what makes it reusable:
import requests
from cookietuner import CapturedSession, capture_session
session: CapturedSession = capture_session(
url="https://example.com/login",
domains={"example.com"},
until={"session_id"},
until_url="*/dashboard*",
)
reuse = requests.Session()
reuse.headers.update(session.headers())
for cookie in session.cookies:
reuse.cookies.set(cookie.name, cookie.value, domain=cookie.domain, path=cookie.path)
reuse.get("https://example.com/api/me")
It takes the same arguments as capture, finishes on the same conditions and
raises the same errors. The identity is read once the window is up and before
the first poll, so it comes back whichever way the capture ends: the conditions
holding, the window closing, or Chrome quitting.
Reading it runs one expression in the page the capture opened, which is why it
is a separate function: capture touches that page not at all.
capture_session_async¶
The same thing for an async application, with the same arguments:
from cookietuner import capture_session_async
session = await capture_session_async(
url="https://example.com/login",
until={"sb-*-auth-token"},
settle=2,
)
join_chunks¶
Reassembles a cookie an auth library split into name.0, name.1, … (Supabase's
SSR helpers do this above about 4 KB). capture never rewrites cookies, so this
is how you get the value back:
from cookietuner import capture, join_chunks
cookies = capture(url="https://example.com/login", domains={"example.com"})
token = join_chunks(cookies, "sb-abcdef-auth-token") # str | None
A cookie named exactly name wins over a chunk set. Returns None if neither a
whole cookie nor a complete set is there, and raises ValueError if the cookies
given hold more than one value for name, which a capture over several domains
can produce: filter to one domain first.
main¶
The console-script entry point, cookietuner on the command line. Calling it
runs the CLI and exits the process, so a library should call capture instead.
Errors¶
CaptureError is the base class for every capture failure, and the temporary
profile is cleaned up before any of them is raised. Each carries the exit code
the CLI reports it with, as exc.exit_code, so a script can tell them apart
without reading messages. Messages name cookies and conditions, never values or
live URLs.
| Error | Exit code | Raised when |
|---|---|---|
CaptureError |
1 | Base class; also any capture failure with no more specific type |
CaptureTimeout |
3 | timeout passed before the conditions held; names the unmet ones |
CaptureIncomplete |
4 | The browser ended before the conditions held |
ChromeNotFound |
5 | No usable Google Chrome |
ProfileAccessDenied |
6 | Chrome's profile folder couldn't be read, with a hint about macOS privacy settings. Only passwords=True needs it |
PasswordsExtensionMissing |
7 | The profile was readable but the iCloud Passwords extension isn't installed to copy from |
2 is left alone, because that is what the command line itself exits with when an option is wrong.
CookieStoreUnreadable is separate, and does not subclass CaptureError: it is
raised by the store readers (chrome.get_cookies, chrome.list_profiles) when a
browser's cookie database exists but this process may not read it.
| Error | Raised when |
|---|---|
CookieStoreUnreadable |
A browser's cookie store exists but can't be read |
from cookietuner import CaptureIncomplete, CaptureTimeout, capture
try:
cookies = capture(
url="https://example.com/login",
until={"session_id"},
timeout=120,
)
except CaptureTimeout as exc:
print(f"gave up: {exc}")
except CaptureIncomplete as exc:
print(f"browser closed too soon: {exc}")
Safari¶
get_cookies¶
from cookietuner.safari import get_cookies
# Get all cookies
cookies = get_cookies()
# Filter by domain
cookies = get_cookies(domain="apple.com")
list_profiles¶
from cookietuner.safari import list_profiles
profiles = list_profiles() # Safari only has one profile
Example: Export cookies for requests¶
from cookietuner.chrome import get_cookies
import requests
# Get cookies for a specific domain
cookies = get_cookies(domain="api.example.com")
# Create a requests session with the cookies
session = requests.Session()
for cookie in cookies:
session.cookies.set(
cookie.name,
cookie.value,
domain=cookie.domain,
path=cookie.path,
)
# Use the session
response = session.get("https://api.example.com/endpoint")