Coverage for human_requests/abstraction/cookies.py: 82%
73 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-13 21:41 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-13 21:41 +0000
1from dataclasses import dataclass, field
2from datetime import datetime
3from typing import Any, Iterable, Iterator, Literal, Mapping
4from urllib.parse import urlsplit
6from playwright.async_api import StorageStateCookie
9@dataclass
10class Cookie:
11 """
12 A dataclass containing the information about a cookie.
14 Please, see the MDN Web Docs for the full documentation:
15 https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
16 """
18 name: str
19 """This is the name of the cookie
20 that will be used to identify the cookie in the Cookie header."""
22 value: str
23 """This is the value that will be sent with the Cookie header."""
25 path: str = "/"
26 """This is the path from which the cookie will be readable."""
28 domain: str = ""
29 """This is the domain from which the cookie will be readable."""
31 expires: int = 0
32 """This is the date when the cookie will be deleted. Coded in Unix timestamp."""
34 max_age: int = 0
35 """This is the maximum age of the cookie in seconds."""
37 same_site: Literal["Lax", "Strict", "None"] = "Lax"
38 """This is the policy that determines whether the cookie will be sent with requests."""
40 secure: bool = False
41 """This is whether the cookie will be sent over a secure connection."""
43 http_only: bool = False
44 """This is whether the cookie will be accessible to JavaScript."""
46 def expires_as_datetime(self) -> datetime:
47 """This is the same as the `expires` property but as a datetime object."""
48 return datetime.fromtimestamp(self.expires)
50 def max_age_as_datetime(self) -> datetime:
51 """This is the same as the `max_age` property but as a datetime object."""
52 return datetime.fromtimestamp(self.max_age)
54 def to_playwright_like_dict(self) -> StorageStateCookie:
55 """Return a dictionary compatible with Playwright StorageState cookies."""
56 return {
57 "name": self.name,
58 "value": self.value,
59 "domain": self.domain or "",
60 "path": self.path or "/",
61 "expires": float(self.expires or 0),
62 "httpOnly": bool(self.http_only or False),
63 "secure": bool(self.secure or False),
64 "sameSite": self.same_site,
65 }
67 @staticmethod
68 def from_playwright_like_dict(data: Mapping[str, Any]) -> "Cookie":
69 """Accept any mapping (dict or Playwright's StorageStateCookie)."""
70 return Cookie(
71 name=str(data["name"]),
72 value=str(data["value"]),
73 domain=str(data.get("domain") or ""),
74 path=str(data.get("path") or "/"),
75 expires=int(data.get("expires") or 0),
76 secure=bool(data.get("secure")),
77 http_only=bool(data.get("httpOnly")),
78 )
81@dataclass
82class CookieManager:
83 """Convenient jar-style wrapper + Playwright conversion."""
85 storage: list[Cookie] = field(default_factory=list)
87 # ────── dunder helpers ──────
88 def __iter__(self) -> Iterator[Cookie]:
89 return iter(self.storage)
91 def __len__(self) -> int:
92 return len(self.storage)
94 def __bool__(self) -> bool:
95 return bool(self.storage)
97 # ────── CRUD ──────
98 def get(self, name: str, domain: str | None = None, path: str | None = None) -> Cookie | None:
99 """Get a cookie by name, domain, and path."""
100 return next(
101 (
102 c
103 for c in self.storage
104 if c.name == name
105 and (domain is None or c.domain == domain)
106 and (path is None or c.path == path)
107 ),
108 None,
109 )
111 def get_for_domain(self, url_or_domain: str) -> list[Cookie]:
112 """Get all cookies available for a domain/URL."""
113 host = urlsplit(url_or_domain).hostname or url_or_domain.split(":")[0]
114 if not host:
115 return []
117 def _match(cookie_domain: str, h: str) -> bool:
118 return h == cookie_domain or h.endswith("." + cookie_domain)
120 return [c for c in self.storage if _match(c.domain, host)]
122 def add(self, cookie: Cookie | Iterable[Cookie]) -> None:
123 """Add a cookie or cookies."""
125 def _add_one(c: Cookie) -> None:
126 key = (c.domain, c.path, c.name)
127 for i, old in enumerate(self.storage):
128 if (old.domain, old.path, old.name) == key:
129 self.storage[i] = c
130 break
131 else:
132 self.storage.append(c)
134 if isinstance(cookie, Iterable) and not isinstance(cookie, Cookie):
135 for c in cookie:
136 _add_one(c)
137 else:
138 _add_one(cookie)
140 def delete(
141 self, name: str, domain: str | None = None, path: str | None = None
142 ) -> Cookie | None:
143 """Delete a cookie by name, domain, and path."""
144 for i, c in enumerate(self.storage):
145 if (
146 c.name == name
147 and (domain is None or c.domain == domain)
148 and (path is None or c.path == path)
149 ):
150 return self.storage.pop(i)
151 return None
153 # ────── Playwright helpers ──────
154 def to_playwright(self) -> list[StorageStateCookie]:
155 """Serialize all cookies into a format understood by Playwright."""
156 return [c.to_playwright_like_dict() for c in self.storage]
158 def add_from_playwright(self, raw_cookies: Iterable[Mapping[str, Any]]) -> None:
159 """Inverse operation — add a list of Playwright cookies/mappings to the jar."""
160 self.add(Cookie.from_playwright_like_dict(rc) for rc in raw_cookies)