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.5.0
to
1.6.0
+11
-0
CHANGELOG.md

@@ -0,1 +1,12 @@

v1.6.0 (2024-07-18)
-------------------
- Drop support for Python 3.7
- Support PEP 740
- `provenance_sha256` and `provenance_url` fields added to
`DistributionPackage`
- `get_provenance()` method added to `PyPISimple`
- `NoProvenanceError` exception type added
- Add `url` fields to the `DigestMismatchError`, `NoDigestsError`, and
`NoMetadataError` classes
v1.5.0 (2024-02-24)

@@ -2,0 +13,0 @@ -------------------

+1
-0

@@ -61,2 +61,3 @@ .. currentmodule:: pypi_simple

.. autoexception:: NoMetadataError()
.. autoexception:: NoProvenanceError()
.. autoexception:: NoSuchProjectError()

@@ -63,0 +64,0 @@ .. autoexception:: UnsupportedContentTypeError()

@@ -6,2 +6,18 @@ .. currentmodule:: pypi_simple

v1.6.0 (2024-07-18)
-------------------
- Drop support for Python 3.7
- Support :pep:`740`
- `~DistributionPackage.provenance_sha256` and
`~DistributionPackage.provenance_url` fields added to
`DistributionPackage`
- `~PyPISimple.get_provenance()` method added to `PyPISimple`
- `NoProvenanceError` exception type added
- Add ``url`` fields to the `DigestMismatchError`, `NoDigestsError`, and
`NoMetadataError` classes
v1.5.0 (2024-02-24)

@@ -8,0 +24,0 @@ -------------------

+14
-11

@@ -18,8 +18,7 @@ Examples

# Requirements:
# Python 3.8+
# packaging 23.1+
# pypi_simple 1.3+
# pypi_simple 1.5+
from packaging.metadata import parse_email
from pypi_simple import PyPISimple
from pypi_simple import NoMetadataError, PyPISimple

@@ -29,11 +28,15 @@ with PyPISimple() as client:

for pkg in page.packages:
if pkg.has_metadata:
src = client.get_package_metadata(pkg)
md, _ = parse_email(src)
if deps := md.get("requires_dist"):
print(f"Dependencies for {pkg.filename}:")
for d in deps:
print(f" {d}")
if pkg.has_metadata is not False:
try:
src = client.get_package_metadata(pkg)
except NoMetadataError:
print(f"{pkg.filename}: No metadata available")
else:
print(f"Dependencies for {pkg.filename}: NONE")
md, _ = parse_email(src)
if deps := md.get("requires_dist"):
print(f"Dependencies for {pkg.filename}:")
for d in deps:
print(f" {d}")
else:
print(f"Dependencies for {pkg.filename}: NONE")
else:

@@ -40,0 +43,0 @@ print(f"{pkg.filename}: No metadata available")

@@ -22,4 +22,4 @@ .. module:: pypi_simple

specified in :pep:`503` and updated by :pep:`592`, :pep:`629`, :pep:`658`,
:pep:`691`, :pep:`700`, :pep:`708`, and :pep:`714`. With it, you can query
`the Python Package Index (PyPI) <https://pypi.org>`_ and other `pip
:pep:`691`, :pep:`700`, :pep:`708`, :pep:`714`, and :pep:`740`. With it, you
can query `the Python Package Index (PyPI) <https://pypi.org>`_ and other `pip
<https://pip.pypa.io>`_-compatible repositories for a list of their available

@@ -33,3 +33,3 @@ projects and lists of each project's available package files. The library also

============
``pypi-simple`` requires Python 3.7 or higher. Just use `pip
``pypi-simple`` requires Python 3.8 or higher. Just use `pip
<https://pip.pypa.io>`_ for Python 3 (You have pip, right?) to install it::

@@ -47,4 +47,4 @@

Example
=======
Examples
========

@@ -83,2 +83,3 @@ Get information about a package:

:doc:`See more examples in the docs. <examples>`

@@ -85,0 +86,0 @@ Indices and tables

+19
-15

@@ -1,4 +0,4 @@

