From b17b7fdad58f31538c3b362c54a769075e43690e Mon Sep 17 00:00:00 2001 From: Prakhar54-byte Date: Mon, 6 Jul 2026 02:06:01 +0530 Subject: [PATCH 1/8] fix(download): use numeric sort key for artifact version URLs version_urls.sort(reverse=True) performs a plain lexicographic sort, which returns the wrong 'latest' version for semver-style IDs: ['2.10.0', '2.9.0'] -> lexicographic latest is '2.9.0' (wrong) Add _parse_version_key() which splits the trailing URL segment on non-digit characters and compares each part as an integer, giving correct numeric ordering with no new dependencies (re is stdlib). Date-style versions (2022.12.01) continue to work correctly. Closes # --- databusclient/api/download.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/databusclient/api/download.py b/databusclient/api/download.py index 312af45..de4744c 100644 --- a/databusclient/api/download.py +++ b/databusclient/api/download.py @@ -844,6 +844,18 @@ def _download_artifact( ) +def _parse_version_key(url: str) -> tuple: + """Return a numeric sort key derived from the version segment of a Databus URL. + + Splits the trailing version segment by non-digit characters and compares each + part as an integer, so '2.10.0' correctly sorts after '2.9.0' (unlike plain + lexicographic sort where '2.9' > '2.10' as strings). + """ + segment = url.rstrip("/").split("/")[-1] + parts = re.split(r"[^0-9]+", segment) + return tuple(int(p) for p in parts if p.isdigit()) + + def _get_databus_versions_of_artifact( json_str: str, all_versions: bool ) -> str | List[str]: @@ -875,7 +887,7 @@ def _get_databus_versions_of_artifact( if not version_urls: raise ValueError("No versions found in artifact JSON-LD") - version_urls.sort(reverse=True) # Sort versions in descending order + version_urls.sort(key=_parse_version_key, reverse=True) if all_versions: return version_urls From b3b9dc142ec35f95ab6a2b2fe754777e202fc69e Mon Sep 17 00:00:00 2001 From: Prakhar54-byte Date: Sat, 19 Sep 2026 00:19:47 +0530 Subject: [PATCH 2/8] as per request updated the test_download file for the _parse_version_key function --- tests/test_download.py | 49 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/tests/test_download.py b/tests/test_download.py index 94d8813..9c66845 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -1,8 +1,14 @@ """Download Tests""" +import json import pytest -from databusclient.api.download import download as api_download +from databusclient.api.download import ( + download as api_download, + _parse_version_key, + _get_databus_versions_of_artifact, +) + # TODO: overall test structure not great, needs refactoring @@ -35,3 +41,44 @@ def test_with_query(): ) def test_with_collection(): api_download("tmp", DEFAULT_ENDPOINT, [TEST_COLLECTION]) + + +@pytest.mark.parametrize( + "versions, expected_order", + [ + # ISO dates + ( + ["2026-09-17", "2026-09-18", "2025-12-31"], + ["2026-09-18", "2026-09-17", "2025-12-31"], + ), + # CalVer + ( + ["2026.09.17", "2026.10.01", "2026.09.05"], + ["2026.10.01", "2026.09.17", "2026.09.05"], + ), + # Pure numeric + ( + ["9", "10", "1"], + ["10", "9", "1"], + ), + # Dotted numeric + ( + ["2.9", "2.10", "2.1"], + ["2.10", "2.9", "2.1"], + ), + # Stable SemVer + ( + ["2.9.0", "2.10.0", "2.1.0"], + ["2.10.0", "2.9.0", "2.1.0"], + ), + # v-prefixed numeric + ( + ["v2.9.0", "v2.10.0", "v2.1.0"], + ["v2.10.0", "v2.9.0", "v2.1.0"], + ), + ], +) +def test_parse_version_key_sorting(versions, expected_order): + assert sorted(versions, key=_parse_version_key, reverse=True) == expected_order + + From ad7d1c5bb8a82f6b1ff8117219df42f223cec12d Mon Sep 17 00:00:00 2001 From: Prakhar54-byte Date: Sat, 19 Sep 2026 00:59:52 +0530 Subject: [PATCH 3/8] code rabbbit suggestion included --- tests/test_download.py | 63 ++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/tests/test_download.py b/tests/test_download.py index 8084566..b686ba6 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -75,43 +75,40 @@ def test_with_collection(): api_download("tmp", DEFAULT_ENDPOINT, [TEST_COLLECTION]) +# ponytail: minimal abstract version sort test across all supported formats @pytest.mark.parametrize( - "versions, expected_order", + "versions, expected", [ - # ISO dates - ( - ["2026-09-17", "2026-09-18", "2025-12-31"], - ["2026-09-18", "2026-09-17", "2025-12-31"], - ), - # CalVer - ( - ["2026.09.17", "2026.10.01", "2026.09.05"], - ["2026.10.01", "2026.09.17", "2026.09.05"], - ), - # Pure numeric - ( - ["9", "10", "1"], - ["10", "9", "1"], - ), - # Dotted numeric - ( - ["2.9", "2.10", "2.1"], - ["2.10", "2.9", "2.1"], - ), - # Stable SemVer - ( - ["2.9.0", "2.10.0", "2.1.0"], - ["2.10.0", "2.9.0", "2.1.0"], - ), - # v-prefixed numeric - ( - ["v2.9.0", "v2.10.0", "v2.1.0"], - ["v2.10.0", "v2.9.0", "v2.1.0"], - ), + (["2026-09-17", "2026-09-18", "2025-12-31"], ["2026-09-18", "2026-09-17", "2025-12-31"]), # ISO date + (["2026.09.17", "2026.10.01", "2026.09.05"], ["2026.10.01", "2026.09.17", "2026.09.05"]), # CalVer + (["9", "10", "1"], ["10", "9", "1"]), # Numeric + (["2.9", "2.10", "2.1"], ["2.10", "2.9", "2.1"]), # Dotted numeric + (["2.9.0", "2.10.0", "2.1.0"], ["2.10.0", "2.9.0", "2.1.0"]), # SemVer + (["v2.9.0", "v2.10.0", "v2.1.0"], ["v2.10.0", "v2.9.0", "v2.1.0"]), # v-prefix ], ) -def test_parse_version_key_sorting(versions, expected_order): - assert sorted(versions, key=_parse_version_key, reverse=True) == expected_order +def test_parse_version_key_sorting(versions, expected): + assert sorted(versions, key=_parse_version_key, reverse=True) == expected + + +def test_get_databus_versions_sorts_stable_before_matching_prerelease(): + stable_url = "https://databus.example.org/account/group/artifact/2.10.0" + prerelease_url = f"{stable_url}-rc.1" + artifact = json.dumps( + { + "databus:hasVersion": [ + {"@id": prerelease_url}, + {"@id": stable_url}, + ] + } + ) + + assert _get_databus_versions_of_artifact(artifact, all_versions=True) == [ + stable_url, + prerelease_url, + ] + assert _get_databus_versions_of_artifact(artifact, all_versions=False) == stable_url + def test_404_records_failed_manifest_entry(monkeypatch): From 5b0a5ca84b3b9e629e5af23622002327418094b2 Mon Sep 17 00:00:00 2001 From: Prakhar54-byte Date: Sun, 27 Sep 2026 23:32:21 +0530 Subject: [PATCH 4/8] lint issue solved --- tests/test_download.py | 52 +++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/tests/test_download.py b/tests/test_download.py index b686ba6..9007091 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -1,18 +1,17 @@ """Download Tests""" -import json import bz2 +import json import pytest from click.testing import CliRunner - from databusclient.api.download import ( - download as api_download, - _parse_version_key, _get_databus_versions_of_artifact, + _parse_version_key, +) +from databusclient.api.download import ( + download as api_download, ) - -from databusclient.api.download import download as api_download from databusclient.cli import app # TODO: overall test structure not great, needs refactoring @@ -36,7 +35,10 @@ class FakeHeadResp: status_code = 200 - headers = {} + + # headers = {} + def __init__(self): + self.headers = {} class FakeGetResp: @@ -79,12 +81,18 @@ def test_with_collection(): @pytest.mark.parametrize( "versions, expected", [ - (["2026-09-17", "2026-09-18", "2025-12-31"], ["2026-09-18", "2026-09-17", "2025-12-31"]), # ISO date - (["2026.09.17", "2026.10.01", "2026.09.05"], ["2026.10.01", "2026.09.17", "2026.09.05"]), # CalVer - (["9", "10", "1"], ["10", "9", "1"]), # Numeric - (["2.9", "2.10", "2.1"], ["2.10", "2.9", "2.1"]), # Dotted numeric - (["2.9.0", "2.10.0", "2.1.0"], ["2.10.0", "2.9.0", "2.1.0"]), # SemVer - (["v2.9.0", "v2.10.0", "v2.1.0"], ["v2.10.0", "v2.9.0", "v2.1.0"]), # v-prefix + ( + ["2026-09-17", "2026-09-18", "2025-12-31"], + ["2026-09-18", "2026-09-17", "2025-12-31"], + ), # ISO date + ( + ["2026.09.17", "2026.10.01", "2026.09.05"], + ["2026.10.01", "2026.09.17", "2026.09.05"], + ), # CalVer + (["9", "10", "1"], ["10", "9", "1"]), # Numeric + (["2.9", "2.10", "2.1"], ["2.10", "2.9", "2.1"]), # Dotted numeric + (["2.9.0", "2.10.0", "2.1.0"], ["2.10.0", "2.9.0", "2.1.0"]), # SemVer + (["v2.9.0", "v2.10.0", "v2.1.0"], ["v2.10.0", "v2.9.0", "v2.1.0"]), # v-prefix ], ) def test_parse_version_key_sorting(versions, expected): @@ -110,24 +118,30 @@ def test_get_databus_versions_sorts_stable_before_matching_prerelease(): assert _get_databus_versions_of_artifact(artifact, all_versions=False) == stable_url - def test_404_records_failed_manifest_entry(monkeypatch): - from databusclient.manifest.context import ManifestContext import databusclient.api.download as dl + from databusclient.manifest.context import ManifestContext class FakeGetResp: status_code = 404 - headers = {"content-length": "0"} + # headers = {"content-length": "0"} + def __init__(self): + self.headers = {"content-length":"0"} def raise_for_status(self): import requests + raise requests.exceptions.HTTPError(response=self) monkeypatch.setattr("requests.head", lambda *a, **k: FakeHeadResp()) monkeypatch.setattr("requests.get", lambda *a, **k: FakeGetResp()) ctx = ManifestContext(command="download") - dl._download_file("https://databus.dbpedia.org/account/notexisting", localDir=".", manifest_context=ctx) + dl._download_file( + "https://databus.dbpedia.org/account/notexisting", + localDir=".", + manifest_context=ctx, + ) assert len(ctx.files) == 1 assert ctx.files[0]["status"] == "failed" @@ -180,7 +194,9 @@ def test_failed_download_does_not_create_graph_sidecar(monkeypatch, tmp_path): class FakeGet404Resp: status_code = 404 - headers = {"content-length": "0"} + # headers = {"content-length": "0"} + def __init__(self): + self.headers = {"content-length": "0"} def raise_for_status(self): import requests From ebf9bb8ae536f5b78f923ee0817f1e49036b1c6e Mon Sep 17 00:00:00 2001 From: Prakhar54-byte Date: Sun, 27 Sep 2026 23:46:15 +0530 Subject: [PATCH 5/8] ci issue resolved --- tests/test_download.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_download.py b/tests/test_download.py index 9007091..9b24208 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -124,9 +124,10 @@ def test_404_records_failed_manifest_entry(monkeypatch): class FakeGetResp: status_code = 404 + # headers = {"content-length": "0"} def __init__(self): - self.headers = {"content-length":"0"} + self.headers = {"content-length": "0"} def raise_for_status(self): import requests @@ -194,6 +195,7 @@ def test_failed_download_does_not_create_graph_sidecar(monkeypatch, tmp_path): class FakeGet404Resp: status_code = 404 + # headers = {"content-length": "0"} def __init__(self): self.headers = {"content-length": "0"} From 5358616e817994d7e02baa48d98a8baac94498b0 Mon Sep 17 00:00:00 2001 From: Prakhar54-byte Date: Mon, 28 Sep 2026 00:44:32 +0530 Subject: [PATCH 6/8] Fix version sorting --- databusclient/api/download.py | 94 ++++++++++++++++++++--------------- tests/test_download.py | 90 +++++++++++++++++++++++++++++---- 2 files changed, 134 insertions(+), 50 deletions(-) diff --git a/databusclient/api/download.py b/databusclient/api/download.py index 4b8b65c..bd9233f 100644 --- a/databusclient/api/download.py +++ b/databusclient/api/download.py @@ -1,31 +1,30 @@ -import json -import os import bz2 import gzip +import json import lzma -from typing import List, Optional, Tuple +import os import re import shutil import tempfile +from datetime import UTC, datetime from urllib.parse import urlparse import requests from SPARQLWrapper import JSON, SPARQLWrapper from tqdm import tqdm -from datetime import datetime, timezone from databusclient.api.utils import ( + compute_sha256_and_length, fetch_databus_jsonld, get_databus_id_parts_from_file_url, - compute_sha256_and_length, ) from databusclient.filehandling.format import ( + FORMAT_TO_EXTENSION, convert_file, + detect_format_from_filename, get_converted_filename, - normalize_format, get_format_class, - detect_format_from_filename, - FORMAT_TO_EXTENSION, + normalize_format, ) # Compression format mappings @@ -94,7 +93,7 @@ def _collect_files(directory: str) -> list[str]: ) -def _detect_compression_format(filename: str) -> Optional[str]: +def _detect_compression_format(filename: str) -> str | None: """Detect compression format from file extension. Args: @@ -111,8 +110,8 @@ def _detect_compression_format(filename: str) -> Optional[str]: def _should_convert_compression( - filename: str, compression: Optional[str] -) -> Tuple[bool, Optional[str]]: + filename: str, compression: str | None +) -> tuple[bool, str | None]: """Determine if a file should have its compression format converted or compressed. Source compression is detected automatically from the file extension. @@ -213,7 +212,9 @@ def _convert_compression_format( # Decompression-only path: target_format == 'none' if target_format.lower() == "none": - print(f"Decompressing {os.path.basename(source_file)} -> {os.path.basename(target_file)}") + print( + f"Decompressing {os.path.basename(source_file)} -> {os.path.basename(target_file)}" + ) try: with source_module.open(source_file, "rb") as sf: with open(target_file, "wb") as tf: @@ -371,7 +372,7 @@ def _extract_checksums_from_jsonld(json_str: str) -> dict: return checksums -def _resolve_checksums_for_urls(file_urls: List[str], databus_key: str | None) -> dict: +def _resolve_checksums_for_urls(file_urls: list[str], databus_key: str | None) -> dict: """ Group file URLs by their Version URI, fetch each Version JSON-LD once, and return a combined url->checksum mapping for the provided URLs. @@ -381,7 +382,9 @@ def _resolve_checksums_for_urls(file_urls: List[str], databus_key: str | None) - versions_map: dict = {} for file_url in file_urls: try: - host, accountId, groupId, artifactId, versionId, fileId = get_databus_id_parts_from_file_url(file_url) + host, accountId, groupId, artifactId, versionId, fileId = ( + get_databus_id_parts_from_file_url(file_url) + ) except Exception: continue if versionId is None: @@ -586,14 +589,14 @@ def _download_file( # --- 5. Verify download size --- if total_size_in_bytes != 0 and progress_bar.n != total_size_in_bytes: - raise IOError("Downloaded size does not match Content-Length header") + raise OSError("Downloaded size does not match Content-Length header") # --- 6. Validate checksum on original downloaded file (BEFORE conversion) --- actual_checksum = None if validate_checksum: try: actual_checksum, _ = compute_sha256_and_length(filename) - except (OSError, IOError) as e: + except OSError as e: print(f"WARNING: error computing checksum for {filename}: {e}") actual_checksum = None @@ -611,7 +614,7 @@ def _download_file( os.remove(filename) except OSError: pass - raise IOError( + raise OSError( f"Checksum mismatch for {filename}: expected {expected_checksum}, got {actual_checksum}" ) @@ -630,7 +633,7 @@ def _download_file( status="success", sha256=actual_checksum or expected_checksum, size_bytes=total_size_in_bytes if total_size_in_bytes else None, - downloaded_at=datetime.now(timezone.utc).isoformat(), + downloaded_at=datetime.now(UTC).isoformat(), ) return @@ -656,7 +659,9 @@ def _download_file( # Decompress — strip compression extension, save plain file. target_filename = _get_converted_filename(file, source_fmt, "none") target_filepath = os.path.join(localDir, target_filename) - _convert_compression_format(filename, target_filepath, source_fmt, "none") + _convert_compression_format( + filename, target_filepath, source_fmt, "none" + ) else: target_filename = _get_converted_filename(file, source_fmt, compression) target_filepath = os.path.join(localDir, target_filename) @@ -673,7 +678,7 @@ def _download_file( status="success", sha256=actual_checksum or expected_checksum, size_bytes=total_size_in_bytes if total_size_in_bytes else None, - downloaded_at=datetime.now(timezone.utc).isoformat(), + downloaded_at=datetime.now(UTC).isoformat(), ) return @@ -690,7 +695,9 @@ def _download_file( file, source_fmt, compression ) target_filepath = os.path.join(localDir, target_filename) - _convert_compression_format(filename, target_filepath, source_fmt, compression) + _convert_compression_format( + filename, target_filepath, source_fmt, compression + ) final_paths = [target_filepath] else: final_paths = [filename] @@ -702,7 +709,7 @@ def _download_file( status="success", sha256=actual_checksum or expected_checksum, size_bytes=total_size_in_bytes if total_size_in_bytes else None, - downloaded_at=datetime.now(timezone.utc).isoformat(), + downloaded_at=datetime.now(UTC).isoformat(), ) return @@ -741,9 +748,12 @@ def _download_file( source_format_for_mapping = detect_format_from_filename(conversion_input_path) source_class_for_mapping = ( get_format_class(source_format_for_mapping) - if source_format_for_mapping else None + if source_format_for_mapping + else None + ) + is_quad_to_triple = ( + source_class_for_mapping == "quads" and target_class == "triples" ) - is_quad_to_triple = (source_class_for_mapping == "quads" and target_class == "triples") if is_quad_to_triple: # Output directory name = original filename with compression and @@ -775,7 +785,7 @@ def _download_file( status="success", sha256=actual_checksum or expected_checksum, size_bytes=total_size_in_bytes if total_size_in_bytes else None, - downloaded_at=datetime.now(timezone.utc).isoformat(), + downloaded_at=datetime.now(UTC).isoformat(), ) return @@ -807,7 +817,9 @@ def _download_file( if source_compression is not None: if should_convert_compression and compression: # 'none' means no recompression after format conversion - final_compression = None if compression.lower() == "none" else compression + final_compression = ( + None if compression.lower() == "none" else compression + ) else: final_compression = source_compression elif compression and compression.lower() != "none": @@ -847,12 +859,12 @@ def _download_file( status="success", sha256=actual_checksum or expected_checksum, size_bytes=total_size_in_bytes if total_size_in_bytes else None, - downloaded_at=datetime.now(timezone.utc).isoformat(), + downloaded_at=datetime.now(UTC).isoformat(), ) def _download_files( - urls: List[str], + urls: list[str], localDir: str, vault_token_file: str = None, databus_key: str = None, @@ -948,7 +960,7 @@ def _query_sparql_endpoint(endpoint_url, query, databus_key=None) -> dict: def _get_file_download_urls_from_sparql_query( endpoint_url, query, databus_key=None -) -> List[str]: +) -> list[str]: """Execute a SPARQL query to get databus file download URLs. Args: @@ -965,7 +977,7 @@ def _get_file_download_urls_from_sparql_query( if not isinstance(bindings, list): raise ValueError("Invalid SPARQL response: 'bindings' missing or not a list") - urls: List[str] = [] + urls: list[str] = [] for binding in bindings: if not isinstance(binding, dict) or len(binding) != 1: @@ -1232,13 +1244,15 @@ def _parse_version_key(url: str) -> tuple: lexicographic sort where '2.9' > '2.10' as strings). """ segment = url.rstrip("/").split("/")[-1] - parts = re.split(r"[^0-9]+", segment) - return tuple(int(p) for p in parts if p.isdigit()) + prerelease = re.search(r"-(?=[A-Za-z])", segment) + numeric_segment = segment[: prerelease.start()] if prerelease else segment + parts = re.split(r"[^0-9]+", numeric_segment) + return (tuple(int(p) for p in parts if p.isdigit()), not bool(prerelease)) def _get_databus_versions_of_artifact( json_str: str, all_versions: bool -) -> str | List[str]: +) -> str | list[str]: """Parse the JSON-LD of a databus artifact to extract URLs of its versions. Args: @@ -1274,7 +1288,7 @@ def _get_databus_versions_of_artifact( return version_urls[0] -def _get_file_download_urls_from_artifact_jsonld(json_str: str) -> List[str]: +def _get_file_download_urls_from_artifact_jsonld(json_str: str) -> list[str]: """Parse the JSON-LD of a databus artifact version to extract download URLs. Args: @@ -1284,7 +1298,7 @@ def _get_file_download_urls_from_artifact_jsonld(json_str: str) -> List[str]: List of all file download URLs in the artifact version. """ - databusIdUrl: List[str] = [] + databusIdUrl: list[str] = [] json_dict = json.loads(json_str) graph = json_dict.get("@graph", []) @@ -1352,7 +1366,7 @@ def _download_group( ) -def _get_databus_artifacts_of_group(json_str: str) -> List[str]: +def _get_databus_artifacts_of_group(json_str: str) -> list[str]: """ Parse the JSON-LD of a databus group to extract URLs of all artifacts. @@ -1373,7 +1387,7 @@ def _get_databus_artifacts_of_group(json_str: str) -> List[str]: f"Unexpected type for 'databus:hasArtifact': {type(artifacts).__name__}" ) - result: List[str] = [] + result: list[str] = [] for item in artifacts_iter: if not isinstance(item, dict): continue @@ -1389,7 +1403,7 @@ def _get_databus_artifacts_of_group(json_str: str) -> List[str]: def download( localDir: str, endpoint: str, - databusURIs: List[str], + databusURIs: list[str], token=None, databus_key=None, all_versions=None, @@ -1425,7 +1439,9 @@ def download( """ _validate_graph_mode(graph_mode) for databusURI in databusURIs: - host, account, group, artifact, version, file = get_databus_id_parts_from_file_url(databusURI) + host, account, group, artifact, version, file = ( + get_databus_id_parts_from_file_url(databusURI) + ) # Determine endpoint per-URI if not explicitly provided uri_endpoint = endpoint diff --git a/tests/test_download.py b/tests/test_download.py index 9b24208..34cbcd5 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -77,22 +77,90 @@ def test_with_collection(): api_download("tmp", DEFAULT_ENDPOINT, [TEST_COLLECTION]) -# ponytail: minimal abstract version sort test across all supported formats +# @pytest.mark.parametrize( +# "versions, expected", +# [ +# ( +# ["2026-09-17", "2026-09-18", "2025-12-31"], +# ["2026-09-18", "2026-09-17", "2025-12-31"], +# ), # ISO date +# ( +# ["2026.09.17", "2026.10.01", "2026.09.05"], +# ["2026.10.01", "2026.09.17", "2026.09.05"], +# ), # CalVer +# (["9", "10", "1"], ["10", "9", "1"]), # Numeric +# (["2.9", "2.10", "2.1"], ["2.10", "2.9", "2.1"]), # Dotted numeric +# (["2.9.0", "2.10.0", "2.1.0"], ["2.10.0", "2.9.0", "2.1.0"]), # SemVer +# (["v2.9.0", "v2.10.0", "v2.1.0"], ["v2.10.0", "v2.9.0", "v2.1.0"]), # v-prefix +# ], +# ) +# def test_parse_version_key_sorting(versions, expected): +# assert sorted(versions, key=_parse_version_key, reverse=True) == expected + + @pytest.mark.parametrize( "versions, expected", [ ( - ["2026-09-17", "2026-09-18", "2025-12-31"], - ["2026-09-18", "2026-09-17", "2025-12-31"], - ), # ISO date + [ + "https://databus.dbpedia.org/example/2026-09-17", + "https://databus.dbpedia.org/example/2026-09-18", + "https://databus.dbpedia.org/example/2025-12-31", + ], + [ + "https://databus.dbpedia.org/example/2026-09-18", + "https://databus.dbpedia.org/example/2026-09-17", + "https://databus.dbpedia.org/example/2025-12-31", + ], + ), + ( + [ + "https://databus.dbpedia.org/example/2026.09.17", + "https://databus.dbpedia.org/example/2026.10.01", + "https://databus.dbpedia.org/example/2026.09.05", + ], + [ + "https://databus.dbpedia.org/example/2026.10.01", + "https://databus.dbpedia.org/example/2026.09.17", + "https://databus.dbpedia.org/example/2026.09.05", + ], + ), + ( + [ + "https://databus.dbpedia.org/example/2.9", + "https://databus.dbpedia.org/example/2.10", + "https://databus.dbpedia.org/example/2.1", + ], + [ + "https://databus.dbpedia.org/example/2.10", + "https://databus.dbpedia.org/example/2.9", + "https://databus.dbpedia.org/example/2.1", + ], + ), + ( + [ + "https://databus.dbpedia.org/example/2.9.0", + "https://databus.dbpedia.org/example/2.10.0", + "https://databus.dbpedia.org/example/2.1.0", + ], + [ + "https://databus.dbpedia.org/example/2.10.0", + "https://databus.dbpedia.org/example/2.9.0", + "https://databus.dbpedia.org/example/2.1.0", + ], + ), ( - ["2026.09.17", "2026.10.01", "2026.09.05"], - ["2026.10.01", "2026.09.17", "2026.09.05"], - ), # CalVer - (["9", "10", "1"], ["10", "9", "1"]), # Numeric - (["2.9", "2.10", "2.1"], ["2.10", "2.9", "2.1"]), # Dotted numeric - (["2.9.0", "2.10.0", "2.1.0"], ["2.10.0", "2.9.0", "2.1.0"]), # SemVer - (["v2.9.0", "v2.10.0", "v2.1.0"], ["v2.10.0", "v2.9.0", "v2.1.0"]), # v-prefix + [ + "https://databus.dbpedia.org/example/v2.9.0", + "https://databus.dbpedia.org/example/v2.10.0", + "https://databus.dbpedia.org/example/v2.1.0", + ], + [ + "https://databus.dbpedia.org/example/v2.10.0", + "https://databus.dbpedia.org/example/v2.9.0", + "https://databus.dbpedia.org/example/v2.1.0", + ], + ), ], ) def test_parse_version_key_sorting(versions, expected): From b7956a515b11e545b43d64879756295d444eb0e5 Mon Sep 17 00:00:00 2001 From: Prakhar54-byte Date: Mon, 28 Sep 2026 01:00:41 +0530 Subject: [PATCH 7/8] Fix version sorting --- databusclient/api/download.py | 16 ++++++++++++++-- tests/test_download.py | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/databusclient/api/download.py b/databusclient/api/download.py index bd9233f..43c8fcd 100644 --- a/databusclient/api/download.py +++ b/databusclient/api/download.py @@ -1241,13 +1241,25 @@ def _parse_version_key(url: str) -> tuple: Splits the trailing version segment by non-digit characters and compares each part as an integer, so '2.10.0' correctly sorts after '2.9.0' (unlike plain - lexicographic sort where '2.9' > '2.10' as strings). + lexicographic sort where '2.9' > '2.10' as strings). Stable releases sort + ahead of prereleases, whose identifiers are compared individually. """ segment = url.rstrip("/").split("/")[-1] prerelease = re.search(r"-(?=[A-Za-z])", segment) numeric_segment = segment[: prerelease.start()] if prerelease else segment parts = re.split(r"[^0-9]+", numeric_segment) - return (tuple(int(p) for p in parts if p.isdigit()), not bool(prerelease)) + prerelease_key = () + if prerelease: + identifiers = re.split(r"[.-]", segment[prerelease.start() + 1 :]) + prerelease_key = tuple( + (1, int(identifier)) if identifier.isdigit() else (0, identifier.lower()) + for identifier in identifiers + ) + return ( + tuple(int(p) for p in parts if p.isdigit()), + not bool(prerelease), + prerelease_key, + ) def _get_databus_versions_of_artifact( diff --git a/tests/test_download.py b/tests/test_download.py index 34cbcd5..0243433 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -186,6 +186,28 @@ def test_get_databus_versions_sorts_stable_before_matching_prerelease(): assert _get_databus_versions_of_artifact(artifact, all_versions=False) == stable_url +def test_get_databus_versions_sorts_prerelease_identifiers_numerically(): + stable_url = "https://databus.example.org/account/group/artifact/2.10.0" + rc1_url = f"{stable_url}-rc.1" + rc2_url = f"{stable_url}-rc.2" + artifact = json.dumps( + { + "databus:hasVersion": [ + {"@id": rc1_url}, + {"@id": stable_url}, + {"@id": rc2_url}, + ] + } + ) + + assert _get_databus_versions_of_artifact(artifact, all_versions=True) == [ + stable_url, + rc2_url, + rc1_url, + ] + assert _get_databus_versions_of_artifact(artifact, all_versions=False) == stable_url + + def test_404_records_failed_manifest_entry(monkeypatch): import databusclient.api.download as dl from databusclient.manifest.context import ManifestContext From e610a64ec61788d4ebf91624bf88fa1430aae784 Mon Sep 17 00:00:00 2001 From: Prakhar54-byte Date: Tue, 29 Sep 2026 20:23:51 +0530 Subject: [PATCH 8/8] test: add version sorting tests --- databusclient/api/download.py | 20 ++-------- tests/test_download.py | 69 +---------------------------------- 2 files changed, 4 insertions(+), 85 deletions(-) diff --git a/databusclient/api/download.py b/databusclient/api/download.py index 43c8fcd..cb24c02 100644 --- a/databusclient/api/download.py +++ b/databusclient/api/download.py @@ -1241,25 +1241,11 @@ def _parse_version_key(url: str) -> tuple: Splits the trailing version segment by non-digit characters and compares each part as an integer, so '2.10.0' correctly sorts after '2.9.0' (unlike plain - lexicographic sort where '2.9' > '2.10' as strings). Stable releases sort - ahead of prereleases, whose identifiers are compared individually. + lexicographic sort where '2.9' > '2.10' as strings). """ segment = url.rstrip("/").split("/")[-1] - prerelease = re.search(r"-(?=[A-Za-z])", segment) - numeric_segment = segment[: prerelease.start()] if prerelease else segment - parts = re.split(r"[^0-9]+", numeric_segment) - prerelease_key = () - if prerelease: - identifiers = re.split(r"[.-]", segment[prerelease.start() + 1 :]) - prerelease_key = tuple( - (1, int(identifier)) if identifier.isdigit() else (0, identifier.lower()) - for identifier in identifiers - ) - return ( - tuple(int(p) for p in parts if p.isdigit()), - not bool(prerelease), - prerelease_key, - ) + parts = re.split(r"[^0-9]+", segment) + return tuple(int(part) for part in parts if part.isdigit()) def _get_databus_versions_of_artifact( diff --git a/tests/test_download.py b/tests/test_download.py index 0243433..fcfd16a 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -5,10 +5,7 @@ import pytest from click.testing import CliRunner -from databusclient.api.download import ( - _get_databus_versions_of_artifact, - _parse_version_key, -) +from databusclient.api.download import _parse_version_key from databusclient.api.download import ( download as api_download, ) @@ -77,27 +74,6 @@ def test_with_collection(): api_download("tmp", DEFAULT_ENDPOINT, [TEST_COLLECTION]) -# @pytest.mark.parametrize( -# "versions, expected", -# [ -# ( -# ["2026-09-17", "2026-09-18", "2025-12-31"], -# ["2026-09-18", "2026-09-17", "2025-12-31"], -# ), # ISO date -# ( -# ["2026.09.17", "2026.10.01", "2026.09.05"], -# ["2026.10.01", "2026.09.17", "2026.09.05"], -# ), # CalVer -# (["9", "10", "1"], ["10", "9", "1"]), # Numeric -# (["2.9", "2.10", "2.1"], ["2.10", "2.9", "2.1"]), # Dotted numeric -# (["2.9.0", "2.10.0", "2.1.0"], ["2.10.0", "2.9.0", "2.1.0"]), # SemVer -# (["v2.9.0", "v2.10.0", "v2.1.0"], ["v2.10.0", "v2.9.0", "v2.1.0"]), # v-prefix -# ], -# ) -# def test_parse_version_key_sorting(versions, expected): -# assert sorted(versions, key=_parse_version_key, reverse=True) == expected - - @pytest.mark.parametrize( "versions, expected", [ @@ -167,47 +143,6 @@ def test_parse_version_key_sorting(versions, expected): assert sorted(versions, key=_parse_version_key, reverse=True) == expected -def test_get_databus_versions_sorts_stable_before_matching_prerelease(): - stable_url = "https://databus.example.org/account/group/artifact/2.10.0" - prerelease_url = f"{stable_url}-rc.1" - artifact = json.dumps( - { - "databus:hasVersion": [ - {"@id": prerelease_url}, - {"@id": stable_url}, - ] - } - ) - - assert _get_databus_versions_of_artifact(artifact, all_versions=True) == [ - stable_url, - prerelease_url, - ] - assert _get_databus_versions_of_artifact(artifact, all_versions=False) == stable_url - - -def test_get_databus_versions_sorts_prerelease_identifiers_numerically(): - stable_url = "https://databus.example.org/account/group/artifact/2.10.0" - rc1_url = f"{stable_url}-rc.1" - rc2_url = f"{stable_url}-rc.2" - artifact = json.dumps( - { - "databus:hasVersion": [ - {"@id": rc1_url}, - {"@id": stable_url}, - {"@id": rc2_url}, - ] - } - ) - - assert _get_databus_versions_of_artifact(artifact, all_versions=True) == [ - stable_url, - rc2_url, - rc1_url, - ] - assert _get_databus_versions_of_artifact(artifact, all_versions=False) == stable_url - - def test_404_records_failed_manifest_entry(monkeypatch): import databusclient.api.download as dl from databusclient.manifest.context import ManifestContext @@ -319,8 +254,6 @@ def test_invalid_graph_mode_value_is_rejected_by_cli(): def test_cli_manifest_records_graph_mode(monkeypatch, tmp_path): - import json - captured = {} def fake_download(**kwargs):