Skip to content

Commit 87b1b88

Browse files
alex-omophubalex-omophubclaude
authored
Fhir resolver (#10)
* Enhance HTTP client with FHIR resource support and improve rate-limit handling - Added FHIR resource access to both synchronous and asynchronous OMOPHub clients. - Updated CHANGELOG to reflect changes in rate-limit handling, including honoring the `Retry-After` header and implementing exponential backoff with jitter for retries. - Included new FHIR-related types in the type definitions for better integration. * Back merge (#7) * CI pipeline * Publish action * Refactor type hinting for vocabulary_ids to use builtins.list for consistency * Import builtins conditionally in type checking for improved clarity * Codecov settings in CI * Update README.md to include Codecov badge and change License badge color * Update CHANGELOG for v0.2.0: add parameters to `concepts.get_by_code()` for synonyms and relationships, and update User-Agent header format. * Update CHANGELOG.md * Add website link to README.md * Readme updates * Documentation * Corrections * v1.3.0 preparation * Release preparation * Integration tests * Improved tests * Examples update * Downloads badge * Sponsorship * Add integration tests for standard concept filtering and multiple filters with pagination * Prepare release v1.3.1 * Refactor tests for API key validation and enhance request handling tests. Updated synchronous and asynchronous client tests to use monkeypatching for API key checks. Added new tests for handling raw requests, including error parsing, rate limits, and JSON decoding issues. * Update vocab_version in tests for consistency across mock responses and client configurations. * Extending mapping method with source_codes option * Add semantic and similar search functionality with corresponding types and integration tests - Introduced `semantic` and `similar` search methods in the search resource, allowing for advanced concept searches using neural embeddings and similarity algorithms. - Added new TypedDicts for `SemanticSearchResult`, `SemanticSearchMeta`, `SimilarConcept`, and `SimilarSearchResult` to structure the response data. - Implemented integration tests for semantic search, including filtering and pagination, as well as tests for finding similar concepts by ID and name. - Updated type imports and ensured compatibility with existing search functionality. * Add semantic search examples to README * Refactor README and integration tests to streamline result extraction - Updated README example to reflect changes in the results structure for semantic search. - Refactored integration tests to utilize a new `extract_data` function for consistent handling of results and similar concepts, improving code clarity and maintainability. * Update CI workflow to support multiple branches and manual triggering * Update minimum similarity threshold in search.py from 0.3 to 0.5 for improved filtering accuracy. * v1.4.0 release * Increase rate limit delay in integration tests from 1 second to 2 seconds for improved test reliability. * Update version handling * Prepare v1.4.1 release Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add retry logic for server errors in SyncHTTPClient and AsyncHTTPClientImpl - Implemented retry mechanism for handling server errors (502, 503, 504) in both synchronous and asynchronous HTTP clients. - Added exponential backoff delay for retries to improve resilience against temporary server issues. * Refactor retry condition formatting in SyncHTTPClient and AsyncHTTPClientImpl * Add bulk search functionality to the API - Introduced `bulk_basic` and `bulk_semantic` methods for executing multiple lexical and semantic searches in a single API call, respectively. - Updated the README to include examples for bulk search usage. - Added corresponding types for bulk search inputs and responses in the type definitions. - Implemented integration and unit tests to validate the new bulk search features. * Update type definitions to include bulk search and semantic search types - Added new types for bulk search and semantic search functionalities to the `__all__` exports in `__init__.py`. - Removed previously commented sections for clarity and organization. * Enhance integration tests for bulk search functionality - Updated assertions in `test_bulk_basic_search` to verify that all expected search IDs are present in the results. - Added a check in `test_bulk_semantic_search` to confirm that the SNOMED vocabulary filter is applied to the results. * Prep for v1.5.0 release * Examples update * Update GitHub Actions workflows to use latest action versions - Upgraded `actions/checkout` from v4 to v6 in both `ci.yml` and `publish.yml`. - Updated `codecov/codecov-action` from v4 to v5 in `ci.yml`. - Changed `actions/upload-artifact` and `actions/download-artifact` from v4 to v5 in `publish.yml`. * Add retry logic with exponential backoff and jitter for rate limits and server errors (#6) - Introduced a new `_calculate_retry_delay` function to handle retry delays based on the Retry-After header and exponential backoff with jitter. - Updated `SyncHTTPClient` and `AsyncHTTPClientImpl` to utilize the new retry delay calculation for handling rate limits (429) and server errors (502, 503, 504). - Enhanced retry mechanism to improve resilience against temporary issues. Co-authored-by: alex-omophub <sdk@omophub.com> * Update CHANGELOG for v1.5.1 release --------- Co-authored-by: alex-omophub <sdk@omophub.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Update CHANGELOG for v1.5.1 release and adjust Python version requirement in CONTRIBUTING.md. Refactor error handling in API request processing by introducing a shared `_parse_and_raise` function to streamline JSON response parsing and error management. Enhance async search functionality with pagination support and update type exports in `__init__.py`. * Refactor pagination and type hinting in async functions. Update `_parse_and_raise` to remove type ignore comment. Add tests for minimal CodeableConcept resolution and async FHIR property caching. * Implement FHIR-to-OMOP concept resolution in v1.6.0, adding methods for single and batch resolution of FHIR codings, and CodeableConcept handling. Refactor shared response parsing in API requests to improve maintainability. Update README with usage examples and correct Python version requirement in CONTRIBUTING.md. * Refactor test for pagination to enforce async callable requirement. Update test name and docstring for clarity, and change fetch_page to be an async function. --------- Co-authored-by: alex-omophub <sdk@omophub.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1915148 commit 87b1b88

13 files changed

Lines changed: 1199 additions & 216 deletions

File tree

CHANGELOG.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,33 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.6.0] - 2026-04-10
9+
10+
### Added
11+
12+
- **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.
13+
- `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.
14+
- `resolve_batch()`: Batch-resolve up to 100 FHIR codings per request with inline per-item error reporting. Failed items do not fail the batch.
15+
- `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.
16+
- New TypedDict types for FHIR resolver: `FhirResolveResult`, `FhirResolution`, `FhirBatchResult`, `FhirBatchSummary`, `FhirCodeableConceptResult`, `ResolvedConcept`, `RecommendedConceptOutput`.
17+
- Both sync (`OMOPHub`) and async (`AsyncOMOPHub`) clients support FHIR resolver methods via `client.fhir.*`.
18+
19+
### Changed
20+
21+
- **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.
22+
- **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`.
23+
- **`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`.
24+
25+
### Fixed
26+
27+
- Python prerequisite in CONTRIBUTING.md corrected from `3.9+` to `3.10+` (matching `pyproject.toml`).
28+
- `__all__` in `types/__init__.py` sorted per RUF022.
29+
830
## [1.5.1] - 2026-04-08
931

1032
### Fixed
1133

12-
- **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:
34+
- **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.
1335
- Updated `examples/search_concepts.py` to reflect current API.
1436

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

120-
[Unreleased]: https://github.com/omopHub/omophub-python/compare/v1.4.1...HEAD
142+
[Unreleased]: https://github.com/omopHub/omophub-python/compare/v1.6.0...HEAD
143+
[1.6.0]: https://github.com/omopHub/omophub-python/compare/v1.5.1...v1.6.0
144+
[1.5.1]: https://github.com/omopHub/omophub-python/compare/v1.5.0...v1.5.1
145+
[1.5.0]: https://github.com/omopHub/omophub-python/compare/v1.4.1...v1.5.0
121146
[1.4.1]: https://github.com/omopHub/omophub-python/compare/v1.4.0...v1.4.1
122147
[1.4.0]: https://github.com/omopHub/omophub-python/compare/v1.3.1...v1.4.0
123148
[1.3.1]: https://github.com/omopHub/omophub-python/compare/v1.3.0...v1.3.1

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Feature requests are welcome! Please open an issue with:
5353

5454
### Prerequisites
5555

56-
- Python 3.9+
56+
- Python 3.10+
5757
- pip
5858

5959
### Installation

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,46 @@ mappings = client.mappings.get_by_code("ICD10CM", "E11.9", target_vocabulary="SN
5858
ancestors = client.hierarchy.ancestors(201826, max_levels=3)
5959
```
6060

61+
## FHIR-to-OMOP Resolution
62+
63+
Resolve FHIR coded values to OMOP standard concepts in one call:
64+
65+
```python
66+
# Single FHIR Coding → OMOP concept + CDM target table
67+
result = client.fhir.resolve(
68+
system="http://snomed.info/sct",
69+
code="44054006",
70+
resource_type="Condition",
71+
)
72+
print(result["resolution"]["target_table"]) # "condition_occurrence"
73+
print(result["resolution"]["mapping_type"]) # "direct"
74+
75+
# ICD-10-CM → traverses "Maps to" automatically
76+
result = client.fhir.resolve(
77+
system="http://hl7.org/fhir/sid/icd-10-cm",
78+
code="E11.9",
79+
)
80+
print(result["resolution"]["standard_concept"]["vocabulary_id"]) # "SNOMED"
81+
82+
# Batch resolve up to 100 codings
83+
batch = client.fhir.resolve_batch([
84+
{"system": "http://snomed.info/sct", "code": "44054006"},
85+
{"system": "http://loinc.org", "code": "2339-0"},
86+
{"system": "http://www.nlm.nih.gov/research/umls/rxnorm", "code": "197696"},
87+
])
88+
print(f"Resolved {batch['summary']['resolved']}/{batch['summary']['total']}")
89+
90+
# CodeableConcept with vocabulary preference (SNOMED wins over ICD-10)
91+
result = client.fhir.resolve_codeable_concept(
92+
coding=[
93+
{"system": "http://snomed.info/sct", "code": "44054006"},
94+
{"system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9"},
95+
],
96+
resource_type="Condition",
97+
)
98+
print(result["best_match"]["resolution"]["source_concept"]["vocabulary_id"]) # "SNOMED"
99+
```
100+
61101
## Semantic Search
62102

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

204245
## Configuration
205246

src/omophub/_client.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from ._request import AsyncRequest, Request
1818
from .resources.concepts import AsyncConcepts, Concepts
1919
from .resources.domains import AsyncDomains, Domains
20+
from .resources.fhir import AsyncFhir, Fhir
2021
from .resources.hierarchy import AsyncHierarchy, Hierarchy
2122
from .resources.mappings import AsyncMappings, Mappings
2223
from .resources.relationships import AsyncRelationships, Relationships
@@ -97,6 +98,14 @@ def __init__(
9798
self._mappings: Mappings | None = None
9899
self._vocabularies: Vocabularies | None = None
99100
self._domains: Domains | None = None
101+
self._fhir: Fhir | None = None
102+
103+
@property
104+
def fhir(self) -> Fhir:
105+
"""Access the FHIR resolver resource."""
106+
if self._fhir is None:
107+
self._fhir = Fhir(self._request)
108+
return self._fhir
100109

101110
@property
102111
def concepts(self) -> Concepts:
@@ -228,6 +237,14 @@ def __init__(
228237
self._mappings: AsyncMappings | None = None
229238
self._vocabularies: AsyncVocabularies | None = None
230239
self._domains: AsyncDomains | None = None
240+
self._fhir: AsyncFhir | None = None
241+
242+
@property
243+
def fhir(self) -> AsyncFhir:
244+
"""Access the FHIR resolver resource."""
245+
if self._fhir is None:
246+
self._fhir = AsyncFhir(self._request)
247+
return self._fhir
231248

232249
@property
233250
def concepts(self) -> AsyncConcepts:

src/omophub/_pagination.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from urllib.parse import urlencode
77

88
if TYPE_CHECKING:
9-
from collections.abc import AsyncIterator, Callable, Iterator
9+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
1010

1111
from ._types import PaginationMeta
1212

@@ -106,7 +106,7 @@ def paginate_sync(
106106

107107

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

123123
while True:
124-
# Note: fetch_page should be an async function
125-
result = fetch_page(page, page_size)
126-
if hasattr(result, "__await__"):
127-
items, meta = await result # type: ignore
128-
else:
129-
items, meta = result
124+
items, meta = await fetch_page(page, page_size)
130125

131126
for item in items:
132127
yield item

0 commit comments

Comments
 (0)