Metadata-Version: 2.1
Metadata-Version: 2.3
Name: pypi-simple
Version: 1.5.0
Version: 1.6.0
Summary: PyPI Simple Repository API client library

@@ -15,3 +15,2 @@ Project-URL: Source Code, https://github.com/jwodder/pypi-simple

Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8

@@ -28,3 +27,3 @@ Classifier: Programming Language :: Python :: 3.9

Classifier: Typing :: Typed
Requires-Python: >=3.7
Requires-Python: >=3.8
Requires-Dist: beautifulsoup4~=4.5

@@ -35,3 +34,2 @@ Requires-Dist: mailbits~=0.2

Requires-Dist: requests~=2.20
Requires-Dist: typing-extensions; python_version < '3.8'
Provides-Extra: tqdm

@@ -41,3 +39,5 @@ Requires-Dist: tqdm; extra == 'tqdm'

.. image:: https://www.repostatus.org/badges/latest/active.svg
|repostatus| |ci-status| |coverage| |pyversions| |license|
.. |repostatus| image:: https://www.repostatus.org/badges/latest/active.svg
:target: https://www.repostatus.org/#active

@@ -47,13 +47,13 @@ :alt: Project Status: Active — The project has reached a stable, usable

.. image:: https://github.com/jwodder/pypi-simple/actions/workflows/test.yml/badge.svg
.. |ci-status| image:: https://github.com/jwodder/pypi-simple/actions/workflows/test.yml/badge.svg
:target: https://github.com/jwodder/pypi-simple/actions/workflows/test.yml
:alt: CI Status
.. image:: https://codecov.io/gh/jwodder/pypi-simple/branch/master/graph/badge.svg
.. |coverage| image:: https://codecov.io/gh/jwodder/pypi-simple/branch/master/graph/badge.svg
:target: https://codecov.io/gh/jwodder/pypi-simple
.. image:: https://img.shields.io/pypi/pyversions/pypi-simple.svg
.. |pyversions| image:: https://img.shields.io/pypi/pyversions/pypi-simple.svg
:target: https://pypi.org/project/pypi-simple/
.. image:: https://img.shields.io/github/license/jwodder/pypi-simple.svg
.. |license| image:: https://img.shields.io/github/license/jwodder/pypi-simple.svg
:target: https://opensource.org/licenses/MIT

@@ -70,4 +70,4 @@ :alt: MIT License

specified in :pep:`503` and updated by :pep:`592`, :pep:`629`, :pep:`658`,
:pep:`691`, :pep:`700`, :pep:`708`, and :pep:`714`. With it, you can query
`the Python Package Index (PyPI) <https://pypi.org>`_ and other `pip
:pep:`691`, :pep:`700`, :pep:`708`, :pep:`714`, and :pep:`740`. With it, you
can query `the Python Package Index (PyPI) <https://pypi.org>`_ and other `pip
<https://pip.pypa.io>`_-compatible repositories for a list of their available

@@ -85,3 +85,3 @@ projects and lists of each project's available package files. The library also

============
``pypi-simple`` requires Python 3.7 or higher. Just use `pip
``pypi-simple`` requires Python 3.8 or higher. Just use `pip
<https://pip.pypa.io>`_ for Python 3 (You have pip, right?) to install it::

@@ -99,4 +99,4 @@

Example
=======
Examples
========

@@ -134,1 +134,5 @@ Get information about a package:

)
`See more examples in the docs.`__
__ https://pypi-simple.readthedocs.io/en/stable/examples.html

@@ -10,3 +10,3 @@ [build-system]

readme = "README.rst"
requires-python = ">=3.7"
requires-python = ">=3.8"
license = "MIT"

@@ -30,3 +30,2 @@ license-files = { paths = ["LICENSE"] }

"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",

@@ -52,3 +51,2 @@ "Programming Language :: Python :: 3.9",

"requests ~= 2.20",
"typing_extensions; python_version < '3.8'",
]

@@ -55,0 +53,0 @@

@@ -1,2 +0,4 @@

