tableauserverclient 0.37__py3-none-any.whl → 0.39__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. tableauserverclient/bin/_version.py → _version.py +3 -3
  2. bin/__init__.py +3 -0
  3. bin/_version.py +21 -0
  4. {tableauserverclient/helpers → helpers}/strings.py +25 -1
  5. {tableauserverclient/models → models}/__init__.py +15 -1
  6. models/collection_item.py +52 -0
  7. {tableauserverclient/models → models}/connection_item.py +16 -2
  8. {tableauserverclient/models → models}/custom_view_item.py +8 -0
  9. {tableauserverclient/models → models}/data_freshness_policy_item.py +3 -3
  10. {tableauserverclient/models → models}/datasource_item.py +113 -3
  11. models/extensions_item.py +186 -0
  12. models/extract_item.py +82 -0
  13. {tableauserverclient/models → models}/favorites_item.py +21 -8
  14. {tableauserverclient/models → models}/flow_item.py +3 -3
  15. {tableauserverclient/models → models}/group_item.py +18 -1
  16. {tableauserverclient/models → models}/groupset_item.py +14 -0
  17. {tableauserverclient/models → models}/interval_item.py +42 -1
  18. models/location_item.py +53 -0
  19. models/oidc_item.py +82 -0
  20. {tableauserverclient/models → models}/permissions_item.py +2 -0
  21. {tableauserverclient/models → models}/project_item.py +141 -29
  22. {tableauserverclient/models → models}/property_decorators.py +2 -2
  23. {tableauserverclient/models → models}/reference_item.py +12 -6
  24. {tableauserverclient/models → models}/schedule_item.py +67 -1
  25. {tableauserverclient/models → models}/site_item.py +54 -0
  26. {tableauserverclient/models → models}/table_item.py +7 -3
  27. {tableauserverclient/models → models}/tableau_auth.py +13 -6
  28. {tableauserverclient/models → models}/tableau_types.py +13 -1
  29. {tableauserverclient/models → models}/user_item.py +111 -4
  30. {tableauserverclient/models → models}/view_item.py +79 -5
  31. {tableauserverclient/models → models}/workbook_item.py +153 -3
  32. {tableauserverclient/server → server}/endpoint/__init__.py +4 -0
  33. {tableauserverclient/server → server}/endpoint/databases_endpoint.py +101 -18
  34. {tableauserverclient/server → server}/endpoint/datasources_endpoint.py +155 -25
  35. {tableauserverclient/server → server}/endpoint/dqw_endpoint.py +16 -6
  36. {tableauserverclient/server → server}/endpoint/endpoint.py +39 -0
  37. server/endpoint/extensions_endpoint.py +79 -0
  38. {tableauserverclient/server → server}/endpoint/flow_task_endpoint.py +1 -1
  39. {tableauserverclient/server → server}/endpoint/flows_endpoint.py +5 -4
  40. server/endpoint/oidc_endpoint.py +157 -0
  41. {tableauserverclient/server → server}/endpoint/projects_endpoint.py +12 -0
  42. server/endpoint/schedules_endpoint.py +328 -0
  43. {tableauserverclient/server → server}/endpoint/sites_endpoint.py +18 -1
  44. {tableauserverclient/server → server}/endpoint/tables_endpoint.py +140 -17
  45. {tableauserverclient/server → server}/endpoint/users_endpoint.py +296 -10
  46. {tableauserverclient/server → server}/endpoint/views_endpoint.py +23 -0
  47. {tableauserverclient/server → server}/endpoint/workbooks_endpoint.py +124 -9
  48. {tableauserverclient/server → server}/query.py +36 -0
  49. {tableauserverclient/server → server}/request_factory.py +286 -2
  50. {tableauserverclient/server → server}/request_options.py +139 -3
  51. {tableauserverclient/server → server}/server.py +46 -0
  52. {tableauserverclient-0.37.dist-info → tableauserverclient-0.39.dist-info}/METADATA +5 -26
  53. tableauserverclient-0.39.dist-info/RECORD +107 -0
  54. {tableauserverclient-0.37.dist-info → tableauserverclient-0.39.dist-info}/WHEEL +1 -1
  55. tableauserverclient-0.39.dist-info/top_level.txt +4 -0
  56. tableauserverclient/__init__.py +0 -141
  57. tableauserverclient/config.py +0 -27
  58. tableauserverclient/datetime_helpers.py +0 -45
  59. tableauserverclient/exponential_backoff.py +0 -30
  60. tableauserverclient/filesys_helpers.py +0 -63
  61. tableauserverclient/namespace.py +0 -37
  62. tableauserverclient/py.typed +0 -0
  63. tableauserverclient/server/endpoint/schedules_endpoint.py +0 -151
  64. tableauserverclient-0.37.dist-info/RECORD +0 -106
  65. tableauserverclient-0.37.dist-info/licenses/LICENSE.versioneer +0 -7
  66. tableauserverclient-0.37.dist-info/top_level.txt +0 -1
  67. {tableauserverclient/helpers → helpers}/__init__.py +0 -0
  68. {tableauserverclient/helpers → helpers}/headers.py +0 -0
  69. {tableauserverclient/helpers → helpers}/logging.py +0 -0
  70. {tableauserverclient/models → models}/column_item.py +0 -0
  71. {tableauserverclient/models → models}/connection_credentials.py +0 -0
  72. {tableauserverclient/models → models}/data_acceleration_report_item.py +0 -0
  73. {tableauserverclient/models → models}/data_alert_item.py +0 -0
  74. {tableauserverclient/models → models}/database_item.py +0 -0
  75. {tableauserverclient/models → models}/dqw_item.py +0 -0
  76. {tableauserverclient/models → models}/exceptions.py +0 -0
  77. {tableauserverclient/models → models}/fileupload_item.py +0 -0
  78. {tableauserverclient/models → models}/flow_run_item.py +0 -0
  79. {tableauserverclient/models → models}/job_item.py +0 -0
  80. {tableauserverclient/models → models}/linked_tasks_item.py +0 -0
  81. {tableauserverclient/models → models}/metric_item.py +0 -0
  82. {tableauserverclient/models → models}/pagination_item.py +0 -0
  83. {tableauserverclient/models → models}/revision_item.py +0 -0
  84. {tableauserverclient/models → models}/server_info_item.py +0 -0
  85. {tableauserverclient/models → models}/subscription_item.py +0 -0
  86. {tableauserverclient/models → models}/tag_item.py +0 -0
  87. {tableauserverclient/models → models}/target.py +0 -0
  88. {tableauserverclient/models → models}/task_item.py +0 -0
  89. {tableauserverclient/models → models}/virtual_connection_item.py +0 -0
  90. {tableauserverclient/models → models}/webhook_item.py +0 -0
  91. {tableauserverclient/server → server}/__init__.py +0 -0
  92. {tableauserverclient/server → server}/endpoint/auth_endpoint.py +0 -0
  93. {tableauserverclient/server → server}/endpoint/custom_views_endpoint.py +0 -0
  94. {tableauserverclient/server → server}/endpoint/data_acceleration_report_endpoint.py +0 -0
  95. {tableauserverclient/server → server}/endpoint/data_alert_endpoint.py +0 -0
  96. {tableauserverclient/server → server}/endpoint/default_permissions_endpoint.py +0 -0
  97. {tableauserverclient/server → server}/endpoint/exceptions.py +0 -0
  98. {tableauserverclient/server → server}/endpoint/favorites_endpoint.py +0 -0
  99. {tableauserverclient/server → server}/endpoint/fileuploads_endpoint.py +0 -0
  100. {tableauserverclient/server → server}/endpoint/flow_runs_endpoint.py +0 -0
  101. {tableauserverclient/server → server}/endpoint/groups_endpoint.py +0 -0
  102. {tableauserverclient/server → server}/endpoint/groupsets_endpoint.py +0 -0
  103. {tableauserverclient/server → server}/endpoint/jobs_endpoint.py +0 -0
  104. {tableauserverclient/server → server}/endpoint/linked_tasks_endpoint.py +0 -0
  105. {tableauserverclient/server → server}/endpoint/metadata_endpoint.py +0 -0
  106. {tableauserverclient/server → server}/endpoint/metrics_endpoint.py +0 -0
  107. {tableauserverclient/server → server}/endpoint/permissions_endpoint.py +0 -0
  108. {tableauserverclient/server → server}/endpoint/resource_tagger.py +0 -0
  109. {tableauserverclient/server → server}/endpoint/server_info_endpoint.py +0 -0
  110. {tableauserverclient/server → server}/endpoint/subscriptions_endpoint.py +0 -0
  111. {tableauserverclient/server → server}/endpoint/tasks_endpoint.py +0 -0
  112. {tableauserverclient/server → server}/endpoint/virtual_connections_endpoint.py +0 -0
  113. {tableauserverclient/server → server}/endpoint/webhooks_endpoint.py +0 -0
  114. {tableauserverclient/server → server}/exceptions.py +0 -0
  115. {tableauserverclient/server → server}/filter.py +0 -0
  116. {tableauserverclient/server → server}/pager.py +0 -0
  117. {tableauserverclient/server → server}/sort.py +0 -0
  118. {tableauserverclient-0.37.dist-info → tableauserverclient-0.39.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,8 @@
1
1
  import logging
2
- from typing import Union
2
+ from typing import Optional, Union, TYPE_CHECKING
3
3
  from collections.abc import Iterable
4
4
 
5
+ from tableauserverclient.models.permissions_item import PermissionsRule
5
6
  from tableauserverclient.server.endpoint.dqw_endpoint import _DataQualityWarningEndpoint
6
7
  from tableauserverclient.server.endpoint.endpoint import api, Endpoint
7
8
  from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
@@ -12,6 +13,10 @@ from tableauserverclient.models import TableItem, ColumnItem, PaginationItem
12
13
  from tableauserverclient.server.pager import Pager
13
14
 
14
15
  from tableauserverclient.helpers.logging import logger
16
+ from tableauserverclient.server.request_options import RequestOptions
17
+
18
+ if TYPE_CHECKING:
19
+ from tableauserverclient.models import DQWItem, PermissionsRule
15
20
 
16
21
 
17
22
  class Tables(Endpoint, TaggingMixin[TableItem]):
@@ -22,11 +27,29 @@ class Tables(Endpoint, TaggingMixin[TableItem]):
22
27
  self._data_quality_warnings = _DataQualityWarningEndpoint(self.parent_srv, "table")
23
28
 
24
29
  @property
25
- def baseurl(self):
30
+ def baseurl(self) -> str:
26
31
  return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/tables"
27
32
 
28
33
  @api(version="3.5")
29
- def get(self, req_options=None):
34
+ def get(self, req_options: Optional[RequestOptions] = None) -> tuple[list[TableItem], PaginationItem]:
35
+ """
36
+ Get information about all tables on the site. Endpoint is paginated, and
37
+ will return a default of 100 items per page. Use the `req_options`
38
+ parameter to customize the request.
39
+
40
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_metadata.htm#query_tables
41
+
42
+ Parameters
43
+ ----------
44
+ req_options : RequestOptions, optional
45
+ Options to customize the request. If not provided, defaults to None.
46
+
47
+ Returns
48
+ -------
49
+ tuple[list[TableItem], PaginationItem]
50
+ A tuple containing a list of TableItem objects and a PaginationItem
51
+ object.
52
+ """
30
53
  logger.info("Querying all tables on site")
31
54
  url = self.baseurl
32
55
  server_response = self.get_request(url, req_options)
@@ -36,7 +59,27 @@ class Tables(Endpoint, TaggingMixin[TableItem]):
36
59
 
37
60
  # Get 1 table
38
61
  @api(version="3.5")
39
- def get_by_id(self, table_id):
62
+ def get_by_id(self, table_id: str) -> TableItem:
63
+ """
64
+ Get information about a single table on the site.
65
+
66
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_metadata.htm#query_table
67
+
68
+ Parameters
69
+ ----------
70
+ table_id : str
71
+ The ID of the table to retrieve.
72
+
73
+ Returns
74
+ -------
75
+ TableItem
76
+ A TableItem object representing the table.
77
+
78
+ Raises
79
+ ------
80
+ ValueError
81
+ If the table ID is not provided.
82
+ """
40
83
  if not table_id:
41
84
  error = "table ID undefined."
42
85
  raise ValueError(error)
@@ -46,7 +89,24 @@ class Tables(Endpoint, TaggingMixin[TableItem]):
46
89
  return TableItem.from_response(server_response.content, self.parent_srv.namespace)[0]
47
90
 
48
91
  @api(version="3.5")
49
- def delete(self, table_id):
92
+ def delete(self, table_id: str) -> None:
93
+ """
94
+ Delete a single table from the server.
95
+
96
+ Parameters
97
+ ----------
98
+ table_id : str
99
+ The ID of the table to delete.
100
+
101
+ Returns
102
+ -------
103
+ None
104
+
105
+ Raises
106
+ ------
107
+ ValueError
108
+ If the table ID is not provided.
109
+ """
50
110
  if not table_id:
51
111
  error = "Database ID undefined."
52
112
  raise ValueError(error)
@@ -55,7 +115,27 @@ class Tables(Endpoint, TaggingMixin[TableItem]):
55
115
  logger.info(f"Deleted single table (ID: {table_id})")
56
116
 
57
117
  @api(version="3.5")
58
- def update(self, table_item):
118
+ def update(self, table_item: TableItem) -> TableItem:
119
+ """
120
+ Update a table on the server.
121
+
122
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_metadata.htm#update_table
123
+
124
+ Parameters
125
+ ----------
126
+ table_item : TableItem
127
+ The TableItem object to update.
128
+
129
+ Returns
130
+ -------
131
+ TableItem
132
+ The updated TableItem object.
133
+
134
+ Raises
135
+ ------
136
+ MissingRequiredFieldError
137
+ If the table item is missing an ID.
138
+ """
59
139
  if not table_item.id:
60
140
  error = "table item missing ID."
61
141
  raise MissingRequiredFieldError(error)
@@ -69,21 +149,46 @@ class Tables(Endpoint, TaggingMixin[TableItem]):
69
149
 
70
150
  # Get all columns of the table
71
151
  @api(version="3.5")
72
- def populate_columns(self, table_item, req_options=None):
152
+ def populate_columns(self, table_item: TableItem, req_options: Optional[RequestOptions] = None) -> None:
153
+ """
154
+ Populate the columns of a table item. Sets a fetcher function to
155
+ retrieve the columns when needed.
156
+
157
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_metadata.htm#query_columns
158
+
159
+ Parameters
160
+ ----------
161
+ table_item : TableItem
162
+ The TableItem object to populate columns for.
163
+
164
+ req_options : RequestOptions, optional
165
+ Options to customize the request. If not provided, defaults to None.
166
+
167
+ Returns
168
+ -------
169
+ None
170
+
171
+ Raises
172
+ ------
173
+ MissingRequiredFieldError
174
+ If the table item is missing an ID.
175
+ """
73
176
  if not table_item.id:
74
177
  error = "Table item missing ID. table must be retrieved from server first."
75
178
  raise MissingRequiredFieldError(error)
76
179
 
77
180
  def column_fetcher():
78
181
  return Pager(
79
- lambda options: self._get_columns_for_table(table_item, options),
182
+ lambda options: self._get_columns_for_table(table_item, options), # type: ignore
80
183
  req_options,
81
184
  )
82
185
 
83
186
  table_item._set_columns(column_fetcher)
84
187
  logger.info(f"Populated columns for table (ID: {table_item.id}")
85
188
 
86
- def _get_columns_for_table(self, table_item, req_options=None):
189
+ def _get_columns_for_table(
190
+ self, table_item: TableItem, req_options: Optional[RequestOptions] = None
191
+ ) -> tuple[list[ColumnItem], PaginationItem]:
87
192
  url = f"{self.baseurl}/{table_item.id}/columns"
88
193
  server_response = self.get_request(url, req_options)
89
194
  columns = ColumnItem.from_response(server_response.content, self.parent_srv.namespace)
@@ -91,7 +196,25 @@ class Tables(Endpoint, TaggingMixin[TableItem]):
91
196
  return columns, pagination_item
92
197
 
93
198
  @api(version="3.5")
94
- def update_column(self, table_item, column_item):
199
+ def update_column(self, table_item: TableItem, column_item: ColumnItem) -> ColumnItem:
200
+ """
201
+ Update the description of a column in a table.
202
+
203
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_metadata.htm#update_column
204
+
205
+ Parameters
206
+ ----------
207
+ table_item : TableItem
208
+ The TableItem object representing the table.
209
+
210
+ column_item : ColumnItem
211
+ The ColumnItem object representing the column to update.
212
+
213
+ Returns
214
+ -------
215
+ ColumnItem
216
+ The updated ColumnItem object.
217
+ """
95
218
  url = f"{self.baseurl}/{table_item.id}/columns/{column_item.id}"
96
219
  update_req = RequestFactory.Column.update_req(column_item)
97
220
  server_response = self.put_request(url, update_req)
@@ -101,31 +224,31 @@ class Tables(Endpoint, TaggingMixin[TableItem]):
101
224
  return column
102
225
 
103
226
  @api(version="3.5")
104
- def populate_permissions(self, item):
227
+ def populate_permissions(self, item: TableItem) -> None:
105
228
  self._permissions.populate(item)
106
229
 
107
230
  @api(version="3.5")
108
- def update_permissions(self, item, rules):
231
+ def update_permissions(self, item: TableItem, rules: list[PermissionsRule]) -> list[PermissionsRule]:
109
232
  return self._permissions.update(item, rules)
110
233
 
111
234
  @api(version="3.5")
112
- def delete_permission(self, item, rules):
235
+ def delete_permission(self, item: TableItem, rules: list[PermissionsRule]) -> None:
113
236
  return self._permissions.delete(item, rules)
114
237
 
115
238
  @api(version="3.5")
116
- def populate_dqw(self, item):
239
+ def populate_dqw(self, item: TableItem) -> None:
117
240
  self._data_quality_warnings.populate(item)
118
241
 
119
242
  @api(version="3.5")
120
- def update_dqw(self, item, warning):
243
+ def update_dqw(self, item: TableItem, warning: "DQWItem") -> list["DQWItem"]:
121
244
  return self._data_quality_warnings.update(item, warning)
122
245
 
123
246
  @api(version="3.5")
124
- def add_dqw(self, item, warning):
247
+ def add_dqw(self, item: TableItem, warning: "DQWItem") -> list["DQWItem"]:
125
248
  return self._data_quality_warnings.add(item, warning)
126
249
 
127
250
  @api(version="3.5")
128
- def delete_dqw(self, item):
251
+ def delete_dqw(self, item: TableItem) -> None:
129
252
  self._data_quality_warnings.clear(item)
130
253
 
131
254
  @api(version="3.9")
@@ -1,14 +1,19 @@
1
+ from collections.abc import Iterable
1
2
  import copy
3
+ import csv
4
+ import io
5
+ import itertools
2
6
  import logging
3
7
  from typing import Optional
8
+ import warnings
4
9
 
5
10
  from tableauserverclient.server.query import QuerySet
6
11
 
7
- from .endpoint import QuerysetEndpoint, api
8
- from .exceptions import MissingRequiredFieldError, ServerResponseError
12
+ from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
13
+ from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError, ServerResponseError
9
14
  from tableauserverclient.server import RequestFactory, RequestOptions
10
- from tableauserverclient.models import UserItem, WorkbookItem, PaginationItem, GroupItem
11
- from ..pager import Pager
15
+ from tableauserverclient.models import UserItem, WorkbookItem, PaginationItem, GroupItem, JobItem
16
+ from tableauserverclient.server.pager import Pager
12
17
 
13
18
  from tableauserverclient.helpers.logging import logger
14
19
 
@@ -87,7 +92,7 @@ class Users(QuerysetEndpoint[UserItem]):
87
92
 
88
93
  if req_options is None:
89
94
  req_options = RequestOptions()
90
- req_options._all_fields = True
95
+ req_options.all_fields = True
91
96
 
92
97
  url = self.baseurl
93
98
  server_response = self.get_request(url, req_options)
@@ -344,7 +349,34 @@ class Users(QuerysetEndpoint[UserItem]):
344
349
 
345
350
  # Add new users to site. This does not actually perform a bulk action, it's syntactic sugar
346
351
  @api(version="2.0")
347
- def add_all(self, users: list[UserItem]):
352
+ def add_all(self, users: list[UserItem]) -> tuple[list[UserItem], list[UserItem]]:
353
+ """
354
+ Syntactic sugar for calling users.add multiple times. This method has
355
+ been deprecated in favor of using the bulk_add which accomplishes the
356
+ same task in one API call.
357
+
358
+ .. deprecated:: v0.41.0
359
+ `add_all` will be removed as its functionality is replicated via
360
+ the `bulk_add` method.
361
+
362
+ Parameters
363
+ ----------
364
+ users: list[UserItem]
365
+ A list of UserItem objects to add to the site. Each UserItem object
366
+ will be passed to the `add` method individually.
367
+
368
+ Returns
369
+ -------
370
+ tuple[list[UserItem], list[UserItem]]
371
+ The first element of the tuple is a list of UserItem objects that
372
+ were successfully added to the site. The second element is a list
373
+ of UserItem objects that failed to be added to the site.
374
+
375
+ Warnings
376
+ --------
377
+ This method is deprecated. Use the `bulk_add` method instead.
378
+ """
379
+ warnings.warn("This method is deprecated, use bulk_add method instead.", DeprecationWarning)
348
380
  created = []
349
381
  failed = []
350
382
  for user in users:
@@ -357,8 +389,143 @@ class Users(QuerysetEndpoint[UserItem]):
357
389
 
358
390
  # helping the user by parsing a file they could have used to add users through the UI
359
391
  # line format: Username [required], password, display name, license, admin, publish
392
+ @api(version="3.15")
393
+ def bulk_add(self, users: Iterable[UserItem]) -> JobItem:
394
+ """
395
+ When adding users in bulk, the server will return a job item that can be used to track the progress of the
396
+ operation. This method will return the job item that was created when the users were added.
397
+
398
+ For each user, name is required, and other fields are optional. If connected to activte directory and
399
+ the user name is not unique across domains, then the domain attribute must be populated on
400
+ the UserItem.
401
+
402
+ The user's display name is read from the fullname attribute.
403
+
404
+ Email is optional, but if provided, it must be a valid email address.
405
+
406
+ If auth_setting is not provided, and idp_configuration_id is None, then
407
+ default is ServerDefault.
408
+
409
+ If site_role is not provided, the default is Unlicensed.
410
+
411
+ Password is optional, and only used if the server is using local
412
+ authentication. If using any other authentication method, the password
413
+ should not be provided.
414
+
415
+ Details about administrator level and publishing capability are
416
+ inferred from the site_role.
417
+
418
+ If the user belongs to a different IDP configuration, the UserItem's
419
+ idp_configuration_id attribute must be set to the IDP configuration ID
420
+ that the user belongs to.
421
+
422
+ Parameters
423
+ ----------
424
+ users: Iterable[UserItem]
425
+ An iterable of UserItem objects to add to the site. See above for
426
+ what fields are required and optional.
427
+
428
+ Returns
429
+ -------
430
+ JobItem
431
+ The job that is started for adding the users in bulk.
432
+
433
+ Examples
434
+ --------
435
+ >>> import tableauserverclient as TSC
436
+ >>> server = TSC.Server('http://localhost')
437
+ >>> # Login to the server
438
+
439
+ >>> # Create a list of UserItem objects to add to the site
440
+ >>> users = [
441
+ >>> TSC.UserItem(name="user1", site_role="Unlicensed"),
442
+ >>> TSC.UserItem(name="user2", site_role="Explorer"),
443
+ >>> TSC.UserItem(name="user3", site_role="Creator"),
444
+ >>> ]
445
+
446
+ >>> # Set the domain name for the users
447
+ >>> for user in users:
448
+ >>> user.domain_name = "example.com"
449
+
450
+ >>> # Add the users to the site
451
+ >>> job = server.users.bulk_add(users)
452
+
453
+ """
454
+ url = f"{self.baseurl}/import"
455
+ # Allow for iterators to be passed into the function
456
+ csv_users, xml_users = itertools.tee(users, 2)
457
+ csv_content = create_users_csv(csv_users)
458
+
459
+ xml_request, content_type = RequestFactory.User.import_from_csv_req(csv_content, xml_users)
460
+ server_response = self.post_request(url, xml_request, content_type)
461
+ return JobItem.from_response(server_response.content, self.parent_srv.namespace).pop()
462
+
463
+ @api(version="3.15")
464
+ def bulk_remove(self, users: Iterable[UserItem]) -> None:
465
+ """
466
+ Remove multiple users from the site. The users are identified by their
467
+ domain and name. The users are removed in bulk, so the server will not
468
+ return a job item to track the progress of the operation nor a response
469
+ for each user that was removed.
470
+
471
+ Parameters
472
+ ----------
473
+ users: Iterable[UserItem]
474
+ An iterable of UserItem objects to remove from the site. Each
475
+ UserItem object should have the domain and name attributes set.
476
+
477
+ Returns
478
+ -------
479
+ None
480
+
481
+ Examples
482
+ --------
483
+ >>> import tableauserverclient as TSC
484
+ >>> server = TSC.Server('http://localhost')
485
+ >>> # Login to the server
486
+
487
+ >>> # Find the users to remove
488
+ >>> example_users = server.users.filter(domain_name="example.com")
489
+ >>> server.users.bulk_remove(example_users)
490
+ """
491
+ url = f"{self.baseurl}/delete"
492
+ csv_content = remove_users_csv(users)
493
+ request, content_type = RequestFactory.User.delete_csv_req(csv_content)
494
+ server_response = self.post_request(url, request, content_type)
495
+ return None
496
+
360
497
  @api(version="2.0")
361
498
  def create_from_file(self, filepath: str) -> tuple[list[UserItem], list[tuple[UserItem, ServerResponseError]]]:
499
+ """
500
+ Syntactic sugar for calling users.add multiple times. This method has
501
+ been deprecated in favor of using the bulk_add which accomplishes the
502
+ same task in one API call.
503
+
504
+ .. deprecated:: v0.41.0
505
+ `add_all` will be removed as its functionality is replicated via
506
+ the `bulk_add` method.
507
+
508
+ Parameters
509
+ ----------
510
+ filepath: str
511
+ The path to the CSV file containing the users to add to the site.
512
+ The file is read in line by line and each line is passed to the
513
+ `add` method.
514
+
515
+ Returns
516
+ -------
517
+ tuple[list[UserItem], list[tuple[UserItem, ServerResponseError]]]
518
+ The first element of the tuple is a list of UserItem objects that
519
+ were successfully added to the site. The second element is a list
520
+ of tuples where the first element is the UserItem object that failed
521
+ to be added to the site and the second element is the ServerResponseError
522
+ that was raised when attempting to add the user.
523
+
524
+ Warnings
525
+ --------
526
+ This method is deprecated. Use the `bulk_add` method instead.
527
+ """
528
+ warnings.warn("This method is deprecated, use bulk_add instead", DeprecationWarning)
362
529
  created = []
363
530
  failed = []
364
531
  if not filepath.find("csv"):
@@ -381,10 +548,15 @@ class Users(QuerysetEndpoint[UserItem]):
381
548
 
382
549
  # Get workbooks for user
383
550
  @api(version="2.0")
384
- def populate_workbooks(self, user_item: UserItem, req_options: Optional[RequestOptions] = None) -> None:
551
+ def populate_workbooks(
552
+ self, user_item: UserItem, req_options: Optional[RequestOptions] = None, owned_only: bool = False
553
+ ) -> None:
385
554
  """
386
555
  Returns information about the workbooks that the specified user owns
387
- and has Read (view) permissions for.
556
+ or has Read (view) permissions for. If owned_only is set to True,
557
+ only the workbooks that the user owns are returned. If owned_only is
558
+ set to False, all workbooks that the user has Read (view) permissions
559
+ for are returned.
388
560
 
389
561
  This method retrieves the workbook information for the specified user.
390
562
  The REST API is designed to return only the information you ask for
@@ -402,6 +574,10 @@ class Users(QuerysetEndpoint[UserItem]):
402
574
  req_options : Optional[RequestOptions]
403
575
  Optional request options to filter and sort the results.
404
576
 
577
+ owned_only : bool, default=False
578
+ If True, only the workbooks that the user owns are returned.
579
+ If False, all workbooks that the user has Read (view) permissions
580
+
405
581
  Returns
406
582
  -------
407
583
  None
@@ -423,14 +599,22 @@ class Users(QuerysetEndpoint[UserItem]):
423
599
  raise MissingRequiredFieldError(error)
424
600
 
425
601
  def wb_pager():
426
- return Pager(lambda options: self._get_wbs_for_user(user_item, options), req_options)
602
+ def func(req_options):
603
+ return self._get_wbs_for_user(user_item, req_options, owned_only=owned_only)
604
+
605
+ return Pager(func, req_options)
427
606
 
428
607
  user_item._set_workbooks(wb_pager)
429
608
 
430
609
  def _get_wbs_for_user(
431
- self, user_item: UserItem, req_options: Optional[RequestOptions] = None
610
+ self,
611
+ user_item: UserItem,
612
+ req_options: Optional[RequestOptions] = None,
613
+ owned_only: bool = False,
432
614
  ) -> tuple[list[WorkbookItem], PaginationItem]:
433
615
  url = f"{self.baseurl}/{user_item.id}/workbooks"
616
+ if owned_only:
617
+ url += "?ownedBy=true"
434
618
  server_response = self.get_request(url, req_options)
435
619
  logger.info(f"Populated workbooks for user (ID: {user_item.id})")
436
620
  workbook_item = WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)
@@ -552,3 +736,105 @@ class Users(QuerysetEndpoint[UserItem]):
552
736
  """
553
737
 
554
738
  return super().filter(*invalid, page_size=page_size, **kwargs)
739
+
740
+
741
+ def create_users_csv(users: Iterable[UserItem]) -> bytes:
742
+ """
743
+ Create a CSV byte string from an Iterable of UserItem objects. The CSV will
744
+ have the following columns, and no header row:
745
+
746
+ - Username
747
+ - Password
748
+ - Display Name
749
+ - License
750
+ - Admin Level
751
+ - Publish capability
752
+ - Email
753
+
754
+ Parameters
755
+ ----------
756
+ users: Iterable[UserItem]
757
+ An iterable of UserItem objects to create the CSV from.
758
+
759
+ Returns
760
+ -------
761
+ bytes
762
+ A byte string containing the CSV data.
763
+ """
764
+ with io.StringIO() as output:
765
+ writer = csv.writer(output, quoting=csv.QUOTE_MINIMAL)
766
+ for user in users:
767
+ site_role = user.site_role or "Unlicensed"
768
+ if site_role == "ServerAdministrator":
769
+ license = "Creator"
770
+ admin_level = "System"
771
+ elif site_role.startswith("SiteAdministrator"):
772
+ admin_level = "Site"
773
+ license = site_role.replace("SiteAdministrator", "")
774
+ else:
775
+ license = site_role
776
+ admin_level = ""
777
+
778
+ if any(x in site_role for x in ("Creator", "Admin", "Publish")):
779
+ publish = 1
780
+ else:
781
+ publish = 0
782
+
783
+ writer.writerow(
784
+ (
785
+ f"{user.domain_name}\\{user.name}" if user.domain_name else user.name,
786
+ getattr(user, "password", ""),
787
+ user.fullname,
788
+ license,
789
+ admin_level,
790
+ publish,
791
+ user.email,
792
+ )
793
+ )
794
+ output.seek(0)
795
+ result = output.read().encode("utf-8")
796
+ return result
797
+
798
+
799
+ def remove_users_csv(users: Iterable[UserItem]) -> bytes:
800
+ """
801
+ Create a CSV byte string from an Iterable of UserItem objects. This function
802
+ only consumes the domain and name attributes of the UserItem objects. The
803
+ CSV will have space for the following columns, though only the first column
804
+ will be populated, and no header row:
805
+
806
+ - Username
807
+ - Password
808
+ - Display Name
809
+ - License
810
+ - Admin Level
811
+ - Publish capability
812
+ - Email
813
+
814
+ Parameters
815
+ ----------
816
+ users: Iterable[UserItem]
817
+ An iterable of UserItem objects to create the CSV from.
818
+
819
+ Returns
820
+ -------
821
+ bytes
822
+ A byte string containing the CSV data.
823
+ """
824
+ with io.StringIO() as output:
825
+ writer = csv.writer(output, quoting=csv.QUOTE_MINIMAL)
826
+ for user in users:
827
+ writer.writerow(
828
+ (
829
+ f"{user.domain_name}\\{user.name}" if user.domain_name else user.name,
830
+ None,
831
+ None,
832
+ None,
833
+ None,
834
+ None,
835
+ None,
836
+ )
837
+ )
838
+ output.seek(0)
839
+ result = output.read().encode("utf-8")
840
+ return result
@@ -371,6 +371,29 @@ class Views(QuerysetEndpoint[ViewItem], TaggingMixin[ViewItem]):
371
371
  # Returning view item to stay consistent with datasource/view update functions
372
372
  return view_item
373
373
 
374
+ @api(version="3.27")
375
+ def delete(self, view: ViewItem | str) -> None:
376
+ """
377
+ Deletes a view in a workbook. If you delete the only view in a workbook,
378
+ the workbook is deleted. Can be used to remove hidden views when
379
+ republishing or migrating to a different environment.
380
+
381
+ REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm#delete_view
382
+
383
+ Parameters
384
+ ----------
385
+ view: ViewItem | str
386
+ The ViewItem or the luid for the view to be deleted.
387
+
388
+ Returns
389
+ -------
390
+ None
391
+ """
392
+ id_ = getattr(view, "id", view)
393
+ self.delete_request(f"{self.baseurl}/{id_}")
394
+ logger.info(f"View({id_}) deleted.")
395
+ return None
396
+
374
397
  @api(version="1.0")
375
398
  def add_tags(self, item: Union[ViewItem, str], tags: Union[Iterable[str], str]) -> set[str]:
376
399
  """