Skip to content

Commit 77a1054

Browse files
INTEG-3102 - list-devices bugfix. Also adds list-agents command.
1 parent ea2a71f commit 77a1054

File tree

7 files changed

+219
-2
lines changed

7 files changed

+219
-2
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@
99
how a consumer would use the library or CLI tool (e.g. adding unit tests, updating documentation, etc) are not captured
1010
here.
1111

12+
## Unreleased
13+
14+
### Added
15+
- The `incydr users list-agents` command to list all agents associated with a user.
16+
17+
### Deprecated
18+
- The `incydr users list-devices` command is now properly marked as deprecated. Use `incydr users list-agents` instead.
19+
- The `sdk.users.v1.get_devices` method is now properly marked as deprecated. Use `sdk.agents.v1.iter_all` instead.
20+
21+
### Fixed
22+
- A bug where `sdk.users.v1.get_devices` would cause an error.
23+
1224
## 2.8.1 - 2026-01-21
1325

1426
### Added

src/_incydr_cli/cmds/users.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121
from _incydr_cli.cmds.options.output_options import table_format_option
2222
from _incydr_cli.cmds.options.output_options import TableFormat
2323
from _incydr_cli.cmds.options.utils import user_lookup_callback
24+
from _incydr_cli.cmds.utils import deprecation_warning
2425
from _incydr_cli.cmds.utils import user_lookup
2526
from _incydr_cli.core import IncydrCommand
2627
from _incydr_cli.core import IncydrGroup
2728
from _incydr_cli.file_readers import AutoDecodedFile
29+
from _incydr_sdk.agents.models import Agent
2830
from _incydr_sdk.core.client import Client
2931
from _incydr_sdk.devices.models import Device
3032
from _incydr_sdk.users.client import RoleNotFoundError
@@ -136,8 +138,9 @@ def list_devices(
136138
columns: Optional[str],
137139
):
138140
"""
139-
List devices associated with a particular user.
141+
DEPRECATED - use list-agents instead.
140142
"""
143+
deprecation_warning("DEPRECATED. Use list-agents instead.")
141144
client = Client()
142145
devices = client.users.v1.get_devices(user).devices
143146

