tableauserverclient 0.38__py3-none-any.whl → 0.40__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 (46) hide show
  1. _version.py +21 -0
  2. tableauserverclient/__init__.py +10 -5
  3. tableauserverclient/bin/__init__.py +3 -0
  4. tableauserverclient/bin/_version.py +3 -3
  5. tableauserverclient/models/__init__.py +9 -0
  6. tableauserverclient/models/collection_item.py +52 -0
  7. tableauserverclient/models/connection_item.py +16 -2
  8. tableauserverclient/models/custom_view_item.py +8 -0
  9. tableauserverclient/models/data_freshness_policy_item.py +3 -3
  10. tableauserverclient/models/datasource_item.py +3 -1
  11. tableauserverclient/models/extensions_item.py +186 -0
  12. tableauserverclient/models/favorites_item.py +21 -8
  13. tableauserverclient/models/flow_item.py +1 -1
  14. tableauserverclient/models/group_item.py +7 -1
  15. tableauserverclient/models/groupset_item.py +14 -0
  16. tableauserverclient/models/interval_item.py +2 -1
  17. tableauserverclient/models/oidc_item.py +82 -0
  18. tableauserverclient/models/permissions_item.py +2 -0
  19. tableauserverclient/models/project_item.py +3 -2
  20. tableauserverclient/models/property_decorators.py +2 -2
  21. tableauserverclient/models/reference_item.py +12 -6
  22. tableauserverclient/models/schedule_item.py +10 -1
  23. tableauserverclient/models/site_item.py +26 -0
  24. tableauserverclient/models/tableau_auth.py +13 -6
  25. tableauserverclient/models/user_item.py +10 -3
  26. tableauserverclient/models/workbook_item.py +2 -2
  27. tableauserverclient/server/endpoint/__init__.py +4 -0
  28. tableauserverclient/server/endpoint/datasources_endpoint.py +152 -22
  29. tableauserverclient/server/endpoint/extensions_endpoint.py +79 -0
  30. tableauserverclient/server/endpoint/flow_task_endpoint.py +1 -1
  31. tableauserverclient/server/endpoint/flows_endpoint.py +5 -4
  32. tableauserverclient/server/endpoint/oidc_endpoint.py +157 -0
  33. tableauserverclient/server/endpoint/projects_endpoint.py +12 -0
  34. tableauserverclient/server/endpoint/schedules_endpoint.py +48 -1
  35. tableauserverclient/server/endpoint/users_endpoint.py +274 -5
  36. tableauserverclient/server/endpoint/views_endpoint.py +23 -0
  37. tableauserverclient/server/endpoint/workbooks_endpoint.py +124 -9
  38. tableauserverclient/server/request_factory.py +281 -2
  39. tableauserverclient/server/request_options.py +12 -2
  40. tableauserverclient/server/server.py +4 -0
  41. {tableauserverclient-0.38.dist-info → tableauserverclient-0.40.dist-info}/METADATA +5 -26
  42. {tableauserverclient-0.38.dist-info → tableauserverclient-0.40.dist-info}/RECORD +45 -39
  43. {tableauserverclient-0.38.dist-info → tableauserverclient-0.40.dist-info}/WHEEL +1 -1
  44. tableauserverclient-0.38.dist-info/licenses/LICENSE.versioneer +0 -7
  45. {tableauserverclient-0.38.dist-info → tableauserverclient-0.40.dist-info}/licenses/LICENSE +0 -0
  46. {tableauserverclient-0.38.dist-info → tableauserverclient-0.40.dist-info}/top_level.txt +0 -0
@@ -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
 
@@ -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"):
@@ -569,3 +736,105 @@ class Users(QuerysetEndpoint[UserItem]):
569
736
  """
570
737
 
571
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
  """
@@ -30,9 +30,12 @@ from tableauserverclient.models import WorkbookItem, ConnectionItem, ViewItem, P
30
30
  from tableauserverclient.server import RequestFactory
31
31
 
32
32
  from typing import (
33
+ Literal,
33
34
  Optional,
34
35
  TYPE_CHECKING,
36
+ TypeVar,
35
37
  Union,
38
+ overload,
36
39
  )
37
40
  from collections.abc import Iterable, Sequence
38
41
 
@@ -325,16 +328,92 @@ class Workbooks(QuerysetEndpoint[WorkbookItem], TaggingMixin[WorkbookItem]):
325
328
  logger.info(f"Updated workbook item (ID: {workbook_item.id} & connection item {connection_item.id})")
326
329
  return connection
327
330
 
