Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

617 bulk widget creation #619

Merged
merged 7 commits into from
Dec 3, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/python_testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ jobs:
pip install pytest betamax pytest-runner pytest-cov coverage
pytest --cov=pykechain tests

- name: Upload coverage to coveralls.io
if: matrix.python-version == 3.6
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
run: |
pip install coveralls
coveralls
# - name: Upload coverage to coveralls.io
# if: matrix.python-version == 3.6
# env:
# COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
# run: |
# pip install coveralls
# coveralls

- name: Check docs and distribution
if: matrix.python-version == 3.6
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Change Log
---------------
* Bugfix: `Part.scope()` retrieves the part's scope regardless of its status.
* Improved `Client` exception messages when retrieving singular objects, e.g. client.scope()
* Added bulk widget creation and editing of widgets.

3.1.5 (29NOV19)
---------------
Expand Down
272 changes: 185 additions & 87 deletions pykechain/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2059,46 +2059,16 @@ def create_team(self, name, user, description=None, options=None, is_hidden=Fals
new_team.refresh()
return new_team

def create_widget(self, activity, widget_type, meta, title=None, order=None,
parent=None, readable_models=None, writable_models=None, **kwargs):
# type: (Union[Activity,text_type], text_type, Optional[text_type], Optional[Text], Optional[int], Optional[Widget, text_type], Optional[List], Optional[List], **Any) -> Widget # noqa:E501
"""
Create a widget inside an activity.

If you want to associate models (and instances) in a single go, you may provide a list of `Property`
(models) to the `readable_model_ids` or `writable_model_ids`.

Alternatively you can use the alias, `inputs` and `outputs` which connect to respectively
`readable_model_ids` and `writable_models_ids`.
def _validate_widget(self, activity, widget_type, title, meta, order, parent, readable_models, writable_models,
**kwargs):

:param activity: activity objects to create the widget in.
:type activity: :class:`Activity` or UUID
:param widget_type: type of the widget, one of :class:`WidgetTypes`
:type: string
:param meta: meta dictionary of the widget.
:type meta: dict
:param title: (optional) title of the widget
:type title: str or None
:param order: (optional) order in the activity of the widget.
:type order: int or None
:param parent: (optional) parent of the widget for Multicolumn and Multirow widget.
:type parent: :class:`Widget` or UUID
:param readable_models: (optional) list of property model ids to be configured as readable (alias = inputs)
:type readable_models: list of properties or list of property id's
:param writable_models: (optional) list of property model ids to be configured as writable (alias = ouputs)
:type writable_models: list of properties or list of property id's
:param kwargs: (optional) additional keyword=value arguments to create widget
:return: the created subclass of :class:`Widget`
:rtype: :class:`Widget`
:raises IllegalArgumentError: when an illegal argument is send.
:raises APIError: when an API Error occurs.
"""
if isinstance(activity, (Activity, Activity2)):
activity = activity.id
elif is_uuid(activity):
pass
else:
raise IllegalArgumentError("`activity` should be either an `Activity` or a uuid.")

if not isinstance(widget_type, (string_types, text_type)) and widget_type not in WidgetTypes.values():
raise IllegalArgumentError("`widget_type` should be one of '{}'".format(WidgetTypes.values()))
if order is not None and not isinstance(order, int):
Expand All @@ -2114,6 +2084,44 @@ def create_widget(self, activity, widget_type, meta, title=None, order=None,
parent = parent
elif parent is not None:
raise IllegalArgumentError("`parent` should be provided as a widget or uuid")

data = dict(
activity_id=activity,
widget_type=widget_type,
title=title,
meta=meta,
parent_id=parent
)

if not title:
data.pop('title')

if order is not None:
data.update(dict(order=order))

if kwargs:
data.update(**kwargs)

readable_model_ids, writable_model_ids = self._validate_related_models(
readable_models=readable_models,
writable_models=writable_models,
**kwargs,
)

return data, readable_model_ids, writable_model_ids

@staticmethod
def _validate_related_models(readable_models, writable_models, **kwargs):
# type: (List, List, **Any) -> Tuple[List, List]
"""
Verify the format and content of the readable and writable models.

:param readable_models: list of Properties or UUIDs
:param writable_models: list of Properties or UUIDs
:param kwargs: option to insert "inputs" and "outputs", instead of new inputs.
:return: Tuple with both input lists, now with only UUIDs
:rtype Tuple[List, List]
"""
if kwargs.get('inputs'):
readable_models = kwargs.pop('inputs')
if kwargs.get('outputs'):
Expand Down Expand Up @@ -2143,23 +2151,54 @@ def create_widget(self, activity, widget_type, meta, title=None, order=None,
else:
IllegalArgumentError("`writable_models` should be provided as a list of uuids or property models")

data = dict(
activity_id=activity,
return readable_model_ids, writable_model_ids

def create_widget(self, activity, widget_type, meta, title=None, order=None,
parent=None, readable_models=None, writable_models=None, **kwargs):
# type: (Union[Activity,text_type], text_type, Optional[text_type], Optional[Text], Optional[int], Optional[Widget, text_type], Optional[List], Optional[List], **Any) -> Widget # noqa:E501
"""
Create a widget inside an activity.

If you want to associate models (and instances) in a single go, you may provide a list of `Property`
(models) to the `readable_model_ids` or `writable_model_ids`.

Alternatively you can use the alias, `inputs` and `outputs` which connect to respectively
`readable_model_ids` and `writable_models_ids`.

:param activity: activity objects to create the widget in.
:type activity: :class:`Activity` or UUID
:param widget_type: type of the widget, one of :class:`WidgetTypes`
:type: string
:param meta: meta dictionary of the widget.
:type meta: dict
:param title: (optional) title of the widget
:type title: str or None
:param order: (optional) order in the activity of the widget.
:type order: int or None
:param parent: (optional) parent of the widget for Multicolumn and Multirow widget.
:type parent: :class:`Widget` or UUID
:param readable_models: (optional) list of property model ids to be configured as readable (alias = inputs)
:type readable_models: list of properties or list of property id's
:param writable_models: (optional) list of property model ids to be configured as writable (alias = ouputs)
:type writable_models: list of properties or list of property id's
:param kwargs: (optional) additional keyword=value arguments to create widget
:return: the created subclass of :class:`Widget`
:rtype: :class:`Widget`
:raises IllegalArgumentError: when an illegal argument is send.
:raises APIError: when an API Error occurs.
"""
data, readable_model_ids, writable_model_ids = self._validate_widget(
activity=activity,
widget_type=widget_type,
title=title,
meta=meta,
parent_id=parent
order=order,
parent=parent,
readable_models=readable_models,
writable_models=writable_models,
**kwargs
)

if not title:
data.pop('title')

if kwargs:
data.update(**kwargs)

if order is not None:
data.update(dict(order=order))

# perform the call
url = self._build_url('widgets')
response = self._request('POST', url, params=API_EXTRA_PARAMS['widgets'], json=data)
Expand All @@ -2176,6 +2215,49 @@ def create_widget(self, activity, widget_type, meta, title=None, order=None,

return widget

def create_widgets(self, widgets, **kwargs):
# type: (List[Dict], **Any) -> List[Widget]
"""
Bulk-create of widgets.

:param widgets: list of dictionaries defining the configuration of the widget.
:type widgets: List[Dict]
:return: list of `Widget` objects
:rtype List[Widget]
"""
bulk_data = list()
bulk_associations = list()
for widget in widgets:
data, readable_model_ids, writable_model_ids = self._validate_widget(
activity=widget.get('activity'),
widget_type=widget.get('widget_type'),
title=widget.get('title'),
meta=widget.get('meta'),
order=widget.get('order'),
parent=widget.get('parent'),
readable_models=widget.get('readable_models'),
writable_models=widget.get('writable_models'),
**widget.pop('kwargs', dict()),
)
bulk_data.append(data)
bulk_associations.append((readable_model_ids, writable_model_ids))

url = self._build_url('widgets_bulk_create')
response = self._request('POST', url, params=API_EXTRA_PARAMS['widgets'], json=bulk_data)

if response.status_code != requests.codes.created: # pragma: no cover
raise APIError("Could not create a widgets ({})\n\n{}".format(response, response.json().get('traceback')))

# create the widget and do postprocessing
widgets = list()
for widget_response in response.json().get('results')[0]:
widget = Widget.create(json=widget_response, client=self)
widgets.append(widget)

self.update_widgets_associations(widgets=widgets, associations=bulk_associations, **kwargs)

return widgets

def update_widget_associations(self, widget, readable_models=None, writable_models=None, **kwargs):
# type: (Union[Widget,text_type], Optional[List], Optional[List], **Any) -> None
"""
Expand All @@ -2196,56 +2278,72 @@ def update_widget_associations(self, widget, readable_models=None, writable_mode
:raises APIError: when the associations could not be changed
:raise IllegalArgumentError: when the list is not of the right type
"""
if isinstance(widget, Widget):
widget_id = widget.id
elif is_uuid(widget):
widget_id = widget
else:
raise IllegalArgumentError("`widget` should be provided as a Widget or a uuid")
self.update_widgets_associations(
widgets=[widget],
associations=[(readable_models, writable_models)],
**kwargs
)

if kwargs.get('inputs'):
readable_models = kwargs.pop('inputs')
if kwargs.get('outputs'):
writable_models = kwargs.pop('outputs')
def update_widgets_associations(self, widgets, associations, **kwargs):
# type: (List[Union[Widget, text_type]], List[Tuple[List, List]], **Any) -> None
"""
Update associations on multiple widgets in bulk.

readable_model_properties_ids = []
if readable_models is not None:
if not isinstance(readable_models, (list, tuple, set)):
raise IllegalArgumentError("`readable_models` should be provided as a list of uuids or property models")
for input in readable_models:
if is_uuid(input):
readable_model_properties_ids.append(input)
elif isinstance(input, (Property2, Property)):
readable_model_properties_ids.append(input.id)
else:
IllegalArgumentError("`readable_models` should be provided as a list of uuids or property models")
This is an absolute list of associations. If no property model id's are provided, then the associations are
emptied out and replaced with no associations.

writable_model_properties_ids = []
if writable_models is not None:
if not isinstance(writable_models, (list, tuple, set)):
raise IllegalArgumentError("`writable_models` should be provided as a list of uuids or property models")
for output in writable_models:
if is_uuid(output):
writable_model_properties_ids.append(output)
elif isinstance(output, (Property2, Property)):
writable_model_properties_ids.append(output.id)
else:
IllegalArgumentError("`writable_models` should be provided as a list of uuids or property models")
data = dict(
id=widget_id,
readable_model_properties_ids=readable_model_properties_ids,
writable_model_properties_ids=writable_model_properties_ids
)
:param widgets: list of widgets to update associations for
:type widgets: :class: list
:param associations: list of tuples, each tuple containing 2 lists of properties
(of :class:`Property` or property_ids (uuids)
:type associations: List[Tuple]
:return: None
:raises APIError: when the associations could not be changed
:raise IllegalArgumentError: when the list is not of the right type
"""
if not isinstance(widgets, List):
raise IllegalArgumentError("`widgets` must be provided as a list of widgets.")

if kwargs:
data.update(**kwargs)
widget_ids = list()
for widget in widgets:
if isinstance(widget, Widget):
widget_id = widget.id
elif is_uuid(widget):
widget_id = widget
else:
raise IllegalArgumentError("Each widget should be provided as a Widget or a uuid")
widget_ids.append(widget_id)

if not isinstance(associations, List) and all(isinstance(a, tuple) and len(a) == 2 for a in associations):
raise IllegalArgumentError(
'`associations` must be a list of tuples, defining the readable and writable models per widget.')

bulk_data = list()
for widget_id, association in zip(widget_ids, associations):
readable_models, writable_models = association

readable_model_ids, writable_model_ids = self._validate_related_models(
readable_models=readable_models,
writable_models=writable_models,
)

data = dict(
id=widget_id,
readable_model_properties_ids=readable_model_ids,
writable_model_properties_ids=writable_model_ids,
)

if kwargs:
data.update(**kwargs)

bulk_data.append(data)

# perform the call
url = self._build_url('widget_update_associations', widget_id=widget_id)
response = self._request('PUT', url, params=API_EXTRA_PARAMS['widget'], json=data)
url = self._build_url('widgets_update_associations')
response = self._request('PUT', url, params=API_EXTRA_PARAMS['widgets'], json=bulk_data)

if response.status_code != requests.codes.ok: # pragma: no cover
raise APIError("Could not update associations of the widget ({})".format((response, response.json())))
raise APIError("Could not update associations of the widgets ({})".format((response, response.json())))

return None

Expand Down
5 changes: 3 additions & 2 deletions pykechain/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@
'property2_download': 'api/v3/properties/{property_id}/download',
'widgets': 'api/widgets.json',
'widget': 'api/widgets/{widget_id}.json',
'widgets_update_associations': 'api/widgets/update_associations.json',
'widgets_update_associations': 'api/widgets/bulk_update_associations.json',
'widget_update_associations': 'api/widgets/{widget_id}/update_associations.json',
'widgets_bulk_create': 'api/widgets/bulk_create',
'widgets_bulk_delete': 'api/widgets/bulk_delete',
'widgets_bulk_update': 'api/widgets/bulk_update',
'widgets_schemas': 'api/widgets/schemas',
Expand Down Expand Up @@ -110,5 +111,5 @@
'parent_id', 'progress', 'has_subwidgets', 'scope_id'])},
'widget': {'fields': ",".join(
['id', 'name', 'ref', 'created_at', 'updated_at', 'title', 'widget_type', 'meta', 'order', 'activity_id',
'parent_id', 'progress', 'has_subwidgets', 'scope_id'])}
'parent_id', 'progress', 'has_subwidgets', 'scope_id'])},
}
7 changes: 6 additions & 1 deletion pykechain/models/widgets/widgets_manager.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import warnings
from typing import Sized, Any, Iterable, Union, AnyStr, Optional, Text, Dict
from typing import Sized, Any, Iterable, Union, AnyStr, Optional, Text, Dict, List

import requests
from six import string_types, text_type
Expand Down Expand Up @@ -91,6 +91,11 @@ def __getitem__(self, key):
return found
raise NotFoundError("Could not find widget with index, title, ref, or id '{}'".format(key))

def create_widgets(self, widgets):
# type: (List[Dict]) -> List[Widget]
"""Bulk creation of widgets."""
return self._client.create_widgets(widgets=widgets)

def create_widget(self, *args, **kwargs):
# type: (*Any, **Any) -> Widget
"""Create a widget inside an activity.
Expand Down
Loading