pypi-simple
Advanced tools
+10
-0
@@ -0,1 +1,11 @@ | ||
| v1.8.0 (2025-09-03) | ||
| ------------------- | ||
| - Provenance support belatedly updated to match a change to PEP 740: | ||
| - `DistributionPackage.provenance_sha256` is now deprecated and is always | ||
| `None` | ||
| - `DistributionPackage.provenance_url` is now determined correctly and is | ||
| `None` when no provenance file is declared | ||
| - `PyPISimple.get_provenance()` no longer verifies the provenance's digest, | ||
| and the `verify` argument is now deprecated | ||
| v1.7.0 (2025-07-28) | ||
@@ -2,0 +12,0 @@ ------------------- |
+12
-0
@@ -6,2 +6,14 @@ .. currentmodule:: pypi_simple | ||
| v1.8.0 (2025-09-03) | ||
| ------------------- | ||
| - Provenance support belatedly updated to match a change to :pep:`740`: | ||
| - `DistributionPackage.provenance_sha256` is now deprecated and is always | ||
| `None` | ||
| - `DistributionPackage.provenance_url` is now determined correctly and is | ||
| `None` when no provenance file is declared | ||
| - `PyPISimple.get_provenance()` no longer verifies the provenance's digest, | ||
| and the ``verify`` argument is now deprecated | ||
| v1.7.0 (2025-07-28) | ||
@@ -8,0 +20,0 @@ ------------------- |
+1
-1
| Metadata-Version: 2.4 | ||
| Name: pypi-simple | ||
| Version: 1.7.0 | ||
| Version: 1.8.0 | ||
| Summary: PyPI Simple Repository API client library | ||
@@ -5,0 +5,0 @@ Project-URL: Source Code, https://github.com/jwodder/pypi-simple |
@@ -17,3 +17,3 @@ """ | ||
| __version__ = "1.7.0" | ||
| __version__ = "1.8.0" | ||
| __author__ = "John Thorvald Wodder II" | ||
@@ -20,0 +20,0 @@ __author_email__ = "pypi-simple@varonathe.org" |
@@ -100,10 +100,16 @@ from __future__ import annotations | ||
| #: | ||
| #: The SHA 256 digest of the package file's :pep:`740` ``.provenance`` | ||
| #: file. | ||
| #: .. deprecated:: 1.8.0 | ||
| #: | ||
| #: If `provenance_sha256` is non-`None`, then the package repository | ||
| #: provides a ``.provenance`` file for the package. If it is `None`, no | ||
| #: conclusions can be drawn. | ||
| provenance_sha256: Optional[str] = None | ||
| #: This attribute is deprecated; its value is always `None`. | ||
| provenance_sha256: None = None | ||
| #: .. versionadded:: 1.6.0 | ||
| #: | ||
| #: .. versionchanged:: 1.8.0 | ||
| #: | ||
| #: ``provenance_url`` can now be `None` | ||
| #: | ||
| #: The URL of the package file's :pep:`740` provenance file, if any | ||
| provenance_url: Optional[str] = None | ||
| @property | ||
@@ -125,10 +131,2 @@ def sig_url(self) -> str: | ||
| @property | ||
| def provenance_url(self) -> str: | ||
| """ | ||
| The URL of the package file's :pep:`740` ``.provenance`` file, if it | ||
| exists; cf. `provenance_sha256` | ||
| """ | ||
| return url_add_suffix(self.url, ".provenance") | ||
| @classmethod | ||
@@ -188,3 +186,3 @@ def from_link( | ||
| has_metadata=has_metadata, | ||
| provenance_sha256=link.get_str_attrib("data-provenance"), | ||
| provenance_url=link.get_str_attrib("data-provenance"), | ||
| ) | ||
@@ -243,3 +241,3 @@ | ||
| upload_time=file.upload_time, | ||
| provenance_sha256=file.provenance, | ||
| provenance_url=None if file.provenance is None else str(file.provenance), | ||
| ) | ||
@@ -246,0 +244,0 @@ |
@@ -501,3 +501,3 @@ from __future__ import annotations | ||
| pkg: DistributionPackage, | ||
| verify: bool = True, | ||
| verify: bool = True, # noqa: U100 | ||
| timeout: float | tuple[float, float] | None = None, | ||
@@ -509,16 +509,15 @@ headers: Optional[dict[str, str]] = None, | ||
| Retrieve the :pep:`740` ``.provenance`` file for the given | ||
| .. versionchanged:: 1.8.0 | ||
| The ``verify`` argument is now deprecated and does nothing. | ||
| Retrieve the :pep:`740` provenance file for the given | ||
| `DistributionPackage` and decode it as JSON. | ||
| Not all packages have ``.provenance`` files available for download; cf. | ||
| `DistributionPackage.provenance_sha256`. This method will always | ||
| attempt to download the ``.provenance`` file regardless of the value of | ||
| `DistributionPackage.provenance_sha256`; if the server replies with a | ||
| 404, a `NoProvenanceError` is raised. | ||
| Not all packages have provenance files available for download. If | ||
| `DistributionPackage.provenance_url` is `None` or if the server replies | ||
| with a 404, a `NoProvenanceError` is raised. | ||
| :param DistributionPackage pkg: | ||
| the distribution package to retrieve the ``.provenance`` file of | ||
| :param bool verify: | ||
| whether to verify the ``.provenance`` file's SHA 256 digest against | ||
| the retrieved data | ||
| the distribution package to retrieve the provenance file of | ||
| :param timeout: optional timeout to pass to the ``requests`` call | ||
@@ -529,28 +528,15 @@ :type timeout: float | tuple[float,float] | None | ||
| :rtype: dict[str, Any] | ||
| :raises NoProvenanceError: | ||
| if the repository responds with a 404 error code | ||
| if ``provenance_url`` is `None` or the repository responds with a | ||
| 404 error code | ||
| :raises requests.HTTPError: if the repository responds with an HTTP | ||
| error code other than 404 | ||
| :raises NoDigestsError: | ||
| if ``verify`` is true and ``pkg.provenance_sha256`` is `None` | ||
| :raises DigestMismatchError: | ||
| if ``verify`` is true and the digest of the downloaded data does | ||
| not match the expected value | ||
| """ | ||
| digester: AbstractDigestChecker | ||
| if verify: | ||
| if pkg.provenance_sha256 is not None: | ||
| digests = {"sha256": pkg.provenance_sha256} | ||
| else: | ||
| digests = {} | ||
| digester = DigestChecker(digests, pkg.provenance_url) | ||
| else: | ||
| digester = NullDigestChecker() | ||
| r = self.s.get(pkg.provenance_url, timeout=timeout, headers=headers) | ||
| url = pkg.provenance_url | ||
| if url is None: | ||
| raise NoProvenanceError(pkg.filename, None) | ||
| r = self.s.get(url, timeout=timeout, headers=headers) | ||
| if r.status_code == 404: | ||
| raise NoProvenanceError(pkg.filename, pkg.provenance_url) | ||
| raise NoProvenanceError(pkg.filename, url) | ||
| r.raise_for_status() | ||
| digester.update(r.content) | ||
| digester.finalize() | ||
| return json.loads(r.content) # type: ignore[no-any-return] |
@@ -0,1 +1,4 @@ | ||
| from typing import Optional | ||
| class UnsupportedRepoVersionError(Exception): | ||
@@ -152,13 +155,22 @@ """ | ||
| Raised by `PyPISimple.get_provenance()` when a request for a | ||
| ``.provenance`` file fails with a 404 error code | ||
| .. versionchanged:: 1.8.0 | ||
| ``url`` can now be `None` | ||
| Raised by `PyPISimple.get_provenance()` when passed a `DistributionPackage` | ||
| with a `None` ``provenance_url`` or when a request for a provenance file | ||
| fails with a 404 error code | ||
| """ | ||
| def __init__(self, filename: str, url: str) -> None: | ||
| def __init__(self, filename: str, url: Optional[str]) -> None: | ||
| #: The filename of the package whose provenance was requested | ||
| self.filename = filename | ||
| #: The URL to which the failed request was made | ||
| #: The URL to which the failed request was made, or `None` if | ||
| #: ``provenance_url`` was `None` | ||
| self.url = url | ||
| def __str__(self) -> str: | ||
| return f"No .provenance file found for {self.filename} at {self.url}" | ||
| if self.url is None: | ||
| return f"No provenance file declared for {self.filename}" | ||
| else: | ||
| return f"No provenance file found for {self.filename} at {self.url}" |
| from __future__ import annotations | ||
| from datetime import datetime | ||
| from typing import Any, Dict, List, Optional, Union | ||
| from pydantic import BaseModel, Field, StrictBool, field_validator | ||
| from pydantic import BaseModel, Field, HttpUrl, StrictBool, field_validator | ||
| from .enums import ProjectStatus | ||
@@ -44,3 +44,3 @@ | ||
| upload_time: Optional[datetime] = None | ||
| provenance: Optional[str] = None | ||
| provenance: Optional[HttpUrl] = None | ||
@@ -47,0 +47,0 @@ @property |
+10
-34
| from __future__ import annotations | ||
| import filecmp | ||
| import hashlib | ||
| import json | ||
@@ -327,3 +326,3 @@ from pathlib import Path | ||
| body=body_decl | ||
| + b'<a href="../files/project-0.1.0-p\xC3\xBF42-none-any.whl">project-0.1.0-p\xC3\xBF42-none-any.whl</a>', | ||
| + b'<a href="../files/project-0.1.0-p\xc3\xbf42-none-any.whl">project-0.1.0-p\xc3\xbf42-none-any.whl</a>', | ||
| content_type=content_type, | ||
@@ -336,7 +335,7 @@ ) | ||
| DistributionPackage( | ||
| filename="project-0.1.0-p\xFF42-none-any.whl", | ||
| filename="project-0.1.0-p\xff42-none-any.whl", | ||
| project="project", | ||
| version="0.1.0", | ||
| package_type="wheel", | ||
| url="https://test.nil/simple/files/project-0.1.0-p\xFF42-none-any.whl", | ||
| url="https://test.nil/simple/files/project-0.1.0-p\xff42-none-any.whl", | ||
| digests={}, | ||
@@ -376,3 +375,3 @@ requires_python=None, | ||
| body=body_decl | ||
| + b'<a href="../files/project-0.1.0-p\xC3\xBF42-none-any.whl">project-0.1.0-p\xC3\xBF42-none-any.whl</a>', | ||
| + b'<a href="../files/project-0.1.0-p\xc3\xbf42-none-any.whl">project-0.1.0-p\xc3\xbf42-none-any.whl</a>', | ||
| content_type=content_type, | ||
@@ -385,7 +384,7 @@ ) | ||
| DistributionPackage( | ||
| filename="project-0.1.0-p\u0102\u017C42-none-any.whl", | ||
| filename="project-0.1.0-p\u0102\u017c42-none-any.whl", | ||
| project="project", | ||
| version="0.1.0", | ||
| package_type="wheel", | ||
| url="https://test.nil/simple/files/project-0.1.0-p\u0102\u017C42-none-any.whl", | ||
| url="https://test.nil/simple/files/project-0.1.0-p\u0102\u017c42-none-any.whl", | ||
| digests={}, | ||
@@ -1024,5 +1023,5 @@ requires_python=None, | ||
| has_sig=None, | ||
| provenance_sha256=hashlib.sha256(provenance_bytes).hexdigest(), | ||
| provenance_url="https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl.provenance", | ||
| ) | ||
| assert simple.get_provenance(pkg, verify=True) == provenance | ||
| assert simple.get_provenance(pkg) == provenance | ||
@@ -1048,2 +1047,3 @@ | ||
| has_sig=None, | ||
| provenance_url="https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl.provenance", | ||
| ) | ||
@@ -1059,27 +1059,3 @@ with pytest.raises(NoProvenanceError) as excinfo: | ||
| str(excinfo.value) | ||
| == "No .provenance file found for sampleproject-1.2.3-py3-none-any.whl at https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl.provenance" | ||
| == "No provenance file found for sampleproject-1.2.3-py3-none-any.whl at https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl.provenance" | ||
| ) | ||
| @responses.activate | ||
| def test_get_provenance_verify_no_digest() -> None: | ||
| responses.add( | ||
| method=responses.GET, | ||
| url="https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl.provenance", | ||
| body="Does not exist", | ||
| status=404, | ||
| ) | ||
| with PyPISimple("https://test.nil/simple/") as simple: | ||
| pkg = DistributionPackage( | ||
| filename="sampleproject-1.2.3-py3-none-any.whl", | ||
| project="sampleproject", | ||
| version="1.2.3", | ||
| package_type="wheel", | ||
| url="https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl", | ||
| digests={}, | ||
| requires_python=None, | ||
| has_sig=None, | ||
| provenance_sha256=None, | ||
| ) | ||
| with pytest.raises(NoDigestsError): | ||
| simple.get_provenance(pkg, verify=True) |
@@ -99,3 +99,3 @@ from __future__ import annotations | ||
| "data-yanked": "Oopsy.", | ||
| "data-provenance": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", | ||
| "data-provenance": "https://example.com/pypi-provenance/qypi-0.1.0-py3-none-any.whl.provenance", | ||
| }, | ||
@@ -120,3 +120,3 @@ ), | ||
| has_metadata=True, | ||
| provenance_sha256="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", | ||
| provenance_url="https://example.com/pypi-provenance/qypi-0.1.0-py3-none-any.whl.provenance", | ||
| ), | ||
@@ -204,23 +204,2 @@ ), | ||
| def test_provenance_url() -> None: | ||
| pkg = DistributionPackage( | ||
| filename="qypi-0.1.0-py3-none-any.whl", | ||
| url="https://files.pythonhosted.org/packages/82/fc/9e25534641d7f63be93079bc07fa92bab136ddf5d4181059a1308a346f96/qypi-0.1.0-py3-none-any.whl", | ||
| digests={ | ||
| "sha256": "da69d28dcd527c0e372b3fa7b92fc333b327f8470175f035abc4e351b539189f" | ||
| }, | ||
| has_sig=True, | ||
| requires_python="~= 3.6", | ||
| project="qypi", | ||
| version="0.1.0", | ||
| package_type="wheel", | ||
| is_yanked=False, | ||
| yanked_reason=None, | ||
| ) | ||
| assert ( | ||
| pkg.provenance_url | ||
| == "https://files.pythonhosted.org/packages/82/fc/9e25534641d7f63be93079bc07fa92bab136ddf5d4181059a1308a346f96/qypi-0.1.0-py3-none-any.whl.provenance" | ||
| ) | ||
| def test_from_json_data_no_metadata() -> None: | ||
@@ -282,8 +261,8 @@ pkg = DistributionPackage.from_json_data( | ||
| "yanked": False, | ||
| "provenance": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", | ||
| "provenance": "https://example.com/pypi-provenance/argset-0.1.0-py3-none-any.whl.provenance", | ||
| } | ||
| ) | ||
| assert ( | ||
| pkg.provenance_sha256 | ||
| == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" | ||
| pkg.provenance_url | ||
| == "https://example.com/pypi-provenance/argset-0.1.0-py3-none-any.whl.provenance" | ||
| ) |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
301102
-0.35%6069
-0.82%