uv/crates/puffin-client/src/api.rs
Charlie Marsh ed68d31e03
Add a basic test for the resolver (#86)
Mocks out the PyPI client using some checked-in fixtures. The test is
very basic, and I'm not very happy with all the ceremony around the
mocks and such, but it's an interesting experiment at least.
2023-10-11 03:30:53 +00:00

187 lines
5.3 KiB
Rust

use std::fmt::Debug;
use futures::{AsyncRead, StreamExt, TryStreamExt};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use tracing::trace;
use url::Url;
use puffin_package::metadata::Metadata21;
use puffin_package::package_name::PackageName;
use crate::client::PypiClient;
use crate::error::PypiClientError;
impl PypiClient {
/// Fetch a package from the `PyPI` simple API.
pub async fn simple(
&self,
package_name: impl AsRef<str>,
) -> Result<SimpleJson, PypiClientError> {
// Format the URL for PyPI.
let mut url = self.registry.join("simple")?;
url.path_segments_mut()
.unwrap()
.push(PackageName::normalize(&package_name).as_ref());
url.path_segments_mut().unwrap().push("");
url.set_query(Some("format=application/vnd.pypi.simple.v1+json"));
trace!(
"fetching metadata for {} from {}",
package_name.as_ref(),
url
);
// Fetch from the registry.
let text = self.simple_impl(&package_name, &url).await?;
serde_json::from_str(&text)
.map_err(move |e| PypiClientError::from_json_err(e, String::new()))
}
async fn simple_impl(
&self,
package_name: impl AsRef<str>,
url: &Url,
) -> Result<String, PypiClientError> {
Ok(self
.client
.get(url.clone())
.header("Accept-Encoding", "gzip")
.send()
.await?
.error_for_status()
.map_err(|err| {
if err.status() == Some(StatusCode::NOT_FOUND) {
PypiClientError::PackageNotFound(
(*self.registry).clone(),
package_name.as_ref().to_string(),
)
} else {
PypiClientError::RequestError(err)
}
})?
.text()
.await?)
}
/// Fetch the metadata from a wheel file.
pub async fn file(&self, file: File) -> Result<Metadata21, PypiClientError> {
// Per PEP 658, if `data-dist-info-metadata` is available, we can request it directly;
// otherwise, send to our dedicated caching proxy.
let url = if file.data_dist_info_metadata.is_available() {
Url::parse(&format!("{}.metadata", file.url))?
} else {
self.proxy.join(file.url.parse::<Url>()?.path())?
};
trace!("fetching file {} from {}", file.filename, url);
// Fetch from the registry.
let text = self.file_impl(&file.filename, &url).await?;
Metadata21::parse(text.as_bytes()).map_err(std::convert::Into::into)
}
async fn file_impl(
&self,
filename: impl AsRef<str>,
url: &Url,
) -> Result<String, PypiClientError> {
Ok(self
.client
.get(url.clone())
.send()
.await?
.error_for_status()
.map_err(|err| {
if err.status() == Some(StatusCode::NOT_FOUND) {
PypiClientError::FileNotFound(
(*self.registry).clone(),
filename.as_ref().to_string(),
)
} else {
PypiClientError::RequestError(err)
}
})?
.text()
.await?)
}
/// Stream a file from an external URL.
pub async fn stream_external(
&self,
url: &Url,
) -> Result<Box<dyn AsyncRead + Unpin + Send + Sync>, PypiClientError> {
Ok(Box::new(
self.uncached_client
.get(url.to_string())
.send()
.await?
.error_for_status()?
.bytes_stream()
.map(|r| match r {
Ok(bytes) => Ok(bytes),
Err(err) => Err(std::io::Error::new(std::io::ErrorKind::Other, err)),
})
.into_async_read(),
))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimpleJson {
pub files: Vec<File>,
pub meta: Meta,
pub name: String,
pub versions: Vec<String>,
}
// TODO(charlie): Can we rename this? What does this look like for source distributions?
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct File {
pub core_metadata: Metadata,
pub data_dist_info_metadata: Metadata,
pub filename: String,
pub hashes: Hashes,
pub requires_python: Option<String>,
pub size: usize,
pub upload_time: String,
pub url: String,
pub yanked: Yanked,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Metadata {
Bool(bool),
Hashes(Hashes),
}
impl Metadata {
pub fn is_available(&self) -> bool {
match self {
Self::Bool(is_available) => *is_available,
Self::Hashes(_) => true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Yanked {
Bool(bool),
Reason(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hashes {
pub sha256: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Meta {
#[serde(rename = "_last-serial")]
pub last_serial: i64,
pub api_version: String,
}