From fa95bce295e06a1830c9e39721dcb5d9e0df2b88 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Sat, 4 Jul 2026 02:15:32 +0200 Subject: [PATCH 1/4] more fields in pypi section --- ecosystem/cli/ci.py | 4 +++ ecosystem/pypi.py | 62 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/ecosystem/cli/ci.py b/ecosystem/cli/ci.py index bed7b109f7..2ee2f86696 100644 --- a/ecosystem/cli/ci.py +++ b/ecosystem/cli/ci.py @@ -133,6 +133,10 @@ def update_member_data( dao = DAO(path=resources_dir) for member in dao.get_all(member_id): print(f"\n::group:: {member.name}️ ({member.name_id})") + if member.status == "Alumni": + print('member.status == "Alumni", so skip') + print("::endgroup::") + continue for update_method_str in to_update: print(f"Updating {update_method_str}️") update_method = getattr(member, f"update_{update_method_str}") diff --git a/ecosystem/pypi.py b/ecosystem/pypi.py index d3294b9d3c..b831a5bb4e 100644 --- a/ecosystem/pypi.py +++ b/ecosystem/pypi.py @@ -43,6 +43,8 @@ class PyPIData(JsonSerializable): "description", "url", "development_status", + "status", + "maintainers", "requires_qiskit", "compatible_with_qiskit_v1", "compatible_with_qiskit_v2", @@ -54,7 +56,6 @@ class PyPIData(JsonSerializable): aliases = { "version": "info.version", "url": "info.package_url", - "license": "info.license_expression", "description": "info.summary", "development_status": "info.classifiers[?('Development Status' in @)]", } @@ -68,6 +69,7 @@ def __init__(self, package_name: str, **kwargs): self.package_name = canonicalize_name(package_name, validate=True) self._kwargs = kwargs or {} self._pypi_json = None + self._pypi_simple_json = None self._pypistats_json = None self._all_qiskit_versions = None @@ -103,12 +105,22 @@ def from_url(cls, pypi_project_url): def update_json(self): """ - Fetches remote json data from https://pypi.org/pypi/{self.package_name}/json + Fetches remote jsons data from: + - https://pypi.org/pypi/{self.package_name}/json + - https://pypi.org/simple/{self.package_name}/ + - https://pypistats.org/api/packages/{self.package_name}/ """ try: self._pypi_json = request_json(f"pypi.org/pypi/{self.package_name}/json") except EcosystemError: pass + try: + self._pypi_simple_json = request_json( + f"https://pypi.org/simple/{self.package_name}/", + headers={"Accept": "application/vnd.pypi.simple.v1+json"}, + ) + except EcosystemError: + pass if self._pypistats_json is None: self._pypistats_json = self.request_pypistats() @@ -167,7 +179,7 @@ def requires_qiskit(self): if requirement.name == "qiskit": if len(requirement.specifier): self._kwargs["requires_qiskit"] = str(requirement.specifier) - else: + elif self._kwargs["requires_qiskit"] != ">=0": logger.warning( "%s depends on qiskit but with empty specifier. " 'Forcing one, ">=0"', @@ -333,3 +345,47 @@ def last_180_days_downloads(self): "without_mirrors" ) return self._kwargs.get("last_180_days_downloads") + + @property + def license(self): + """Package license""" + if self._pypi_json: + info_license = self._pypi_json.get("info", {}).get("license") + if info_license and len(info_license) < 50: + return info_license + info_license_expression = self._pypi_json.get("info", {}).get( + "license_expression" + ) + if info_license_expression: + return info_license_expression + for classifier in self._pypi_json.get("info", {}).get("classifiers"): + if classifier.startswith("License :: "): + parts = [part.strip() for part in classifier.split("::")] + if len(parts) == 3: + return parts[2] + continue + return self._kwargs.get("license") + + @property + def maintainers(self): + """Package maintainers (or owners)""" + maintainers = [] + if self._pypi_json: + ownership = self._pypi_json.get("ownership") + organization = ownership.get("organization") + if organization: + maintainers.append(f"https://pypi.org/org/{organization}/") + maintainers += [ + f"https://pypi.org/user/{u['user']}/" + for u in ownership["roles"] + if u["role"] == "Owner" + ] + return maintainers or self._kwargs.get("maintainers") + + @property + def status(self): + """Project status""" + project_status = None + if self._pypi_simple_json: + return self._pypi_simple_json.get("project-status", {}).get("status") + return project_status or self._kwargs.get("status") From cb08c3d5cc957b5be5abb2084623da122c2d4732 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Sat, 4 Jul 2026 02:49:21 +0200 Subject: [PATCH 2/4] test and lint --- ecosystem/pypi.py | 23 ++++++++++++++++------- tests/test_pypi.py | 18 +++++++++++++----- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/ecosystem/pypi.py b/ecosystem/pypi.py index b831a5bb4e..1addf7d370 100644 --- a/ecosystem/pypi.py +++ b/ecosystem/pypi.py @@ -110,19 +110,25 @@ def update_json(self): - https://pypi.org/simple/{self.package_name}/ - https://pypistats.org/api/packages/{self.package_name}/ """ + self._pypi_json = self.request_pypi() + self._pypi_simple_json = self.request_pypi_simple() + if self._pypistats_json is None: + self._pypistats_json = self.request_pypistats() + + def request_pypi(self): try: - self._pypi_json = request_json(f"pypi.org/pypi/{self.package_name}/json") + return request_json(f"pypi.org/pypi/{self.package_name}/json") except EcosystemError: - pass + return None + + def request_pypi_simple(self): try: - self._pypi_simple_json = request_json( + return request_json( f"https://pypi.org/simple/{self.package_name}/", headers={"Accept": "application/vnd.pypi.simple.v1+json"}, ) except EcosystemError: - pass - if self._pypistats_json is None: - self._pypistats_json = self.request_pypistats() + return None def __getattr__(self, item): if self._pypi_json: @@ -179,7 +185,10 @@ def requires_qiskit(self): if requirement.name == "qiskit": if len(requirement.specifier): self._kwargs["requires_qiskit"] = str(requirement.specifier) - elif self._kwargs["requires_qiskit"] != ">=0": + elif ( + "requires_qiskit" not in self._kwargs + or self._kwargs["requires_qiskit"] != ">=0" + ): logger.warning( "%s depends on qiskit but with empty specifier. " 'Forcing one, ">=0"', diff --git a/tests/test_pypi.py b/tests/test_pypi.py index ebb7fc3144..0c58ca18b7 100644 --- a/tests/test_pypi.py +++ b/tests/test_pypi.py @@ -90,18 +90,26 @@ def test_from_url_rejects_invalid_pypi_urls(self): def test_update_json_fetches_pypi_and_pypistats_data(self): """update_json stores PyPI and stats payloads.""" pypi_payload = {"info": {"version": "1.2.3"}} + pypi_simple_payload = {"project-status": {"status": "active"}} stats_payload = {"recent_downloads": {"last_month": 10}} pypi_data = PyPIData("banana-compiler") - with patch("ecosystem.pypi.request_json", return_value=pypi_payload) as request: + with patch.object( + PyPIData, "request_pypi", return_value=pypi_payload + ) as request_pypi: with patch.object( - PyPIData, "request_pypistats", return_value=stats_payload - ) as request_stats: - pypi_data.update_json() + PyPIData, "request_pypi_simple", return_value=pypi_simple_payload + ) as request_simple: + with patch.object( + PyPIData, "request_pypistats", return_value=stats_payload + ) as request_stats: + pypi_data.update_json() - request.assert_called_once_with("pypi.org/pypi/banana-compiler/json") + request_pypi.assert_called_once_with() + request_simple.assert_called_once_with() request_stats.assert_called_once_with() self.assertEqual(pypi_payload, pypi_data.pypi_json) + self.assertEqual("active", pypi_data.status) self.assertEqual(10, pypi_data.last_month_downloads) def test_update_json_ignores_pypi_fetch_errors(self): From 7d689cc042d7991eb96c03274ab68b3725a43af1 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Sat, 4 Jul 2026 03:15:12 +0200 Subject: [PATCH 3/4] lint --- ecosystem/pypi.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ecosystem/pypi.py b/ecosystem/pypi.py index 1addf7d370..5b848b70d6 100644 --- a/ecosystem/pypi.py +++ b/ecosystem/pypi.py @@ -30,7 +30,7 @@ from .request import request_json -class PyPIData(JsonSerializable): +class PyPIData(JsonSerializable): # pylint: disable=too-many-public-methods """ The PyPI data related to a project """ @@ -116,12 +116,14 @@ def update_json(self): self._pypistats_json = self.request_pypistats() def request_pypi(self): + """Fetches https://pypi.org/pypi/{self.package_name}/json""" try: return request_json(f"pypi.org/pypi/{self.package_name}/json") except EcosystemError: return None def request_pypi_simple(self): + """Fetches https://pypi.org/simple/{self.package_name}/""" try: return request_json( f"https://pypi.org/simple/{self.package_name}/", From b3981e0b9b76f78ba637f37be7f0c9707da76804 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Sat, 4 Jul 2026 10:05:10 +0200 Subject: [PATCH 4/4] --- ecosystem/pypi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecosystem/pypi.py b/ecosystem/pypi.py index 5b848b70d6..9a9dc29d90 100644 --- a/ecosystem/pypi.py +++ b/ecosystem/pypi.py @@ -30,7 +30,7 @@ from .request import request_json -class PyPIData(JsonSerializable): # pylint: disable=too-many-public-methods +class PyPIData(JsonSerializable): # pylint: disable=too-many-public-methods """ The PyPI data related to a project """