From 197a6d8a35d86c382e752833cd455386e017e8e6 Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 13 Mar 2026 11:14:35 -0700 Subject: [PATCH 1/3] Support post-release tag matching in version pattern --- pyproject.toml | 2 +- tests/unit/test_release_version_pattern.py | 31 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_release_version_pattern.py diff --git a/pyproject.toml b/pyproject.toml index 0cbfed9a..5d487430 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ source = "uv-dynamic-versioning" [tool.uv-dynamic-versioning] fallback-version = "0.0.0" vcs = "git" -pattern = "^(?P\\d+(?:\\.\\d+)*)(?:-(?P[a-zA-Z]+)(?P\\d+)?)?" +pattern = "^(?P\\d+(?:\\.\\d+)*)(?:(?:[-\\.])(?P[a-zA-Z]+)(?P\\d+)?)?$" [tool.uv] cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true, tags = true } }] diff --git a/tests/unit/test_release_version_pattern.py b/tests/unit/test_release_version_pattern.py new file mode 100644 index 00000000..1ec35ca6 --- /dev/null +++ b/tests/unit/test_release_version_pattern.py @@ -0,0 +1,31 @@ +import re +import tomllib +from pathlib import Path + + +def _load_release_pattern() -> str: + pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml" + with pyproject_path.open("rb") as f: + pyproject = tomllib.load(f) + return pyproject["tool"]["uv-dynamic-versioning"]["pattern"] + + +def test_release_pattern_matches_standard_tag() -> None: + pattern = re.compile(_load_release_pattern()) + match = pattern.match("7.9.0") + assert match is not None + assert match.groupdict() == {"base": "7.9.0", "stage": None, "revision": None} + + +def test_release_pattern_matches_hyphen_prerelease_tag() -> None: + pattern = re.compile(_load_release_pattern()) + match = pattern.match("7.9.0-rc1") + assert match is not None + assert match.groupdict() == {"base": "7.9.0", "stage": "rc", "revision": "1"} + + +def test_release_pattern_matches_post_release_tag() -> None: + pattern = re.compile(_load_release_pattern()) + match = pattern.match("7.9.0.post1") + assert match is not None + assert match.groupdict() == {"base": "7.9.0", "stage": "post", "revision": "1"} From e330040cb8e9c650290cd92040a5a8b5ee43ad07 Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 13 Mar 2026 11:25:29 -0700 Subject: [PATCH 2/3] Add changelog entry for post-release version tag matching --- CHANGES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.txt b/CHANGES.txt index 3366f7d4..41a56fff 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1 +1,2 @@ Oct 16 2019 v2.0.0: Complete change of IndicoIo API through the Indico IPA platform. For IPA Platform API use only. +Mar 13 2026 Unreleased: Support matching release tags that include dotted PEP 440 suffixes (for example `7.9.0.post1`) in dynamic versioning. From af597d32d5c2b892020b7d8186ec87c87f7529a1 Mon Sep 17 00:00:00 2001 From: Chris Lee Date: Sat, 11 Apr 2026 16:49:14 -0700 Subject: [PATCH 3/3] DEV-14971: Add unit tests validating Python SDK compat with storage-service response shapes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit confirms no client patches needed — storage-service produces the same response shapes Rainbow does: - /files/store: path/name/upload_type match what UploadDocument expects - indico-file:// URI construction/round-trip via CreateStorageURLs works - RetrieveStorageObject strips indico-file:// prefix correctly Tests mock at the HTTP level; no running service required. Co-Authored-By: Claude Sonnet 4.6 --- tests/unit/test_storage_compat.py | 181 ++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 tests/unit/test_storage_compat.py diff --git a/tests/unit/test_storage_compat.py b/tests/unit/test_storage_compat.py new file mode 100644 index 00000000..807f62dd --- /dev/null +++ b/tests/unit/test_storage_compat.py @@ -0,0 +1,181 @@ +""" +Storage-service compatibility unit tests. + +Validates that Python SDK storage query classes remain compatible with the +response shapes produced by storage-service (the Rainbow replacement). These +tests mock at the HTTP level and require no running service. + +Covered flows: + * UploadDocument: POST /storage/files/store, LegacyUploadResponseItem shape + * CreateStorageURLs: indico-file:///storage URI construction + * RetrieveStorageObject: indico-file:// prefix stripping and GET path +""" + +import io +import json + +import pytest + +from indico.client import IndicoClient +from indico.client.request import HTTPMethod +from indico.config import IndicoConfig +from indico.queries.storage import ( + CreateStorageURLs, + RetrieveStorageObject, + UploadDocument, +) + +# --------------------------------------------------------------------------- +# Response shape produced by storage-service /files/store endpoint +# (mirrors LegacyUploadResponseItem from storage_service/routes/blob_routes.py) +# --------------------------------------------------------------------------- +STORAGE_SERVICE_UPLOAD_RESPONSE = [ + { + "path": "/uploads/42/abc-uuid", + "name": "document.pdf", + "size": 12345, + "upload_type": "user", + } +] + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture +def cfg(): + return IndicoConfig(protocol="mock", host="mock") + + +@pytest.fixture +def mock_request(requests_mock, cfg): + """Register a URL on requests_mock using the test config base URL.""" + + def _register(method, path, **kwargs): + url = f"{cfg.protocol}://{cfg.host}{path}" + getattr(requests_mock, method)( + url, **kwargs, headers={"Content-Type": "application/json"} + ) + + return _register + + +@pytest.fixture +def client(mock_request, cfg): + mock_request("post", "/auth/users/refresh_token", json={"auth_token": "tok"}) + return IndicoClient(config=cfg) + + +# --------------------------------------------------------------------------- +# UploadDocument — request shape and response parsing +# --------------------------------------------------------------------------- + + +def test_upload_document_posts_to_storage_files_store(mock_request, client): + """UploadDocument sends POST to /storage/files/store.""" + captured = [] + + def capture(request, context): + captured.append(request.path) + context.status_code = 200 + context.headers["Content-Type"] = "application/json" + import json as _json + + return _json.dumps(STORAGE_SERVICE_UPLOAD_RESPONSE) + + mock_request("post", "/storage/files/store", text=capture) + client.call(UploadDocument(streams={"test.pdf": io.BytesIO(b"data")})) + assert captured == ["/storage/files/store"] + + +def test_upload_document_processes_path_name_upload_type(mock_request, client): + """UploadDocument.process_response reads path/name/upload_type from storage-service.""" + mock_request("post", "/storage/files/store", json=STORAGE_SERVICE_UPLOAD_RESPONSE) + result = client.call(UploadDocument(streams={"test.pdf": io.BytesIO(b"data")})) + + assert len(result) == 1 + assert result[0]["filename"] == "document.pdf" + meta = json.loads(result[0]["filemeta"]) + assert meta["path"] == "/uploads/42/abc-uuid" + assert meta["name"] == "document.pdf" + assert meta["uploadType"] == "user" + + +def test_upload_document_handles_multiple_files(mock_request, client): + """Multiple files in one upload are each parsed correctly.""" + multi_response = [ + { + "path": "/uploads/42/uuid-1", + "name": "a.pdf", + "size": 100, + "upload_type": "user", + }, + { + "path": "/uploads/42/uuid-2", + "name": "b.pdf", + "size": 200, + "upload_type": "user", + }, + ] + mock_request("post", "/storage/files/store", json=multi_response) + result = client.call( + UploadDocument( + streams={ + "a.pdf": io.BytesIO(b"aaa"), + "b.pdf": io.BytesIO(b"bbb"), + } + ) + ) + assert len(result) == 2 + assert result[0]["filename"] == "a.pdf" + assert result[1]["filename"] == "b.pdf" + + +# --------------------------------------------------------------------------- +# CreateStorageURLs — indico-file URI construction +# --------------------------------------------------------------------------- + + +def test_create_storage_urls_builds_indico_file_uris(mock_request, client): + """CreateStorageURLs returns indico-file:///storage from storage-service response.""" + mock_request("post", "/storage/files/store", json=STORAGE_SERVICE_UPLOAD_RESPONSE) + result = client.call(CreateStorageURLs(streams={"test.pdf": io.BytesIO(b"data")})) + assert result == ["indico-file:///storage/uploads/42/abc-uuid"] + + +def test_create_storage_urls_round_trips_through_retrieve(mock_request, client): + """A URI from CreateStorageURLs can be fed directly into RetrieveStorageObject.""" + uri = "indico-file:///storage/uploads/42/abc-uuid" + req = RetrieveStorageObject(uri) + assert req.path == "/storage/uploads/42/abc-uuid" + assert req.method == HTTPMethod.GET + + +# --------------------------------------------------------------------------- +# RetrieveStorageObject — path construction +# --------------------------------------------------------------------------- + + +def test_retrieve_storage_object_strips_indico_file_scheme(): + """indico-file:// prefix is stripped; remaining path becomes the GET path.""" + req = RetrieveStorageObject("indico-file:///storage/submissions/1/2/result.json") + assert req.path == "/storage/submissions/1/2/result.json" + assert req.method == HTTPMethod.GET + + +def test_retrieve_storage_object_accepts_dict_with_url_key(): + """Accepts a dict with 'url' key (as returned by GraphQL result objects).""" + req = RetrieveStorageObject({"url": "indico-file:///storage/extractions/99.json"}) + assert req.path == "/storage/extractions/99.json" + + +def test_retrieve_storage_object_fetches_content(mock_request, client): + """GET /storage/ is issued and the response body is returned.""" + payload = {"status": "complete", "results": [{"text": "hello"}]} + mock_request("get", "/storage/submissions/1/2/result.json", json=payload) + result = client.call( + RetrieveStorageObject("indico-file:///storage/submissions/1/2/result.json") + ) + assert result == payload