.. image:: https://www.repostatus.org/badges/latest/active.svg
|repostatus| |ci-status| |coverage| |pyversions| |license|
.. |repostatus| image:: https://www.repostatus.org/badges/latest/active.svg
:target: https://www.repostatus.org/#active

@@ -6,13 +8,13 @@ :alt: Project Status: Active — The project has reached a stable, usable

.. image:: https://github.com/jwodder/pypi-simple/actions/workflows/test.yml/badge.svg
.. |ci-status| image:: https://github.com/jwodder/pypi-simple/actions/workflows/test.yml/badge.svg
:target: https://github.com/jwodder/pypi-simple/actions/workflows/test.yml
:alt: CI Status
.. image:: https://codecov.io/gh/jwodder/pypi-simple/branch/master/graph/badge.svg
.. |coverage| image:: https://codecov.io/gh/jwodder/pypi-simple/branch/master/graph/badge.svg
:target: https://codecov.io/gh/jwodder/pypi-simple
.. image:: https://img.shields.io/pypi/pyversions/pypi-simple.svg
.. |pyversions| image:: https://img.shields.io/pypi/pyversions/pypi-simple.svg
:target: https://pypi.org/project/pypi-simple/
.. image:: https://img.shields.io/github/license/jwodder/pypi-simple.svg
.. |license| image:: https://img.shields.io/github/license/jwodder/pypi-simple.svg
:target: https://opensource.org/licenses/MIT

@@ -29,4 +31,4 @@ :alt: MIT License

specified in :pep:`503` and updated by :pep:`592`, :pep:`629`, :pep:`658`,
:pep:`691`, :pep:`700`, :pep:`708`, and :pep:`714`. With it, you can query
`the Python Package Index (PyPI) <https://pypi.org>`_ and other `pip
:pep:`691`, :pep:`700`, :pep:`708`, :pep:`714`, and :pep:`740`. With it, you
can query `the Python Package Index (PyPI) <https://pypi.org>`_ and other `pip
<https://pip.pypa.io>`_-compatible repositories for a list of their available

@@ -44,3 +46,3 @@ projects and lists of each project's available package files. The library also

============
``pypi-simple`` requires Python 3.7 or higher. Just use `pip
``pypi-simple`` requires Python 3.8 or higher. Just use `pip
<https://pip.pypa.io>`_ for Python 3 (You have pip, right?) to install it::

@@ -58,4 +60,4 @@

Example
=======
Examples
========

@@ -93,1 +95,5 @@ Get information about a package:

)
`See more examples in the docs.`__
__ https://pypi-simple.readthedocs.io/en/stable/examples.html

@@ -17,3 +17,3 @@ """

__version__ = "1.5.0"
__version__ = "1.6.0"
__author__ = "John Thorvald Wodder II"

@@ -72,6 +72,9 @@ __author_email__ = "pypi-simple@varonathe.org"

from .classes import DistributionPackage, IndexPage, ProjectPage
from .client import NoMetadataError, NoSuchProjectError, PyPISimple
from .client import PyPISimple
from .errors import (
DigestMismatchError,
NoDigestsError,
NoMetadataError,
NoProvenanceError,
NoSuchProjectError,
UnexpectedRepoVersionWarning,

@@ -94,2 +97,3 @@ UnparsableFilenameError,

"NoMetadataError",
"NoProvenanceError",
"NoSuchProjectError",

@@ -96,0 +100,0 @@ "PYPI_SIMPLE_ENDPOINT",

@@ -13,3 +13,3 @@ from __future__ import annotations

from .pep691 import File, Project, ProjectList
from .util import basejoin, check_repo_version
from .util import basejoin, check_repo_version, url_add_suffix

@@ -46,9 +46,9 @@

#:
#: - ``'dumb'``
#: - ``'egg'``
#: - ``'msi'``
#: - ``'rpm'``
#: - ``'sdist'``
#: - ``'wheel'``
#: - ``'wininst'``
#: - ``"dumb"``
#: - ``"egg"``
#: - ``"msi"``
#: - ``"rpm"``
#: - ``"sdist"``
#: - ``"wheel"``
#: - ``"wininst"``
package_type: Optional[str]

@@ -99,2 +99,12 @@

#: .. versionadded:: 1.6.0
#:
#: The SHA 256 digest of the package file's :pep:`740` ``.provenance``
#: file.
#:
#: 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
@property

@@ -106,4 +116,3 @@ def sig_url(self) -> str:

"""
u = urlparse(self.url)
return urlunparse((u[0], u[1], u[2] + ".asc", "", "", ""))
return url_add_suffix(self.url, ".asc")

