Skip to content

Commit 8091a91

Browse files
author
alex-omophub
committed
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.
1 parent 55255df commit 8091a91

4 files changed

Lines changed: 139 additions & 9 deletions

File tree

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

src/omophub/_request.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def _parse_and_raise(
7474
retry_after=retry_after,
7575
)
7676

77-
return data # type: ignore[return-value]
77+
return data
7878

7979

8080
class Request(Generic[T]):

src/omophub/resources/search.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@ async def fetch_page(
699699
meta = result.get("meta", {}).get("pagination")
700700
return results, meta
701701

702+
item: SemanticSearchResult
702703
async for item in paginate_async(fetch_page, page_size):
703704
yield item
704705

tests/unit/resources/test_fhir.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,25 @@ def test_resolve_codeable_concept_with_all_options(
353353
assert body["include_recommendations"] is True
354354
assert body["include_quality"] is True
355355

356+
@respx.mock
357+
def test_resolve_codeable_concept_minimal(self, sync_client: OMOPHub, base_url: str) -> None:
358+
"""CodeableConcept with no optional flags (covers False branches)."""
359+
route = respx.post(f"{base_url}/fhir/resolve/codeable-concept").mock(
360+
return_value=Response(200, json=CODEABLE_CONCEPT_RESPONSE)
361+
)
362+
363+
sync_client.fhir.resolve_codeable_concept(
364+
coding=[{"system": "http://snomed.info/sct", "code": "44054006"}],
365+
)
366+
367+
import json
368+
369+
body = json.loads(route.calls[0].request.content)
370+
assert "text" not in body
371+
assert "resource_type" not in body
372+
assert "include_recommendations" not in body
373+
assert "include_quality" not in body
374+
356375
@respx.mock
357376
def test_resolve_sends_correct_body(self, sync_client: OMOPHub, base_url: str) -> None:
358377
"""Verify the POST body includes only non-None parameters."""
@@ -439,3 +458,118 @@ async def test_async_resolve_codeable_concept(
439458
)
440459

441460
assert result["best_match"] is not None
461+
462+
@respx.mock
463+
@pytest.mark.anyio
464+
async def test_async_resolve_vocabulary_id_bypass(
465+
self, async_client: AsyncOMOPHub, base_url: str
466+
) -> None:
467+
"""Async resolve with vocabulary_id exercises the bypass branch."""
468+
route = respx.post(f"{base_url}/fhir/resolve").mock(
469+
return_value=Response(200, json=ICD10_MAPPED_RESPONSE)
470+
)
471+
472+
result = await async_client.fhir.resolve(
473+
vocabulary_id="ICD10CM",
474+
code="E11.9",
475+
include_recommendations=True,
476+
recommendations_limit=3,
477+
include_quality=True,
478+
)
479+
480+
import json
481+
482+
body = json.loads(route.calls[0].request.content)
483+
assert body["vocabulary_id"] == "ICD10CM"
484+
assert body["include_recommendations"] is True
485+
assert body["recommendations_limit"] == 3
486+
assert body["include_quality"] is True
487+
assert "resolution" in result
488+
489+
@respx.mock
490+
@pytest.mark.anyio
491+
async def test_async_resolve_batch_all_flags(
492+
self, async_client: AsyncOMOPHub, base_url: str
493+
) -> None:
494+
"""Async batch with include_recommendations exercises that branch."""
495+
route = respx.post(f"{base_url}/fhir/resolve/batch").mock(
496+
return_value=Response(200, json=BATCH_RESPONSE)
497+
)
498+
499+
await async_client.fhir.resolve_batch(
500+
[{"system": "http://snomed.info/sct", "code": "44054006"}],
501+
resource_type="Condition",
502+
include_recommendations=True,
503+
recommendations_limit=5,
504+
include_quality=True,
505+
)
506+
507+
import json
508+
509+
body = json.loads(route.calls[0].request.content)
510+
assert body["include_recommendations"] is True
511+
assert body["recommendations_limit"] == 5
512+
assert body["include_quality"] is True
513+
514+
@respx.mock
515+
@pytest.mark.anyio
516+
async def test_async_fhir_property_cached(self, async_client: AsyncOMOPHub, base_url: str) -> None:
517+
"""Accessing client.fhir twice returns the same cached instance."""
518+
fhir1 = async_client.fhir
519+
fhir2 = async_client.fhir
520+
assert fhir1 is fhir2
521+
522+
523+
@respx.mock
524+
@pytest.mark.anyio
525+
async def test_async_resolve_codeable_minimal(
526+
self, async_client: AsyncOMOPHub, base_url: str
527+
) -> None:
528+
"""Async codeable concept with no optional flags (covers False branches)."""
529+
route = respx.post(f"{base_url}/fhir/resolve/codeable-concept").mock(
530+
return_value=Response(200, json=CODEABLE_CONCEPT_RESPONSE)
531+
)
532+
533+
await async_client.fhir.resolve_codeable_concept(
534+
coding=[{"system": "http://snomed.info/sct", "code": "44054006"}],
535+
)
536+
537+
import json
538+
539+
body = json.loads(route.calls[0].request.content)
540+
assert "text" not in body
541+
assert "resource_type" not in body
542+
assert "include_recommendations" not in body
543+
assert "include_quality" not in body
544+
545+
@respx.mock
546+
@pytest.mark.anyio
547+
async def test_async_resolve_batch_minimal(
548+
self, async_client: AsyncOMOPHub, base_url: str
549+
) -> None:
550+
"""Async batch with no optional flags (covers False branches)."""
551+
route = respx.post(f"{base_url}/fhir/resolve/batch").mock(
552+
return_value=Response(200, json=BATCH_RESPONSE)
553+
)
554+
555+
await async_client.fhir.resolve_batch(
556+
[{"system": "http://snomed.info/sct", "code": "44054006"}],
557+
)
558+
559+
import json
560+
561+
body = json.loads(route.calls[0].request.content)
562+
assert "resource_type" not in body
563+
assert "include_recommendations" not in body
564+
assert "include_quality" not in body
565+
566+
567+
class TestFhirPropertyCaching:
568+
"""Test lazy-property cache hit on both client types."""
569+
570+
@respx.mock
571+
def test_sync_fhir_property_cached(self, sync_client: OMOPHub) -> None:
572+
"""Accessing client.fhir twice returns the same cached instance."""
573+
fhir1 = sync_client.fhir
574+
fhir2 = sync_client.fhir
575+
assert fhir1 is fhir2

0 commit comments

Comments
 (0)