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.4.1
to
1.5.0
+15
-0
CHANGELOG.md

@@ -0,1 +1,16 @@

v1.5.0 (2024-02-24)
-------------------
- **Bugfix**: Fix parsing of "true" `data-core-metadata` attributes and
handling of the attribute's absence (contributed by
[@thatch](https://github.com/thatch))
- `DistributionPackage.has_metadata` will now be `None` if this attribute
was absent in the HTML returned by PyPI. Previously, it would be `False`
under this circumstance.
- Added `PyPISimple.get_package_metadata_bytes()` (contributed by
[@thatch](https://github.com/thatch))
- `PyPISimple.get_package_metadata()` now always decodes responses as UTF-8
(contributed by [@thatch](https://github.com/thatch))
- Request methods now take optional `headers` arguments (contributed by
[@thatch](https://github.com/thatch))
v1.4.1 (2024-01-30)

@@ -2,0 +17,0 @@ -------------------

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

v1.5.0 (2024-02-24)
-------------------
- **Bugfix**: Fix parsing of "true" ``data-core-metadata`` attributes and
handling of the attribute's absence (contributed by `@thatch
<https://github.com/thatch>`_)
- `DistributionPackage.has_metadata` will now be `None` if this attribute was
absent in the HTML returned by PyPI. Previously, it would be `False` under
this circumstance.
- Added `PyPISimple.get_package_metadata_bytes()` (contributed by `@thatch
<https://github.com/thatch>`_)
- `PyPISimple.get_package_metadata()` now always decodes responses as UTF-8
(contributed by `@thatch <https://github.com/thatch>`_)
- Request methods now take optional ``headers`` arguments (contributed by
`@thatch <https://github.com/thatch>`_)
v1.4.1 (2024-01-30)

@@ -8,0 +28,0 @@ -------------------

+1
-1
Metadata-Version: 2.1
Name: pypi-simple
Version: 1.4.1
Version: 1.5.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.4.1"
__version__ = "1.5.0"
__author__ = "John Thorvald Wodder II"

@@ -20,0 +20,0 @@ __author_email__ = "pypi-simple@varonathe.org"

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

metadata_digests: Optional[dict[str, str]]
has_metadata = None
if mddigest is not None:

@@ -152,2 +153,3 @@ metadata_digests = {}

metadata_digests[m[1]] = m[2]
has_metadata = bool(m) or mddigest.lower() == "true"
else:

@@ -168,3 +170,3 @@ metadata_digests = None

metadata_digests=metadata_digests,
has_metadata=metadata_digests is not None,
has_metadata=has_metadata,
)

@@ -171,0 +173,0 @@

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

accept: Optional[str] = None,
headers: Optional[dict[str, str]] = None,
) -> IndexPage:

@@ -117,2 +118,6 @@ """

.. versionchanged:: 1.5.0
``headers`` parameter added
:param timeout: optional timeout to pass to the ``requests`` call

@@ -124,2 +129,4 @@ :type timeout: float | tuple[float,float] | None

defaults to the value supplied on client instantiation
:param Optional[dict[str, str]] headers:
Custom headers to provide for the request.
:rtype: IndexPage

@@ -133,4 +140,9 @@ :raises requests.HTTPError: if the repository responds with an HTTP

"""
request_headers = {"Accept": accept or self.accept}
if headers:
request_headers.update(headers)
r = self.s.get(
self.endpoint, timeout=timeout, headers={"Accept": accept or self.accept}
self.endpoint,
timeout=timeout,
headers=request_headers,
)

@@ -145,2 +157,3 @@ r.raise_for_status()

accept: Optional[str] = None,
headers: Optional[dict[str, str]] = None,
) -> Iterator[str]:

@@ -172,2 +185,6 @@ """

.. versionchanged:: 1.5.0
``headers`` parameter added
:param int chunk_size: how many bytes to read from the response at a

@@ -181,2 +198,4 @@ time

defaults to the value supplied on client instantiation
:param Optional[dict[str, str]] headers:
Custom headers to provide for the request.
:rtype: Iterator[str]

@@ -190,2 +209,5 @@ :raises requests.HTTPError: if the repository responds with an HTTP

"""
request_headers = {"Accept": accept or self.accept}
if headers:
request_headers.update(headers)
with self.s.get(

@@ -195,3 +217,3 @@ self.endpoint,

timeout=timeout,
headers={"Accept": accept or self.accept},
headers=request_headers,
) as r:

@@ -217,2 +239,3 @@ r.raise_for_status()

accept: Optional[str] = None,
headers: Optional[dict[str, str]] = None,
) -> ProjectPage:

@@ -232,2 +255,6 @@ """

.. versionchanged:: 1.5.0
``headers`` parameter added
:param str project: The name of the project to fetch information on.

@@ -241,2 +268,4 @@ The name does not need to be normalized.

defaults to the value supplied on client instantiation
:param Optional[dict[str, str]] headers:
Custom headers to provide for the request.
:rtype: ProjectPage

@@ -252,4 +281,7 @@ :raises NoSuchProjectError: if the repository responds with a 404 error

"""
request_headers = {"Accept": accept or self.accept}
if headers:
request_headers.update(headers)
url = self.get_project_url(project)
r = self.s.get(url, timeout=timeout, headers={"Accept": accept or self.accept})
r = self.s.get(url, timeout=timeout, headers=request_headers)
if r.status_code == 404:

@@ -278,2 +310,3 @@ raise NoSuchProjectError(project, url)

timeout: float | tuple[float, float] | None = None,
headers: Optional[dict[str, str]] = None,
) -> None:

@@ -293,2 +326,6 @@ """

.. versionchanged:: 1.5.0
``headers`` parameter added
:param DistributionPackage pkg: the distribution package to download

@@ -306,2 +343,4 @@ :param path:

:type timeout: float | tuple[float,float] | None
:param Optional[dict[str, str]] headers:
Custom headers to provide for the request.
:raises requests.HTTPError: if the repository responds with an HTTP

@@ -323,3 +362,3 @@ error code

digester = NullDigestChecker()
with self.s.get(pkg.url, stream=True, timeout=timeout) as r:
with self.s.get(pkg.url, stream=True, timeout=timeout, headers=headers) as r:
r.raise_for_status()

@@ -348,3 +387,3 @@ try:

def get_package_metadata(
def get_package_metadata_bytes(
self,

@@ -354,9 +393,12 @@ pkg: DistributionPackage,

timeout: float | tuple[float, float] | None = None,
) -> str:
headers: Optional[dict[str, str]] = None,
) -> bytes:
"""
.. versionadded:: 1.3.0
.. versionadded:: 1.5.0
Retrieve the `distribution metadata`_ for the given
`DistributionPackage`. The metadata can then be parsed with, for
example, |the packaging package|_.
`DistributionPackage` as raw bytes. This method is lower-level than
`PyPISimple.get_package_metadata()` and is most appropriate if you want
to defer interpretation of the data (e.g., if you're just writing to a
file) or want to customize the handling of non-UTF-8 data.

@@ -370,8 +412,2 @@ Not all packages have distribution metadata available for download; the

.. _distribution metadata:
https://packaging.python.org/en/latest/specifications/core-metadata/
.. |the packaging package| replace:: the ``packaging`` package
.. _the packaging package:
https://packaging.pypa.io/en/stable/metadata.html
:param DistributionPackage pkg:

@@ -383,2 +419,5 @@ the distribution package to retrieve the metadata of

:type timeout: float | tuple[float,float] | None
:param Optional[dict[str, str]] headers:
Custom headers to provide for the request.
:rtype: bytes

@@ -401,3 +440,3 @@ :raises NoMetadataError:

digester = NullDigestChecker()
r = self.s.get(pkg.metadata_url, timeout=timeout)
r = self.s.get(pkg.metadata_url, timeout=timeout, headers=headers)
if r.status_code == 404:

@@ -408,5 +447,64 @@ raise NoMetadataError(pkg.filename)

digester.finalize()
return r.text
return r.content
def get_package_metadata(
self,
pkg: DistributionPackage,
verify: bool = True,
timeout: float | tuple[float, float] | None = None,
headers: Optional[dict[str, str]] = None,
) -> str:
"""
.. versionadded:: 1.3.0
Retrieve the `distribution metadata`_ for the given
`DistributionPackage` and decode it as UTF-8. The metadata can then be
parsed with, for example, |the packaging package|_.
Not all packages have distribution metadata available for download; the
`DistributionPackage.has_metadata` attribute can be used to check
whether the repository reported the availability of the metadata. This
method will always attempt to download metadata regardless of the value
of `~DistributionPackage.has_metadata`; if the server replies with a
404, a `NoMetadataError` is raised.
.. _distribution metadata:
https://packaging.python.org/en/latest/specifications/core-metadata/
.. |the packaging package| replace:: the ``packaging`` package
.. _the packaging package:
https://packaging.pypa.io/en/stable/metadata.html
.. versionchanged:: 1.5.0
``headers`` parameter added
:param DistributionPackage pkg:
the distribution package to retrieve the metadata of
:param bool verify:
whether to verify the metadata's digests 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: str
:raises NoMetadataError:
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 the given package's metadata does not
have any digests with known algorithms
:raises DigestMismatchError:
if ``verify`` is true and the digest of the downloaded data does
not match the expected value
"""
return self.get_package_metadata_bytes(
pkg,
verify,
timeout,
headers,
).decode("utf-8", "surrogateescape")
class NoSuchProjectError(Exception):

@@ -413,0 +511,0 @@ """

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

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -111,3 +111,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -128,3 +128,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -145,3 +145,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -162,3 +162,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -179,3 +179,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -229,3 +229,3 @@ ],

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -246,3 +246,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -307,3 +307,3 @@ ],

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -352,3 +352,3 @@ ],

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -400,3 +400,3 @@ ],

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -644,3 +644,3 @@ ],

metadata_digests=None,
has_metadata=False,
has_metadata=None,
)

@@ -675,3 +675,3 @@ dest = tmp_path / str(pkg.project) / pkg.filename

metadata_digests=None,
has_metadata=False,
has_metadata=None,
)

@@ -708,3 +708,3 @@ dest = tmp_path / str(pkg.project) / pkg.filename

metadata_digests=None,
has_metadata=False,
has_metadata=None,
)

@@ -746,3 +746,3 @@ dest = tmp_path / str(pkg.project) / pkg.filename

metadata_digests=None,
has_metadata=False,
has_metadata=None,
)

@@ -792,3 +792,3 @@ dest = tmp_path / str(pkg.project) / pkg.filename

metadata_digests=None,
has_metadata=False,
has_metadata=None,
)

@@ -849,3 +849,3 @@ dest = tmp_path / str(pkg.project) / pkg.filename

metadata_digests=None,
has_metadata=False,
has_metadata=None,
)

@@ -864,1 +864,89 @@ dest = tmp_path / str(pkg.project) / pkg.filename

assert spy.updates == [65535] * (size // 65535) + [size % 65535]
@responses.activate
def test_metadata_encoding() -> None:
responses.add(
method=responses.GET,
url="https://test.nil/simple/packages/example-0.0.1-py3-none-any.whl.metadata",
body=b"\xe2\x98\x83", # unicode snowman
)
responses.add(
method=responses.GET,
url="https://test.nil/simple/packages/example-0.0.2-py3-none-any.whl.metadata",
body=b"\xff\xfe\x03\x26", # unicode snowman in utf-16
)
with PyPISimple("https://test.nil/simple/") as simple:
pkg = DistributionPackage(
filename="example-0.0.1-py3-none-any.whl",
project="example",
version="0.0.1",
package_type="wheel",
url="https://test.nil/simple/packages/example-0.0.1-py3-none-any.whl",
digests={},
requires_python=None,
has_sig=None,
has_metadata=True,
metadata_digests={"sha1": "2686137311c038a99622242fdb662b88c221c08d"},
)
assert simple.get_package_metadata_bytes(pkg) == b"\xe2\x98\x83"
assert simple.get_package_metadata(pkg) == "\u2603"
pkg = DistributionPackage(
filename="example-0.0.2-py3-none-any.whl",
project="example",
version="0.0.2",
package_type="wheel",
url="https://test.nil/simple/packages/example-0.0.2-py3-none-any.whl",
digests={},
requires_python=None,
has_sig=None,
has_metadata=True,
metadata_digests={"sha1": "7381ac0d9ddb35e4074acfd9cf72ea47314da70b"},
)
assert simple.get_package_metadata_bytes(pkg) == b"\xff\xfe\x03\x26"
assert simple.get_package_metadata(pkg) == "\udcff\udcfe\u0003\u0026"
@responses.activate
def test_custom_headers_get_index_page() -> None:
with (DATA_DIR / "simple01.html").open() as fp:
responses.add(
method=responses.GET,
url="https://test.nil/simple/",
body=fp.read(),
content_type="text/html",
match=[responses.matchers.header_matcher({"X-Custom": "foo"})],
)
with PyPISimple("https://test.nil/simple/") as simple:
# Just check that the method returns successfully
simple.get_index_page(headers={"X-Custom": "foo"})
@responses.activate
def test_custom_headers_stream_project_names() -> None:
with (DATA_DIR / "simple01.html").open() as fp:
responses.add(
method=responses.GET,
url="https://test.nil/simple/",
body=fp.read(),
content_type="text/html",
match=[responses.matchers.header_matcher({"X-Custom": "foo"})],
)
with PyPISimple("https://test.nil/simple/") as simple:
# Just check that the method returns successfully
list(simple.stream_project_names(headers={"X-Custom": "foo"}))
@responses.activate
def test_custom_headers_get_project_page() -> None:
with (DATA_DIR / "aws-adfs-ebsco.html").open() as fp:
responses.add(
method=responses.GET,
url="https://test.nil/simple/aws-adfs-ebsco/",
body=fp.read(),
content_type="text/html",
match=[responses.matchers.header_matcher({"X-Custom": "foo"})],
)
with PyPISimple("https://test.nil/simple/") as simple:
simple.get_project_page("aws-adfs-ebsco", headers={"X-Custom": "foo"})

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

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -127,3 +127,3 @@ ),

"data-gpg-sig": "false",
"data-core-metadata": "sha256=true",
"data-core-metadata": "true",
},

@@ -130,0 +130,0 @@ ),

@@ -50,3 +50,3 @@ from datetime import datetime, timezone

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -67,3 +67,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -84,3 +84,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -101,3 +101,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -118,3 +118,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -135,3 +135,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -152,3 +152,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -169,3 +169,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -186,3 +186,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -203,3 +203,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -220,3 +220,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -237,3 +237,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -267,3 +267,3 @@ ],

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -284,3 +284,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -301,3 +301,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -318,3 +318,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -348,3 +348,3 @@ ],

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -365,3 +365,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -382,3 +382,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -399,3 +399,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -416,3 +416,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -446,3 +446,3 @@ ],

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -463,3 +463,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -493,3 +493,3 @@ ],

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -510,3 +510,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -548,3 +548,3 @@ ],

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -565,3 +565,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -582,3 +582,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -599,3 +599,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -616,3 +616,3 @@ DistributionPackage(

metadata_digests=None,
has_metadata=False,
has_metadata=None,
),

@@ -619,0 +619,0 @@ ],