Skip to content

Commit 788b11a

Browse files
committed
feat: implement users bulk_remove
1 parent 26e266a commit 788b11a

File tree

3 files changed

+63
-0
lines changed

3 files changed

+63
-0
lines changed

tableauserverclient/server/endpoint/users_endpoint.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ def bulk_add(self, users: Iterable[UserItem]) -> JobItem:
117117
server_response = self.post_request(url, xml_request, content_type)
118118
return JobItem.from_response(server_response.content, self.parent_srv.namespace).pop()
119119

120+
@api(version="3.15")
121+
def bulk_remove(self, users: Iterable[UserItem]) -> None:
122+
url = f"{self.baseurl}/delete"
123+
csv_content = remove_users_csv(users)
124+
request, content_type = RequestFactory.User.delete_csv_req(csv_content)
125+
server_response = self.post_request(url, request, content_type)
126+
return None
127+
120128
@api(version="2.0")
121129
def create_from_file(self, filepath: str) -> tuple[list[UserItem], list[tuple[UserItem, ServerResponseError]]]:
122130
import warnings
@@ -268,3 +276,23 @@ def create_users_csv(users: Iterable[UserItem], identity_pool=None) -> bytes:
268276
output.seek(0)
269277
result = output.read().encode("utf-8")
270278
return result
279+
280+
281+
def remove_users_csv(users: Iterable[UserItem]) -> bytes:
282+
with io.StringIO() as output:
283+
writer = csv.writer(output, quoting=csv.QUOTE_MINIMAL)
284+
for user in users:
285+
writer.writerow(
286+
(
287+
f"{user.domain_name}\\{user.name}" if user.domain_name else user.name,
288+
None,
289+
None,
290+
None,
291+
None,
292+
None,
293+
None,
294+
)
295+
)
296+
output.seek(0)
297+
result = output.read().encode("utf-8")
298+
return result

tableauserverclient/server/request_factory.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,12 @@ def import_from_csv_req(self, csv_content: bytes, users: Iterable[UserItem]):
946946
}
947947
return _add_multipart(parts)
948948

949+
def delete_csv_req(self, csv_content: bytes):
950+
parts = {
951+
"tableau_user_delete": ("tsc_users_file.csv", csv_content, "file"),
952+
}
953+
return _add_multipart(parts)
954+
949955

950956
class WorkbookRequest:
951957
def _generate_xml(

test/test_user.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,32 @@ def test_bulk_add_no_name(self):
353353

354354
with pytest.raises(ValueError, match="User name must be populated."):
355355
self.server.users.bulk_add(users)
356+
357+
def test_bulk_remove(self):
358+
self.server.version = "3.15"
359+
users = [
360+
TSC.UserItem("Alice"),
361+
TSC.UserItem("Bob"),
362+
]
363+
users[1]._domain_name = "example.com"
364+
with requests_mock.mock() as m:
365+
m.post(f"{self.server.users.baseurl}/delete")
366+
367+
self.server.users.bulk_remove(users)
368+
369+
assert m.last_request.method == "POST"
370+
assert m.last_request.url == f"{self.server.users.baseurl}/delete"
371+
372+
body = m.last_request.body.replace(b"\r\n", b"\n")
373+
assert body.startswith(b"--") # Check if it's a multipart request
374+
boundary = body.split(b"\n")[0].strip()
375+
376+
content = next(seg for seg in body.split(boundary) if seg.strip())
377+
assert b'Content-Disposition: form-data; name="tableau_user_delete"' in content
378+
assert b"Content-Type: file" in content
379+
380+
content = content.replace(b"\r\n", b"\n")
381+
csv_data = content.split(b"\n\n")[1].decode("utf-8")
382+
for user, row in zip(users, csv_data.split("\n")):
383+
name, *_ = row.split(",")
384+
assert name == f"{user.domain_name}\\{user.name}" if user.domain_name else user.name

0 commit comments

Comments
 (0)