@@ -116,5 +125,12 @@ @property

"""
u = urlparse(self.url)
return urlunparse((u[0], u[1], u[2] + ".metadata", "", "", ""))
return url_add_suffix(self.url, ".metadata")
@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

@@ -174,2 +190,3 @@ def from_link(

has_metadata=has_metadata,
provenance_sha256=link.get_str_attrib("data-provenance"),
)

@@ -228,2 +245,3 @@

upload_time=file.upload_time,
provenance_sha256=file.provenance,
)

@@ -230,0 +248,0 @@

from __future__ import annotations
from collections.abc import Callable, Iterator
import json
import os

@@ -13,3 +14,8 @@ from pathlib import Path

from .classes import DistributionPackage, IndexPage, ProjectPage
from .errors import UnsupportedContentTypeError
from .errors import (
NoMetadataError,
NoProvenanceError,
NoSuchProjectError,
UnsupportedContentTypeError,
)
from .html_stream import parse_links_stream_response

@@ -324,4 +330,4 @@ from .progress import ProgressTracker, null_progress_tracker

:param bool keep_on_error:
whether to keep (true) or delete (false) the downloaded file if an
error occurs
whether to keep (true) or delete (false; default) the downloaded
file if an error occurs
:param progress: a callable for constructing a progress tracker

@@ -345,3 +351,3 @@ :param timeout: optional timeout to pass to the ``requests`` call

if verify:
digester = DigestChecker(pkg.digests)
digester = DigestChecker(pkg.digests, pkg.url)
else:

@@ -419,3 +425,3 @@ digester = NullDigestChecker()

if verify:
digester = DigestChecker(pkg.metadata_digests or {})
digester = DigestChecker(pkg.metadata_digests or {}, pkg.metadata_url)
else:

@@ -425,3 +431,3 @@ digester = NullDigestChecker()

if r.status_code == 404:
raise NoMetadataError(pkg.filename)
raise NoMetadataError(pkg.filename, pkg.metadata_url)
r.raise_for_status()

@@ -491,32 +497,57 @@ digester.update(r.content)

def get_provenance(
self,
pkg: DistributionPackage,
verify: bool = True,
timeout: float | tuple[float, float] | None = None,
headers: Optional[dict[str, str]] = None,
) -> dict[str, Any]:
"""
.. versionadded:: 1.6.0
class NoSuchProjectError(Exception):
"""
Raised by `PyPISimple.get_project_page()` when a request for a project
fails with a 404 error code
"""
Retrieve the :pep:`740` ``.provenance`` file for the given
`DistributionPackage` and decode it as JSON.
def __init__(self, project: str, url: str) -> None:
#: The name of the project requested
self.project = project
#: The URL to which the failed request was made
self.url = url
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.
def __str__(self) -> str:
return f"No details about project {self.project!r} available at {self.url}"
: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
:param timeout: optional timeout to pass to the ``requests`` call
:type timeout: float | tuple[float,float] | None
:param Optional[dict[str, str]] headers:
Custom headers to provide for the request.
:rtype: dict[str, Any]
class NoMetadataError(Exception):
"""
.. versionadded:: 1.3.0
Raised by `PyPISimple.get_package_metadata()` when a request for
distribution metadata fails with a 404 error code
"""
def __init__(self, filename: str) -> None:
#: The filename of the package whose metadata was requested
self.filename = filename
def __str__(self) -> str:
return f"No distribution metadata found for {self.filename}"
:raises NoProvenanceError:
if 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)
if r.status_code == 404:
raise NoProvenanceError(pkg.filename, pkg.provenance_url)
r.raise_for_status()
digester.update(r.content)
digester.finalize()
return json.loads(r.content) # type: ignore[no-any-return]

