From 16c11bdbabbd6c164c0fac81e3ce83175e604c6c Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Wed, 17 Nov 2021 09:08:37 -0600 Subject: [PATCH 1/4] Type hint projects --- tableauserverclient/models/project_item.py | 43 +++++++++++-------- tableauserverclient/models/workbook_item.py | 2 +- .../server/endpoint/projects_endpoint.py | 22 ++++++---- test/test_project.py | 30 ++++++------- 4 files changed, 56 insertions(+), 41 deletions(-) diff --git a/tableauserverclient/models/project_item.py b/tableauserverclient/models/project_item.py index c6525a2ca..2eee3069c 100644 --- a/tableauserverclient/models/project_item.py +++ b/tableauserverclient/models/project_item.py @@ -6,19 +6,28 @@ from .exceptions import UnpopulatedPropertyError +from typing import List, Optional, TYPE_CHECKING + + class ProjectItem(object): class ContentPermissions: - LockedToProject = "LockedToProject" - ManagedByOwner = "ManagedByOwner" - LockedToProjectWithoutNested = "LockedToProjectWithoutNested" - - def __init__(self, name, description=None, content_permissions=None, parent_id=None): + LockedToProject: str = "LockedToProject" + ManagedByOwner: str = "ManagedByOwner" + LockedToProjectWithoutNested: str = "LockedToProjectWithoutNested" + + def __init__( + self, + name: str, + description: Optional[str] = None, + content_permissions: Optional[str] = None, + parent_id: Optional[str] = None, + ) -> None: self._content_permissions = None - self._id = None - self.description = description - self.name = name - self.content_permissions = content_permissions - self.parent_id = parent_id + self._id: Optional[str] = None + self.description: Optional[str] = description + self.name: str = name + self.content_permissions: Optional[str] = content_permissions + self.parent_id: Optional[str] = parent_id self._permissions = None self._default_workbook_permissions = None @@ -31,7 +40,7 @@ def content_permissions(self): @content_permissions.setter @property_is_enum(ContentPermissions) - def content_permissions(self, value): + def content_permissions(self, value: Optional[str]) -> None: self._content_permissions = value @property @@ -63,24 +72,24 @@ def default_flow_permissions(self): return self._default_flow_permissions() @property - def id(self): + def id(self) -> Optional[str]: return self._id @property - def name(self): + def name(self) -> str: return self._name @name.setter @property_not_empty - def name(self, value): + def name(self, value: str) -> None: self._name = value @property - def owner_id(self): + def owner_id(self) -> Optional[str]: return self._owner_id @owner_id.setter - def owner_id(self, value): + def owner_id(self, value: str) -> None: raise NotImplementedError("REST API does not currently support updating project owner.") def is_default(self): @@ -126,7 +135,7 @@ def _set_default_permissions(self, permissions, content_type): ) @classmethod - def from_response(cls, resp, ns): + def from_response(cls, resp, ns) -> List["ProjectItem"]: all_project_items = list() parsed_response = ET.fromstring(resp) all_project_xml = parsed_response.findall(".//t:project", namespaces=ns) diff --git a/tableauserverclient/models/workbook_item.py b/tableauserverclient/models/workbook_item.py index 8e342686c..f7ba75c73 100644 --- a/tableauserverclient/models/workbook_item.py +++ b/tableauserverclient/models/workbook_item.py @@ -12,7 +12,7 @@ import copy import uuid -from typing import Dict, List, Optional, Set, TYPE_CHECKING +from typing import Dict, List, Optional, Set, TYPE_CHECKING, Union if TYPE_CHECKING: from .connection_item import ConnectionItem diff --git a/tableauserverclient/server/endpoint/projects_endpoint.py b/tableauserverclient/server/endpoint/projects_endpoint.py index 782a21654..9213af068 100644 --- a/tableauserverclient/server/endpoint/projects_endpoint.py +++ b/tableauserverclient/server/endpoint/projects_endpoint.py @@ -1,3 +1,4 @@ +from tableauserverclient.server.request_factory import ProjectRequest from .endpoint import api, Endpoint, XML_CONTENT_TYPE from .exceptions import MissingRequiredFieldError from .permissions_endpoint import _PermissionsEndpoint @@ -9,20 +10,26 @@ logger = logging.getLogger("tableau.endpoint.projects") +from typing import List, Optional, Tuple, TYPE_CHECKING + +if TYPE_CHECKING: + from ..server import Server + from ..request_options import RequestOptions + class Projects(Endpoint): - def __init__(self, parent_srv): + def __init__(self, parent_srv: "Server") -> None: super(Projects, self).__init__(parent_srv) self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl) self._default_permissions = _DefaultPermissionsEndpoint(parent_srv, lambda: self.baseurl) @property - def baseurl(self): + def baseurl(self) -> str: return "{0}/sites/{1}/projects".format(self.parent_srv.baseurl, self.parent_srv.site_id) @api(version="2.0") - def get(self, req_options=None): + def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[ProjectItem], PaginationItem]: logger.info("Querying all projects on site") url = self.baseurl server_response = self.get_request(url, req_options) @@ -31,7 +38,7 @@ def get(self, req_options=None): return all_project_items, pagination_item @api(version="2.0") - def delete(self, project_id): + def delete(self, project_id: str) -> None: if not project_id: error = "Project ID undefined." raise ValueError(error) @@ -40,7 +47,7 @@ def delete(self, project_id): logger.info("Deleted single project (ID: {0})".format(project_id)) @api(version="2.0") - def update(self, project_item, samples=False): + def update(self, project_item: ProjectItem, samples: bool = False) -> ProjectItem: if not project_item.id: error = "Project item missing ID." raise MissingRequiredFieldError(error) @@ -54,8 +61,7 @@ def update(self, project_item, samples=False): return updated_project @api(version="2.0") - def create(self, project_item, samples=False): - params = {"params": {RequestOptions.Field.PublishSamples: samples }} + def create(self, project_item: ProjectItem, samples: bool = False) -> ProjectItem: url = self.baseurl create_req = RequestFactory.Project.create_req(project_item) server_response = self.post_request(url, create_req, XML_CONTENT_TYPE, params) @@ -64,7 +70,7 @@ def create(self, project_item, samples=False): return new_project @api(version="2.0") - def populate_permissions(self, item): + def populate_permissions(self, item: ProjectItem) -> None: self._permissions.populate(item) @api(version="2.0") diff --git a/test/test_project.py b/test/test_project.py index be43b063e..7b85ea885 100644 --- a/test/test_project.py +++ b/test/test_project.py @@ -17,7 +17,7 @@ class ProjectTests(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self.server = TSC.Server('http://test') # Fake signin @@ -26,7 +26,7 @@ def setUp(self): self.baseurl = self.server.projects.baseurl - def test_get(self): + def test_get(self) -> None: with open(GET_XML, 'rb') as f: response_xml = f.read().decode('utf-8') with requests_mock.mock() as m: @@ -54,19 +54,19 @@ def test_get(self): self.assertEqual('1d0304cd-3796-429f-b815-7258370b9b74', all_projects[2].parent_id) self.assertEqual('dd2239f6-ddf1-4107-981a-4cf94e415794', all_projects[2].owner_id) - def test_get_before_signin(self): + def test_get_before_signin(self) -> None: self.server._auth_token = None self.assertRaises(TSC.NotSignedInError, self.server.projects.get) - def test_delete(self): + def test_delete(self) -> None: with requests_mock.mock() as m: m.delete(self.baseurl + '/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', status_code=204) self.server.projects.delete('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760') - def test_delete_missing_id(self): + def test_delete_missing_id(self) -> None: self.assertRaises(ValueError, self.server.projects.delete, '') - def test_update(self): + def test_update(self) -> None: with open(UPDATE_XML, 'rb') as f: response_xml = f.read().decode('utf-8') with requests_mock.mock() as m: @@ -84,7 +84,7 @@ def test_update(self): self.assertEqual('LockedToProject', single_project.content_permissions) self.assertEqual('9a8f2265-70f3-4494-96c5-e5949d7a1120', single_project.parent_id) - def test_content_permission_locked_to_project_without_nested(self): + def test_content_permission_locked_to_project_without_nested(self) -> None: with open(SET_CONTENT_PERMISSIONS_XML, 'rb') as f: response_xml = f.read().decode('utf-8') with requests_mock.mock() as m: @@ -101,7 +101,7 @@ def test_content_permission_locked_to_project_without_nested(self): self.assertEqual('LockedToProjectWithoutNested', project_item.content_permissions) self.assertEqual('7687bc43-a543-42f3-b86f-80caed03a813', project_item.parent_id) - def test_update_datasource_default_permission(self): + def test_update_datasource_default_permission(self) -> None: response_xml = read_xml_asset(UPDATE_DATASOURCE_DEFAULT_PERMISSIONS_XML) with requests_mock.mock() as m: m.put(self.baseurl + '/b4065286-80f0-11ea-af1b-cb7191f48e45/default-permissions/datasources', @@ -130,11 +130,11 @@ def test_update_datasource_default_permission(self): self.assertEqual('Allow', updated_capabilities['Write']) self.assertEqual('Allow', updated_capabilities['Connect']) - def test_update_missing_id(self): + def test_update_missing_id(self) -> None: single_project = TSC.ProjectItem('test') self.assertRaises(TSC.MissingRequiredFieldError, self.server.projects.update, single_project) - def test_create(self): + def test_create(self) -> None: with open(CREATE_XML, 'rb') as f: response_xml = f.read().decode('utf-8') with requests_mock.mock() as m: @@ -150,10 +150,10 @@ def test_create(self): self.assertEqual('ManagedByOwner', new_project.content_permissions) self.assertEqual('9a8f2265-70f3-4494-96c5-e5949d7a1120', new_project.parent_id) - def test_create_missing_name(self): + def test_create_missing_name(self) -> None: self.assertRaises(ValueError, TSC.ProjectItem, '') - def test_populate_permissions(self): + def test_populate_permissions(self) -> None: with open(asset(POPULATE_PERMISSIONS_XML), 'rb') as f: response_xml = f.read().decode('utf-8') with requests_mock.mock() as m: @@ -171,7 +171,7 @@ def test_populate_permissions(self): TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, }) - def test_populate_workbooks(self): + def test_populate_workbooks(self) -> None: response_xml = read_xml_asset(POPULATE_WORKBOOK_DEFAULT_PERMISSIONS_XML) with requests_mock.mock() as m: m.get(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/default-permissions/workbooks', @@ -204,7 +204,7 @@ def test_populate_workbooks(self): TSC.Permission.Capability.ChangeHierarchy: TSC.Permission.Mode.Allow, }) - def test_delete_permission(self): + def test_delete_permission(self) -> None: with open(asset(POPULATE_PERMISSIONS_XML), 'rb') as f: response_xml = f.read().decode('utf-8') with requests_mock.mock() as m: @@ -236,7 +236,7 @@ def test_delete_permission(self): m.delete('{}/{}/Write/Allow'.format(self.baseurl, endpoint), status_code=204) self.server.projects.delete_permission(item=single_project, rules=rules) - def test_delete_workbook_default_permission(self): + def test_delete_workbook_default_permission(self) -> None: with open(asset(POPULATE_WORKBOOK_DEFAULT_PERMISSIONS_XML), 'rb') as f: response_xml = f.read().decode('utf-8') From 150a79cb90cbc9f7c92e36a5c26db50911befc29 Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Sun, 24 Oct 2021 14:09:56 -0500 Subject: [PATCH 2/4] Type hint ProjectRequest --- tableauserverclient/server/request_factory.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 1e0c357dc..dd44d57af 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -5,6 +5,11 @@ from ..models import TaskItem, UserItem, GroupItem, PermissionsRule, FavoriteItem +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..models import ProjectItem + def _add_multipart(parts): mime_multipart_parts = list() @@ -398,7 +403,7 @@ def _add_all_capabilities(self, capabilities_element, capabilities_map): class ProjectRequest(object): - def update_req(self, project_item): + def update_req(self, project_item: "ProjectItem") -> bytes: xml_request = ET.Element("tsRequest") project_element = ET.SubElement(xml_request, "project") if project_item.name: @@ -411,7 +416,7 @@ def update_req(self, project_item): project_element.attrib["parentProjectId"] = project_item.parent_id return ET.tostring(xml_request) - def create_req(self, project_item): + def create_req(self, project_item: "ProjectItem") -> bytes: xml_request = ET.Element("tsRequest") project_element = ET.SubElement(xml_request, "project") project_element.attrib["name"] = project_item.name From 66f420bba7e5c376eeacb4ab51f42829401604e1 Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Tue, 2 Nov 2021 16:49:01 -0500 Subject: [PATCH 3/4] Re-add params to project create --- tableauserverclient/server/endpoint/projects_endpoint.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tableauserverclient/server/endpoint/projects_endpoint.py b/tableauserverclient/server/endpoint/projects_endpoint.py index 9213af068..23ae0afb6 100644 --- a/tableauserverclient/server/endpoint/projects_endpoint.py +++ b/tableauserverclient/server/endpoint/projects_endpoint.py @@ -62,6 +62,7 @@ def update(self, project_item: ProjectItem, samples: bool = False) -> ProjectIte @api(version="2.0") def create(self, project_item: ProjectItem, samples: bool = False) -> ProjectItem: + params = {"params": {RequestOptions.Field.PublishSamples: samples }} url = self.baseurl create_req = RequestFactory.Project.create_req(project_item) server_response = self.post_request(url, create_req, XML_CONTENT_TYPE, params) From 7f2435ab59807f51c61b29a504797d888dc3c908 Mon Sep 17 00:00:00 2001 From: Jac Date: Thu, 27 Jan 2022 22:14:27 -0800 Subject: [PATCH 4/4] Update test_group.py --- test/test_group.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_group.py b/test/test_group.py index fbfb488f1..d5af4cf0e 100644 --- a/test/test_group.py +++ b/test/test_group.py @@ -146,7 +146,6 @@ def test_add_user_missing_user_id(self) -> None: self.assertRaises(ValueError, self.server.groups.add_user, single_group, "") - def test_add_user_missing_group_id(self) -> None: single_group = TSC.GroupItem("test") single_group._users = []