331
+ # Update workbook_connections
332
+ @api(version="3.26")
333
+ def update_connections(
334
+ self,
335
+ workbook_item: WorkbookItem,
336
+ connection_luids: Iterable[str],
337
+ authentication_type: str,
338
+ username: Optional[str] = None,
339
+ password: Optional[str] = None,
340
+ embed_password: Optional[bool] = None,
341
+ ) -> list[ConnectionItem]:
342
+ """
343
+ Bulk updates one or more workbook connections by LUID, including authenticationType, username, password, and embedPassword.
344
+
345
+ Parameters
346
+ ----------
347
+ workbook_item : WorkbookItem
348
+ The workbook item containing the connections.
349
+
350
+ connection_luids : Iterable of str
351
+ The connection LUIDs to update.
352
+
353
+ authentication_type : str
354
+ The authentication type to use (e.g., 'AD Service Principal').
355
+
356
+ username : str, optional
357
+ The username to set (e.g., client ID for keypair auth).
358
+
359
+ password : str, optional
360
+ The password or secret to set.
361
+
362
+ embed_password : bool, optional
363
+ Whether to embed the password.
364
+
365
+ Returns
366
+ -------
367
+ Iterable of str
368
+ The connection LUIDs that were updated.
369
+ """
370
+
371
+ url = f"{self.baseurl}/{workbook_item.id}/connections"
372
+
373
+ request_body = RequestFactory.Workbook.update_connections_req(
374
+ connection_luids,
375
+ authentication_type,
376
+ username=username,
377
+ password=password,
378
+ embed_password=embed_password,
379
+ )
380
+
381
+ # Send request
382
+ server_response = self.put_request(url, request_body)
383
+ connection_items = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
384
+ updated_ids: list[str] = [conn.id for conn in connection_items]
385
+
386
+ logger.info(f"Updated connections for workbook {workbook_item.id}: {', '.join(updated_ids)}")
387
+ return connection_items
388
+
389
+ T = TypeVar("T", bound=FileObjectW)
390
+
391
+ @overload
392
+ def download(
393
+ self,
394
+ workbook_id: str,
395
+ filepath: T,
396
+ include_extract: bool = True,
397
+ ) -> T: ...
398
+
399
+ @overload
400
+ def download(
401
+ self,
402
+ workbook_id: str,
403
+ filepath: Optional[FilePath] = None,
404
+ include_extract: bool = True,
405
+ ) -> str: ...
406
+
328
407
  # Download workbook contents with option of passing in filepath
329
408
  @api(version="2.0")
330
409
  @parameter_added_in(no_extract="2.5")
331
410
  @parameter_added_in(include_extract="2.5")
332
411
  def download(
333
412
  self,
334
- workbook_id: str,
335
- filepath: Optional[PathOrFileW] = None,
336
- include_extract: bool = True,
337
- ) -> PathOrFileW:
413
+ workbook_id,
414
+ filepath=None,
415
+ include_extract=True,
416
+ ):
338
417
  """
339
418
  Downloads a workbook to the specified directory (optional).
340
419
 
@@ -683,6 +762,30 @@ class Workbooks(QuerysetEndpoint[WorkbookItem], TaggingMixin[WorkbookItem]):
683
762
  """
684
763
  return self._permissions.delete(item, capability_item)
685
764
 
765
+ @overload
766
+ def publish(
767
+ self,
768
+ workbook_item: WorkbookItem,
769
+ file: PathOrFileR,
770
+ mode: str,
771
+ connections: Optional[Sequence[ConnectionItem]],
772
+ as_job: Literal[False],
773
+ skip_connection_check: bool,
774
+ parameters=None,
775
+ ) -> WorkbookItem: ...
776
+
777
+ @overload
778
+ def publish(
779
+ self,
780
+ workbook_item: WorkbookItem,
781
+ file: PathOrFileR,
782
+ mode: str,
783
+ connections: Optional[Sequence[ConnectionItem]],
784
+ as_job: Literal[True],
785
+ skip_connection_check: bool,
786
+ parameters=None,
787
+ ) -> JobItem: ...
788
+
686
789
  @api(version="2.0")
687
790
  @parameter_added_in(as_job="3.0")
688
791
  @parameter_added_in(connections="2.8")
@@ -919,15 +1022,27 @@ class Workbooks(QuerysetEndpoint[WorkbookItem], TaggingMixin[WorkbookItem]):
919
1022
  revisions = RevisionItem.from_response(server_response.content, self.parent_srv.namespace, workbook_item)
920
1023
  return revisions
921
1024
 
1025
+ T = TypeVar("T", bound=FileObjectW)
1026
+
1027
+ @overload
1028
+ def download_revision(
1029
+ self, workbook_id: str, revision_number: Optional[str], filepath: T, include_extract: bool
1030
+ ) -> T: ...
1031
+
1032
+ @overload
1033
+ def download_revision(
1034
+ self, workbook_id: str, revision_number: Optional[str], filepath: Optional[FilePath], include_extract: bool
1035
+ ) -> str: ...
1036
+
922
1037
  # Download 1 workbook revision by revision number
923
1038
  @api(version="2.3")
924
1039
  def download_revision(
925
1040
  self,
926
- workbook_id: str,
927
- revision_number: Optional[str],
928
- filepath: Optional[PathOrFileW] = None,
929
- include_extract: bool = True,
930
- ) -> PathOrFileW:
1041
+ workbook_id,
1042
+ revision_number,
1043
+ filepath,
1044
+ include_extract=True,
1045
+ ):
931
1046
  """
932
1047
  Downloads a workbook revision to the specified directory (optional).
933
1048