@@ -53,7 +53,7 @@ async def list_by_asset_id(
5353 Assets of the specified class matching the provided identifiers.
5454
5555 Example:
56- ```python linenums="1" hl_lines="13 17 "
56+ ```python linenums="1" hl_lines="14 18 "
5757 import asyncio
5858
5959 from armis_sdk.clients.assets_client import AssetsClient
@@ -78,6 +78,8 @@ async def main():
7878 asyncio.run(main())
7979 ```
8080 """
81+ if not asset_ids :
82+ raise ArmisError ("asset_ids must not be empty" )
8183 filter_ = {
8284 "filter_criteria" : "ASSET_ID" ,
8385 "asset_ids" : asset_ids ,
@@ -86,6 +88,44 @@ async def main():
8688 async for item in self ._list_assets (asset_class , fields , filter_ ):
8789 yield item
8890
91+ async def list_by_boundary_id (
92+ self ,
93+ asset_class : type [AssetT ],
94+ boundary_ids : list [int ],
95+ fields : list [str ] | None = None ,
96+ ) -> AsyncIterator [AssetT ]:
97+ """List assets by boundary ID.
98+
99+ Args:
100+ asset_class: The asset class to list. Must inherit from [Asset][armis_sdk.entities.asset.Asset].
101+ boundary_ids: A list of boundary IDs to filter by.
102+ fields: Optional list of fields to retrieve. If None, all non-custom fields are retrieved.
103+
104+ Yields:
105+ Assets of the specified class belonging to any of the provided boundaries.
106+
107+ Example:
108+ ```python linenums="1" hl_lines="10"
109+ import asyncio
110+
111+ from armis_sdk.clients.assets_client import AssetsClient
112+ from armis_sdk.entities.device import Device
113+
114+
115+ async def main():
116+ assets_client = AssetsClient()
117+
118+ async for device in assets_client.list_by_boundary_id(Device, [1, 2, 3]):
119+ print(device)
120+
121+
122+ asyncio.run(main())
123+ ```
124+ """
125+ filter_ = self ._build_boundary_id_filter (boundary_ids )
126+ async for item in self ._list_assets (asset_class , fields , filter_ ):
127+ yield item
128+
89129 async def list_by_last_seen (
90130 self ,
91131 asset_class : type [AssetT ],
@@ -106,7 +146,7 @@ async def list_by_last_seen(
106146 ArmisError: If last_seen is neither datetime nor timedelta.
107147
108148 Example:
109- ```python linenums="1" hl_lines="11 15 "
149+ ```python linenums="1" hl_lines="12 16 "
110150 import asyncio
111151 import datetime
112152
@@ -129,15 +169,115 @@ async def main():
129169 asyncio.run(main())
130170 ```
131171 """
132- filter_ : dict [str , str | int ] = {"filter_criteria" : "LAST_SEEN" }
172+ filter_ = self ._build_last_seen_filter (last_seen )
173+ async for item in self ._list_assets (asset_class , fields , filter_ ):
174+ yield item
133175
134- if isinstance (last_seen , datetime .datetime ):
135- filter_ ["last_seen_ge" ] = last_seen .isoformat ()
136- elif isinstance (last_seen , datetime .timedelta ):
137- filter_ ["last_seen_seconds" ] = int (last_seen .total_seconds ())
138- else :
139- raise ArmisError (f"Invalid 'last_seen' type { type (last_seen )} " )
176+ async def list_by_multiple (
177+ self ,
178+ asset_class : type [AssetT ],
179+ last_seen : datetime .datetime | datetime .timedelta | None = None ,
180+ site_ids : list [int ] | None = None ,
181+ boundary_ids : list [int ] | None = None ,
182+ fields : list [str ] | None = None ,
183+ ) -> AsyncIterator [AssetT ]:
184+ """List assets matching multiple filter criteria simultaneously (AND logic).
185+
186+ At least one of `last_seen`, `site_ids`, or `boundary_ids` must be provided.
187+ Each criterion that is provided is applied as an AND condition.
140188
189+ Args:
190+ asset_class: The asset class to list. Must inherit from [Asset][armis_sdk.entities.asset.Asset].
191+ last_seen: Either a datetime (assets seen on or after this time) or timedelta (assets seen within this duration).
192+ site_ids: A list of site IDs to filter by.
193+ boundary_ids: A list of boundary IDs to filter by.
194+ fields: Optional list of fields to retrieve. If None, all non-custom fields are retrieved.
195+
196+ Yields:
197+ Assets of the specified class matching all provided criteria.
198+
199+ Raises:
200+ ArmisError: If no filter criteria are provided, or if last_seen is an invalid type.
201+
202+ Example:
203+ ```python linenums="1" hl_lines="11-15"
204+ import asyncio
205+ import datetime
206+
207+ from armis_sdk.clients.assets_client import AssetsClient
208+ from armis_sdk.entities.device import Device
209+
210+
211+ async def main():
212+ assets_client = AssetsClient()
213+
214+ async for device in assets_client.list_by_multiple(
215+ Device,
216+ site_ids=[1, 2],
217+ last_seen=datetime.timedelta(hours=1),
218+ ):
219+ print(device)
220+
221+
222+ asyncio.run(main())
223+ ```
224+ """
225+ filters = []
226+
227+ if last_seen is not None :
228+ filters .append (self ._build_last_seen_filter (last_seen ))
229+
230+ if site_ids is not None :
231+ filters .append (self ._build_site_id_filter (site_ids ))
232+
233+ if boundary_ids is not None :
234+ filters .append (self ._build_boundary_id_filter (boundary_ids ))
235+
236+ if not filters :
237+ raise ArmisError ("At least one of filter must be provided" )
238+
239+ filter_ = {
240+ "filter_criteria" : "MULTIPLE" ,
241+ "filters" : filters ,
242+ }
243+ async for item in self ._list_assets (asset_class , fields , filter_ ):
244+ yield item
245+
246+ async def list_by_site_id (
247+ self ,
248+ asset_class : type [AssetT ],
249+ site_ids : list [int ],
250+ fields : list [str ] | None = None ,
251+ ) -> AsyncIterator [AssetT ]:
252+ """List assets by site ID.
253+
254+ Args:
255+ asset_class: The asset class to list. Must inherit from [Asset][armis_sdk.entities.asset.Asset].
256+ site_ids: A list of site IDs to filter by.
257+ fields: Optional list of fields to retrieve. If None, all non-custom fields are retrieved.
258+
259+ Yields:
260+ Assets of the specified class belonging to any of the provided sites.
261+
262+ Example:
263+ ```python linenums="1" hl_lines="10"
264+ import asyncio
265+
266+ from armis_sdk.clients.assets_client import AssetsClient
267+ from armis_sdk.entities.device import Device
268+
269+
270+ async def main():
271+ assets_client = AssetsClient()
272+
273+ async for device in assets_client.list_by_site_id(Device, [1, 2, 3]):
274+ print(device)
275+
276+
277+ asyncio.run(main())
278+ ```
279+ """
280+ filter_ = self ._build_site_id_filter (site_ids )
141281 async for item in self ._list_assets (asset_class , fields , filter_ ):
142282 yield item
143283
@@ -151,7 +291,7 @@ async def list_fields(self, asset_class: type[AssetT]) -> AsyncIterator[AssetFie
151291 Field descriptions including field name, type, and other metadata.
152292
153293 Example:
154- ```python linenums="1" hl_lines="9 "
294+ ```python linenums="1" hl_lines="10 "
155295 import asyncio
156296
157297 from armis_sdk.clients.assets_client import AssetsClient
@@ -249,6 +389,29 @@ async def main():
249389 if errors :
250390 raise BulkUpdateError (errors )
251391
392+ @staticmethod
393+ def _build_boundary_id_filter (boundary_ids : list [int ]) -> dict :
394+ if not boundary_ids :
395+ raise ArmisError ("boundary_ids must not be empty" )
396+ return {"filter_criteria" : "BOUNDARY_ID" , "boundary_ids" : boundary_ids }
397+
398+ @staticmethod
399+ def _build_last_seen_filter (last_seen : datetime .datetime | datetime .timedelta ) -> dict :
400+ filter_ : dict [str , str | int ] = {"filter_criteria" : "LAST_SEEN" }
401+ if isinstance (last_seen , datetime .datetime ):
402+ filter_ ["last_seen_ge" ] = last_seen .isoformat ()
403+ elif isinstance (last_seen , datetime .timedelta ):
404+ filter_ ["last_seen_seconds" ] = int (last_seen .total_seconds ())
405+ else :
406+ raise ArmisError (f"Invalid 'last_seen' type { type (last_seen )} " )
407+ return filter_
408+
409+ @staticmethod
410+ def _build_site_id_filter (site_ids : list [int ]) -> dict :
411+ if not site_ids :
412+ raise ArmisError ("site_ids must not be empty" )
413+ return {"filter_criteria" : "SITE_ID" , "site_ids" : site_ids }
414+
252415 @classmethod
253416 def _create_bulk_update_request (
254417 cls ,
0 commit comments