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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`data` field only, so the `meta.pagination` that would tell you the set was
truncated is not part of what you get back.

- `relationship_ids` on `Mappings.get()` / `get_iter()` and their async
counterparts, as a list or a comma-separated string. The server defaults to
`["Maps to"]`, so a composite concept returns only half its decomposition
unless `"Maps to value"` is asked for too: "Allergy to penicillin G" maps to
"Allergy to drug" via `Maps to` and to "penicillin G" via `Maps to value`.

### Fixed

- `include_invalid=False` now reaches the server on `Mappings.get()` /
Expand Down
56 changes: 56 additions & 0 deletions src/omophub/resources/mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def get(
concept_id: int,
*,
target_vocabulary: str | None = None,
relationship_ids: str | list[str] | None = None,
include_invalid: bool | None = None,
page: int = 1,
page_size: int = 100,
Expand All @@ -40,6 +41,13 @@ def get(
Args:
concept_id: The concept ID
target_vocabulary: Filter to a specific target vocabulary (e.g., "ICD10CM")
relationship_ids: Relationship types to return, as a list or a
comma-separated string. Defaults server-side to ``["Maps to"]``.
Pass ``["Maps to", "Maps to value"]`` to also get the
Value-as-Concept decomposition of composite concepts -- e.g.
"Allergy to penicillin G" maps to "Allergy to drug" via
``Maps to`` and to "penicillin G" via ``Maps to value``, and the
default returns only the first of those.
include_invalid: Whether to return mappings whose relationship or
target concept is deprecated. Omit to take the server default,
which for this endpoint is to **include** them; pass ``False``
Expand All @@ -59,6 +67,12 @@ def get(
params: dict[str, Any] = {"page": page, "page_size": page_size}
if target_vocabulary:
params["target_vocabulary"] = target_vocabulary
if relationship_ids:
params["relationship_ids"] = (
",".join(relationship_ids)
if isinstance(relationship_ids, list)
else relationship_ids
)
if include_invalid is not None:
params["include_invalid"] = "true" if include_invalid else "false"
if vocab_release:
Expand All @@ -73,6 +87,7 @@ def get_iter(
concept_id: int,
*,
target_vocabulary: str | None = None,
relationship_ids: str | list[str] | None = None,
include_invalid: bool | None = None,
page_size: int = 100,
vocab_release: str | None = None,
Expand All @@ -86,6 +101,13 @@ def get_iter(
Args:
concept_id: The concept ID
target_vocabulary: Filter to a specific target vocabulary (e.g., "ICD10CM")
relationship_ids: Relationship types to return, as a list or a
comma-separated string. Defaults server-side to ``["Maps to"]``.
Pass ``["Maps to", "Maps to value"]`` to also get the
Value-as-Concept decomposition of composite concepts -- e.g.
"Allergy to penicillin G" maps to "Allergy to drug" via
``Maps to`` and to "penicillin G" via ``Maps to value``, and the
default returns only the first of those.
include_invalid: Whether to return mappings whose relationship or
target concept is deprecated. Omit to take the server default,
which for this endpoint is to **include** them; pass ``False``
Expand All @@ -104,6 +126,12 @@ def fetch_page(
params: dict[str, Any] = {"page": page, "page_size": size}
if target_vocabulary:
params["target_vocabulary"] = target_vocabulary
if relationship_ids:
params["relationship_ids"] = (
",".join(relationship_ids)
if isinstance(relationship_ids, list)
else relationship_ids
)
if include_invalid is not None:
params["include_invalid"] = "true" if include_invalid else "false"
if vocab_release:
Expand Down Expand Up @@ -192,6 +220,7 @@ async def get(
concept_id: int,
*,
target_vocabulary: str | None = None,
relationship_ids: str | list[str] | None = None,
include_invalid: bool | None = None,
page: int = 1,
page_size: int = 100,
Expand All @@ -208,6 +237,13 @@ async def get(
Args:
concept_id: The concept ID
target_vocabulary: Filter to a specific target vocabulary (e.g., "ICD10CM")
relationship_ids: Relationship types to return, as a list or a
comma-separated string. Defaults server-side to ``["Maps to"]``.
Pass ``["Maps to", "Maps to value"]`` to also get the
Value-as-Concept decomposition of composite concepts -- e.g.
"Allergy to penicillin G" maps to "Allergy to drug" via
``Maps to`` and to "penicillin G" via ``Maps to value``, and the
default returns only the first of those.
include_invalid: Whether to return mappings whose relationship or
target concept is deprecated. Omit to take the server default,
which for this endpoint is to **include** them; pass ``False``
Expand All @@ -227,6 +263,12 @@ async def get(
params: dict[str, Any] = {"page": page, "page_size": page_size}
if target_vocabulary:
params["target_vocabulary"] = target_vocabulary
if relationship_ids:
params["relationship_ids"] = (
",".join(relationship_ids)
if isinstance(relationship_ids, list)
else relationship_ids
)
if include_invalid is not None:
params["include_invalid"] = "true" if include_invalid else "false"
if vocab_release:
Expand All @@ -241,6 +283,7 @@ async def get_iter(
concept_id: int,
*,
target_vocabulary: str | None = None,
relationship_ids: str | list[str] | None = None,
include_invalid: bool | None = None,
page_size: int = 100,
vocab_release: str | None = None,
Expand All @@ -254,6 +297,13 @@ async def get_iter(
Args:
concept_id: The concept ID
target_vocabulary: Filter to a specific target vocabulary (e.g., "ICD10CM")
relationship_ids: Relationship types to return, as a list or a
comma-separated string. Defaults server-side to ``["Maps to"]``.
Pass ``["Maps to", "Maps to value"]`` to also get the
Value-as-Concept decomposition of composite concepts -- e.g.
"Allergy to penicillin G" maps to "Allergy to drug" via
``Maps to`` and to "penicillin G" via ``Maps to value``, and the
default returns only the first of those.
include_invalid: Whether to return mappings whose relationship or
target concept is deprecated. Omit to take the server default,
which for this endpoint is to **include** them; pass ``False``
Expand All @@ -272,6 +322,12 @@ async def fetch_page(
params: dict[str, Any] = {"page": page, "page_size": size}
if target_vocabulary:
params["target_vocabulary"] = target_vocabulary
if relationship_ids:
params["relationship_ids"] = (
",".join(relationship_ids)
if isinstance(relationship_ids, list)
else relationship_ids
)
if include_invalid is not None:
params["include_invalid"] = "true" if include_invalid else "false"
if vocab_release:
Expand Down
59 changes: 59 additions & 0 deletions tests/unit/resources/test_mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,65 @@ def test_get_mappings_with_filters(
assert "target_vocabulary=ICD10CM" in url_str
assert "include_invalid=true" in url_str

@respx.mock
def test_get_mappings_sends_relationship_ids(
self, sync_client: OMOPHub, base_url: str
) -> None:
"""Value-as-Concept is unreachable without this parameter.

The server defaults to `Maps to` alone, so a composite concept returns
only half its decomposition unless `Maps to value` is asked for.
"""
route = respx.get(f"{base_url}/concepts/4167462/mappings").mock(
return_value=Response(200, json={"success": True, "data": {"mappings": []}})
)

sync_client.mappings.get(4167462)
assert "relationship_ids" not in str(route.calls[0].request.url)

sync_client.mappings.get(4167462, relationship_ids=["Maps to", "Maps to value"])
assert route.calls[1].request.url.params["relationship_ids"] == (
"Maps to,Maps to value"
)

# A bare string is passed through unjoined, matching concepts.relationships().
sync_client.mappings.get(4167462, relationship_ids="Maps to value")
assert route.calls[2].request.url.params["relationship_ids"] == "Maps to value"

@respx.mock
def test_get_iter_forwards_relationship_ids(
self, sync_client: OMOPHub, base_url: str
) -> None:
"""The filter has to survive the pagination helper too."""
route = respx.get(f"{base_url}/concepts/4167462/mappings").mock(
return_value=Response(
200,
json={
"success": True,
"data": {"mappings": []},
"meta": {
"pagination": {
"page": 1,
"page_size": 100,
"total_items": 0,
"total_pages": 0,
"has_next": False,
"has_previous": False,
}
},
},
)
)

list(
sync_client.mappings.get_iter(
4167462, relationship_ids=["Maps to", "Maps to value"]
)
)
assert route.calls[0].request.url.params["relationship_ids"] == (
"Maps to,Maps to value"
)

@respx.mock
def test_get_mappings_include_invalid_is_tri_state(
self, sync_client: OMOPHub, base_url: str
Expand Down
Loading