Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
f92fbc8
CI pipeline
alex-omophub Dec 3, 2025
a213047
Publish action
alex-omophub Dec 3, 2025
c289a9f
Refactor type hinting for vocabulary_ids to use builtins.list for con…
Dec 3, 2025
2d4673b
Import builtins conditionally in type checking for improved clarity
Dec 3, 2025
e4aa3a2
Codecov settings in CI
alex-omophub Dec 4, 2025
b7d0202
Update README.md to include Codecov badge and change License badge color
Dec 4, 2025
fb404ad
Update CHANGELOG for v0.2.0: add parameters to `concepts.get_by_code(…
Dec 9, 2025
3897dd4
Update CHANGELOG.md
alex-omophub Dec 9, 2025
b162d13
Add website link to README.md
Dec 22, 2025
b4f560b
Readme updates
Dec 27, 2025
b336c96
Documentation
Dec 27, 2025
52d6f51
Corrections
Dec 29, 2025
f37ba3a
v1.3.0 preparation
Jan 6, 2026
e8cb1d6
Release preparation
Jan 6, 2026
e0711ab
Integration tests
Jan 6, 2026
680059c
Improved tests
Jan 6, 2026
ebdba7c
Examples update
Jan 6, 2026
6784613
Downloads badge
Jan 8, 2026
1c3770f
Sponsorship
alex-omophub Jan 19, 2026
7bdf440
Add integration tests for standard concept filtering and multiple fil…
Jan 24, 2026
0a64db9
Prepare release v1.3.1
Jan 24, 2026
5dcb15b
Refactor tests for API key validation and enhance request handling te…
Jan 24, 2026
88cb440
Update vocab_version in tests for consistency across mock responses a…
Jan 27, 2026
b6c74dd
Extending mapping method with source_codes option
Jan 30, 2026
c89789c
Add semantic and similar search functionality with corresponding type…
Feb 16, 2026
156be70
Add semantic search examples to README
Feb 16, 2026
1c87524
Refactor README and integration tests to streamline result extraction
Feb 16, 2026
89b8684
Update CI workflow to support multiple branches and manual triggering
Feb 16, 2026
db326bd
Merge pull request #1 from OMOPHub/semantic-search
alex-omophub Feb 18, 2026
52a5113
Update minimum similarity threshold in search.py from 0.3 to 0.5 for …
Feb 18, 2026
26c9007
v1.4.0 release
Feb 23, 2026
2a3d387
Increase rate limit delay in integration tests from 1 second to 2 sec…
Feb 23, 2026
494cc8a
Update version handling
Feb 28, 2026
3d3f3a2
Prepare v1.4.1 release
Feb 28, 2026
05df251
Add retry logic for server errors in SyncHTTPClient and AsyncHTTPClie…
Mar 18, 2026
b2a98db
Refactor retry condition formatting in SyncHTTPClient and AsyncHTTPCl…
Mar 19, 2026
aa1c81e
Merge pull request #4 from OMOPHub/retry-on-error
alex-omophub Mar 19, 2026
87c5b74
Add bulk search functionality to the API
Mar 26, 2026
9233f8a
Update type definitions to include bulk search and semantic search types
Mar 26, 2026
5a73447
Enhance integration tests for bulk search functionality
Mar 26, 2026
b972391
Merge pull request #5 from OMOPHub/bulk-search
alex-omophub Mar 26, 2026
eee0a13
Prep for v1.5.0 release
Mar 26, 2026
4b0f544
Examples update
Mar 26, 2026
e3561ea
Update GitHub Actions workflows to use latest action versions
Mar 27, 2026
214f1b5
Add retry logic with exponential backoff and jitter for rate limits a…
alex-omophub Mar 30, 2026
1915148
Update CHANGELOG for v1.5.1 release
Apr 8, 2026
140c7ca
Enhance HTTP client with FHIR resource support and improve rate-limit…
Apr 10, 2026
cca1f05
Merge branch 'develop' into fhir-resolver
alex-omophub Apr 10, 2026
55255df
Update CHANGELOG for v1.5.1 release and adjust Python version require…
Apr 10, 2026
8091a91
Refactor pagination and type hinting in async functions. Update `_par…
Apr 10, 2026
85ddaae
Implement FHIR-to-OMOP concept resolution in v1.6.0, adding methods f…
Apr 10, 2026
4be3a32
Refactor test for pagination to enforce async callable requirement. U…
Apr 10, 2026
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
29 changes: 27 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,33 @@ 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.6.0] - 2026-04-10

### Added

