From d59946b55a34e59270c1fed9cc3957518466e8e5 Mon Sep 17 00:00:00 2001 From: Lee Graber Date: Thu, 27 Oct 2016 07:56:46 -0700 Subject: [PATCH 1/2] First step to adding tasks --- tableauserverclient/__init__.py | 2 +- tableauserverclient/models/__init__.py | 1 + tableauserverclient/models/exceptions.py | 3 + tableauserverclient/models/item_types.py | 3 + tableauserverclient/models/schedule_item.py | 9 ++ tableauserverclient/models/task_item.py | 91 +++++++++++++++++++ tableauserverclient/server/__init__.py | 2 +- .../server/endpoint/schedules_endpoint.py | 12 ++- 8 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 tableauserverclient/models/item_types.py create mode 100644 tableauserverclient/models/task_item.py diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py index 9e56919c6..a9b1644e7 100644 --- a/tableauserverclient/__init__.py +++ b/tableauserverclient/__init__.py @@ -1,7 +1,7 @@ from .namespace import NAMESPACE from .models import ConnectionItem, DatasourceItem,\ GroupItem, PaginationItem, ProjectItem, ScheduleItem, \ - SiteItem, TableauAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \ + SiteItem, TableauAuth, TaskItem, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \ HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem from .server import RequestOptions, Filter, Sort, Server, ServerResponseError,\ MissingRequiredFieldError, NotSignedInError diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py index 276684d66..9e1b60287 100644 --- a/tableauserverclient/models/__init__.py +++ b/tableauserverclient/models/__init__.py @@ -8,6 +8,7 @@ from .schedule_item import ScheduleItem from .site_item import SiteItem from .tableau_auth import TableauAuth +from .task_item import TaskItem from .user_item import UserItem from .view_item import ViewItem from .workbook_item import WorkbookItem diff --git a/tableauserverclient/models/exceptions.py b/tableauserverclient/models/exceptions.py index 28d738e73..0a5b1a6e1 100644 --- a/tableauserverclient/models/exceptions.py +++ b/tableauserverclient/models/exceptions.py @@ -1,2 +1,5 @@ class UnpopulatedPropertyError(Exception): pass + +class ResponseBodyError(Exception): + passs \ No newline at end of file diff --git a/tableauserverclient/models/item_types.py b/tableauserverclient/models/item_types.py new file mode 100644 index 000000000..9a4562f3d --- /dev/null +++ b/tableauserverclient/models/item_types.py @@ -0,0 +1,3 @@ +class ItemTypes: + Datasource = 'Datasource' + Workbook = 'Workbook' \ No newline at end of file diff --git a/tableauserverclient/models/schedule_item.py b/tableauserverclient/models/schedule_item.py index b0f7d1edb..a47a42465 100644 --- a/tableauserverclient/models/schedule_item.py +++ b/tableauserverclient/models/schedule_item.py @@ -3,6 +3,7 @@ from .interval_item import IntervalItem, HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval from .property_decorators import property_is_enum, property_not_nullable, property_is_int +from .exceptions import UnpopulatedPropertyError from .. import NAMESPACE @@ -31,6 +32,7 @@ def __init__(self, name, priority, schedule_type, execution_order, interval_item self.name = name self.priority = priority self.schedule_type = schedule_type + self._tasks = None @property def created_at(self): @@ -98,6 +100,13 @@ def state(self, value): def updated_at(self): return self._updated_at + @property + def tasks(self): + if self._tasks is None: + error = "Schedule item must be populated with its tasks first." + raise UnpopulatedPropertyError(error) + return self._tasks + def _parse_common_tags(self, schedule_xml): if not isinstance(schedule_xml, ET.Element): schedule_xml = ET.fromstring(schedule_xml).find('.//t:schedule', namespaces=NAMESPACE) diff --git a/tableauserverclient/models/task_item.py b/tableauserverclient/models/task_item.py new file mode 100644 index 000000000..ff19fef4f --- /dev/null +++ b/tableauserverclient/models/task_item.py @@ -0,0 +1,91 @@ +import xml.etree.ElementTree as ET +from .. import NAMESPACE +from item_types import ItemTypes +from exceptions import ResponseBodyError +from property_decorators import property_is_int, property_is_enum + +class TaskItem(object): + + class RefreshType: + Full = "Full" + Incremental = "Incremental" + + def __init__(self, id): + self._id = id + + @property + def priority(self): + return self._priority + + @priority.setter + @property_is_int(range=(1, 100)) + def priority(self, value): + self._priority = value + + @property + def task_type(self): + return self._task_type + + @task_type.setter + @property_is_enum(RefreshType) + def task_type(self, value): + self._task_type = value + + @property + def item_type(self): + return self._item_type + + @item_type.setter + def item_type(self, value): + # Check that it is Datasource or Workbook + if not (value in [ItemTypes.Datasource, ItemTypes.Workbook]): + error = "Invalid value: {0}. item_type must be of either ItemTypes.Datasource or ItemTypes.Workbook".format(value) + raise ValueError(error) + self._item_type = value + + @property + def item_id(self): + return self._item_id + + @item_id.setter + def item_id(self, value): + self._item_id = value + + def _set_values(self, priority, type, item_type, item_id): + self.priority = priority + self.type = type + self.item_type = item_type + self.item_id = item_id + + @classmethod + def from_response(cls, resp): + tasks_items = list() + parsed_response = ET.fromstring(resp) + extract_tags = parsed_response.findall('.//t:extract', namespaces=NAMESPACE) + for extract_tag in extract_tags: + (id, priority, type, item_type, item_id) = cls._parse_element(extract_tag) + + task = cls(id) + task._set_values(priority, type, item_type, item_id) + tasks_items.append(task) + return tasks_items + + @staticmethod + def _parse_element(extract_tag): + id = extract_tag.get('id') + priority = extract_tag.get('priority') + type = extract_tag.get('type') + + datasource_tag = extract_tag.find('.//t:datasource', namespaces=NAMESPACE) + workbook_tag = extract_tag.find('.//t:workbook', namespaces=NAMESPACE) + if datasource_tag is not None: + item_type = ItemTypes.Datasource + item_id = datasource_tag.get('id') + elif workbook_tag is not None: + item_type = ItemTypes.Workbook + item_id = workbook_tag.get('id') + else: + error = "Missing workbook / datasource element for refresh task" + raise ResponseBodyError(error) + + return id, priority, type, item_type, item_id \ No newline at end of file diff --git a/tableauserverclient/server/__init__.py b/tableauserverclient/server/__init__.py index a8b78b7fc..2f3e93372 100644 --- a/tableauserverclient/server/__init__.py +++ b/tableauserverclient/server/__init__.py @@ -4,7 +4,7 @@ from .sort import Sort from .. import ConnectionItem, DatasourceItem,\ GroupItem, PaginationItem, ProjectItem, ScheduleItem, SiteItem, TableauAuth,\ - UserItem, ViewItem, WorkbookItem, NAMESPACE + TaskItem, UserItem, ViewItem, WorkbookItem, NAMESPACE from .endpoint import Auth, Datasources, Endpoint, Groups, Projects, Schedules, \ Sites, Users, Views, Workbooks, ServerResponseError, MissingRequiredFieldError from .server import Server diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index 9b4721941..59533ad09 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -1,6 +1,6 @@ from .endpoint import Endpoint from .exceptions import MissingRequiredFieldError -from .. import RequestFactory, PaginationItem, ScheduleItem +from .. import RequestFactory, PaginationItem, ScheduleItem, TaskItem import logging import copy @@ -47,6 +47,16 @@ def update(self, schedule_item): updated_schedule = copy.copy(schedule_item) return updated_schedule._parse_common_tags(server_response.content) + def populate_tasks(self, schedule_item, req_options): + if not schedule_item.id: + error = "Schedule item missing ID." + raise MissingRequiredFieldError(error) + url = "{0}/{1}/extracts".format(self.baseurl, schedule_item.id) + server_response = self.get_request(url, req_options) + # Adding to an existing list ... how to handle paging and knowing what paging might already have been done + pagination_item = PaginationItem.from_response(server_response.content) + schedule_item.tasks = TaskItem.from_response(server_response.content) + def create(self, schedule_item): if schedule_item.interval_item is None: error = "Interval item must be defined." From 030c9d760f3a5b475b08a122f0cec095c70a769f Mon Sep 17 00:00:00 2001 From: Lee Graber Date: Thu, 27 Oct 2016 23:53:41 -0700 Subject: [PATCH 2/2] Intermediate checkin (working but still a work in progress) --- tableauserverclient/__init__.py | 4 +- tableauserverclient/models/__init__.py | 2 +- tableauserverclient/models/exceptions.py | 2 +- .../models/extract_refresh_task_item.py | 93 +++++++++++++++++++ tableauserverclient/models/schedule_item.py | 9 -- tableauserverclient/models/task_item.py | 90 ++---------------- tableauserverclient/server/__init__.py | 6 +- .../server/endpoint/__init__.py | 1 + .../endpoint/extract_refreshes_endpoint.py | 21 +++++ .../server/endpoint/schedules_endpoint.py | 12 +-- .../server/endpoint/tasks_endpoint.py | 11 +++ tableauserverclient/server/server.py | 3 +- 12 files changed, 142 insertions(+), 112 deletions(-) create mode 100644 tableauserverclient/models/extract_refresh_task_item.py create mode 100644 tableauserverclient/server/endpoint/extract_refreshes_endpoint.py create mode 100644 tableauserverclient/server/endpoint/tasks_endpoint.py diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py index a9b1644e7..af69ee128 100644 --- a/tableauserverclient/__init__.py +++ b/tableauserverclient/__init__.py @@ -1,7 +1,7 @@ from .namespace import NAMESPACE -from .models import ConnectionItem, DatasourceItem,\ +from .models import ConnectionItem, DatasourceItem, ExtractRefreshTaskItem,\ GroupItem, PaginationItem, ProjectItem, ScheduleItem, \ - SiteItem, TableauAuth, TaskItem, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \ + SiteItem, TableauAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \ HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem from .server import RequestOptions, Filter, Sort, Server, ServerResponseError,\ MissingRequiredFieldError, NotSignedInError diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py index 9e1b60287..596d3464b 100644 --- a/tableauserverclient/models/__init__.py +++ b/tableauserverclient/models/__init__.py @@ -1,6 +1,7 @@ from .connection_item import ConnectionItem from .datasource_item import DatasourceItem from .exceptions import UnpopulatedPropertyError +from .extract_refresh_task_item import ExtractRefreshTaskItem from .group_item import GroupItem from .interval_item import IntervalItem, DailyInterval, WeeklyInterval, MonthlyInterval, HourlyInterval from .pagination_item import PaginationItem @@ -8,7 +9,6 @@ from .schedule_item import ScheduleItem from .site_item import SiteItem from .tableau_auth import TableauAuth -from .task_item import TaskItem from .user_item import UserItem from .view_item import ViewItem from .workbook_item import WorkbookItem diff --git a/tableauserverclient/models/exceptions.py b/tableauserverclient/models/exceptions.py index 0a5b1a6e1..fc5ddd71f 100644 --- a/tableauserverclient/models/exceptions.py +++ b/tableauserverclient/models/exceptions.py @@ -2,4 +2,4 @@ class UnpopulatedPropertyError(Exception): pass class ResponseBodyError(Exception): - passs \ No newline at end of file + pass \ No newline at end of file diff --git a/tableauserverclient/models/extract_refresh_task_item.py b/tableauserverclient/models/extract_refresh_task_item.py new file mode 100644 index 000000000..ae8dc1ac1 --- /dev/null +++ b/tableauserverclient/models/extract_refresh_task_item.py @@ -0,0 +1,93 @@ +import xml.etree.ElementTree as ET +from .. import NAMESPACE +from item_types import ItemTypes +from task_item import TaskItem +from exceptions import ResponseBodyError +from property_decorators import property_is_int, property_is_enum + +class ExtractRefreshTaskItem(TaskItem): + + class RefreshType: + FullRefresh = "FullRefresh" + IncrementalRefresh = "IncrementalRefresh" + + # A user should never create this item. It should only ever be created by a return call from + # a REST API. Possibly we could be more vigilant in hiding the constructor + def __init__(self, id, schedule_id, priority, refresh_type, item_type, item_id): + super(ExtractRefreshTaskItem, self).__init__(id, schedule_id) + self.priority = priority + self.type = type + self.refresh_type = refresh_type + self.item_type = item_type + self.item_id = item_id + + @property + def priority(self): + return self._priority + + @priority.setter + @property_is_int(range=(1, 100)) + def priority(self, value): + self._priority = value + + @property + def refresh_type(self): + return self._refresh_type + + @refresh_type.setter + @property_is_enum(RefreshType) + def refresh_type(self, value): + self._refresh_type = value + + @property + def item_type(self): + return self._item_type + + @item_type.setter + def item_type(self, value): + # Check that it is Datasource or Workbook + if not (value in [ItemTypes.Datasource, ItemTypes.Workbook]): + error = "Invalid value: {0}. item_type must be either ItemTypes.Datasource or ItemTypes.Workbook".format(value) + raise ValueError(error) + self._item_type = value + + @property + def item_id(self): + return self._item_id + + @item_id.setter + def item_id(self, value): + self._item_id = value + + + @classmethod + def from_response(cls, resp, schedule_id): + tasks_items = list() + parsed_response = ET.fromstring(resp) + extract_tags = parsed_response.findall('.//t:extract', namespaces=NAMESPACE) + for extract_tag in extract_tags: + (id, priority, refresh_type, item_type, item_id) = cls._parse_element(extract_tag) + + task = cls(id, schedule_id, priority, refresh_type, item_type, item_id) + tasks_items.append(task) + return tasks_items + + @staticmethod + def _parse_element(extract_tag): + id = extract_tag.get('id') + priority = int(extract_tag.get('priority')) + refresh_type = extract_tag.get('type') + + datasource_tag = extract_tag.find('.//t:datasource', namespaces=NAMESPACE) + workbook_tag = extract_tag.find('.//t:workbook', namespaces=NAMESPACE) + if datasource_tag is not None: + item_type = ItemTypes.Datasource + item_id = datasource_tag.get('id') + elif workbook_tag is not None: + item_type = ItemTypes.Workbook + item_id = workbook_tag.get('id') + else: + error = "Missing workbook / datasource element for refresh task" + raise ResponseBodyError(error) + + return id, priority, refresh_type, item_type, item_id \ No newline at end of file diff --git a/tableauserverclient/models/schedule_item.py b/tableauserverclient/models/schedule_item.py index a47a42465..b0f7d1edb 100644 --- a/tableauserverclient/models/schedule_item.py +++ b/tableauserverclient/models/schedule_item.py @@ -3,7 +3,6 @@ from .interval_item import IntervalItem, HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval from .property_decorators import property_is_enum, property_not_nullable, property_is_int -from .exceptions import UnpopulatedPropertyError from .. import NAMESPACE @@ -32,7 +31,6 @@ def __init__(self, name, priority, schedule_type, execution_order, interval_item self.name = name self.priority = priority self.schedule_type = schedule_type - self._tasks = None @property def created_at(self): @@ -100,13 +98,6 @@ def state(self, value): def updated_at(self): return self._updated_at - @property - def tasks(self): - if self._tasks is None: - error = "Schedule item must be populated with its tasks first." - raise UnpopulatedPropertyError(error) - return self._tasks - def _parse_common_tags(self, schedule_xml): if not isinstance(schedule_xml, ET.Element): schedule_xml = ET.fromstring(schedule_xml).find('.//t:schedule', namespaces=NAMESPACE) diff --git a/tableauserverclient/models/task_item.py b/tableauserverclient/models/task_item.py index ff19fef4f..9e9b9294b 100644 --- a/tableauserverclient/models/task_item.py +++ b/tableauserverclient/models/task_item.py @@ -1,91 +1,13 @@ -import xml.etree.ElementTree as ET -from .. import NAMESPACE -from item_types import ItemTypes -from exceptions import ResponseBodyError -from property_decorators import property_is_int, property_is_enum - class TaskItem(object): - class RefreshType: - Full = "Full" - Incremental = "Incremental" - - def __init__(self, id): + def __init__(self, id, schedule_id): self._id = id + self._schedule_id = schedule_id @property - def priority(self): - return self._priority - - @priority.setter - @property_is_int(range=(1, 100)) - def priority(self, value): - self._priority = value - - @property - def task_type(self): - return self._task_type - - @task_type.setter - @property_is_enum(RefreshType) - def task_type(self, value): - self._task_type = value - - @property - def item_type(self): - return self._item_type - - @item_type.setter - def item_type(self, value): - # Check that it is Datasource or Workbook - if not (value in [ItemTypes.Datasource, ItemTypes.Workbook]): - error = "Invalid value: {0}. item_type must be of either ItemTypes.Datasource or ItemTypes.Workbook".format(value) - raise ValueError(error) - self._item_type = value + def id(self): + return self._id @property - def item_id(self): - return self._item_id - - @item_id.setter - def item_id(self, value): - self._item_id = value - - def _set_values(self, priority, type, item_type, item_id): - self.priority = priority - self.type = type - self.item_type = item_type - self.item_id = item_id - - @classmethod - def from_response(cls, resp): - tasks_items = list() - parsed_response = ET.fromstring(resp) - extract_tags = parsed_response.findall('.//t:extract', namespaces=NAMESPACE) - for extract_tag in extract_tags: - (id, priority, type, item_type, item_id) = cls._parse_element(extract_tag) - - task = cls(id) - task._set_values(priority, type, item_type, item_id) - tasks_items.append(task) - return tasks_items - - @staticmethod - def _parse_element(extract_tag): - id = extract_tag.get('id') - priority = extract_tag.get('priority') - type = extract_tag.get('type') - - datasource_tag = extract_tag.find('.//t:datasource', namespaces=NAMESPACE) - workbook_tag = extract_tag.find('.//t:workbook', namespaces=NAMESPACE) - if datasource_tag is not None: - item_type = ItemTypes.Datasource - item_id = datasource_tag.get('id') - elif workbook_tag is not None: - item_type = ItemTypes.Workbook - item_id = workbook_tag.get('id') - else: - error = "Missing workbook / datasource element for refresh task" - raise ResponseBodyError(error) - - return id, priority, type, item_type, item_id \ No newline at end of file + def schedule_id(self): + return self._schedule_id \ No newline at end of file diff --git a/tableauserverclient/server/__init__.py b/tableauserverclient/server/__init__.py index 2f3e93372..39910ad29 100644 --- a/tableauserverclient/server/__init__.py +++ b/tableauserverclient/server/__init__.py @@ -2,10 +2,10 @@ from .request_options import RequestOptions from .filter import Filter from .sort import Sort -from .. import ConnectionItem, DatasourceItem,\ +from .. import ConnectionItem, DatasourceItem, ExtractRefreshTaskItem,\ GroupItem, PaginationItem, ProjectItem, ScheduleItem, SiteItem, TableauAuth,\ - TaskItem, UserItem, ViewItem, WorkbookItem, NAMESPACE + UserItem, ViewItem, WorkbookItem, NAMESPACE from .endpoint import Auth, Datasources, Endpoint, Groups, Projects, Schedules, \ - Sites, Users, Views, Workbooks, ServerResponseError, MissingRequiredFieldError + Sites, Tasks, Users, Views, Workbooks, ServerResponseError, MissingRequiredFieldError from .server import Server from .exceptions import NotSignedInError diff --git a/tableauserverclient/server/endpoint/__init__.py b/tableauserverclient/server/endpoint/__init__.py index 65e15c683..e61ed6f88 100644 --- a/tableauserverclient/server/endpoint/__init__.py +++ b/tableauserverclient/server/endpoint/__init__.py @@ -6,6 +6,7 @@ from .projects_endpoint import Projects from .schedules_endpoint import Schedules from .sites_endpoint import Sites +from .tasks_endpoint import Tasks from .users_endpoint import Users from .views_endpoint import Views from .workbooks_endpoint import Workbooks diff --git a/tableauserverclient/server/endpoint/extract_refreshes_endpoint.py b/tableauserverclient/server/endpoint/extract_refreshes_endpoint.py new file mode 100644 index 000000000..383528b32 --- /dev/null +++ b/tableauserverclient/server/endpoint/extract_refreshes_endpoint.py @@ -0,0 +1,21 @@ +from .endpoint import Endpoint +from .. import PaginationItem, ExtractRefreshTaskItem +import logging + +logger = logging.getLogger('tableau.endpoint.tasks') + +class ExtractRefreshes(Endpoint): + def __init__(self, parent_srv): + super(ExtractRefreshes, self).__init__() + self.parent_srv = parent_srv + + @property + def baseurl(self): + return "{0}/sites/{1}".format(self.parent_srv.baseurl, self.parent_srv.site_id) + + def get_for_schedule(self, schedule_id, req_options=None): + url = "{0}/schedules/{1}/extracts".format(self.baseurl, schedule_id) + server_response = self.get_request(url, req_options) + pagination_item = PaginationItem.from_response(server_response.content) + tasks = ExtractRefreshTaskItem.from_response(server_response.content, schedule_id) + return tasks, pagination_item \ No newline at end of file diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index 59533ad09..9b4721941 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -1,6 +1,6 @@ from .endpoint import Endpoint from .exceptions import MissingRequiredFieldError -from .. import RequestFactory, PaginationItem, ScheduleItem, TaskItem +from .. import RequestFactory, PaginationItem, ScheduleItem import logging import copy @@ -47,16 +47,6 @@ def update(self, schedule_item): updated_schedule = copy.copy(schedule_item) return updated_schedule._parse_common_tags(server_response.content) - def populate_tasks(self, schedule_item, req_options): - if not schedule_item.id: - error = "Schedule item missing ID." - raise MissingRequiredFieldError(error) - url = "{0}/{1}/extracts".format(self.baseurl, schedule_item.id) - server_response = self.get_request(url, req_options) - # Adding to an existing list ... how to handle paging and knowing what paging might already have been done - pagination_item = PaginationItem.from_response(server_response.content) - schedule_item.tasks = TaskItem.from_response(server_response.content) - def create(self, schedule_item): if schedule_item.interval_item is None: error = "Interval item must be defined." diff --git a/tableauserverclient/server/endpoint/tasks_endpoint.py b/tableauserverclient/server/endpoint/tasks_endpoint.py new file mode 100644 index 000000000..92aaebcf8 --- /dev/null +++ b/tableauserverclient/server/endpoint/tasks_endpoint.py @@ -0,0 +1,11 @@ +from .endpoint import Endpoint +from extract_refreshes_endpoint import ExtractRefreshes +import logging + +logger = logging.getLogger('tableau.endpoint.tasks') + +class Tasks(Endpoint): + def __init__(self, parent_srv): + super(Tasks, self).__init__() + self.parent_srv = parent_srv + self.extract_refreshes = ExtractRefreshes(parent_srv) \ No newline at end of file diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index 8c28c1825..d01386fdf 100644 --- a/tableauserverclient/server/server.py +++ b/tableauserverclient/server/server.py @@ -1,5 +1,5 @@ from .exceptions import NotSignedInError -from .endpoint import Sites, Views, Users, Groups, Workbooks, Datasources, Projects, Auth, Schedules +from .endpoint import Sites, Views, Users, Groups, Workbooks, Datasources, Projects, Auth, Schedules, Tasks import requests @@ -27,6 +27,7 @@ def __init__(self, server_address): self.datasources = Datasources(self) self.projects = Projects(self) self.schedules = Schedules(self) + self.tasks = Tasks(self) def add_http_options(self, options_dict): self._http_options.update(options_dict)