Skip to content

Commit 760a6c6

Browse files
committed
feat: implement users bulk_remove
1 parent 89714f4 commit 760a6c6

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
@@ -116,6 +116,14 @@ def bulk_add(self, users: Iterable[UserItem]) -> JobItem:
116116
server_response = self.post_request(url, xml_request, content_type)
117117
return JobItem.from_response(server_response.content, self.parent_srv.namespace).pop()
118118

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

tableauserverclient/server/request_factory.py

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

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

949955
class WorkbookRequest(object):
950956
def _generate_xml(

test/test_user.py

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

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

0 commit comments

Comments
 (0)