From b3d740c0bc3c07f9c7f813909bdc6a657a63af3d Mon Sep 17 00:00:00 2001 From: Chris Shin Date: Thu, 14 Mar 2019 11:08:24 -0700 Subject: [PATCH 1/5] Add new fields to ProjectItem and clean up parsing logic --- tableauserverclient/models/project_item.py | 121 +++++++++++++----- .../server/endpoint/projects_endpoint.py | 18 ++- tableauserverclient/server/request_factory.py | 13 +- test/assets/project_get.xml | 15 ++- test/test_project.py | 42 +++--- 5 files changed, 134 insertions(+), 75 deletions(-) diff --git a/tableauserverclient/models/project_item.py b/tableauserverclient/models/project_item.py index 92e0282ae..99e49f92b 100644 --- a/tableauserverclient/models/project_item.py +++ b/tableauserverclient/models/project_item.py @@ -1,5 +1,6 @@ import xml.etree.ElementTree as ET -from .property_decorators import property_is_enum, property_not_empty +from .property_decorators import property_is_enum, property_not_empty, property_not_nullable +from ..datetime_helpers import parse_datetime class ProjectItem(object): @@ -8,11 +9,16 @@ class ContentPermissions: ManagedByOwner = 'ManagedByOwner' def __init__(self, name, description=None, content_permissions=None, parent_id=None): - self._content_permissions = None + self._created_at = None self._id = None + self._owner_id = None + self._owner_name = None + self._top_level_project = None + self._updated_at = None + + self.content_permissions = content_permissions self.description = description self.name = name - self.content_permissions = content_permissions self.parent_id = parent_id @property @@ -24,6 +30,18 @@ def content_permissions(self): def content_permissions(self, value): self._content_permissions = value + @property + def created_at(self): + return self._created_at + + @property + def description(self): + return self._description + + @description.setter + def description(self, value): + self._description = value + @property def id(self): return self._id @@ -34,32 +52,62 @@ def name(self): @name.setter @property_not_empty + @property_not_nullable def name(self, value): self._name = value + @property + def owner_id(self): + return self._owner_id + + @property + def owner_name(self): + return self._owner_name + + @property + def parent_id(self): + return self._parent_id + + @parent_id.setter + def parent_id(self, value): + self._parent_id = value + + @property + def top_level_project(self): + return self._top_level_project + + @property + def updated_at(self): + return self._updated_at + def is_default(self): return self.name.lower() == 'default' - def _parse_common_tags(self, project_xml): - if not isinstance(project_xml, ET.Element): - project_xml = ET.fromstring(project_xml).find('.//t:project', namespaces=NAMESPACE) - - if project_xml is not None: - (_, name, description, content_permissions, parent_id) = self._parse_element(project_xml) - self._set_values(None, name, description, content_permissions, parent_id) - return self - - def _set_values(self, project_id, name, description, content_permissions, parent_id): - if project_id is not None: - self._id = project_id - if name: - self._name = name - if description: - self.description = description - if content_permissions: - self._content_permissions = content_permissions - if parent_id: - self.parent_id = parent_id + def _set_values(self, project_fields): + if 'contentPermissions' in project_fields: + self._content_permissions = project_fields['contentPermissions'] + if 'createdAt' in project_fields: + self._created_at = parse_datetime(project_fields['createdAt']) + if 'description' in project_fields: + self._description = project_fields['description'] + if 'id' in project_fields: + self._id = project_fields['id'] + if 'name' in project_fields: + self._name = project_fields['name'] + if 'parentProjectId' in project_fields: + self._parent_id = project_fields['parentProjectId'] + if 'topLevelProject' in project_fields: + self._top_level_project = string_to_bool(project_fields['topLevelProject']) + if 'updatedAt' in project_fields: + self._updated_at = parse_datetime(project_fields['updatedAt']) + if 'owner' in project_fields: + owner_fields = project_fields['owner'] + if 'id' in owner_fields: + self._owner_id = owner_fields['id'] + if 'name' in owner_fields: + self._owner_name = owner_fields['name'] + if self.parent_id is not None: + self._top_level_project = False @classmethod def from_response(cls, resp, ns): @@ -68,18 +116,23 @@ def from_response(cls, resp, ns): all_project_xml = parsed_response.findall('.//t:project', namespaces=ns) for project_xml in all_project_xml: - (id, name, description, content_permissions, parent_id) = cls._parse_element(project_xml) - project_item = cls(name) - project_item._set_values(id, name, description, content_permissions, parent_id) + project_fields = cls._parse_element(project_xml, ns) + project_item = cls(project_fields['name']) + project_item._set_values(project_fields) all_project_items.append(project_item) return all_project_items @staticmethod - def _parse_element(project_xml): - id = project_xml.get('id', None) - name = project_xml.get('name', None) - description = project_xml.get('description', None) - content_permissions = project_xml.get('contentPermissions', None) - parent_id = project_xml.get('parentProjectId', None) - - return id, name, description, content_permissions, parent_id + def _parse_element(project_xml, ns): + project_fields = project_xml.attrib + owner_elem = project_xml.find('.//t:owner', namespaces=ns) + if owner_elem is not None: + owner_fields = owner_elem.attrib + project_fields['owner'] = owner_fields + + return project_fields + + +# Used to convert string represented boolean to a boolean type +def string_to_bool(s): + return s.lower() == 'true' diff --git a/tableauserverclient/server/endpoint/projects_endpoint.py b/tableauserverclient/server/endpoint/projects_endpoint.py index 8157e1f59..b44452c2e 100644 --- a/tableauserverclient/server/endpoint/projects_endpoint.py +++ b/tableauserverclient/server/endpoint/projects_endpoint.py @@ -1,4 +1,4 @@ -from .endpoint import Endpoint, api +from .endpoint import Endpoint, api, parameter_added_in from .exceptions import MissingRequiredFieldError from .. import RequestFactory, ProjectItem, PaginationItem import logging @@ -14,7 +14,7 @@ def baseurl(self): @api(version="2.0") def get(self, req_options=None): logger.info('Querying all projects on site') - url = self.baseurl + url = self.baseurl + '?fields=_all_' server_response = self.get_request(url, req_options) pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) all_project_items = ProjectItem.from_response(server_response.content, self.parent_srv.namespace) @@ -30,22 +30,28 @@ def delete(self, project_id): logger.info('Deleted single project (ID: {0})'.format(project_id)) @api(version="2.0") - def update(self, project_item): + @parameter_added_in(publish_samples="2.5") + def update(self, project_item, publish_samples=False): if not project_item.id: error = "Project item missing ID." raise MissingRequiredFieldError(error) url = "{0}/{1}".format(self.baseurl, project_item.id) - update_req = RequestFactory.Project.update_req(project_item) + if publish_samples: + url += '?publishSamples=true' + update_req = RequestFactory.Project.common_req(project_item) server_response = self.put_request(url, update_req) logger.info('Updated project item (ID: {0})'.format(project_item.id)) updated_project = ProjectItem.from_response(server_response.content, self.parent_srv.namespace)[0] return updated_project @api(version="2.0") - def create(self, project_item): + @parameter_added_in(publish_samples="2.5") + def create(self, project_item, publish_samples=False): url = self.baseurl - create_req = RequestFactory.Project.create_req(project_item) + if publish_samples: + url += '?publishSamples=true' + create_req = RequestFactory.Project.common_req(project_item) server_response = self.post_request(url, create_req) new_project = ProjectItem.from_response(server_response.content, self.parent_srv.namespace)[0] logger.info('Created new project (ID: {0})'.format(new_project.id)) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 7f0a3ac3b..3ebf77e22 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -170,18 +170,7 @@ def add_req(self, permission_item): class ProjectRequest(object): - def update_req(self, project_item): - xml_request = ET.Element('tsRequest') - project_element = ET.SubElement(xml_request, 'project') - if project_item.name: - project_element.attrib['name'] = project_item.name - if project_item.description: - project_element.attrib['description'] = project_item.description - if project_item.content_permissions: - project_element.attrib['contentPermissions'] = project_item.content_permissions - return ET.tostring(xml_request) - - def create_req(self, project_item): + def common_req(self, project_item): xml_request = ET.Element('tsRequest') project_element = ET.SubElement(xml_request, 'project') project_element.attrib['name'] = project_item.name diff --git a/test/assets/project_get.xml b/test/assets/project_get.xml index 777412b30..bd2d6e01e 100644 --- a/test/assets/project_get.xml +++ b/test/assets/project_get.xml @@ -1,9 +1,12 @@ - - + + - - - + + + + + + - + \ No newline at end of file diff --git a/test/test_project.py b/test/test_project.py index c0958f761..1c86c3b5c 100644 --- a/test/test_project.py +++ b/test/test_project.py @@ -2,6 +2,7 @@ import os import requests_mock import tableauserverclient as TSC +from tableauserverclient.datetime_helpers import format_datetime TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets') @@ -27,23 +28,30 @@ def test_get(self): m.get(self.baseurl, text=response_xml) all_projects, pagination_item = self.server.projects.get() - self.assertEqual(3, pagination_item.total_available) - self.assertEqual('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', all_projects[0].id) - self.assertEqual('default', all_projects[0].name) - self.assertEqual('The default project that was automatically created by Tableau.', - all_projects[0].description) - self.assertEqual('ManagedByOwner', all_projects[0].content_permissions) - self.assertEqual(None, all_projects[0].parent_id) - - self.assertEqual('1d0304cd-3796-429f-b815-7258370b9b74', all_projects[1].id) - self.assertEqual('Tableau', all_projects[1].name) - self.assertEqual('ManagedByOwner', all_projects[1].content_permissions) - self.assertEqual(None, all_projects[1].parent_id) - - self.assertEqual('4cc52973-5e3a-4d1f-a4fb-5b5f73796edf', all_projects[2].id) - self.assertEqual('Tableau > Child 1', all_projects[2].name) - self.assertEqual('ManagedByOwner', all_projects[2].content_permissions) - self.assertEqual('1d0304cd-3796-429f-b815-7258370b9b74', all_projects[2].parent_id) + self.assertEqual(2, pagination_item.total_available) + pr1 = all_projects[0] + pr2 = all_projects[1] + + self.assertEqual('bdd975c6-4042-11e9-a712-975dc31937aa', pr1.id) + self.assertEqual('Default', pr1.name) + self.assertEqual('The default project that was automatically created by Tableau.', pr1.description) + self.assertEqual(True, pr1.top_level_project) + self.assertEqual('2019-03-06T19:04:57Z', format_datetime(pr1.created_at)) + self.assertEqual('2019-03-06T19:04:58Z', format_datetime(pr1.updated_at)) + self.assertEqual('ManagedByOwner', pr1.content_permissions) + self.assertEqual('f9e32d4b-ca36-43bb-bc58-29ad45b10be5', pr1.owner_id) + self.assertEqual('_system', pr1.owner_name) + + self.assertEqual('7e593a18-c6e2-469c-9aca-4b2782693777', pr2.id) + self.assertEqual('update', pr2.name) + self.assertEqual('upd', pr2.description) + self.assertEqual(False, pr2.top_level_project) + self.assertEqual('bdd975c6-4042-11e9-a712-975dc31937aa', pr2.parent_id) + self.assertEqual('2019-03-13T22:18:18Z', format_datetime(pr2.created_at)) + self.assertEqual('2019-03-14T17:13:40Z', format_datetime(pr2.updated_at)) + self.assertEqual('ManagedByOwner', pr2.content_permissions) + self.assertEqual('344356bd-a847-4d6c-8370-8b2821498cdb', pr2.owner_id) + self.assertEqual('testadmin', pr2.owner_name) def test_get_before_signin(self): self.server._auth_token = None From d2fedfc780a4d1d701bbe7844d0b528a4a87db4d Mon Sep 17 00:00:00 2001 From: Jac Date: Thu, 23 Jan 2025 01:51:37 -0800 Subject: [PATCH 2/5] fixed indentation? --- tableauserverclient/models/project_item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tableauserverclient/models/project_item.py b/tableauserverclient/models/project_item.py index 04080f0cf..274b73915 100644 --- a/tableauserverclient/models/project_item.py +++ b/tableauserverclient/models/project_item.py @@ -195,7 +195,7 @@ def _parse_common_tags(self, project_xml, ns): self._set_values(None, name, description, content_permissions, parent_id) return self - def _set_values(self, project_fields): + def _set_values(self, project_fields): if 'contentPermissions' in project_fields: self._content_permissions = project_fields['contentPermissions'] if 'createdAt' in project_fields: From 468249152b0fd369661265183054547c26558e21 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Fri, 24 Jan 2025 03:20:21 -0800 Subject: [PATCH 3/5] fix test checks/input --- test/assets/project_get.xml | 8 +++----- test/test_project.py | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/test/assets/project_get.xml b/test/assets/project_get.xml index ce604cd8f..38e64da50 100644 --- a/test/assets/project_get.xml +++ b/test/assets/project_get.xml @@ -1,13 +1,11 @@ - + - - + - - + \ No newline at end of file diff --git a/test/test_project.py b/test/test_project.py index a80d4919c..8a9a69947 100644 --- a/test/test_project.py +++ b/test/test_project.py @@ -43,7 +43,6 @@ def test_get(self) -> None: self.assertEqual("ManagedByOwner", all_projects[0].content_permissions) self.assertEqual(None, all_projects[0].parent_id) self.assertEqual("dd2239f6-ddf1-4107-981a-4cf94e415794", all_projects[0].owner_id) - self.assertEqual('f9e32d4b-ca36-43bb-bc58-29ad45b10be5', all_projects[0].owner_id) self.assertEqual('_system', all_projects[0].owner_name) self.assertEqual("1d0304cd-3796-429f-b815-7258370b9b74", all_projects[1].id) From fa0129e4edadf8fb4c7d18bc895d5b24af0c4dce Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Fri, 24 Jan 2025 03:20:48 -0800 Subject: [PATCH 4/5] fix code --- tableauserverclient/models/project_item.py | 98 +++++++------------ .../server/endpoint/projects_endpoint.py | 4 +- 2 files changed, 34 insertions(+), 68 deletions(-) diff --git a/tableauserverclient/models/project_item.py b/tableauserverclient/models/project_item.py index 274b73915..0f5ddb113 100644 --- a/tableauserverclient/models/project_item.py +++ b/tableauserverclient/models/project_item.py @@ -39,12 +39,12 @@ def __init__( self.parent_id: Optional[str] = parent_id self._samples: Optional[bool] = samples self._owner_id: Optional[str] = None - + self._created_at = None self._owner_name: Optional[str] = None - self._top_level_project = None + self._top_level_project: Optional[bool] = None self._updated_at = None - + self._permissions = None self._default_workbook_permissions = None self._default_datasource_permissions = None @@ -181,46 +181,7 @@ def is_default(self): return self.name.lower() == "default" def _parse_common_tags(self, project_xml, ns): - if not isinstance(project_xml, ET.Element): - project_xml = fromstring(project_xml).find(".//t:project", namespaces=ns) - - if project_xml is not None: - ( - _, - name, - description, - content_permissions, - parent_id, - ) = self._parse_element(project_xml) - self._set_values(None, name, description, content_permissions, parent_id) - return self - - def _set_values(self, project_fields): - if 'contentPermissions' in project_fields: - self._content_permissions = project_fields['contentPermissions'] - if 'createdAt' in project_fields: - self._created_at = parse_datetime(project_fields['createdAt']) - if 'description' in project_fields: - self._description = project_fields['description'] - if 'id' in project_fields: - self._id = project_fields['id'] - if 'name' in project_fields: - self._name = project_fields['name'] - if 'parentProjectId' in project_fields: - self._parent_id = project_fields['parentProjectId'] - if 'topLevelProject' in project_fields: - self._top_level_project = string_to_bool(project_fields['topLevelProject']) - if 'updatedAt' in project_fields: - self._updated_at = parse_datetime(project_fields['updatedAt']) - if 'owner' in project_fields: - owner_fields = project_fields['owner'] - if 'id' in owner_fields: - self._owner_id = owner_fields['id'] - if 'name' in owner_fields: - self._owner_name = owner_fields['name'] - if self.parent_id is not None: - self._top_level_project = False - + return ProjectItem.from_xml(ProjectItem, project_xml, ns) def _set_permissions(self, permissions): self._permissions = permissions @@ -240,33 +201,40 @@ def from_response(cls, resp, ns) -> list["ProjectItem"]: all_project_xml = parsed_response.findall(".//t:project", namespaces=ns) for project_xml in all_project_xml: - project_item = cls.from_xml(project_xml) + project_item = cls.from_xml(project_xml, ns) all_project_items.append(project_item) return all_project_items @classmethod - def from_xml(cls, project_xml, namespace=None) -> "ProjectItem": + def from_xml(cls, project_xml, ns) -> "ProjectItem": project_item = cls() - project_item._set_values(*cls._parse_element(project_xml)) - return project_item - - @staticmethod - def _parse_element(project_xml, ns): - id = project_xml.get("id", None) - name = project_xml.get("name", None) - description = project_xml.get("description", None) - content_permissions = project_xml.get("contentPermissions", None) - parent_id = project_xml.get("parentProjectId", None) - project_fields = project_xml.attrib - owner_elem = project_xml.find('.//t:owner', namespaces=ns) + if project_xml.get("contentPermissions", None): + project_item._content_permissions = project_xml.get("contentPermissions") + if project_xml.get("createdAt", None): + project_item._created_at = parse_datetime(project_xml.get("createdAt")) + if project_xml.get("description", None): + project_item._description = project_xml.get("description") + if project_xml.get("id", None): + project_item._id = project_xml.get("id") + if project_xml.get("name", None): + project_item._name = project_xml.get("name") + if project_xml.get("parentProjectId", None): + project_item._parent_id = project_xml.get("parentProjectId") + if project_xml.get("topLevelProject", None): + project_item._top_level_project = ProjectItem.string_to_bool(project_xml.get("topLevelProject")) + if project_xml.get("updatedAt", None): + project_item._updated_at = parse_datetime(project_xml.get("updatedAt")) + + if project_item.parent_id is not None: + project_item._top_level_project = False + + owner_elem = project_xml.find(".//t:owner", ns) if owner_elem is not None: - owner_fields = owner_elem.attrib - project_fields['owner'] = owner_fields - owner_id = owner_elem.get("id", None) - - return project_fields + project_item._owner_id = owner_elem.get("id", None) + project_item._owner_name = owner_elem.get("name", None) + return project_item -# Used to convert string represented boolean to a boolean type -def string_to_bool(s): - return s.lower() == 'true' + # Used to convert string represented boolean to a boolean type + def string_to_bool(s): + return s.lower() == "true" diff --git a/tableauserverclient/server/endpoint/projects_endpoint.py b/tableauserverclient/server/endpoint/projects_endpoint.py index 3a13f44d7..078aa68bd 100644 --- a/tableauserverclient/server/endpoint/projects_endpoint.py +++ b/tableauserverclient/server/endpoint/projects_endpoint.py @@ -1,7 +1,6 @@ from .endpoint import Endpoint, api, parameter_added_in from .exceptions import MissingRequiredFieldError -from .. import RequestFactory, ProjectItem, PaginationItem import logging @@ -10,8 +9,7 @@ from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint from tableauserverclient.server import RequestFactory, RequestOptions -from tableauserverclient.models.permissions_item import PermissionsRule -from tableauserverclient.models import ProjectItem, PaginationItem, Resource +from tableauserverclient.models import PaginationItem, PermissionsRule, ProjectItem, Resource from typing import Optional, TYPE_CHECKING From 3ce8b679079cfe7ef57cb601f80b44eb3730e4ac Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Fri, 24 Jan 2025 03:22:46 -0800 Subject: [PATCH 5/5] black --- tableauserverclient/server/endpoint/projects_endpoint.py | 3 +-- test/test_project.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tableauserverclient/server/endpoint/projects_endpoint.py b/tableauserverclient/server/endpoint/projects_endpoint.py index 078aa68bd..ffda4ea7f 100644 --- a/tableauserverclient/server/endpoint/projects_endpoint.py +++ b/tableauserverclient/server/endpoint/projects_endpoint.py @@ -1,4 +1,3 @@ - from .endpoint import Endpoint, api, parameter_added_in from .exceptions import MissingRequiredFieldError @@ -36,7 +35,7 @@ def baseurl(self) -> str: @api(version="2.0") def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[ProjectItem], PaginationItem]: logger.info("Querying all projects on site") - url = self.baseurl + '?fields=_all_' + url = self.baseurl + "?fields=_all_" server_response = self.get_request(url, req_options) pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) all_project_items = ProjectItem.from_response(server_response.content, self.parent_srv.namespace) diff --git a/test/test_project.py b/test/test_project.py index 8a9a69947..8ca662855 100644 --- a/test/test_project.py +++ b/test/test_project.py @@ -43,7 +43,7 @@ def test_get(self) -> None: self.assertEqual("ManagedByOwner", all_projects[0].content_permissions) self.assertEqual(None, all_projects[0].parent_id) self.assertEqual("dd2239f6-ddf1-4107-981a-4cf94e415794", all_projects[0].owner_id) - self.assertEqual('_system', all_projects[0].owner_name) + self.assertEqual("_system", all_projects[0].owner_name) self.assertEqual("1d0304cd-3796-429f-b815-7258370b9b74", all_projects[1].id) self.assertEqual("Tableau", all_projects[1].name) @@ -56,7 +56,7 @@ def test_get(self) -> None: self.assertEqual("ManagedByOwner", all_projects[2].content_permissions) self.assertEqual("1d0304cd-3796-429f-b815-7258370b9b74", all_projects[2].parent_id) self.assertEqual("dd2239f6-ddf1-4107-981a-4cf94e415794", all_projects[2].owner_id) - self.assertEqual('testadmin', all_projects[2].owner_name) + self.assertEqual("testadmin", all_projects[2].owner_name) def test_get_before_signin(self) -> None: self.server._auth_token = None