From 94e08a153bd907d686e8ca84053cdd08dcf9b528 Mon Sep 17 00:00:00 2001 From: shamilbi Date: Mon, 2 Mar 2026 18:24:50 +0200 Subject: [PATCH 01/10] Accept-Encoding: gzip --- morgan/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/morgan/__init__.py b/morgan/__init__.py index bb72c79..2cab956 100644 --- a/morgan/__init__.py +++ b/morgan/__init__.py @@ -2,6 +2,7 @@ import argparse import configparser +import gzip import hashlib import json import os @@ -166,12 +167,18 @@ def _mirror( # noqa: C901, PLR0912 f"{self.index_url}{requirement.name}/", headers={ "Accept": "application/vnd.pypi.simple.v1+json", + "Accept-Encoding": "gzip", }, ) response_url = "" - with urllib.request.urlopen(request) as response: # noqa: S310 - data = json.load(response) + with urllib.request.urlopen(request) as response: + bytes1 = response.read() + try: + bytes2 = gzip.decompress(bytes1) + data = json.loads(bytes2) + except gzip.BadGzipFile: + data = json.loads(bytes1) response_url = str(response.url) if not data: msg = f"Failed loading metadata: {response}" From c854e906e07b04e161f563f1648001e22b1aecfd Mon Sep 17 00:00:00 2001 From: shamilbi Date: Mon, 2 Mar 2026 18:32:27 +0200 Subject: [PATCH 02/10] fix: ruff warnings --- morgan/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/morgan/__init__.py b/morgan/__init__.py index 2cab956..93b11de 100644 --- a/morgan/__init__.py +++ b/morgan/__init__.py @@ -140,6 +140,7 @@ def copy_server(self): with open(outpath, "w") as out: out.write(inspect.getsource(server)) + # ruff: noqa: PLR0915 def _mirror( # noqa: C901, PLR0912 self, requirement: packaging.requirements.Requirement, @@ -163,7 +164,7 @@ def _mirror( # noqa: C901, PLR0912 # get information about this package from the Simple API in JSON # format as per PEP 691 - request = urllib.request.Request( # noqa: S310 + request = urllib.request.Request( f"{self.index_url}{requirement.name}/", headers={ "Accept": "application/vnd.pypi.simple.v1+json", @@ -172,6 +173,7 @@ def _mirror( # noqa: C901, PLR0912 ) response_url = "" + # ruff: noqa: S310 with urllib.request.urlopen(request) as response: bytes1 = response.read() try: @@ -603,7 +605,7 @@ def _download_file( return True print("\t{}...".format(fileinfo["url"]), end=" ") - with urllib.request.urlopen(fileinfo["url"]) as inp, open(target, "wb") as out: # noqa: S310 + with urllib.request.urlopen(fileinfo["url"]) as inp, open(target, "wb") as out: out.write(inp.read()) print("done") From 3d2f9501e4c1a53d8a75b01e138a78c5120f648b Mon Sep 17 00:00:00 2001 From: shamilbi Date: Tue, 3 Mar 2026 19:20:20 +0200 Subject: [PATCH 03/10] +download_req() --- morgan/utils.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/morgan/utils.py b/morgan/utils.py index a2e48cb..909d26e 100644 --- a/morgan/utils.py +++ b/morgan/utils.py @@ -1,7 +1,10 @@ from __future__ import annotations +import gzip +import json import os import re +import urllib.request from collections import OrderedDict from typing import TYPE_CHECKING, Iterable @@ -48,6 +51,7 @@ def is_simple_case(self, req: Requirement) -> bool: if not specifier: return True # ruff: noqa: SLF001 + # pylint: disable=protected-access if all(spec.operator in (">", ">=") for spec in specifier._specs): return True return False @@ -151,3 +155,31 @@ def __setitem__(self, key, value): self[key].extend(value) else: super().__setitem__(key, value) + + +def download_req(index_url: str, req_name: str) -> tuple[dict, str]: + # get information about this package from the Simple API in JSON + # format as per PEP 691 + url = index_url.rstrip("/") + request = urllib.request.Request( + f"{url}/{req_name}/", + headers={ + "Accept": "application/vnd.pypi.simple.v1+json", + "Accept-Encoding": "gzip", + }, + ) + + response_url = "" + # ruff: noqa: S310 + with urllib.request.urlopen(request) as response: + bytes1 = response.read() + try: + bytes2 = gzip.decompress(bytes1) + data = json.loads(bytes2) + except gzip.BadGzipFile: + data = json.loads(bytes1) + response_url = str(response.url) + if data: + return data, response_url + msg = f"Failed loading metadata: {response}" + raise RuntimeError(msg) From 1030920500e8c138afed5586095a3d351f9d99d0 Mon Sep 17 00:00:00 2001 From: shamilbi Date: Tue, 3 Mar 2026 19:22:09 +0200 Subject: [PATCH 04/10] refactor: use download_req() --- morgan/__init__.py | 33 ++++----------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/morgan/__init__.py b/morgan/__init__.py index 93b11de..c09d747 100644 --- a/morgan/__init__.py +++ b/morgan/__init__.py @@ -2,9 +2,7 @@ import argparse import configparser -import gzip import hashlib -import json import os import os.path import re @@ -27,6 +25,7 @@ from morgan.utils import ( Cache, ListExtendingOrderedDict, + download_req, is_requirement_relevant, to_single_dash, touch_file, @@ -140,7 +139,6 @@ def copy_server(self): with open(outpath, "w") as out: out.write(inspect.getsource(server)) - # ruff: noqa: PLR0915 def _mirror( # noqa: C901, PLR0912 self, requirement: packaging.requirements.Requirement, @@ -160,31 +158,7 @@ def _mirror( # noqa: C901, PLR0912 else: print(f"{requirement}") - data: dict | None = None - - # get information about this package from the Simple API in JSON - # format as per PEP 691 - request = urllib.request.Request( - f"{self.index_url}{requirement.name}/", - headers={ - "Accept": "application/vnd.pypi.simple.v1+json", - "Accept-Encoding": "gzip", - }, - ) - - response_url = "" - # ruff: noqa: S310 - with urllib.request.urlopen(request) as response: - bytes1 = response.read() - try: - bytes2 = gzip.decompress(bytes1) - data = json.loads(bytes2) - except gzip.BadGzipFile: - data = json.loads(bytes1) - response_url = str(response.url) - if not data: - msg = f"Failed loading metadata: {response}" - raise RuntimeError(msg) + data, response_url = download_req(self.index_url, requirement.name) # check metadata version ~1.0 v_str = data["meta"]["api-version"] @@ -458,7 +432,7 @@ def _matches_environments( # noqa: C901, PLR0912 if fileinfo.get("tags"): # At least one of the tags must match ALL of our environments for tag in fileinfo["tags"]: - (intrp_name, intrp_ver) = parse_interpreter(tag.interpreter) + intrp_name, intrp_ver = parse_interpreter(tag.interpreter) if intrp_name not in ("py", "cp"): continue @@ -605,6 +579,7 @@ def _download_file( return True print("\t{}...".format(fileinfo["url"]), end=" ") + # ruff: noqa: S310 with urllib.request.urlopen(fileinfo["url"]) as inp, open(target, "wb") as out: out.write(inp.read()) print("done") From f4f138f272a0b231816ac6f7f41dd8f5bc73602b Mon Sep 17 00:00:00 2001 From: shamilbi Date: Tue, 3 Mar 2026 20:06:08 +0200 Subject: [PATCH 05/10] fix: Content-Encoding: https://github.com/ido50/morgan/pull/86#discussion_r2878060290 --- morgan/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/morgan/utils.py b/morgan/utils.py index 909d26e..1fa74b1 100644 --- a/morgan/utils.py +++ b/morgan/utils.py @@ -172,12 +172,12 @@ def download_req(index_url: str, req_name: str) -> tuple[dict, str]: response_url = "" # ruff: noqa: S310 with urllib.request.urlopen(request) as response: - bytes1 = response.read() - try: - bytes2 = gzip.decompress(bytes1) - data = json.loads(bytes2) - except gzip.BadGzipFile: - data = json.loads(bytes1) + # Check if response is gzip-encoded + if response.headers.get("Content-Encoding") == "gzip": + with gzip.GzipFile(fileobj=response) as gzip_response: + data = json.load(gzip_response) + else: + data = json.load(response) response_url = str(response.url) if data: return data, response_url From 2f1aedb12a29d6b215fbaf4e3aa8c9a6b3178ace Mon Sep 17 00:00:00 2001 From: shamilbi Date: Wed, 8 Jul 2026 20:10:18 +0200 Subject: [PATCH 06/10] sync with main --- morgan/__init__.py | 18 ++++++++++-------- tests/test_init.py | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/morgan/__init__.py b/morgan/__init__.py index c09d747..a52bd80 100644 --- a/morgan/__init__.py +++ b/morgan/__init__.py @@ -147,17 +147,20 @@ def _mirror( # noqa: C901, PLR0912 if self._processed_pkgs.check(requirement): return None - # Check if requirement is relevant for any environment - if not is_requirement_relevant(requirement, self.envs.values()): - print(f"\tSkipping {requirement}, not relevant for any environment") - self._processed_pkgs.add(requirement) # Mark as processed - return None - + # Display the cause of 'Skipping...' + extras = None if required_by: + extras = required_by.extras print(f"[{required_by}]: {requirement}") else: print(f"{requirement}") + # Check if requirement is relevant for any environment + if not is_requirement_relevant(requirement, self.envs.values(), extras=extras): + print("\tSkipping, not relevant for any environment") + self._processed_pkgs.add(requirement) # Mark as processed + return None + data, response_url = download_req(self.index_url, requirement.name) # check metadata version ~1.0 @@ -579,8 +582,7 @@ def _download_file( return True print("\t{}...".format(fileinfo["url"]), end=" ") - # ruff: noqa: S310 - with urllib.request.urlopen(fileinfo["url"]) as inp, open(target, "wb") as out: + with urllib.request.urlopen(fileinfo["url"]) as inp, open(target, "wb") as out: # noqa: S310 out.write(inp.read()) print("done") diff --git a/tests/test_init.py b/tests/test_init.py index d7b65f4..56b9123 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -2,8 +2,10 @@ from __future__ import annotations import argparse +import contextlib import hashlib import os +import urllib.error import packaging.requirements import packaging.version @@ -376,6 +378,25 @@ def test_filter_files_with_latest_version_mirrored( assert self.extract_versions(filtered_files) == expected_versions + def test_skipping(self, make_mirrorer): + """Test that requirement not skipping.""" + mirrorer = make_mirrorer(mirror_all_versions=False) + required_by = packaging.requirements.Requirement("pyjwt[crypto]==2.10.1") + requirement = packaging.requirements.Requirement( + 'cryptography>=3.4.0; extra == "crypto"', + ) + + res = {} + with contextlib.suppress(urllib.error.HTTPError): + # if skipping then None + res = mirrorer._mirror( # noqa: SLF001 + requirement=requirement, + required_by=required_by, + ) + # if not skipping then HTTPError + + assert res is not None + @pytest.mark.parametrize( ("version_spec", "expected_versions"), [ From df4c517d50c383f51181deaadde789f883c1eee4 Mon Sep 17 00:00:00 2001 From: shamilbi Date: Wed, 8 Jul 2026 20:17:21 +0200 Subject: [PATCH 07/10] sync with main --- morgan/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/morgan/__init__.py b/morgan/__init__.py index a52bd80..1e5d8a8 100644 --- a/morgan/__init__.py +++ b/morgan/__init__.py @@ -435,7 +435,7 @@ def _matches_environments( # noqa: C901, PLR0912 if fileinfo.get("tags"): # At least one of the tags must match ALL of our environments for tag in fileinfo["tags"]: - intrp_name, intrp_ver = parse_interpreter(tag.interpreter) + (intrp_name, intrp_ver) = parse_interpreter(tag.interpreter) if intrp_name not in ("py", "cp"): continue From 387f9e5c8631851b3af53b6406a25dc17b233bca Mon Sep 17 00:00:00 2001 From: shamilbi Date: Wed, 8 Jul 2026 20:23:45 +0200 Subject: [PATCH 08/10] sync with main --- .github/workflows/formatting.yml | 4 ++-- .github/workflows/tests.yml | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 9d320fc..47ad016 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -14,9 +14,9 @@ jobs: ruff-format: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install ruff based off pyproject.toml - uses: astral-sh/ruff-action@4919ec5cf1f49eff0871dbcea0da843445b837e6 # v3.6.1 + uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0 with: args: "format --check --diff" # only check formatting here version-file: "pyproject.toml" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 248b1c6..d1c3931 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,9 +29,9 @@ jobs: platform-version: "ubuntu-22.04" runs-on: ${{ matrix.platform-version }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -45,18 +45,18 @@ jobs: ruff: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install ruff based off pyproject.toml - uses: astral-sh/ruff-action@4919ec5cf1f49eff0871dbcea0da843445b837e6 # v3.6.1 + uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0 with: version-file: "pyproject.toml" mypy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: "3.x" - name: Install dependencies From 4bd99a8860af74e026eab5f144a7856a4e99cdca Mon Sep 17 00:00:00 2001 From: shamilbi Date: Wed, 8 Jul 2026 21:01:27 +0200 Subject: [PATCH 09/10] refactor: download_req -> __init__.py --- morgan/__init__.py | 30 +++++++++++++++++++++++++++++- morgan/utils.py | 31 ------------------------------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/morgan/__init__.py b/morgan/__init__.py index 1e5d8a8..30aac4a 100644 --- a/morgan/__init__.py +++ b/morgan/__init__.py @@ -2,7 +2,9 @@ import argparse import configparser +import gzip import hashlib +import json import os import os.path import re @@ -25,7 +27,6 @@ from morgan.utils import ( Cache, ListExtendingOrderedDict, - download_req, is_requirement_relevant, to_single_dash, touch_file, @@ -834,5 +835,32 @@ def my_url(arg): Mirrorer(args).copy_server() +def download_req(index_url: str, req_name: str) -> tuple[dict, str]: + # get information about this package from the Simple API in JSON + # format as per PEP 691 + url = index_url.rstrip("/") + request = urllib.request.Request( # noqa: S310 + f"{url}/{req_name}/", + headers={ + "Accept": "application/vnd.pypi.simple.v1+json", + "Accept-Encoding": "gzip", + }, + ) + + response_url = "" + with urllib.request.urlopen(request) as response: # noqa: S310 + # Check if response is gzip-encoded + if response.headers.get("Content-Encoding") == "gzip": + with gzip.GzipFile(fileobj=response) as gzip_response: + data = json.load(gzip_response) + else: + data = json.load(response) + response_url = str(response.url) + if data: + return data, response_url + msg = f"Failed loading metadata: {response}" + raise RuntimeError(msg) + + if __name__ == "__main__": main() diff --git a/morgan/utils.py b/morgan/utils.py index 1fa74b1..711ae37 100644 --- a/morgan/utils.py +++ b/morgan/utils.py @@ -1,10 +1,7 @@ from __future__ import annotations -import gzip -import json import os import re -import urllib.request from collections import OrderedDict from typing import TYPE_CHECKING, Iterable @@ -155,31 +152,3 @@ def __setitem__(self, key, value): self[key].extend(value) else: super().__setitem__(key, value) - - -def download_req(index_url: str, req_name: str) -> tuple[dict, str]: - # get information about this package from the Simple API in JSON - # format as per PEP 691 - url = index_url.rstrip("/") - request = urllib.request.Request( - f"{url}/{req_name}/", - headers={ - "Accept": "application/vnd.pypi.simple.v1+json", - "Accept-Encoding": "gzip", - }, - ) - - response_url = "" - # ruff: noqa: S310 - with urllib.request.urlopen(request) as response: - # Check if response is gzip-encoded - if response.headers.get("Content-Encoding") == "gzip": - with gzip.GzipFile(fileobj=response) as gzip_response: - data = json.load(gzip_response) - else: - data = json.load(response) - response_url = str(response.url) - if data: - return data, response_url - msg = f"Failed loading metadata: {response}" - raise RuntimeError(msg) From cf5895b8a54eed6564d62a3220fb657b5b6cd000 Mon Sep 17 00:00:00 2001 From: shamilbi Date: Wed, 8 Jul 2026 21:26:02 +0200 Subject: [PATCH 10/10] sync with main --- morgan/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/morgan/utils.py b/morgan/utils.py index 711ae37..a2e48cb 100644 --- a/morgan/utils.py +++ b/morgan/utils.py @@ -48,7 +48,6 @@ def is_simple_case(self, req: Requirement) -> bool: if not specifier: return True # ruff: noqa: SLF001 - # pylint: disable=protected-access if all(spec.operator in (">", ">=") for spec in specifier._specs): return True return False