@@ -55,19 +55,25 @@ class UnsupportedRepoVersionError(Exception):

"""
Raised by `PyPISimple.download_package()` and
`PyPISimple.get_package_metadata()` with ``verify=True`` when the given
package or package metadata does not have any digests with known algorithms
Raised by `PyPISimple`'s download methods when passed ``verify=True`` and
the resource being downloaded does not have any digests with known
algorithms
"""
pass
def __init__(self, url: str) -> None:
#: The URL of the resource being downloaded
#:
#: .. versionadded:: 1.6.0
self.url = url
def __str__(self) -> str:
return f"No digests with known algorithms available for resource at {self.url}"
class DigestMismatchError(ValueError):
"""
Raised by `PyPISimple.download_package()` and
`PyPISimple.get_package_metadata()` with ``verify=True`` when the digest of
the downloaded data does not match the expected value
Raised by `PyPISimple`'s download methods when passed ``verify=True`` and
the digest of the downloaded data does not match the expected value
"""
def __init__(
self, algorithm: str, expected_digest: str, actual_digest: str
self, *, algorithm: str, expected_digest: str, actual_digest: str, url: str
) -> None:

@@ -80,7 +86,11 @@ #: The name of the digest algorithm used

self.actual_digest = actual_digest
#: The URL of the resource being downloaded
#:
#: .. versionadded:: 1.6.0
self.url = url
def __str__(self) -> str:
return (
f"{self.algorithm} digest of downloaded data is"
f" {self.actual_digest!r} instead of expected {self.expected_digest!r}"
f"{self.algorithm} digest of {self.url} is {self.actual_digest!r}"
f" instead of expected {self.expected_digest!r}"
)

@@ -102,1 +112,55 @@

return f"Cannot parse package filename: {self.filename!r}"
class NoSuchProjectError(Exception):
"""
Raised by `PyPISimple.get_project_page()` when a request for a project
fails with a 404 error code
"""
def __init__(self, project: str, url: str) -> None:
#: The name of the project requested
self.project = project
#: The URL to which the failed request was made
self.url = url
def __str__(self) -> str:
return f"No details about project {self.project!r} available at {self.url}"
class NoMetadataError(Exception):
"""
.. versionadded:: 1.3.0
Raised by `PyPISimple.get_package_metadata()` when a request for
distribution metadata fails with a 404 error code
"""
def __init__(self, filename: str, url: str) -> None:
#: The filename of the package whose metadata was requested
self.filename = filename
#: The URL to which the failed request was made
#:
#: .. versionadded:: 1.6.0
self.url = url
def __str__(self) -> str:
return f"No distribution metadata found for {self.filename} at {self.url}"
class NoProvenanceError(Exception):
"""
.. versionadded:: 1.6.0
Raised by `PyPISimple.get_provenance()` when a request for a
``.provenance`` file fails with a 404 error code
"""
def __init__(self, filename: str, url: str) -> None:
#: The filename of the package whose provenance was requested
self.filename = filename
#: The URL to which the failed request was made
self.url = url
def __str__(self) -> str:
return f"No .provenance file found for {self.filename} at {self.url}"

@@ -91,20 +91,20 @@ from __future__ import annotations

The package type may be any of the following strings:
The package type will be one of the following strings:
- ``'dumb'``
- ``'egg'``
- ``'msi'``
- ``'rpm'``
- ``'sdist'``
- ``'wheel'``
- ``'wininst'``
- ``"dumb"``
- ``"egg"``
- ``"msi"``
- ``"rpm"``
- ``"sdist"``
- ``"wheel"``
- ``"wininst"``
Note that some filenames (e.g., :file:`1-2-3.tar.gz`) may be ambiguous as
to which part is the project name and which is the version. In order to
resolve the ambiguity, the expected value for the project name (*modulo*
normalization) can be supplied as the ``project_name`` argument to the
function. If the filename can be parsed with the given string in the role
of the project name, the results of that parse will be returned; otherwise,
the function will fall back to breaking the project & version apart at an
unspecified point.
resolve the ambiguity, the expected value for the project name can be
supplied as the ``project_name`` argument to the function; it need not be
normalized. If the filename can be parsed with the given string in the
role of the project name, the results of that parse will be returned;
otherwise, the function will fall back to breaking the project & version
apart at an unspecified point.

