10000 Add support for metric items and endpoints (#960) · tableau/server-client-python@2194c0d · GitHub
[go: up one dir, main page]

Skip to content

Commit 2194c0d

Browse files
jorwoodsjacalata
authored andcommitted
Add support for metric items and endpoints (#960)
* Add support for metric items and endpoints
1 parent ea6d494 commit 2194c0d

13 files changed

+443
-5
lines changed

tableauserverclient/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
PersonalAccessTokenAuth,
3737
FlowRunItem,
3838
RevisionItem,
39+
MetricItem,
3940
)
4041
from .namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE
4142
from .server import (

tableauserverclient/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
HourlyInterval,
2020
)
2121
from .job_item import JobItem, BackgroundJobItem
22+
from .metric_item import MetricItem
2223
from .pagination_item import PaginationItem
2324
from .permissions_item import PermissionsRule, Permission
2425
from .personal_access_token_auth import PersonalAccessTokenAuth
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import xml.etree.ElementTree as ET
2+
from ..datetime_helpers import parse_datetime
3+
from .property_decorators import property_is_boolean, property_is_datetime
4+
from .tag_item import TagItem
5+
from typing import List, Optional, TYPE_CHECKING, Set
6+
7+
if TYPE_CHECKING:
8+
from datetime import datetime
9+
10+
11+
class MetricItem(object):
12+
def __init__(self, name: Optional[str] = None):
13+
self._id: Optional[str] = None
14+
self._name: Optional[str] = name
15+
self._description: Optional[str] = None
16+
self._webpage_url: Optional[str] = None
17+
self._created_at: Optional["datetime"] = None
18+
self._updated_at: Optional["datetime"] = None
19+
self._suspended: Optional[bool] = None
20+
self._project_id: Optional[str] = None
21+
self._project_name: Optional[str] = None
22+
self._owner_id: Optional[str] = None
23+
self._view_id: Optional[str] = None
24+
self._initial_tags: Set[str] = set()
25+
self.tags: Set[str] = set()
26+
27+
@property
28+
def id(self) -> Optional[str]:
29+
return self._id
30+
31+
@id.setter
32+
def id(self, value: Optional[str]) -> None:
33+
self._id = value
34+
35+
@property
36+
def name(self) -> Optional[str]:
37+
return self._name
38+
39+
@name.setter
40+
def name(self, value: Optional[str]) -> None:
41+
self._name = value
42+
43+
@property
44+
def description(self) -> Optional[str]:
45+
return self._description
46+
47+
@description.setter
48+
def description(self, value: Optional[str]) -> None:
49+
self._description = value
50+
51+
@property
52+
def webpage_url(self) -> Optional[str]:
53+
return self._webpage_url
54+
55+
@property
56+
def created_at(self) -> Optional["datetime"]:
57+
return self._created_at
58+
59+
@created_at.setter
60+
@property_is_datetime
61+
def created_at(self, value: "datetime") -> None:
62+
self._created_at = value
63+
64+
@property
65+
def updated_at(self) -> Optional["datetime"]:
66+
return self._updated_at
67+
68+
@updated_at.setter
69+
@property_is_datetime
70+
def updated_at(self, value: "datetime") -> None:
71+
self._updated_at = value
72+
73+
@property
74+
def suspended(self) -> Optional[bool]:
75+
return self._suspended
76+
77+
@suspended.setter
78+
@property_is_boolean
79+
def suspended(self, value: bool) -> None:
80+
self._suspended = value
81+
82+
@property
83+
def project_id(self) -> Optional[str]:
84+
return self._project_id
85+
86+
@project_id.setter
87+
def project_id(self, value: Optional[str]) -> None:
88+
self._project_id = value
89+
90+
@property
91+
def project_name(self) -> Optional[str]:
92+
return self._project_name
93+
94+
@project_name.setter
95+
def project_name(self, value: Optional[str]) -> None:
96+
self._project_name = value
97+
98+
@property
99+
def owner_id(self) -> Optional[str]:
100+
return self._owner_id
101+
102+
@owner_id.setter
103+
def owner_id(self, value: Optional[str]) -> None:
104+
self._owner_id = value
105+
106+
@property
107+
def view_id(self) -> Optional[str]:
108+
return self._view_id
109+
110+
@view_id.setter
111+
def view_id(self, value: Optional[str]) -> None:
112+
self._view_id = value
113+
114+
def __repr__(self):
115+
return "<MetricItem# name={_name} id={_id} owner_id={_owner_id}>".format(**vars(self))
116+
117+
@classmethod
118+
def from_response(
119+
cls,
120+
resp: bytes,
121+
ns,
122+
) -> List["MetricItem"]:
123+
all_metric_items = list()
124+
parsed_response = ET.fromstring(resp)
125+
all_metric_xml = parsed_response.findall(".//t:metric", namespaces=ns)
126+
for metric_xml in all_metric_xml:
127+
metric_item = cls()
128+
metric_item._id = metric_xml.get("id", None)
129+
metric_item._name = metric_xml.get("name", None)
130+
metric_item._description = metric_xml.get("description", None)
131+
metric_item._webpage_url = metric_xml.get("webpageUrl", None)
132+
metric_item._created_at = parse_datetime(metric_xml.get("createdAt", None))
133+
metric_item._updated_at = parse_datetime(metric_xml.get("updatedAt", None))
134+
metric_item._suspended = string_to_bool(metric_xml.get("suspended", ""))
135+
for owner in metric_xml.findall(".//t:owner", namespaces=ns):
136+
metric_item._owner_id = owner.get("id", None)
137+
138+
for project in metric_xml.findall(".//t:project", namespaces=ns):
139+
metric_item._project_id = project.get("id", None)
140+
metric_item._project_name = project.get("name", None)
141+
142+
for view in metric_xml.findall(".//t:underlyingView", namespaces=ns):
143+
metric_item._view_id = view.get("id", None)
144+
145+
tags = set()
146+
tags_elem = metric_xml.find(".//t:tags", namespaces=ns)
147+
if tags_elem is not None:
148+
all_tags = TagItem.from_xml_element(tags_elem, ns)
149+
tags = all_tags
150+
151+
metric_item.tags = tags
152+
metric_item._initial_tags = tags
153+
154+
all_metric_items.append(metric_item)
155+
return all_metric_items
156+
157+
158+
# Used to convert string represented boolean to a boolean type
159+
def string_to_bool(s: str) -> bool:
160+
return s.lower() == "true"

tableauserverclient/models/revision_item.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,8 @@ def user_name(self) -> Optional[str]:
5353

5454
def __repr__(self):
5555
return (
56-
"<RevisionItem# revisionNumber={_revision_number} "
57-
"current={_current} deleted={_deleted} user={_user_id}>".format(**self.__dict__)
58-
)
56+
"<RevisionItem# revisionNumber={_revision_number} " "current={_current} deleted={_deleted} user={_user_id}>"
57+
).format(**self.__dict__)
5958

