|
1 | 1 | use std::collections::hash_map::DefaultHasher;
|
| 2 | +use std::fmt; |
| 3 | +use std::fmt::Formatter; |
2 | 4 | use std::hash::{Hash, Hasher};
|
3 | 5 |
|
4 | 6 | use idna::punycode::decode_to_string;
|
| 7 | +use pyo3::exceptions::PyValueError; |
5 | 8 | use pyo3::once_cell::GILOnceCell;
|
6 |
| -use pyo3::prelude::*; |
7 | 9 | use pyo3::pyclass::CompareOp;
|
8 |
| -use pyo3::types::PyDict; |
| 10 | +use pyo3::types::{PyDict, PyType}; |
| 11 | +use pyo3::{intern, prelude::*}; |
9 | 12 | use url::Url;
|
10 | 13 |
|
| 14 | +use crate::tools::SchemaDict; |
11 | 15 | use crate::SchemaValidator;
|
12 | 16 |
|
13 | 17 | static SCHEMA_DEFINITION_URL: GILOnceCell<SchemaValidator> = GILOnceCell::new();
|
@@ -150,6 +154,42 @@ impl PyUrl {
|
150 | 154 | fn __getnewargs__(&self) -> (&str,) {
|
151 | 155 | (self.__str__(),)
|
152 | 156 | }
|
| 157 | + |
| 158 | + #[classmethod] |
| 159 | + #[pyo3(signature=(*, scheme, host, username=None, password=None, port=None, path=None, query=None, fragment=None))] |
| 160 | + #[allow(clippy::too_many_arguments)] |
| 161 | + pub fn build<'a>( |
| 162 | + cls: &'a PyType, |
| 163 | + scheme: &str, |
| 164 | + host: &str, |
| 165 | + username: Option<&str>, |
| 166 | + password: Option<&str>, |
| 167 | + port: Option<u16>, |
| 168 | + path: Option<&str>, |
| 169 | + query: Option<&str>, |
| 170 | + fragment: Option<&str>, |
| 171 | + ) -> PyResult<&'a PyAny> { |
| 172 | + let url_host = UrlHostParts { |
| 173 | + username: username.map(Into::into), |
| 174 | + password: password.map(Into::into), |
| 175 | + host: Some(host.into()), |
| 176 | + port, |
| 177 | + }; |
| 178 | + let mut url = format!("{scheme}://{url_host}"); |
| 179 | + if let Some(path) = path { |
| 180 | + url.push('/'); |
| 181 | + url.push_str(path); |
| 182 | + } |
| 183 | + if let Some(query) = query { |
| 184 | + url.push('?'); |
| 185 | + url.push_str(query); |
| 186 | + } |
| 187 | + if let Some(fragment) = fragment { |
| 188 | + url.push('#'); |
| 189 | + url.push_str(fragment); |
| 190 | + } |
| 191 | + cls.call1((url,)) |
| 192 | + } |
153 | 193 | }
|
154 | 194 |
|
155 | 195 | #[pyclass(name = "MultiHostUrl", module = "pydantic_core._pydantic_core", subclass)]
|
@@ -314,6 +354,114 @@ impl PyMultiHostUrl {
|
314 | 354 | fn __getnewargs__(&self) -> (String,) {
|
315 | 355 | (self.__str__(),)
|
316 | 356 | }
|
| 357 | + |
| 358 | + #[classmethod] |
| 359 | + #[pyo3(signature=(*, scheme, hosts=None, path=None, query=None, fragment=None, host=None, username=None, password=None, port=None))] |
| 360 | + #[allow(clippy::too_many_arguments)] |
| 361 | + pub fn build<'a>( |
| 362 | + cls: &'a PyType, |
| 363 | + scheme: &str, |
| 364 | + hosts: Option<Vec<UrlHostParts>>, |
| 365 | + path: Option<&str>, |
| 366 | + query: Option<&str>, |
| 367 | + fragment: Option<&str>, |
| 368 | + // convenience parameters to build with a single host |
| 369 | + host: Option<&str>, |
| 370 | + username: Option<&str>, |
| 371 | + password: Option<&str>, |
| 372 | + port: Option<u16>, |
| 373 | + ) -> PyResult<&'a PyAny> { |
| 374 | + let mut url = |
| 375 | + if hosts.is_some() && (host.is_some() || username.is_some() || password.is_some() || port.is_some()) { |
| 376 | + return Err(PyValueError::new_err( |
| 377 | + "expected one of `hosts` or singular values to be set.", |
| 378 | + )); |
| 379 | + } else if let Some(hosts) = hosts { |
| 380 | + // check all of host / user / password / port empty |
| 381 | + // build multi-host url |
| 382 | + let mut multi_url = format!("{scheme}://"); |
| 383 | + for (index, single_host) in hosts.iter().enumerate() { |
| 384 | + if single_host.is_empty() { |
| 385 | + return Err(PyValueError::new_err( |
| 386 | + "expected one of 'host', 'username', 'password' or 'port' to be set", |
| 387 | + )); |
| 388 | + } |
| 389 | + multi_url.push_str(&single_host.to_string()); |
| 390 | + if index != hosts.len() - 1 { |
| 391 | + multi_url.push(','); |
| 392 | + }; |
| 393 | + } |
| 394 | + multi_url |
| 395 | + } else if host.is_some() { |
| 396 | + let url_host = UrlHostParts { |
| 397 | + username: username.map(Into::into), |
| 398 | + password: password.map(Into::into), |
| 399 | + host: host.map(Into::into), |
| 400 | + port: port.map(Into::into), |
| 401 | + }; |
| 402 | + format!("{scheme}://{url_host}") |
| 403 | + } else { |
| 404 | + return Err(PyValueError::new_err("expected either `host` or `hosts` to be set")); |
| 405 | + }; |
| 406 | + |
| 407 | + if let Some(path) = path { |
| 408 | + url.push('/'); |
| 409 | + url.push_str(path); |
| 410 | + } |
| 411 | + if let Some(query) = query { |
| 412 | + url.push('?'); |
| 413 | + url.push_str(query); |
| 414 | + } |
| 415 | + if let Some(fragment) = fragment { |
| 416 | + url.push('#'); |
| 417 | + url.push_str(fragment); |
| 418 | + } |
| 419 | + cls.call1((url,)) |
| 420 | + } |
| 421 | +} |
| 422 | + |
| 423 | +pub struct UrlHostParts { |
| 424 | + username: Option<String>, |
| 425 | + password: Option<String>, |
| 426 | + host: Option<String>, |
| 427 | + port: Option<u16>, |
| 428 | +} |
| 429 | + |
| 430 | +impl UrlHostParts { |
| 431 | + fn is_empty(&self) -> bool { |
| 432 | + self.host.is_none() && self.password.is_none() && self.host.is_none() && self.port.is_none() |
| 433 | + } |
| 434 | +} |
| 435 | + |
| 436 | +impl FromPyObject<'_> for UrlHostParts { |
| 437 | + fn extract(ob: &'_ PyAny) -> PyResult<Self> { |
| 438 | + let py = ob.py(); |
| 439 | + let dict = ob.downcast::<PyDict>()?; |
| 440 | + Ok(UrlHostParts { |
| 441 | + username: dict.get_as(intern!(py, "username"))?, |
| 442 | + password: dict.get_as(intern!(py, "password"))?, |
| 443 | + host: dict.get_as(intern!(py, "host"))?, |
| 444 | + port: dict.get_as(intern!(py, "port"))?, |
| 445 | + }) |
| 446 | + } |
| 447 | +} |
| 448 | + |
| 449 | +impl fmt::Display for UrlHostParts { |
| 450 | + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { |
| 451 | + match (&self.username, &self.password) { |
| 452 | + (Some(username), None) => write!(f, "{username}@")?, |
| 453 | + (None, Some(password)) => write!(f, ":{password}@")?, |
| 454 | + (Some(username), Some(password)) => write!(f, "{username}:{password}@")?, |
| 455 | + (None, None) => {} |
| 456 | + }; |
| 457 | + if let Some(host) = &self.host { |
| 458 | + write!(f, "{host}")?; |
| 459 | + } |
| 460 | + if let Some(port) = self.port { |
| 461 | + write!(f, ":{port}")?; |
| 462 | + } |
| 463 | + Ok(()) |
| 464 | + } |
317 | 465 | }
|
318 | 466 |
|
319 | 467 | fn host_to_dict<'a>(py: Python<'a>, lib_url: &Url) -> PyResult<&'a PyDict> {
|
|
0 commit comments