@@ -164,6 +167,45 @@ def list_devices(
164167
click.echo(item.json())
165168

166169

170+
@users.command(cls=IncydrCommand)
171+
@user_arg
172+
@table_format_option
173+
@columns_option
174+
@logging_options
175+
def list_agents(
176+
user,
177+
format_: TableFormat,
178+
columns: Optional[str],
179+
):
180+
"""
181+
List agents associated with a particular user.
182+
"""
183+
client = Client()
184+
if "@" in user:
185+
user = client.users.v1.get_user(user).user_id
186+
devices = list(client.agents.v1.iter_all(user_id=user))
187+
188+
if format_ == TableFormat.csv:
189+
render.csv(Agent, devices, columns=columns, flat=True)
190+
elif format_ == TableFormat.table:
191+
columns = columns or [
192+
"agent_id",
193+
"name",
194+
"os_hostname",
195+
"active",
196+
"agent_type",
197+
"agent_health_issue_types",
198+
"last_connected",
199+
]
200+
render.table(Agent, devices, columns=columns, flat=False)
201+
elif format_ == TableFormat.json_pretty:
202+
for item in devices:
203+
console.print_json(item.json())
204+
else:
205+
for item in devices:
206+
click.echo(item.json())
207+
208+
167209
@users.command("list-roles", cls=IncydrCommand)
168210
@user_arg
169211
@table_format_option

src/_incydr_sdk/agents/client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def get_page(
3939
page_size: int = 500,
4040
agent_healthy: bool = None,
4141
agent_health_issue_types: Union[List[str], str] = None,
42+
user_id: str = None,
4243
) -> AgentsPage:
4344
"""
4445
Get a page of agents.
@@ -55,6 +56,7 @@ def get_page(
5556
* **sort_key**: [`SortKeys`][agents-sort-keys] - Values on which the response will be sorted. Defaults to agent name.
5657
* **agent_healthy**: `bool | None` - Optionally retrieve agents with this health status. Agents that have no health issue types are considered healthy.
5758
* **agent_health_issue_types**: `List[str] | str` - Optionally retrieve agents that have (at least) any of the given issue type(s). Health issue types include the following: `NOT_CONNECTING`, `NOT_SENDING_SECURITY_EVENTS`, `SECURITY_INGEST_REJECTED`, `MISSING_MACOS_PERMISSION_FULL_DISK_ACCESS`, `MISSING_MACOS_PERMISSION_ACCESSIBILITY`.
59+
* **user_id**: `str` - Optionally retrieve only agents associated with this user ID.
5860
5961
**Returns**: An [`AgentsPage`][agentspage-model] object.
6062
"""
@@ -69,6 +71,7 @@ def get_page(
6971
srtKey=sort_key,
7072
pageSize=page_size,
7173
page=page_num,
74+
userId=user_id,
7275
)
7376
response = self._parent.session.get("/v1/agents", params=data.dict())
7477
return AgentsPage.parse_response(response)
@@ -82,6 +85,7 @@ def iter_all(
8285
page_size: int = 500,
8386
agent_healthy: bool = None,
8487
agent_health_issue_types: List[str] = None,
88+
user_id: str = None,
8589
) -> Iterator[Agent]:
8690
"""
8791
Iterate over all agents.
@@ -100,6 +104,7 @@ def iter_all(
100104
sort_key=sort_key,
101105
page_num=page_num,
102106
page_size=page_size,
107+
user_id=user_id,
103108
)
104109
yield from page.agents
105110
if len(page.agents) < page_size:

src/_incydr_sdk/agents/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class QueryAgentsRequest(BaseModel):
106106
srtDir: Optional[str] = None
107107
pageSize: Optional[int] = None
108108
page: Optional[int] = None
109+
userId: Optional[str] = None
109110

110111
@field_validator("srtDir")
111112
@classmethod

src/_incydr_sdk/devices/models.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@ class Device(ResponseModel):
4646
"""
4747

4848
device_id: Optional[str] = Field(
49+
None,
4950
alias="deviceId",
5051
description="A globally unique ID (guid) for this device.",
5152
)
5253
legacy_device_id: Optional[str] = Field(
54+
None,
5355
alias="legacyDeviceId",
5456
description="The device ID to use for older console-based APIs that require a device Id.",
5557
)
@@ -59,38 +61,47 @@ class Device(ResponseModel):
5961
description="Device Hostname according to the device's OS.",
6062
)
6163
status: Optional[str] = Field(
64+
None,
6265
description="Device status. Values: Active, Deactivated, Blocked, Deauthorized (Active/Deactivated followed by optional Blocked and/or Deauthorized)",
6366
)
6467
active: Optional[bool] = Field(
68+
None,
6569
description="True means the device will show up on reports, etc.",
6670
)
6771
blocked: Optional[bool] = Field(
72+
None,
6873
description="True means device continues backing up, but restores and logins are disabled.",
6974
)
7075
alert_state: Optional[int] = Field(
76+
None,
7177
alias="alertState",
7278
description="0=ok, 1=connection warning, 2=connection critical",
7379
)
7480
user_id: Optional[str] = Field(
81+
None,
7582
alias="userId",
7683
description="A globally unique ID for this user.",
7784
)
7885
legacy_user_id: Optional[str] = Field(
86+
None,
7987
alias="legacyUserId",
8088
description='The user ID to use for older console-based APIs that require a user Id.\r\nIf your endpoint domain starts with "console" instead of "api", use this Id for endpoints that require a userId.',
8189
)
8290
org_id: Optional[str] = Field(
91+
None,
8392
alias="orgId",
8493
description="An ID for the Code42 organization of the user owning this device.",
8594
)
8695
legacy_org_id: Optional[str] = Field(
96+
None,
8797
alias="legacyOrgId",
8898
description='The org ID to use for older console-based APIs that require an org Id.\r\nIf your endpoint domain starts with "console" instead of "api", use this Id for endpoints that require an orgId.',
8999
)
90100
org_guid: Optional[str] = Field(
91-
alias="orgGuid", description="The globally unique org ID."
101+
None, alias="orgGuid", description="The globally unique org ID."
92102
)
93103
external_reference: Optional[str] = Field(
104+
None,
94105
alias="externalReferenceInfo",
95106
description="Optional external reference information, such as a serial number, asset tag, employee ID, or help desk issue ID.",
96107
)
@@ -100,25 +111,31 @@ class Device(ResponseModel):
100111
description="The last day and time this device was connected to the server.",
101112
)
102113
os_name: Optional[str] = Field(
114+
None,
103115
alias="osName",
104116
description="Operating system name. Values: Windows*, Mac OS X, Linux, Android, iOS, SunOS, etc",
105117
)
106118
os_version: Optional[str] = Field(
119+
None,
107120
alias="osVersion",
108121
description="Operating system version. Values: 10.5.1, 6.2, etc",
109122
)
110123
os_arch: Optional[str] = Field(
124+
None,
111125
alias="osArch",
112126
description="Hardware architecture. Values: i386, amd64, sparc, x86, etc",
113127
)
114128
address: Optional[str] = Field(
129+
None,
115130
description="Internal IP address and port. Example: 192.168.42.1:4282",
116131
)
117132
remote_address: Optional[str] = Field(
133+
None,
118134
alias="remoteAddress",
119135
description="External IP address and port. Example: 171.22.110.41:13958",
120136
)
121137
time_zone: Optional[str] = Field(
138+
None,
122139
alias="timeZone",
123140
description="Examples: Australia/Canberra, Asia/Calcutta",
124141
)
@@ -127,14 +144,17 @@ class Device(ResponseModel):
127144
description="Device build version long number, will only be applicable to CP4/SP devices.",
128145
)
129146
creation_date: Optional[datetime] = Field(
147+
None,
130148
alias="creationDate",
131149
description="Date and time this device was created.",
132150
)
133151
modification_date: Optional[datetime] = Field(
152+
None,
134153
alias="modificationDate",
135154
description="Date and time this device was last modified.",
136155
)
137156
login_date: Optional[datetime] = Field(
157+
None,
138158
alias="loginDate",
139159
description="Date and time this device was last logged in.",
140160
)

