use pyo3::prelude::*;
use pyo3::types::{PyFloat, PyList, PyString};
#[pyclass(name = "Cookie")]
#[derive(Debug, Clone)]
pub struct PyCookie {
inner: eggfetch_core::cookie::Cookie,
}
impl PyCookie {
pub fn from_core(cookie: eggfetch_core::cookie::Cookie) -> Self {
Self { inner: cookie }
}
pub fn inner(&self) -> &eggfetch_core::cookie::Cookie {
&self.inner
}
}
#[pymethods]
impl PyCookie {
#[getter]
fn name(&self) -> &str {
self.inner.name()
}
#[getter]
fn value(&self) -> &str {
self.inner.value()
}
#[getter]
fn domain(&self) -> &str {
self.inner.domain()
}
#[getter]
fn is_host_only(&self) -> bool {
self.inner.is_host_only()
}
#[getter]
fn path(&self) -> &str {
self.inner.path()
}
#[getter]
fn is_secure(&self) -> bool {
self.inner.is_secure()
}
#[getter]
fn is_http_only(&self) -> bool {
self.inner.is_http_only()
}
#[getter]
fn same_site<'py>(&self, py: Python<'py>) -> Bound<'py, PyAny> {
match self.inner.same_site() {
Some(eggfetch_core::cookie::SameSite::Strict) => PyString::new(py, "Strict").into_any(),
Some(eggfetch_core::cookie::SameSite::Lax) => PyString::new(py, "Lax").into_any(),
Some(eggfetch_core::cookie::SameSite::None) => PyString::new(py, "None").into_any(),
None => py.None().into_bound(py).into_any(),
}
}
#[getter]
fn expires<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
match self.inner.expires() {
Some(t) => {
let secs = t
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?
.as_secs_f64();
Ok(PyFloat::new(py, secs).into_any())
}
None => Ok(py.None().into_bound(py).into_any()),
}
}
#[getter]
fn is_persistent(&self) -> bool {
self.inner.is_persistent()
}
#[getter]
fn name_value_pair(&self) -> String {
self.inner.name_value_pair()
}
fn __repr__(&self) -> String {
format!(
"<Cookie name='{}' domain='{}' path='{}'>",
self.inner.name(),
self.inner.domain(),
self.inner.path()
)
}
fn __str__(&self) -> String {
self.inner.name_value_pair()
}
}
#[pyclass(name = "Cookies")]
#[derive(Debug, Clone)]
pub struct PyCookies {
jar: eggfetch_core::cookie::CookieJar,
}
impl PyCookies {
pub fn from_jar(jar: eggfetch_core::cookie::CookieJar) -> Self {
Self { jar }
}
}
#[pymethods]
impl PyCookies {
#[new]
fn py_new() -> Self {
Self {
jar: eggfetch_core::cookie::CookieJar::new(),
}
}
fn __len__(&self) -> usize {
self.jar.len()
}
fn __iter__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let names: Vec<PyObject> = self
.jar
.all_cookies()
.iter()
.map(|c| PyString::new(py, c.name()).into())
.collect();
let list = PyList::new(py, names)?;
py.import("builtins")?.getattr("iter")?.call1((list,))
}
fn __contains__(&self, name: &str) -> bool {
self.jar.get(name, None, None).is_some()
}
fn __getitem__(&self, name: &str) -> PyResult<PyCookie> {
self.jar
.get(name, None, None)
.map(PyCookie::from_core)
.ok_or_else(|| {
PyErr::new::<pyo3::exceptions::PyKeyError, _>(format!("cookie '{name}' not found"))
})
}
fn __setitem__(&self, name: &str, cookie: &Bound<'_, PyAny>) -> PyResult<()> {
let py_cookie: PyCookie = cookie.extract()?;
if py_cookie.inner.name() != name {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
"cookie mapping key must match cookie.name",
));
}
self.jar
.set(py_cookie.inner.clone())
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
Ok(())
}
fn __delitem__(&self, name: &str) -> PyResult<()> {
let cookies = self.jar.all_cookies();
let matching: Vec<_> = cookies.iter().filter(|c| c.name() == name).collect();
let cookie = match matching.as_slice() {
[] => {
return Err(PyErr::new::<pyo3::exceptions::PyKeyError, _>(format!(
"cookie '{name}' not found"
)))
}
[cookie] => *cookie,
_ => {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"cookie '{name}' is ambiguous; specify its domain and path"
)))
}
};
self.jar.delete(name, cookie.domain(), cookie.path());
Ok(())
}
#[pyo3(signature = (name, default=None))]
fn get<'py>(
&self,
py: Python<'py>,
name: &str,
default: Option<&Bound<'py, PyAny>>,
) -> PyResult<Bound<'py, PyAny>> {
match self.jar.get(name, None, None) {
Some(cookie) => Ok(Py::new(py, PyCookie::from_core(cookie))?
.into_bound(py)
.into_any()),
None => match default {
Some(d) => Ok(d.clone().into_any()),
None => Ok(py.None().into_bound(py).into_any()),
},
}
}
#[pyo3(signature = (name, value, *, domain=None, path="/"))]
fn set(
&self,
name: &str,
value: &str,
domain: Option<&str>,
path: Option<&str>,
) -> PyResult<()> {
let url_str = format!(
"http://{}{}",
domain.unwrap_or("localhost"),
path.unwrap_or("/")
);
let url = url::Url::parse(&url_str)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
let temp_jar = eggfetch_core::cookie::CookieJar::new();
let set_cookie = if let Some(d) = domain {
format!("{name}={value}; Domain={d}; Path={}", path.unwrap_or("/"))
} else {
format!("{name}={value}; Path={}", path.unwrap_or("/"))
};
temp_jar.update_from_response(&url, &[set_cookie]);
if let Some(cookie) = temp_jar.all_cookies().into_iter().next() {
if cookie.name() != name || cookie.value() != value {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
"invalid cookie name, value, domain, or path",
));
}
self.jar
.set(cookie)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
} else {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
"invalid cookie name, value, domain, or path",
));
}
Ok(())
}
fn clear(&self) {
self.jar.clear();
}
fn values<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
let cookies: Vec<PyCookie> = self
.jar
.all_cookies()
.into_iter()
.map(PyCookie::from_core)
.collect();
PyList::new(py, cookies)
}
fn items<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
let py_tuple = py.import("builtins")?.getattr("tuple")?;
let items: Vec<PyObject> = self
.jar
.all_cookies()
.into_iter()
.map(|c| {
let name: PyObject = PyString::new(py, c.name()).into();
let cookie: PyObject = Py::new(py, PyCookie::from_core(c))?.into_any();
let tup = py_tuple.call1((PyList::new(py, [name, cookie])?,))?;
Ok(tup.into())
})
.collect::<PyResult<Vec<_>>>()?;
PyList::new(py, items)
}
fn __repr__(&self) -> String {
format!("Cookies({})", self.jar.len())
}
}