import os
from typing import Optional, List
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
from validator_collection import validators, checkers
try:
import asana
HAS_ASANA = True
except ImportError:
HAS_ASANA = False
try:
import jira
HAS_JIRA = True
except ImportError:
HAS_JIRA = False
from highcharts_core.options.series.base import SeriesBase
from highcharts_gantt import errors, monday
from highcharts_gantt.options.plot_options.gantt import GanttOptions
from highcharts_gantt.options.series.data.gantt import GanttData, GanttDataCollection
from highcharts_gantt.utility_functions import mro__to_untrimmed_dict, is_ndarray
[docs]class GanttSeries(SeriesBase, GanttOptions):
"""Options to configure a Gantt series.
Gantt charts are a type of chart used to visualize efforts executed in a sequence.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
@property
def data(self) -> Optional[List[GanttData] | GanttDataCollection]:
"""Collection of data that represents the series. Defaults to
:obj:`None <python:None>`.
While the series type returns a collection of :class:`GanttData` instances,
it accepts as input an iterable of :class:`GanttData` instances or
:class:`dict <python:dict>` instances that can be coerced to :class:`GanttData`.
:rtype: :class:`list <python:list>` of :class:`GanttData` or
:class:`GanttDataCollection` or
:obj:`None <python:None>`
"""
return self._data
@data.setter
def data(self, value):
if not is_ndarray(value) and not value:
self._data = None
else:
self._data = GanttData.from_array(value)
@classmethod
def _get_kwargs_from_dict(cls, as_dict):
kwargs = {
'accessibility': as_dict.get('accessibility', None),
'allow_point_select': as_dict.get('allowPointSelect', None),
'animation': as_dict.get('animation', None),
'class_name': as_dict.get('className', None),
'clip': as_dict.get('clip', None),
'color': as_dict.get('color', None),
'cursor': as_dict.get('cursor', None),
'custom': as_dict.get('custom', None),
'dash_style': as_dict.get('dashStyle', None),
'data_labels': as_dict.get('dataLabels', None),
'description': as_dict.get('description', None),
'enable_mouse_tracking': as_dict.get('enableMouseTracking', None),
'events': as_dict.get('events', None),
'include_in_data_export': as_dict.get('includeInDataExport', None),
'keys': as_dict.get('keys', None),
'label': as_dict.get('label', None),
'legend_symbol': as_dict.get('legendSymbol', None),
'linked_to': as_dict.get('linkedTo', None),
'marker': as_dict.get('marker', None),
'on_point': as_dict.get('onPoint', None),
'opacity': as_dict.get('opacity', None),
'point': as_dict.get('point', None),
'point_description_formatter': as_dict.get('pointDescriptionFormatter', None),
'selected': as_dict.get('selected', None),
'show_checkbox': as_dict.get('showCheckbox', None),
'show_in_legend': as_dict.get('showInLegend', None),
'skip_keyboard_navigation': as_dict.get('skipKeyboardNavigation', None),
'states': as_dict.get('states', None),
'sticky_tracking': as_dict.get('stickyTracking', None),
'threshold': as_dict.get('threshold', None),
'tooltip': as_dict.get('tooltip', None),
'turbo_threshold': as_dict.get('turboThreshold', None),
'visible': as_dict.get('visible', None),
'animation_limit': as_dict.get('animationLimit', None),
'boost_blending': as_dict.get('boostBlending', None),
'boost_threshold': as_dict.get('boostThreshold', None),
'color_axis': as_dict.get('colorAxis', None),
'color_index': as_dict.get('colorIndex', None),
'color_key': as_dict.get('colorKey', None),
'connect_ends': as_dict.get('connectEnds', None),
'connect_nulls': as_dict.get('connectNulls', None),
'crisp': as_dict.get('crisp', None),
'crop_threshold': as_dict.get('cropThreshold', None),
'data_sorting': as_dict.get('dataSorting', None),
'drag_drop': as_dict.get('dragDrop', None),
'fill_color': as_dict.get('fillColor', None),
'fill_opacity': as_dict.get('fillOpacity', None),
'find_nearest_point_by': as_dict.get('findNearestPointBy', None),
'get_extremes_from_all': as_dict.get('getExtremesFromAll', None),
'inactive_other_points': as_dict.get('inactiveOtherPoints', None),
'linecap': as_dict.get('linecap', None),
'line_color': as_dict.get('lineColor', None),
'line_width': as_dict.get('lineWidth', None),
'negative_color': as_dict.get('negativeColor', None),
'negative_fill_color': as_dict.get('negativeFillColor', None),
'point_interval': as_dict.get('pointInterval', None),
'point_interval_unit': as_dict.get('pointIntervalUnit', None),
'point_placement': as_dict.get('pointPlacement', None),
'point_start': as_dict.get('pointStart', None),
'relative_x_value': as_dict.get('relativeXValue', None),
'shadow': as_dict.get('shadow', None),
'soft_threshold': as_dict.get('softThreshold', None),
'stacking': as_dict.get('stacking', None),
'step': as_dict.get('step', None),
'track_by_area': as_dict.get('trackByArea', None),
'zone_axis': as_dict.get('zoneAxis', None),
'zones': as_dict.get('zones', None),
'border_color': as_dict.get('borderColor', None),
'border_radius': as_dict.get('borderRadius', None),
'border_width': as_dict.get('borderWidth', None),
'center_in_category': as_dict.get('centerInCategory', None),
'color_by_point': as_dict.get('colorByPoint', None),
'colors': as_dict.get('colors', None),
'grouping': as_dict.get('grouping', None),
'group_padding': as_dict.get('groupPadding', None),
'max_point_width': as_dict.get('maxPointWidth', None),
'min_point_length': as_dict.get('minPointLength', None),
'point_padding': as_dict.get('pointPadding', None),
'point_range': as_dict.get('pointRange', None),
'point_width': as_dict.get('pointWidth', None),
'group_z_padding': as_dict.get('groupZPadding', None),
'partial_fill': as_dict.get('partialFill', None),
'data_as_columns': as_dict.get('dataAsColumns', None),
'data': as_dict.get('data', None),
'id': as_dict.get('id', None),
'index': as_dict.get('index', None),
'legend_index': as_dict.get('legendIndex', None),
'name': as_dict.get('name', None),
'z_index': as_dict.get('zIndex', None),
'connectors': as_dict.get('connectors', None),
}
return kwargs
def _to_untrimmed_dict(self, in_cls = None) -> dict:
untrimmed = mro__to_untrimmed_dict(self, in_cls = in_cls)
return untrimmed
[docs] def load_from_asana(self,
project_gid,
section_gid = None,
completed_since = None,
use_html_description = True,
personal_access_token = None,
asana_client = None,
api_request_params = None,
connection_kwargs = None,
connection_callback = None):
"""Replace the data in the
:class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`
instance with data from an `Asana <https://www.asana.com/>`__ project.
.. note::
**Highcharts Gantt for Python** can create an Asana API client for you,
authenticating using the :term:`Personal Access Token` method supported by
the Asana API. However, if you wish to use the more-involved OAuth2 handshake
process you will need to create your own Asana API client using the
`asana-python <https://pypi.org/project/asana/>`__ library.
The reason for this is because the OAuth2 handshake has various permutations
involving redirects, token refreshes, etc. which are outside the scope of the
**Highcharts Gantt for Python** library, and if you are integrating
**Highcharts Gantt for Python** into a larger application you are likely
already facilitating the OAuth2 dance in a fashion appropriate for your use
case.
:param project_gid: The globally unique ID of the Project whose tasks should be
used to assemble the Gantt series.
.. tip::
You can find your Asana Project GID in your browser URL bar:
.. figure:: /_static/asana-project-gid.png
:align: center
:alt: Asana Project GID
:type project_gid: :class:`str <python:str>`
:param section_gid: The optional unique ID of the section whose tasks should be
used to assemble the Gantt chart. Defaults to :obj:`None <python:None>`, which
returns all tasks in the project.
:type section_gid: :class:`str <python:str>` or :obj:`None <python:None>`
:param completed_since: An optional filter which only returns tasks that have
been completed after this date. Defaults to :obj:`None <python:None>`, which
returns all tasks.
:type completed_since: :class:`datetime <python:datetime.datetime>` or
:obj:`None <python:None>`
:param use_html_description: If ``True``, will use the Asana task's HTML notes
in the data point's
:meth:`.description <highcharts_gantt.options.series.data.gantt.GanttData.description>`
field. If ``False``, will use the non-HTML notes. Defaults to ``True``.
:type use_html_description: :class:`bool <python:bool>`
:param personal_access_token: A Personal Access Token created by Asana.
Defaults to :obj:`None <python:None>`, which tries to determine its value
by looking in the ``ASANA_PERSONAL_ACCESS_TOKEN`` environment variable.
:type personal_access_token: :class:`str <python:str>` or
:obj:`None <python:None>`
:param api_request_params: Collection of additional request parameters to
submit to the Asana API. Defaults to :obj:`None <python:None>`.
:type api_request_params: :class:`dict <python:dict>` or
:obj:`None <python:None>`
:param connection_kwargs: Set of keyword arugments to supply to the
:class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>`
constructor, besides the :meth:`.to <highcharts_gantt.options.series.data.connect.DataConnection.to>`
property which is derived from the task. Defaults
to :obj:`None <python:None>`
:type connection_kwargs: :class:`dict <python:dict>` or
:obj:`None <python:None>`
:param connection_callback: A custom Python function or method which accepts two
keyword arguments: ``connection_target`` (which expects the dependency
:class:`dict <python:dict>` object from the Asana task), and ``asana_task``
(which expects the Asana task :class:`dict <pythoN:dict>` object). The
function should return a
:class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>` instance. Defaults to
:obj:`None <python:None>`
.. tip::
The ``connection_callback`` argument is useful if you want to customize the
connection styling based on properties included in the Asana task.
:type connection_callback: Callable or :obj:`None <python:None>`
:returns: A :class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`
populated with data from the indicated Asana project/section.
:rtype: :class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`
:raises HighchartsDependencyError: if the
`asana <https://pypi.org/project/asana/>`__ Python library is not available
in the runtime environment.
:raises HighchartsValueError: if ``connection_callback`` is not
:obj:`None <python:None>`, but is not callable
:raises HighchartsValueError: if ``asana_client`` is not
:obj:`None <python:None>`, but is not a valid :class:`asana.client.Client>`
instance
:raises AsanaAuthenticationError: if ``asana_client`` is not authenticated or
if no personal access token is supplied
"""
if not HAS_ASANA:
raise errors.HighchartsDependencyError('The .from_asana() method requires '
'the asana Python library. However '
'your runtime environment does not '
'seem to have it. Please install and'
' try again.')
project_gid = validators.string(project_gid, coerce_value = True)
section_gid = validators.string(section_gid,
allow_empty = True,
coerce_value = True)
completed_since = validators.datetime(completed_since, allow_empty = True)
api_request_params = validators.dict(api_request_params,
allow_empty = True) or {}
if not personal_access_token:
personal_access_token = os.getenv('ASANA_PERSONAL_ACCESS_TOKEN', None)
if asana_client and not isinstance(asana_client, asana.client.Client):
raise errors.HighchartsValueError(f'asana_client must be a valid asana '
f'Client instance. Was: '
f'{asana_client.__class__.__name__}')
if asana_client and not asana_client.session.token:
raise errors.AsanaAuthenticationError('asana_client is not authenticated')
if asana_client:
client = asana_client
elif not personal_access_token:
raise errors.AsanaAuthenticationError('from_asana() requires either a '
'personal access token or an '
'authenticated Asana client. '
'Neither was supplied.')
else:
client = asana.Client.access_token(personal_access_token)
client.LOG_ASANA_CHANGE_WARNINGS = False
if section_gid:
request = client.tasks.get_tasks_for_section
request_params = {
'section_gid': section_gid
}
else:
request = client.tasks.get_tasks_for_project
request_params = {
'project_gid': project_gid
}
request_params['params'] = {}
if completed_since:
request_params['params']['completed_since'] = completed_since.isoformat()
opt_fields = ['approval_status',
'assignee_status',
'completed',
'completed_at',
'dependencies',
'dependents',
'due_at',
'due_on',
'html_notes',
'name',
'notes',
'start_at',
'start_on',
'assignee',
'assignee_section',
'parent',
'permalink_url',
'custom_fields',
'resource_type',
'resource_subtype']
request_params['params']['opt_fields'] = opt_fields
for key in request_params:
api_request_params[key] = request_params[key]
try:
tasks = [x for x in request(**api_request_params)]
except asana.error.NoAuthorizationError:
raise errors.AsanaAuthenticationError('the authentication method supplied returned as unauthorized')
data_points = [GanttData.from_asana(x,
use_html_description = use_html_description,
connection_callback = connection_callback,
connection_kwargs = connection_kwargs)
for x in tasks]
self.data = data_points
[docs] @classmethod
def from_asana(cls,
project_gid,
section_gid = None,
completed_since = None,
use_html_description = True,
personal_access_token = None,
asana_client = None,
api_request_params = None,
connection_kwargs = None,
connection_callback = None,
series_kwargs = None):
"""Create a
:class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`
instance from an `Asana <https://www.asana.com/>`__ project.
.. note::
**Highcharts Gantt for Python** can create an Asana API client for you,
authenticating using the :term:`Personal Access Token` method supported by
the Asana API. However, if you wish to use the more-involved OAuth2 handshake
process you will need to create your own Asana API client using the
`asana-python <https://pypi.org/project/asana/>`__ library.
The reason for this is because the OAuth2 handshake has various permutations
involving redirects, token refreshes, etc. which are outside the scope of the
**Highcharts Gantt for Python** library, and if you are integrating
**Highcharts Gantt for Python** into a larger application you are likely
already facilitating the OAuth2 dance in a fashion appropriate for your use
case.
:param project_gid: The globally unique ID of the Project whose tasks should be
used to assemble the Gantt chart.
.. tip::
You can find your Asana Project GID in your browser URL bar:
.. figure:: /_static/asana-project-gid.png
:align: center
:alt: Asana Project GID
:type project_gid: :class:`str <python:str>`
:param section_gid: The optional unique ID of the section whose tasks should be
used to assemble the Gantt chart. Defaults to :obj:`None <python:None>`, which
returns all tasks in the project.
:type section_gid: :class:`str <python:str>` or :obj:`None <python:None>`
:param completed_since: An optional filter which only returns tasks that have
been completed after this date. Defaults to :obj:`None <python:None>`, which
returns all tasks.
:type completed_since: :class:`datetime <python:datetime.datetime>` or
:obj:`None <python:None>`
:param use_html_description: If ``True``, will use the Asana task's HTML notes
in the data point's
:meth:`.description <highcharts_gantt.options.series.data.gantt.GanttData.description>`
field. If ``False``, will use the non-HTML notes. Defaults to ``True``.
:type use_html_description: :class:`bool <python:bool>`
:param personal_access_token: A Personal Access Token created by Asana.
Defaults to :obj:`None <python:None>`, which tries to determine its value
by looking in the ``ASANA_PERSONAL_ACCESS_TOKEN`` environment variable.
:type personal_access_token: :class:`str <python:str>` or
:obj:`None <python:None>`
:param api_request_params: Collection of additional request parameters to
submit to the Asana API. Defaults to :obj:`None <python:None>`.
:type api_request_params: :class:`dict <python:dict>` or
:obj:`None <python:None>`
:param connection_kwargs: Set of keyword arugments to supply to the
:class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>`
constructor, besides the :meth:`.to <highcharts_gantt.options.series.data.connect.DataConnection.to>`
property which is derived from the task. Defaults
to :obj:`None <python:None>`
:type connection_kwargs: :class:`dict <python:dict>` or
:obj:`None <python:None>`
:param connection_callback: A custom Python function or method which accepts two
keyword arguments: ``connection_target`` (which expects the dependency
:class:`dict <python:dict>` object from the Asana task), and ``asana_task``
(which expects the Asana task :class:`dict <pythoN:dict>` object). The
function should return a
:class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>` instance. Defaults to
:obj:`None <python:None>`
.. tip::
The ``connection_callback`` argument is useful if you want to customize the
connection styling based on properties included in the Asana task.
:type connection_callback: Callable or :obj:`None <python:None>`
:param series_kwargs: Collection of additional keyword arguments to use when
instantiating the
:class:`GanttSeries <highcharts_gantt.options.series.GanttSeries>` (besides
the ``data`` argument, which will be determined from the Asana tasks).
Defaults to :obj:`None <python:None>`.
:type series_kwargs: :class:`dict <python:dict>` or :obj:`None <python:None>`
:returns: A :class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`
populated with data from the indicated Asana project/section.
:rtype: :class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`
:raises HighchartsDependencyError: if the
`asana <https://pypi.org/project/asana/>`__ Python library is not available
in the runtime environment.
:raises HighchartsValueError: if ``connection_callback`` is not
:obj:`None <python:None>`, but is not callable
:raises HighchartsValueError: if ``asana_client`` is not
:obj:`None <python:None>`, but is not a valid :class:`asana.client.Client>`
instance
:raises AsanaAuthenticationError: if ``asana_client`` is not authenticated or
if no personal access token is supplied
"""
series_kwargs = validators.dict(series_kwargs, allow_empty = True) or {}
instance = cls(**series_kwargs)
instance.load_from_asana(project_gid,
section_gid = section_gid,
completed_since = completed_since,
use_html_description = use_html_description,
personal_access_token = personal_access_token,
asana_client = asana_client,
api_request_params = api_request_params,
connection_kwargs = connection_kwargs,
connection_callback = connection_callback)
return instance
[docs] def load_from_monday(self,
board_id,
api_token = None,
template = None,
property_column_map = None,
connection_kwargs = None,
connection_callback = None):
"""Update the :class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>` instance
with data from a `Monday.com <https://www.monday.com>`__ work board.
:param board_id: The ID of the Monday.com board whose items should be retrieved
to populate the Gantt series.
.. tip::
You can find your Asana Project GID in your browser URL bar:
.. figure:: /_static/monday-board-id.png
:align: center
:alt: Monday.com Board ID
:type board_id: :class:`int <python:int>`
:param api_token: The Monday.com API token to use when authenticating your
request against the Monday.com API. Defaults to :obj:`None <python:None>`,
which will then try to determine the token from the ``MONDAY_API_TOKEN``
environment variable.
.. warning::
If no token is either passed to the method *or* found in the
``MONDAY_API_TOKEN`` environment variable, calling this method will raise
an error.
:type api_token: :class:`str <python:str>` or :obj:`None <python:None>`
:param template: The name of a standard Mpnday.com board template supported by
**Highcharts for Python**. If supplied, will override the
``property_column_map`` argument. Defaults to :obj:`None <python:None>`.
.. note::
If ``property_column_map`` is set, the ``template`` argument will be
*ignored* and overridden by ``property_column_map``.
:type template: :class:`str <python:str>` or :obj:`None <python:None>`
:param property_column_map: A :class:`dict <python:dict>` used to map Monday.com
columns to their corresponding
:class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`
properties. Keys are expected to be
:class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`
properties, while values are expected to be Monday.com column field names.
Defaults to :obj:`None <python:None>`.
.. note::
If ``property_column_map`` is supplied, its settings *override* the
``template`` setting.
:type property_column_map: :class:`dict <python:dict>` or
:obj:`None <python:None>`
:param connection_kwargs: Set of keyword arugments to supply to the
:class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>`
constructor, besides the :meth:`.to <highcharts_gantt.options.series.data.connect.DataConnection.to>`
property which is derived from the task. Defaults
to :obj:`None <python:None>`
:type connection_kwargs: :class:`dict <python:dict>` or
:obj:`None <python:None>`
:param connection_callback: A custom Python function or method which accepts two
keyword arguments: ``connection_target`` (which expects the dependency
:class:`dict <python:dict>` object from the Asana task), and ``asana_task``
(which expects the Asana task :class:`dict <pythoN:dict>` object). The
function should return a
:class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>` instance. Defaults to
:obj:`None <python:None>`
.. tip::
The ``connection_callback`` argument is useful if you want to customize the
connection styling based on properties included in the Asana task.
:type connection_callback: Callable or :obj:`None <python:None>`
:raises HighchartsDependencyError: if the
`monday <https://pypi.org/project/monday/>`__ Python library is not available
in the runtime environment
:raises MondayAuthenticationError: if there is no Monday.com API token supplied
:raises HighchartsValueError: if both ``template`` and ``property_column_map``
are empty
"""
tasks = monday.get_tasks(board_id, api_token = api_token)
data_points = [GanttData.from_monday(tasks[x],
template = template,
property_column_map = property_column_map,
connection_kwargs = connection_kwargs,
connection_callback = connection_callback) for x in tasks]
self.data = data_points
[docs] @classmethod
def from_monday(cls,
board_id,
api_token = None,
template = None,
property_column_map = None,
connection_kwargs = None,
connection_callback = None,
series_kwargs = None):
"""Create a :class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>` instance from a
`Monday.com <https://www.monday.com>`__ work board.
:param board_id: The ID of the Monday.com board whose items should be retrieved
to populate the Gantt series.
.. tip::
You can find your Asana Project GID in your browser URL bar:
.. figure:: /_static/monday-board-id.png
:align: center
:alt: Monday.com Board ID
:type board_id: :class:`int <python:int>`
:param api_token: The Monday.com API token to use when authenticating your
request against the Monday.com API. Defaults to :obj:`None <python:None>`,
which will then try to determine the token from the ``MONDAY_API_TOKEN``
environment variable.
.. warning::
If no token is either passed to the method *or* found in the
``MONDAY_API_TOKEN`` environment variable, calling this method will raise
an error.
:type api_token: :class:`str <python:str>` or :obj:`None <python:None>`
:param template: The name of a standard Mpnday.com board template supported by
**Highcharts for Python**. If supplied, will override the
``property_column_map`` argument. Defaults to :obj:`None <python:None>`.
.. note::
If ``property_column_map`` is set, the ``template`` argument will be
*ignored* and overridden by ``property_column_map``.
:type template: :class:`str <python:str>` or :obj:`None <python:None>`
:param property_column_map: A :class:`dict <python:dict>` used to map Monday.com
columns to their corresponding
:class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`
properties. Keys are expected to be
:class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`
properties, while values are expected to be Monday.com column field names.
Defaults to :obj:`None <python:None>`.
.. note::
If ``property_column_map`` is supplied, its settings *override* the
``template`` setting.
:type property_column_map: :class:`dict <python:dict>` or
:obj:`None <python:None>`
:param connection_kwargs: Set of keyword arugments to supply to the
:class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>`
constructor, besides the :meth:`.to <highcharts_gantt.options.series.data.connect.DataConnection.to>`
property which is derived from the task. Defaults
to :obj:`None <python:None>`
:type connection_kwargs: :class:`dict <python:dict>` or
:obj:`None <python:None>`
:param connection_callback: A custom Python function or method which accepts two
keyword arguments: ``connection_target`` (which expects the dependency
:class:`dict <python:dict>` object from the Asana task), and ``asana_task``
(which expects the Asana task :class:`dict <pythoN:dict>` object). The
function should return a
:class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>` instance. Defaults to
:obj:`None <python:None>`
.. tip::
The ``connection_callback`` argument is useful if you want to customize the
connection styling based on properties included in the Asana task.
:type connection_callback: Callable or :obj:`None <python:None>`
:param series_kwargs: Collection of additional keyword arguments to use when
instantiating the
:class:`GanttSeries <highcharts_gantt.options.series.GanttSeries>` (besides
the ``data`` argument, which will be determined from the Asana tasks).
Defaults to :obj:`None <python:None>`.
:type series_kwargs: :class:`dict <python:dict>` or :obj:`None <python:None>`
:returns: A :class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`
populated with data from the indicated Asana project/section.
:rtype: :class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`
:raises HighchartsDependencyError: if the
`monday <https://pypi.org/project/monday/>`__ Python library is not available
in the runtime environment
:raises MondayAuthenticationError: if there is no Monday.com API token supplied
:raises HighchartsValueError: if both ``template`` and ``property_column_map``
are empty
"""
series_kwargs = validators.dict(series_kwargs, allow_empty = True) or {}
tasks = monday.get_tasks(board_id, api_token = api_token)
data_points = [GanttData.from_monday(tasks[x],
template = template,
property_column_map = property_column_map,
connection_kwargs = connection_kwargs,
connection_callback = connection_callback) for x in tasks]
instance = cls(**series_kwargs)
instance.data = data_points
return instance
[docs] def load_from_jira(self,
project_key,
server = None,
jql = None,
username = None,
password_or_token = None,
oauth_dict = None,
client_kwargs = None,
jira_client = None,
connection_kwargs = None,
connection_callback = None):
"""Update the
:class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`
instance with data from an `Atlassian JIRA <https://www.atlassian.com/>`__ project.
.. note::
**Highcharts Gantt for Python** can create a JIRA API client for you,
authenticating using either the :term:`Basic Authentication` or
:term:`Access Token` methods supported by the JIRA API. However, if you wish to
use the more-involved OAuth2 handshake, you can do so yourself and either
* supply an ``oauth_dict`` argument containing the OAuth2 configuration
details, or
* supply a fully-authenticated ``jira_client``
The reason for this is because the OAuth2 handshake has various permutations
involving redirects, token refreshes, etc. which are outside the scope of the
**Highcharts Gantt for Python** library, and if you are integrating
**Highcharts Gantt for Python** into a larger application you are likely
already facilitating the OAuth2 dance in a fashion appropriate for your use
case.
:param project_key: The globally unique key of the Project whose tasks should be
used to assemble the Gantt chart. For example, ``JRA``.
:type project_key: :class:`str <python:str>`
:param server: The URL of the JIRA instance from which data should be retrieved.
Defaults to :obj:`None <python:None>`, which looks for a value in the ``HIGHCHARTS_JIRA_SERVER`` environment
variable. If no value is found there, will then fallback to JIRA Cloud: ``'https://jira.atlasian.com'``.
.. note::
This argument will override the comparable setting in ``client_kwargs`` if
``client_kwargs`` is supplied.
:type server: :class:`str <python:str>` or :obj:`None <python:None>`
:param jql: An optional :term:`JIRA Query Language` query string to further
narrow the issues returned from JIRA. Defaults to :obj:`None <python:None>`.
:type jql: :class:`str <python:str>` or :obj:`None <python:None>`
:param username: The username to use when authenticating using either ``basic``
or ``token`` authentication. Defaults to :obj:`None <python:None>`, which
looks for a value in the ``HIGHCHARTS_JIRA_USERNAME`` environment variable.
.. note::
If ``oauth2_dict`` is supplied, the ``username`` argument will be ignored
since OAuth2 authentication will be used.
:type username: :class:`str <python:str>` or :obj:`None <python:None>`
:param password_or_token: The password or access token to use when
authenticating using either ``basic`` or ``token`` authentication. Defaults
to :obj:`None <python:None>`, which looks for a vlaue in the
``HIGHCHARTS_JIRA_TOKEN`` environment variable.
.. note::
If ``oauth_dict`` is supplied, the ``password_or_token`` will be ignored
since OAuth2 authentication will be used.
:type password_or_token: :class:`str <python:str>` or :obj:`None <python:None>`
:param oauth_dict: A :class:`dict <python:dict>` of key/value pairs providing
configuration of the Oauth2 authentication details. Expected keys are:
* ``'access_token'``
* ``'access_token_secret'``
* ``'consumer_key'``
* ``'key_cert'``
Defaults to :obj:`None <python:None>`.
.. note::
To use OAuth2 authentication, an ``oauth_dict`` *must* be supplied. If you
wish to force either basic or token authentication, make sure this argument
remains :obj:`None <python:None>`.
:type oauth_dict: :class:`dict <python:dict>` or :obj:`None <python:None>`
:param client_kwargs: An optional :class:`dict <python:dict>` providing keyword
arguments to use when instantiating the JIRA client.
:type client_kwargs: :class:`dict <python:dict>` or :obj:`None <python:None>`
:param jira_client: A fully-configured and fully-authenticated JIRA API client.
Defaults to :obj:`None <python:None>`.
:type jira_client: :class:`jira.client.JIRA <jira:jira.client.JIRA>` instance
that has been fully authenticated
:param connection_kwargs: Set of keyword arugments to supply to the
:class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>`
constructor, besides the
:meth:`.to <highcharts_gantt.options.series.data.connect.DataConnection.to>`
property which is derived from the task. Defaults to :obj:`None <python:None>`
:type connection_kwargs: :class:`dict <python:dict>` or
:obj:`None <python:None>`
:param connection_callback: A custom Python function or method which accepts two
keyword arguments: ``connection_target`` (which expects the dependency
:class:`Issue <jira:jira.resources.Issue>` object from the initial
:class:`Issue <jira:jira.resources.Issue>`), and ``issue``
(which expects the initial :class:`Issue <jira:jira.resources.Issue>`
object). The function should return a
:class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>`
instance. Defaults to :obj:`None <python:None>`.
.. tip::
The ``connection_callback`` argument is useful if you want to customize the
connection styling based on properties included in the target issue.
:type connection_callback: Callable or :obj:`None <python:None>`
:raises HighchartsDependencyError: if the `jira <https://jira.readthedocs.io/>`__
Python library is not available in the runtime environment.
:raises JIRAAuthenticationError: if no authentication details are provided or if
the authentication process fails
:raises JIRAProjectNotFoundError: if the ``project_key`` is not found in the JIRA
``server`` indicated
.. tip::
This can happen if authentication fails silently, which can happen when using the
JIRA Cloud environment.
:raises HighchartsValueError: if other keyword arguments are misconfigured
"""
if not HAS_JIRA:
raise errors.HighchartsDependencyError('The .from_jira() method depends '
'on the jira Python library. This '
'library was not found in the '
'runtime environment. Please install'
' and try again.')
if not jira_client:
client_kwargs = validators.dict(client_kwargs, allow_empty = True) or {}
if not server:
server = os.getenv('HIGHCHARTS_JIRA_SERVER',
client_kwargs.get('server', 'https://jira.atlassian.com'))
client_kwargs['server'] = validators.url(server, allow_special_ips = True)
project_key = validators.string(project_key)
username = validators.string(username, allow_empty = True) \
or os.getenv('HIGHCHARTS_JIRA_USERNAME', None)
password_or_token = validators.string(password_or_token, allow_empty = True) \
or os.getenv('HIGHCHARTS_JIRA_TOKEN', None)
use_basic = oauth_dict is None and username is not None
use_token = oauth_dict is None and username is None
if use_basic:
client_kwargs['basic_auth'] = (username, password_or_token)
elif use_token:
client_kwargs['token_auth'] = password_or_token
elif oauth_dict is not None:
client_kwargs['oauth'] = validators.dict(oauth_dict,
allow_empty = False)
else:
raise errors.JIRAAuthenticationError('no authentication details '
'provided')
try:
jira_client = jira.JIRA(**client_kwargs)
except jira.JIRAError as error:
if error.status_code == 401:
raise errors.JIRAAuthenticationError('JIRA failed to authenticate')
else:
raise error
else:
if not isinstance(jira_client, jira.client.JIRA):
raise errors.HighchartsValueError(f'jira_client must be a valid '
f'jira.client.JIRA instance. Was: '
f'{jira_client.__class__.__name__}')
if not jira_client._session:
raise errors.JIRAAuthenticationError('jira_client is not authenticated')
if jql and f'project = {project_key}' not in jql and f'project={project_key}' not in jql:
raise errors.HighchartsValueError(f'jql contains a project reference '
f'that does not match project_key '
f'("{project_key}").')
elif not jql:
jql = f'project = {project_key}'
try:
issues = jira_client.search_issues(jql)
except jira.JIRAError as error:
if error.status_code == 400:
raise errors.JIRAProjectNotFoundError(f'No JIRA project with key "{project_key}" '
f'was found. Note that this may be because '
f'your authentication failed silently, '
f'a common issue when using JIRA Cloud.')
else:
raise error
if issues.total > len(issues):
issues.extend(jira_client.search_issues(jql, startAt = len(issues)))
data_points_with_none = [
GanttData.from_jira(x,
connection_kwargs = connection_kwargs,
connection_callback = connection_callback)
for x in issues
]
data_points = [x for x in data_points_with_none if x is not None]
self.data = data_points
[docs] @classmethod
def from_jira(cls,
project_key,
server = None,
jql = None,
username = None,
password_or_token = None,
oauth_dict = None,
client_kwargs = None,
jira_client = None,
connection_kwargs = None,
connection_callback = None,
series_kwargs = None):
"""Create a
:class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`
instance from an `Atlassian JIRA <https://www.atlassian.com/>`__ project.
.. note::
**Highcharts Gantt for Python** can create a JIRA API client for you,
authenticating using either the :term:`Basic Authentication` or
:term:`Access Token` methods supported by the JIRA API. However, if you wish to use the more-involved OAuth2
handshake, you can do so yourself and either
* supply an ``oauth_dict`` argument containing the OAuth2 configuration
details, or
* supply a fully-authenticated ``jira_client``
The reason for this is because the OAuth2 handshake has various permutations
involving redirects, token refreshes, etc. which are outside the scope of the
**Highcharts Gantt for Python** library, and if you are integrating
**Highcharts Gantt for Python** into a larger application you are likely
already facilitating the OAuth2 dance in a fashion appropriate for your use
case.
:param project_key: The globally unique key of the Project whose tasks should be
used to assemble the Gantt chart. For example, ``JRA``.
:type project_key: :class:`str <python:str>`
:param server: The URL of the JIRA instance from which data should be retrieved.
Defaults to :obj:`None <python:None>`, which looks for a value in the ``HIGHCHARTS_JIRA_SERVER`` environment
variable. If no value is found there, will then fallback to JIRA Cloud: ``'https://jira.atlasian.com'``.
.. note::
This argument will override the comparable setting in ``client_kwargs`` if
``client_kwargs`` is supplied.
:type server: :class:`str <python:str>` or :obj:`None <python:None>`
:param jql: An optional :term:`JIRA Query Language` query string to further
narrow the issues returned from JIRA. Defaults to :obj:`None <python:None>`.
:type jql: :class:`str <python:str>` or :obj:`None <python:None>`
:param username: The username to use when authenticating using either ``basic``
or ``token`` authentication. Defaults to :obj:`None <python:None>`, which
looks for a value in the ``HIGHCHARTS_JIRA_USERNAME`` environment variable.
.. note::
If ``oauth2_dict`` is supplied, the ``username`` argument will be ignored
since OAuth2 authentication will be used.
:type username: :class:`str <python:str>` or :obj:`None <python:None>`
:param password_or_token: The password or access token to use when
authenticating using either ``basic`` or ``token`` authentication. Defaults
to :obj:`None <python:None>`, which looks for a vlaue in the
``HIGHCHARTS_JIRA_TOKEN`` environment variable.
.. note::
If ``oauth_dict`` is supplied, the ``password_or_token`` will be ignored
since OAuth2 authentication will be used.
:type password_or_token: :class:`str <python:str>` or :obj:`None <python:None>`
:param oauth_dict: A :class:`dict <python:dict>` of key/value pairs providing
configuration of the Oauth2 authentication details. Expected keys are:
* ``'access_token'``
* ``'access_token_secret'``
* ``'consumer_key'``
* ``'key_cert'``
Defaults to :obj:`None <python:None>`.
.. note::
To use OAuth2 authentication, an ``oauth_dict`` *must* be supplied. If you
wish to force either basic or token authentication, make sure this argument
remains :obj:`None <python:None>`.
:type oauth_dict: :class:`dict <python:dict>` or :obj:`None <python:None>`
:param client_kwargs: An optional :class:`dict <python:dict>` providing keyword
arguments to use when instantiating the JIRA client.
:type client_kwargs: :class:`dict <python:dict>` or :obj:`None <python:None>`
:param jira_client: A fully-configured and fully-authenticated JIRA API client.
Defaults to :obj:`None <python:None>`.
:type jira_client: :class:`jira.client.JIRA <jira:jira.client.JIRA>` instance
that has been fully authenticated
:param connection_kwargs: Set of keyword arugments to supply to the
:class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>`
constructor, besides the
:meth:`.to <highcharts_gantt.options.series.data.connect.DataConnection.to>`
property which is derived from the task. Defaults to :obj:`None <python:None>`
:type connection_kwargs: :class:`dict <python:dict>` or
:obj:`None <python:None>`
:param connection_callback: A custom Python function or method which accepts two
keyword arguments: ``connection_target`` (which expects the dependency
:class:`Issue <jira:jira.resources.Issue>` object from the initial
:class:`Issue <jira:jira.resources.Issue>`), and ``issue``
(which expects the initial :class:`Issue <jira:jira.resources.Issue>`
object). The function should return a
:class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>`
instance. Defaults to :obj:`None <python:None>`.
.. tip::
The ``connection_callback`` argument is useful if you want to customize the
connection styling based on properties included in the target issue.
:type connection_callback: Callable or :obj:`None <python:None>`
:param series_kwargs: Collection of additional keyword arguments to use when
instantiating the
:class:`GanttSeries <highcharts_gantt.options.series.GanttSeries>` (besides
the ``data`` argument, which will be determined from the JIRA issues).
Defaults to :obj:`None <python:None>`.
:type series_kwargs: :class:`dict <python:dict>` or :obj:`None <python:None>`
:returns: A :
class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`
populated with data from the indicated JIRA project.
:rtype: :class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`
:raises HighchartsDependencyError: if the `jira <https://jira.readthedocs.io/>`__
Python library is not available in the runtime environment.
:raises JIRAAuthenticationError: if no authentication details are provided or if
the authentication process fails
:raises JIRAProjectNotFoundError: if the ``project_key`` is not found in the JIRA
``server`` indicated
.. tip::
This can happen if authentication fails silently, which can happen when using the
JIRA Cloud environment.
:raises HighchartsValueError: if other keyword arguments are misconfigured
"""
series_kwargs = validators.dict(series_kwargs, allow_empty = True) or {}
instance = cls(**series_kwargs)
instance.load_from_jira(project_key = project_key,
server = server,
jql = jql,
username = username,
password_or_token = password_or_token,
oauth_dict = oauth_dict,
client_kwargs = client_kwargs,
jira_client = jira_client,
connection_kwargs = connection_kwargs,
connection_callback = connection_callback)
return instance
@classmethod
def _data_collection_class(cls):
"""Returns the class object used for the data collection.
:rtype: :class:`DataPointCollection <highcharts_core.options.series.data.collections.DataPointCollection>`
descendent
"""
return GanttDataCollection
@classmethod
def _data_point_class(cls):
"""Returns the class object used for individual data points.
:rtype: :class:`DataBase <highcharts_core.options.series.data.base.DataBase>`
descendent
"""
return GanttData