Skip to content

Commit 622502c

Browse files
committed
feat: implement users bulk_remove
1 parent be23f2f commit 622502c

3 files changed

Lines changed: 63 additions & 0 deletions

File tree

tableauserverclient/server/endpoint/users_endpoint.py

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

380+
@api(version="3.15")
381+
def bulk_remove(self, users: Iterable[UserItem]) -> None:
382+
url = f"{self.baseurl}/delete"
383+
csv_content = remove_users_csv(users)
384+
request, content_type = RequestFactory.User.delete_csv_req(csv_content)
385+
server_response = self.post_request(url, request, content_type)
386+
return None
387+
380388
@api(version="2.0")
381389
def create_from_file(self, filepath: str) -> tuple[list[UserItem], list[tuple[UserItem, ServerResponseError]]]:
382390
import warnings
@@ -632,3 +640,23 @@ def create_users_csv(users: Iterable[UserItem], identity_pool=None) -> bytes:
632640
output.seek(0)
633641
result = output.read().encode("utf-8")
634642
return result
643+
644+
645+
def remove_users_csv(users: Iterable[UserItem]) -> bytes:
646+
with io.StringIO() as output:
647+
writer = csv.writer(output, quoting=csv.QUOTE_MINIMAL)
648+
for user in users:
649+
writer.writerow(
650+
(
651+
f"{user.domain_name}\\{user.name}" if user.domain_name else user.name,
652+
None,
653+
None,
654+
None,
655+
None,
656+
None,
657+
None,
658+
)
659+
)
660+
output.seek(0)
661+
result = output.read().encode("utf-8")
662+
return result

tableauserverclient/server/request_factory.py

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

954+
def delete_csv_req(self, csv_content: bytes):
955+
parts = {
956+
"tableau_user_delete": ("tsc_users_file.csv", csv_content, "file"),
957+
}
958+
return _add_multipart(parts)
959+
954960

955961
class WorkbookRequest:
956962
def _generate_xml(

test/test_user.py

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

440440
with pytest.raises(ValueError, match="User name must be populated."):
441441
self.server.users.bulk_add(users)
442+
443+
def test_bulk_remove(self):
444+
self.server.version = "3.15"
445+
users = [
446+
TSC.UserItem("Alice"),
447+
TSC.UserItem("Bob"),
448+
]
449+
users[1]._domain_name = "example.com"
450+
with requests_mock.mock() as m:
451+
m.post(f"{self.server.users.baseurl}/delete")
452+
453+
self.server.users.bulk_remove(users)
454+
455+
assert m.last_request.method == "POST"
456+
assert m.last_request.url == f"{self.server.users.baseurl}/delete"
457+
458+
body = m.last_request.body.replace(b"\r\n", b"\n")
459+
assert body.startswith(b"--") # Check if it's a multipart request
460+
boundary = body.split(b"\n")[0].strip()
461+
462+
content = next(seg for seg in body.split(boundary) if seg.strip())
463+
assert b'Content-Disposition: form-data; name="tableau_user_delete"' in content
464+
assert b"Content-Type: file" in content
465+
466+
content = content.replace(b"\r\n", b"\n")
467+
csv_data = content.split(b"\n\n")[1].decode("utf-8")
468+
for user, row in zip(users, csv_data.split("\n")):
469+
name, *_ = row.split(",")
470+
assert name == f"{user.domain_name}\\{user.name}" if user.domain_name else user.name

0 commit comments

Comments
 (0)