Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: 2
updates:
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "weekly"
allow:
- dependency-type: "all"
groups:
python-minor-patch:
applies-to: version-updates
patterns: ["*"]
update-types: ["minor", "patch"]
python-security:
applies-to: security-updates
patterns: ["*"]
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@ on:
workflow_dispatch: # Allow manual triggering

jobs:
dependency-audit:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install audit tools
run: python -m pip install uv pip-audit

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The audit job installs uv and pip-audit from floating PyPI releases, so its security result and runner code change on every run. Pin tested tool versions and preferably their hashes in a checked-in requirements file.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .github/workflows/ci.yml, line 22:

<comment>The audit job installs `uv` and `pip-audit` from floating PyPI releases, so its security result and runner code change on every run. Pin tested tool versions and preferably their hashes in a checked-in requirements file.</comment>

<file context>
@@ -8,6 +8,27 @@ on:
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install audit tools
+        run: python -m pip install uv pip-audit
+      - name: Export locked dependencies including all extras
+        run: >-
</file context>

- name: Export locked dependencies including all extras
run: >-
uv export --locked --all-extras --no-emit-project
--output-file "$RUNNER_TEMP/requirements-audit.txt"
- name: Audit locked dependencies
run: >-
pip-audit --require-hashes --disable-pip --progress-spinner off
--requirement "$RUNNER_TEMP/requirements-audit.txt"

lint:
runs-on: ubuntu-latest
steps:
Expand Down
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ 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.10.0] - 2026-09-08

### Changed

- Autocomplete now calls the canonical `/search/autocomplete` endpoint and

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This release adds anyio and certifi as required runtime dependencies and raises the fhirpy extra's floors, but the 1.10.0 entry does not mention any of it. Add a dependency note, as the 1.7.1 entry does, since installers and downstream pinning are affected.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At CHANGELOG.md, line 12:

<comment>This release adds `anyio` and `certifi` as required runtime dependencies and raises the `fhirpy` extra's floors, but the 1.10.0 entry does not mention any of it. Add a dependency note, as the 1.7.1 entry does, since installers and downstream pinning are affected.</comment>

<file context>
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
+
+### Changed
+
+- Autocomplete now calls the canonical `/search/autocomplete` endpoint and
+  models its compact suggestion entries. `domain_ids` and the deprecated
+  `domains` alias remain supported.
</file context>

models its compact suggestion entries. `domain_ids` and the deprecated

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The 1.10.0 entry doesn't say that Suggestion now includes type and count, or that the 1.9.1 'seven-field' model was reworked into a compact one with optional fields. Name the added fields and the shape change so users know what to expect.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At CHANGELOG.md, line 13:

<comment>The 1.10.0 entry doesn't say that `Suggestion` now includes `type` and `count`, or that the 1.9.1 'seven-field' model was reworked into a compact one with optional fields. Name the added fields and the shape change so users know what to expect.</comment>

<file context>
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
+### Changed
+
+- Autocomplete now calls the canonical `/search/autocomplete` endpoint and
+  models its compact suggestion entries. `domain_ids` and the deprecated
+  `domains` alias remain supported.
+
</file context>

`domains` alias remain supported.

## [1.9.1] - 2026-09-07

### Added
Expand Down Expand Up @@ -328,7 +336,8 @@ and are not shipped in the wheel/sdist.
- Full type hints and PEP 561 compliance
- HTTP/2 support via httpx

[Unreleased]: https://github.com/omopHub/omophub-python/compare/v1.9.1...HEAD
[Unreleased]: https://github.com/omopHub/omophub-python/compare/v1.10.0...HEAD
[1.10.0]: https://github.com/omopHub/omophub-python/compare/v1.9.1...v1.10.0
[1.9.1]: https://github.com/omopHub/omophub-python/compare/v1.9.0...v1.9.1
[1.9.0]: https://github.com/omopHub/omophub-python/compare/v1.8.1...v1.9.0
[1.8.1]: https://github.com/omopHub/omophub-python/compare/v1.8.0...v1.8.1
Expand Down
10 changes: 9 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ classifiers = [
]
dependencies = [
"httpx>=0.27.0",
"anyio>=4.14.2", # Security floor for HTTPX's transitive dependency
"certifi>=2026.10.19", # Exclude compromised emSign certificate authorities
"typing_extensions>=4.5.0",
]

