From 3e9e13291cbe6fc5b4f177421fd6923ddc306082 Mon Sep 17 00:00:00 2001 From: Cecilia Stevens <63068179+ceciliastevens@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:29:17 -0400 Subject: [PATCH 01/11] add watchlistV2 client to sdk --- src/_incydr_sdk/alerts/client.py | 2 + src/_incydr_sdk/watchlists/client.py | 634 ++++++++++++++++++ src/_incydr_sdk/watchlists/models/requests.py | 25 + .../watchlists/models/responses.py | 92 ++- tests/test_watchlists.py | 558 +++++++++++++++ 5 files changed, 1306 insertions(+), 5 deletions(-) diff --git a/src/_incydr_sdk/alerts/client.py b/src/_incydr_sdk/alerts/client.py index 5774b465..745fef8e 100644 --- a/src/_incydr_sdk/alerts/client.py +++ b/src/_incydr_sdk/alerts/client.py @@ -22,6 +22,8 @@ class AlertsV1: """ Client for `/v1/alerts` endpoints. + This client is deprecated. Use the Sessions client instead. + Usage example: >>> import incydr diff --git a/src/_incydr_sdk/watchlists/client.py b/src/_incydr_sdk/watchlists/client.py index 85993ae2..7550069a 100644 --- a/src/_incydr_sdk/watchlists/client.py +++ b/src/_incydr_sdk/watchlists/client.py @@ -2,24 +2,32 @@ from typing import Iterator from typing import List from typing import Union +from warnings import warn from _incydr_sdk.enums.watchlists import WatchlistType from _incydr_sdk.exceptions import WatchlistNotFoundError from _incydr_sdk.watchlists.models.requests import CreateWatchlistRequest from _incydr_sdk.watchlists.models.requests import ListWatchlistsRequest +from _incydr_sdk.watchlists.models.requests import ListWatchlistsRequestV2 +from _incydr_sdk.watchlists.models.requests import UpdateExcludedActorsRequest from _incydr_sdk.watchlists.models.requests import UpdateExcludedUsersRequest +from _incydr_sdk.watchlists.models.requests import UpdateIncludedActorsRequest from _incydr_sdk.watchlists.models.requests import UpdateIncludedDepartmentsRequest from _incydr_sdk.watchlists.models.requests import UpdateIncludedDirectoryGroupsRequest from _incydr_sdk.watchlists.models.requests import UpdateIncludedUsersRequest from _incydr_sdk.watchlists.models.requests import UpdateWatchlistRequest +from _incydr_sdk.watchlists.models.responses import ExcludedActorsList from _incydr_sdk.watchlists.models.responses import ExcludedUsersList +from _incydr_sdk.watchlists.models.responses import IncludedActorsList from _incydr_sdk.watchlists.models.responses import IncludedDepartment from _incydr_sdk.watchlists.models.responses import IncludedDepartmentsList from _incydr_sdk.watchlists.models.responses import IncludedDirectoryGroup from _incydr_sdk.watchlists.models.responses import IncludedDirectoryGroupsList from _incydr_sdk.watchlists.models.responses import IncludedUsersList from _incydr_sdk.watchlists.models.responses import Watchlist +from _incydr_sdk.watchlists.models.responses import WatchlistActor from _incydr_sdk.watchlists.models.responses import WatchlistMembersList +from _incydr_sdk.watchlists.models.responses import WatchlistMembersListV2 from _incydr_sdk.watchlists.models.responses import WatchlistsPage from _incydr_sdk.watchlists.models.responses import WatchlistUser @@ -28,6 +36,7 @@ class WatchlistsClient: def __init__(self, parent): self._parent = parent self._v1 = None + self._v2 = None @property def v1(self): @@ -35,11 +44,511 @@ def v1(self): self._v1 = WatchlistsV1(self._parent) return self._v1 + @property + def v2(self): + if self._v2 is None: + self._v2 = WatchlistsV2(self._parent) + return self._v2 + + +class WatchlistsV2: + """ + Client for `/v2/watchlists` endpoints. + + Usage example: + + >>> import incydr + >>> client = incydr.Client(**kwargs) + >>> client.watchlists.v2.get_page() + """ + + def __init__(self, parent): + self._parent = parent + self._watchlist_type_id_map = {} + self._uri = "/v2/watchlists" + + def get_page( + self, page_num: int = 1, page_size: int = None, actor_id: str = None + ) -> WatchlistsPage: + """ + Get a page of watchlists. + + Filter results by passing appropriate parameters: + + **Parameters**: + + * **page_num**: `int` - Page number for results, starting at 1. + * **page_size**: `int` - Max number of results to return for a page. + * **actor_id**: `str` - Matches watchlists where the actor is a member. + + **Returns**: A [`WatchlistsPage`][watchlistspage-model] object. + """ + page_size = page_size or self._parent.settings.page_size + data = ListWatchlistsRequestV2( + page=page_num, pageSize=page_size, actorId=actor_id + ) + response = self._parent.session.get(self._uri, params=data.dict()) + return WatchlistsPage.parse_response(response) + + def iter_all( + self, page_size: int = None, actor_id: str = None + ) -> Iterator[Watchlist]: + """ + Iterate over all watchlists. + + Accepts the same parameters as `.get_page()` excepting `page_num`. + + **Returns**: A generator yielding individual [`Watchlist`][watchlist-model] objects. + """ + page_size = page_size or self._parent.settings.page_size + for page_num in count(1): + page = self.get_page( + page_num=page_num, page_size=page_size, actor_id=actor_id + ) + yield from page.watchlists + if len(page.watchlists) < page_size: + break + + def get(self, watchlist_id: str) -> Watchlist: + """ + Get a single watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + + **Returns**: A [`Watchlist`][watchlist-model] object. + """ + response = self._parent.session.get(f"{self._uri}/{watchlist_id}") + return Watchlist.parse_response(response) + + def create( + self, watchlist_type: WatchlistType, title: str = None, description: str = None + ) -> Watchlist: + """ + Create a new watchlist. + + **Parameters**: + + * **watchlist_type**: [`WatchlistType`][watchlist-types] (required) - Type of the watchlist to create. + * **title**: The required title for a custom watchlist. + * **description**: The optional description for a custom watchlist. + + **Returns**: A ['Watchlist`][watchlist-model] object. + """ + if watchlist_type == "CUSTOM": + if title is None: + raise ValueError("`title` value is required for custom watchlists.") + + data = CreateWatchlistRequest( + description=description, title=title, watchlistType=watchlist_type + ) + response = self._parent.session.post(url=self._uri, json=data.dict()) + watchlist = Watchlist.parse_response(response) + self._watchlist_type_id_map[watchlist_type] = watchlist.watchlist_id + return watchlist + + def delete(self, watchlist_id: str): + """ + Delete a watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + + **Returns**: A `requests.Response` indicating success. + """ + return self._parent.session.delete(f"{self._uri}/{watchlist_id}") + + def update( + self, watchlist_id: str, title: str = None, description: str = None + ) -> Watchlist: + """ + Update a custom watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + * **title**: `str` - Updated title for a custom watchlist. Defaults to None. + * **description: `str` - Updated description for a custom watchlist. Pass an empty string to clear this field. Defaults to None. + + **Returns**: A [`Watchlist`][watchlist-model] object. + """ + paths = [] + if title: + paths += ["title"] + if description: + paths += ["description"] + query = {"paths": paths} + data = UpdateWatchlistRequest(description=description, title=title) + response = self._parent.session.patch( + f"{self._uri}/{watchlist_id}", params=query, json=data.dict() + ) + return Watchlist.parse_response(response) + + def get_member(self, watchlist_id: str, actor_id: str) -> WatchlistActor: + """ + Get a single member of a watchlist. A member may have been added as an included actor, or is a member of an included department, etc. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + * **actor_id**: `str` (required) - Unique actor ID. + + **Returns**: A [`WatchlistActor`][watchlistactor-model] object. + """ + response = self._parent.session.get( + f"{self._uri}/{watchlist_id}/members/{actor_id}" + ) + return WatchlistActor.parse_response(response) + + def list_members( + self, watchlist_id: Union[str, WatchlistType] + ) -> WatchlistMembersListV2: + """ + Get a list of all members of a watchlist. These actors may have been added as an included actor, or are members of an included department, etc. + + **Parameters**: + + * **watchlist_id**: `str`(required) - Watchlist ID. + + **Returns**: A [`WatchlistMembersListV2`][watchlistmemberslistv2-model] object. + """ + response = self._parent.session.get(f"{self._uri}/{watchlist_id}/members") + return WatchlistMembersListV2.parse_response(response) + + def add_included_actors(self, watchlist_id: str, actor_ids: Union[str, List[str]]): + """ + Include individual actors on a watchlist. + + **Parameters**: + + * **watchlist**: `str` (required) - Watchlist ID. + * **actor_ids**: `str`, `List[str]` (required) - List of unique actor IDs to include on the watchlist. A maximum + of 100 actors can be processed in a single request. + + **Returns**: A `requests.Response` indicating success. + """ + data = UpdateIncludedActorsRequest( + actorIds=actor_ids if isinstance(actor_ids, List) else [actor_ids], + watchlistId=watchlist_id, + ) + return self._parent.session.post( + url=f"{self._uri}/{watchlist_id}/included-actors/add", json=data.dict() + ) + + def remove_included_actors( + self, watchlist_id: str, actor_ids: Union[str, List[str]] + ): + """ + Remove included actors from a watchlist. + + **Parameters**: + + * **watchlist**: `str` (required) - Watchlist ID. + * **actor_ids**: `str`, `List[str]` (required) - List of unique actor IDs to remove from the watchlist. A maximum + of 100 actors can be processed in a single request. + + **Returns**: A `requests.Response` indicating success. + """ + + data = UpdateIncludedActorsRequest( + actorIds=actor_ids if isinstance(actor_ids, List) else [actor_ids], + watchlistId=watchlist_id, + ) + return self._parent.session.post( + url=f"{self._uri}/{watchlist_id}/included-actors/delete", json=data.dict() + ) + + def get_included_actor(self, watchlist_id: str, actor_id: str) -> WatchlistActor: + """ + Get an included actor from a watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + * **actor_id**: `str` (required) - Unique actor ID. + + **Returns**: A [`WatchlistActor`][watchlistactor-model] object. + """ + response = self._parent.session.get( + url=f"{self._uri}/{watchlist_id}/included-actors/{actor_id}" + ) + return WatchlistActor.parse_response(response) + + def list_included_actors(self, watchlist_id: str) -> IncludedActorsList: + """ + List individual actors included on a watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + + **Returns**: An [`IncludedActorsList`][includedactorslist-model] object. + """ + + response = self._parent.session.get( + url=f"{self._uri}/{watchlist_id}/included-actors" + ) + return IncludedActorsList.parse_response(response) + + def add_excluded_actors(self, watchlist_id: str, actor_ids: Union[str, List[str]]): + """ + Exclude individual actors from a watchlist. + + **Parameters**: + + * **watchlist**: `str` (required) - Watchlist ID. + * **actor_ids**: `str`, `List[str]` (required) - List of unique actor IDs to exclude from the watchlist. A maximum + of 100 actors can be processed in a single request. + + **Returns**: A `requests.Response` indicating success. + """ + data = UpdateExcludedActorsRequest( + actorIds=actor_ids if isinstance(actor_ids, List) else [actor_ids], + ) + return self._parent.session.post( + url=f"{self._uri}/{watchlist_id}/excluded-actors/add", json=data.dict() + ) + + def remove_excluded_actors( + self, watchlist_id: str, actor_ids: Union[str, List[str]] + ): + """ + Remove excluded actors from a watchlist. + + **Parameters**: + + * **watchlist**: `str` (required) - Watchlist ID. + * **actor_ids**: `str`, `List[str]` (required) - List of unique actor IDs to remove from the exclusion list. A + maximum of 100 actors can be processed in a single request. + + **Returns**: A `requests.Response` indicating success. + """ + data = UpdateExcludedActorsRequest( + actorIds=actor_ids if isinstance(actor_ids, List) else [actor_ids], + ) + return self._parent.session.post( + url=f"{self._uri}/{watchlist_id}/excluded-actors/delete", json=data.dict() + ) + + def list_excluded_actors(self, watchlist_id: str) -> ExcludedActorsList: + """ + List individual actors excluded from a watchlist. + + * **watchlist_id**: `str` (required) - Watchlist ID. + + **Returns**: An [`ExcludedActorsList`][excludedactorslist-model] object. + """ + response = self._parent.session.get( + f"{self._uri}/{watchlist_id}/excluded-actors" + ) + return ExcludedActorsList.parse_response(response) + + def get_excluded_actor(self, watchlist_id: str, actor_id: str) -> WatchlistActor: + """ + Get an excluded actor from a watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + * **actor_id**: `str` (required) - Unique actor ID. + + **Returns**: A [`WatchlistActor`][watchlistactor-model] object. + """ + response = self._parent.session.get( + f"{self._uri}/{watchlist_id}/excluded-actors/{actor_id}" + ) + return WatchlistActor.parse_response(response) + + def add_directory_groups(self, watchlist_id: str, group_ids: Union[str, List[str]]): + """ + Include directory groups on a watchlist. Use the `directory_groups` client to see available directory groups. + + **Parameters**: + + * **watchlist**: `str` (required) - Watchlist ID. + * **group_ids**: `str`, `List[str]` (required) - List of directory group IDs to include on the watchlist. + + **Returns**: A `requests.Response` indicating success. + """ + data = UpdateIncludedDirectoryGroupsRequest( + groupIds=group_ids if isinstance(group_ids, List) else [group_ids] + ) + return self._parent.session.post( + url=f"{self._uri}/{watchlist_id}/included-directory-groups/add", + json=data.dict(), + ) + + def remove_directory_groups( + self, watchlist_id: str, group_ids: Union[str, List[str]] + ): + """ + Remove included directory groups from a watchlist. + + **Parameters**: + + * **watchlist**: `str` (required) - Watchlist ID. + * **group_ids**: `str`, `List[str]` (required) - List of directory group IDs to remove from the watchlist. + + **Returns**: A `requests.Response` indicating success. + """ + data = UpdateIncludedDirectoryGroupsRequest( + groupIds=group_ids if isinstance(group_ids, List) else [group_ids] + ) + return self._parent.session.post( + url=f"{self._uri}/{watchlist_id}/included-directory-groups/delete", + json=data.dict(), + ) + + def list_directory_groups(self, watchlist_id: str) -> IncludedDirectoryGroupsList: + """ + List directory groups included on a watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + + **Returns**: An [`IncludedDirectoryGroupsList`][includeddirectorygroupslist-model] object. + """ + + response = self._parent.session.get( + f"{self._uri}/{watchlist_id}/included-directory-groups" + ) + return IncludedDirectoryGroupsList.parse_response(response) + + def get_directory_group( + self, watchlist_id: str, group_id: str + ) -> IncludedDirectoryGroup: + """ + Get an included directory group from a watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + * **group_id**: `str` (required) - Directory group ID. + + **Returns**: An [`IncludedDirectoryGroup`][includeddirectorygroup-model] object. + """ + + response = self._parent.session.get( + f"{self._uri}/{watchlist_id}/included-directory-groups/{group_id}" + ) + return IncludedDirectoryGroup.parse_response(response) + + def add_departments( + self, + watchlist_id: str, + departments: Union[str, List[str]], + ): + """ + Include departments on a watchlist. Use the `departments` client to see available departments. + + **Parameters**: + + * **watchlist**: `str` (required) - Watchlist ID. + * **departments**: `str`, `List[str]` (required) - List of departments to include on the watchlist. + + **Returns**: A `requests.Response` indicating success. + """ + data = UpdateIncludedDepartmentsRequest( + departments=departments if isinstance(departments, List) else [departments] + ) + return self._parent.session.post( + url=f"{self._uri}/{watchlist_id}/included-departments/add", json=data.dict() + ) + + def remove_departments( + self, + watchlist_id: str, + departments: Union[str, List[str]], + ): + """ + Remove included departments from a watchlist. + + **Parameters**: + + * **watchlist**: `str` - Watchlist ID or a watchlist type. An ID must be provided for custom watchlists. + * **departments**: `str`, `List[str]` (required) - List of departments to remove from the watchlist. + + **Returns**: A `requests.Response` indicating success. + """ + data = UpdateIncludedDepartmentsRequest( + departments=departments if isinstance(departments, List) else [departments] + ) + return self._parent.session.post( + url=f"{self._uri}/{watchlist_id}/included-departments/delete", + json=data.dict(), + ) + + def list_departments(self, watchlist_id: str) -> IncludedDepartmentsList: + """ + List departments included on a watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + + **Returns**: An [`IncludedDepartmentsList`][includeddepartmentslist-model] object. + """ + response = self._parent.session.get( + f"{self._uri}/{watchlist_id}/included-departments" + ) + return IncludedDepartmentsList.parse_response(response) + + def get_department(self, watchlist_id: str, department: str) -> IncludedDepartment: + """ + Get an included department from a watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + * **department**: `str` (required) - A included department. + + **Returns**: An [`IncludedDepartment`][includeddepartment-model] object. + """ + response = self._parent.session.get( + f"{self._uri}/{watchlist_id}/included-departments/{department}" + ) + return IncludedDepartment.parse_response(response) + + def get_id_by_name(self, name: Union[str, WatchlistType]): + """ + Get a watchlist ID by either its type (ex: `DEPARTING_EMPLOYEE`) or its title in the case of `CUSTOM` watchlists. + + **Parameters**: + + * **name**: `str`, [`WatchlistType`][watchlist-types] (required) - A `WatchlistType` or in the case of `CUSTOM` watchlists, the watchlist `title`. + + **Returns**: A watchlist ID (`str`). + """ + + def _lookup_ids(self): + """Map watchlist types to IDs, if they exist.""" + self._watchlist_type_id_map = {} + watchlists = self.get_page(page_size=100).watchlists + for item in watchlists: + if item.list_type == "CUSTOM": + # store title for custom lists instead of list_type + self._watchlist_type_id_map[item.title] = item.watchlist_id + self._watchlist_type_id_map[item.list_type] = item.watchlist_id + + watchlist_id = self._watchlist_type_id_map.get(name) + if not watchlist_id: + # if not found, reset ID cache + _lookup_ids(self) + watchlist_id = self._watchlist_type_id_map.get(name) + if not watchlist_id: + raise WatchlistNotFoundError(name) + return watchlist_id + class WatchlistsV1: """ Client for `/v1/watchlists` endpoints. + This client is deprecated. Use the WatchlistsV2 client instead. + Usage example: >>> import incydr @@ -68,6 +577,11 @@ def get_page( **Returns**: A [`WatchlistsPage`][watchlistspage-model] object. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) page_size = page_size or self._parent.settings.page_size data = ListWatchlistsRequest(page=page_num, pageSize=page_size, userId=user_id) response = self._parent.session.get(self._uri, params=data.dict()) @@ -83,6 +597,11 @@ def iter_all( **Returns**: A generator yielding individual [`Watchlist`][watchlist-model] objects. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) page_size = page_size or self._parent.settings.page_size for page_num in count(1): page = self.get_page( @@ -102,6 +621,11 @@ def get(self, watchlist_id: str) -> Watchlist: **Returns**: A [`Watchlist`][watchlist-model] object. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) response = self._parent.session.get(f"{self._uri}/{watchlist_id}") return Watchlist.parse_response(response) @@ -119,6 +643,11 @@ def create( **Returns**: A ['Watchlist`][watchlist-model] object. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) if watchlist_type == "CUSTOM": if title is None: raise ValueError("`title` value is required for custom watchlists.") @@ -141,6 +670,11 @@ def delete(self, watchlist_id: str): **Returns**: A `requests.Response` indicating success. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) return self._parent.session.delete(f"{self._uri}/{watchlist_id}") def update( @@ -157,6 +691,11 @@ def update( **Returns**: A [`Watchlist`][watchlist-model] object. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) paths = [] if title: paths += ["title"] @@ -180,6 +719,11 @@ def get_member(self, watchlist_id: str, user_id: str) -> WatchlistUser: **Returns**: A [`WatchlistUser`][watchlistuser-model] object. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) response = self._parent.session.get( f"{self._uri}/{watchlist_id}/members/{user_id}" ) @@ -197,6 +741,11 @@ def list_members( **Returns**: A [`WatchlistMembersList`][watchlistmemberslist-model] object. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) response = self._parent.session.get(f"{self._uri}/{watchlist_id}/members") return WatchlistMembersList.parse_response(response) @@ -212,6 +761,11 @@ def add_included_users(self, watchlist_id: str, user_ids: Union[str, List[str]]) **Returns**: A `requests.Response` indicating success. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) data = UpdateIncludedUsersRequest( userIds=user_ids if isinstance(user_ids, List) else [user_ids], watchlistId=watchlist_id, @@ -232,6 +786,11 @@ def remove_included_users(self, watchlist_id: str, user_ids: Union[str, List[str **Returns**: A `requests.Response` indicating success. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) data = UpdateIncludedUsersRequest( userIds=user_ids if isinstance(user_ids, List) else [user_ids], @@ -252,6 +811,11 @@ def get_included_user(self, watchlist_id: str, user_id: str) -> WatchlistUser: **Returns**: A [`WatchlistUser`][watchlistuser-model] object. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) response = self._parent.session.get( url=f"{self._uri}/{watchlist_id}/included-users/{user_id}" ) @@ -267,6 +831,11 @@ def list_included_users(self, watchlist_id: str) -> IncludedUsersList: **Returns**: An [`IncludedUsersList`][includeduserslist-model] object. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) response = self._parent.session.get( url=f"{self._uri}/{watchlist_id}/included-users" @@ -285,6 +854,11 @@ def add_excluded_users(self, watchlist_id: str, user_ids: Union[str, List[str]]) **Returns**: A `requests.Response` indicating success. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) data = UpdateExcludedUsersRequest( userIds=user_ids if isinstance(user_ids, List) else [user_ids], ) @@ -304,6 +878,11 @@ def remove_excluded_users(self, watchlist_id: str, user_ids: Union[str, List[str **Returns**: A `requests.Response` indicating success. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) data = UpdateExcludedUsersRequest( userIds=user_ids if isinstance(user_ids, List) else [user_ids], ) @@ -319,6 +898,11 @@ def list_excluded_users(self, watchlist_id: str) -> ExcludedUsersList: **Returns**: An [`ExcludedUsersList`][excludeduserslist-model] object. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) response = self._parent.session.get( f"{self._uri}/{watchlist_id}/excluded-users" ) @@ -335,6 +919,11 @@ def get_excluded_user(self, watchlist_id: str, user_id: str) -> WatchlistUser: **Returns**: A [`WatchlistUser`][watchlistuser-model] object. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) response = self._parent.session.get( f"{self._uri}/{watchlist_id}/excluded-users/{user_id}" ) @@ -351,6 +940,11 @@ def add_directory_groups(self, watchlist_id: str, group_ids: Union[str, List[str **Returns**: A `requests.Response` indicating success. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) data = UpdateIncludedDirectoryGroupsRequest( groupIds=group_ids if isinstance(group_ids, List) else [group_ids] ) @@ -372,6 +966,11 @@ def remove_directory_groups( **Returns**: A `requests.Response` indicating success. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) data = UpdateIncludedDirectoryGroupsRequest( groupIds=group_ids if isinstance(group_ids, List) else [group_ids] ) @@ -390,6 +989,11 @@ def list_directory_groups(self, watchlist_id: str) -> IncludedDirectoryGroupsLis **Returns**: An [`IncludedUsersList`][includeduserslist-model] object. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) response = self._parent.session.get( f"{self._uri}/{watchlist_id}/included-directory-groups" @@ -409,6 +1013,11 @@ def get_directory_group( **Returns**: An [`IncludedDirectoryGroup`][includeddirectorygroup-model] object. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) response = self._parent.session.get( f"{self._uri}/{watchlist_id}/included-directory-groups/{group_id}" @@ -430,6 +1039,11 @@ def add_departments( **Returns**: A `requests.Response` indicating success. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) data = UpdateIncludedDepartmentsRequest( departments=departments if isinstance(departments, List) else [departments] ) @@ -452,6 +1066,11 @@ def remove_departments( **Returns**: A `requests.Response` indicating success. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) data = UpdateIncludedDepartmentsRequest( departments=departments if isinstance(departments, List) else [departments] ) @@ -470,6 +1089,11 @@ def list_departments(self, watchlist_id: str) -> IncludedDepartmentsList: **Returns**: An [`IncludedDepartmentsList`][includeddepartmentslist-model] object. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) response = self._parent.session.get( f"{self._uri}/{watchlist_id}/included-departments" ) @@ -486,6 +1110,11 @@ def get_department(self, watchlist_id: str, department: str) -> IncludedDepartme **Returns**: An [`IncludedDepartment`][includeddepartment-model] object. """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) response = self._parent.session.get( f"{self._uri}/{watchlist_id}/included-departments/{department}" ) @@ -501,6 +1130,11 @@ def get_id_by_name(self, name: Union[str, WatchlistType]): **Returns**: A watchlist ID (`str`). """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) def _lookup_ids(self): """Map watchlist types to IDs, if they exist.""" diff --git a/src/_incydr_sdk/watchlists/models/requests.py b/src/_incydr_sdk/watchlists/models/requests.py index 6f48ff76..6a4336ee 100644 --- a/src/_incydr_sdk/watchlists/models/requests.py +++ b/src/_incydr_sdk/watchlists/models/requests.py @@ -17,6 +17,14 @@ class UpdateExcludedUsersRequest(BaseModel): ) +class UpdateExcludedActorsRequest(BaseModel): + actorIds: Optional[List[str]] = Field( + None, + description="A list of actor IDs to add or remove.", + max_items=100, + ) + + class UpdateIncludedDepartmentsRequest(BaseModel): departments: Optional[List[str]] = Field( None, description="A list of departments to add or remove." @@ -40,6 +48,17 @@ class UpdateIncludedUsersRequest(BaseModel): ) +class UpdateIncludedActorsRequest(BaseModel): + actorIds: Optional[List[str]] = Field( + None, + description="A list of actor IDs to add or remove.", + max_items=100, + ) + watchlistId: Optional[str] = Field( + None, description="A unique watchlist ID.", example="123" + ) + + class CreateWatchlistRequest(BaseModel): description: Optional[constr(max_length=250)] = Field( None, @@ -63,6 +82,12 @@ class ListWatchlistsRequest(BaseModel): userId: Optional[str] +class ListWatchlistsRequestV2(BaseModel): + page: int = 1 + pageSize: int = 100 + actorId: Optional[str] + + class UpdateWatchlistRequest(BaseModel): description: Optional[constr(max_length=250)] = Field( None, diff --git a/src/_incydr_sdk/watchlists/models/responses.py b/src/_incydr_sdk/watchlists/models/responses.py index a19c3028..e3e78371 100644 --- a/src/_incydr_sdk/watchlists/models/responses.py +++ b/src/_incydr_sdk/watchlists/models/responses.py @@ -82,6 +82,24 @@ class WatchlistUser(ResponseModel): username: Optional[str] = Field(None, example="foo@bar.com") +class WatchlistActor(ResponseModel): + """ + A model representing a user whose associated with a watchlist. + + **Fields**: + + * **added_time**: `datetime` - The time the user was associated with the watchlist. + * **actor_id**: `str` - Unique actor ID. + * **actorname**: `str - Actor name. + """ + + added_time: datetime = Field(None, alias="addedTime") + actor_id: Optional[str] = Field( + None, description="A unique actor ID.", example="23", alias="actorId" + ) + actorname: Optional[str] = Field(None, example="foo@bar.com") + + class ExcludedUsersList(ResponseModel): """ A model representing a list of users excluded from a watchlist. @@ -102,6 +120,26 @@ class ExcludedUsersList(ResponseModel): ) +class ExcludedActorsList(ResponseModel): + """ + A model representing a list of actors excluded from a watchlist. + Excluded actors are those that have been individually excluded from that list. + + **Fields**: + + * **excluded_actors**: `List[WatchlistActor]` - The list of excluded actors. + * **total_count**: `int` + """ + + excluded_users: Optional[List[WatchlistActor]] = Field(None, alias="excludedActors") + total_count: Optional[int] = Field( + None, + description="The total count of all excluded actors.", + example=10, + alias="totalCount", + ) + + class IncludedDepartmentsList(ResponseModel): """ A model representing a list of departments included on a watchlist. @@ -149,14 +187,34 @@ class IncludedUsersList(ResponseModel): A model representing a list of users included on a watchlist. Included users are those that have been individually included on that list. - * **included_users**: `List[WatchlistUser]` - The list of included users. - * **total_count**: `int` - The total count of all included users. + * **included_users**: `List[WatchlistUser]` - The list of included users or actors. + * **total_count**: `int` - The total count of all included users or actors. """ included_users: Optional[List[WatchlistUser]] = Field(None, alias="includedUsers") total_count: Optional[int] = Field( None, - description="The total count of all included users.", + description="The total count of all included users or actors.", + example=10, + alias="totalCount", + ) + + +class IncludedActorsList(ResponseModel): + """ + A model representing a list of actors included on a watchlist. + Included users are those that have been individually included on that list. + + * **included_actors**: `List[WatchlistActor]` - The list of included users or actors. + * **total_count**: `int` - The total count of all included users or actors. + """ + + included_actors: Optional[List[WatchlistActor]] = Field( + None, alias="includedActors" + ) + total_count: Optional[int] = Field( + None, + description="The total count of all included users or actors.", example=10, alias="totalCount", ) @@ -211,13 +269,13 @@ class WatchlistMembersList(ResponseModel): **Fields**: - * **watchlist_members**: `List[WatchlistUser]` - The list of watchlist members. + * **watchlist_members**: `Union[List[WatchlistUser], List[WatchlistActor]]` - The list of watchlist members. * **total_count**: `int` - Total count of members on the watchlist. """ total_count: Optional[int] = Field( None, - description="The total count of all included users..", + description="The total count of all included users or actors.", example=10, alias="totalCount", ) @@ -226,6 +284,30 @@ class WatchlistMembersList(ResponseModel): ) +class WatchlistMembersListV2(ResponseModel): + """ + A model representing a list of watchlist members. + Watchlist members are users who are on a list, whether it is because they are individually included, + or because they are part of a department or directory group that is included. + + + **Fields**: + + * **watchlist_members**: `Union[List[WatchlistUser], List[WatchlistActor]]` - The list of watchlist members. + * **total_count**: `int` - Total count of members on the watchlist. + """ + + total_count: Optional[int] = Field( + None, + description="The total count of all included users or actors.", + example=10, + alias="totalCount", + ) + watchlist_members: Optional[List[WatchlistActor]] = Field( + None, alias="watchlistMembers" + ) + + class Watchlist(ResponseModel): """ A model representing an Incydr Watchlist. diff --git a/tests/test_watchlists.py b/tests/test_watchlists.py index 7a3cd22f..8bb15d64 100644 --- a/tests/test_watchlists.py +++ b/tests/test_watchlists.py @@ -10,14 +10,18 @@ from _incydr_sdk.core.client import Client from _incydr_sdk.enums.watchlists import WatchlistType from _incydr_sdk.exceptions import WatchlistNotFoundError +from _incydr_sdk.watchlists.models.responses import ExcludedActorsList from _incydr_sdk.watchlists.models.responses import ExcludedUsersList +from _incydr_sdk.watchlists.models.responses import IncludedActorsList from _incydr_sdk.watchlists.models.responses import IncludedDepartment from _incydr_sdk.watchlists.models.responses import IncludedDepartmentsList from _incydr_sdk.watchlists.models.responses import IncludedDirectoryGroup from _incydr_sdk.watchlists.models.responses import IncludedDirectoryGroupsList from _incydr_sdk.watchlists.models.responses import IncludedUsersList from _incydr_sdk.watchlists.models.responses import Watchlist +from _incydr_sdk.watchlists.models.responses import WatchlistActor from _incydr_sdk.watchlists.models.responses import WatchlistMembersList +from _incydr_sdk.watchlists.models.responses import WatchlistMembersListV2 from _incydr_sdk.watchlists.models.responses import WatchlistsPage from _incydr_sdk.watchlists.models.responses import WatchlistUser from tests.conftest import TEST_TOKEN @@ -77,6 +81,18 @@ "username": "baz@bar.com", } +TEST_ACTOR_1 = { + "addedTime": "2022-07-18T16:39:51.356082Z", + "actorId": TEST_ID, + "actorname": "foo@bar.com", +} +TEST_ACTOR_2 = { + "addedTime": "2022-08-18T16:39:51.356082Z", + "actorId": "user-43", + "actorname": "baz@bar.com", +} + + TEST_DEPARTMENT_1 = {"addedTime": "2022-07-18T16:39:51.356082Z", "name": "Engineering"} TEST_DEPARTMENT_2 = {"addedTime": "2022-08-18T16:39:51.356082Z", "name": "Marketing"} @@ -112,6 +128,18 @@ def mock_create_custom(httpserver_auth: HTTPServer): ).respond_with_json(TEST_WATCHLIST_2) +@pytest.fixture +def mock_create_custom_v2(httpserver_auth: HTTPServer): + data = { + "description": "custom watchlist", + "title": "test", + "watchlistType": "CUSTOM", + } + httpserver_auth.expect_request( + "/v2/watchlists", method="POST", json=data + ).respond_with_json(TEST_WATCHLIST_2) + + @pytest.fixture def mock_create_departing_employee(httpserver_auth: HTTPServer): data = {"description": None, "title": None, "watchlistType": "DEPARTING_EMPLOYEE"} @@ -120,6 +148,14 @@ def mock_create_departing_employee(httpserver_auth: HTTPServer): ).respond_with_json(TEST_WATCHLIST_1) +@pytest.fixture +def mock_create_departing_employee_v2(httpserver_auth: HTTPServer): + data = {"description": None, "title": None, "watchlistType": "DEPARTING_EMPLOYEE"} + httpserver_auth.expect_request( + "/v2/watchlists", method="POST", json=data + ).respond_with_json(TEST_WATCHLIST_1) + + @pytest.fixture def mock_delete(httpserver_auth: HTTPServer): httpserver_auth.expect_request( @@ -127,6 +163,13 @@ def mock_delete(httpserver_auth: HTTPServer): ).respond_with_data() +@pytest.fixture +def mock_delete_v2(httpserver_auth: HTTPServer): + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}", method="DELETE" + ).respond_with_data() + + @pytest.fixture def mock_get_all(httpserver_auth: HTTPServer): data = {"watchlists": [TEST_WATCHLIST_1, TEST_WATCHLIST_2], "totalCount": 2} @@ -138,6 +181,17 @@ def mock_get_all(httpserver_auth: HTTPServer): ).respond_with_json(data) +@pytest.fixture +def mock_get_all_v2(httpserver_auth: HTTPServer): + data = {"watchlists": [TEST_WATCHLIST_1, TEST_WATCHLIST_2], "totalCount": 2} + + query = {"page": 1, "pageSize": 100} + + httpserver_auth.expect_request( + "/v2/watchlists", method="GET", query_string=urlencode(query) + ).respond_with_json(data) + + @pytest.fixture def mock_get_all_members(httpserver_auth: HTTPServer): data = {"watchlistMembers": [TEST_USER_1, TEST_USER_2], "totalCount": 2} @@ -146,6 +200,14 @@ def mock_get_all_members(httpserver_auth: HTTPServer): ).respond_with_json(data) +@pytest.fixture +def mock_get_all_members_v2(httpserver_auth: HTTPServer): + data = {"watchlistMembers": [TEST_ACTOR_1, TEST_ACTOR_2], "totalCount": 2} + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/members" + ).respond_with_json(data) + + @pytest.fixture def mock_get_all_included_users(httpserver_auth: HTTPServer): data = {"includedUsers": [TEST_USER_1, TEST_USER_2], "totalCount": 2} @@ -154,6 +216,14 @@ def mock_get_all_included_users(httpserver_auth: HTTPServer): ).respond_with_json(data) +@pytest.fixture +def mock_get_all_included_actors(httpserver_auth: HTTPServer): + data = {"includedActors": [TEST_ACTOR_1, TEST_ACTOR_2], "totalCount": 2} + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/included-actors" + ).respond_with_json(data) + + @pytest.fixture def mock_get_all_excluded_users(httpserver_auth: HTTPServer): data = {"excludedUsers": [TEST_USER_1, TEST_USER_2], "totalCount": 2} @@ -162,6 +232,14 @@ def mock_get_all_excluded_users(httpserver_auth: HTTPServer): ).respond_with_json(data) +@pytest.fixture +def mock_get_all_excluded_actors(httpserver_auth: HTTPServer): + data = {"excludedActors": [TEST_ACTOR_1, TEST_ACTOR_2], "totalCount": 2} + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/excluded-actors" + ).respond_with_json(data) + + @pytest.fixture def mock_get_all_departments(httpserver_auth: HTTPServer): data = { @@ -173,6 +251,17 @@ def mock_get_all_departments(httpserver_auth: HTTPServer): ).respond_with_json(data) +@pytest.fixture +def mock_get_all_departments_v2(httpserver_auth: HTTPServer): + data = { + "includedDepartments": [TEST_DEPARTMENT_1, TEST_DEPARTMENT_2], + "totalCount": 2, + } + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/included-departments" + ).respond_with_json(data) + + @pytest.fixture def mock_get_all_directory_groups(httpserver_auth: HTTPServer): data = {"includedDirectoryGroups": [TEST_GROUP_1, TEST_GROUP_2], "totalCount": 2} @@ -181,6 +270,14 @@ def mock_get_all_directory_groups(httpserver_auth: HTTPServer): ).respond_with_json(data) +@pytest.fixture +def mock_get_all_directory_groups_v2(httpserver_auth: HTTPServer): + data = {"includedDirectoryGroups": [TEST_GROUP_1, TEST_GROUP_2], "totalCount": 2} + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/included-directory-groups" + ).respond_with_json(data) + + @pytest.fixture def mock_get_member(httpserver_auth: HTTPServer): httpserver_auth.expect_request( @@ -188,6 +285,13 @@ def mock_get_member(httpserver_auth: HTTPServer): ).respond_with_json(TEST_USER_1) +@pytest.fixture +def mock_get_member_v2(httpserver_auth: HTTPServer): + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/members/{TEST_ID}", method="GET" + ).respond_with_json(TEST_ACTOR_1) + + @pytest.fixture def mock_get_included_user(httpserver_auth: HTTPServer): httpserver_auth.expect_request( @@ -195,6 +299,13 @@ def mock_get_included_user(httpserver_auth: HTTPServer): ).respond_with_json(TEST_USER_1) +@pytest.fixture +def mock_get_included_actor(httpserver_auth: HTTPServer): + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/included-actors/{TEST_ID}" + ).respond_with_json(TEST_ACTOR_1) + + @pytest.fixture def mock_get_excluded_user(httpserver_auth: HTTPServer): httpserver_auth.expect_request( @@ -202,6 +313,13 @@ def mock_get_excluded_user(httpserver_auth: HTTPServer): ).respond_with_json(TEST_USER_1) +@pytest.fixture +def mock_get_excluded_actor(httpserver_auth: HTTPServer): + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/excluded-actors/{TEST_ID}" + ).respond_with_json(TEST_ACTOR_1) + + @pytest.fixture def mock_get_department(httpserver_auth: HTTPServer): httpserver_auth.expect_request( @@ -209,6 +327,13 @@ def mock_get_department(httpserver_auth: HTTPServer): ).respond_with_json(TEST_DEPARTMENT_1) +@pytest.fixture +def mock_get_department_v2(httpserver_auth: HTTPServer): + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/included-departments/{TEST_ID}" + ).respond_with_json(TEST_DEPARTMENT_1) + + @pytest.fixture def mock_get_directory_group(httpserver_auth: HTTPServer): httpserver_auth.expect_request( @@ -216,6 +341,439 @@ def mock_get_directory_group(httpserver_auth: HTTPServer): ).respond_with_json(TEST_GROUP_1) +@pytest.fixture +def mock_get_directory_group_v2(httpserver_auth: HTTPServer): + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/included-directory-groups/{TEST_ID}" + ).respond_with_json(TEST_GROUP_1) + + +# ------ V2 ------ + + +def test_get_page_when_default_params_returns_expected_data_v2(mock_get_all_v2): + c = Client() + page = c.watchlists.v2.get_page() + assert isinstance(page, WatchlistsPage) + assert page.watchlists[0].json() == json.dumps(TEST_WATCHLIST_1) + assert page.watchlists[1].json() == json.dumps(TEST_WATCHLIST_2) + assert page.total_count == len(page.watchlists) == 2 + + +def test_get_page_when_custom_params_returns_expected_data_v2( + httpserver_auth: HTTPServer, +): + data = {"watchlists": [TEST_WATCHLIST_1, TEST_WATCHLIST_2], "totalCount": 2} + + query = {"page": 2, "pageSize": 42, "actorId": "user-42"} + + httpserver_auth.expect_request( + "/v2/watchlists", method="GET", query_string=urlencode(query) + ).respond_with_json(data) + + c = Client() + page = c.watchlists.v2.get_page(page_num=2, page_size=42, actor_id="user-42") + assert isinstance(page, WatchlistsPage) + assert page.watchlists[0].json() == json.dumps(TEST_WATCHLIST_1) + assert page.watchlists[1].json() == json.dumps(TEST_WATCHLIST_2) + assert page.total_count == len(page.watchlists) == 2 + + +def test_iter_all_when_default_params_returns_expected_data_v2( + httpserver_auth: HTTPServer, +): + query_1 = { + "page": 1, + "pageSize": 2, + } + query_2 = { + "page": 2, + "pageSize": 2, + } + + data_1 = {"watchlists": [TEST_WATCHLIST_1, TEST_WATCHLIST_2], "totalCount": 2} + data_2 = {"watchlists": [TEST_WATCHLIST_3], "totalCount": 1} + + httpserver_auth.expect_ordered_request( + "/v2/watchlists", method="GET", query_string=urlencode(query_1) + ).respond_with_json(data_1) + httpserver_auth.expect_ordered_request( + "/v2/watchlists", method="GET", query_string=urlencode(query_2) + ).respond_with_json(data_2) + + client = Client() + iterator = client.watchlists.v2.iter_all(page_size=2) + total = 0 + expected = [TEST_WATCHLIST_1, TEST_WATCHLIST_2, TEST_WATCHLIST_3] + for item in iterator: + total += 1 + assert isinstance(item, Watchlist) + assert item.json() == json.dumps(expected.pop(0)) + assert total == 3 + + +def test_get_returns_expected_data_v2(httpserver_auth: HTTPServer): + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}", method="GET" + ).respond_with_json(TEST_WATCHLIST_1) + + c = Client() + watchlist = c.watchlists.v2.get(TEST_WATCHLIST_ID) + assert isinstance(watchlist, Watchlist) + assert watchlist.watchlist_id == TEST_WATCHLIST_ID + assert watchlist.json() == json.dumps(TEST_WATCHLIST_1) + + +def test_create_when_required_params_returns_expected_data_v2( + mock_create_departing_employee_v2, +): + c = Client() + watchlist = c.watchlists.v2.create(WatchlistType.DEPARTING_EMPLOYEE) + assert isinstance(watchlist, Watchlist) + assert watchlist.json() == json.dumps(TEST_WATCHLIST_1) + + +def test_create_when_all_params_returns_expected_data_v2(mock_create_custom_v2): + c = Client() + watchlist = c.watchlists.v2.create( + "CUSTOM", title="test", description="custom watchlist" + ) + assert isinstance(watchlist, Watchlist) + assert watchlist.json() == json.dumps(TEST_WATCHLIST_2) + + +def test_create_when_custom_and_no_title_raises_error_v2(httpserver_auth: HTTPServer): + c = Client() + with pytest.raises(ValueError) as err: + c.watchlists.v2.create("CUSTOM") + assert err.value.args[0] == "`title` value is required for custom watchlists." + + +def test_delete_returns_expected_data_v2(mock_delete_v2): + c = Client() + assert c.watchlists.v2.delete(TEST_WATCHLIST_ID).status_code == 200 + + +def test_update_when_all_params_returns_expected_data_v2(httpserver_auth: HTTPServer): + query = {"paths": ["title", "description"]} + data = {"description": "updated description", "title": "updated title"} + watchlist = TEST_WATCHLIST_2.copy() + watchlist["title"] = "updated title" + watchlist["description"] = "updated description" + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}", + method="PATCH", + query_string=urlencode(query, doseq=True), + json=data, + ).respond_with_json(watchlist) + + c = Client() + response = c.watchlists.v2.update( + TEST_WATCHLIST_ID, title="updated title", description="updated description" + ) + assert isinstance(response, Watchlist) + assert response.json() == json.dumps(watchlist) + + +def test_update_when_one_param_returns_expected_data_v2(httpserver_auth: HTTPServer): + query = {"paths": ["title"]} + data = {"title": "updated title", "description": None} + watchlist = TEST_WATCHLIST_2.copy() + watchlist["title"] = "updated title" + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}", + method="PATCH", + query_string=urlencode(query, doseq=True), + json=data, + ).respond_with_json(watchlist) + + c = Client() + response = c.watchlists.v2.update(TEST_WATCHLIST_ID, title="updated title") + assert isinstance(response, Watchlist) + assert response.json() == json.dumps(watchlist) + + +def test_get_member_returns_expected_data_v2(mock_get_member_v2): + c = Client() + member = c.watchlists.v2.get_member(TEST_WATCHLIST_ID, TEST_ID) + assert isinstance(member, WatchlistActor) + assert member.actor_id == TEST_ID + assert member.actorname == "foo@bar.com" + assert member.added_time == datetime.datetime.fromisoformat( + TEST_ACTOR_1["addedTime"].replace("Z", "+00:00") + ) + assert member.json() == json.dumps(TEST_ACTOR_1) + + +def test_list_members_returns_expected_data_v2(mock_get_all_members_v2): + c = Client() + members = c.watchlists.v2.list_members(TEST_WATCHLIST_ID) + assert isinstance(members, WatchlistMembersListV2) + assert isinstance(members.watchlist_members[0], WatchlistActor) + assert members.watchlist_members[0].json() == json.dumps(TEST_ACTOR_1) + assert members.watchlist_members[1].json() == json.dumps(TEST_ACTOR_2) + assert members.total_count == len(members.watchlist_members) == 2 + + +@valid_ids_param +def test_add_included_users_returns_expected_data_v2( + httpserver_auth: HTTPServer, input, expected +): + data = {"actorIds": expected, "watchlistId": TEST_WATCHLIST_ID} + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/included-actors/add", json=data + ).respond_with_data() + c = Client() + assert ( + c.watchlists.v2.add_included_actors(TEST_WATCHLIST_ID, input).status_code == 200 + ) + + +def test_add_included_users_raises_validation_error_when_more_than_100_users_passed_v2( + httpserver_auth: HTTPServer, +): + c = Client() + with pytest.raises(pydantic.ValidationError): + c.watchlists.v2.add_included_actors( + watchlist_id=TEST_WATCHLIST_ID, actor_ids=list(range(101)) + ) + + +@valid_ids_param +def test_remove_included_users_returns_expected_data_v2( + httpserver_auth: HTTPServer, input, expected +): + data = {"actorIds": expected, "watchlistId": TEST_WATCHLIST_ID} + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/included-actors/delete", json=data + ).respond_with_data() + c = Client() + assert ( + c.watchlists.v2.remove_included_actors(TEST_WATCHLIST_ID, input).status_code + == 200 + ) + + +def test_remove_included_users_raises_validation_error_when_more_than_100_users_passed_v2( + httpserver_auth: HTTPServer, +): + c = Client() + with pytest.raises(pydantic.ValidationError): + c.watchlists.v2.remove_included_actors( + watchlist_id=TEST_WATCHLIST_ID, actor_ids=list(range(101)) + ) + + +def test_get_included_user_returns_expected_data_v2(mock_get_included_actor): + c = Client() + user = c.watchlists.v2.get_included_actor(TEST_WATCHLIST_ID, TEST_ID) + assert isinstance(user, WatchlistActor) + assert user.actor_id == TEST_ID + assert user.actorname == "foo@bar.com" + assert user.added_time == datetime.datetime.fromisoformat( + TEST_ACTOR_1["addedTime"].replace("Z", "+00:00") + ) + assert user.json() == json.dumps(TEST_ACTOR_1) + + +def test_list_included_users_returns_expected_data_v2(mock_get_all_included_actors): + c = Client() + users = c.watchlists.v2.list_included_actors(TEST_WATCHLIST_ID) + assert isinstance(users, IncludedActorsList) + assert users.included_actors[0].json() == json.dumps(TEST_ACTOR_1) + assert users.included_actors[1].json() == json.dumps(TEST_ACTOR_2) + assert users.total_count == len(users.included_actors) == 2 + + +@valid_ids_param +def test_add_excluded_users_returns_expected_data_v2( + httpserver_auth: HTTPServer, input, expected +): + data = {"actorIds": expected} + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/excluded-actors/add", json=data + ).respond_with_data() + c = Client() + assert ( + c.watchlists.v2.add_excluded_actors(TEST_WATCHLIST_ID, input).status_code == 200 + ) + + +@valid_ids_param +def test_remove_excluded_users_returns_expected_data_v2( + httpserver_auth: HTTPServer, input, expected +): + data = {"actorIds": expected} + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/excluded-actors/delete", json=data + ).respond_with_data() + c = Client() + assert ( + c.watchlists.v2.remove_excluded_actors(TEST_WATCHLIST_ID, input).status_code + == 200 + ) + + +def test_get_excluded_user_returns_expected_data_v2(mock_get_excluded_actor): + c = Client() + user = c.watchlists.v2.get_excluded_actor(TEST_WATCHLIST_ID, TEST_ID) + assert isinstance(user, WatchlistActor) + assert user.actor_id == TEST_ID + assert user.actorname == "foo@bar.com" + assert user.added_time == datetime.datetime.fromisoformat( + TEST_ACTOR_1["addedTime"].replace("Z", "+00:00") + ) + assert user.json() == json.dumps(TEST_ACTOR_1) + + +def test_list_excluded_users_returns_expected_data_v2(mock_get_all_excluded_actors): + c = Client() + users = c.watchlists.v2.list_excluded_actors(TEST_WATCHLIST_ID) + assert isinstance(users, ExcludedActorsList) + assert users.excluded_users[0].json() == json.dumps(TEST_ACTOR_1) + assert users.excluded_users[1].json() == json.dumps(TEST_ACTOR_2) + assert users.total_count == len(users.excluded_users) == 2 + + +@valid_ids_param +def test_add_directory_groups_returns_expected_data_v2( + httpserver_auth: HTTPServer, input, expected +): + data = {"groupIds": expected} + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/included-directory-groups/add", json=data + ).respond_with_data() + c = Client() + assert ( + c.watchlists.v2.add_directory_groups(TEST_WATCHLIST_ID, input).status_code + == 200 + ) + + +@valid_ids_param +def test_remove_directory_groups_returns_expected_data_v2( + httpserver_auth: HTTPServer, input, expected +): + data = {"groupIds": expected} + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/included-directory-groups/add", json=data + ).respond_with_data() + c = Client() + assert ( + c.watchlists.v2.add_directory_groups(TEST_WATCHLIST_ID, input).status_code + == 200 + ) + + +def test_list_included_directory_groups_returns_expected_data_v2( + mock_get_all_directory_groups_v2, +): + c = Client() + groups = c.watchlists.v2.list_directory_groups(TEST_WATCHLIST_ID) + assert isinstance(groups, IncludedDirectoryGroupsList) + assert groups.included_directory_groups[0].json() == json.dumps(TEST_GROUP_1) + assert groups.included_directory_groups[1].json() == json.dumps(TEST_GROUP_2) + assert groups.total_count == len(groups.included_directory_groups) == 2 + + +def test_get_directory_group_returns_expected_data_v2(mock_get_directory_group_v2): + c = Client() + group = c.watchlists.v2.get_directory_group(TEST_WATCHLIST_ID, TEST_ID) + assert isinstance(group, IncludedDirectoryGroup) + assert group.group_id == TEST_ID + assert group.name == "Sales" + assert group.added_time == datetime.datetime.fromisoformat( + TEST_GROUP_1["addedTime"].replace("Z", "+00:00") + ) + assert group.json() == json.dumps(TEST_GROUP_1) + + +@valid_ids_param +def test_add_departments_returns_expected_data_v2( + httpserver_auth: HTTPServer, input, expected +): + data = {"departments": expected} + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/included-departments/add", json=data + ).respond_with_data() + c = Client() + assert c.watchlists.v2.add_departments(TEST_WATCHLIST_ID, input).status_code == 200 + + +@valid_ids_param +def test_remove_departments_returns_expected_data_v2( + httpserver_auth: HTTPServer, input, expected +): + data = {"departments": expected} + httpserver_auth.expect_request( + f"/v2/watchlists/{TEST_WATCHLIST_ID}/included-departments/delete", json=data + ).respond_with_data() + c = Client() + assert ( + c.watchlists.v2.remove_departments(TEST_WATCHLIST_ID, input).status_code == 200 + ) + + +def test_list_included_departments_returns_expected_data_v2( + mock_get_all_departments_v2, +): + c = Client() + departments = c.watchlists.v2.list_departments(TEST_WATCHLIST_ID) + assert isinstance(departments, IncludedDepartmentsList) + assert departments.included_departments[0].json() == json.dumps(TEST_DEPARTMENT_1) + assert departments.included_departments[1].json() == json.dumps(TEST_DEPARTMENT_2) + assert departments.total_count == len(departments.included_departments) == 2 + + +def test_get_department_returns_expected_data_v2(mock_get_department_v2): + c = Client() + department = c.watchlists.v2.get_department(TEST_WATCHLIST_ID, TEST_ID) + assert isinstance(department, IncludedDepartment) + assert department.name == "Engineering" + assert department.added_time == datetime.datetime.fromisoformat( + TEST_DEPARTMENT_1["addedTime"].replace("Z", "+00:00") + ) + assert department.json() == json.dumps(TEST_DEPARTMENT_1) + + +@pytest.mark.parametrize( + ("name", "expected"), + [ + ("DEPARTING_EMPLOYEE", TEST_WATCHLIST_ID), + (WatchlistType.DEPARTING_EMPLOYEE, TEST_WATCHLIST_ID), + ("test", "1-watchlist-43"), + ], +) +def test_get_id_by_name_returns_id_v2(httpserver_auth: HTTPServer, name, expected): + data = {"watchlists": [TEST_WATCHLIST_1, TEST_WATCHLIST_2], "totalCount": 2} + query = {"page": 1, "pageSize": 100} + httpserver_auth.expect_request( + "/v2/watchlists", method="GET", query_string=urlencode(query) + ).respond_with_json(data) + + c = Client() + actual = c.watchlists.v2.get_id_by_name(name) + assert expected == actual + + +def test_get_id_by_name_when_no_id_raises_error_v2(httpserver_auth: HTTPServer): + data = {"watchlists": [], "totalCount": 0} + query = {"page": 1, "pageSize": 100} + httpserver_auth.expect_request( + "/v2/watchlists", method="GET", query_string=urlencode(query) + ).respond_with_json(data) + + c = Client() + with pytest.raises(WatchlistNotFoundError) as err: + c.watchlists.v2.get_id_by_name("name") + assert ( + "No watchlist matching the type or title 'name' was found." in err.value.args[0] + ) + + +# ------ V1 ------ + + def test_get_page_when_default_params_returns_expected_data(mock_get_all): c = Client() page = c.watchlists.v1.get_page() From 51f6694a93c3983d06ac27cf02f46d6269c715e6 Mon Sep 17 00:00:00 2001 From: Cecilia Stevens <63068179+ceciliastevens@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:05:43 -0400 Subject: [PATCH 02/11] update watchlists cli for v2 --- src/_incydr_cli/cmds/options/utils.py | 10 + src/_incydr_cli/cmds/utils.py | 18 + src/_incydr_cli/cmds/watchlists.py | 376 ++++++++++++------ .../watchlists/models/responses.py | 4 +- tests/test_watchlists.py | 78 ++-- 5 files changed, 322 insertions(+), 164 deletions(-) diff --git a/src/_incydr_cli/cmds/options/utils.py b/src/_incydr_cli/cmds/options/utils.py index 780301de..4b673a87 100644 --- a/src/_incydr_cli/cmds/options/utils.py +++ b/src/_incydr_cli/cmds/options/utils.py @@ -1,5 +1,6 @@ import click +from _incydr_cli.cmds.utils import actor_lookup from _incydr_cli.cmds.utils import user_lookup from _incydr_sdk.core.client import Client @@ -21,3 +22,12 @@ def user_lookup_callback(ctx, param, value): if "@" in str(value): return user_lookup(Client(), value) return value + + +def actor_lookup_callback(ctx, param, value): + if not value: + return + # only call user_lookup if username to prevent unnecessary client inits with obj() + if "@" in str(value): + return actor_lookup(Client(), value) + return value diff --git a/src/_incydr_cli/cmds/utils.py b/src/_incydr_cli/cmds/utils.py index 06313353..40e9b99e 100644 --- a/src/_incydr_cli/cmds/utils.py +++ b/src/_incydr_cli/cmds/utils.py @@ -8,6 +8,8 @@ from click import echo from click import style +from _incydr_sdk.actors.client import ActorNotFoundError + def deprecation_warning(text): echo(style(text, fg="red"), err=True) @@ -29,6 +31,22 @@ def user_lookup(client, value): return value +def actor_lookup(client, value): + """ + Returns the actor ID for a given actor name, or returns the value unchanged if not a username. + + Used with the `actor_lookup_callback` method on user args. + """ + if "@" in str(value): + # assume username/email was passed + try: + return client.actors.v1.get_actor_by_name(value).actor_id + except ActorNotFoundError: + raise ValueError(f"User with username '{value}' not found.") + # else return ID + return value + + class warn_interrupt: """A context decorator class used to wrap functions where a keyboard interrupt could potentially leave things in a bad state. Warns the user with provided message and exits when wrapped diff --git a/src/_incydr_cli/cmds/watchlists.py b/src/_incydr_cli/cmds/watchlists.py index 2365a621..0e187717 100644 --- a/src/_incydr_cli/cmds/watchlists.py +++ b/src/_incydr_cli/cmds/watchlists.py @@ -15,8 +15,10 @@ from _incydr_cli.cmds.options.output_options import columns_option from _incydr_cli.cmds.options.output_options import table_format_option from _incydr_cli.cmds.options.output_options import TableFormat +from _incydr_cli.cmds.options.utils import actor_lookup_callback from _incydr_cli.cmds.options.utils import user_lookup_callback -from _incydr_cli.cmds.utils import user_lookup +from _incydr_cli.cmds.utils import actor_lookup +from _incydr_cli.cmds.utils import deprecation_warning from _incydr_cli.core import incompatible_with from _incydr_cli.core import IncydrCommand from _incydr_cli.core import IncydrGroup @@ -28,6 +30,7 @@ from _incydr_sdk.watchlists.models.responses import IncludedDepartment from _incydr_sdk.watchlists.models.responses import IncludedDirectoryGroup from _incydr_sdk.watchlists.models.responses import Watchlist +from _incydr_sdk.watchlists.models.responses import WatchlistActor from _incydr_sdk.watchlists.models.responses import WatchlistUser MAX_USER_DISPLAY_COUNT = 25 @@ -51,7 +54,7 @@ def get_watchlist_id_callback(ctx, param, value): except ValueError: # if not an ID value client = Client() - return client.watchlists.v1.get_id_by_name(value) + return client.watchlists.v2.get_id_by_name(value) @click.group(cls=IncydrGroup) @@ -84,16 +87,23 @@ def watchlists(): @watchlists.command("list", cls=IncydrCommand) +@click.option( + "--actor", + default=None, + help="Filter by watchlists where the actor is a member. Accepts an actor ID or actor name. Performs an additional lookup if an actor name is passed", + callback=actor_lookup_callback, +) @click.option( "--user", default=None, - help="Filter by watchlists where the user is a member. Accepts a user ID or a username. Performs an additional lookup if a username is passed", + help="DEPRECATED. Use Actor instead. Filter by watchlists where the user is a member. Accepts a user ID or a username. Performs an additional lookup if a username is passed", callback=user_lookup_callback, ) @table_format_option @columns_option @logging_options def list_( + actor: str = None, user: str = None, format_: TableFormat = None, columns: str = None, @@ -101,8 +111,10 @@ def list_( """ List watchlists. """ + if user and not actor: + actor = user client = Client() - watchlists = client.watchlists.v1.iter_all(user_id=user) + watchlists = client.watchlists.v2.iter_all(actor_id=actor) _output_results(watchlists, Watchlist, format_, columns) @@ -120,41 +132,45 @@ def show( If using `rich`, outputs a summary of watchlist information and membership. This includes the following: - * included_users - * excluded_users + * included_actors + * excluded_actors * included_departments * included_directory_groups - Lists of users will be truncated to only display the first 25 members, use the `list-included-users` and `list-excluded-users` + Lists of actors will be truncated to only display the first 25 members, use the `list-included-actors` and `list-excluded-actors` commands respectively to see more details. If not using `rich`, outputs watchlist information in JSON without additional membership summary information. """ client = Client() - watchlist_response = client.watchlists.v1.get(watchlist) + watchlist_response = client.watchlists.v2.get(watchlist) if not client.settings.use_rich: click.echo(watchlist_response.json()) return - included_users = client.watchlists.v1.list_included_users(watchlist).included_users - excluded_users = client.watchlists.v1.list_excluded_users(watchlist).excluded_users - departments = client.watchlists.v1.list_departments(watchlist).included_departments - dir_groups = client.watchlists.v1.list_directory_groups( + included_actors = client.watchlists.v2.list_included_actors( + watchlist + ).included_actors + excluded_actors = client.watchlists.v2.list_excluded_actors( + watchlist + ).excluded_actors + departments = client.watchlists.v2.list_departments(watchlist).included_departments + dir_groups = client.watchlists.v2.list_directory_groups( watchlist ).included_directory_groups t = Table(title=f"{watchlist_response.list_type} Watchlist") t.add_column("Stats") tables = [] - if included_users: + if included_actors: tables.append( - models_as_table(WatchlistUser, included_users[:MAX_USER_DISPLAY_COUNT]) + models_as_table(WatchlistActor, included_actors[:MAX_USER_DISPLAY_COUNT]) ) t.add_column("Included Users") - if excluded_users: + if excluded_actors: tables.append( - models_as_table(WatchlistUser, excluded_users[:MAX_USER_DISPLAY_COUNT]) + models_as_table(WatchlistActor, excluded_actors[:MAX_USER_DISPLAY_COUNT]) ) t.add_column("Excluded Users") if departments: @@ -201,7 +217,7 @@ def create( The `--title` (required) and `--description` (optional) options are exclusively for creating CUSTOM watchlists. """ client = Client() - watchlist = client.watchlists.v1.create(watchlist_type, title, description) + watchlist = client.watchlists.v2.create(watchlist_type, title, description) console.print( f"Successfully created {watchlist.list_type} watchlist with ID: '{watchlist.watchlist_id}'." ) @@ -220,7 +236,7 @@ def delete( `CUSTOM` watchlists must be specified by title or ID. """ client = Client() - client.watchlists.v1.delete(watchlist) + client.watchlists.v2.delete(watchlist) console.print(f"Successfully deleted watchlist with ID: '{watchlist}'.") @@ -249,29 +265,29 @@ def update( description = "" client = Client() - client.watchlists.v1.update(watchlist_id, title, description) + client.watchlists.v2.update(watchlist_id, title, description) console.print(f"Successfully updated watchlist with ID: '{watchlist_id}'.") @watchlists.command(cls=IncydrCommand) @watchlist_arg @click.option( - "--users", + "--actors", default=None, type=FileOrString(), - help="List of user IDs or usernames to include on the watchlist. " - "An additional lookup is performed if a username is passed. Argument can be " - "passed as a comma-delimited string or from a CSV file with a single 'user' " - "column if prefixed with '@', e.g. '--users @users.csv'.", + help="List of actor IDs or actor names to include on the watchlist. " + "An additional lookup is performed if an actor name is passed. Argument can be " + "passed as a comma-delimited string or from a CSV file with a single 'actor' " + "column if prefixed with '@', e.g. '--actors @actors.csv'.", ) @click.option( - "--excluded-users", + "--excluded-actors", default=None, type=FileOrString(), - help="List of user IDs or usernames to exclude from the watchlist. " - "An additional lookup is performed if a username is passed. Argument can be " - "passed as a comma-delimited string or from a CSV file with a single 'user' " - "column if prefixed with '@', e.g. '--users @users.csv'.", + help="List of actor IDs or actor names to exclude from the watchlist. " + "An additional lookup is performed if an actor name is passed. Argument can be " + "passed as a comma-delimited string or from a CSV file with a single 'actor' " + "column if prefixed with '@', e.g. '--excluded-actors @actors.csv'.", ) @click.option( "--departments", @@ -287,61 +303,86 @@ def update( "Individual users from the directory groups will be added as watchlist members, where group information comes " "from SCIM or User Directory Sync.", ) +@click.option( + "--users", + default=None, + type=FileOrString(), + help="DEPRECATED. Use --actors instead. List of user IDs or usernames to include on the watchlist. " + "An additional lookup is performed if a username is passed. Argument can be " + "passed as a comma-delimited string or from a CSV file with a single 'user' " + "column if prefixed with '@', e.g. '--users @users.csv'.", +) +@click.option( + "--excluded-users", + default=None, + type=FileOrString(), + help="DEPRECATED. Use --excluded-actors instead. List of user IDs or usernames to exclude from the watchlist. " + "An additional lookup is performed if a username is passed. Argument can be " + "passed as a comma-delimited string or from a CSV file with a single 'user' " + "column if prefixed with '@', e.g. '--users @users.csv'.", +) @input_format_option @logging_options def add( watchlist: str, - users=None, - excluded_users=None, + actors=None, + excluded_actors=None, departments=None, directory_groups=None, + users=None, + excluded_users=None, format_=None, ): """ - Manage watchlist membership by including or excluding individual users and/or groups. + Manage watchlist membership by including or excluding individual actors and/or groups. Add any of the following members to a watchlist with the corresponding options: - * users - * excluded-users + * actors + * excluded-actors * departments * directory-groups WATCHLIST can be specified by watchlist type (ex: `DEPARTING_EMPLOYEE`) or ID. `CUSTOM` watchlists must be specified by title or ID. - If adding or excluding more than 100 users in a single run, the CLI will automatically batch + If adding or excluding more than 100 actors in a single run, the CLI will automatically batch requests due to a limit of 100 per request on the backend. """ client = Client() - # Add included users - if users: - user_ids, errors = _get_user_ids(client, users, format_=format_) + if users and not actors: + actors = users + if excluded_users and not excluded_actors: + excluded_actors = excluded_users + + # Add included actors + if actors: + actor_ids, errors = _get_actor_ids(client, actors, format_=format_) succeeded = 0 - for chunk in chunked(user_ids, size=100): + for chunk in chunked(actor_ids, size=100): try: - client.watchlists.v1.add_included_users(watchlist, chunk) + client.watchlists.v2.add_included_actors(watchlist, chunk) console.print( - f"Successfully included {len(chunk)} users on watchlist with ID: '{watchlist}'" + f"Successfully included {len(chunk)} actors on watchlist with ID: '{watchlist}'" ) succeeded += len(chunk) except requests.HTTPError as err: - if "User not found" in err.response.text: + if "Actor not found" in err.response.text: console.print( - "Problem processing batch of users, will attempt each individually." + "Problem processing batch of actors, will attempt each individually." ) chunk_succeeded = 0 - for user in chunk: + for actor in chunk: try: - client.watchlists.v1.add_included_users(watchlist, user) + client.watchlists.v2.add_included_actors(watchlist, actor) succeeded += 1 chunk_succeeded += 1 except requests.HTTPError as err: client.settings.logger.error( - f"Problem adding userId={user} to watchlist={watchlist}: {err.response.text}" + f"Problem adding actorId={actor} to watchlist={watchlist}: {err.response.text}" ) - errors.append(user) + errors.append(actor) console.print( f"Successfully included {chunk_succeeded} users on watchlist with ID: '{watchlist}'" ) @@ -351,45 +392,45 @@ def add( console.print("[red]The following usernames/user IDs were not found:") console.print("\t" + "\n\t".join(errors)) - # Add excluded users - if excluded_users: + # Add excluded actors + if excluded_actors: succeeded = 0 - user_ids, errors = _get_user_ids(client, excluded_users, format_=format_) - for chunk in chunked(user_ids, size=100): + actor_ids, errors = _get_actor_ids(client, excluded_actors, format_=format_) + for chunk in chunked(actor_ids, size=100): try: - client.watchlists.v1.add_excluded_users(watchlist, chunk) + client.watchlists.v2.add_excluded_actors(watchlist, chunk) console.print( - f"Successfully excluded {len(chunk)} users from watchlist with ID: '{watchlist}'" + f"Successfully excluded {len(chunk)} actors from watchlist with ID: '{watchlist}'" ) succeeded += len(chunk) except requests.HTTPError as err: - if "User not found" in err.response.text: + if "Actor not found" in err.response.text: console.print( - "Problem processing batch of users, will attempt each individually." + "Problem processing batch of actors, will attempt each individually." ) chunk_succeeded = 0 - for user in chunk: + for actor in chunk: try: - client.watchlists.v1.add_excluded_users(watchlist, user) + client.watchlists.v2.add_excluded_actors(watchlist, actor) succeeded += 1 chunk_succeeded += 1 except requests.HTTPError as err: client.settings.logger.error( - f"Problem excluding userId={user} from watchlist={watchlist}: {err.response.text}" + f"Problem excluding actorId={actor} from watchlist={watchlist}: {err.response.text}" ) - errors.append(user) + errors.append(actor) console.print( f"Successfully excluded {chunk_succeeded} users from watchlist with ID: '{watchlist}'" ) else: raise err if errors: - console.print("[red]The following usernames/user IDs were not found:") + console.print("[red]The following actornames/actor IDs were not found:") console.print("\t" + "\n\t".join(errors)) # Add departments if departments: - client.watchlists.v1.add_departments( + client.watchlists.v2.add_departments( watchlist, [i.strip() for i in departments.split(",")] ) console.print( @@ -398,7 +439,7 @@ def add( # Add directory groups if directory_groups: - client.watchlists.v1.add_directory_groups( + client.watchlists.v2.add_directory_groups( watchlist, [i.strip() for i in directory_groups.split(",")] ) console.print( @@ -409,23 +450,25 @@ def add( @watchlists.command(cls=IncydrCommand) @watchlist_arg @click.option( - "--users", + "--actors", default=None, type=FileOrString(), - help="List of included user IDs or usernames to remove from the watchlist. " - "An additional lookup is performed if a username is passed.Argument can be " - "passed as a comma-delimited string or as a file if prefixed with '@', e.g. '--users @users.csv'. " - "File should have a single 'user' field. File format can either be CSV or JSON Lines format, " + help="List of actor IDs or actor names to remove from the watchlist. " + "An additional lookup is performed if an actor name is passed. Argument can be " + "passed as a comma-delimited string or from a CSV file with a single 'actor' " + "column if prefixed with '@', e.g. '--actors @actors.csv'. " + "File should have a single 'actor' field. File format can either be CSV or JSON Lines format, " "as specified with the --format option (Default is CSV).", ) @click.option( - "--excluded-users", + "--excluded-actors", default=None, type=FileOrString(), - help="List of excluded user IDs or usernames to remove from the watchlist. " - "An additional lookup is performed if a username is passed. Argument can be " - "passed as a comma-delimited string or as a file if prefixed with '@', e.g. '--users @users.csv'. " - "File should have a single 'user' field. File format can either be CSV or JSON Lines format, " + help="List of actor IDs or actor names to remove from the watchlist. " + "An additional lookup is performed if an actor name is passed. Argument can be " + "passed as a comma-delimited string or from a CSV file with a single 'actor' " + "column if prefixed with '@', e.g. '--excluded-actors @actors.csv'. " + "File should have a single 'actor' field. File format can either be CSV or JSON Lines format, " "as specified with the --format option (Default is CSV).", ) @click.option( @@ -442,14 +485,36 @@ def add( "Individual users from the directory groups will be added as watchlist members, where group information comes " "from SCIM or User Directory Sync.", ) +@click.option( + "--users", + default=None, + type=FileOrString(), + help="DEPRECATED. Use --actors instead. List of included user IDs or usernames to remove from the watchlist. " + "An additional lookup is performed if a username is passed.Argument can be " + "passed as a comma-delimited string or as a file if prefixed with '@', e.g. '--users @users.csv'. " + "File should have a single 'user' field. File format can either be CSV or JSON Lines format, " + "as specified with the --format option (Default is CSV).", +) +@click.option( + "--excluded-users", + default=None, + type=FileOrString(), + help="DEPRECATED. Use --excluded-actors instead. List of excluded user IDs or usernames to remove from the watchlist. " + "An additional lookup is performed if a username is passed. Argument can be " + "passed as a comma-delimited string or as a file if prefixed with '@', e.g. '--users @users.csv'. " + "File should have a single 'user' field. File format can either be CSV or JSON Lines format, " + "as specified with the --format option (Default is CSV).", +) @input_format_option @logging_options def remove( watchlist: str, - users=None, - excluded_users=None, + actors=None, + excluded_actors=None, departments=None, directory_groups=None, + users=None, + excluded_users=None, format_=None, ): """ @@ -470,81 +535,90 @@ def remove( """ client = Client() + if users and not actors: + actors = users + if excluded_users and not excluded_actors: + excluded_actors = excluded_users + # Remove included users - if users: - user_ids, errors = _get_user_ids(client, users, format_=format_) + if actors: + actor_ids, errors = _get_actor_ids(client, actors, format_=format_) succeeded = 0 - for chunk in chunked(user_ids, size=100): + for chunk in chunked(actor_ids, size=100): try: - client.watchlists.v1.remove_included_users(watchlist, chunk) + client.watchlists.v2.remove_included_actors(watchlist, chunk) console.print( - f"Successfully removed {len(chunk)} included users on watchlist with ID: '{watchlist}'" + f"Successfully removed {len(chunk)} included actors on watchlist with ID: '{watchlist}'" ) succeeded += len(chunk) except requests.HTTPError as err: - if "User not found" in err.response.text: + if "Actor not found" in err.response.text: console.print( - "Problem processing batch of users, will attempt each individually." + "Problem processing batch of actors, will attempt each individually." ) chunk_succeeded = 0 - for user in chunk: + for actor in chunk: try: - client.watchlists.v1.remove_included_users(watchlist, user) + client.watchlists.v2.remove_included_actors( + watchlist, actor + ) succeeded += 1 chunk_succeeded += 1 except requests.HTTPError as err: client.settings.logger.error( - f"Problem removing userId={user} from watchlist={watchlist}: {err.response.text}" + f"Problem removing actorId={actor} from watchlist={watchlist}: {err.response.text}" ) - errors.append(user) + errors.append(actor) console.print( - f"Successfully removed {chunk_succeeded} users from watchlist with ID: '{watchlist}'" + f"Successfully removed {chunk_succeeded} actors from watchlist with ID: '{watchlist}'" ) else: raise err if errors: - console.print("[red]The following usernames/user IDs were not found:") + console.print("[red]The following actornames/actor IDs were not found:") console.print("\t" + "\n\t".join(errors)) # Remove excluded users - if excluded_users: - user_ids, errors = _get_user_ids(client, excluded_users, format_=format_) + if excluded_actors: + actor_ids, errors = _get_actor_ids(client, excluded_actors, format_=format_) succeeded = 0 - for chunk in chunked(user_ids, size=100): + for chunk in chunked(actor_ids, size=100): try: - client.watchlists.v1.remove_excluded_users(watchlist, chunk) + client.watchlists.v2.remove_excluded_actors(watchlist, chunk) console.print( - f"Successfully removed {len(chunk)} excluded users from watchlist with ID: '{watchlist}'" + f"Successfully removed {len(chunk)} excluded actors from watchlist with ID: '{watchlist}'" ) succeeded += len(chunk) except requests.HTTPError as err: if "User not found" in err.response.text: console.print( - "Problem processing batch of users, will attempt each individually." + "Problem processing batch of actors, will attempt each individually." ) chunk_succeeded = 0 - for user in chunk: + for actor in chunk: try: - client.watchlists.v1.remove_excluded_users(watchlist, user) + client.watchlists.v2.remove_excluded_actors( + watchlist, actor + ) succeeded += 1 chunk_succeeded += 1 except requests.HTTPError as err: client.settings.logger.error( - f"Problem removing excluded userId={user} from watchlist={watchlist}: {err.response.text}" + f"Problem removing excluded actorId={actor} from watchlist={watchlist}: {err.response.text}" ) - errors.append(user) + errors.append(actor) console.print( - f"Successfully removed {chunk_succeeded} excluded users from watchlist with ID: '{watchlist}'" + f"Successfully removed {chunk_succeeded} excluded actors from watchlist with ID: '{watchlist}'" ) else: raise err if errors: - console.print("[red]The following usernames/user IDs were not found:") + console.print("[red]The following actornames/actor IDs were not found:") console.print("\t" + "\n\t".join(errors)) # Remove departments if departments: - client.watchlists.v1.remove_departments( + client.watchlists.v2.remove_departments( watchlist, [i.strip() for i in departments.split(",")] ) console.print( @@ -553,7 +627,7 @@ def remove( # Remove directory groups if directory_groups: - client.watchlists.v1.remove_directory_groups( + client.watchlists.v2.remove_directory_groups( watchlist, [i.strip() for i in directory_groups.split(",")] ) console.print( @@ -580,8 +654,8 @@ def list_members( `CUSTOM` watchlists must be specified by title or ID. """ client = Client() - members = client.watchlists.v1.list_members(watchlist) - _output_results(members.watchlist_members, WatchlistUser, format_, columns) + members = client.watchlists.v2.list_members(watchlist) + _output_results(members.watchlist_members, WatchlistActor, format_, columns) @watchlists.command(cls=IncydrCommand) @@ -589,20 +663,20 @@ def list_members( @table_format_option @columns_option @logging_options -def list_included_users( +def list_included_actors( watchlist: str, format_: str, columns: Optional[str], ): """ - List users explicitly included on a watchlist. + List actors explicitly included on a watchlist. WATCHLIST can be specified by watchlist type (ex: `DEPARTING_EMPLOYEE`) or ID. `CUSTOM` watchlists must be specified by title or ID. """ client = Client() - users = client.watchlists.v1.list_included_users(watchlist) - _output_results(users.included_users, WatchlistUser, format_, columns) + users = client.watchlists.v2.list_included_actors(watchlist) + _output_results(users.included_actors, WatchlistActor, format_, columns) @watchlists.command(cls=IncydrCommand) @@ -610,20 +684,20 @@ def list_included_users( @table_format_option @columns_option @logging_options -def list_excluded_users( +def list_excluded_actors( watchlist: str, format_: TableFormat, columns: Optional[str], ): """ - List users excluded from a watchlist. + List actors excluded from a watchlist. WATCHLIST can be specified by watchlist type (ex: `DEPARTING_EMPLOYEE`) or ID. `CUSTOM` watchlists must be specified by title or ID. """ client = Client() - users = client.watchlists.v1.list_excluded_users(watchlist) - _output_results(users.excluded_users, WatchlistUser, format_, columns) + users = client.watchlists.v2.list_excluded_actors(watchlist) + _output_results(users.excluded_actors, WatchlistActor, format_, columns) @watchlists.command(cls=IncydrCommand) @@ -643,7 +717,7 @@ def list_directory_groups( `CUSTOM` watchlists must be specified by title or ID. """ client = Client() - groups = client.watchlists.v1.list_directory_groups(watchlist) + groups = client.watchlists.v2.list_directory_groups(watchlist) _output_results( groups.included_directory_groups, IncludedDirectoryGroup, format_, columns ) @@ -666,38 +740,38 @@ def list_departments( `CUSTOM` watchlists must be specified by title or ID. """ client = Client() - deps = client.watchlists.v1.list_departments(watchlist) + deps = client.watchlists.v2.list_departments(watchlist) _output_results(deps.included_departments, IncludedDepartment, format_, columns) -def _get_user_ids(client, users, format_=None): +def _get_actor_ids(client, actors, format_=None): ids, errors = [], [] - if isinstance(users, str): - for user in users.split(","): + if isinstance(actors, str): + for actor in actors.split(","): try: - user_id = user_lookup(client, user) - ids.append(user_id) + actor = actor_lookup(client, actor) + ids.append(actor) except ValueError: client.settings.logger.error( - f"Problem looking up userId for username: {user}" + f"Problem looking up actorId for actor name: {actor}" ) - errors.append(user) + errors.append(actor) else: if format_ == "csv": - users = UserCSV.parse_csv(users) + actors = UserCSV.parse_csv(actors) else: - users = UserJSON.parse_json_lines(users) + actors = UserJSON.parse_json_lines(actors) for row in track( - users, - description="Reading users...", + actors, + description="Reading actors...", transient=True, ): try: - user_id = user_lookup(client, row.user) - ids.append(user_id) + actor_id = actor_lookup(client, row.user) + ids.append(actor_id) except ValueError: client.settings.logger.error( - f"Problem looking up userId for username: {row.user}" + f"Problem looking up actorId for actor name: {row.user}" ) errors.append(row.user) return ids, errors @@ -714,3 +788,51 @@ def _output_results(results, model, format_, columns=None): else: for item in results: console.print(item.json(), highlight=False) + + +# ---------- Deprecated 2025-03 ---------- + + +@watchlists.command(cls=IncydrCommand) +@watchlist_arg +@table_format_option +@columns_option +@logging_options +def list_included_users( + watchlist: str, + format_: str, + columns: Optional[str], +): + """ + DEPRECATED. List users explicitly included on a watchlist. + + WATCHLIST can be specified by watchlist type (ex: `DEPARTING_EMPLOYEE`) or ID. + `CUSTOM` watchlists must be specified by title or ID. + """ + deprecation_warning("DEPRECATED. Use list_included_actors instead.") + client = Client() + users = client.watchlists.v1.list_included_users(watchlist) + _output_results(users.included_users, WatchlistUser, format_, columns) + + +@watchlists.command(cls=IncydrCommand) +@watchlist_arg +@table_format_option +@columns_option +@logging_options +def list_excluded_users( + watchlist: str, + format_: TableFormat, + columns: Optional[str], +): + """ + DEPRECATED. List users excluded from a watchlist. + + WATCHLIST can be specified by watchlist type (ex: `DEPARTING_EMPLOYEE`) or ID. + `CUSTOM` watchlists must be specified by title or ID. + """ + deprecation_warning("DEPRECATED. Use list_excluded_actors instead.") + + client = Client() + users = client.watchlists.v1.list_excluded_users(watchlist) + _output_results(users.excluded_users, WatchlistUser, format_, columns) diff --git a/src/_incydr_sdk/watchlists/models/responses.py b/src/_incydr_sdk/watchlists/models/responses.py index e3e78371..26f75f4a 100644 --- a/src/_incydr_sdk/watchlists/models/responses.py +++ b/src/_incydr_sdk/watchlists/models/responses.py @@ -131,7 +131,9 @@ class ExcludedActorsList(ResponseModel): * **total_count**: `int` """ - excluded_users: Optional[List[WatchlistActor]] = Field(None, alias="excludedActors") + excluded_actors: Optional[List[WatchlistActor]] = Field( + None, alias="excludedActors" + ) total_count: Optional[int] = Field( None, description="The total count of all excluded actors.", diff --git a/tests/test_watchlists.py b/tests/test_watchlists.py index 8bb15d64..dd4109d7 100644 --- a/tests/test_watchlists.py +++ b/tests/test_watchlists.py @@ -630,9 +630,9 @@ def test_list_excluded_users_returns_expected_data_v2(mock_get_all_excluded_acto c = Client() users = c.watchlists.v2.list_excluded_actors(TEST_WATCHLIST_ID) assert isinstance(users, ExcludedActorsList) - assert users.excluded_users[0].json() == json.dumps(TEST_ACTOR_1) - assert users.excluded_users[1].json() == json.dumps(TEST_ACTOR_2) - assert users.total_count == len(users.excluded_users) == 2 + assert users.excluded_actors[0].json() == json.dumps(TEST_ACTOR_1) + assert users.excluded_actors[1].json() == json.dumps(TEST_ACTOR_2) + assert users.total_count == len(users.excluded_actors) == 2 @valid_ids_param @@ -1193,7 +1193,7 @@ def test_get_id_by_name_when_no_id_raises_error(httpserver_auth: HTTPServer): def test_cli_list_makes_expected_call( - httpserver_auth: HTTPServer, runner, mock_get_all + httpserver_auth: HTTPServer, runner, mock_get_all_v2 ): result = runner.invoke(incydr, ["watchlists", "list"]) httpserver_auth.check() @@ -1210,13 +1210,13 @@ def test_cli_list_makes_expected_call( def test_cli_show_makes_expected_call( httpserver_auth: HTTPServer, runner, - mock_get_all, + mock_get_all_v2, watchlist_input, watchlist_expected, - mock_get_all_included_users, - mock_get_all_excluded_users, - mock_get_all_departments, - mock_get_all_directory_groups, + mock_get_all_included_actors, + mock_get_all_excluded_actors, + mock_get_all_departments_v2, + mock_get_all_directory_groups_v2, ): # mock unordered token request to follow watchlist arg callback auth_response = dict( @@ -1229,7 +1229,7 @@ def test_cli_show_makes_expected_call( ) httpserver_auth.expect_request( - f"/v1/watchlists/{watchlist_expected}", method="GET" + f"/v2/watchlists/{watchlist_expected}", method="GET" ).respond_with_json(TEST_WATCHLIST_1) result = runner.invoke(incydr, ["watchlists", "show", watchlist_input]) @@ -1238,7 +1238,7 @@ def test_cli_show_makes_expected_call( def test_cli_create_default_watchlist_makes_expected_call( - httpserver_auth: HTTPServer, runner, mock_create_departing_employee + httpserver_auth: HTTPServer, runner, mock_create_departing_employee_v2 ): result = runner.invoke(incydr, ["watchlists", "create", "DEPARTING_EMPLOYEE"]) httpserver_auth.check() @@ -1246,7 +1246,7 @@ def test_cli_create_default_watchlist_makes_expected_call( def test_cli_create_custom_watchlist_makes_expected_call( - httpserver_auth: HTTPServer, runner, mock_create_custom + httpserver_auth: HTTPServer, runner, mock_create_custom_v2 ): result = runner.invoke( incydr, @@ -1265,7 +1265,7 @@ def test_cli_create_custom_watchlist_makes_expected_call( def test_cli_delete_makes_expected_call( - httpserver_auth: HTTPServer, runner, mock_delete + httpserver_auth: HTTPServer, runner, mock_delete_v2 ): result = runner.invoke(incydr, ["watchlists", "delete", TEST_WATCHLIST_ID]) httpserver_auth.check() @@ -1279,7 +1279,7 @@ def test_cli_update_makes_expected_call(httpserver_auth: HTTPServer, runner): watchlist["title"] = "updated title" watchlist["description"] = "updated description" httpserver_auth.expect_request( - f"/v1/watchlists/{TEST_WATCHLIST_ID}", + f"/v2/watchlists/{TEST_WATCHLIST_ID}", method="PATCH", query_string=urlencode(query, doseq=True), json=data, @@ -1304,11 +1304,11 @@ def test_cli_update_makes_expected_call(httpserver_auth: HTTPServer, runner): @pytest.mark.parametrize( "command,mock_server_call", [ - ("list-members", "mock_get_all_members"), - ("list-included-users", "mock_get_all_included_users"), - ("list-excluded-users", "mock_get_all_excluded_users"), - ("list-directory-groups", "mock_get_all_directory_groups"), - ("list-departments", "mock_get_all_departments"), + ("list-members", "mock_get_all_members_v2"), + ("list-included-actors", "mock_get_all_included_actors"), + ("list-excluded-actors", "mock_get_all_excluded_actors"), + ("list-directory-groups", "mock_get_all_directory_groups_v2"), + ("list-departments", "mock_get_all_departments_v2"), ], ) def test_cli_list_members_makes_expected_call( @@ -1325,21 +1325,27 @@ def test_cli_list_members_makes_expected_call( "option, command, path_group, url_path, expected_request", [ ( - "users", + "actors", "add", - "included-users", + "included-actors", "add", - {"userIds": USER_IDS, "watchlistId": TEST_WATCHLIST_ID}, + {"actorIds": USER_IDS, "watchlistId": TEST_WATCHLIST_ID}, + ), + ( + "actors", + "remove", + "included-actors", + "delete", + {"actorIds": USER_IDS, "watchlistId": TEST_WATCHLIST_ID}, ), + ("excluded-actors", "add", "excluded-actors", "add", {"actorIds": USER_IDS}), ( - "users", + "excluded-actors", "remove", - "included-users", + "excluded-actors", "delete", - {"userIds": USER_IDS, "watchlistId": TEST_WATCHLIST_ID}, + {"actorIds": USER_IDS}, ), - ("excluded-users", "add", "excluded-users", "add", {"userIds": USER_IDS}), - ("excluded-users", "remove", "excluded-users", "delete", {"userIds": USER_IDS}), ], ) @@ -1355,7 +1361,7 @@ def test_cli_update_users_when_list_makes_expected_call( expected_request, ): httpserver_auth.expect_request( - f"/v1/watchlists/{TEST_WATCHLIST_ID}/{path_group}/{url_path}", + f"/v2/watchlists/{TEST_WATCHLIST_ID}/{path_group}/{url_path}", method="POST", json=expected_request, ).respond_with_data() @@ -1382,7 +1388,7 @@ def test_cli_update_users_when_csv_makes_expected_call( p.write_text("user\n" + "\n".join(USER_IDS)) httpserver_auth.expect_request( - f"/v1/watchlists/{TEST_WATCHLIST_ID}/{path_group}/{url_path}", + f"/v2/watchlists/{TEST_WATCHLIST_ID}/{path_group}/{url_path}", method="POST", json=expected_request, ).respond_with_data() @@ -1409,7 +1415,7 @@ def test_cli_update_users_when_json_makes_expected_call( p.write_text("\n".join([f'{{ "userId": "{u}" }}' for u in USER_IDS])) httpserver_auth.expect_request( - f"/v1/watchlists/{TEST_WATCHLIST_ID}/{path_group}/{url_path}", + f"/v2/watchlists/{TEST_WATCHLIST_ID}/{path_group}/{url_path}", method="POST", json=expected_request, ).respond_with_data() @@ -1450,7 +1456,7 @@ def test_cli_update_departments_and_directory_groups_makes_expected_call( expected_request, ): httpserver_auth.expect_request( - f"/v1/watchlists/{TEST_WATCHLIST_ID}/included-{option}/{url_path}", + f"/v2/watchlists/{TEST_WATCHLIST_ID}/included-{option}/{url_path}", method="POST", json=expected_request, ).respond_with_data() @@ -1467,20 +1473,20 @@ def test_cli_update_users_batches_by_100(httpserver_auth: HTTPServer, runner, ac USER_IDS = [f"user-{i}" for i in range(150)] httpserver_auth.expect_request( - f"/v1/watchlists/{TEST_WATCHLIST_ID}/included-users/{action[1]}", + f"/v2/watchlists/{TEST_WATCHLIST_ID}/included-actors/{action[1]}", method="POST", - json={"userIds": USER_IDS[:100], "watchlistId": TEST_WATCHLIST_ID}, + json={"actorIds": USER_IDS[:100], "watchlistId": TEST_WATCHLIST_ID}, ).respond_with_data() httpserver_auth.expect_request( - f"/v1/watchlists/{TEST_WATCHLIST_ID}/included-users/{action[1]}", + f"/v2/watchlists/{TEST_WATCHLIST_ID}/included-actors/{action[1]}", method="POST", - json={"userIds": USER_IDS[100:], "watchlistId": TEST_WATCHLIST_ID}, + json={"actorIds": USER_IDS[100:], "watchlistId": TEST_WATCHLIST_ID}, ).respond_with_data() result = runner.invoke( incydr, - ["watchlists", action[0], TEST_WATCHLIST_ID, "--users", ",".join(USER_IDS)], + ["watchlists", action[0], TEST_WATCHLIST_ID, "--actors", ",".join(USER_IDS)], ) httpserver_auth.check() assert result.exit_code == 0 From 5ae07e3fb2fdf0a7ea16971d57bd4c67b291bef4 Mon Sep 17 00:00:00 2001 From: Cecilia Stevens <63068179+ceciliastevens@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:23:45 -0400 Subject: [PATCH 03/11] deprecate devices, docs --- docs/cli/index.md | 9 ++++++--- docs/sdk/clients/devices.md | 2 +- docs/sdk/clients/risk_profiles.md | 2 +- docs/sdk/enums.md | 4 +++- docs/sdk/models.md | 10 ++++++++-- mkdocs.yml | 12 ++++++------ src/_incydr_cli/cmds/alerts.py | 2 +- src/_incydr_cli/cmds/devices.py | 8 +++++++- src/_incydr_cli/cmds/risk_profiles.py | 2 +- src/_incydr_sdk/devices/client.py | 18 ++++++++++++++++++ src/_incydr_sdk/risk_profiles/client.py | 2 ++ 11 files changed, 54 insertions(+), 17 deletions(-) diff --git a/docs/cli/index.md b/docs/cli/index.md index 25f5727d..f13af2a9 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -28,15 +28,18 @@ The following subcommand groups are available under the `incydr` command: * [Actors](cmds/actors.md) * [Agents](cmds/agents.md) * [Alert Rules](cmds/alert_rules.md) -* [Alerts (Deprecated)](cmds/alerts.md) * [Audit Log](cmds/audit_log.md) * [Cases](cmds/cases.md) * [Departments](cmds/departments.md) -* [Devices](cmds/devices.md) * [Directory Groups](cmds/directory_groups.md) * [File Events](cmds/file_events.md) -* [Risk Profiles](cmds/risk_profiles.md) * [Sessions](cmds/sessions.md) * [Trusted Activities](cmds/trusted_activities.md) * [Users](cmds/users.md) * [Watchlists](cmds/watchlists.md) + +Deprecated command groups: + +* [Alerts (Deprecated)](cmds/alerts.md) +* [Devices (Deprecated)](cmds/devices.md) +* [Risk Profiles (Deprecated)](cmds/risk_profiles.md) diff --git a/docs/sdk/clients/devices.md b/docs/sdk/clients/devices.md index 524ad6b5..91a58c09 100644 --- a/docs/sdk/clients/devices.md +++ b/docs/sdk/clients/devices.md @@ -1,4 +1,4 @@ -# Devices +# Devices (Deprecated) ::: _incydr_sdk.devices.client.DevicesV1 :docstring: diff --git a/docs/sdk/clients/risk_profiles.md b/docs/sdk/clients/risk_profiles.md index 043d95bf..5a2fe540 100644 --- a/docs/sdk/clients/risk_profiles.md +++ b/docs/sdk/clients/risk_profiles.md @@ -1,4 +1,4 @@ -# Risk Profiles +# Risk Profiles (Deprecated) ::: _incydr_sdk.risk_profiles.client.RiskProfilesV1 :docstring: diff --git a/docs/sdk/enums.md b/docs/sdk/enums.md index 95b4e538..f0cc4dc4 100644 --- a/docs/sdk/enums.md +++ b/docs/sdk/enums.md @@ -124,7 +124,9 @@ Alerts has been replaced by [Sessions](#sessions). * **CLOSED**: `"CLOSED"` * **OPEN**: `"OPEN"` -## Devices +## Devices (Deprecated) + +Devices has been replaced by [Agents](#agents) ### Devices Sort Keys diff --git a/docs/sdk/models.md b/docs/sdk/models.md index 1723c253..99126e76 100644 --- a/docs/sdk/models.md +++ b/docs/sdk/models.md @@ -124,7 +124,10 @@ Alerts has been replaced by [Sessions](#sessions). ::: incydr.models.DepartmentsPage :docstring: -## Devices +## Devices (Deprecated) + +Devices has been replaced by [Agents](#agents). + --- ### `Device` model @@ -220,7 +223,10 @@ Alerts has been replaced by [Sessions](#sessions). :::incydr.models.Role :docstring: -## Risk Profiles +## Risk Profiles (Deprecated) + +Risk Profiles have been replaced by [Actors](#actors). + --- ### `RiskProfile` model diff --git a/mkdocs.yml b/mkdocs.yml index 6bd8d068..a961cc6b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -43,22 +43,22 @@ nav: - Reference: - Actors: 'sdk/clients/actors.md' - Agents: 'sdk/clients/agents.md' - - Alerts (Deprecated): 'sdk/clients/alerts.md' - Alert Rules: 'sdk/clients/alert_rules.md' - Alert Querying: 'sdk/clients/alert_queries.md' - Audit Log: 'sdk/clients/audit_log.md' - Cases: 'sdk/clients/cases.md' - Customer: 'sdk/clients/customer.md' - Departments: 'sdk/clients/departments.md' - - Devices: 'sdk/clients/devices.md' - Directory Groups: 'sdk/clients/directory_groups.md' - File Events: 'sdk/clients/file_events.md' - File Event Querying: 'sdk/clients/file_event_queries.md' - - Risk Profiles: 'sdk/clients/risk_profiles.md' - Sessions: 'sdk/clients/sessions.md' - Trusted Activites: 'sdk/clients/trusted_activities.md' - Users: 'sdk/clients/users.md' - Watchlists: 'sdk/clients/watchlists.md' + - Alerts (Deprecated): 'sdk/clients/alerts.md' + - Devices (Deprecated): 'sdk/clients/devices.md' + - Risk Profiles (Deprecated): 'sdk/clients/risk_profiles.md' - Enums: 'sdk/enums.md' - Models: 'sdk/models.md' - CLI: @@ -71,19 +71,19 @@ nav: - Commands: - Actors: 'cli/cmds/actors.md' - Agents: 'cli/cmds/agents.md' - - Alerts (Deprecated): 'cli/cmds/alerts.md' - Alert Rules: 'cli/cmds/alert_rules.md' - Audit Log: 'cli/cmds/audit_log.md' - Cases: 'cli/cmds/cases.md' - Departments: 'cli/cmds/departments.md' - - Devices: 'cli/cmds/devices.md' - Directory Groups: 'cli/cmds/directory_groups.md' - File Events: 'cli/cmds/file_events.md' - - Risk Profiles: 'cli/cmds/risk_profiles.md' - Sessions: 'cli/cmds/sessions.md' - Trusted Activites: 'cli/cmds/trusted_activities.md' - Users: 'cli/cmds/users.md' - Watchlists: 'cli/cmds/watchlists.md' + - Alerts (Deprecated): 'cli/cmds/alerts.md' + - Devices (Deprecated): 'cli/cmds/devices.md' + - Risk Profiles (Deprecated): 'cli/cmds/risk_profiles.md' - Guides: - Introduction: 'integration-guides/index.md' - Microsoft Sentinel: diff --git a/src/_incydr_cli/cmds/alerts.py b/src/_incydr_cli/cmds/alerts.py index afaffd80..812022c3 100644 --- a/src/_incydr_cli/cmds/alerts.py +++ b/src/_incydr_cli/cmds/alerts.py @@ -47,7 +47,7 @@ @click.group(cls=IncydrGroup) @logging_options def alerts(): - """View and manage alerts.""" + """DEPRECATED. Use the Sessions command group instead. View and manage alerts.""" deprecation_warning(DEPRECATION_TEXT) diff --git a/src/_incydr_cli/cmds/devices.py b/src/_incydr_cli/cmds/devices.py index 0793d1cd..4ae2f510 100644 --- a/src/_incydr_cli/cmds/devices.py +++ b/src/_incydr_cli/cmds/devices.py @@ -9,6 +9,7 @@ from _incydr_cli.cmds.options.output_options import SingleFormat from _incydr_cli.cmds.options.output_options import table_format_option from _incydr_cli.cmds.options.output_options import TableFormat +from _incydr_cli.cmds.utils import deprecation_warning from _incydr_cli.core import IncydrCommand from _incydr_cli.core import IncydrGroup from _incydr_sdk.core.client import Client @@ -16,10 +17,15 @@ from _incydr_sdk.utils import model_as_card +# Deprecated 2025-03 +DEPRECATION_TEXT = "DeprecationWarning: Devices commands are deprecated. Use the 'incydr agents' command group instead." + + @click.group(cls=IncydrGroup) @logging_options def devices(): - """View devices.""" + """DEPRECATED. Use the agents command group instead. View devices.""" + deprecation_warning(DEPRECATION_TEXT) @devices.command("list", cls=IncydrCommand) diff --git a/src/_incydr_cli/cmds/risk_profiles.py b/src/_incydr_cli/cmds/risk_profiles.py index b6b43a99..d1103051 100644 --- a/src/_incydr_cli/cmds/risk_profiles.py +++ b/src/_incydr_cli/cmds/risk_profiles.py @@ -29,7 +29,7 @@ @click.group(cls=IncydrGroup) @logging_options def risk_profiles(): - """View and manage risk profiles.""" + """DEPRECATED. Use the Actors command group instead. View and manage risk profiles.""" deprecation_warning(DEPRECATION_TEXT) diff --git a/src/_incydr_sdk/devices/client.py b/src/_incydr_sdk/devices/client.py index 9f72dc08..5125eb85 100644 --- a/src/_incydr_sdk/devices/client.py +++ b/src/_incydr_sdk/devices/client.py @@ -1,5 +1,6 @@ from itertools import count from typing import Iterator +from warnings import warn from .models import Device from .models import DevicesPage @@ -23,6 +24,8 @@ def v1(self): class DevicesV1: """Client for `/v1/devices` endpoints. + This client is deprecated. Use the Agents client instead. + Usage example: >>> import incydr @@ -43,6 +46,11 @@ def get_device(self, device_id: str) -> Device: **Returns**: A [`Device`][device-model] object representing the device. """ + warn( + "Devices endpoints are deprecated. Replaced by Agents.", + DeprecationWarning, + stacklevel=2, + ) response = self._parent.session.get(f"/v1/devices/{device_id}") return Device.parse_response(response) @@ -71,6 +79,11 @@ def get_page( **Returns**: A ['DevicesPage'][devicespage-model] object. """ + warn( + "Devices endpoints are deprecated. Replaced by Agents.", + DeprecationWarning, + stacklevel=2, + ) page_size = page_size or self._parent.settings.page_size data = QueryDevicesRequest( page=page_num, @@ -98,6 +111,11 @@ def iter_all( **Returns**: A generator yielding individual [`Device`][device-model] objects. """ + warn( + "Devices endpoints are deprecated. Replaced by Agents.", + DeprecationWarning, + stacklevel=2, + ) page_size = page_size or self._parent.settings.page_size for page_num in count(1): page = self.get_page( diff --git a/src/_incydr_sdk/risk_profiles/client.py b/src/_incydr_sdk/risk_profiles/client.py index 3629869e..ad90942e 100644 --- a/src/_incydr_sdk/risk_profiles/client.py +++ b/src/_incydr_sdk/risk_profiles/client.py @@ -29,6 +29,8 @@ class RiskProfilesV1: """ Client for `/v1/user-risk-profiles` endpoints. + This client is deprecated. Use the Actors client instead. + Usage example: >>> import incydr From 2ba4a14c0a52b1335ee2cce1a37de55fe319461d Mon Sep 17 00:00:00 2001 From: Cecilia Stevens <63068179+ceciliastevens@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:29:28 -0400 Subject: [PATCH 04/11] changelog --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9ed1938..8f3e98bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,24 @@ how a consumer would use the library or CLI tool (e.g. adding unit tests, updating documentation, etc) are not captured here. +## Unreleased + +### Added + +- `watchlists.v2` methods are added to the SDK, for parity with the API. +- New CLI watchlist commands `list-excluded-actors` and `list-included-actors` to replace the deprecated `list-excluded-users` and `list-included-users`. + +### Updated + +- The CLI's `watchlists` commands now use the v2 watchlist API. These commands correctly use `actor_id` instead of `user_id`. While the previous user_id parameters will still work for now, we recommend that users switch as soon as possible to using actor_id instead. + +### Deprecated + +- Devices methods in the SDK and CLI are deprecated. Use the Agents methods instead. +- Risk Profiles methods in the SDK and CLI, already deprecated, are more clearly marked. +- The SDK's `watchlists.v1` methods are deprecated. +- The CLI's watchlist group `list-excluded-users` and `list-included-users` commands are deprecated. Use `list-excluded-actors` and `list-included-actors` instead. + ## 2.2.4 - 2025-03-11 ### Added From b8b2c22d1aea78c6ce47de17cf9bf6e1226dc1a1 Mon Sep 17 00:00:00 2001 From: Cecilia Stevens <63068179+ceciliastevens@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:10:28 -0400 Subject: [PATCH 05/11] watchlist documentation --- docs/sdk/clients/watchlists.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sdk/clients/watchlists.md b/docs/sdk/clients/watchlists.md index faae615d..cb4e3838 100644 --- a/docs/sdk/clients/watchlists.md +++ b/docs/sdk/clients/watchlists.md @@ -1,5 +1,9 @@ # Watchlists +::: _incydr_sdk.watchlists.client.WatchlistsV2 + :docstring: + :members: + ::: _incydr_sdk.watchlists.client.WatchlistsV1 :docstring: :members: From 3bc25301927c7f259f08a57a3ef2cf03ad71a355 Mon Sep 17 00:00:00 2001 From: Cecilia Stevens <63068179+ceciliastevens@users.noreply.github.com> Date: Fri, 14 Mar 2025 11:54:03 -0400 Subject: [PATCH 06/11] INTEG-2781: support paging for watchlist GET apis --- src/_incydr_sdk/watchlists/client.py | 135 ++++++++++++++++++++++++--- 1 file changed, 123 insertions(+), 12 deletions(-) diff --git a/src/_incydr_sdk/watchlists/client.py b/src/_incydr_sdk/watchlists/client.py index 7550069a..2ddc5b69 100644 --- a/src/_incydr_sdk/watchlists/client.py +++ b/src/_incydr_sdk/watchlists/client.py @@ -203,7 +203,7 @@ def get_member(self, watchlist_id: str, actor_id: str) -> WatchlistActor: return WatchlistActor.parse_response(response) def list_members( - self, watchlist_id: Union[str, WatchlistType] + self, watchlist_id: Union[str, WatchlistType], page_num: int = None, page_size: int = None ) -> WatchlistMembersListV2: """ Get a list of all members of a watchlist. These actors may have been added as an included actor, or are members of an included department, etc. @@ -211,11 +211,34 @@ def list_members( **Parameters**: * **watchlist_id**: `str`(required) - Watchlist ID. + * **page_num**: `int` (optional) - The page number to fetch, starting at 1. + * **page_size**: `int` (optional) - Max number of results to return for a page. **Returns**: A [`WatchlistMembersListV2`][watchlistmemberslistv2-model] object. """ - response = self._parent.session.get(f"{self._uri}/{watchlist_id}/members") + page_size = page_size or self._parent.settings.page_size + response = self._parent.session.get(f"{self._uri}/{watchlist_id}/members", params = {"page": page_num, "page_size": page_size}) return WatchlistMembersListV2.parse_response(response) + + def iter_all_members( + self, watchlist_id: Union[str, WatchlistType], page_size: int = None + ) -> Iterator[WatchlistActor]: + """ + Iterate over all members of a watchlist. These actors may have been added as an included actor, or are members of an included department, etc. + + Accepts the same parameters as `.list_members()` excepting `page_num`. + + **Returns**: A generator that yields [`WatchlistActor`][watchlistactor-model] objects. + """ + page_size = page_size or self._parent.settings.page_size + for page_num in count(1): + page = self.list_members( + watchlist_id=watchlist_id, page_size=page_size, page_num=page_num + ) + yield from page.watchlist_members + if len(page.watchlist_members) < page_size: + break + def add_included_actors(self, watchlist_id: str, actor_ids: Union[str, List[str]]): """ @@ -276,21 +299,44 @@ def get_included_actor(self, watchlist_id: str, actor_id: str) -> WatchlistActor ) return WatchlistActor.parse_response(response) - def list_included_actors(self, watchlist_id: str) -> IncludedActorsList: + def list_included_actors(self, watchlist_id: str, page_num: int = None, page_size: int = None) -> IncludedActorsList: """ List individual actors included on a watchlist. **Parameters**: * **watchlist_id**: `str` (required) - Watchlist ID. + * **page_num**: `int` (optional) - The page number to fetch, starting at 1. + * **page_size**: `int` (optional) - Max number of results to return for a page. **Returns**: An [`IncludedActorsList`][includedactorslist-model] object. """ - + page_size = page_size or self._parent.settings.page_size response = self._parent.session.get( - url=f"{self._uri}/{watchlist_id}/included-actors" + url=f"{self._uri}/{watchlist_id}/included-actors", params = {"page": page_num, "page_size": page_size} ) return IncludedActorsList.parse_response(response) + + def iter_all_included_actors( + self, watchlist_id: str, page_size: int = None + ) -> Iterator[WatchlistActor]: + """ + Iterate over all individual actors included on a watchlist. + + Accepts the same parameters as `.list_included_actors()` excepting `page_num`. + + **Returns**: A generator yielding individual [`WatchlistActor`][watchlistactor-model] objects. + """ + page_size = page_size or self._parent.settings.page_size + for page_num in count(1): + page = self.list_included_actors( + watchlist_id = watchlist_id, + page_num=page_num, page_size=page_size + ) + yield from page.included_actors + if len(page.included_actors) < page_size: + break + def add_excluded_actors(self, watchlist_id: str, actor_ids: Union[str, List[str]]): """ @@ -332,18 +378,40 @@ def remove_excluded_actors( url=f"{self._uri}/{watchlist_id}/excluded-actors/delete", json=data.dict() ) - def list_excluded_actors(self, watchlist_id: str) -> ExcludedActorsList: + def list_excluded_actors(self, watchlist_id: str, page_num: int = None, page_size: int = None) -> ExcludedActorsList: """ List individual actors excluded from a watchlist. * **watchlist_id**: `str` (required) - Watchlist ID. + * **page_num**: `int` (optional) - The page number to fetch, starting at 1. + * **page_size**: `int` (optional) - Max number of results to return for a page. + **Returns**: An [`ExcludedActorsList`][excludedactorslist-model] object. """ + page_size = page_size or self._parent.settings.page_size response = self._parent.session.get( - f"{self._uri}/{watchlist_id}/excluded-actors" + f"{self._uri}/{watchlist_id}/excluded-actors", + params={"page": page_num, "page_size": page_size} ) return ExcludedActorsList.parse_response(response) + + def iter_all_excluded_actors(self, watchlist_id: str, page_size: int = None) -> Iterator[WatchlistActor]: + """ + Iterate over all individual actors excluded from a watchlist. + + Accepts the same parameters as `.list_excluded_actors()` excepting `page_num`. + + **Returns**: A generator yielding individual [`WatchlistActor`][watchlistactor-model] objects. + """ + page_size = page_size or self._parent.settings.page_size + for page_num in count(1): + page = self.list_excluded_actors( + page_num=page_num, page_size=page_size, watchlist_id=watchlist_id + ) + yield from page.excluded_actors + if len(page.excluded_actors) < page_size: + break def get_excluded_actor(self, watchlist_id: str, actor_id: str) -> WatchlistActor: """ @@ -401,21 +469,42 @@ def remove_directory_groups( json=data.dict(), ) - def list_directory_groups(self, watchlist_id: str) -> IncludedDirectoryGroupsList: + def list_directory_groups(self, watchlist_id: str, page_num: int = None, page_size: int = None) -> IncludedDirectoryGroupsList: """ List directory groups included on a watchlist. **Parameters**: * **watchlist_id**: `str` (required) - Watchlist ID. + * **page_num**: `int` (optional) - The page number to fetch, starting at 1. + * **page_size**: `int` (optional) - Max number of results to return for a page. **Returns**: An [`IncludedDirectoryGroupsList`][includeddirectorygroupslist-model] object. """ - + page_size = page_size or self._parent.settings.page_size response = self._parent.session.get( - f"{self._uri}/{watchlist_id}/included-directory-groups" + f"{self._uri}/{watchlist_id}/included-directory-groups", + params={"page": page_num, "page_size": page_size} ) return IncludedDirectoryGroupsList.parse_response(response) + + def iter_all_directory_groups(self, watchlist_id: str, page_size: int = None) -> Iterator[IncludedDirectoryGroup]: + """ + Iterate over all directory groups included on a watchlist. + + Accepts the same parameters as `.list_directory_groups()` excepting `page_num`. + + **Returns**: A generator yielding individual [`IncludedDirectoryGroup`][includeddirectorygroup-model] objects. + """ + page_size = page_size or self._parent.settings.page_size + for page_num in count(1): + page = self.list_directory_groups( + page_num=page_num, page_size=page_size, watchlist_id=watchlist_id + ) + yield from page.included_directory_groups + if len(page.included_directory_groups) < page_size: + break + def get_directory_group( self, watchlist_id: str, group_id: str @@ -481,20 +570,42 @@ def remove_departments( json=data.dict(), ) - def list_departments(self, watchlist_id: str) -> IncludedDepartmentsList: + def list_departments(self, watchlist_id: str, page_num: int = None, page_size: int = None) -> IncludedDepartmentsList: """ List departments included on a watchlist. **Parameters**: * **watchlist_id**: `str` (required) - Watchlist ID. + * **page_num**: `int` - Page number for results, starting at 1. + * **page_size**: `int` - Max number of results to return for a page. **Returns**: An [`IncludedDepartmentsList`][includeddepartmentslist-model] object. """ response = self._parent.session.get( - f"{self._uri}/{watchlist_id}/included-departments" + f"{self._uri}/{watchlist_id}/included-departments", + params={"page": page_num, "page_size": page_size} ) return IncludedDepartmentsList.parse_response(response) + + def iter_all_departments(self, watchlist_id: str, page_size: int = None) -> Iterator[IncludedDepartment]: + """ + Iterate over all departments included on a watchlist. + + Accepts the same parameters as `.list_departments()` excepting `page_num`. + + **Returns**: A generator yielding individual [`IncludedDepartment`][includeddepartment-model] objects. + + """ + page_size = page_size or self._parent.settings.page_size + for page_num in count(1): + page = self.list_departments( + page_num=page_num, page_size=page_size, watchlist_id=watchlist_id + ) + yield from page.included_departments + if len(page.included_departments) < page_size: + break + def get_department(self, watchlist_id: str, department: str) -> IncludedDepartment: """ From 623205aaa4355b7a5f99af6bb91e7fcb6499e56c Mon Sep 17 00:00:00 2001 From: Cecilia Stevens <63068179+ceciliastevens@users.noreply.github.com> Date: Fri, 14 Mar 2025 12:07:37 -0400 Subject: [PATCH 07/11] cli should use iterators --- src/_incydr_cli/cmds/watchlists.py | 20 ++++++++++---------- tests/test_watchlists.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/_incydr_cli/cmds/watchlists.py b/src/_incydr_cli/cmds/watchlists.py index 0e187717..3ef344a7 100644 --- a/src/_incydr_cli/cmds/watchlists.py +++ b/src/_incydr_cli/cmds/watchlists.py @@ -654,8 +654,8 @@ def list_members( `CUSTOM` watchlists must be specified by title or ID. """ client = Client() - members = client.watchlists.v2.list_members(watchlist) - _output_results(members.watchlist_members, WatchlistActor, format_, columns) + members = list(client.watchlists.v2.iter_all_members(watchlist)) + _output_results(members, WatchlistActor, format_, columns) @watchlists.command(cls=IncydrCommand) @@ -675,8 +675,8 @@ def list_included_actors( `CUSTOM` watchlists must be specified by title or ID. """ client = Client() - users = client.watchlists.v2.list_included_actors(watchlist) - _output_results(users.included_actors, WatchlistActor, format_, columns) + users = list(client.watchlists.v2.iter_all_included_actors(watchlist)) + _output_results(users, WatchlistActor, format_, columns) @watchlists.command(cls=IncydrCommand) @@ -696,8 +696,8 @@ def list_excluded_actors( `CUSTOM` watchlists must be specified by title or ID. """ client = Client() - users = client.watchlists.v2.list_excluded_actors(watchlist) - _output_results(users.excluded_actors, WatchlistActor, format_, columns) + users = list(client.watchlists.v2.iter_all_excluded_actors(watchlist)) + _output_results(users, WatchlistActor, format_, columns) @watchlists.command(cls=IncydrCommand) @@ -717,9 +717,9 @@ def list_directory_groups( `CUSTOM` watchlists must be specified by title or ID. """ client = Client() - groups = client.watchlists.v2.list_directory_groups(watchlist) + groups = list(client.watchlists.v2.iter_all_directory_groups(watchlist)) _output_results( - groups.included_directory_groups, IncludedDirectoryGroup, format_, columns + groups, IncludedDirectoryGroup, format_, columns ) @@ -740,8 +740,8 @@ def list_departments( `CUSTOM` watchlists must be specified by title or ID. """ client = Client() - deps = client.watchlists.v2.list_departments(watchlist) - _output_results(deps.included_departments, IncludedDepartment, format_, columns) + deps = list(client.watchlists.v2.iter_all_departments(watchlist)) + _output_results(deps, IncludedDepartment, format_, columns) def _get_actor_ids(client, actors, format_=None): diff --git a/tests/test_watchlists.py b/tests/test_watchlists.py index dd4109d7..b1882497 100644 --- a/tests/test_watchlists.py +++ b/tests/test_watchlists.py @@ -254,7 +254,7 @@ def mock_get_all_departments(httpserver_auth: HTTPServer): @pytest.fixture def mock_get_all_departments_v2(httpserver_auth: HTTPServer): data = { - "includedDepartments": [TEST_DEPARTMENT_1, TEST_DEPARTMENT_2], + "included_departments": [TEST_DEPARTMENT_1, TEST_DEPARTMENT_2], "totalCount": 2, } httpserver_auth.expect_request( From f0ced9fff974d31006cfe1b02bd48d383e7f79f7 Mon Sep 17 00:00:00 2001 From: Cecilia Stevens <63068179+ceciliastevens@users.noreply.github.com> Date: Fri, 14 Mar 2025 12:25:32 -0400 Subject: [PATCH 08/11] style --- src/_incydr_cli/cmds/watchlists.py | 4 +- src/_incydr_sdk/watchlists/client.py | 78 +++++++++++++++++----------- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/src/_incydr_cli/cmds/watchlists.py b/src/_incydr_cli/cmds/watchlists.py index 3ef344a7..e83b263f 100644 --- a/src/_incydr_cli/cmds/watchlists.py +++ b/src/_incydr_cli/cmds/watchlists.py @@ -718,9 +718,7 @@ def list_directory_groups( """ client = Client() groups = list(client.watchlists.v2.iter_all_directory_groups(watchlist)) - _output_results( - groups, IncludedDirectoryGroup, format_, columns - ) + _output_results(groups, IncludedDirectoryGroup, format_, columns) @watchlists.command(cls=IncydrCommand) diff --git a/src/_incydr_sdk/watchlists/client.py b/src/_incydr_sdk/watchlists/client.py index 2ddc5b69..73f9a8b0 100644 --- a/src/_incydr_sdk/watchlists/client.py +++ b/src/_incydr_sdk/watchlists/client.py @@ -203,7 +203,10 @@ def get_member(self, watchlist_id: str, actor_id: str) -> WatchlistActor: return WatchlistActor.parse_response(response) def list_members( - self, watchlist_id: Union[str, WatchlistType], page_num: int = None, page_size: int = None + self, + watchlist_id: Union[str, WatchlistType], + page_num: int = None, + page_size: int = None, ) -> WatchlistMembersListV2: """ Get a list of all members of a watchlist. These actors may have been added as an included actor, or are members of an included department, etc. @@ -212,16 +215,19 @@ def list_members( * **watchlist_id**: `str`(required) - Watchlist ID. * **page_num**: `int` (optional) - The page number to fetch, starting at 1. - * **page_size**: `int` (optional) - Max number of results to return for a page. + * **page_size**: `int` (optional) - Max number of results to return for a page. **Returns**: A [`WatchlistMembersListV2`][watchlistmemberslistv2-model] object. """ page_size = page_size or self._parent.settings.page_size - response = self._parent.session.get(f"{self._uri}/{watchlist_id}/members", params = {"page": page_num, "page_size": page_size}) + response = self._parent.session.get( + f"{self._uri}/{watchlist_id}/members", + params={"page": page_num, "page_size": page_size}, + ) return WatchlistMembersListV2.parse_response(response) - + def iter_all_members( - self, watchlist_id: Union[str, WatchlistType], page_size: int = None + self, watchlist_id: Union[str, WatchlistType], page_size: int = None ) -> Iterator[WatchlistActor]: """ Iterate over all members of a watchlist. These actors may have been added as an included actor, or are members of an included department, etc. @@ -239,7 +245,6 @@ def iter_all_members( if len(page.watchlist_members) < page_size: break - def add_included_actors(self, watchlist_id: str, actor_ids: Union[str, List[str]]): """ Include individual actors on a watchlist. @@ -299,7 +304,9 @@ def get_included_actor(self, watchlist_id: str, actor_id: str) -> WatchlistActor ) return WatchlistActor.parse_response(response) - def list_included_actors(self, watchlist_id: str, page_num: int = None, page_size: int = None) -> IncludedActorsList: + def list_included_actors( + self, watchlist_id: str, page_num: int = None, page_size: int = None + ) -> IncludedActorsList: """ List individual actors included on a watchlist. @@ -307,18 +314,19 @@ def list_included_actors(self, watchlist_id: str, page_num: int = None, page_siz * **watchlist_id**: `str` (required) - Watchlist ID. * **page_num**: `int` (optional) - The page number to fetch, starting at 1. - * **page_size**: `int` (optional) - Max number of results to return for a page. + * **page_size**: `int` (optional) - Max number of results to return for a page. **Returns**: An [`IncludedActorsList`][includedactorslist-model] object. """ page_size = page_size or self._parent.settings.page_size response = self._parent.session.get( - url=f"{self._uri}/{watchlist_id}/included-actors", params = {"page": page_num, "page_size": page_size} + url=f"{self._uri}/{watchlist_id}/included-actors", + params={"page": page_num, "page_size": page_size}, ) return IncludedActorsList.parse_response(response) - + def iter_all_included_actors( - self, watchlist_id: str, page_size: int = None + self, watchlist_id: str, page_size: int = None ) -> Iterator[WatchlistActor]: """ Iterate over all individual actors included on a watchlist. @@ -330,14 +338,12 @@ def iter_all_included_actors( page_size = page_size or self._parent.settings.page_size for page_num in count(1): page = self.list_included_actors( - watchlist_id = watchlist_id, - page_num=page_num, page_size=page_size + watchlist_id=watchlist_id, page_num=page_num, page_size=page_size ) yield from page.included_actors if len(page.included_actors) < page_size: break - def add_excluded_actors(self, watchlist_id: str, actor_ids: Union[str, List[str]]): """ Exclude individual actors from a watchlist. @@ -378,13 +384,15 @@ def remove_excluded_actors( url=f"{self._uri}/{watchlist_id}/excluded-actors/delete", json=data.dict() ) - def list_excluded_actors(self, watchlist_id: str, page_num: int = None, page_size: int = None) -> ExcludedActorsList: + def list_excluded_actors( + self, watchlist_id: str, page_num: int = None, page_size: int = None + ) -> ExcludedActorsList: """ List individual actors excluded from a watchlist. * **watchlist_id**: `str` (required) - Watchlist ID. * **page_num**: `int` (optional) - The page number to fetch, starting at 1. - * **page_size**: `int` (optional) - Max number of results to return for a page. + * **page_size**: `int` (optional) - Max number of results to return for a page. **Returns**: An [`ExcludedActorsList`][excludedactorslist-model] object. @@ -392,11 +400,13 @@ def list_excluded_actors(self, watchlist_id: str, page_num: int = None, page_siz page_size = page_size or self._parent.settings.page_size response = self._parent.session.get( f"{self._uri}/{watchlist_id}/excluded-actors", - params={"page": page_num, "page_size": page_size} + params={"page": page_num, "page_size": page_size}, ) return ExcludedActorsList.parse_response(response) - - def iter_all_excluded_actors(self, watchlist_id: str, page_size: int = None) -> Iterator[WatchlistActor]: + + def iter_all_excluded_actors( + self, watchlist_id: str, page_size: int = None + ) -> Iterator[WatchlistActor]: """ Iterate over all individual actors excluded from a watchlist. @@ -469,7 +479,9 @@ def remove_directory_groups( json=data.dict(), ) - def list_directory_groups(self, watchlist_id: str, page_num: int = None, page_size: int = None) -> IncludedDirectoryGroupsList: + def list_directory_groups( + self, watchlist_id: str, page_num: int = None, page_size: int = None + ) -> IncludedDirectoryGroupsList: """ List directory groups included on a watchlist. @@ -477,18 +489,20 @@ def list_directory_groups(self, watchlist_id: str, page_num: int = None, page_si * **watchlist_id**: `str` (required) - Watchlist ID. * **page_num**: `int` (optional) - The page number to fetch, starting at 1. - * **page_size**: `int` (optional) - Max number of results to return for a page. + * **page_size**: `int` (optional) - Max number of results to return for a page. **Returns**: An [`IncludedDirectoryGroupsList`][includeddirectorygroupslist-model] object. """ page_size = page_size or self._parent.settings.page_size response = self._parent.session.get( f"{self._uri}/{watchlist_id}/included-directory-groups", - params={"page": page_num, "page_size": page_size} + params={"page": page_num, "page_size": page_size}, ) return IncludedDirectoryGroupsList.parse_response(response) - - def iter_all_directory_groups(self, watchlist_id: str, page_size: int = None) -> Iterator[IncludedDirectoryGroup]: + + def iter_all_directory_groups( + self, watchlist_id: str, page_size: int = None + ) -> Iterator[IncludedDirectoryGroup]: """ Iterate over all directory groups included on a watchlist. @@ -505,7 +519,6 @@ def iter_all_directory_groups(self, watchlist_id: str, page_size: int = None) -> if len(page.included_directory_groups) < page_size: break - def get_directory_group( self, watchlist_id: str, group_id: str ) -> IncludedDirectoryGroup: @@ -570,7 +583,9 @@ def remove_departments( json=data.dict(), ) - def list_departments(self, watchlist_id: str, page_num: int = None, page_size: int = None) -> IncludedDepartmentsList: + def list_departments( + self, watchlist_id: str, page_num: int = None, page_size: int = None + ) -> IncludedDepartmentsList: """ List departments included on a watchlist. @@ -584,18 +599,20 @@ def list_departments(self, watchlist_id: str, page_num: int = None, page_size: i """ response = self._parent.session.get( f"{self._uri}/{watchlist_id}/included-departments", - params={"page": page_num, "page_size": page_size} + params={"page": page_num, "page_size": page_size}, ) return IncludedDepartmentsList.parse_response(response) - - def iter_all_departments(self, watchlist_id: str, page_size: int = None) -> Iterator[IncludedDepartment]: + + def iter_all_departments( + self, watchlist_id: str, page_size: int = None + ) -> Iterator[IncludedDepartment]: """ Iterate over all departments included on a watchlist. Accepts the same parameters as `.list_departments()` excepting `page_num`. **Returns**: A generator yielding individual [`IncludedDepartment`][includeddepartment-model] objects. - + """ page_size = page_size or self._parent.settings.page_size for page_num in count(1): @@ -606,7 +623,6 @@ def iter_all_departments(self, watchlist_id: str, page_size: int = None) -> Iter if len(page.included_departments) < page_size: break - def get_department(self, watchlist_id: str, department: str) -> IncludedDepartment: """ Get an included department from a watchlist. From 81e1b9268840b217db614a5d5825db8194c04f39 Mon Sep 17 00:00:00 2001 From: Cecilia Stevens <63068179+ceciliastevens@users.noreply.github.com> Date: Mon, 17 Mar 2025 12:53:32 -0400 Subject: [PATCH 09/11] fix audit bug --- CHANGELOG.md | 4 ++++ src/_incydr_sdk/audit_log/client.py | 2 +- tests/test_audit_log.py | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f3e98bd..90861755 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ - The CLI's `watchlists` commands now use the v2 watchlist API. These commands correctly use `actor_id` instead of `user_id`. While the previous user_id parameters will still work for now, we recommend that users switch as soon as possible to using actor_id instead. +### Fixed + +- A bug where the api endpoint used to download audit log events was incorrect. + ### Deprecated - Devices methods in the SDK and CLI are deprecated. Use the Agents methods instead. diff --git a/src/_incydr_sdk/audit_log/client.py b/src/_incydr_sdk/audit_log/client.py index db6a7e43..37b9b249 100644 --- a/src/_incydr_sdk/audit_log/client.py +++ b/src/_incydr_sdk/audit_log/client.py @@ -244,7 +244,7 @@ def download_events( ) download_response = self._parent.session.get( - f"/v1/audit/redeemDownloadToken?downloadToken={export_response.json()['downloadToken']}" + f"/v1/audit/redeem-download-token?downloadToken={export_response.json()['downloadToken']}" ) filename = get_filename_from_content_disposition( diff --git a/tests/test_audit_log.py b/tests/test_audit_log.py index bc60f901..4171d8eb 100644 --- a/tests/test_audit_log.py +++ b/tests/test_audit_log.py @@ -166,7 +166,7 @@ def test_download_events_when_default_params_makes_expected_calls( } httpserver_auth.expect_request( - "/v1/audit/redeemDownloadToken", query_string=export_event_data + "/v1/audit/redeem-download-token", query_string=export_event_data ).respond_with_json(redeem_events_data) client = Client() @@ -365,7 +365,7 @@ def test_cli_download_makes_expected_call( } httpserver_auth.expect_request( - "/v1/audit/redeemDownloadToken", query_string=export_event_data + "/v1/audit/redeem-download-token", query_string=export_event_data ).respond_with_json(redeem_events_data) result = runner.invoke(incydr, ["audit-log", "download", "--path", str(tmp_path)]) From 377b5bc5c9a9089744644d6a7336a184f4169dc1 Mon Sep 17 00:00:00 2001 From: Cecilia Stevens <63068179+ceciliastevens@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:05:08 -0400 Subject: [PATCH 10/11] fix models.py, models.md. move watchlists v1 client to its own file for easy removal once the deprecation period ends. --- docs/sdk/models.md | 28 + src/_incydr_cli/cmds/watchlists.py | 2 +- src/_incydr_sdk/watchlists/client.py | 622 +---------------- src/_incydr_sdk/watchlists/clientv1.py | 639 ++++++++++++++++++ .../watchlists/models/responses.py | 24 +- src/incydr/models.py | 9 + tests/test_watchlists.py | 6 +- 7 files changed, 693 insertions(+), 637 deletions(-) create mode 100644 src/_incydr_sdk/watchlists/clientv1.py diff --git a/docs/sdk/models.md b/docs/sdk/models.md index 99126e76..20d61b70 100644 --- a/docs/sdk/models.md +++ b/docs/sdk/models.md @@ -252,23 +252,51 @@ Risk Profiles have been replaced by [Actors](#actors). ::: incydr.models.WatchlistsPage :docstring: +### `WatchlistActor` model + +::: incydr.models.WatchlistActor + :docstring: + ### `WatchlistUser` model +WatchlistUser is deprecated. Use WatchlistActor instead. + ::: incydr.models.WatchlistUser :docstring: +### `WatchlistMembersListV2` model + +::: incydr.models.WatchlistMembersListV2 + :docstring: + ### `WatchlistMembersList` model +WatchlistMembersList is deprecated. Use WatchlistMembersListV2 instead. + ::: incydr.models.WatchlistMembersList :docstring: +### `IncludedActorsList` model + +::: incydr.models.IncludedActorsList + :docstring: + +### `ExcludedActorsList` model + +::: incydr.models.ExcludedActorsList + :docstring: + ### `IncludedUsersList` model +IncludedUsersList is deprecated. Use IncludedActorsList instead. + ::: incydr.models.IncludedUsersList :docstring: ### `ExcludedUsersList` model +ExcludedUsersList is deprecated. Use ExcludedActorsList instead. + ::: incydr.models.ExcludedUsersList :docstring: diff --git a/src/_incydr_cli/cmds/watchlists.py b/src/_incydr_cli/cmds/watchlists.py index e83b263f..830728ac 100644 --- a/src/_incydr_cli/cmds/watchlists.py +++ b/src/_incydr_cli/cmds/watchlists.py @@ -788,7 +788,7 @@ def _output_results(results, model, format_, columns=None): console.print(item.json(), highlight=False) -# ---------- Deprecated 2025-03 ---------- +# Deprecated 2025-03. Will be removed 2026-03. @watchlists.command(cls=IncydrCommand) diff --git a/src/_incydr_sdk/watchlists/client.py b/src/_incydr_sdk/watchlists/client.py index 73f9a8b0..cc3f9d01 100644 --- a/src/_incydr_sdk/watchlists/client.py +++ b/src/_incydr_sdk/watchlists/client.py @@ -2,34 +2,27 @@ from typing import Iterator from typing import List from typing import Union -from warnings import warn from _incydr_sdk.enums.watchlists import WatchlistType from _incydr_sdk.exceptions import WatchlistNotFoundError +from _incydr_sdk.watchlists.clientv1 import WatchlistsV1 from _incydr_sdk.watchlists.models.requests import CreateWatchlistRequest -from _incydr_sdk.watchlists.models.requests import ListWatchlistsRequest from _incydr_sdk.watchlists.models.requests import ListWatchlistsRequestV2 from _incydr_sdk.watchlists.models.requests import UpdateExcludedActorsRequest -from _incydr_sdk.watchlists.models.requests import UpdateExcludedUsersRequest from _incydr_sdk.watchlists.models.requests import UpdateIncludedActorsRequest from _incydr_sdk.watchlists.models.requests import UpdateIncludedDepartmentsRequest from _incydr_sdk.watchlists.models.requests import UpdateIncludedDirectoryGroupsRequest -from _incydr_sdk.watchlists.models.requests import UpdateIncludedUsersRequest from _incydr_sdk.watchlists.models.requests import UpdateWatchlistRequest from _incydr_sdk.watchlists.models.responses import ExcludedActorsList -from _incydr_sdk.watchlists.models.responses import ExcludedUsersList from _incydr_sdk.watchlists.models.responses import IncludedActorsList from _incydr_sdk.watchlists.models.responses import IncludedDepartment from _incydr_sdk.watchlists.models.responses import IncludedDepartmentsList from _incydr_sdk.watchlists.models.responses import IncludedDirectoryGroup from _incydr_sdk.watchlists.models.responses import IncludedDirectoryGroupsList -from _incydr_sdk.watchlists.models.responses import IncludedUsersList from _incydr_sdk.watchlists.models.responses import Watchlist from _incydr_sdk.watchlists.models.responses import WatchlistActor -from _incydr_sdk.watchlists.models.responses import WatchlistMembersList from _incydr_sdk.watchlists.models.responses import WatchlistMembersListV2 from _incydr_sdk.watchlists.models.responses import WatchlistsPage -from _incydr_sdk.watchlists.models.responses import WatchlistUser class WatchlistsClient: @@ -668,616 +661,3 @@ def _lookup_ids(self): if not watchlist_id: raise WatchlistNotFoundError(name) return watchlist_id - - -class WatchlistsV1: - """ - Client for `/v1/watchlists` endpoints. - - This client is deprecated. Use the WatchlistsV2 client instead. - - Usage example: - - >>> import incydr - >>> client = incydr.Client(**kwargs) - >>> client.watchlists.v1.get_page() - """ - - def __init__(self, parent): - self._parent = parent - self._watchlist_type_id_map = {} - self._uri = "/v1/watchlists" - - def get_page( - self, page_num: int = 1, page_size: int = None, user_id: str = None - ) -> WatchlistsPage: - """ - Get a page of watchlists. - - Filter results by passing appropriate parameters: - - **Parameters**: - - * **page_num**: `int` - Page number for results, starting at 1. - * **page_size**: `int` - Max number of results to return for a page. - * **user_id**: `str` - Matches watchlists where the user is a member. - - **Returns**: A [`WatchlistsPage`][watchlistspage-model] object. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - page_size = page_size or self._parent.settings.page_size - data = ListWatchlistsRequest(page=page_num, pageSize=page_size, userId=user_id) - response = self._parent.session.get(self._uri, params=data.dict()) - return WatchlistsPage.parse_response(response) - - def iter_all( - self, page_size: int = None, user_id: str = None - ) -> Iterator[Watchlist]: - """ - Iterate over all watchlists. - - Accepts the same parameters as `.get_page()` excepting `page_num`. - - **Returns**: A generator yielding individual [`Watchlist`][watchlist-model] objects. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - page_size = page_size or self._parent.settings.page_size - for page_num in count(1): - page = self.get_page( - page_num=page_num, page_size=page_size, user_id=user_id - ) - yield from page.watchlists - if len(page.watchlists) < page_size: - break - - def get(self, watchlist_id: str) -> Watchlist: - """ - Get a single watchlist. - - **Parameters**: - - * **watchlist_id**: `str` (required) - Watchlist ID. - - **Returns**: A [`Watchlist`][watchlist-model] object. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - response = self._parent.session.get(f"{self._uri}/{watchlist_id}") - return Watchlist.parse_response(response) - - def create( - self, watchlist_type: WatchlistType, title: str = None, description: str = None - ) -> Watchlist: - """ - Create a new watchlist. - - **Parameters**: - - * **watchlist_type**: [`WatchlistType`][watchlist-types] (required) - Type of the watchlist to create. - * **title**: The required title for a custom watchlist. - * **description**: The optional description for a custom watchlist. - - **Returns**: A ['Watchlist`][watchlist-model] object. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - if watchlist_type == "CUSTOM": - if title is None: - raise ValueError("`title` value is required for custom watchlists.") - - data = CreateWatchlistRequest( - description=description, title=title, watchlistType=watchlist_type - ) - response = self._parent.session.post(url=self._uri, json=data.dict()) - watchlist = Watchlist.parse_response(response) - self._watchlist_type_id_map[watchlist_type] = watchlist.watchlist_id - return watchlist - - def delete(self, watchlist_id: str): - """ - Delete a watchlist. - - **Parameters**: - - * **watchlist_id**: `str` (required) - Watchlist ID. - - **Returns**: A `requests.Response` indicating success. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - return self._parent.session.delete(f"{self._uri}/{watchlist_id}") - - def update( - self, watchlist_id: str, title: str = None, description: str = None - ) -> Watchlist: - """ - Update a custom watchlist. - - **Parameters**: - - * **watchlist_id**: `str` (required) - Watchlist ID. - * **title**: `str` - Updated title for a custom watchlist. Defaults to None. - * **description: `str` - Updated description for a custom watchlist. Pass an empty string to clear this field. Defaults to None. - - **Returns**: A [`Watchlist`][watchlist-model] object. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - paths = [] - if title: - paths += ["title"] - if description: - paths += ["description"] - query = {"paths": paths} - data = UpdateWatchlistRequest(description=description, title=title) - response = self._parent.session.patch( - f"{self._uri}/{watchlist_id}", params=query, json=data.dict() - ) - return Watchlist.parse_response(response) - - def get_member(self, watchlist_id: str, user_id: str) -> WatchlistUser: - """ - Get a single member of a watchlist. A member may have been added as an included user, or is a member of an included department, etc. - - **Parameters**: - - * **watchlist_id**: `str` (required) - Watchlist ID. - * **user_id**: `str` (required) - Unique user ID. - - **Returns**: A [`WatchlistUser`][watchlistuser-model] object. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - response = self._parent.session.get( - f"{self._uri}/{watchlist_id}/members/{user_id}" - ) - return WatchlistUser.parse_response(response) - - def list_members( - self, watchlist_id: Union[str, WatchlistType] - ) -> WatchlistMembersList: - """ - Get a list of all members of a watchlist. These users may have been added as an included user, or are members of an included department, etc. - - **Parameters**: - - * **watchlist_id**: `str`(required) - Watchlist ID. - - **Returns**: A [`WatchlistMembersList`][watchlistmemberslist-model] object. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - response = self._parent.session.get(f"{self._uri}/{watchlist_id}/members") - return WatchlistMembersList.parse_response(response) - - def add_included_users(self, watchlist_id: str, user_ids: Union[str, List[str]]): - """ - Include individual users on a watchlist. - - **Parameters**: - - * **watchlist**: `str` (required) - Watchlist ID. - * **user_ids**: `str`, `List[str]` (required) - List of unique user IDs to include on the watchlist. A maximum - of 100 users can be processed in a single request. - - **Returns**: A `requests.Response` indicating success. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - data = UpdateIncludedUsersRequest( - userIds=user_ids if isinstance(user_ids, List) else [user_ids], - watchlistId=watchlist_id, - ) - return self._parent.session.post( - url=f"{self._uri}/{watchlist_id}/included-users/add", json=data.dict() - ) - - def remove_included_users(self, watchlist_id: str, user_ids: Union[str, List[str]]): - """ - Remove included users from a watchlist. - - **Parameters**: - - * **watchlist**: `str` (required) - Watchlist ID. - * **user_ids**: `str`, `List[str]` (required) - List of unique user IDs to remove from the watchlist. A maximum - of 100 users can be processed in a single request. - - **Returns**: A `requests.Response` indicating success. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - - data = UpdateIncludedUsersRequest( - userIds=user_ids if isinstance(user_ids, List) else [user_ids], - watchlistId=watchlist_id, - ) - return self._parent.session.post( - url=f"{self._uri}/{watchlist_id}/included-users/delete", json=data.dict() - ) - - def get_included_user(self, watchlist_id: str, user_id: str) -> WatchlistUser: - """ - Get an included user from a watchlist. - - **Parameters**: - - * **watchlist_id**: `str` (required) - Watchlist ID. - * **user_id**: `str` (required) - Unique user ID. - - **Returns**: A [`WatchlistUser`][watchlistuser-model] object. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - response = self._parent.session.get( - url=f"{self._uri}/{watchlist_id}/included-users/{user_id}" - ) - return WatchlistUser.parse_response(response) - - def list_included_users(self, watchlist_id: str) -> IncludedUsersList: - """ - List individual users included on a watchlist. - - **Parameters**: - - * **watchlist_id**: `str` (required) - Watchlist ID. - - **Returns**: An [`IncludedUsersList`][includeduserslist-model] object. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - - response = self._parent.session.get( - url=f"{self._uri}/{watchlist_id}/included-users" - ) - return IncludedUsersList.parse_response(response) - - def add_excluded_users(self, watchlist_id: str, user_ids: Union[str, List[str]]): - """ - Exclude individual users from a watchlist. - - **Parameters**: - - * **watchlist**: `str` (required) - Watchlist ID. - * **user_ids**: `str`, `List[str]` (required) - List of unique user IDs to exclude from the watchlist. A maximum - of 100 users can be processed in a single request. - - **Returns**: A `requests.Response` indicating success. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - data = UpdateExcludedUsersRequest( - userIds=user_ids if isinstance(user_ids, List) else [user_ids], - ) - return self._parent.session.post( - url=f"{self._uri}/{watchlist_id}/excluded-users/add", json=data.dict() - ) - - def remove_excluded_users(self, watchlist_id: str, user_ids: Union[str, List[str]]): - """ - Remove excluded users from a watchlist. - - **Parameters**: - - * **watchlist**: `str` (required) - Watchlist ID. - * **user_ids**: `str`, `List[str]` (required) - List of unique user IDs to remove from the exclusion list. A - maximum of 100 users can be processed in a single request. - - **Returns**: A `requests.Response` indicating success. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - data = UpdateExcludedUsersRequest( - userIds=user_ids if isinstance(user_ids, List) else [user_ids], - ) - return self._parent.session.post( - url=f"{self._uri}/{watchlist_id}/excluded-users/delete", json=data.dict() - ) - - def list_excluded_users(self, watchlist_id: str) -> ExcludedUsersList: - """ - List individual users excluded from a watchlist. - - * **watchlist_id**: `str` (required) - Watchlist ID. - - **Returns**: An [`ExcludedUsersList`][excludeduserslist-model] object. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - response = self._parent.session.get( - f"{self._uri}/{watchlist_id}/excluded-users" - ) - return ExcludedUsersList.parse_response(response) - - def get_excluded_user(self, watchlist_id: str, user_id: str) -> WatchlistUser: - """ - Get an excluded user from a watchlist. - - **Parameters**: - - * **watchlist_id**: `str` (required) - Watchlist ID. - * **user_id**: `str` (required) - Unique user ID. - - **Returns**: A [`WatchlistUser`][watchlistuser-model] object. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - response = self._parent.session.get( - f"{self._uri}/{watchlist_id}/excluded-users/{user_id}" - ) - return WatchlistUser.parse_response(response) - - def add_directory_groups(self, watchlist_id: str, group_ids: Union[str, List[str]]): - """ - Include directory groups on a watchlist. Use the `directory_groups` client to see available directory groups. - - **Parameters**: - - * **watchlist**: `str` (required) - Watchlist ID. - * **group_ids**: `str`, `List[str]` (required) - List of directory group IDs to include on the watchlist. - - **Returns**: A `requests.Response` indicating success. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - data = UpdateIncludedDirectoryGroupsRequest( - groupIds=group_ids if isinstance(group_ids, List) else [group_ids] - ) - return self._parent.session.post( - url=f"{self._uri}/{watchlist_id}/included-directory-groups/add", - json=data.dict(), - ) - - def remove_directory_groups( - self, watchlist_id: str, group_ids: Union[str, List[str]] - ): - """ - Remove included directory groups from a watchlist. - - **Parameters**: - - * **watchlist**: `str` (required) - Watchlist ID. - * **group_ids**: `str`, `List[str]` (required) - List of directory group IDs to remove from the watchlist. - - **Returns**: A `requests.Response` indicating success. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - data = UpdateIncludedDirectoryGroupsRequest( - groupIds=group_ids if isinstance(group_ids, List) else [group_ids] - ) - return self._parent.session.post( - url=f"{self._uri}/{watchlist_id}/included-directory-groups/delete", - json=data.dict(), - ) - - def list_directory_groups(self, watchlist_id: str) -> IncludedDirectoryGroupsList: - """ - List directory groups included on a watchlist. - - **Parameters**: - - * **watchlist_id**: `str` (required) - Watchlist ID. - - **Returns**: An [`IncludedUsersList`][includeduserslist-model] object. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - - response = self._parent.session.get( - f"{self._uri}/{watchlist_id}/included-directory-groups" - ) - return IncludedDirectoryGroupsList.parse_response(response) - - def get_directory_group( - self, watchlist_id: str, group_id: str - ) -> IncludedDirectoryGroup: - """ - Get an included directory group from a watchlist. - - **Parameters**: - - * **watchlist_id**: `str` (required) - Watchlist ID. - * **group_id**: `str` (required) - Directory group ID. - - **Returns**: An [`IncludedDirectoryGroup`][includeddirectorygroup-model] object. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - - response = self._parent.session.get( - f"{self._uri}/{watchlist_id}/included-directory-groups/{group_id}" - ) - return IncludedDirectoryGroup.parse_response(response) - - def add_departments( - self, - watchlist_id: str, - departments: Union[str, List[str]], - ): - """ - Include departments on a watchlist. Use the `departments` client to see available departments. - - **Parameters**: - - * **watchlist**: `str` (required) - Watchlist ID. - * **departments**: `str`, `List[str]` (required) - List of departments to include on the watchlist. - - **Returns**: A `requests.Response` indicating success. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - data = UpdateIncludedDepartmentsRequest( - departments=departments if isinstance(departments, List) else [departments] - ) - return self._parent.session.post( - url=f"{self._uri}/{watchlist_id}/included-departments/add", json=data.dict() - ) - - def remove_departments( - self, - watchlist_id: str, - departments: Union[str, List[str]], - ): - """ - Remove included departments from a watchlist. - - **Parameters**: - - * **watchlist**: `str` - Watchlist ID or a watchlist type. An ID must be provided for custom watchlists. - * **departments**: `str`, `List[str]` (required) - List of departments to remove from the watchlist. - - **Returns**: A `requests.Response` indicating success. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - data = UpdateIncludedDepartmentsRequest( - departments=departments if isinstance(departments, List) else [departments] - ) - return self._parent.session.post( - url=f"{self._uri}/{watchlist_id}/included-departments/delete", - json=data.dict(), - ) - - def list_departments(self, watchlist_id: str) -> IncludedDepartmentsList: - """ - List departments included on a watchlist. - - **Parameters**: - - * **watchlist_id**: `str` (required) - Watchlist ID. - - **Returns**: An [`IncludedDepartmentsList`][includeddepartmentslist-model] object. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - response = self._parent.session.get( - f"{self._uri}/{watchlist_id}/included-departments" - ) - return IncludedDepartmentsList.parse_response(response) - - def get_department(self, watchlist_id: str, department: str) -> IncludedDepartment: - """ - Get an included department from a watchlist. - - **Parameters**: - - * **watchlist_id**: `str` (required) - Watchlist ID. - * **department**: `str` (required) - A included department. - - **Returns**: An [`IncludedDepartment`][includeddepartment-model] object. - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - response = self._parent.session.get( - f"{self._uri}/{watchlist_id}/included-departments/{department}" - ) - return IncludedDepartment.parse_response(response) - - def get_id_by_name(self, name: Union[str, WatchlistType]): - """ - Get a watchlist ID by either its type (ex: `DEPARTING_EMPLOYEE`) or its title in the case of `CUSTOM` watchlists. - - **Parameters**: - - * **name**: `str`, [`WatchlistType`][watchlist-types] (required) - A `WatchlistType` or in the case of `CUSTOM` watchlists, the watchlist `title`. - - **Returns**: A watchlist ID (`str`). - """ - warn( - "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", - DeprecationWarning, - stacklevel=2, - ) - - def _lookup_ids(self): - """Map watchlist types to IDs, if they exist.""" - self._watchlist_type_id_map = {} - watchlists = self.get_page(page_size=100).watchlists - for item in watchlists: - if item.list_type == "CUSTOM": - # store title for custom lists instead of list_type - self._watchlist_type_id_map[item.title] = item.watchlist_id - self._watchlist_type_id_map[item.list_type] = item.watchlist_id - - watchlist_id = self._watchlist_type_id_map.get(name) - if not watchlist_id: - # if not found, reset ID cache - _lookup_ids(self) - watchlist_id = self._watchlist_type_id_map.get(name) - if not watchlist_id: - raise WatchlistNotFoundError(name) - return watchlist_id diff --git a/src/_incydr_sdk/watchlists/clientv1.py b/src/_incydr_sdk/watchlists/clientv1.py new file mode 100644 index 00000000..0a2d4461 --- /dev/null +++ b/src/_incydr_sdk/watchlists/clientv1.py @@ -0,0 +1,639 @@ +from itertools import count +from typing import Iterator +from typing import List +from typing import Union +from warnings import warn + +from _incydr_sdk.enums.watchlists import WatchlistType +from _incydr_sdk.exceptions import WatchlistNotFoundError +from _incydr_sdk.watchlists.models.requests import CreateWatchlistRequest +from _incydr_sdk.watchlists.models.requests import ListWatchlistsRequest +from _incydr_sdk.watchlists.models.requests import UpdateExcludedUsersRequest +from _incydr_sdk.watchlists.models.requests import UpdateIncludedDepartmentsRequest +from _incydr_sdk.watchlists.models.requests import UpdateIncludedDirectoryGroupsRequest +from _incydr_sdk.watchlists.models.requests import UpdateIncludedUsersRequest +from _incydr_sdk.watchlists.models.requests import UpdateWatchlistRequest +from _incydr_sdk.watchlists.models.responses import ExcludedUsersList +from _incydr_sdk.watchlists.models.responses import IncludedDepartment +from _incydr_sdk.watchlists.models.responses import IncludedDepartmentsList +from _incydr_sdk.watchlists.models.responses import IncludedDirectoryGroup +from _incydr_sdk.watchlists.models.responses import IncludedDirectoryGroupsList +from _incydr_sdk.watchlists.models.responses import IncludedUsersList +from _incydr_sdk.watchlists.models.responses import Watchlist +from _incydr_sdk.watchlists.models.responses import WatchlistMembersList +from _incydr_sdk.watchlists.models.responses import WatchlistsPage +from _incydr_sdk.watchlists.models.responses import WatchlistUser + +# This client is deprecated 2025-03. It will be removed 2026-03. + +class WatchlistsV1: + """ + Client for `/v1/watchlists` endpoints. + + This client is deprecated. Use the WatchlistsV2 client instead. + + Usage example: + + >>> import incydr + >>> client = incydr.Client(**kwargs) + >>> client.watchlists.v1.get_page() + """ + + def __init__(self, parent): + self._parent = parent + self._watchlist_type_id_map = {} + self._uri = "/v1/watchlists" + + def get_page( + self, page_num: int = 1, page_size: int = None, user_id: str = None + ) -> WatchlistsPage: + """ + Get a page of watchlists. + + Filter results by passing appropriate parameters: + + **Parameters**: + + * **page_num**: `int` - Page number for results, starting at 1. + * **page_size**: `int` - Max number of results to return for a page. + * **user_id**: `str` - Matches watchlists where the user is a member. + + **Returns**: A [`WatchlistsPage`][watchlistspage-model] object. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + page_size = page_size or self._parent.settings.page_size + data = ListWatchlistsRequest(page=page_num, pageSize=page_size, userId=user_id) + response = self._parent.session.get(self._uri, params=data.dict()) + return WatchlistsPage.parse_response(response) + + def iter_all( + self, page_size: int = None, user_id: str = None + ) -> Iterator[Watchlist]: + """ + Iterate over all watchlists. + + Accepts the same parameters as `.get_page()` excepting `page_num`. + + **Returns**: A generator yielding individual [`Watchlist`][watchlist-model] objects. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + page_size = page_size or self._parent.settings.page_size + for page_num in count(1): + page = self.get_page( + page_num=page_num, page_size=page_size, user_id=user_id + ) + yield from page.watchlists + if len(page.watchlists) < page_size: + break + + def get(self, watchlist_id: str) -> Watchlist: + """ + Get a single watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + + **Returns**: A [`Watchlist`][watchlist-model] object. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + response = self._parent.session.get(f"{self._uri}/{watchlist_id}") + return Watchlist.parse_response(response) + + def create( + self, watchlist_type: WatchlistType, title: str = None, description: str = None + ) -> Watchlist: + """ + Create a new watchlist. + + **Parameters**: + + * **watchlist_type**: [`WatchlistType`][watchlist-types] (required) - Type of the watchlist to create. + * **title**: The required title for a custom watchlist. + * **description**: The optional description for a custom watchlist. + + **Returns**: A ['Watchlist`][watchlist-model] object. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + if watchlist_type == "CUSTOM": + if title is None: + raise ValueError("`title` value is required for custom watchlists.") + + data = CreateWatchlistRequest( + description=description, title=title, watchlistType=watchlist_type + ) + response = self._parent.session.post(url=self._uri, json=data.dict()) + watchlist = Watchlist.parse_response(response) + self._watchlist_type_id_map[watchlist_type] = watchlist.watchlist_id + return watchlist + + def delete(self, watchlist_id: str): + """ + Delete a watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + + **Returns**: A `requests.Response` indicating success. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + return self._parent.session.delete(f"{self._uri}/{watchlist_id}") + + def update( + self, watchlist_id: str, title: str = None, description: str = None + ) -> Watchlist: + """ + Update a custom watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + * **title**: `str` - Updated title for a custom watchlist. Defaults to None. + * **description: `str` - Updated description for a custom watchlist. Pass an empty string to clear this field. Defaults to None. + + **Returns**: A [`Watchlist`][watchlist-model] object. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + paths = [] + if title: + paths += ["title"] + if description: + paths += ["description"] + query = {"paths": paths} + data = UpdateWatchlistRequest(description=description, title=title) + response = self._parent.session.patch( + f"{self._uri}/{watchlist_id}", params=query, json=data.dict() + ) + return Watchlist.parse_response(response) + + def get_member(self, watchlist_id: str, user_id: str) -> WatchlistUser: + """ + Get a single member of a watchlist. A member may have been added as an included user, or is a member of an included department, etc. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + * **user_id**: `str` (required) - Unique user ID. + + **Returns**: A [`WatchlistUser`][watchlistuser-model] object. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + response = self._parent.session.get( + f"{self._uri}/{watchlist_id}/members/{user_id}" + ) + return WatchlistUser.parse_response(response) + + def list_members( + self, watchlist_id: Union[str, WatchlistType] + ) -> WatchlistMembersList: + """ + Get a list of all members of a watchlist. These users may have been added as an included user, or are members of an included department, etc. + + **Parameters**: + + * **watchlist_id**: `str`(required) - Watchlist ID. + + **Returns**: A [`WatchlistMembersList`][watchlistmemberslist-model] object. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + response = self._parent.session.get(f"{self._uri}/{watchlist_id}/members") + return WatchlistMembersList.parse_response(response) + + def add_included_users(self, watchlist_id: str, user_ids: Union[str, List[str]]): + """ + Include individual users on a watchlist. + + **Parameters**: + + * **watchlist**: `str` (required) - Watchlist ID. + * **user_ids**: `str`, `List[str]` (required) - List of unique user IDs to include on the watchlist. A maximum + of 100 users can be processed in a single request. + + **Returns**: A `requests.Response` indicating success. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + data = UpdateIncludedUsersRequest( + userIds=user_ids if isinstance(user_ids, List) else [user_ids], + watchlistId=watchlist_id, + ) + return self._parent.session.post( + url=f"{self._uri}/{watchlist_id}/included-users/add", json=data.dict() + ) + + def remove_included_users(self, watchlist_id: str, user_ids: Union[str, List[str]]): + """ + Remove included users from a watchlist. + + **Parameters**: + + * **watchlist**: `str` (required) - Watchlist ID. + * **user_ids**: `str`, `List[str]` (required) - List of unique user IDs to remove from the watchlist. A maximum + of 100 users can be processed in a single request. + + **Returns**: A `requests.Response` indicating success. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + + data = UpdateIncludedUsersRequest( + userIds=user_ids if isinstance(user_ids, List) else [user_ids], + watchlistId=watchlist_id, + ) + return self._parent.session.post( + url=f"{self._uri}/{watchlist_id}/included-users/delete", json=data.dict() + ) + + def get_included_user(self, watchlist_id: str, user_id: str) -> WatchlistUser: + """ + Get an included user from a watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + * **user_id**: `str` (required) - Unique user ID. + + **Returns**: A [`WatchlistUser`][watchlistuser-model] object. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + response = self._parent.session.get( + url=f"{self._uri}/{watchlist_id}/included-users/{user_id}" + ) + return WatchlistUser.parse_response(response) + + def list_included_users(self, watchlist_id: str) -> IncludedUsersList: + """ + List individual users included on a watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + + **Returns**: An [`IncludedUsersList`][includeduserslist-model] object. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + + response = self._parent.session.get( + url=f"{self._uri}/{watchlist_id}/included-users" + ) + return IncludedUsersList.parse_response(response) + + def add_excluded_users(self, watchlist_id: str, user_ids: Union[str, List[str]]): + """ + Exclude individual users from a watchlist. + + **Parameters**: + + * **watchlist**: `str` (required) - Watchlist ID. + * **user_ids**: `str`, `List[str]` (required) - List of unique user IDs to exclude from the watchlist. A maximum + of 100 users can be processed in a single request. + + **Returns**: A `requests.Response` indicating success. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + data = UpdateExcludedUsersRequest( + userIds=user_ids if isinstance(user_ids, List) else [user_ids], + ) + return self._parent.session.post( + url=f"{self._uri}/{watchlist_id}/excluded-users/add", json=data.dict() + ) + + def remove_excluded_users(self, watchlist_id: str, user_ids: Union[str, List[str]]): + """ + Remove excluded users from a watchlist. + + **Parameters**: + + * **watchlist**: `str` (required) - Watchlist ID. + * **user_ids**: `str`, `List[str]` (required) - List of unique user IDs to remove from the exclusion list. A + maximum of 100 users can be processed in a single request. + + **Returns**: A `requests.Response` indicating success. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + data = UpdateExcludedUsersRequest( + userIds=user_ids if isinstance(user_ids, List) else [user_ids], + ) + return self._parent.session.post( + url=f"{self._uri}/{watchlist_id}/excluded-users/delete", json=data.dict() + ) + + def list_excluded_users(self, watchlist_id: str) -> ExcludedUsersList: + """ + List individual users excluded from a watchlist. + + * **watchlist_id**: `str` (required) - Watchlist ID. + + **Returns**: An [`ExcludedUsersList`][excludeduserslist-model] object. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + response = self._parent.session.get( + f"{self._uri}/{watchlist_id}/excluded-users" + ) + return ExcludedUsersList.parse_response(response) + + def get_excluded_user(self, watchlist_id: str, user_id: str) -> WatchlistUser: + """ + Get an excluded user from a watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + * **user_id**: `str` (required) - Unique user ID. + + **Returns**: A [`WatchlistUser`][watchlistuser-model] object. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + response = self._parent.session.get( + f"{self._uri}/{watchlist_id}/excluded-users/{user_id}" + ) + return WatchlistUser.parse_response(response) + + def add_directory_groups(self, watchlist_id: str, group_ids: Union[str, List[str]]): + """ + Include directory groups on a watchlist. Use the `directory_groups` client to see available directory groups. + + **Parameters**: + + * **watchlist**: `str` (required) - Watchlist ID. + * **group_ids**: `str`, `List[str]` (required) - List of directory group IDs to include on the watchlist. + + **Returns**: A `requests.Response` indicating success. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + data = UpdateIncludedDirectoryGroupsRequest( + groupIds=group_ids if isinstance(group_ids, List) else [group_ids] + ) + return self._parent.session.post( + url=f"{self._uri}/{watchlist_id}/included-directory-groups/add", + json=data.dict(), + ) + + def remove_directory_groups( + self, watchlist_id: str, group_ids: Union[str, List[str]] + ): + """ + Remove included directory groups from a watchlist. + + **Parameters**: + + * **watchlist**: `str` (required) - Watchlist ID. + * **group_ids**: `str`, `List[str]` (required) - List of directory group IDs to remove from the watchlist. + + **Returns**: A `requests.Response` indicating success. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + data = UpdateIncludedDirectoryGroupsRequest( + groupIds=group_ids if isinstance(group_ids, List) else [group_ids] + ) + return self._parent.session.post( + url=f"{self._uri}/{watchlist_id}/included-directory-groups/delete", + json=data.dict(), + ) + + def list_directory_groups(self, watchlist_id: str) -> IncludedDirectoryGroupsList: + """ + List directory groups included on a watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + + **Returns**: An [`IncludedUsersList`][includeduserslist-model] object. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + + response = self._parent.session.get( + f"{self._uri}/{watchlist_id}/included-directory-groups" + ) + return IncludedDirectoryGroupsList.parse_response(response) + + def get_directory_group( + self, watchlist_id: str, group_id: str + ) -> IncludedDirectoryGroup: + """ + Get an included directory group from a watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + * **group_id**: `str` (required) - Directory group ID. + + **Returns**: An [`IncludedDirectoryGroup`][includeddirectorygroup-model] object. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + + response = self._parent.session.get( + f"{self._uri}/{watchlist_id}/included-directory-groups/{group_id}" + ) + return IncludedDirectoryGroup.parse_response(response) + + def add_departments( + self, + watchlist_id: str, + departments: Union[str, List[str]], + ): + """ + Include departments on a watchlist. Use the `departments` client to see available departments. + + **Parameters**: + + * **watchlist**: `str` (required) - Watchlist ID. + * **departments**: `str`, `List[str]` (required) - List of departments to include on the watchlist. + + **Returns**: A `requests.Response` indicating success. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + data = UpdateIncludedDepartmentsRequest( + departments=departments if isinstance(departments, List) else [departments] + ) + return self._parent.session.post( + url=f"{self._uri}/{watchlist_id}/included-departments/add", json=data.dict() + ) + + def remove_departments( + self, + watchlist_id: str, + departments: Union[str, List[str]], + ): + """ + Remove included departments from a watchlist. + + **Parameters**: + + * **watchlist**: `str` - Watchlist ID or a watchlist type. An ID must be provided for custom watchlists. + * **departments**: `str`, `List[str]` (required) - List of departments to remove from the watchlist. + + **Returns**: A `requests.Response` indicating success. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + data = UpdateIncludedDepartmentsRequest( + departments=departments if isinstance(departments, List) else [departments] + ) + return self._parent.session.post( + url=f"{self._uri}/{watchlist_id}/included-departments/delete", + json=data.dict(), + ) + + def list_departments(self, watchlist_id: str) -> IncludedDepartmentsList: + """ + List departments included on a watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + + **Returns**: An [`IncludedDepartmentsList`][includeddepartmentslist-model] object. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + response = self._parent.session.get( + f"{self._uri}/{watchlist_id}/included-departments" + ) + return IncludedDepartmentsList.parse_response(response) + + def get_department(self, watchlist_id: str, department: str) -> IncludedDepartment: + """ + Get an included department from a watchlist. + + **Parameters**: + + * **watchlist_id**: `str` (required) - Watchlist ID. + * **department**: `str` (required) - A included department. + + **Returns**: An [`IncludedDepartment`][includeddepartment-model] object. + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + response = self._parent.session.get( + f"{self._uri}/{watchlist_id}/included-departments/{department}" + ) + return IncludedDepartment.parse_response(response) + + def get_id_by_name(self, name: Union[str, WatchlistType]): + """ + Get a watchlist ID by either its type (ex: `DEPARTING_EMPLOYEE`) or its title in the case of `CUSTOM` watchlists. + + **Parameters**: + + * **name**: `str`, [`WatchlistType`][watchlist-types] (required) - A `WatchlistType` or in the case of `CUSTOM` watchlists, the watchlist `title`. + + **Returns**: A watchlist ID (`str`). + """ + warn( + "WatchlistsV1 is deprecated and replaced by WatchlistsV2.", + DeprecationWarning, + stacklevel=2, + ) + + def _lookup_ids(self): + """Map watchlist types to IDs, if they exist.""" + self._watchlist_type_id_map = {} + watchlists = self.get_page(page_size=100).watchlists + for item in watchlists: + if item.list_type == "CUSTOM": + # store title for custom lists instead of list_type + self._watchlist_type_id_map[item.title] = item.watchlist_id + self._watchlist_type_id_map[item.list_type] = item.watchlist_id + + watchlist_id = self._watchlist_type_id_map.get(name) + if not watchlist_id: + # if not found, reset ID cache + _lookup_ids(self) + watchlist_id = self._watchlist_type_id_map.get(name) + if not watchlist_id: + raise WatchlistNotFoundError(name) + return watchlist_id diff --git a/src/_incydr_sdk/watchlists/models/responses.py b/src/_incydr_sdk/watchlists/models/responses.py index 26f75f4a..d8ab7abe 100644 --- a/src/_incydr_sdk/watchlists/models/responses.py +++ b/src/_incydr_sdk/watchlists/models/responses.py @@ -90,14 +90,14 @@ class WatchlistActor(ResponseModel): * **added_time**: `datetime` - The time the user was associated with the watchlist. * **actor_id**: `str` - Unique actor ID. - * **actorname**: `str - Actor name. + * **actor_name**: `str - Actor name. """ added_time: datetime = Field(None, alias="addedTime") actor_id: Optional[str] = Field( None, description="A unique actor ID.", example="23", alias="actorId" ) - actorname: Optional[str] = Field(None, example="foo@bar.com") + actor_name: Optional[str] = Field(None, example="foo@bar.com", alias="actorname") class ExcludedUsersList(ResponseModel): @@ -189,14 +189,14 @@ class IncludedUsersList(ResponseModel): A model representing a list of users included on a watchlist. Included users are those that have been individually included on that list. - * **included_users**: `List[WatchlistUser]` - The list of included users or actors. - * **total_count**: `int` - The total count of all included users or actors. + * **included_users**: `List[WatchlistUser]` - The list of included users. + * **total_count**: `int` - The total count of all included users. """ included_users: Optional[List[WatchlistUser]] = Field(None, alias="includedUsers") total_count: Optional[int] = Field( None, - description="The total count of all included users or actors.", + description="The total count of all included users.", example=10, alias="totalCount", ) @@ -207,8 +207,8 @@ class IncludedActorsList(ResponseModel): A model representing a list of actors included on a watchlist. Included users are those that have been individually included on that list. - * **included_actors**: `List[WatchlistActor]` - The list of included users or actors. - * **total_count**: `int` - The total count of all included users or actors. + * **included_actors**: `List[WatchlistActor]` - The list of included actors. + * **total_count**: `int` - The total count of all included actors. """ included_actors: Optional[List[WatchlistActor]] = Field( @@ -216,7 +216,7 @@ class IncludedActorsList(ResponseModel): ) total_count: Optional[int] = Field( None, - description="The total count of all included users or actors.", + description="The total count of all included actors.", example=10, alias="totalCount", ) @@ -271,13 +271,13 @@ class WatchlistMembersList(ResponseModel): **Fields**: - * **watchlist_members**: `Union[List[WatchlistUser], List[WatchlistActor]]` - The list of watchlist members. + * **watchlist_members**: `List[WatchlistUser]` - The list of watchlist members. * **total_count**: `int` - Total count of members on the watchlist. """ total_count: Optional[int] = Field( None, - description="The total count of all included users or actors.", + description="The total count of all included users.", example=10, alias="totalCount", ) @@ -295,13 +295,13 @@ class WatchlistMembersListV2(ResponseModel): **Fields**: - * **watchlist_members**: `Union[List[WatchlistUser], List[WatchlistActor]]` - The list of watchlist members. + * **watchlist_members**: `List[WatchlistActor]` - The list of watchlist members. * **total_count**: `int` - Total count of members on the watchlist. """ total_count: Optional[int] = Field( None, - description="The total count of all included users or actors.", + description="The total count of all included actors.", example=10, alias="totalCount", ) diff --git a/src/incydr/models.py b/src/incydr/models.py index 62677a43..44e59aa7 100644 --- a/src/incydr/models.py +++ b/src/incydr/models.py @@ -33,17 +33,22 @@ from _incydr_sdk.users.models import UpdateRolesResponse from _incydr_sdk.users.models import UserRole from _incydr_sdk.users.models import UsersPage +from _incydr_sdk.watchlists.models.responses import ExcludedActorsList from _incydr_sdk.watchlists.models.responses import ExcludedUsersList +from _incydr_sdk.watchlists.models.responses import IncludedActorsList from _incydr_sdk.watchlists.models.responses import IncludedDepartment from _incydr_sdk.watchlists.models.responses import IncludedDepartmentsList from _incydr_sdk.watchlists.models.responses import IncludedDirectoryGroup from _incydr_sdk.watchlists.models.responses import IncludedDirectoryGroupsList from _incydr_sdk.watchlists.models.responses import IncludedUsersList from _incydr_sdk.watchlists.models.responses import Watchlist +from _incydr_sdk.watchlists.models.responses import WatchlistActor from _incydr_sdk.watchlists.models.responses import WatchlistMembersList +from _incydr_sdk.watchlists.models.responses import WatchlistMembersListV2 from _incydr_sdk.watchlists.models.responses import WatchlistsPage from _incydr_sdk.watchlists.models.responses import WatchlistUser + __all__ = [ "Actor", "ActorFamily", @@ -79,9 +84,13 @@ "DirectoryGroup", "Watchlist", "WatchlistsPage", + "WatchlistMembersListV2", "WatchlistMembersList", + "IncludedActorsList", + "ExcludedActorsList", "ExcludedUsersList", "IncludedUsersList", + "WatchlistActor", "WatchlistUser", "IncludedDepartmentsList", "IncludedDepartment", diff --git a/tests/test_watchlists.py b/tests/test_watchlists.py index b1882497..edb454b2 100644 --- a/tests/test_watchlists.py +++ b/tests/test_watchlists.py @@ -498,7 +498,7 @@ def test_get_member_returns_expected_data_v2(mock_get_member_v2): member = c.watchlists.v2.get_member(TEST_WATCHLIST_ID, TEST_ID) assert isinstance(member, WatchlistActor) assert member.actor_id == TEST_ID - assert member.actorname == "foo@bar.com" + assert member.actor_name == "foo@bar.com" assert member.added_time == datetime.datetime.fromisoformat( TEST_ACTOR_1["addedTime"].replace("Z", "+00:00") ) @@ -569,7 +569,7 @@ def test_get_included_user_returns_expected_data_v2(mock_get_included_actor): user = c.watchlists.v2.get_included_actor(TEST_WATCHLIST_ID, TEST_ID) assert isinstance(user, WatchlistActor) assert user.actor_id == TEST_ID - assert user.actorname == "foo@bar.com" + assert user.actor_name == "foo@bar.com" assert user.added_time == datetime.datetime.fromisoformat( TEST_ACTOR_1["addedTime"].replace("Z", "+00:00") ) @@ -619,7 +619,7 @@ def test_get_excluded_user_returns_expected_data_v2(mock_get_excluded_actor): user = c.watchlists.v2.get_excluded_actor(TEST_WATCHLIST_ID, TEST_ID) assert isinstance(user, WatchlistActor) assert user.actor_id == TEST_ID - assert user.actorname == "foo@bar.com" + assert user.actor_name == "foo@bar.com" assert user.added_time == datetime.datetime.fromisoformat( TEST_ACTOR_1["addedTime"].replace("Z", "+00:00") ) From b96388a78106810fbc08d9f07f178a3ec9e38471 Mon Sep 17 00:00:00 2001 From: Cecilia Stevens <63068179+ceciliastevens@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:06:39 -0400 Subject: [PATCH 11/11] style --- src/_incydr_sdk/watchlists/clientv1.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_incydr_sdk/watchlists/clientv1.py b/src/_incydr_sdk/watchlists/clientv1.py index 0a2d4461..33b707ff 100644 --- a/src/_incydr_sdk/watchlists/clientv1.py +++ b/src/_incydr_sdk/watchlists/clientv1.py @@ -26,6 +26,7 @@ # This client is deprecated 2025-03. It will be removed 2026-03. + class WatchlistsV1: """ Client for `/v1/watchlists` endpoints.