src/_incydr_sdk/users/client.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Iterator
33
from typing import List
44
from typing import Union
5+
from warnings import warn
56

67
from pydantic import parse_obj_as
78
from requests import Response
@@ -126,6 +127,52 @@ def get_devices(
126127
* **sort_dir**: `SortDirection` - 'asc' or 'desc'. The direction in which to sort the response based on the corresponding key. Defaults to 'asc'.
127128
* **sort_key**: `SortKeys` - One or more values on which the response will be sorted. Defaults to device name.
128129
130+
**Returns**: A [`DevicesPage`][devicespage-model] object.
131+
"""
132+
warn(
133+
"users.v1.get_devices is deprecated and replaced by agents.v1.iter_all.",
134+
DeprecationWarning,
135+
stacklevel=2,
136+
)
137+
page_size = page_size or self._parent.settings.page_size
138+
data = QueryDevicesRequest(
139+
page=page_num,
140+
pageSize=page_size,
141+
sortKey=sort_key,
142+
sortDirection=sort_dir,
143+
active=active,
144+
blocked=blocked,
145+
)
146+
response = self._parent.session.get(
147+
f"/v1/users/{user_id}/devices", params=data.dict()
148+
)
149+
return DevicesPage.parse_response(response)
150+
151+
def get_agents(
152+
self,
153+
user_id: str,
154+
active: bool = None,
155+
blocked: bool = None,
156+
page_num: int = 1,
157+
page_size: int = None,
158+
sort_dir: SortDirection = SortDirection.ASC,
159+
sort_key: SortKeys = SortKeys.NAME,
160+
) -> DevicesPage:
161+
"""
162+
Get a page of agents associated with a specific user.
163+
164+
Filter results by passing the appropriate parameters:
165+
166+
**Parameters**:
167+
168+
* **user_id**: `str` (required) - The unique ID for the user.
169+
* **active**: `bool` - Whether or not the device is active. If true, the device will show up on reports, etc.
170+
* **blocked**: `bool` - Whether or not the device is blocked. If true, restores and logins are disabled.
171+
* **page_num**: `int` - Page number for results. Defaulting to 1.
172+
* **page_size**: `int` - Max number of results to return per page. Defaulting to client's `page_size` settings.
173+
* **sort_dir**: `SortDirection` - 'asc' or 'desc'. The direction in which to sort the response based on the corresponding key. Defaults to 'asc'.
174+
* **sort_key**: `SortKeys` - One or more values on which the response will be sorted. Defaults to device name.
175+
129176
**Returns**: A [`DevicesPage`][devicespage-model] object.
130177
"""
131178
page_size = page_size or self._parent.settings.page_size

0 commit comments

Comments
 (0)