@@ -111,0 +111,0 @@ .. versionchanged:: 1.0.0

@@ -39,2 +39,3 @@ from __future__ import annotations

upload_time: Optional[datetime] = None
provenance: Optional[str] = None

@@ -41,0 +42,0 @@ @property

from __future__ import annotations
from collections.abc import Callable
import sys
from types import TracebackType
from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Any, Optional, Protocol, runtime_checkable
if sys.version_info[:2] >= (3, 8):
from typing import Protocol, runtime_checkable
else:
from typing_extensions import Protocol, runtime_checkable
if TYPE_CHECKING:

@@ -26,4 +20,3 @@ from typing_extensions import Self

def __enter__(self) -> Self:
...
def __enter__(self) -> Self: ...

@@ -35,7 +28,5 @@ def __exit__(

exc_tb: Optional[TracebackType],
) -> Optional[bool]:
...
) -> Optional[bool]: ...
def update(self, increment: int) -> None:
...
def update(self, increment: int) -> None: ...

@@ -42,0 +33,0 @@

@@ -5,3 +5,3 @@ from __future__ import annotations

from typing import Any, Optional
from urllib.parse import urljoin
from urllib.parse import urljoin, urlparse, urlunparse
import warnings

@@ -50,8 +50,6 @@ from packaging.version import Version

@abstractmethod
def update(self, blob: bytes) -> None:
...
def update(self, blob: bytes) -> None: ...
@abstractmethod
def finalize(self) -> None:
...
def finalize(self) -> None: ...

@@ -68,5 +66,6 @@

class DigestChecker(AbstractDigestChecker):
def __init__(self, digests: dict[str, str]) -> None:
def __init__(self, digests: dict[str, str], url: str) -> None:
self.digesters: dict[str, Any] = {}
self.expected: dict[str, str] = {}
self.url = url
for alg, value in digests.items():

@@ -81,3 +80,3 @@ try:

if not self.digesters:
raise NoDigestsError("No digests with known algorithms available")
raise NoDigestsError(self.url)

@@ -96,2 +95,12 @@ def update(self, blob: bytes) -> None:

actual_digest=actual,
url=self.url,
)
def url_add_suffix(url: str, suffix: str) -> str:
"""
Append `suffix` to the path portion of the URL `url`. Any query parameters
or fragments on the URL are discarded.
"""
u = urlparse(url)
return urlunparse((u[0], u[1], u[2] + suffix, "", "", ""))
from __future__ import annotations
import filecmp
import hashlib
import json

@@ -16,2 +17,4 @@ from pathlib import Path

NoDigestsError,
NoMetadataError,
NoProvenanceError,
NoSuchProjectError,

@@ -669,3 +672,6 @@ ProgressTracker,

simple.download_package(pkg, dest)
assert str(excinfo.value) == "No digests with known algorithms available"
assert (
str(excinfo.value)
== "No digests with known algorithms available for resource at https://test.nil/simple/packages/click_loglevel-0.4.0.post1-py3-none-any.whl"
)
assert not dest.exists()

@@ -703,3 +709,3 @@

