You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

pypi-simple

Package Overview
Dependencies
Maintainers
1
Versions
21
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

pypi-simple - pypi Package Compare versions

Comparing version
1.7.0
to
1.8.0
+10
-0
CHANGELOG.md

@@ -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 @@ -------------------

@@ -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

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"
)