8000 Jorwoods/type hint flows (#937) · tableau/server-client-python@fb49f31 · GitHub
[go: up one dir, main page]

Skip to content

Commit fb49f31

Browse files
authored
Jorwoods/type hint flows (#937)
* Type hint Flows * Type hint request_factory for flows * Add ConnectionItem type hints on to Flow publish request * Clean up flows publish signature * Fix flow update_permissions type hint * Fix update_permissions type hint * Add workbook.hidden_views type hint * Update permissions only accepts an iterable * Fix formatting
1 parent f40d4bc commit fb49f31

File tree

4 files changed

+82
-58
lines changed

4 files changed

+82
-58
lines changed

tableauserverclient/models/flow_item.py

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,28 @@
55
from ..datetime_helpers import parse_datetime
66
import copy
77

8+
from typing import List, Optional, TYPE_CHECKING, Set
9+
10+
if TYPE_CHECKING:
11+
import datetime
12+
from .connection_item import ConnectionItem
13+
from .permissions_item import Permission
14+
from .dqw_item import DQWItem
15+
816

917
class FlowItem(object):
10-
def __init__(self, project_id, name=None):
11-
self._webpage_url = None
12-
self._created_at = None
13-
self._id = None
14-
self._initial_tags = set()
15-
self._project_name = None
16-
self._updated_at = None
17-
self.name = name
18-
self.owner_id = None
19-
self.project_id = project_id
20-
self.tags = set()
21-
self.description = None
18+
def __init__(self, project_id: str, name: Optional[str] = None) -> None:
19+
self._webpage_url: Optional[str] = None
20+
self._created_at: Optional["datetime.datetime"] = None
21+
self._id: Optional[str] = None
22+
self._initial_tags: Set[str] = set()
23+
self._project_name: Optional[str] = None
24+
self._updated_at: Optional["datetime.datetime"] = None
25+
self.name: Optional[str] = name
26+
self.owner_id: Optional[str] = None
27+
self.project_id: str = project_id
28+
self.tags: Set[str] = set()
29+
self.description: Optional[str] = None
2230

2331
self._connections = None
2432
self._permissions = None
@@ -39,11 +47,11 @@ def permissions(self):
3947
return self._permissions()
4048

4149
@property
42-
def webpage_url(self):
50+
def webpage_url(self) -> Optional[str]:
4351
return self._webpage_url
4452

4553
@property
46-
def created_at(self):
54+
def created_at(self) -> Optional["datetime.datetime"]:
4755
return self._created_at
4856

4957
@property
@@ -54,36 +62,36 @@ def dqws(self):
5462
return self._data_quality_warnings()
5563

5664
@property
57-
def id(self):
65+
def id(self) -> Optional[str]:
5866
return self._id
5967

6068
@property
61-
def project_id(self):
69+
def project_id(self) -> str:
6270
return self._project_id
6371

6472
@project_id.setter
6573
@property_not_nullable
66-
def project_id(self, value):
74+
def project_id(self, value: str) -> None:
6775
self._project_id = value
6876

6977
@property
70-
def description(self):
78+
def description(self) -> Optional[str]:
7179
return self._description
7280

7381
@description.setter
74-
def description(self, value):
82+
def description(self, value: str) -> None:
7583
self._description = value
7684

7785
@property
78-
def project_name(self):
86+
def project_name(self) -> Optional[str]:
7987
return self._project_name
8088

8189
@property
82-
def flow_type(self):
90+
def flow_type(self): # What is this? It doesn't seem to get set anywhere.
8391
return self._flow_type
8492

8593
@property
86-
def updated_at(self):
94+
def updated_at(self) -> Optional["datetime.datetime"]:
8795
return self._updated_at
8896

8997
def _set_connections(self, connections):
@@ -161,7 +169,7 @@ def _set_values(
161169
self.owner_id = owner_id
162170

163171
@classmethod
164-
def from_response(cls, resp, ns):
172+
def from_response(cls, resp, ns) -> List["FlowItem"]:
165173
all_flow_items = list()
166174
parsed_response = ET.fromstring(resp)
167175
all_flow_xml = parsed_response.findall(".//t:flow", namespaces=ns)

tableauserverclient/server/endpoint/flows_endpoint.py

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,23 @@
1313
import cgi
1414
from contextlib import closing
1515

16+
from typing import Iterable, List, Optional, TYPE_CHECKING, Tuple, Union
17+
1618
# The maximum size of a file that can be published in a single request is 64MB
1719
FILESIZE_LIMIT = 1024 * 1024 * 64 # 64MB
1820

1921
ALLOWED_FILE_EXTENSIONS = ["tfl", "tflx"]
2022

2123
logger = logging.getLogger("tableau.endpoint.flows")
2224

25+
if TYPE_CHECKING:
26+
from .. import DQWItem
27+
from ..request_options import RequestOptions
28+
from ...models.permissions_item import Permission, PermissionsRule
29+
30+
31+
FilePath = Union[str, os.PathLike]
32+
2333

2434
class Flows(Endpoint):
2535
def __init__(self, parent_srv):
@@ -29,12 +39,12 @@ def __init__(self, parent_srv):
2939
self._data_quality_warnings = _DataQualityWarningEndpoint(self.parent_srv, "flow")
3040

3141
@property
32-
def baseurl(self):
42+
def baseurl(self) -> str:
3343
return "{0}/sites/{1}/flows".format(self.parent_srv.baseurl, self.parent_srv.site_id)
3444

3545
# Get all flows
3646
@api(version="3.3")
37-
def get(self, req_options=None):
47+
def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[FlowItem], PaginationItem]:
3848
logger.info("Querying all flows on site")
3949
url = self.baseurl
4050
server_response = self.get_request(url, req_options)
@@ -44,7 +54,7 @@ def get(self, req_options=None):
4454

4555
# Get 1 flow by id
4656
@api(version="3.3")
47-
def get_by_id(self, flow_id):
57+
def get_by_id(self, flow_id: str) -> FlowItem:
4858
if not flow_id:
4959
error = "Flow ID undefined."
5060
raise ValueError(error)
@@ -55,7 +65,7 @@ def get_by_id(self, flow_id):
5565

5666
# Populate flow item's connections
5767
@api(version="3.3")
58-
def populate_connections(self, flow_item):
68+
def populate_connections(self, flow_item: FlowItem) -> None:
5969
if not flow_item.id:
6070
error = "Flow item missing ID. Flow must be retrieved from server first."
6171
raise MissingRequiredFieldError(error)
@@ -66,15 +76,15 @@ def connections_fetcher():
6676
flow_item._set_connections(connections_fetcher)
6777
logger.info("Populated connections for flow (ID: {0})".format(flow_item.id))
6878

69-
def _get_flow_connections(self, flow_item, req_options=None):
79+
def _get_flow_connections(self, flow_item, req_options: Optional["RequestOptions"] = None) -> List[ConnectionItem]:
7080
url = "{0}/{1}/connections".format(self.baseurl, flow_item.id)
7181
server_response = self.get_request(url, req_options)
7282
connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
7383
return connections
7484

7585
# Delete 1 flow by id
7686
@api(version="3.3")
77-
def delete(self, flow_id):
87+
def delete(self, flow_id: str) -> None:
7888
if not flow_id:
7989
error = "Flow ID undefined."
8090
raise ValueError(error)
@@ -84,7 +94,7 @@ def delete(self, flow_id):
8494

8595
# Download 1 flow by id
8696
@api(version="3.3")
87-
def download(self, flow_id, filepath=None):
97+
def download(self, flow_id: str, filepath: FilePath = None) -> str:
8898
if not flow_id:
8999
error = "Flow ID undefined."
90100
raise ValueError(error)
@@ -105,7 +115,7 @@ def download(self, flow_id, filepath=None):
105115

106116
# Update flow
107117
@api(version="3.3")
108-
def update(self, flow_item):
118+
def update(self, flow_item: FlowItem) -> FlowItem:
109119
if not flow_item.id:
110120
error = "Flow item missing ID. Flow must be retrieved from server first."
111121
raise MissingRequiredFieldError(error)
@@ -122,7 +132,7 @@ def update(self, flow_item):
122132

123133
# Update flow connections
124134
@api(version="3.3")
125-
def update_connection(self, flow_item, connection_item):
135+
def update_connection(self, flow_item: FlowItem, connection_item: ConnectionItem) -> ConnectionItem:
126136
url = "{0}/{1}/connections/{2}".format(self.baseurl, flow_item.id, connection_item.id)
127137

128138
update_req = RequestFactory.Connection.update_req(connection_item)
@@ -133,7 +143,7 @@ def update_connection(self, flow_item, connection_item):
133143
return connection
134144

135145
@api(version="3.3")
136-
def refresh(self, flow_item):
146+
def refresh(self, flow_item: FlowItem) -> JobItem:
137147
url = "{0}/{1}/run".format(self.baseurl, flow_item.id)
138148
empty_req = RequestFactory.Empty.empty_req()
139149
server_response = self.post_request(url, empty_req)
@@ -142,7 +152,9 @@ def refresh(self, flow_item):
142152

143153
# Publish flow
144154
@api(version="3.3")
145-
def publish(self, flow_item, file_path, mode, connections=None):
155+
def publish(
156+
self, flow_item: FlowItem, file_path: FilePath, mode: str, connections: Optional[List[ConnectionItem]] = None
157+
) -> FlowItem:
146158
if not os.path.isfile(file_path):
147159
error = "File path does not lead to an existing file."
148160
raise IOError(error)
@@ -189,13 +201,8 @@ def publish(self, flow_item, file_path, mode, connections=None):
189201
logger.info("Published {0} (ID: {1})".format(filename, new_flow.id))
190202
return new_flow
191203

192-
server_response = self.post_request(url, xml_request, content_type)
193-
new_flow = FlowItem.from_response(server_response.content, self.parent_srv.namespace)[0]
194-
logger.info("Published {0} (ID: {1})".format(filename, new_flow.id))
195-
return new_flow
196-
197204
@api(version="3.3")
198-
def populate_permissions(self, item):
205+
def populate_permissions(self, item: FlowItem) -> None:
199206
self._permissions.populate(item)
200207

201208
@api(version="3.3")
@@ -209,25 +216,25 @@ def update_permission(self, item, permission_item):
209216
self._permissions.update(item, permission_item)
210217

211218
@api(version="3.3")
212-
def update_permissions(self, item, permission_item):
219+
def update_permissions(self, item: FlowItem, permission_item: Iterable["PermissionsRule"]) -> None:
213220
self._permissions.update(item, permission_item)
214221

215222
@api(version="3.3")
216-
def delete_permission(self, item, capability_item):
223+
def delete_permission(self, item: FlowItem, capability_item: "PermissionsRule") -> None:
217224
self._permissions.delete(item, capability_item)
218225

219226
@api(version="3.5")
220-
def populate_dqw(self, item):
227+
def populate_dqw(self, item: FlowItem) -> None:
221228
self._data_quality_warnings.populate(item)
222229

223230
@api(version="3.5")
224-
def update_dqw(self, item, warning):
231+
def update_dqw(self, item: FlowItem, warning: "DQWItem") -> None:
225232
return self._data_quality_warnings.update(item, warning)
226233

227234
@api(version="3.5")
228-
def add_dqw(self, item, warning):
235+
def add_dqw(self, item: FlowItem, warning: "DQWItem") -> None:
229236
return self._data_quality_warnings.add(item, warning)
230237

231238
@api(version="3.5")
232-
def delete_dqw(self, item):
239+
def delete_dqw(self, item: FlowItem) -> None:
233240
self._data_quality_warnings.clear(item)

tableauserverclient/server/request_factory.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55

66
from ..models import TaskItem, UserItem, GroupItem, PermissionsRule, FavoriteItem
77

8-
from typing import Optional, TYPE_CHECKING
8+
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple
99

1010
if TYPE_CHECKING:
1111
from ..models import DataAlertItem
12+
from ..models import FlowItem
13+
from ..models import ConnectionItem
1214

1315

14-
def _add_multipart(parts):
16+
def _add_multipart(parts: Dict) -> Tuple[Any, str]:
1517
mime_multipart_parts = list()
1618
for name, (filename, data, content_type) in parts.items():
1719
multipart_part = RequestField(name=name, data=data, filename=filename)
@@ -302,10 +304,11 @@ def chunk_req(self, chunk):
302304

303305

304306
class FlowRequest(object):
305-
def _generate_xml(self, flow_item, connections=None):
307+
def _generate_xml(self, flow_item: "FlowItem", connections: Optional[List["ConnectionItem"]] = None) -> bytes:
306308
xml_request = ET.Element("tsRequest")
307309
flow_element = ET.SubElement(xml_request, "flow")
308-
flow_element.attrib["name"] = flow_item.name
310+
if flow_item.name is not None:
311+
flow_element.attrib["name"] = flow_item.name
309312
project_element = ET.SubElement(flow_element, "project")
310313
project_element.attrib["id"] = flow_item.project_id
311314

@@ -315,7 +318,7 @@ def _generate_xml(self, flow_item, connections=None):
315318
_add_connections_element(connections_element, connection)
316319
return ET.tostring(xml_request)
317320

318-
def update_req(self, flow_item):
321+
def update_req(self, flow_item: "FlowItem") -> bytes:
319322
xml_request = ET.Element("tsRequest")
320323
flow_element = ET.SubElement(xml_request, "flow")
321324
if flow_item.project_id:
@@ -327,7 +330,13 @@ def update_req(self, flow_item):
327330

328331
return ET.tostring(xml_request)
329332

330-
def publish_req(self, flow_item, filename, file_contents, connections=None):
333+
def publish_req(
334+
self,
335+
flow_item: "FlowItem",
336+
filename: str,
337+
file_contents: bytes,
338+
connections: Optional[List["ConnectionItem"]] = None,
339+
) -> Tuple[Any, str]:
331340
xml_request = self._generate_xml(flow_item, connections)
332341

333342
parts = {
@@ -336,7 +345,7 @@ def publish_req(self, flow_item, filename, file_contents, connections=None):
336345
}
337346
return _add_multipart(parts)
338347

339-
def publish_req_chunked(self, flow_item, connections=None):
348+
def publish_req_chunked(self, flow_item, connections=None) -> Tuple[Any, str]:
340349
xml_request = self._generate_xml(flow_item, connections)
341350

342351
parts = {"request_payload": ("", xml_request, "text/xml")}

test/test_flow.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717

1818
class FlowTests(unittest.TestCase):
19-
def setUp(self):
19+
def setUp(self) -> None:
2020
self.server = TSC.Server("http://test")
2121

2222
# Fake signin
@@ -26,7 +26,7 @@ def setUp(self):
2626

2727
self.baseurl = self.server.flows.baseurl
2828

29-
def test_get(self):
29+
def test_get(self) -> None:
3030
response_xml = read_xml_asset(GET_XML)
3131
with requests_mock.mock() as m:
3232
m.get(self.baseurl, text=response_xml)
@@ -53,7 +53,7 @@ def test_get(self):
5353
self.assertEqual("aa23f4ac-906f-11e9-86fb-3f0f71412e77", all_flows[1].project_id)
5454
self.assertEqual("9127d03f-d996-405f-b392-631b25183a0f", all_flows[1].owner_id)
5555

56-
def test_update(self):
56+
def test_update(self) -> None:
5757
response_xml = read_xml_asset(UPDATE_XML)
5858
with requests_mock.mock() as m:
5959
m.put(self.baseurl + "/587daa37-b84d-4400-a9a2-aa90e0be7837", text=response_xml)
@@ -68,7 +68,7 @@ def test_update(self):
6868
self.assertEqual("7ebb3f20-0fd2-4f27-a2f6-c539470999e2", single_datasource.owner_id)
6969
self.assertEqual("So fun to see", single_datasource.description)
7070

71-
def test_populate_connections(self):
71+
def test_populate_connections(self) -> None:
7272
response_xml = read_xml_asset(POPULATE_CONNECTIONS_XML)
7373
with requests_mock.mock() as m:
7474
m.get(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections", text=response_xml)
@@ -97,7 +97,7 @@ def test_populate_connections(self):
9797
self.assertEqual("sally", conn3.username)
9898
self.assertEqual(True, conn3.embed_password)
9999

100-
def test_populate_permissions(self):
100+
def test_populate_permissions(self) -> None:
101101
with open(asset(POPULATE_PERMISSIONS_XML), "rb") as f:
102102
response_xml = f.read().decode("utf-8")
103103
with requests_mock.mock() as m:

0 commit comments

Comments
 (0)
0