Expand All @@ -65,7 +67,13 @@ dev = [
# External FHIR client libraries. Duck-typed by the resolver, so not
# required for core SDK use. Install via `pip install omophub[fhirpy]`
# or `pip install omophub[fhir-resources]` if you want these helpers.
fhirpy = ["fhirpy>=1.4.0"]
fhirpy = [
"fhirpy>=1.4.0",
# Security floors for fhirpy's HTTP clients and their dependencies.
"aiohttp>=3.14.4",
"multidict>=6.9.1",
"urllib3>=2.8.0",
]
fhir-resources = ["fhir.resources>=7.0.0"]

[project.urls]
Expand Down
8 changes: 4 additions & 4 deletions src/omophub/resources/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ class AdvancedSearchParams(TypedDict, total=False):
def _read_suggestions(payload: Any) -> list[Suggestion]:
"""Read the suggestions out of an autocomplete payload.

`/search/suggest` has returned two shapes over its life: a bare list, and
an object with a `suggestions` key. Both are accepted.
Autocomplete payloads have existed as both a bare list and an object with
a `suggestions` key. Both are accepted.

Anything else raises. This used to `return []`, which made a changed
response indistinguishable from "no suggestions matched" - a caller saw an
Expand Down Expand Up @@ -343,7 +343,7 @@ def autocomplete(
if selected_domains:
params["domain_ids"] = ",".join(selected_domains)

payload = self._request.get("/search/suggest", params=params)
payload = self._request.get("/search/autocomplete", params=params)
return _read_suggestions(payload)

def semantic(
Expand Down Expand Up @@ -722,7 +722,7 @@ async def autocomplete(
if selected_domains:
params["domain_ids"] = ",".join(selected_domains)

payload = await self._request.get("/search/suggest", params=params)
payload = await self._request.get("/search/autocomplete", params=params)
return _read_suggestions(payload)

async def semantic(
Expand Down
4 changes: 3 additions & 1 deletion src/omophub/types/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@


class Suggestion(TypedDict, total=False):
"""Autocomplete suggestion, including optional enriched concept metadata."""
"""Compact autocomplete suggestion with optional legacy concept metadata."""

suggestion: Required[str]
type: str
count: int
concept_id: int
concept_code: str
vocabulary_id: str
Expand Down
14 changes: 7 additions & 7 deletions tests/unit/resources/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def test_autocomplete(self, sync_client: OMOPHub, base_url: str) -> None:
],
},
}
route = respx.get(f"{base_url}/search/suggest").mock(
route = respx.get(f"{base_url}/search/autocomplete").mock(
return_value=Response(200, json=autocomplete_response)
)

Expand All @@ -208,7 +208,7 @@ def test_autocomplete_accepts_a_bare_list(
self, sync_client: OMOPHub, base_url: str
) -> None:
"""The older response shape stays supported."""
respx.get(f"{base_url}/search/suggest").mock(
respx.get(f"{base_url}/search/autocomplete").mock(
return_value=Response(
200,
json={"success": True, "data": [{"suggestion": "diabetes"}]},
Expand All @@ -222,7 +222,7 @@ def test_autocomplete_returns_empty_for_no_matches(
self, sync_client: OMOPHub, base_url: str
) -> None:
"""An empty list is a real answer and must not raise."""
respx.get(f"{base_url}/search/suggest").mock(
respx.get(f"{base_url}/search/autocomplete").mock(
return_value=Response(
200,
json={"success": True, "data": {"query": "zzz", "suggestions": []}},
Expand Down Expand Up @@ -253,7 +253,7 @@ def test_autocomplete_raises_on_an_unreadable_payload(
empty result: the caller saw an empty box with no way to learn the SDK
could no longer read the server.
"""
respx.get(f"{base_url}/search/suggest").mock(
respx.get(f"{base_url}/search/autocomplete").mock(
return_value=Response(200, json={"success": True, "data": payload})
)

Expand All @@ -268,7 +268,7 @@ def test_autocomplete_maps_deprecated_domains_alias(
self, sync_client: OMOPHub, base_url: str
) -> None:
"""The old SDK option remains compatible without leaking to the API."""
route = respx.get(f"{base_url}/search/suggest").mock(
route = respx.get(f"{base_url}/search/autocomplete").mock(
return_value=Response(
200,
json={
Expand Down Expand Up @@ -364,7 +364,7 @@ async def test_async_autocomplete_raises_on_an_unreadable_payload(
self, async_client: omophub.AsyncOMOPHub, base_url: str
) -> None:
"""The async path shares the sync path's reader, and its contract."""
respx.get(f"{base_url}/search/suggest").mock(
respx.get(f"{base_url}/search/autocomplete").mock(
return_value=Response(
200, json={"success": True, "data": {"query": "asp"}}
)
Expand Down Expand Up @@ -396,7 +396,7 @@ async def test_async_autocomplete(
],
},
}
respx.get(f"{base_url}/search/suggest").mock(
respx.get(f"{base_url}/search/autocomplete").mock(
return_value=Response(200, json=autocomplete_response)
)

Expand Down
Loading
Loading