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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/corva_api_client/resources/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ``<record_type>.<field>``
strings, comma-separated strings, or the explicit full-payload values
``"*"`` and ``"all"``. Relationships are included only when their
Expand All @@ -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)
Expand Down
70 changes: 70 additions & 0 deletions tests/test_assets_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
Loading