from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Iterable, Iterator, Literal, Mapping
from urllib.parse import urlsplit
from playwright.async_api import StorageStateCookie
@dataclass
[docs]
class Cookie:
"""
A dataclass containing the information about a cookie.
Please, see the MDN Web Docs for the full documentation:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
"""
"""This is the name of the cookie
that will be used to identify the cookie in the Cookie header."""
"""This is the value that will be sent with the Cookie header."""
"""This is the path from which the cookie will be readable."""
"""This is the domain from which the cookie will be readable."""
"""This is the date when the cookie will be deleted. Coded in Unix timestamp."""
"""This is the maximum age of the cookie in seconds."""
[docs]
same_site: Literal["Lax", "Strict", "None"] = "Lax"
"""This is the policy that determines whether the cookie will be sent with requests."""
"""This is whether the cookie will be sent over a secure connection."""
[docs]
http_only: bool = False
"""This is whether the cookie will be accessible to JavaScript."""
[docs]
def expires_as_datetime(self) -> datetime:
"""This is the same as the `expires` property but as a datetime object."""
return datetime.fromtimestamp(self.expires)
[docs]
def max_age_as_datetime(self) -> datetime:
"""This is the same as the `max_age` property but as a datetime object."""
return datetime.fromtimestamp(self.max_age)
[docs]
def to_playwright_like_dict(self) -> StorageStateCookie:
"""Return a dictionary compatible with Playwright StorageState cookies."""
return {
"name": self.name,
"value": self.value,
"domain": self.domain or "",
"path": self.path or "/",
"expires": float(self.expires or 0),
"httpOnly": bool(self.http_only or False),
"secure": bool(self.secure or False),
"sameSite": self.same_site,
}
@staticmethod
[docs]
def from_playwright_like_dict(data: Mapping[str, Any]) -> "Cookie":
"""Accept any mapping (dict or Playwright's StorageStateCookie)."""
return Cookie(
name=str(data["name"]),
value=str(data["value"]),
domain=str(data.get("domain") or ""),
path=str(data.get("path") or "/"),
expires=int(data.get("expires") or 0),
secure=bool(data.get("secure")),
http_only=bool(data.get("httpOnly")),
)
@dataclass
[docs]
class CookieManager:
"""Convenient jar-style wrapper + Playwright conversion."""
[docs]
storage: list[Cookie] = field(default_factory=list)
# ────── dunder helpers ──────
[docs]
def __iter__(self) -> Iterator[Cookie]:
return iter(self.storage)
[docs]
def __len__(self) -> int:
return len(self.storage)
[docs]
def __bool__(self) -> bool:
return bool(self.storage)
# ────── CRUD ──────
[docs]
def get(self, name: str, domain: str | None = None, path: str | None = None) -> Cookie | None:
"""Get a cookie by name, domain, and path."""
return next(
(
c
for c in self.storage
if c.name == name
and (domain is None or c.domain == domain)
and (path is None or c.path == path)
),
None,
)
[docs]
def get_for_domain(self, url_or_domain: str) -> list[Cookie]:
"""Get all cookies available for a domain/URL."""
host = urlsplit(url_or_domain).hostname or url_or_domain.split(":")[0]
if not host:
return []
def _match(cookie_domain: str, h: str) -> bool:
return h == cookie_domain or h.endswith("." + cookie_domain)
return [c for c in self.storage if _match(c.domain, host)]
[docs]
def add(self, cookie: Cookie | Iterable[Cookie]) -> None:
"""Add a cookie or cookies."""
def _add_one(c: Cookie) -> None:
key = (c.domain, c.path, c.name)
for i, old in enumerate(self.storage):
if (old.domain, old.path, old.name) == key:
self.storage[i] = c
break
else:
self.storage.append(c)
if isinstance(cookie, Iterable) and not isinstance(cookie, Cookie):
for c in cookie:
_add_one(c)
else:
_add_one(cookie)
[docs]
def delete(
self, name: str, domain: str | None = None, path: str | None = None
) -> Cookie | None:
"""Delete a cookie by name, domain, and path."""
for i, c in enumerate(self.storage):
if (
c.name == name
and (domain is None or c.domain == domain)
and (path is None or c.path == path)
):
return self.storage.pop(i)
return None
# ────── Playwright helpers ──────
[docs]
def to_playwright(self) -> list[StorageStateCookie]:
"""Serialize all cookies into a format understood by Playwright."""
return [c.to_playwright_like_dict() for c in self.storage]
[docs]
def add_from_playwright(self, raw_cookies: Iterable[Mapping[str, Any]]) -> None:
"""Inverse operation — add a list of Playwright cookies/mappings to the jar."""
self.add(Cookie.from_playwright_like_dict(rc) for rc in raw_cookies)