assert str(excinfo.value) == (
"sha256 digest of downloaded data is"
"sha256 digest of https://test.nil/simple/packages/click_loglevel-0.4.0.post1-py3-none-any.whl is"
" '17e88db187afd62c16e5debf3e6527cd006bc012bc90b51a810cd80c2d511f43'"

@@ -741,3 +747,3 @@ " instead of expected"

assert str(excinfo.value) == (
"sha256 digest of downloaded data is"
"sha256 digest of https://test.nil/simple/packages/click_loglevel-0.4.0.post1-py3-none-any.whl is"
" '17e88db187afd62c16e5debf3e6527cd006bc012bc90b51a810cd80c2d511f43'"

@@ -880,3 +886,2 @@ " instead of expected"

assert simple.get_package_metadata(pkg) == "\u2603"
pkg = DistributionPackage(

@@ -899,2 +904,34 @@ filename="example-0.0.2-py3-none-any.whl",

@responses.activate
def test_metadata_404() -> None:
responses.add(
method=responses.GET,
url="https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl.metadata",
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,
)
with pytest.raises(NoMetadataError) as excinfo:
simple.get_package_metadata(pkg, verify=False)
assert excinfo.value.filename == "sampleproject-1.2.3-py3-none-any.whl"
assert (
excinfo.value.url
== "https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl.metadata"
)
assert (
str(excinfo.value)
== "No distribution metadata found for sampleproject-1.2.3-py3-none-any.whl at https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl.metadata"
)
@responses.activate
def test_custom_headers_get_index_page() -> None:

@@ -941,1 +978,108 @@ with (DATA_DIR / "simple01.html").open() as fp:

simple.get_project_page("aws-adfs-ebsco", headers={"X-Custom": "foo"})
@responses.activate
def test_get_provenance() -> None:
provenance = {
"version": 1,
"attestation_bundles": [
{
"publisher": {
"kind": "important-ci-service",
"claims": {},
"vendor-property": "foo",
"another-property": 123,
},
"attestations": [
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "sampleproject-1.2.3.tar.gz",
"digest": {
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
}
],
"predicateType": "https://some-arbitrary-predicate.example.com/v1",
"predicate": {"something-else": "foo"},
}
],
}
],
}
provenance_bytes = json.dumps(provenance, sort_keys=True, indent=4).encode("utf-8")
responses.add(
method=responses.GET,
url="https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl.provenance",
body=provenance_bytes,
)
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=hashlib.sha256(provenance_bytes).hexdigest(),
)
assert simple.get_provenance(pkg, verify=True) == provenance
@responses.activate
def test_get_provenance_404() -> 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,
)
with pytest.raises(NoProvenanceError) as excinfo:
simple.get_provenance(pkg, verify=False)
assert excinfo.value.filename == "sampleproject-1.2.3-py3-none-any.whl"
assert (
excinfo.value.url
== "https://test.nil/simple/packages/sampleproject-1.2.3-py3-none-any.whl.provenance"
)
assert (
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"
)
@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,2 +99,3 @@ from __future__ import annotations

"data-yanked": "Oopsy.",
"data-provenance": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},

@@ -119,2 +120,3 @@ ),

has_metadata=True,
provenance_sha256="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
),

@@ -202,2 +204,23 @@ ),

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:

@@ -247,1 +270,20 @@ pkg = DistributionPackage.from_json_data(

assert pkg.metadata_digests == metadata_digests
def test_from_json_data_provenance() -> None:
pkg = DistributionPackage.from_json_data(
{
"filename": "argset-0.1.0-py3-none-any.whl",
"hashes": {
"sha256": "107a632c7112faceb9fd6e93658dd461154713db250f7ffde5bd473e17cf1db5"
},
"requires-python": "~=3.6",
"url": "https://files.pythonhosted.org/packages/b5/2b/7aa284f345e37f955d86e4cd57b1039b573552b0fc29d1a522ec05c1ee41/argset-0.1.0-py3-none-any.whl",
"yanked": False,
"provenance": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
}
)
assert (
pkg.provenance_sha256
== "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
)
[tox]
envlist = lint,typing,py37,py38,py39,py310,py311,py312,pypy3
envlist = lint,typing,py38,py39,py310,py311,py312,pypy3
skip_missing_interpreters = True

@@ -70,3 +70,3 @@ isolated_build = True

extend-select = B901,B902,B950
ignore = A003,B005,E203,E262,E266,E501,U101,W503
ignore = A003,A005,B005,E203,E262,E266,E501,E704,U101,W503

@@ -73,0 +73,0 @@ per-file-ignores =