6059
@classmethod
6160
def from_response(cls, resp: bytes, ns, resource_item) -> List["RevisionItem"]:

tableauserverclient/models/tag_item.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
from typing import Set
2+
import xml.etree.ElementTree as ET
13
from defusedxml.ElementTree import fromstring
24

35

46
class TagItem(object):
57
@classmethod
6-
def from_response(cls, resp, ns):
8+
def from_response(cls, resp: bytes, ns) -> Set[str]:
79
return cls.from_xml_element(fromstring(resp), ns)
810

911
@classmethod
10-
def from_xml_element(cls, parsed_response, ns):
12+
def from_xml_element(cls, parsed_response: ET.Element, ns) -> Set[str]:
1113
all_tags = set()
1214
tag_elem = parsed_response.findall(".//t:tag", namespaces=ns)
1315
for tag_xml in tag_elem:

tableauserverclient/server/endpoint/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from .groups_endpoint import Groups
1717
from .jobs_endpoint import Jobs
1818
from .metadata_endpoint import Metadata
19+
from .metrics_endpoint import Metrics
1920
from .projects_endpoint import Projects
2021
from .schedules_endpoint import Schedules
2122
from .server_info_endpoint import ServerInfo
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from .endpoint import QuerysetEndpoint, api
2+
from .exceptions import MissingRequiredFieldError
3+
from .permissions_endpoint import _PermissionsEndpoint
4+
from .dqw_endpoint import _DataQualityWarningEndpoint
5+
from .resource_tagger import _ResourceTagger
6+
from .. import RequestFactory, PaginationItem
7+
from ...models.metric_item import MetricItem
8+
9+
import logging
10+
import copy
11+
12+
from typing import List, Optional, TYPE_CHECKING, Tuple
13+
14+
if TYPE_CHECKING:
15+
from ..request_options import RequestOptions
16+
from ...server import Server
17+
18+
19+
logger = logging.getLogger("tableau.endpoint.metrics")
20+
21+
22+
class Metrics(QuerysetEndpoint):
23+
def __init__(self, parent_srv: "Server") -> None:
24+
super(Metrics, self).__init__(parent_srv)
25+
self._resource_tagger = _ResourceTagger(parent_srv)
26+
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
27+
self._data_quality_warnings = _DataQualityWarningEndpoint(self.parent_srv, "metric")
28+
29+
@property
30+
def baseurl(self) -> str:
31+
return "{0}/sites/{1}/metrics".format(self.parent_srv.baseurl, self.parent_srv.site_id)
32+
33+
# Get all metrics
34+
@api(version="3.9")
35+
def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[MetricItem], PaginationItem]:
36+
logger.info("Querying all metrics on site")
37+
url = self.baseurl
38+
server_response = self.get_request(url, req_options)
39+
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
40+
all_metric_items = MetricItem.from_response(server_response.content, self.parent_srv.namespace)
41+
return all_metric_items, pagination_item
42+
43+
# Get 1 metric by id
44+
@api(version="3.9")
45+
def get_by_id(self, metric_id: str) -> MetricItem:
46+
if not metric_id:
47+
error = "Metric ID undefined."
48+
raise ValueError(error)
49+
logger.info("Querying single metric (ID: {0})".format(metric_id))
50+
url = "{0}/{1}".format(self.baseurl, metric_id)
51+
server_response = self.get_request(url)
52+
return MetricItem.from_response(server_response.content, self.parent_srv.namespace)[0]
53+
54+
# Delete 1 metric by id
55+
@api(version="3.9")
56+
def delete(self, metric_id: str) -> None:
57+
if not metric_id:
58+
error = "Metric ID undefined."
59+
raise ValueError(error)
60+
url = "{0}/{1}".format(self.baseurl, metric_id)
61+
self.delete_request(url)
62+
logger.info("Deleted single metric (ID: {0})".format(metric_id))
63+
64+
# Update metric
65+
@api(version="3.9")
66+
def update(self, metric_item: MetricItem) -> MetricItem:
67+
if not metric_item.id:
68+
error = "Metric item missing ID. Metric must be retrieved from server first."
69+
raise MissingRequiredFieldError(error)
70+
71+
self._resource_tagger.update_tags(self.baseurl, metric_item)
72+
73+
# Update the metric itself
74+
url = "{0}/{1}".format(self.baseurl, metric_item.id)
75+
update_req = RequestFactory.Metric.update_req(metric_item)
76+
server_response = self.put_request(url, update_req)
77+
logger.info("Updated metric item (ID: {0})".format(metric_item.id))
78+
return MetricItem.from_response(server_response.content, self.parent_srv.namespace)[0]

