def options(self, options_list): """ Set the options of the select list property if the property is a property model. Will raise an exception if the property is not a model. Will also raise an exception if the list is of incorrect form and contains non string, floats or integers. :param options_list: list of options to set, using only strings, integers and floats in the list. :raises: APIError, AssertionError Example ------- >>> a_model = project.model('Model') >>> select_list_property = a_model.property('a_select_list_property') Get the list of options on the property >>> select_list_property.options ['1', '2', '3', '4'] Update the list of options >>> select_list_property.options = [1,3,5,"wow"] """ if not isinstance(options_list, list): raise APIError("The list of options should be a list") if self._json_data.get('category') != 'MODEL': raise APIError( "We can only update the options list of the model of this property. The model of this " "property has id '{}'".format(self._json_data.get('model'))) # stringify the options list options_list = list(map(str, options_list)) if not self._options or set(options_list) != set(self._options): self._put_options(options_list=options_list) self._options = options_list
def add_with_properties(self, model, name=None, update_dict=None, bulk=True, **kwargs): """ Add a part and update its properties in one go. :param model: model of the part which to add a new instance, should follow the model tree in KE-chain :param name: (optional) name provided for the new instance as string otherwise use the name of the model :param update_dict: dictionary with keys being property names (str) and values being property values :param bulk: True to use the bulk_update_properties API endpoint for KE-chain versions later then 2.1.0b :param kwargs: (optional) additional keyword arguments that will be passed inside the update request :return: :class:`pykechain.models.Part` :raises: APIError, Raises `NotFoundError` when the property name is not a valid property of this part Examples -------- >>> bike = client.scope('Bike Project').part('Bike') >>> wheel_model = client.scope('Bike Project').model('Wheel') >>> bike.add_with_properties(wheel_model, 'Wooden Wheel', {'Spokes': 11, 'Material': 'Wood'}) """ # TODO: add test coverage for this method if self.category != Category.INSTANCE: raise APIError("Part should be of category INSTANCE") name = name or model.name action = 'new_instance_with_properties' properties_update_dict = dict([ (model.property(property_name).id, property_value) for property_name, property_value in update_dict.items() ]) # TODO: add bulk = False flags such that is used the old API (sequential) if bulk: r = self._client._request( 'POST', self._client._build_url('parts'), data=dict(name=name, model=model.id, parent=self.id, properties=json.dumps(properties_update_dict), **kwargs), params=dict(select_action=action)) if r.status_code != requests.codes.created: # pragma: no cover raise APIError('{}: {}'.format(str(r), r.content)) return Part(r.json()['results'][0], client=self._client) else: # do the old way new_part = self.add(model, name=name) # type: Part new_part.update(update_dict=update_dict, bulk=bulk) return new_part
def _update_scope_project_team(self, select_action, user, user_type): """ Update the Project Team of the Scope. Updates include addition or removing of managers or members. :param select_action: type of action to be applied :param user: the username of the user to which the action applies to :param user_type: the type of the user (member or manager) """ if isinstance(user, str): users = self._client._retrieve_users() manager_object = next( (item for item in users['results'] if item["username"] == user), None) if manager_object: url = self._client._build_url('scope', scope_id=self.id) r = self._client._request( 'PUT', url, params={'select_action': select_action}, data={'user_id': manager_object['pk']}) if r.status_code != requests.codes.ok: # pragma: no cover raise APIError("Could not {} {} in Scope".format( select_action.split('_')[0], user_type)) else: raise NotFoundError("User {} does not exist".format(user)) else: raise TypeError( "User {} should be defined as a string".format(user))
def delete(self): """Delete this activity.""" r = self._client._request( 'DELETE', self._client._build_url('activity', activity_id=self.id)) if r.status_code != requests.codes.no_content: raise APIError("Could not delete activity: {} with id {}".format( self.name, self.id))
def _put_value(self, value): url = self._client._build_url('property', property_id=self.id) r = self._client._request('PUT', url, json={'value': value}) if r.status_code != requests.codes.ok: # pragma: no cover raise APIError("Could not update property value") return r.json()['results'][0]['value']
def _download(self): url = self._client._build_url('property_download', property_id=self.id) r = self._client._request('GET', url) if r.status_code != requests.codes.ok: raise APIError("Could not download property value") return r
def _upload(self, data): url = self._client._build_url('property_upload', property_id=self.id) r = self._client._request('POST', url, data={"part": self._json_data['part']}, files={"attachment": data}) if r.status_code != requests.codes.ok: raise APIError("Could not upload attachment")
def delete(self): """Delete this property. :return: None :raises: APIError if delete was not successful """ r = self._client._request( 'DELETE', self._client._build_url('property', property_id=self.id)) if r.status_code != requests.codes.no_content: # pragma: no cover raise APIError("Could not delete property: {} with id {}".format( self.name, self.id))
def add_property(self, *args, **kwargs): # type: (*Any, **Any) -> Property """Add a new property to this model. See :class:`pykechain.Client.create_property` for available parameters. :return: Property """ if self.category != Category.MODEL: raise APIError("Part should be of category MODEL") return self._client.create_property(self, *args, **kwargs)
def edit(self, name=None, description=None, **kwargs): # type: (AnyStr, AnyStr, Any) -> None """ Edit the details of a part (model or instance). For an instance you can edit the Part instance name and the part instance description :param name: optional name of the part to edit :param description: (optional) description of the part :param kwargs: (optional) additional kwargs that will be passed in the during the edit/update request :return: None :raises: APIError Example ------- For changing a part: >>> front_fork = project.part('Front Fork') >>> front_fork.edit(name='Front Fork - updated') >>> front_fork.edit(name='Front Fork cruizer', description='With my ragtop down so my hair can blow' ) for changing a model: >>> front_fork = project.model('Front Fork') >>> front_fork.edit(name='Front Fork basemodel', description='Some description here') """ update_dict = {'id': self.id} if name: if not isinstance(name, str): raise IllegalArgumentError( "name should be provided as a string") update_dict.update({'name': name}) if description: if not isinstance(description, str): raise IllegalArgumentError( "description should be provided as a string") update_dict.update({'description': description}) # TODO: What else can we edit? Don't know how to test for it, so for now I have decide to skip it in the tests if kwargs: # pragma: no cover update_dict.update(**kwargs) r = self._client._request('PUT', self._client._build_url('part', part_id=self.id), json=update_dict) if r.status_code != requests.codes.ok: # pragma: no cover raise APIError("Could not update Part ({})".format(r)) if name: self.name = name
def delete(self): # type: () -> None """Delete this part. :return: None :raises: APIError if delete was not succesfull """ r = self._client._request( 'DELETE', self._client._build_url('part', part_id=self.id)) if r.status_code != requests.codes.no_content: # pragma: no cover raise APIError("Could not delete part: {} with id {}".format( self.name, self.id))
def order_properties(self, property_list=None): """ Order the properties of a part model using a list of property objects or property names. Examples -------- >>> front_fork = client.scope('Bike Project').model('Front Fork') >>> front_fork.order_properties(['Material', 'Height (mm)', 'Color']) >>> front_fork = client.scope('Bike Project').model('Front Fork') >>> material = front_fork.property('Material') >>> height = front_fork.property('Height (mm)') >>> color = front_fork.property('Color') >>> front_fork.order_properties([material, height, color]) """ if self.category != Category.MODEL: raise APIError("Part should be of category MODEL") if not isinstance(property_list, list): raise TypeError( 'Expected a list of strings or Property() objects, got a {} object' .format(type(property_list))) order_dict = dict() for prop in property_list: if isinstance(prop, (str, text_type)): order_dict[self.property( name=prop).id] = property_list.index(prop) else: order_dict[prop.id] = property_list.index(prop) r = self._client._request( 'PUT', self._client._build_url('part', part_id=self.id), data=dict(property_order=json.dumps(order_dict))) if r.status_code != requests.codes.ok: # pragma: no cover raise APIError("Could not reorder properties")
def update(self, name=None, update_dict=None, bulk=True, **kwargs): """ Edit part name and property values in one go. :param name: new part name (defined as a string) :param update_dict: dictionary with keys being property names (str) and values being property values :param bulk: True to use the bulk_update_properties API endpoint for KE-chain versions later then 2.1.0b :param kwargs: (optional) additional keyword arguments that will be passed inside the update request :return: :class:`pykechain.models.Part` :raises: APIError, Raises `NotFoundError` when the property name is not a valid property of this part Example ------- >>> bike = client.scope('Bike Project').part('Bike') >>> bike.update(name='Good name', update_dict={'Gears': 11, 'Total Height': 56.3}, bulk=True) """ # new for 1.5 and KE-chain 2 (released after 14 march 2017) is the 'bulk_update_properties' action on the api # lets first use this one. # dict(name=name, properties=json.dumps(update_dict))) with property ids:value action = 'bulk_update_properties' url = self._client._build_url('part', part_id=self.id) request_body = dict([ (self.property(property_name).id, property_value) for property_name, property_value in update_dict.items() ]) if bulk and len(update_dict.keys()) > 1: if name: if not isinstance(name, str): raise IllegalArgumentError( "Name of the part should be provided as a string") r = self._client._request('PUT', self._client._build_url('part', part_id=self.id), data=dict( name=name, properties=json.dumps(request_body), **kwargs), params=dict(select_action=action)) if r.status_code != requests.codes.ok: # pragma: no cover raise APIError('{}: {}'.format(str(r), r.content)) else: for property_name, property_value in update_dict.items(): self.property(property_name).value = property_value
def edit(self, name=None, description=None, unit=None): # type: (AnyStr, AnyStr, AnyStr) -> None """ Edit the details of a property (model). :param name: (optional) new name of the property to edit :param description: (optional) new description of the property :param unit: (optional) new unit of the property :return: None :raises: APIError Example ------- >>> front_fork = project.part('Front Fork') >>> color_property = front_fork.property(name='Color') >>> color_property.edit(name='Shade', description='Could also be called tint, depending on mixture', >>> unit='RGB') """ update_dict = {'id': self.id} if name: if not isinstance(name, (str, text_type)): raise TypeError( "name should be provided as a string, was provided as '{}'" .format(type(name))) update_dict.update({'name': name}) self.name = name if description: if not isinstance(description, (str, text_type)): raise TypeError( "description should be provided as a string, was provided as '{}'" .format(type(description))) update_dict.update({'description': description}) if unit: if not isinstance(unit, (str, text_type)): raise TypeError( "unit should be provided as a string, was provided as '{}'" .format(type(unit))) update_dict.update({'unit': unit}) r = self._client._request('PUT', self._client._build_url('property', property_id=self.id), json=update_dict) if r.status_code != requests.codes.ok: # pragma: no cover raise APIError("Could not update Property ({})".format(r))
def _put_options(self, options_list): """Save the options to the database. Will check for the correct form of the data. :param options_list: list of options to set. :raises APIError """ url = self._client._build_url('property', property_id=self.id) response = self._client._request( 'PUT', url, json={'options': { "value_choices": options_list }}) if response.status_code != 200: # pragma: no cover raise APIError( "Could not update property value. Response: {}".format( str(response)))
def add_to(self, parent, **kwargs): # type: (Part, **Any) -> Part """Add a new instance of this model to a part. :param parent: part to add the new instance to :return: :class:`pykechain.models.Part` :raises: APIError Example ------- >>> wheel_model = project.model('wheel') >>> bike = project.part('Bike') >>> wheel_model.add_to(bike) """ if self.category != Category.MODEL: raise APIError("Part should be of category MODEL") return self._client.create_part(parent, self, **kwargs)
def configure(self, inputs, outputs): """Configure activity input and output. :param inputs: iterable of input property models :param outputs: iterable of output property models :raises: APIError """ url = self._client._build_url('activity', activity_id=self.id) r = self._client._request( 'PUT', url, params={'select_action': 'update_associations'}, json={ 'inputs': [p.id for p in inputs], 'outputs': [p.id for p in outputs] }) if r.status_code != requests.codes.ok: # pragma: no cover raise APIError("Could not configure activity")
def add(self, model, **kwargs): # type: (Part, **Any) -> Part """Add a new child to this part. This can only act on instances :param model: model to use for the child :return: :class:`pykechain.models.Part` :raises: APIError Example ------- >>> bike = project.part('Bike') >>> wheel_model = project.model('Wheel') >>> bike.add(wheel_model) """ if self.category != Category.INSTANCE: raise APIError("Part should be of category INSTANCE") return self._client.create_part(self, model, **kwargs)
def edit(self, name=None, description=None, start_date=None, due_date=None, assignees=None, status=None): """Edit the details of an activity. :param name: (optionally) edit the name of the activity :param description: (optionally) edit the description of the activity :param start_date: (optionally) edit the start date of the activity as a datetime object (UTC time/timezone aware preferred) :param due_date: (optionally) edit the due_date of the activity as a datetime object (UTC time/timzeone aware preferred) :param assignees: (optionally) edit the assignees of the activity as a list, will overwrite all assignees :param status: (optionally) edit the status of the activity as a string :return: None :raises: NotFoundError, TypeError, APIError Example ------- >>> from datetime import datetime >>> specify_wheel_diameter = project.activity('Specify wheel diameter') >>> specify_wheel_diameter.edit(name='Specify wheel diameter and circumference', ... description='The diameter and circumference are specified in inches', ... start_date=datetime.utcnow(), # naive time is interpreted as UTC time ... assignee='testuser') If we want to provide timezone aware datetime objects we can use the 3rd party convenience library `pytz`. Mind that we need to fetch the timezone first and use `<timezone>.localize(<your datetime>)` to make it work correctly. Using datetime(2017,6,1,23,59,0 tzinfo=<tz>) does NOT work for most timezones with a daylight saving time. Check the pytz documentation. (see http://pythonhosted.org/pytz/#localized-times-and-date-arithmetic) >>> import pytz >>> start_date_tzaware = datetime.now(pytz.utc) >>> mytimezone = pytz.timezone('Europe/Amsterdam') >>> due_date_tzaware = mytimezone.localize(datetime(2019, 10, 27, 23, 59, 0)) >>> specify_wheel_diameter.edit(due_date=due_date_tzaware, start_date=start_date_tzaware) """ update_dict = {'id': self.id} if name: if isinstance(name, (str, text_type)): update_dict.update({'name': name}) self.name = name else: raise TypeError('Name should be a string') if description: if isinstance(description, (str, text_type)): update_dict.update({'description': description}) self.description = description else: raise TypeError('Description should be a string') if start_date: if isinstance(start_date, datetime.datetime): if not start_date.tzinfo: warnings.warn( "The startdate '{}' is naive and not timezone aware, use pytz.timezone info. " "This date is interpreted as UTC time.".format( start_date.isoformat(sep=' '))) update_dict.update( {'start_date': start_date.isoformat(sep='T')}) else: raise TypeError( 'Start date should be a datetime.datetime() object') if due_date: if isinstance(due_date, datetime.datetime): if not due_date.tzinfo: warnings.warn( "The duedate '{}' is naive and not timezone aware, use pytz.timezone info. " "This date is interpreted as UTC time.".format( due_date.isoformat(sep=' '))) update_dict.update({'due_date': due_date.isoformat(sep='T')}) else: raise TypeError( 'Due date should be a datetime.datetime() object') if assignees: if isinstance(assignees, list): project = self._client.scope(self._json_data['scope']['name']) members_list = [ member['username'] for member in project._json_data['members'] ] for assignee in assignees: if assignee not in members_list: raise NotFoundError( "Assignee '{}' should be a member of the scope". format(assignee)) update_dict.update({'assignees': assignees}) else: raise TypeError('Assignees should be a list') if status: if isinstance(status, (str, text_type)): update_dict.update({'status': status}) else: raise TypeError('Status should be a string') url = self._client._build_url('activity', activity_id=self.id) r = self._client._request('PUT', url, json=update_dict) if r.status_code != requests.codes.ok: # pragma: no cover raise APIError("Could not update Activity ({})".format(r)) if status: self._json_data['status'] = str(status) if assignees: self._json_data['assignees'] = assignees if due_date: self._json_data['due_date'] = str(due_date) if start_date: self._json_data['start_date'] = str(start_date)