use crate::browser::Browser;
use crate::params::SearchParams;
use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList};
fn block_on<F, T, E>(future: F) -> PyResult<T>
where
F: std::future::Future<Output = Result<T, E>>,
E: std::fmt::Display,
{
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| PyRuntimeError::new_err(e.to_string()))?
.block_on(future)
.map_err(|e| PyRuntimeError::new_err(e.to_string()))
}
fn parse_region(code: &str) -> PyResult<crate::params::Region> {
use crate::params::Region;
match code {
"xa-ar" => Ok(Region::XaAr),
"xa-en" => Ok(Region::XaEn),
"ar-es" => Ok(Region::ArEs),
"au-en" => Ok(Region::AuEn),
"at-de" => Ok(Region::AtDe),
"be-fr" => Ok(Region::BeFr),
"be-nl" => Ok(Region::BeNl),
"br-pt" => Ok(Region::BrPt),
"bg-bg" => Ok(Region::BgBg),
"ca-en" => Ok(Region::CaEn),
"ca-fr" => Ok(Region::CaFr),
"ct-ca" => Ok(Region::CtCa),
"cl-es" => Ok(Region::ClEs),
"cn-zh" => Ok(Region::CnZh),
"co-es" => Ok(Region::CoEs),
"hr-hr" => Ok(Region::HrHr),
"cz-cs" => Ok(Region::CzCs),
"dk-da" => Ok(Region::DkDa),
"ee-et" => Ok(Region::EeEt),
"fi-fi" => Ok(Region::FiFi),
"fr-fr" => Ok(Region::FrFr),
"de-de" => Ok(Region::DeDe),
"gr-el" => Ok(Region::GrEl),
"hk-tzh" => Ok(Region::HkTzh),
"hu-hu" => Ok(Region::HuHu),
"in-en" => Ok(Region::InEn),
"id-id" => Ok(Region::IdId),
"id-en" => Ok(Region::IdEn),
"ie-en" => Ok(Region::IeEn),
"il-he" => Ok(Region::IlHe),
"it-it" => Ok(Region::ItIt),
"jp-jp" => Ok(Region::JpJp),
"kr-kr" => Ok(Region::KrKr),
"lv-lv" => Ok(Region::LvLv),
"lt-lt" => Ok(Region::LtLt),
"xl-es" => Ok(Region::XlEs),
"my-ms" => Ok(Region::MyMs),
"my-en" => Ok(Region::MyEn),
"mx-es" => Ok(Region::MxEs),
"nl-nl" => Ok(Region::NlNl),
"nz-en" => Ok(Region::NzEn),
"no-no" => Ok(Region::NoNo),
"pe-es" => Ok(Region::PeEs),
"ph-en" => Ok(Region::PhEn),
"ph-tl" => Ok(Region::PhTl),
"pl-pl" => Ok(Region::PlPl),
"pt-pt" => Ok(Region::PtPt),
"ro-ro" => Ok(Region::RoRo),
"ru-ru" => Ok(Region::RuRu),
"sg-en" => Ok(Region::SgEn),
"sk-sk" => Ok(Region::SkSk),
"sl-sl" => Ok(Region::SlSl),
"za-en" => Ok(Region::ZaEn),
"es-es" => Ok(Region::EsEs),
"se-sv" => Ok(Region::SeSv),
"ch-de" => Ok(Region::ChDe),
"ch-fr" => Ok(Region::ChFr),
"ch-it" => Ok(Region::ChIt),
"tw-tzh" => Ok(Region::TwTzh),
"th-th" => Ok(Region::ThTh),
"tr-tr" => Ok(Region::TrTr),
"ua-uk" => Ok(Region::UaUk),
"uk-en" => Ok(Region::UkEn),
"us-en" => Ok(Region::UsEn),
"ue-es" => Ok(Region::UeEs),
"ve-es" => Ok(Region::VeEs),
"vn-vi" => Ok(Region::VnVi),
"wt-wt" => Ok(Region::WtWt),
other => Err(PyRuntimeError::new_err(format!(
"Unknown region code: '{other}'. See https://duckduckgo.com/duckduckgo-help-pages/settings/params/ for valid values."
))),
}
}
#[pyclass(name = "LiteSearchResult", frozen, skip_from_py_object)]
#[derive(Debug, Clone)]
pub struct PyLiteSearchResult {
#[pyo3(get)]
pub title: String,
#[pyo3(get)]
pub url: String,
#[pyo3(get)]
pub snippet: String,
}
#[pymethods]
impl PyLiteSearchResult {
pub fn __repr__(&self) -> String {
format!(
"LiteSearchResult(title={:?}, url={:?})",
self.title, self.url
)
}
}
#[pyclass(name = "ImageResult", frozen, skip_from_py_object)]
#[derive(Debug, Clone)]
pub struct PyImageResult {
#[pyo3(get)]
pub title: String,
#[pyo3(get)]
pub image: String,
#[pyo3(get)]
pub thumbnail: String,
#[pyo3(get)]
pub url: String,
#[pyo3(get)]
pub height: u32,
#[pyo3(get)]
pub width: u32,
#[pyo3(get)]
pub source: String,
}
#[pymethods]
impl PyImageResult {
pub fn __repr__(&self) -> String {
format!("ImageResult(title={:?}, url={:?})", self.title, self.url)
}
}
#[pyclass(name = "NewsResult", frozen, skip_from_py_object)]
#[derive(Debug, Clone)]
pub struct PyNewsResult {
#[pyo3(get)]
pub date: String,
#[pyo3(get)]
pub title: String,
#[pyo3(get)]
pub body: String,
#[pyo3(get)]
pub url: String,
#[pyo3(get)]
pub image: Option<String>,
#[pyo3(get)]
pub source: String,
}
#[pymethods]
impl PyNewsResult {
pub fn __repr__(&self) -> String {
format!(
"NewsResult(title={:?}, url={:?}, date={:?})",
self.title, self.url, self.date
)
}
}
#[pyclass(name = "SearchParams", skip_from_py_object)]
#[derive(Debug, Clone, Default)]
pub struct PySearchParams {
pub(crate) inner: SearchParams,
}
#[pymethods]
impl PySearchParams {
#[new]
pub fn new() -> Self {
Self::default()
}
pub fn region(&self, code: String) -> PyResult<PySearchParams> {
let region = parse_region(&code)?;
Ok(Self {
inner: self.inner.clone().region(region),
})
}
pub fn safe_search(&self, level: String) -> PyResult<PySearchParams> {
use crate::params::SafeSearch;
let safe = match level.as_str() {
"on" => SafeSearch::On,
"moderate" => SafeSearch::Moderate,
"off" => SafeSearch::Off,
other => {
return Err(PyRuntimeError::new_err(format!(
"Unknown safe search level: '{other}'. Expected 'on', 'moderate', or 'off'."
)));
}
};
Ok(Self {
inner: self.inner.clone().safe_search(safe),
})
}
pub fn theme(&self, name: String) -> PySearchParams {
use crate::params::Theme;
let theme = match name.as_str() {
"default" => Theme::Default,
"contrast" => Theme::Contrast,
"retro" => Theme::Retro,
"dark" => Theme::Dark,
"terminal" => Theme::Terminal,
other => Theme::Custom(other.to_string()),
};
Self {
inner: self.inner.clone().theme(theme),
}
}
pub fn source(&self, src: String) -> PySearchParams {
Self {
inner: self.inner.clone().source(src),
}
}
pub fn header_color(&self, color: String) -> PySearchParams {
Self {
inner: self.inner.clone().header_color(color),
}
}
pub fn url_color(&self, color: String) -> PySearchParams {
Self {
inner: self.inner.clone().url_color(color),
}
}
pub fn background_color(&self, color: String) -> PySearchParams {
Self {
inner: self.inner.clone().background_color(color),
}
}
pub fn text_color(&self, color: String) -> PySearchParams {
Self {
inner: self.inner.clone().text_color(color),
}
}
pub fn link_color(&self, color: String) -> PySearchParams {
Self {
inner: self.inner.clone().link_color(color),
}
}
pub fn visited_link_color(&self, color: String) -> PySearchParams {
Self {
inner: self.inner.clone().visited_link_color(color),
}
}
pub fn to_query_pairs(&self) -> Vec<(String, String)> {
self.inner
.to_query_pairs()
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect()
}
pub fn __repr__(&self) -> String {
format!("SearchParams({:?})", self.inner.to_query_pairs())
}
}
#[pyclass(name = "Browser")]
pub struct PyBrowser {
inner: Browser,
}
#[pymethods]
impl PyBrowser {
#[new]
#[pyo3(signature = (user_agent=None, cookie_store=false, proxy=None))]
pub fn new(
user_agent: Option<String>,
cookie_store: bool,
proxy: Option<String>,
) -> PyResult<Self> {
let mut builder = Browser::builder();
if let Some(agent) = user_agent {
builder = builder.user_agent(agent);
}
if cookie_store {
builder = builder.cookie_store(true);
}
if let Some(proxy_url) = proxy {
builder = builder.proxy(proxy_url);
}
let inner = builder
.build()
.map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
Ok(Self { inner })
}
#[pyo3(signature = (query, region="wt-wt", limit=None, user_agent=""))]
pub fn lite_search(
&self,
query: String,
region: &str,
limit: Option<usize>,
user_agent: &str,
) -> PyResult<Vec<PyLiteSearchResult>> {
let results = block_on(self.inner.lite_search(&query, region, limit, user_agent))?;
Ok(results
.into_iter()
.map(|r| PyLiteSearchResult {
title: r.title,
url: r.url,
snippet: r.snippet,
})
.collect())
}
#[pyo3(signature = (query, region="wt-wt", safesearch=true, limit=None, user_agent=""))]
pub fn images(
&self,
query: String,
region: &str,
safesearch: bool,
limit: Option<usize>,
user_agent: &str,
) -> PyResult<Vec<PyImageResult>> {
let results = block_on(
self.inner
.images(&query, region, safesearch, limit, user_agent),
)?;
Ok(results
.into_iter()
.map(|r| PyImageResult {
title: r.title,
image: r.image,
thumbnail: r.thumbnail,
url: r.url,
height: r.height,
width: r.width,
source: r.source,
})
.collect())
}
#[pyo3(signature = (query, region="wt-wt", safesearch=true, limit=None, user_agent=""))]
pub fn news(
&self,
query: String,
region: &str,
safesearch: bool,
limit: Option<usize>,
user_agent: &str,
) -> PyResult<Vec<PyNewsResult>> {
let results = block_on(
self.inner
.news(&query, region, safesearch, limit, user_agent),
)?;
Ok(results
.into_iter()
.map(|r| PyNewsResult {
date: r.date,
title: r.title,
body: r.body,
url: r.url,
image: r.image,
source: r.source,
})
.collect())
}
#[pyo3(signature = (query, params=None))]
pub fn instant_answer(
&self,
py: Python<'_>,
query: String,
params: Option<PyRef<'_, PySearchParams>>,
) -> PyResult<Py<PyDict>> {
let path = format!("?q={}", urlencoding::encode(&query));
let search_params = params.as_deref().map(|p| &p.inner);
let resp = block_on(self.inner.get_api_response(&path, search_params))?;
let d = PyDict::new(py);
d.set_item("heading", resp.heading)?;
d.set_item("abstract_text", resp.abstract_text)?;
d.set_item("abstract_source", resp.abstract_source)?;
d.set_item("abstract_url", resp.abstract_url)?;
d.set_item("answer", resp.answer)?;
d.set_item("answer_type", resp.answer_type)?;
d.set_item("definition", resp.definition)?;
d.set_item("definition_source", resp.definition_source)?;
d.set_item("definition_url", resp.definition_url)?;
d.set_item("entity", resp.entity)?;
d.set_item("image", resp.image)?;
d.set_item("redirect", resp.redirect)?;
d.set_item("type", resp.r#type)?;
let topics = PyList::empty(py);
for topic in resp.related_topics {
let t = PyDict::new(py);
t.set_item("text", topic.text)?;
t.set_item("first_url", topic.first_url)?;
t.set_item("url", topic.url)?;
t.set_item("result", topic.result)?;
topics.append(t)?;
}
d.set_item("related_topics", topics)?;
Ok(d.unbind())
}
}
pub fn register_python_module(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyLiteSearchResult>()?;
m.add_class::<PyImageResult>()?;
m.add_class::<PyNewsResult>()?;
m.add_class::<PySearchParams>()?;
m.add_class::<PyBrowser>()?;
Ok(())
}