tableauserverclient/server/request_factory.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
from os import name
12
import xml.etree.ElementTree as ET
23
from typing import Any, Dict, List, Optional, Tuple, Iterable
34

45
from requests.packages.urllib3.fields import RequestField
56
from requests.packages.urllib3.filepost import encode_multipart_formdata
67

8+
from tableauserverclient.models.metric_item import MetricItem
9+
710
from ..models import ConnectionItem
811
from ..models import DataAlertItem
912
from ..models import FlowItem
@@ -1053,6 +1056,26 @@ def create_req(self, xml_request: ET.Element, webhook_item: "WebhookItem") -> by
10531056
return ET.tostring(xml_request)
10541057

10551058

1059+
class MetricRequest:
1060+
@_tsrequest_wrapped
1061+
def update_req(self, xml_request: ET.Element, metric_item: MetricItem) -> bytes:
1062+
metric_element = ET.SubElement(xml_request, "metric")
1063+
if metric_item.id is not None:
1064+
metric_element.attrib["id"] = metric_item.id
1065+
if metric_item.name is not None:
1066+
metric_element.attrib["name"] = metric_item.name
1067+
if metric_item.description is not None:
1068+
metric_element.attrib["description"] = metric_item.description
1069+
if metric_item.suspended is not None:
1070+
metric_element.attrib["suspended"] = str(metric_item.suspended).lower()
1071+
if metric_item.project_id is not None:
1072+
ET.SubElement(metric_element, "project", {"id": metric_item.project_id})
1073+
if metric_item.owner_id is not None:
1074+
ET.SubElement(metric_element, "owner", {"id": metric_item.owner_id})
1075+
1076+
return ET.tostring(xml_request)
1077+
1078+
10561079
class RequestFactory(object):
10571080
Auth = AuthRequest()
10581081
Connection = Connection()
@@ -1066,6 +1089,7 @@ class RequestFactory(object):
10661089
Fileupload = FileuploadRequest()
10671090
Flow = FlowRequest()
10681091
Group = GroupRequest()
1092+
Metric = MetricRequest()
10691093
Permission = PermissionRequest()
10701094
Project = ProjectRequest()
10711095
Schedule = ScheduleRequest()

