8000 Add support for metric items and endpoints by jorwoods · Pull Request #960 · tableau/server-client-python · GitHub
[go: up one dir, main page]

Skip to content

Add support for metric items and endpoints #960

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Mar 13, 2022
1 change: 1 addition & 0 deletions tableauserverclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
PersonalAccessTokenAuth,
FlowRunItem,
RevisionItem,
MetricItem,
)
from .namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE
from .server import (
Expand Down
1 change: 1 addition & 0 deletions tableauserverclient/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
HourlyInterval,
)
from .job_item import JobItem, BackgroundJobItem
from .metric_item import MetricItem
from .pagination_item import PaginationItem
from .permissions_item import PermissionsRule, Permission
from .personal_access_token_auth import PersonalAccessTokenAuth
Expand Down
160 changes: 160 additions & 0 deletions tableauserverclient/models/metric_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import xml.etree.ElementTree as ET
from ..datetime_helpers import parse_datetime
from .property_decorators import property_is_boolean, property_is_datetime
from .tag_item import TagItem
from typing import List, Optional, TYPE_CHECKING, Set

if TYPE_CHECKING:
from datetime import datetime


class MetricItem(object):
def __init__(self, name: Optional[str] = None):
self._id: Optional[str] = None
self._name: Optional[str] = name
self._description: Optional[str] = None
self._webpage_url: Optional[str] = None
self._created_at: Optional["datetime"] = None
self._updated_at: Optional["datetime"] = None
self._suspended: Optional[bool] = None
self._project_id: Optional[str] = None
self._project_name: Optional[str] = None
self._owner_id: Optional[str] = None
self._view_id: Optional[str] = None
self._initial_tags: Set[str] = set()
self.tags: Set[str] = set()

@property
def id(self) -> Optional[str]:
return self._id

@id.setter
def id(self, value: Optional[str]) -> None:
self._id = value

@property
def name(self) -> Optional[str]:
return self._name

@name.setter
def name(self, value: Optional[str]) -> None:
self._name = value

@property
def description(self) -> Optional[str]:
return self._description

@description.setter
def description(self, value: Optional[str]) -> None:
self._description = value

@property
def webpage_url(self) -> Optional[str]:
return self._webpage_url

@property
def created_at(self) -> Optional["datetime"]:
return self._created_at

@created_at.setter
@property_is_datetime
def created_at(self, value: "datetime") -> None:
self._created_at = value

@property
def updated_at(self) -> Optional["datetime"]:
return self._updated_at

@updated_at.setter
@property_is_datetime
def updated_at(self, value: "datetime") -> None:
self._updated_at = value

@property
def suspended(self) -> Optional[bool]:
return self._suspended

@suspended.setter
@property_is_boolean
def suspended(self, value: bool) -> None:
self._suspended = value

@property
def project_id(self) -> Optional[str]:
return self._project_id

@project_id.setter
def project_id(self, value: Optional[str]) -> None:
self._project_id = value

@property
def project_name(self) -> Optional[str]:
return self._project_name

@project_name.setter
def project_name(self, value: Optional[str]) -> None:
self._project_name = value

@property
def owner_id(self) -> Optional[str]:
return self._owner_id

@owner_id.setter
def owner_id(self, value: Optional[str]) -> None:
self._owner_id = value

@property
def view_id(self) -> Optional[str]:
return self._view_id

@view_id.setter
def view_id(self, value: Optional[str]) -> None:
self._view_id = value

def __repr__(self):
return "<MetricItem# name={_name} id={_id} owner_id={_owner_id}>".format(**vars(self))

@classmethod
def from_response(
cls,
resp: bytes,
ns,
) -> List["MetricItem"]:
all_metric_items = list()
parsed_response = ET.fromstring(resp)
all_metric_xml = parsed_response.findall(".//t:metric", namespaces=ns)
for metric_xml in all_metric_xml:
metric_item = cls()
metric_item._id = metric_xml.get("id", None)
metric_item._name = metric_xml.get("name", None)
metric_item._description = metric_xml.get("description", None)
metric_item._we 8000 bpage_url = metric_xml.get("webpageUrl", None)
metric_item._created_at = parse_datetime(metric_xml.get("createdAt", None))
metric_item._updated_at = parse_datetime(metric_xml.get("updatedAt", None))
metric_item._suspended = string_to_bool(metric_xml.get("suspended", ""))
for owner in metric_xml.findall(".//t:owner", namespaces=ns):
metric_item._owner_id = owner.get("id", None)

for project in metric_xml.findall(".//t:project", namespaces=ns):
metric_item._project_id = project.get("id", None)
metric_item._project_name = project.get("name", None)

