From 12f9f9edc5ff0c59a646931cf986984a6d14412e Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Fri, 10 Sep 2021 23:06:48 -0500 Subject: [PATCH 01/14] Add tests for fetching flow runs --- test/assets/flowruns_get.xml | 18 +++++++++ test/assets/flowruns_get_by_id.xml | 10 +++++ test/test_flowruns.py | 59 ++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 test/assets/flowruns_get.xml create mode 100644 test/assets/flowruns_get_by_id.xml create mode 100644 test/test_flowruns.py diff --git a/test/assets/flowruns_get.xml b/test/assets/flowruns_get.xml new file mode 100644 index 000000000..54e43f0b3 --- /dev/null +++ b/test/assets/flowruns_get.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/test/assets/flowruns_get_by_id.xml b/test/assets/flowruns_get_by_id.xml new file mode 100644 index 000000000..859a85196 --- /dev/null +++ b/test/assets/flowruns_get_by_id.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/test/test_flowruns.py b/test/test_flowruns.py new file mode 100644 index 000000000..1695a54df --- /dev/null +++ b/test/test_flowruns.py @@ -0,0 +1,59 @@ +import unittest +import os +import requests_mock +import xml.etree.ElementTree as ET +import tableauserverclient as TSC +from tableauserverclient.datetime_helpers import format_datetime +from tableauserverclient.server.endpoint.exceptions import InternalServerError +from tableauserverclient.server.request_factory import RequestFactory +from ._utils import read_xml_asset, read_xml_assets, asset + +GET_XML = 'flowruns_get.xml' +GET_BY_ID_XML = 'flowruns_get_by_id.xml' + + +class FlowTests(unittest.TestCase): + def setUp(self): + self.server = TSC.Server('http://test') + + # Fake signin + self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67' + self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM' + self.server.version = "3.10" + + self.baseurl = self.server.flowruns.baseurl + + def test_get(self): + response_xml = read_xml_asset(GET_XML) + with requests_mock.mock() as m: + m.get(self.baseurl, text=response_xml) + all_flowruns, pagination_item = self.server.flowruns.get() + + self.assertEqual(2, pagination_item.total_available) + self.assertEqual('cc2e652d-4a9b-4476-8c93-b238c45db968', all_flowruns[0].id) + self.assertEqual('2021-02-11T01:42:55Z', format_datetime(all_flowruns[0].started_at)) + self.assertEqual('2021-02-11T01:57:38Z', format_datetime(all_flowruns[0].completed_at)) + self.assertEqual('Success', all_flowruns[0].status) + self.assertEqual('100', all_flowruns[0].progress) + self.assertEqual('aa23f4ac-906f-11e9-86fb-3f0f71412e77', all_flowruns[0].background_job_id) + + self.assertEqual('a3104526-c0c6-4ea5-8362-e03fc7cbd7ee', all_flowruns[1].id) + self.assertEqual('2021-02-13T04:05:30Z', format_datetime(all_flowruns[1].started_at)) + self.assertEqual('2021-02-13T04:05:35Z', format_datetime(all_flowruns[1].completed_at)) + self.assertEqual('Failed', all_flowruns[1].status) + self.assertEqual('100', all_flowruns[1].progress) + self.assertEqual('1ad21a9d-2530-4fbf-9064-efd3c736e023', all_flowruns[1].background_job_id) + + def test_get_by_id(self): + response_xml = read_xml_asset(GET_BY_ID_XML) + with requests_mock.mock() as m: + m.get(self.baseurl + "/cc2e652d-4a9b-4476-8c93-b238c45db968") + flowrun = self.server.flowruns.get_by_id("cc2e652d-4a9b-4476-8c93-b238c45db968") + + self.assertEqual('cc2e652d-4a9b-4476-8c93-b238c45db968', flowrun.id) + self.assertEqual('2021-02-11T01:42:55Z', format_datetime(flowrun.started_at)) + self.assertEqual('2021-02-11T01:57:38Z', format_datetime(flowrun.completed_at)) + self.assertEqual('Success', flowrun.status) + self.assertEqual('100', flowrun.progress) + self.assertEqual('aa23f4ac-906f-11e9-86fb-3f0f71412e77', flowrun.background_job_id) + From e1f2fb6b203e3fcfeeebef1c3591f571a923e7e4 Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Sat, 11 Sep 2021 15:42:01 -0500 Subject: [PATCH 02/14] Implement basics of FlowRuns --- tableauserverclient/__init__.py | 1 + tableauserverclient/models/__init__.py | 1 + tableauserverclient/models/flowrun_item.py | 109 ++++++++++++++++++ tableauserverclient/server/__init__.py | 1 + .../server/endpoint/__init__.py | 1 + .../server/endpoint/flowruns_endpoint.py | 47 ++++++++ tableauserverclient/server/server.py | 2 + test/assets/flowruns_get.xml | 9 +- test/assets/flowruns_get_by_id.xml | 6 +- test/test_flowruns.py | 6 +- 10 files changed, 173 insertions(+), 10 deletions(-) create mode 100644 tableauserverclient/models/flowrun_item.py create mode 100644 tableauserverclient/server/endpoint/flowruns_endpoint.py diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py index fcce4e0c7..2ad65d71e 100644 --- a/tableauserverclient/__init__.py +++ b/tableauserverclient/__init__.py @@ -34,6 +34,7 @@ FlowItem, WebhookItem, PersonalAccessTokenAuth, + FlowRunItem ) from .server import ( RequestOptions, diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py index c0ddc2e75..7aa72a922 100644 --- a/tableauserverclient/models/__init__.py +++ b/tableauserverclient/models/__init__.py @@ -10,6 +10,7 @@ from .favorites_item import FavoriteItem from .group_item import GroupItem from .flow_item import FlowItem +from .flowrun_item import FlowRunItem from .interval_item import ( IntervalItem, DailyInterval, diff --git a/tableauserverclient/models/flowrun_item.py b/tableauserverclient/models/flowrun_item.py new file mode 100644 index 000000000..93f1b28d4 --- /dev/null +++ b/tableauserverclient/models/flowrun_item.py @@ -0,0 +1,109 @@ +from typing import Any, Dict, Mapping +import xml.etree.ElementTree as ET +from .property_decorators import property_not_nullable +from ..datetime_helpers import parse_datetime +import itertools +from datetime import datetime + + +class FlowRunItem(object): + def __init__(self) -> None: + self._id: str = None + self._flow_id: str = None + self._status: str = None + self._started_at: datetime = None + self._completed_at: datetime = None + self._progress: str = None + self._background_job_id: str = None + + + @property + def id(self): + return self._id + + + @property + def flow_id(self): + return self._flow_id + + + @property + def status(self): + return self._status + + + @property + def started_at(self): + return self._started_at + + + @property + def completed_at(self): + return self._completed_at + + + @property + def progress(self): + return self._progress + + + @property + def background_job_id(self): + return self._background_job_id + + + def _set_values( + self, + id: str, + flow_id: str, + status: str, + started_at: datetime, + completed_at: datetime, + progress: str, + background_job_id: str + ): + if id is not None: + self._id = id + if flow_id is not None: + self._flow_id = flow_id + if status is not None: + self._status = status + if started_at is not None: + self._started_at = started_at + if completed_at is not None: + self._completed_at = completed_at + if progress is not None: + self._progress = progress + if background_job_id is not None: + self._background_job_id = background_job_id + + + @classmethod + def from_response(cls, resp, ns): + all_flowrun_items = list() + parsed_response = ET.fromstring(resp) + all_flowrun_xml = itertools.chain( + parsed_response.findall(".//t:flowRun[@id]", namespaces=ns), + parsed_response.findall(".//t:flowRuns[@id]", namespaces=ns) + ) + + for flowrun_xml in all_flowrun_xml: + parsed = cls._parse_element(flowrun_xml, ns) + flowrun_item = cls() + flowrun_item._set_values(**parsed) + all_flowrun_items.append(flowrun_item) + return all_flowrun_items + + + @staticmethod + def _parse_element(flowrun_xml: ET.Element, ns: Mapping[str, str]) -> Dict[str, Any]: + result = {} + result['id'] = flowrun_xml.get("id", None) + result['flow_id'] = flowrun_xml.get("flowId", None) + result['status'] = flowrun_xml.get("status", None) + result['started_at'] = parse_datetime(flowrun_xml.get("startedAt", None)) + result['completed_at'] = parse_datetime(flowrun_xml.get("completedAt", None)) + result['progress'] = flowrun_xml.get("progress", None) + result['background_job_id'] = flowrun_xml.get("backgroundJobId", None) + + return result diff --git a/tableauserverclient/server/__init__.py b/tableauserverclient/server/__init__.py index c653a8966..a03763969 100644 --- a/tableauserverclient/server/__init__.py +++ b/tableauserverclient/server/__init__.py @@ -32,6 +32,7 @@ ColumnItem, FlowItem, WebhookItem, + FlowRunItem ) from .endpoint import ( Auth, diff --git a/tableauserverclient/server/endpoint/__init__.py b/tableauserverclient/server/endpoint/__init__.py index 29fe93299..133eaadcb 100644 --- a/tableauserverclient/server/endpoint/__init__.py +++ b/tableauserverclient/server/endpoint/__init__.py @@ -7,6 +7,7 @@ from .favorites_endpoint import Favorites from .fileuploads_endpoint import Fileuploads from .flows_endpoint import Flows +from .flowruns_endpoint import FlowRuns from .exceptions import ( ServerResponseError, MissingRequiredFieldError, diff --git a/tableauserverclient/server/endpoint/flowruns_endpoint.py b/tableauserverclient/server/endpoint/flowruns_endpoint.py new file mode 100644 index 000000000..b7da69008 --- /dev/null +++ b/tableauserverclient/server/endpoint/flowruns_endpoint.py @@ -0,0 +1,47 @@ +from .endpoint import Endpoint, api +from .. import FlowRunItem, PaginationItem + +import logging + +logger = logging.getLogger("tableau.endpoint.flowruns") + + +class FlowRuns(Endpoint): + def __init__(self, parent_srv): + super(FlowRuns, self).__init__(parent_srv) + + @property + def baseurl(self): + return "{0}/sites/{1}/flows/runs".format(self.parent_srv.baseurl, self.parent_srv.site_id) + + # Get all flows + @api(version="3.10") + def get(self, req_options=None): + logger.info("Querying all flow runs 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_flow_items = FlowRunItem.from_response(server_response.content, self.parent_srv.namespace) + return all_flow_items, pagination_item + + # Get 1 flow by id + @api(version="3.10") + def get_by_id(self, flowrun_id): + if not flowrun_id: + error = "Flow ID undefined." + raise ValueError(error) + logger.info("Querying single flow (ID: {0})".format(flowrun_id)) + url = "{0}/{1}".format(self.baseurl, flowrun_id) + server_response = self.get_request(url) + return FlowRunItem.from_response(server_response.content, self.parent_srv.namespace)[0] + + + # Cancel 1 flow run by id + @api(version="3.10") + def cancel(self, flowrun_id): + if not flowrun_id: + error = "Flow ID undefined." + raise ValueError(error) + url = "{0}/{1}".format(self.baseurl, flowrun_id) + self.put_request(url) + logger.info("Deleted single flow (ID: {0})".format(flowrun_id)) diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index a20694a92..347476bae 100644 --- a/tableauserverclient/server/server.py +++ b/tableauserverclient/server/server.py @@ -25,6 +25,7 @@ Favorites, DataAlerts, Fileuploads, + FlowRuns ) from .endpoint.exceptions import ( EndpointUnavailableError, @@ -85,6 +86,7 @@ def __init__(self, server_address, use_server_version=False): self.data_alerts = DataAlerts(self) self.fileuploads = Fileuploads(self) self._namespace = Namespace() + self.flowruns = FlowRuns(self) if use_server_version: self.use_server_version() diff --git a/test/assets/flowruns_get.xml b/test/assets/flowruns_get.xml index 54e43f0b3..bdce4cdfb 100644 --- a/test/assets/flowruns_get.xml +++ b/test/assets/flowruns_get.xml @@ -1,14 +1,15 @@ - + + + backgroundJobId="aa23f4ac-906f-11e9-86fb-3f0f71412e77"/> + - + \ No newline at end of file diff --git a/test/test_flowruns.py b/test/test_flowruns.py index 1695a54df..58193f3be 100644 --- a/test/test_flowruns.py +++ b/test/test_flowruns.py @@ -12,7 +12,7 @@ GET_BY_ID_XML = 'flowruns_get_by_id.xml' -class FlowTests(unittest.TestCase): +class FlowRunTests(unittest.TestCase): def setUp(self): self.server = TSC.Server('http://test') @@ -47,7 +47,7 @@ def test_get(self): def test_get_by_id(self): response_xml = read_xml_asset(GET_BY_ID_XML) with requests_mock.mock() as m: - m.get(self.baseurl + "/cc2e652d-4a9b-4476-8c93-b238c45db968") + m.get(self.baseurl + "/cc2e652d-4a9b-4476-8c93-b238c45db968", text=response_xml) flowrun = self.server.flowruns.get_by_id("cc2e652d-4a9b-4476-8c93-b238c45db968") self.assertEqual('cc2e652d-4a9b-4476-8c93-b238c45db968', flowrun.id) @@ -55,5 +55,5 @@ def test_get_by_id(self): self.assertEqual('2021-02-11T01:57:38Z', format_datetime(flowrun.completed_at)) self.assertEqual('Success', flowrun.status) self.assertEqual('100', flowrun.progress) - self.assertEqual('aa23f4ac-906f-11e9-86fb-3f0f71412e77', flowrun.background_job_id) + self.assertEqual('1ad21a9d-2530-4fbf-9064-efd3c736e023', flowrun.background_job_id) From dbc68884f17a26da0e02ca18a1026b9173a2ced6 Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Sat, 11 Sep 2021 15:54:50 -0500 Subject: [PATCH 03/14] Add tests for cancel flow run --- tableauserverclient/models/flowrun_item.py | 33 +++++++++---------- .../server/endpoint/flowruns_endpoint.py | 6 ++-- test/test_flowruns.py | 11 +++++++ 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/tableauserverclient/models/flowrun_item.py b/tableauserverclient/models/flowrun_item.py index 93f1b28d4..251c667b1 100644 --- a/tableauserverclient/models/flowrun_item.py +++ b/tableauserverclient/models/flowrun_item.py @@ -1,20 +1,17 @@ -from typing import Any, Dict, Mapping import xml.etree.ElementTree as ET -from .property_decorators import property_not_nullable from ..datetime_helpers import parse_datetime import itertools -from datetime import datetime class FlowRunItem(object): def __init__(self) -> None: - self._id: str = None - self._flow_id: str = None - self._status: str = None - self._started_at: datetime = None - self._completed_at: datetime = None - self._progress: str = None - self._background_job_id: str = None + self._id=None + self._flow_id=None + self._status=None + self._started_at=None + self._completed_at=None + self._progress=None + self._background_job_id=None @property @@ -54,13 +51,13 @@ def background_job_id(self): def _set_values( self, - id: str, - flow_id: str, - status: str, - started_at: datetime, - completed_at: datetime, - progress: str, - background_job_id: str + id, + flow_id, + status, + started_at, + completed_at, + progress, + background_job_id, ): if id is not None: self._id = id @@ -96,7 +93,7 @@ def from_response(cls, resp, ns): @staticmethod - def _parse_element(flowrun_xml: ET.Element, ns: Mapping[str, str]) -> Dict[str, Any]: + def _parse_element(flowrun_xml, ns): result = {} result['id'] = flowrun_xml.get("id", None) result['flow_id'] = flowrun_xml.get("flowId", None) diff --git a/tableauserverclient/server/endpoint/flowruns_endpoint.py b/tableauserverclient/server/endpoint/flowruns_endpoint.py index b7da69008..c11eea15b 100644 --- a/tableauserverclient/server/endpoint/flowruns_endpoint.py +++ b/tableauserverclient/server/endpoint/flowruns_endpoint.py @@ -1,3 +1,4 @@ +from tableauserverclient.models import flowrun_item from .endpoint import Endpoint, api from .. import FlowRunItem, PaginationItem @@ -42,6 +43,7 @@ def cancel(self, flowrun_id): if not flowrun_id: error = "Flow ID undefined." raise ValueError(error) - url = "{0}/{1}".format(self.baseurl, flowrun_id) + id_ = getattr(flowrun_id, 'id', flowrun_id) + url = "{0}/{1}".format(self.baseurl, id_) self.put_request(url) - logger.info("Deleted single flow (ID: {0})".format(flowrun_id)) + logger.info("Deleted single flow (ID: {0})".format(id_)) diff --git a/test/test_flowruns.py b/test/test_flowruns.py index 58193f3be..475a41cc8 100644 --- a/test/test_flowruns.py +++ b/test/test_flowruns.py @@ -57,3 +57,14 @@ def test_get_by_id(self): self.assertEqual('100', flowrun.progress) self.assertEqual('1ad21a9d-2530-4fbf-9064-efd3c736e023', flowrun.background_job_id) + def test_cancel_id(self): + with requests_mock.mock() as m: + m.put(self.baseurl + '/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', status_code=204) + self.server.flowruns.cancel('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760') + + def test_cancel_item(self): + run = TSC.FlowRunItem() + run._id = 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760' + with requests_mock.mock() as m: + m.put(self.baseurl + '/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', status_code=204) + self.server.flowruns.cancel(run) From 1210e19d4baec5a650af147a186736ddffa3a79a Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Mon, 13 Sep 2021 08:27:03 -0500 Subject: [PATCH 04/14] Make FlowRuns a Queryset endpoint for easier filtering --- tableauserverclient/server/endpoint/flowruns_endpoint.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tableauserverclient/server/endpoint/flowruns_endpoint.py b/tableauserverclient/server/endpoint/flowruns_endpoint.py index c11eea15b..54f94b941 100644 --- a/tableauserverclient/server/endpoint/flowruns_endpoint.py +++ b/tableauserverclient/server/endpoint/flowruns_endpoint.py @@ -1,5 +1,5 @@ from tableauserverclient.models import flowrun_item -from .endpoint import Endpoint, api +from .endpoint import Endpoint, QuerysetEndpoint, api from .. import FlowRunItem, PaginationItem import logging @@ -7,7 +7,7 @@ logger = logging.getLogger("tableau.endpoint.flowruns") -class FlowRuns(Endpoint): +class FlowRuns(QuerysetEndpoint): def __init__(self, parent_srv): super(FlowRuns, self).__init__(parent_srv) From ac140d1077707a564af1a5f8ff89c584a5cde5fc Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Fri, 17 Sep 2021 09:27:17 -0500 Subject: [PATCH 05/14] Add test for flow refresh endpoint --- tableauserverclient/models/job_item.py | 19 +++++++++++++++++++ test/assets/flow_refresh.xml | 11 +++++++++++ test/test_flow.py | 20 ++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 test/assets/flow_refresh.xml diff --git a/tableauserverclient/models/job_item.py b/tableauserverclient/models/job_item.py index 2a8b6b509..1d432170f 100644 --- a/tableauserverclient/models/job_item.py +++ b/tableauserverclient/models/job_item.py @@ -1,4 +1,5 @@ import xml.etree.ElementTree as ET +from .flowrun_item import FlowRunItem from ..datetime_helpers import parse_datetime @@ -24,6 +25,7 @@ def __init__( finish_code=0, notes=None, mode=None, + flowrun=None, ): self._id = id_ self._type = job_type @@ -34,6 +36,7 @@ def __init__( self._finish_code = finish_code self._notes = notes or [] self._mode = mode + self._flowrun = flowrun @property def id(self): @@ -76,6 +79,14 @@ def mode(self, value): # check for valid data here self._mode = value + @property + def flowrun(self): + return self._flowrun + + @flowrun.setter + def flowrun(self, value): + self._flowrun = value + def __repr__(self): return ( " + + + + + + + \ No newline at end of file diff --git a/test/test_flow.py b/test/test_flow.py index f5c057c30..25ea1162d 100644 --- a/test/test_flow.py +++ b/test/test_flow.py @@ -12,6 +12,7 @@ POPULATE_CONNECTIONS_XML = 'flow_populate_connections.xml' POPULATE_PERMISSIONS_XML = 'flow_populate_permissions.xml' UPDATE_XML = 'flow_update.xml' +REFRESH_XML = 'flow_refresh.xml' class FlowTests(unittest.TestCase): @@ -113,3 +114,22 @@ def test_populate_permissions(self): TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow, TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, }) + + def test_refresh(self): + with open(asset(REFRESH_XML), 'rb') as f: + response_xml = f.read().decode('utf-8') + with requests_mock.mock() as m: + m.post(self.baseurl + '/92967d2d-c7e2-46d0-8847-4802df58f484/run', text=response_xml) + flow_item = TSC.FlowItem('test') + flow_item._id = '92967d2d-c7e2-46d0-8847-4802df58f484' + refresh_job = self.server.flows.refresh(flow_item) + + self.assertEqual(refresh_job.id, 'd1b2ccd0-6dfa-444a-aee4-723dbd6b7c9d') + self.assertEqual(refresh_job.mode, 'Asynchronous') + self.assertEqual(refresh_job.type, 'RunFlow') + self.assertEqual(format_datetime(refresh_job.created_at), '2018-05-22T13:00:29Z') + self.assertIsInstance(refresh_job.flowrun, TSC.FlowRunItem) + self.assertEqual(refresh_job.flowrun.id, 'e0c3067f-2333-4eee-8028-e0a56ca496f6') + self.assertEqual(refresh_job.flowrun.flow_id, '92967d2d-c7e2-46d0-8847-4802df58f484') + self.assertEqual(format_datetime(refresh_job.flowrun.started_at), '2018-05-22T13:00:29Z') + From 6fc3466d700c31a44115cd8c15d2cd38ea73cb83 Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Fri, 17 Sep 2021 13:11:41 -0500 Subject: [PATCH 06/14] Align to naming conventions --- tableauserverclient/models/job_item.py | 26 +++++++++++++------------- tableauserverclient/server/server.py | 2 +- test/test_flow.py | 8 ++++---- test/test_flowruns.py | 10 +++++----- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/tableauserverclient/models/job_item.py b/tableauserverclient/models/job_item.py index 1d432170f..5835e9629 100644 --- a/tableauserverclient/models/job_item.py +++ b/tableauserverclient/models/job_item.py @@ -25,7 +25,7 @@ def __init__( finish_code=0, notes=None, mode=None, - flowrun=None, + flow_run=None, ): self._id = id_ self._type = job_type @@ -36,7 +36,7 @@ def __init__( self._finish_code = finish_code self._notes = notes or [] self._mode = mode - self._flowrun = flowrun + self._flow_run = flow_run @property def id(self): @@ -80,12 +80,12 @@ def mode(self, value): self._mode = value @property - def flowrun(self): - return self._flowrun + def flow_run(self): + return self._flow_run - @flowrun.setter - def flowrun(self, value): - self._flowrun = value + @flow_run.setter + def flow_run(self, value): + self._flow_run = value def __repr__(self): return ( @@ -113,13 +113,13 @@ def _parse_element(cls, element, ns): finish_code = int(element.get("finishCode", -1)) notes = [note.text for note in element.findall(".//t:notes", namespaces=ns)] or None mode = element.get("mode", None) - flowrun = None + flow_run = None for flow_job in element.findall(".//t:runFlowJobType", namespaces=ns): - flowrun = FlowRunItem() - flowrun._id = flow_job.get("flowRunId", None) + flow_run = FlowRunItem() + flow_run._id = flow_job.get("flowRunId", None) for flow in flow_job.findall(".//t:flow", namespaces=ns): - flowrun._flow_id = flow.get("id", None) - flowrun._started_at = created_at or started_at + flow_run._flow_id = flow.get("id", None) + flow_run._started_at = created_at or started_at return cls( id_, type_, @@ -130,7 +130,7 @@ def _parse_element(cls, element, ns): finish_code, notes, mode, - flowrun, + flow_run, ) diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index 347476bae..56fc47849 100644 --- a/tableauserverclient/server/server.py +++ b/tableauserverclient/server/server.py @@ -86,7 +86,7 @@ def __init__(self, server_address, use_server_version=False): self.data_alerts = DataAlerts(self) self.fileuploads = Fileuploads(self) self._namespace = Namespace() - self.flowruns = FlowRuns(self) + self.flow_runs = FlowRuns(self) if use_server_version: self.use_server_version() diff --git a/test/test_flow.py b/test/test_flow.py index 25ea1162d..545623d03 100644 --- a/test/test_flow.py +++ b/test/test_flow.py @@ -128,8 +128,8 @@ def test_refresh(self): self.assertEqual(refresh_job.mode, 'Asynchronous') self.assertEqual(refresh_job.type, 'RunFlow') self.assertEqual(format_datetime(refresh_job.created_at), '2018-05-22T13:00:29Z') - self.assertIsInstance(refresh_job.flowrun, TSC.FlowRunItem) - self.assertEqual(refresh_job.flowrun.id, 'e0c3067f-2333-4eee-8028-e0a56ca496f6') - self.assertEqual(refresh_job.flowrun.flow_id, '92967d2d-c7e2-46d0-8847-4802df58f484') - self.assertEqual(format_datetime(refresh_job.flowrun.started_at), '2018-05-22T13:00:29Z') + self.assertIsInstance(refresh_job.flow_run, TSC.FlowRunItem) + self.assertEqual(refresh_job.flow_run.id, 'e0c3067f-2333-4eee-8028-e0a56ca496f6') + self.assertEqual(refresh_job.flow_run.flow_id, '92967d2d-c7e2-46d0-8847-4802df58f484') + self.assertEqual(format_datetime(refresh_job.flow_run.started_at), '2018-05-22T13:00:29Z') diff --git a/test/test_flowruns.py b/test/test_flowruns.py index 475a41cc8..c85c545dd 100644 --- a/test/test_flowruns.py +++ b/test/test_flowruns.py @@ -21,13 +21,13 @@ def setUp(self): self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM' self.server.version = "3.10" - self.baseurl = self.server.flowruns.baseurl + self.baseurl = self.server.flow_runs.baseurl def test_get(self): response_xml = read_xml_asset(GET_XML) with requests_mock.mock() as m: m.get(self.baseurl, text=response_xml) - all_flowruns, pagination_item = self.server.flowruns.get() + all_flowruns, pagination_item = self.server.flow_runs.get() self.assertEqual(2, pagination_item.total_available) self.assertEqual('cc2e652d-4a9b-4476-8c93-b238c45db968', all_flowruns[0].id) @@ -48,7 +48,7 @@ def test_get_by_id(self): response_xml = read_xml_asset(GET_BY_ID_XML) with requests_mock.mock() as m: m.get(self.baseurl + "/cc2e652d-4a9b-4476-8c93-b238c45db968", text=response_xml) - flowrun = self.server.flowruns.get_by_id("cc2e652d-4a9b-4476-8c93-b238c45db968") + flowrun = self.server.flow_runs.get_by_id("cc2e652d-4a9b-4476-8c93-b238c45db968") self.assertEqual('cc2e652d-4a9b-4476-8c93-b238c45db968', flowrun.id) self.assertEqual('2021-02-11T01:42:55Z', format_datetime(flowrun.started_at)) @@ -60,11 +60,11 @@ def test_get_by_id(self): def test_cancel_id(self): with requests_mock.mock() as m: m.put(self.baseurl + '/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', status_code=204) - self.server.flowruns.cancel('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760') + self.server.flow_runs.cancel('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760') def test_cancel_item(self): run = TSC.FlowRunItem() run._id = 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760' with requests_mock.mock() as m: m.put(self.baseurl + '/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', status_code=204) - self.server.flowruns.cancel(run) + self.server.flow_runs.cancel(run) From 54a4ac1a687621b8b20c02efefa8454a2f3a856c Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Fri, 17 Sep 2021 13:21:27 -0500 Subject: [PATCH 07/14] Apply name change consistently --- tableauserverclient/models/__init__.py | 2 +- .../models/{flowrun_item.py => flow_run_item.py} | 0 tableauserverclient/models/job_item.py | 2 +- tableauserverclient/server/endpoint/__init__.py | 2 +- .../endpoint/{flowruns_endpoint.py => flow_runs_endpoint.py} | 1 - 5 files changed, 3 insertions(+), 4 deletions(-) rename tableauserverclient/models/{flowrun_item.py => flow_run_item.py} (100%) rename tableauserverclient/server/endpoint/{flowruns_endpoint.py => flow_runs_endpoint.py} (97%) diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py index 7aa72a922..e5945782d 100644 --- a/tableauserverclient/models/__init__.py +++ b/tableauserverclient/models/__init__.py @@ -10,7 +10,7 @@ from .favorites_item import FavoriteItem from .group_item import GroupItem from .flow_item import FlowItem -from .flowrun_item import FlowRunItem +from .flow_run_item import FlowRunItem from .interval_item import ( IntervalItem, DailyInterval, diff --git a/tableauserverclient/models/flowrun_item.py b/tableauserverclient/models/flow_run_item.py similarity index 100% rename from tableauserverclient/models/flowrun_item.py rename to tableauserverclient/models/flow_run_item.py diff --git a/tableauserverclient/models/job_item.py b/tableauserverclient/models/job_item.py index 5835e9629..8c21b24e6 100644 --- a/tableauserverclient/models/job_item.py +++ b/tableauserverclient/models/job_item.py @@ -1,5 +1,5 @@ import xml.etree.ElementTree as ET -from .flowrun_item import FlowRunItem +from .flow_run_item import FlowRunItem from ..datetime_helpers import parse_datetime diff --git a/tableauserverclient/server/endpoint/__init__.py b/tableauserverclient/server/endpoint/__init__.py index 133eaadcb..291cd7bbb 100644 --- a/tableauserverclient/server/endpoint/__init__.py +++ b/tableauserverclient/server/endpoint/__init__.py @@ -7,7 +7,7 @@ from .favorites_endpoint import Favorites from .fileuploads_endpoint import Fileuploads from .flows_endpoint import Flows -from .flowruns_endpoint import FlowRuns +from .flow_runs_endpoint import FlowRuns from .exceptions import ( ServerResponseError, MissingRequiredFieldError, diff --git a/tableauserverclient/server/endpoint/flowruns_endpoint.py b/tableauserverclient/server/endpoint/flow_runs_endpoint.py similarity index 97% rename from tableauserverclient/server/endpoint/flowruns_endpoint.py rename to tableauserverclient/server/endpoint/flow_runs_endpoint.py index 54f94b941..1b1414e51 100644 --- a/tableauserverclient/server/endpoint/flowruns_endpoint.py +++ b/tableauserverclient/server/endpoint/flow_runs_endpoint.py @@ -1,4 +1,3 @@ -from tableauserverclient.models import flowrun_item from .endpoint import Endpoint, QuerysetEndpoint, api from .. import FlowRunItem, PaginationItem From 62804298cbc2eacae499517c16ca5c5e031509af Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Thu, 30 Sep 2021 19:57:10 -0500 Subject: [PATCH 08/14] Change flowrun_id into flow_run_id --- .../server/endpoint/flow_runs_endpoint.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tableauserverclient/server/endpoint/flow_runs_endpoint.py b/tableauserverclient/server/endpoint/flow_runs_endpoint.py index 1b1414e51..baf497ebf 100644 --- a/tableauserverclient/server/endpoint/flow_runs_endpoint.py +++ b/tableauserverclient/server/endpoint/flow_runs_endpoint.py @@ -21,28 +21,28 @@ def get(self, req_options=None): url = self.baseurl server_response = self.get_request(url, req_options) pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) - all_flow_items = FlowRunItem.from_response(server_response.content, self.parent_srv.namespace) - return all_flow_items, pagination_item + all_flow_run_items = FlowRunItem.from_response(server_response.content, self.parent_srv.namespace) + return all_flow_run_items, pagination_item # Get 1 flow by id @api(version="3.10") - def get_by_id(self, flowrun_id): - if not flowrun_id: + def get_by_id(self, flow_run_id): + if not flow_run_id: error = "Flow ID undefined." raise ValueError(error) - logger.info("Querying single flow (ID: {0})".format(flowrun_id)) - url = "{0}/{1}".format(self.baseurl, flowrun_id) + logger.info("Querying single flow (ID: {0})".format(flow_run_id)) + url = "{0}/{1}".format(self.baseurl, flow_run_id) server_response = self.get_request(url) return FlowRunItem.from_response(server_response.content, self.parent_srv.namespace)[0] # Cancel 1 flow run by id @api(version="3.10") - def cancel(self, flowrun_id): - if not flowrun_id: + def cancel(self, flow_run_id): + if not flow_run_id: error = "Flow ID undefined." raise ValueError(error) - id_ = getattr(flowrun_id, 'id', flowrun_id) + id_ = getattr(flow_run_id, 'id', flow_run_id) url = "{0}/{1}".format(self.baseurl, id_) self.put_request(url) logger.info("Deleted single flow (ID: {0})".format(id_)) From 83ef17bc124f631b95f1d485e4b082f2aa914164 Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Thu, 30 Sep 2021 20:14:04 -0500 Subject: [PATCH 09/14] Add wait_for_job to FlowRun --- .../server/endpoint/exceptions.py | 11 +++++ .../server/endpoint/flow_runs_endpoint.py | 40 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/tableauserverclient/server/endpoint/exceptions.py b/tableauserverclient/server/endpoint/exceptions.py index 693817ddc..5972f52b5 100644 --- a/tableauserverclient/server/endpoint/exceptions.py +++ b/tableauserverclient/server/endpoint/exceptions.py @@ -77,3 +77,14 @@ def __str__(self): class JobCanceledException(JobFailedException): pass +class FlowRunFailedException(Exception): + def __init__(self, flow_run): + self.background_job_id = flow_run.background_job_id + self.flow_run = flow_run + + def __str__(self): + return f"FlowRun {self.flow_run.id} failed with job id {self.background_job_id}" + + +class FlowRunCanceledException(FlowRunFailedException): + pass diff --git a/tableauserverclient/server/endpoint/flow_runs_endpoint.py b/tableauserverclient/server/endpoint/flow_runs_endpoint.py index baf497ebf..6912a0b5c 100644 --- a/tableauserverclient/server/endpoint/flow_runs_endpoint.py +++ b/tableauserverclient/server/endpoint/flow_runs_endpoint.py @@ -1,10 +1,16 @@ from .endpoint import Endpoint, QuerysetEndpoint, api from .. import FlowRunItem, PaginationItem +from .exceptions import FlowRunFailedException, FlowRunCanceledException +import time import logging logger = logging.getLogger("tableau.endpoint.flowruns") +# Polling for job completion uses exponential backoff for the sleep intervals between polls +ASYNC_JOB_POLL_MIN_INTERVAL=0.5 +ASYNC_JOB_POLL_MAX_INTERVAL=30 +ASYNC_JOB_POLL_BACKOFF_FACTOR=1.4 class FlowRuns(QuerysetEndpoint): def __init__(self, parent_srv): @@ -46,3 +52,37 @@ def cancel(self, flow_run_id): url = "{0}/{1}".format(self.baseurl, id_) self.put_request(url) logger.info("Deleted single flow (ID: {0})".format(id_)) + + + @api(version="3.10") + def wait_for_job(self, flow_run_id, *, timeout=None): + id_ = getattr(flow_run_id, "id", flow_run_id) + wait_start_time = time.time() + logger.debug(f"Waiting for job {id_}") + + current_sleep_interval = ASYNC_JOB_POLL_MIN_INTERVAL + flow_run = self.get_by_id(id_) + while flow_run.completed_at is None: + max_sleep_time = ASYNC_JOB_POLL_MAX_INTERVAL + + if timeout is not None: + elapsed = (time.time() - wait_start_time) + if elapsed >= timeout: + raise TimeoutError(f"Timeout after {elapsed} seconds waiting for asynchronous flow run: {id_}") + max_sleep_time = max(ASYNC_JOB_POLL_MIN_INTERVAL, timeout - elapsed) + + time.sleep(min(current_sleep_interval, max_sleep_time)) + job = self.get_by_id(id_) + current_sleep_interval *= ASYNC_JOB_POLL_BACKOFF_FACTOR + logger.debug(f"\tFlowRun {id_} progress={flow_run.progress}") + + logger.info("FlowRun {} Completed: Status: {}".format(id_, flow_run.status)) + + if flow_run.status == "Success": + return flow_run + elif flow_run.status == "Failed": + raise FlowRunFailedException(flow_run) + elif flow_run.finish_code in ["Canceled", "Cancelled"]: + raise FlowRunCanceledException(flow_run) + else: + raise AssertionError("Unexpected status in flow_run", flow_run) \ No newline at end of file From e9b970322d489101583f722184dbb1eff788befa Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Wed, 6 Oct 2021 20:15:53 -0500 Subject: [PATCH 10/14] Tag wait_for_job with version number --- tableauserverclient/server/endpoint/jobs_endpoint.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tableauserverclient/server/endpoint/jobs_endpoint.py b/tableauserverclient/server/endpoint/jobs_endpoint.py index 906d4a19e..d51cca68c 100644 --- a/tableauserverclient/server/endpoint/jobs_endpoint.py +++ b/tableauserverclient/server/endpoint/jobs_endpoint.py @@ -50,6 +50,7 @@ def get_by_id(self, job_id): new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] return new_job + @api(version="2.6") def wait_for_job(self, job_id, *, timeout=None): if isinstance(job_id, JobItem): job_id = job_id.id From 6627762897a406cef47a8697836f73e5591be72c Mon Sep 17 00:00:00 2001 From: Jordan Woods Date: Wed, 6 Oct 2021 20:26:40 -0500 Subject: [PATCH 11/14] Rewrite flow_run to use ExponentialBackoffTimer --- .../server/endpoint/flow_runs_endpoint.py | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/tableauserverclient/server/endpoint/flow_runs_endpoint.py b/tableauserverclient/server/endpoint/flow_runs_endpoint.py index 6912a0b5c..7cc22bf0c 100644 --- a/tableauserverclient/server/endpoint/flow_runs_endpoint.py +++ b/tableauserverclient/server/endpoint/flow_runs_endpoint.py @@ -1,16 +1,12 @@ from .endpoint import Endpoint, QuerysetEndpoint, api -from .. import FlowRunItem, PaginationItem from .exceptions import FlowRunFailedException, FlowRunCanceledException -import time +from .. import FlowRunItem, PaginationItem +from ...exponential_backoff import ExponentialBackoffTimer import logging logger = logging.getLogger("tableau.endpoint.flowruns") -# Polling for job completion uses exponential backoff for the sleep intervals between polls -ASYNC_JOB_POLL_MIN_INTERVAL=0.5 -ASYNC_JOB_POLL_MAX_INTERVAL=30 -ASYNC_JOB_POLL_BACKOFF_FACTOR=1.4 class FlowRuns(QuerysetEndpoint): def __init__(self, parent_srv): @@ -56,25 +52,17 @@ def cancel(self, flow_run_id): @api(version="3.10") def wait_for_job(self, flow_run_id, *, timeout=None): - id_ = getattr(flow_run_id, "id", flow_run_id) - wait_start_time = time.time() - logger.debug(f"Waiting for job {id_}") + if isinstance(flow_run_id, FlowRunItem): + flow_run_id = flow_run_id.id + assert isinstance(flow_run_id, str) + logger.debug(f"Waiting for flow run {flow_run_id}") - current_sleep_interval = ASYNC_JOB_POLL_MIN_INTERVAL - flow_run = self.get_by_id(id_) + backoffTimer = ExponentialBackoffTimer(timeout=timeout) + flow_run = self.get_by_id(flow_run_id) while flow_run.completed_at is None: - max_sleep_time = ASYNC_JOB_POLL_MAX_INTERVAL - - if timeout is not None: - elapsed = (time.time() - wait_start_time) - if elapsed >= timeout: - raise TimeoutError(f"Timeout after {elapsed} seconds waiting for asynchronous flow run: {id_}") - max_sleep_time = max(ASYNC_JOB_POLL_MIN_INTERVAL, timeout - elapsed) - - time.sleep(min(current_sleep_interval, max_sleep_time)) - job = self.get_by_id(id_) - current_sleep_interval *= ASYNC_JOB_POLL_BACKOFF_FACTOR - logger.debug(f"\tFlowRun {id_} progress={flow_run.progress}") + backoffTimer.sleep() + flow_run = self.get_by_id(flow_run_id) + logger.debug(f"\tFlowRun {flow_run_id} progress={flow_run.progress}") logger.info("FlowRun {} Completed: Status: {}".format(id_, flow_run.status)) From a57cb140b26c702a0dedae0b8752e68993c9a991 Mon Sep 17 00:00:00 2001 From: jorwoods Date: Thu, 7 Oct 2021 08:19:25 -0500 Subject: [PATCH 12/14] Test flow run wait with backoff --- .../server/endpoint/flow_runs_endpoint.py | 4 +- test/_utils.py | 5 +- .../{flowruns_get.xml => flow_runs_get.xml} | 0 ..._get_by_id.xml => flow_runs_get_by_id.xml} | 0 test/assets/flow_runs_get_by_id_failed.xml | 10 +++ .../assets/flow_runs_get_by_id_inprogress.xml | 10 +++ test/test_flowruns.py | 84 +++++++++++++------ 7 files changed, 85 insertions(+), 28 deletions(-) rename test/assets/{flowruns_get.xml => flow_runs_get.xml} (100%) rename test/assets/{flowruns_get_by_id.xml => flow_runs_get_by_id.xml} (100%) create mode 100644 test/assets/flow_runs_get_by_id_failed.xml create mode 100644 test/assets/flow_runs_get_by_id_inprogress.xml diff --git a/tableauserverclient/server/endpoint/flow_runs_endpoint.py b/tableauserverclient/server/endpoint/flow_runs_endpoint.py index 7cc22bf0c..dcc3ae70d 100644 --- a/tableauserverclient/server/endpoint/flow_runs_endpoint.py +++ b/tableauserverclient/server/endpoint/flow_runs_endpoint.py @@ -64,13 +64,13 @@ def wait_for_job(self, flow_run_id, *, timeout=None): flow_run = self.get_by_id(flow_run_id) logger.debug(f"\tFlowRun {flow_run_id} progress={flow_run.progress}") - logger.info("FlowRun {} Completed: Status: {}".format(id_, flow_run.status)) + logger.info("FlowRun {} Completed: Status: {}".format(flow_run_id, flow_run.status)) if flow_run.status == "Success": return flow_run elif flow_run.status == "Failed": raise FlowRunFailedException(flow_run) - elif flow_run.finish_code in ["Canceled", "Cancelled"]: + elif flow_run.status in ["Canceled", "Cancelled"]: raise FlowRunCanceledException(flow_run) else: raise AssertionError("Unexpected status in flow_run", flow_run) \ No newline at end of file diff --git a/test/_utils.py b/test/_utils.py index 93d7a9334..626838f23 100644 --- a/test/_utils.py +++ b/test/_utils.py @@ -29,6 +29,9 @@ def sleep_mock(interval): def get_time(): return mock_time - patch = unittest.mock.patch + try: + patch = unittest.mock.patch + except AttributeError: + from unittest.mock import patch with patch("time.sleep", sleep_mock), patch("time.time", get_time): yield get_time diff --git a/test/assets/flowruns_get.xml b/test/assets/flow_runs_get.xml similarity index 100% rename from test/assets/flowruns_get.xml rename to test/assets/flow_runs_get.xml diff --git a/test/assets/flowruns_get_by_id.xml b/test/assets/flow_runs_get_by_id.xml similarity index 100% rename from test/assets/flowruns_get_by_id.xml rename to test/assets/flow_runs_get_by_id.xml diff --git a/test/assets/flow_runs_get_by_id_failed.xml b/test/assets/flow_runs_get_by_id_failed.xml new file mode 100644 index 000000000..9e766680b --- /dev/null +++ b/test/assets/flow_runs_get_by_id_failed.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/test/assets/flow_runs_get_by_id_inprogress.xml b/test/assets/flow_runs_get_by_id_inprogress.xml new file mode 100644 index 000000000..42e1a77f9 --- /dev/null +++ b/test/assets/flow_runs_get_by_id_inprogress.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/test/test_flowruns.py b/test/test_flowruns.py index c85c545dd..d2e72f31a 100644 --- a/test/test_flowruns.py +++ b/test/test_flowruns.py @@ -4,12 +4,14 @@ import xml.etree.ElementTree as ET import tableauserverclient as TSC from tableauserverclient.datetime_helpers import format_datetime -from tableauserverclient.server.endpoint.exceptions import InternalServerError +from tableauserverclient.server.endpoint.exceptions import FlowRunFailedException from tableauserverclient.server.request_factory import RequestFactory -from ._utils import read_xml_asset, read_xml_assets, asset +from ._utils import read_xml_asset, mocked_time -GET_XML = 'flowruns_get.xml' -GET_BY_ID_XML = 'flowruns_get_by_id.xml' +GET_XML = 'flow_runs_get.xml' +GET_BY_ID_XML = 'flow_runs_get_by_id.xml' +GET_BY_ID_FAILED_XML = 'flow_runs_get_by_id_failed.xml' +GET_BY_ID_INPROGRESS_XML = 'flow_runs_get_by_id_inprogress.xml' class FlowRunTests(unittest.TestCase): @@ -27,35 +29,35 @@ def test_get(self): response_xml = read_xml_asset(GET_XML) with requests_mock.mock() as m: m.get(self.baseurl, text=response_xml) - all_flowruns, pagination_item = self.server.flow_runs.get() + all_flow_runs, pagination_item = self.server.flow_runs.get() self.assertEqual(2, pagination_item.total_available) - self.assertEqual('cc2e652d-4a9b-4476-8c93-b238c45db968', all_flowruns[0].id) - self.assertEqual('2021-02-11T01:42:55Z', format_datetime(all_flowruns[0].started_at)) - self.assertEqual('2021-02-11T01:57:38Z', format_datetime(all_flowruns[0].completed_at)) - self.assertEqual('Success', all_flowruns[0].status) - self.assertEqual('100', all_flowruns[0].progress) - self.assertEqual('aa23f4ac-906f-11e9-86fb-3f0f71412e77', all_flowruns[0].background_job_id) - - self.assertEqual('a3104526-c0c6-4ea5-8362-e03fc7cbd7ee', all_flowruns[1].id) - self.assertEqual('2021-02-13T04:05:30Z', format_datetime(all_flowruns[1].started_at)) - self.assertEqual('2021-02-13T04:05:35Z', format_datetime(all_flowruns[1].completed_at)) - self.assertEqual('Failed', all_flowruns[1].status) - self.assertEqual('100', all_flowruns[1].progress) - self.assertEqual('1ad21a9d-2530-4fbf-9064-efd3c736e023', all_flowruns[1].background_job_id) + self.assertEqual('cc2e652d-4a9b-4476-8c93-b238c45db968', all_flow_runs[0].id) + self.assertEqual('2021-02-11T01:42:55Z', format_datetime(all_flow_runs[0].started_at)) + self.assertEqual('2021-02-11T01:57:38Z', format_datetime(all_flow_runs[0].completed_at)) + self.assertEqual('Success', all_flow_runs[0].status) + self.assertEqual('100', all_flow_runs[0].progress) + self.assertEqual('aa23f4ac-906f-11e9-86fb-3f0f71412e77', all_flow_runs[0].background_job_id) + + self.assertEqual('a3104526-c0c6-4ea5-8362-e03fc7cbd7ee', all_flow_runs[1].id) + self.assertEqual('2021-02-13T04:05:30Z', format_datetime(all_flow_runs[1].started_at)) + self.assertEqual('2021-02-13T04:05:35Z', format_datetime(all_flow_runs[1].completed_at)) + self.assertEqual('Failed', all_flow_runs[1].status) + self.assertEqual('100', all_flow_runs[1].progress) + self.assertEqual('1ad21a9d-2530-4fbf-9064-efd3c736e023', all_flow_runs[1].background_job_id) def test_get_by_id(self): response_xml = read_xml_asset(GET_BY_ID_XML) with requests_mock.mock() as m: m.get(self.baseurl + "/cc2e652d-4a9b-4476-8c93-b238c45db968", text=response_xml) - flowrun = self.server.flow_runs.get_by_id("cc2e652d-4a9b-4476-8c93-b238c45db968") + flow_run = self.server.flow_runs.get_by_id("cc2e652d-4a9b-4476-8c93-b238c45db968") - self.assertEqual('cc2e652d-4a9b-4476-8c93-b238c45db968', flowrun.id) - self.assertEqual('2021-02-11T01:42:55Z', format_datetime(flowrun.started_at)) - self.assertEqual('2021-02-11T01:57:38Z', format_datetime(flowrun.completed_at)) - self.assertEqual('Success', flowrun.status) - self.assertEqual('100', flowrun.progress) - self.assertEqual('1ad21a9d-2530-4fbf-9064-efd3c736e023', flowrun.background_job_id) + self.assertEqual('cc2e652d-4a9b-4476-8c93-b238c45db968', flow_run.id) + self.assertEqual('2021-02-11T01:42:55Z', format_datetime(flow_run.started_at)) + self.assertEqual('2021-02-11T01:57:38Z', format_datetime(flow_run.completed_at)) + self.assertEqual('Success', flow_run.status) + self.assertEqual('100', flow_run.progress) + self.assertEqual('1ad21a9d-2530-4fbf-9064-efd3c736e023', flow_run.background_job_id) def test_cancel_id(self): with requests_mock.mock() as m: @@ -68,3 +70,35 @@ def test_cancel_item(self): with requests_mock.mock() as m: m.put(self.baseurl + '/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', status_code=204) self.server.flow_runs.cancel(run) + + + def test_wait_for_job_finished(self): + # Waiting for an already finished job, directly returns that job's info + response_xml = read_xml_asset(GET_BY_ID_XML) + flow_run_id = 'cc2e652d-4a9b-4476-8c93-b238c45db968' + with mocked_time(), requests_mock.mock() as m: + m.get('{0}/{1}'.format(self.baseurl, flow_run_id), text=response_xml) + flow_run = self.server.flow_runs.wait_for_job(flow_run_id) + + self.assertEqual(flow_run_id, flow_run.id) + self.assertEqual(flow_run.progress, "100") + + + def test_wait_for_job_failed(self): + # Waiting for a failed job raises an exception + response_xml = read_xml_asset(GET_BY_ID_FAILED_XML) + flow_run_id = 'c2b35d5a-e130-471a-aec8-7bc5435fe0e7' + with mocked_time(), requests_mock.mock() as m: + m.get('{0}/{1}'.format(self.baseurl, flow_run_id), text=response_xml) + with self.assertRaises(FlowRunFailedException): + self.server.flow_runs.wait_for_job(flow_run_id) + + + def test_wait_for_job_timeout(self): + # Waiting for a job which doesn't terminate will throw an exception + response_xml = read_xml_asset(GET_BY_ID_INPROGRESS_XML) + flow_run_id = '71afc22c-9c06-40be-8d0f-4c4166d29e6c' + with mocked_time(), requests_mock.mock() as m: + m.get('{0}/{1}'.format(self.baseurl, flow_run_id), text=response_xml) + with self.assertRaises(TimeoutError): + self.server.flow_runs.wait_for_job(flow_run_id, timeout=30) From 1e9c86364831720adff519f8cc15b741dd1c3adf Mon Sep 17 00:00:00 2001 From: jorwoods Date: Thu, 7 Oct 2021 08:23:35 -0500 Subject: [PATCH 13/14] Remove 3.5 from test matrix --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 9a51ac7a9..61476132f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -8,7 +8,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.5, 3.6, 3.7, 3.8, 3.9, 3.10.0-rc.2] + python-version: [3.6, 3.7, 3.8, 3.9, 3.10.0-rc.2] runs-on: ${{ matrix.os }} From 28ae64dc67500f06bc5350115d622e369461bfe1 Mon Sep 17 00:00:00 2001 From: jorwoods Date: Thu, 14 Oct 2021 17:20:52 -0500 Subject: [PATCH 14/14] Standardize spelling of cancelled --- tableauserverclient/server/endpoint/exceptions.py | 4 ++-- tableauserverclient/server/endpoint/flow_runs_endpoint.py | 6 +++--- tableauserverclient/server/endpoint/jobs_endpoint.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tableauserverclient/server/endpoint/exceptions.py b/tableauserverclient/server/endpoint/exceptions.py index 5972f52b5..48dcaf4c8 100644 --- a/tableauserverclient/server/endpoint/exceptions.py +++ b/tableauserverclient/server/endpoint/exceptions.py @@ -75,7 +75,7 @@ def __str__(self): return f"Job {self.job.id} failed with notes {self.notes}" -class JobCanceledException(JobFailedException): +class JobCancelledException(JobFailedException): pass class FlowRunFailedException(Exception): def __init__(self, flow_run): @@ -86,5 +86,5 @@ def __str__(self): return f"FlowRun {self.flow_run.id} failed with job id {self.background_job_id}" -class FlowRunCanceledException(FlowRunFailedException): +class FlowRunCancelledException(FlowRunFailedException): pass diff --git a/tableauserverclient/server/endpoint/flow_runs_endpoint.py b/tableauserverclient/server/endpoint/flow_runs_endpoint.py index dcc3ae70d..2ae1973d4 100644 --- a/tableauserverclient/server/endpoint/flow_runs_endpoint.py +++ b/tableauserverclient/server/endpoint/flow_runs_endpoint.py @@ -1,5 +1,5 @@ from .endpoint import Endpoint, QuerysetEndpoint, api -from .exceptions import FlowRunFailedException, FlowRunCanceledException +from .exceptions import FlowRunFailedException, FlowRunCancelledException from .. import FlowRunItem, PaginationItem from ...exponential_backoff import ExponentialBackoffTimer @@ -70,7 +70,7 @@ def wait_for_job(self, flow_run_id, *, timeout=None): return flow_run elif flow_run.status == "Failed": raise FlowRunFailedException(flow_run) - elif flow_run.status in ["Canceled", "Cancelled"]: - raise FlowRunCanceledException(flow_run) + elif flow_run.status == "Cancelled": + raise FlowRunCancelledException(flow_run) else: raise AssertionError("Unexpected status in flow_run", flow_run) \ No newline at end of file diff --git a/tableauserverclient/server/endpoint/jobs_endpoint.py b/tableauserverclient/server/endpoint/jobs_endpoint.py index d51cca68c..8d321fd2d 100644 --- a/tableauserverclient/server/endpoint/jobs_endpoint.py +++ b/tableauserverclient/server/endpoint/jobs_endpoint.py @@ -1,5 +1,5 @@ from .endpoint import Endpoint, api -from .exceptions import JobCanceledException, JobFailedException +from .exceptions import JobCancelledException, JobFailedException from .. import JobItem, BackgroundJobItem, PaginationItem from ..request_options import RequestOptionsBase from ...exponential_backoff import ExponentialBackoffTimer @@ -71,6 +71,6 @@ def wait_for_job(self, job_id, *, timeout=None): elif job.finish_code == JobItem.FinishCode.Failed: raise JobFailedException(job) elif job.finish_code == JobItem.FinishCode.Cancelled: - raise JobCanceledException(job) + raise JobCancelledException(job) else: raise AssertionError("Unexpected finish_code in job", job)