tableauserverclient/server/server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
DataAlerts,
2828
Fileuploads,
2929
FlowRuns,
30+
Metrics,
3031
)
3132
from .endpoint.exceptions import (
3233
EndpointUnavailableError,
@@ -83,6 +84,7 @@ def __init__(self, server_address, use_server_version=True):
8384
self.fileuploads = Fileuploads(self)
8485
self._namespace = Namespace()
8586
self.flow_runs = FlowRuns(self)
87+
self.metrics = Metrics(self)
8688

8789
if use_server_version:
8890
self.use_server_version()

test/assets/metrics_get.xml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<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">
3+
<pagination pageNumber="1" pageSize="100" totalAvailable="27"/>
4+
<metric id="6561daa3-20e8-407f-ba09-709b178c0b4a"
5+
name="Example metric"
6+
description="Description of my metric."
7+
webpageUrl="https://test/#/site/site-name/metrics/3"
8+
createdAt="2020-01-02T01:02:03Z"
9+
updatedAt="2020-01-02T01:02:03Z"
10+
suspended="true">
11+
<project id="32e79edb-6cfd-47dc-ad79-e8ec2fbb1d33"
12+
name="Default"/>
13+
<owner id="32e79edb-6cfd-47dc-ad79-e8ec2fbb1d33"/>
14+
<tags/>
15+
<underlyingView id="29dae0cd-1862-4a20-a638-e2c2dfa682d4"/>
16+
</metric>
17+
<metric id="721760d9-0aa4-4029-87ae-371c956cea07"
18+
name="Another Example metric"
19+
description="Description of another metric."
20+
webpageUrl="https://test/#/site/site-name/metrics/4"
21+
createdAt="2020-01-03T01:02:03Z"
22+
updatedAt="2020-01-04T01:02:03Z"
23+
suspended="false">
24+
<project id="486e0de0-2258-45bd-99cf-b62013e19f4e"
25+
name="Assets"/>
26+
<owner id="1bbbc2b9-847d-443c-9a1f-dbcf112b8814"/>
27+
<tags>
28+
<tag label="Test" />
29+
<tag label="Asset" />
30+
</tags>
31+
<underlyingView id="7dbfdb63-a6ca-4723-93ee-4fefc71992d3"/>
32+
</metric>
33+
</tsResponse>

0 commit comments

Comments
 (0)
0