for view in metric_xml.findall(".//t:underlyingView", namespaces=ns):
metric_item._view_id = view.get("id", None)

tags = set()
tags_elem = metric_xml.find(".//t:tags", namespaces=ns)
if tags_elem is not None:
all_tags = TagItem.from_xml_element(tags_elem, ns)
tags = all_tags

metric_item.tags = tags
metric_item._initial_tags = tags

all_metric_items.append(metric_item)
return all_metric_items


# Used to convert string represented boolean to a boolean type
def string_to_bool(s: str) -> bool:
return s.lower() == "true"
5 changes: 2 additions & 3 deletions tableauserverclient/models/revision_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,8 @@ def user_name(self) -> Optional[str]:

def __repr__(self):
return (
"<RevisionItem# revisionNumber={_revision_number} "
"current={_current} deleted={_deleted} user={_user_id}>".format(**self.__dict__)
)
"<RevisionItem# revisionNumber={_revision_number} " "current={_current} deleted={_deleted} user={_user_id}>"
).format(**self.__dict__)

@classmethod
def from_response(cls, resp: bytes, ns, resource_item) -> List["RevisionItem"]:
Expand Down
6 changes: 4 additions & 2 deletions tableauserverclient/models/tag_item.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from typing import Set
import xml.etree.ElementTree as ET
from defusedxml.ElementTree import fromstring


class TagItem(object):
@classmethod
def from_response(cls, resp, ns):
def from_response(cls, resp: bytes, ns) -> Set[str]:
return cls.from_xml_element(fromstring(resp), ns)

@classmethod
def from_xml_element(cls, parsed_response, ns):
def from_xml_element(cls, parsed_response: ET.Element, ns) -> Set[str]:
all_tags = set()
tag_elem = parsed_response.findall(".//t:tag", namespaces=ns)
for tag_xml in tag_elem:
Expand Down
1 change: 1 addition & 0 deletions tableauserverclient/server/endpoint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .groups_endpoint import Groups
from .jobs_endpoint import Jobs
from .metadata_endpoint import Metadata
from .metrics_endpoint import Metrics
from .projects_endpoint import Projects
from .schedules_endpoint import Schedules
from .server_info_endpoint import ServerInfo
Expand Down
78 changes: 78 additions & 0 deletions tableauserverclient/server/endpoint/metrics_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from .endpoint import QuerysetEndpoint, api
from .exceptions import MissingRequiredFieldError
from .permissions_endpoint import _PermissionsEndpoint
from .dqw_endpoint import _DataQualityWarningEndpoint
from .resource_tagger import _ResourceTagger
from .. import RequestFactory, PaginationItem
from ...models.metric_item import MetricItem

import logging
import copy

from typing import List, Optional, TYPE_CHECKING, Tuple

if TYPE_CHECKING:
from ..request_options import RequestOptions
from ...server import Server


logger = logging.getLogger("tableau.endpoint.metrics")


class Metrics(QuerysetEndpoint):
def __init__(self, parent_srv: "Server") -> None:
super(Metrics, self).__init__(parent_srv)
self._resource_tagger = _ResourceTagger(parent_srv)
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
self._data_quality_warnings = _DataQualityWarningEndpoint(self.parent_srv, "metric")

@property
def baseurl(self) -> str:
return "{0}/sites/{1}/metrics".format(self.parent_srv.baseurl, self.parent_srv.site_id)

# Get all metrics
@api(version="3.9")
def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[MetricItem], PaginationItem]:
logger.info("Querying all metrics on site")
url = self.baseurl
server_response = self.get_request(url, req_options)
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
all_metric_items = MetricItem.from_response(server_response.content, self.parent_srv.namespace)
return all_metric_items, pagination_item

# Get 1 metric by id
@api(version="3.9")
def get_by_id(self, metric_id: str) -> MetricItem:
if not metric_id:
error = "Metric ID undefined."
raise ValueError(error)
logger.info("Querying single metric (ID: {0})".format(metric_id))
url = "{0}/{1}".format(self.baseurl, metric_id)
server_response = self.get_request(url)
return MetricItem.from_response(server_response.content, self.parent_srv.namespace)[0]

# Delete 1 metric by id
@api(version="3.9")
def delete(self, metric_id: str) -> None:
if not metric_id:
error = "Metric ID undefined."
raise ValueError(error)
url = "{0}/{1}".format(self.baseurl, metric_id)
self.delete_request(url)
logger.info("Deleted single metric (ID: {0})".format(metric_id))

