-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add support for Rust crates/Cargo package index
- Loading branch information
1 parent
ac89cdb
commit 283db0b
Showing
21 changed files
with
1,030 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# Copyright (c) stefan6419846. All rights reserved. | ||
# SPDX-License-Identifier: Apache-2.0 | ||
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. | ||
|
||
from __future__ import annotations | ||
|
||
try: | ||
from importlib.metadata import version as _version | ||
except ImportError: | ||
from importlib_metadata import version as _version | ||
|
||
|
||
VERSION: str = _version("license_tools") | ||
|
||
del _version |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
# Copyright (c) stefan6419846. All rights reserved. | ||
# SPDX-License-Identifier: Apache-2.0 | ||
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. | ||
|
||
""" | ||
Tools related to Cargo/Rust. | ||
""" | ||
|
||
from __future__ import annotations | ||
|
||
from dataclasses import dataclass | ||
from pathlib import Path | ||
from typing import Any, Generator | ||
|
||
import tomli | ||
|
||
from license_tools.utils import download_utils, rendering_utils | ||
from license_tools.utils.download_utils import Download | ||
|
||
|
||
# https://doc.rust-lang.org/cargo/reference/manifest.html | ||
_VERBOSE_NAMES = { | ||
"name": "Name", | ||
"version": "Version", | ||
"authors": "Authors", | ||
"description": "Description", | ||
"readme": "README", | ||
"homepage": "Homepage", | ||
"repository": "Repository", | ||
"license": "License", | ||
"license-file": "License File", | ||
"keywords": "Keywords", | ||
"categories": "Categories", | ||
} | ||
|
||
|
||
def read_toml(path: Path) -> dict[str, Any]: | ||
""" | ||
Read the given TOML file. | ||
:param path: The file to read. | ||
:return: The parsed file content. | ||
""" | ||
return tomli.loads(path.read_text()) | ||
|
||
|
||
def analyze_metadata(path: Path | str) -> dict[str, str | list[str]] | None: | ||
""" | ||
Analyze the Rust package metadata for the given directory. | ||
:param path: The directory/file to analyze. Should either be a directory or `Cargo.toml` file. | ||
:return: The package metadata. | ||
""" | ||
path = Path(path) | ||
if path.name != "Cargo.toml": | ||
if path.joinpath("Cargo.toml").exists(): | ||
path = path / "Cargo.toml" | ||
elif len(list(path.glob("*"))) == 1: | ||
path = next(path.glob("*")) / "Cargo.toml" | ||
else: | ||
raise ValueError(f"No clear Cargo.toml in {path}.") | ||
manifest = read_toml(path) | ||
return manifest.get("package") | ||
|
||
|
||
def check_metadata(path: Path | str) -> str: | ||
""" | ||
Render the relevant details for the given package. | ||
:param path: The package path. | ||
:return: The rendered dictionary-like representation of the relevant fields. | ||
""" | ||
metadata = analyze_metadata(path) | ||
if not metadata: | ||
return "" | ||
return rendering_utils.render_dictionary( | ||
dictionary=metadata, verbose_names_mapping=_VERBOSE_NAMES, multi_value_keys={"authors", "categories", "keywords"} | ||
) | ||
|
||
|
||
@dataclass | ||
class PackageVersion: | ||
name: str | ||
version: str | ||
checksum: str | ||
|
||
def to_download(self) -> Download: | ||
return Download( | ||
url=f"https://crates.io/api/v1/crates/{self.name}/{self.version}/download", | ||
filename=f"{self.name}_{self.version}.crate", | ||
sha256=self.checksum | ||
) | ||
|
||
|
||
def get_package_versions(lock_path: Path | str) -> Generator[PackageVersion, None, None]: | ||
""" | ||
Get the packages from the given lock file. | ||
:param lock_path: The lock file to read. | ||
:return: The packages retrieved from lock file. | ||
""" | ||
data = read_toml(Path(lock_path)) | ||
for package in data["package"]: | ||
if package.get("source") != "registry+https://github.com/rust-lang/crates.io-index": | ||
print("Skipping", package) | ||
continue | ||
yield PackageVersion(name=package["name"], version=package["version"], checksum=package["checksum"]) | ||
|
||
|
||
def download_from_lock_file(lock_path: Path | str, target_directory: Path | str) -> None: | ||
""" | ||
Download the packages from the given lock file. | ||
:param lock_path: The lock file to read. | ||
:param target_directory: The directory to write the packages to. | ||
""" | ||
target_directory = Path(target_directory) | ||
if not target_directory.exists(): | ||
target_directory.mkdir() | ||
|
||
downloads = [package.to_download() for package in get_package_versions(lock_path)] | ||
download_utils.download_one_file_per_second(downloads=downloads, directory=target_directory) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
# Copyright (c) stefan6419846. All rights reserved. | ||
# SPDX-License-Identifier: Apache-2.0 | ||
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. | ||
|
||
""" | ||
Download handling. | ||
""" | ||
|
||
from __future__ import annotations | ||
|
||
import hashlib | ||
import logging | ||
import time | ||
from dataclasses import dataclass | ||
from pathlib import Path | ||
|
||
import requests | ||
|
||
from license_tools.constants import VERSION | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
del logging | ||
|
||
|
||
USER_AGENT = f"https://github.com/stefan6419846/license_tools version {VERSION}" | ||
|
||
|
||
class ChecksumError(ValueError): | ||
pass | ||
|
||
|
||
@dataclass | ||
class Download: | ||
url: str | ||
filename: str | ||
sha256: str | None = None | ||
|
||
def verify_checksum(self, data: bytes) -> None: | ||
""" | ||
Check if the checksum of the given data matches the expected one. | ||
""" | ||
if self.sha256 is not None: | ||
digest = hashlib.sha256(data).hexdigest() | ||
expected = self.sha256 | ||
else: | ||
return | ||
if digest != expected: | ||
raise ChecksumError(f'Checksum mismatch: Got {digest}, expected {expected}!') | ||
|
||
|
||
class DownloadError(ValueError): | ||
pass | ||
|
||
|
||
def get_session() -> requests.Session: | ||
""" | ||
Get an identifiable session. | ||
:return: The session which identifies us against the server. | ||
""" | ||
session = requests.Session() | ||
session.headers.update({"User-Agent": USER_AGENT}) | ||
return session | ||
|
||
|
||
def download_file(download: Download, directory: Path, session: requests.Session | None = None) -> None: | ||
""" | ||
Download the given file. | ||
:param download: Download to perform. | ||
:param directory: Directory to download to. | ||
:param session: Session to use. | ||
""" | ||
if session is None: | ||
session = get_session() | ||
target_path = directory / download.filename | ||
logger.info("Downloading %s to %s ...", download.url, target_path) | ||
response = session.get(download.url) | ||
if not response.ok: | ||
raise DownloadError(f"Download not okay? {download.url} {response}") | ||
download.verify_checksum(response.content) | ||
target_path.write_bytes(response.content) | ||
|
||
|
||
def download_one_file_per_second(downloads: list[Download], directory: Path) -> None: | ||
""" | ||
Download the given files with not more than one request per second. This conforms to | ||
https://crates.io/data-access#api accordingly. | ||
:param downloads: List of downloads to perform. | ||
:param directory: Directory to download to. | ||
""" | ||
session = get_session() | ||
for download in downloads: | ||
download_file(download=download, directory=directory, session=session) | ||
time.sleep(1) |
Oops, something went wrong.