Skip to content

Commit 2772388

Browse files
committed
feat: implement users bulk_remove
1 parent 55f19d9 commit 2772388

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

117+
@api(version="3.15")
118+
def bulk_remove(self, users: Iterable[UserItem]) -> None:
119+
url = f"{self.baseurl}/delete"
120+
csv_content = remove_users_csv(users)
121+
request, content_type = RequestFactory.User.delete_csv_req(csv_content)
122+
server_response = self.post_request(url, request, content_type)
123+
return None
124+
117125
@api(version="2.0")
118126
def create_from_file(self, filepath: str) -> Tuple[List[UserItem], List[Tuple[UserItem, ServerResponseError]]]:
119127
import warnings
@@ -229,3 +237,23 @@ def create_users_csv(users: Iterable[UserItem], identity_pool=None) -> bytes:
229237
output.seek(0)
230238
result = output.read().encode("utf-8")
231239
return result
240+
241+
242+
def remove_users_csv(users: Iterable[UserItem]) -> bytes:
243+
with io.StringIO() as output:
244+
writer = csv.writer(output, quoting=csv.QUOTE_MINIMAL)
245+
for user in users:
246+
writer.writerow(
247+
(
248+
f"{user.domain_name}\\{user.name}" if user.domain_name else user.name,
249+
None,
250+
None,
251+
None,
252+
None,
253+
None,
254+
None,
255+
)
256+
)
257+
output.seek(0)
258+
result = output.read().encode("utf-8")
259+
return result

tableauserverclient/server/request_factory.py

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

900+
def delete_csv_req(self, csv_content: bytes):
901+
parts = {
902+
"tableau_user_delete": ("tsc_users_file.csv", csv_content, "file"),
903+
}
904+
return _add_multipart(parts)
905+
900906

901907
class WorkbookRequest(object):
902908
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)