diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1e92dc..3dd587d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.12" - name: Install dependencies @@ -34,7 +34,7 @@ jobs: python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -58,7 +58,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.12" - name: Install dependencies diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 37cf58e..975131e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v6 with: fetch-depth: 0 # Required for hatch-vcs to get version from tags - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.12" - name: Install build tools @@ -24,7 +24,7 @@ jobs: - name: Build package run: python -m build - name: Upload distributions - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: release-dists path: dist/ @@ -39,7 +39,7 @@ jobs: id-token: write # Required for trusted publishing steps: - name: Download distributions - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: release-dists path: dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 62dae3e..671a92b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.8.1] - 2026-06-01 + +### Changed + +- `Search.semantic()`, `Search.semantic_iter()`, `AsyncSearch.semantic()`, and + `AsyncSearch.semantic_iter()` now call the canonical path + `GET /v1/search/semantic` instead of `GET /v1/concepts/semantic-search`. The + legacy path remains a permanent server-side alias (emits `Deprecation: true` + + `Link: …rel="successor-version"` headers), so older installations of this + SDK continue to work - no breaking change for callers. + ## [1.8.0] - 2026-05-25 ### Added diff --git a/src/omophub/resources/search.py b/src/omophub/resources/search.py index e63eefb..4577281 100644 --- a/src/omophub/resources/search.py +++ b/src/omophub/resources/search.py @@ -322,7 +322,7 @@ def semantic( if threshold is not None: params["threshold"] = threshold - return self._request.get("/concepts/semantic-search", params=params) + return self._request.get("/search/semantic", params=params) def semantic_iter( self, @@ -369,7 +369,7 @@ def fetch_page( if threshold is not None: params["threshold"] = threshold - result = self._request.get_raw("/concepts/semantic-search", params=params) + result = self._request.get_raw("/search/semantic", params=params) data = result.get("data", []) results = data.get("results", data) if isinstance(data, dict) else data @@ -656,7 +656,7 @@ async def semantic( if threshold is not None: params["threshold"] = threshold - return await self._request.get("/concepts/semantic-search", params=params) + return await self._request.get("/search/semantic", params=params) async def semantic_iter( self, @@ -690,9 +690,7 @@ async def fetch_page( if threshold is not None: params["threshold"] = threshold - result = await self._request.get_raw( - "/concepts/semantic-search", params=params - ) + result = await self._request.get_raw("/search/semantic", params=params) data = result.get("data", []) results = data.get("results", data) if isinstance(data, dict) else data diff --git a/tests/unit/resources/test_search.py b/tests/unit/resources/test_search.py index 2b8348f..01719b5 100644 --- a/tests/unit/resources/test_search.py +++ b/tests/unit/resources/test_search.py @@ -299,7 +299,7 @@ def test_semantic_search(self, sync_client: OMOPHub, base_url: str) -> None: }, "meta": {"pagination": {"page": 1, "has_next": False, "total_items": 1}}, } - route = respx.get(f"{base_url}/concepts/semantic-search").mock( + route = respx.get(f"{base_url}/search/semantic").mock( return_value=Response(200, json=semantic_response) ) @@ -316,7 +316,7 @@ def test_semantic_search_with_filters( self, sync_client: OMOPHub, base_url: str ) -> None: """Test semantic search with all filters.""" - route = respx.get(f"{base_url}/concepts/semantic-search").mock( + route = respx.get(f"{base_url}/search/semantic").mock( return_value=Response(200, json={"success": True, "data": {"results": []}}) ) @@ -353,7 +353,7 @@ def test_semantic_iter_single_page( ], "meta": {"pagination": {"page": 1, "has_next": False}}, } - respx.get(f"{base_url}/concepts/semantic-search").mock( + respx.get(f"{base_url}/search/semantic").mock( return_value=Response(200, json=semantic_response) ) @@ -385,7 +385,7 @@ def mock_response(request): return Response(200, json=page1_response) return Response(200, json=page2_response) - respx.get(f"{base_url}/concepts/semantic-search").mock(side_effect=mock_response) + respx.get(f"{base_url}/search/semantic").mock(side_effect=mock_response) results = list(sync_client.search.semantic_iter("diabetes", page_size=1)) assert len(results) == 2 @@ -402,7 +402,7 @@ def test_semantic_iter_empty_response( "data": [], "meta": {"pagination": {"page": 1, "has_next": False}}, } - respx.get(f"{base_url}/concepts/semantic-search").mock( + respx.get(f"{base_url}/search/semantic").mock( return_value=Response(200, json=semantic_response) ) @@ -555,7 +555,7 @@ async def test_async_semantic_search( "results": [{"concept_id": 4329847, "similarity_score": 0.95}], }, } - respx.get(f"{base_url}/concepts/semantic-search").mock( + respx.get(f"{base_url}/search/semantic").mock( return_value=Response(200, json=semantic_response) ) @@ -568,7 +568,7 @@ async def test_async_semantic_with_filters( self, async_client: omophub.AsyncOMOPHub, base_url: str ) -> None: """Test async semantic search with filters.""" - route = respx.get(f"{base_url}/concepts/semantic-search").mock( + route = respx.get(f"{base_url}/search/semantic").mock( return_value=Response(200, json={"success": True, "data": {"results": []}}) ) @@ -591,7 +591,7 @@ async def test_async_semantic_with_all_filters( self, async_client: omophub.AsyncOMOPHub, base_url: str ) -> None: """Test async semantic search with all available filters.""" - route = respx.get(f"{base_url}/concepts/semantic-search").mock( + route = respx.get(f"{base_url}/search/semantic").mock( return_value=Response(200, json={"success": True, "data": {"results": []}}) ) @@ -629,7 +629,7 @@ async def test_async_semantic_iter_single_page( ], "meta": {"pagination": {"page": 1, "has_next": False}}, } - respx.get(f"{base_url}/concepts/semantic-search").mock( + respx.get(f"{base_url}/search/semantic").mock( return_value=Response(200, json=semantic_response) ) @@ -674,7 +674,7 @@ def mock_response(request): return Response(200, json=page2_response) return Response(200, json=page3_response) - respx.get(f"{base_url}/concepts/semantic-search").mock(side_effect=mock_response) + respx.get(f"{base_url}/search/semantic").mock(side_effect=mock_response) results = [] async for item in async_client.search.semantic_iter("diabetes", page_size=1): @@ -696,7 +696,7 @@ async def test_async_semantic_iter_with_filters( "data": [{"concept_id": 1, "similarity_score": 0.9}], "meta": {"pagination": {"page": 1, "has_next": False}}, } - route = respx.get(f"{base_url}/concepts/semantic-search").mock( + route = respx.get(f"{base_url}/search/semantic").mock( return_value=Response(200, json=semantic_response) ) @@ -732,7 +732,7 @@ async def test_async_semantic_iter_empty_response( "data": [], "meta": {"pagination": {"page": 1, "has_next": False}}, } - respx.get(f"{base_url}/concepts/semantic-search").mock( + respx.get(f"{base_url}/search/semantic").mock( return_value=Response(200, json=semantic_response) )