- **FHIR-to-OMOP Concept Resolver** (`client.fhir`): Translate FHIR coded values into OMOP standard concepts, CDM target tables, and optional Phoebe recommendations in a single API call.
- `resolve()`: Resolve a single FHIR `Coding` (system URI + code) or text-only input via semantic search fallback. Returns the standard concept, target CDM table, domain alignment check, and optional mapping quality signal.
- `resolve_batch()`: Batch-resolve up to 100 FHIR codings per request with inline per-item error reporting. Failed items do not fail the batch.
- `resolve_codeable_concept()`: Resolve a FHIR `CodeableConcept` with multiple codings. Automatically picks the best match per OHDSI vocabulary preference (SNOMED > RxNorm > LOINC > CVX > ICD-10). Falls back to the `text` field via semantic search when no coding resolves.
- New TypedDict types for FHIR resolver: `FhirResolveResult`, `FhirResolution`, `FhirBatchResult`, `FhirBatchSummary`, `FhirCodeableConceptResult`, `ResolvedConcept`, `RecommendedConceptOutput`.
- Both sync (`OMOPHub`) and async (`AsyncOMOPHub`) clients support FHIR resolver methods via `client.fhir.*`.

### Changed

- **Extracted shared response parsing** (`_request.py`): The duplicated JSON decode / error-handling / rate-limit-retry logic across `Request._parse_response`, `Request._parse_response_raw`, `AsyncRequest._parse_response`, and `AsyncRequest._parse_response_raw` (4 copies of ~50 lines each) is now a single `_parse_and_raise()` module-level function. All four methods delegate to it, eliminating the risk of divergence bugs.
- **Fixed `paginate_async` signature** (`_pagination.py`): The type hint now correctly declares `Callable[[int, int], Awaitable[tuple[...]]]` instead of `Callable[[int, int], tuple[...]]`, and the runtime `hasattr(__await__)` duck-typing hack has been replaced with a clean `await`.
- **`AsyncSearch.semantic_iter`** now delegates to `paginate_async` instead of manually reimplementing the pagination loop, matching the sync `semantic_iter` which already uses `paginate_sync`.

### Fixed

- Python prerequisite in CONTRIBUTING.md corrected from `3.9+` to `3.10+` (matching `pyproject.toml`).
- `__all__` in `types/__init__.py` sorted per RUF022.

## [1.5.1] - 2026-04-08

### Fixed

- **Rate-limit handling**: HTTP client now respects the `Retry-After` header on `429 Too Many Requests` responses and applies exponential backoff with jitter on retries. Previous versions retried only on `502/503/504` with a fixed `2^attempt * 0.5s` schedule and did not back off on `429` at all, so a client that hit the server's rate limit at high volume could burn through thousands of failed requests in a tight loop. The new behavior:
- **Rate-limit handling**: HTTP client now respects the `Retry-After` header on `429 Too Many Requests` responses and applies exponential backoff with jitter on retries. Previous versions retried only on `502/503/504` with a fixed `2^attempt * 0.5s` schedule and did not back off on `429` at all, so a client that hit the server's rate limit at high volume could burn through thousands of failed requests in a tight loop. The client now honors `Retry-After`, uses exponential backoff with jitter, respects the configured `max_retries`, and caps backoff at 30 seconds.
- Updated `examples/search_concepts.py` to reflect current API.

## [1.5.0] - 2026-03-26
Expand Down Expand Up @@ -117,7 +139,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Full type hints and PEP 561 compliance
- HTTP/2 support via httpx

[Unreleased]: https://github.com/omopHub/omophub-python/compare/v1.4.1...HEAD
[Unreleased]: https://github.com/omopHub/omophub-python/compare/v1.6.0...HEAD
[1.6.0]: https://github.com/omopHub/omophub-python/compare/v1.5.1...v1.6.0
[1.5.1]: https://github.com/omopHub/omophub-python/compare/v1.5.0...v1.5.1
[1.5.0]: https://github.com/omopHub/omophub-python/compare/v1.4.1...v1.5.0
[1.4.1]: https://github.com/omopHub/omophub-python/compare/v1.4.0...v1.4.1
[1.4.0]: https://github.com/omopHub/omophub-python/compare/v1.3.1...v1.4.0
[1.3.1]: https://github.com/omopHub/omophub-python/compare/v1.3.0...v1.3.1
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Feature requests are welcome! Please open an issue with:

### Prerequisites

- Python 3.9+
- Python 3.10+
- pip

