From 4ea81184f6fc4b84806d4fe5714d967aca9e671f Mon Sep 17 00:00:00 2001 From: Bryan Ray Date: Wed, 21 Oct 2026 14:49:10 -0500 Subject: [PATCH] feat(assets): support ID filters in asset search --- README.md | 17 ++++++ src/corva_api_client/resources/assets.py | 15 +++++ tests/test_assets_resource.py | 70 ++++++++++++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/README.md b/README.md index 8f8debf..a3cf94b 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,23 @@ apps = client.apps.search(type="drilling") client.close() ``` +### Asset ID filtering + +Fetch selected fields for known assets in one request: + +```python +from corva_api_client.resources import AssetField + +assets = client.assets.search(ids=[123, 456], fields=[AssetField.NAME]) +``` + +`ids` accepts a nonempty sequence of positive integers and is sent to the API as +repeated `ids[]` parameters. It combines with the other search filters and caller +permissions; results need not include every requested ID or follow input order. +Pass `ids=None` (the default) to omit the filter. Empty sequences, booleans, +strings, and other invalid IDs raise `ValueError` before sending a request. +Normal field defaults and pagination still apply. + ### Asset field selection Asset searches use a compact fieldset by default to avoid returning every asset diff --git a/src/corva_api_client/resources/assets.py b/src/corva_api_client/resources/assets.py index 168c248..3f3819a 100644 --- a/src/corva_api_client/resources/assets.py +++ b/src/corva_api_client/resources/assets.py @@ -222,9 +222,16 @@ def search( per_page: int | None = None, order: str | None = None, visibility: str | None = None, + *, + ids: Sequence[int] | None = None, ): """Search assets using a discoverable JSON:API sparse fieldset. + ``ids`` restricts results to a nonempty sequence of positive integer asset + IDs, intersected with the other filters and caller permissions. IDs are + sent as repeated ``ids[]`` parameters. Pass ``None`` to omit the filter. + Empty sequences and invalid IDs raise ``ValueError`` before any request. + ``fields`` accepts enum members, arbitrary ``.`` strings, comma-separated strings, or the explicit full-payload values ``"*"`` and ``"all"``. Relationships are included only when their @@ -239,6 +246,14 @@ def search( """ params: dict[str, Any] = {} + if ids is not None: + if isinstance(ids, (str, bytes)) or not isinstance(ids, Sequence): + raise ValueError("Asset IDs must be a nonempty sequence of positive integers.") + asset_ids = list(ids) + if not asset_ids or any(type(id) is not int or id <= 0 for id in asset_ids): + raise ValueError("Asset IDs must be a nonempty sequence of positive integers.") + params["ids[]"] = asset_ids + if query: params["search"] = query serialized_types = _serialize_asset_types(types) diff --git a/tests/test_assets_resource.py b/tests/test_assets_resource.py index af8f04e..1888cc9 100644 --- a/tests/test_assets_resource.py +++ b/tests/test_assets_resource.py @@ -183,3 +183,73 @@ def test_search_omits_empty_fields(fields) -> None: assets.search(fields=fields) assert "fields" not in client.get.call_args.kwargs["params"] + + +@pytest.mark.parametrize("ids", [[123, 456], (123, 456)]) +def test_search_filters_ids_with_existing_options(ids) -> None: + client = Mock() + client.get.return_value = {"data": [{"id": "123"}]} + assets = AssetsClient(client) + + result = assets.search( + ids=ids, + query="well", + company_id=80, + visibility="visible", + types="well", + status=AssetStatus.ACTIVE, + fields=[AssetField.NAME], + start=1000, + end=2000, + sort="name", + order="asc", + page=2, + per_page=10, + ) + + assert result == {"data": [{"id": "123"}]} + client.get.assert_called_once_with( + "/v2/assets", + params={ + "ids[]": [123, 456], + "search": "well", + "company_id": 80, + "visibility": "visible", + "types[]": ["well"], + "status[]": ["active"], + "fields": "asset.name", + "start": 1000, + "end": 2000, + "sort": "name", + "order": "asc", + "page": 2, + "per_page": 10, + }, + ) + params = httpx.QueryParams(client.get.call_args.kwargs["params"]) + assert params.get_list("ids[]") == ["123", "456"] + + +@pytest.mark.parametrize( + "ids", [[], (), [0], [-1], [True], [1.0], ["123"], [123, None], "123,456", 123] +) +def test_search_rejects_invalid_ids_before_request(ids) -> None: + client = Mock() + assets = AssetsClient(client) + + with pytest.raises(ValueError, match="nonempty sequence of positive integers"): + assets.search(ids=ids) + + client.get.assert_not_called() + + +def test_search_none_ids_preserves_default_request() -> None: + client = Mock() + assets = AssetsClient(client) + + assets.search(ids=None) + + assert client.get.call_args.kwargs["params"] == { + "fields": "asset.name,asset.asset_type,asset.status", + "sort": "-last_active_at", + }