# Update metric
@api(version="3.9")
def update(self, metric_item: MetricItem) -> MetricItem:
if not metric_item.id:
error = "Metric item missing ID. Metric must be retrieved from server first."
raise MissingRequiredFieldError(error)

self._resource_tagger.update_tags(self.baseurl, metric_item)

# Update the metric itself
url = "{0}/{1}".format(self.baseurl, metric_item.id)
update_req = RequestFactory.Metric.update_req(metric_item)
server_response = self.put_request(url, update_req)
logger.info("Updated metric item (ID: {0})".format(metric_item.id))
return MetricItem.from_response(server_response.content, self.parent_srv.namespace)[0]
24 changes: 24 additions & 0 deletions tableauserverclient/server/request_factory.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from os import name
import xml.etree.ElementTree as ET
from typing import Any, Dict, List, Optional, Tuple, Iterable

from requests.packages.urllib3.fields import RequestField
from requests.packages.urllib3.filepost import encode_multipart_formdata

from tableauserverclient.models.metric_item import MetricItem

from ..models import ConnectionItem
from ..models import DataAlertItem
from ..models import FlowItem
Expand Down Expand Up @@ -1053,6 +1056,26 @@ def create_req(self, xml_request: ET.Element, webhook_item: "WebhookItem") -> by
return ET.tostring(xml_request)


class MetricRequest:
@_tsrequest_wrapped
def update_req(self, xml_request: ET.Element, metric_item: MetricItem) -> bytes:
metric_element = ET.SubElement(xml_request, "metric")
if metric_item.id is not None:
metric_element.attrib["id"] = metric_item.id
if metric_item.name is not None:
metric_element.attrib["name"] = metric_item.name
if metric_item.description is not None:
metric_element.attrib["description"] = metric_item.description
if metric_item.suspended is not None:
metric_element.attrib["suspended"] = str(metric_item.suspended).lower()
if metric_item.project_id is not None:
ET.SubElement(metric_element, "project", {"id": metric_item.project_id})
if metric_item.owner_id is not None:
ET.SubElement(metric_element, "owner", {"id": metric_item.owner_id})

return ET.tostring(xml_request)


class RequestFactory(object):
Auth = AuthRequest()
Connection = Connection()
Expand All @@ -1066,6 +1089,7 @@ class RequestFactory(object):
Fileupload = FileuploadRequest()
Flow = FlowRequest()
Group = GroupRequest()
Metric = MetricRequest()
Permission = PermissionRequest()
Project = ProjectRequest()
Schedule = ScheduleRequest()
Expand Down
2 changes: 2 additions & 0 deletions tableauserverclient/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
DataAlerts,
Fileuploads,
FlowRuns,
Metrics,
)
from .endpoint.exceptions import (
EndpointUnavailableError,
Expand Down Expand Up @@ -83,6 +84,7 @@ def __init__(self, server_address, use_server_version=True):
self.fileuploads = Fileuploads(self)
self._namespace = Namespace()
self.flow_runs = FlowRuns(self)
self.metrics = Metrics(self)

if use_server_version:
self.use_server_version()
Expand Down
33 changes: 33 additions & 0 deletions test/assets/metrics_get.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version='1.0' encoding='UTF-8'?>
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-3.5.xsd">
<pagination pageNumber="1" pageSize="100" totalAvailable="27"/>
<metric id="6561daa3-20e8-407f-ba09-709b178c0b4a"
name="Example metric"
description="Description of my metric."
webpageUrl="https://test/#/site/site-name/metrics/3"
createdAt="2020-01-02T01:02:03Z"
updatedAt="2020-01-02T01:02:03Z"
suspended="true">
<project id="32e79edb-6cfd-47dc-ad79-e8ec2fbb1d33"
name="Default"/>
<owner id="32e79edb-6cfd-47dc-ad79-e8ec2fbb1d33"/>
<tags/>
<underlyingView id="29dae0cd-1862-4a20-a638-e2c2dfa682d4"/>
</metric>
<metric id="721760d9-0aa4-4029-87ae-371c956cea07"
name="Another Example metric"
description="Description of another metric."
webpageUrl="https://test/#/site/site-name/metrics/4"
createdAt="2020-01-03T01:02:03Z"
updatedAt="2020-01-04T01:02:03Z"
suspended="false">
<project id="486e0de0-2258-45bd-99cf-b62013e19f4e"
name="Assets"/>
<owner id="1bbbc2b9-847d-443c-9a1f-dbcf112b8814"/>
<tags>
<tag label="Test" />
<tag label="Asset" />
</tags>
<underlyingView id="7dbfdb63-a6ca-4723-93ee-4fefc71992d3"/>
</metric>
</tsResponse>
Loading
0