### Installation
Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,46 @@ mappings = client.mappings.get_by_code("ICD10CM", "E11.9", target_vocabulary="SN
ancestors = client.hierarchy.ancestors(201826, max_levels=3)
```

## FHIR-to-OMOP Resolution

Resolve FHIR coded values to OMOP standard concepts in one call:

```python
# Single FHIR Coding → OMOP concept + CDM target table
result = client.fhir.resolve(
system="http://snomed.info/sct",
code="44054006",
resource_type="Condition",
)
print(result["resolution"]["target_table"]) # "condition_occurrence"
print(result["resolution"]["mapping_type"]) # "direct"

# ICD-10-CM → traverses "Maps to" automatically
result = client.fhir.resolve(
system="http://hl7.org/fhir/sid/icd-10-cm",
code="E11.9",
)
print(result["resolution"]["standard_concept"]["vocabulary_id"]) # "SNOMED"

# Batch resolve up to 100 codings
batch = client.fhir.resolve_batch([
{"system": "http://snomed.info/sct", "code": "44054006"},
{"system": "http://loinc.org", "code": "2339-0"},
{"system": "http://www.nlm.nih.gov/research/umls/rxnorm", "code": "197696"},
])
print(f"Resolved {batch['summary']['resolved']}/{batch['summary']['total']}")

# CodeableConcept with vocabulary preference (SNOMED wins over ICD-10)
result = client.fhir.resolve_codeable_concept(
coding=[
{"system": "http://snomed.info/sct", "code": "44054006"},
{"system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9"},
],
resource_type="Condition",
)
print(result["best_match"]["resolution"]["source_concept"]["vocabulary_id"]) # "SNOMED"
```

## Semantic Search

Use natural language queries to find concepts using neural embeddings:
Expand Down Expand Up @@ -200,6 +240,7 @@ suggestions = client.concepts.suggest("diab", vocabulary_ids=["SNOMED"], page_si
| `mappings` | Cross-vocabulary mappings | `get()`, `map()` |
| `vocabularies` | Vocabulary metadata | `list()`, `get()`, `stats()` |
| `domains` | Domain information | `list()`, `get()`, `concepts()` |
| `fhir` | FHIR-to-OMOP resolution | `resolve()`, `resolve_batch()`, `resolve_codeable_concept()` |

## Configuration

Expand Down
17 changes: 17 additions & 0 deletions src/omophub/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from ._request import AsyncRequest, Request
from .resources.concepts import AsyncConcepts, Concepts
from .resources.domains import AsyncDomains, Domains
from .resources.fhir import AsyncFhir, Fhir
from .resources.hierarchy import AsyncHierarchy, Hierarchy
from .resources.mappings import AsyncMappings, Mappings
from .resources.relationships import AsyncRelationships, Relationships
Expand Down Expand Up @@ -97,6 +98,14 @@ def __init__(
self._mappings: Mappings | None = None
self._vocabularies: Vocabularies | None = None
self._domains: Domains | None = None
self._fhir: Fhir | None = None

@property
def fhir(self) -> Fhir:
"""Access the FHIR resolver resource."""
if self._fhir is None:
self._fhir = Fhir(self._request)
return self._fhir

@property
def concepts(self) -> Concepts:
Expand Down Expand Up @@ -228,6 +237,14 @@ def __init__(
self._mappings: AsyncMappings | None = None
self._vocabularies: AsyncVocabularies | None = None
self._domains: AsyncDomains | None = None
self._fhir: AsyncFhir | None = None

@property
def fhir(self) -> AsyncFhir:
"""Access the FHIR resolver resource."""
if self._fhir is None:
self._fhir = AsyncFhir(self._request)
return self._fhir

@property
def concepts(self) -> AsyncConcepts:
Expand Down
11 changes: 3 additions & 8 deletions src/omophub/_pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from urllib.parse import urlencode

if TYPE_CHECKING:
from collections.abc import AsyncIterator, Callable, Iterator
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator

from ._types import PaginationMeta

Expand Down Expand Up @@ -106,7 +106,7 @@ def paginate_sync(


async def paginate_async(
fetch_page: Callable[[int, int], tuple[list[T], PaginationMeta | None]],
fetch_page: Callable[[int, int], Awaitable[tuple[list[T], PaginationMeta | None]]],
page_size: int = DEFAULT_PAGE_SIZE,
) -> AsyncIterator[T]:
"""Create an async iterator that auto-paginates through results.
Expand All @@ -121,12 +121,7 @@ async def paginate_async(
page = 1

while True:
# Note: fetch_page should be an async function
result = fetch_page(page, page_size)
if hasattr(result, "__await__"):
items, meta = await result # type: ignore
else:
items, meta = result
items, meta = await fetch_page(page, page_size)

for item in items:
yield item
Expand Down
Loading
Loading