exchangelib.services.update_item API documentation

# Module `exchangelib.services.update_item`

Expand source code

    from collections import OrderedDict

    from .common import EWSAccountService, to_item_id
    from ..ewsdatetime import EWSDate
    from ..fields import FieldPath, IndexedField
    from ..properties import ItemId
    from ..util import create_element, set_xml_value, MNS
    from ..version import EXCHANGE_2013_SP1

    class UpdateItem(EWSAccountService):
        """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation"""

        SERVICE_NAME = 'UpdateItem'
        element_container_name = '{%s}Items' % MNS

        def call(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
                 suppress_read_receipts):
            from ..items import CONFLICT_RESOLUTION_CHOICES, MESSAGE_DISPOSITION_CHOICES, \
                SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES, SEND_ONLY
            if conflict_resolution not in CONFLICT_RESOLUTION_CHOICES:
                raise ValueError("'conflict_resolution' %s must be one of %s" % (
                    conflict_resolution, CONFLICT_RESOLUTION_CHOICES
                ))
            if message_disposition not in MESSAGE_DISPOSITION_CHOICES:
                raise ValueError("'message_disposition' %s must be one of %s" % (
                    message_disposition, MESSAGE_DISPOSITION_CHOICES
                ))
            if send_meeting_invitations_or_cancellations not in SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES:
                raise ValueError("'send_meeting_invitations_or_cancellations' %s must be one of %s" % (
                    send_meeting_invitations_or_cancellations, SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES
                ))
            if suppress_read_receipts not in (True, False):
                raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts)
            if message_disposition == SEND_ONLY:
                raise ValueError('Cannot send-only existing objects. Use SendItem service instead')
            return self._elems_to_objs(self._chunked_get_elements(
                self.get_payload,
                items=items,
                conflict_resolution=conflict_resolution,
                message_disposition=message_disposition,
                send_meeting_invitations_or_cancellations=send_meeting_invitations_or_cancellations,
                suppress_read_receipts=suppress_read_receipts,
            ))

        def _elems_to_objs(self, elems):
            from ..items import Item
            for elem in elems:
                if isinstance(elem, (Exception, type(None))):
                    yield elem
                    continue
                yield Item.id_from_xml(elem)

        def _delete_item_elem(self, field_path):
            deleteitemfield = create_element('t:DeleteItemField')
            return set_xml_value(deleteitemfield, field_path, version=self.account.version)

        def _set_item_elem(self, item_model, field_path, value):
            setitemfield = create_element('t:SetItemField')
            set_xml_value(setitemfield, field_path, version=self.account.version)
            item_elem = create_element(item_model.request_tag())
            field_elem = field_path.field.to_xml(value, version=self.account.version)
            set_xml_value(item_elem, field_elem, version=self.account.version)
            setitemfield.append(item_elem)
            return setitemfield

        @staticmethod
        def _sorted_fields(item_model, fieldnames):
            # Take a list of fieldnames and return the (unique) fields in the order they are mentioned in item_class.FIELDS.
            # Checks that all fieldnames are valid.
            unique_fieldnames = list(OrderedDict.fromkeys(fieldnames))  # Make field names unique ,but keep ordering
            # Loop over FIELDS and not supported_fields(). Upstream should make sure not to update a non-supported field.
            for f in item_model.FIELDS:
                if f.name in unique_fieldnames:
                    unique_fieldnames.remove(f.name)
                    yield f
            if unique_fieldnames:
                raise ValueError("Field name(s) %s are not valid for a '%s' item" % (
                    ', '.join("'%s'" % f for f in unique_fieldnames), item_model.__name__))

        def _get_item_update_elems(self, item, fieldnames):
            from ..items import CalendarItem
            fieldnames_copy = list(fieldnames)

            if item.__class__ == CalendarItem:
                # For CalendarItem items where we update 'start' or 'end', we want to update internal timezone fields
                item.clean_timezone_fields(version=self.account.version)  # Possibly also sets timezone values
                for field_name in ('start', 'end'):
                    if field_name in fieldnames_copy:
                        tz_field_name = item.tz_field_for_field_name(field_name).name
                        if tz_field_name not in fieldnames_copy:
                            fieldnames_copy.append(tz_field_name)

            for field in self._sorted_fields(item_model=item.__class__, fieldnames=fieldnames_copy):
                if field.is_read_only:
                    raise ValueError('%s is a read-only field' % field.name)
                value = self._get_item_value(item, field)
                if value is None or (field.is_list and not value):
                    # A value of None or [] means we want to remove this field from the item
                    yield from self._get_delete_item_elems(field=field)
                else:
                    yield from self._get_set_item_elems(item_model=item.__class__, field=field, value=value)

        def _get_item_value(self, item, field):
            from ..items import CalendarItem
            value = field.clean(getattr(item, field.name), version=self.account.version)  # Make sure the value is OK
            if item.__class__ == CalendarItem:
                # For CalendarItem items where we update 'start' or 'end', we want to send values in the local timezone
                if field.name in ('start', 'end'):
                    if type(value) is EWSDate:
                        # EWS always expects a datetime
                        return item.date_to_datetime(field_name=field.name)
                    tz_field_name = item.tz_field_for_field_name(field.name).name
                    return value.astimezone(getattr(item, tz_field_name))
            return value

        def _get_delete_item_elems(self, field):
            if field.is_required or field.is_required_after_save:
                raise ValueError('%s is a required field and may not be deleted' % field.name)
            for field_path in FieldPath(field=field).expand(version=self.account.version):
                yield self._delete_item_elem(field_path=field_path)

        def _get_set_item_elems(self, item_model, field, value):
            if isinstance(field, IndexedField):
                # Generate either set or delete elements for all combinations of labels and subfields
                supported_labels = field.value_cls.get_field_by_fieldname('label')\
                    .supported_choices(version=self.account.version)
                seen_labels = set()
                subfields = field.value_cls.supported_fields(version=self.account.version)
                for v in value:
                    seen_labels.add(v.label)
                    for subfield in subfields:
                        field_path = FieldPath(field=field, label=v.label, subfield=subfield)
                        subfield_value = getattr(v, subfield.name)
                        if not subfield_value:
                            # Generate delete elements for blank subfield values
                            yield self._delete_item_elem(field_path=field_path)
                        else:
                            # Generate set elements for non-null subfield values
                            yield self._set_item_elem(
                                item_model=item_model,
                                field_path=field_path,
                                value=field.value_cls(**{'label': v.label, subfield.name: subfield_value}),
                            )
                    # Generate delete elements for all subfields of all labels not mentioned in the list of values
                    for label in (label for label in supported_labels if label not in seen_labels):
                        for subfield in subfields:
                            yield self._delete_item_elem(field_path=FieldPath(field=field, label=label, subfield=subfield))
            else:
                yield self._set_item_elem(item_model=item_model, field_path=FieldPath(field=field), value=value)

        def get_payload(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
                        suppress_read_receipts):
            # Takes a list of (Item, fieldnames) tuples where 'Item' is a instance of a subclass of Item and 'fieldnames'
            # are the attribute names that were updated. Returns the XML for an UpdateItem call.
            # an UpdateItem request.
            if self.account.version.build >= EXCHANGE_2013_SP1:
                updateitem = create_element(
                    'm:%s' % self.SERVICE_NAME,
                    attrs=OrderedDict([
                        ('ConflictResolution', conflict_resolution),
                        ('MessageDisposition', message_disposition),
                        ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
                        ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'),
                    ])
                )
            else:
                updateitem = create_element(
                    'm:%s' % self.SERVICE_NAME,
                    attrs=OrderedDict([
                        ('ConflictResolution', conflict_resolution),
                        ('MessageDisposition', message_disposition),
                        ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
                    ])
                )
            itemchanges = create_element('m:ItemChanges')
            version = self.account.version
            for item, fieldnames in items:
                if not item.account:
                    item.account = self.account
                if not fieldnames:
                    raise ValueError('"fieldnames" must not be empty')
                itemchange = create_element('t:ItemChange')
                set_xml_value(itemchange, to_item_id(item, ItemId, version=version), version=version)
                updates = create_element('t:Updates')
                for elem in self._get_item_update_elems(item=item, fieldnames=fieldnames):
                    updates.append(elem)
                itemchange.append(updates)
                itemchanges.append(itemchange)
            if not len(itemchanges):
                raise ValueError('"items" must not be empty')
            updateitem.append(itemchanges)
            return updateitem

## Classes

`class UpdateItem (*args, **kwargs)`

  MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation

  Expand source code

      class UpdateItem(EWSAccountService):
          """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation"""

          SERVICE_NAME = 'UpdateItem'
          element_container_name = '{%s}Items' % MNS

          def call(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
                   suppress_read_receipts):
              from ..items import CONFLICT_RESOLUTION_CHOICES, MESSAGE_DISPOSITION_CHOICES, \
                  SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES, SEND_ONLY
              if conflict_resolution not in CONFLICT_RESOLUTION_CHOICES:
                  raise ValueError("'conflict_resolution' %s must be one of %s" % (
                      conflict_resolution, CONFLICT_RESOLUTION_CHOICES
                  ))
              if message_disposition not in MESSAGE_DISPOSITION_CHOICES:
                  raise ValueError("'message_disposition' %s must be one of %s" % (
                      message_disposition, MESSAGE_DISPOSITION_CHOICES
                  ))
              if send_meeting_invitations_or_cancellations not in SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES:
                  raise ValueError("'send_meeting_invitations_or_cancellations' %s must be one of %s" % (
                      send_meeting_invitations_or_cancellations, SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES
                  ))
              if suppress_read_receipts not in (True, False):
                  raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts)
              if message_disposition == SEND_ONLY:
                  raise ValueError('Cannot send-only existing objects. Use SendItem service instead')
              return self._elems_to_objs(self._chunked_get_elements(
                  self.get_payload,
                  items=items,
                  conflict_resolution=conflict_resolution,
                  message_disposition=message_disposition,
                  send_meeting_invitations_or_cancellations=send_meeting_invitations_or_cancellations,
                  suppress_read_receipts=suppress_read_receipts,
              ))

          def _elems_to_objs(self, elems):
              from ..items import Item
              for elem in elems:
                  if isinstance(elem, (Exception, type(None))):
                      yield elem
                      continue
                  yield Item.id_from_xml(elem)

          def _delete_item_elem(self, field_path):
              deleteitemfield = create_element('t:DeleteItemField')
              return set_xml_value(deleteitemfield, field_path, version=self.account.version)

          def _set_item_elem(self, item_model, field_path, value):
              setitemfield = create_element('t:SetItemField')
              set_xml_value(setitemfield, field_path, version=self.account.version)
              item_elem = create_element(item_model.request_tag())
              field_elem = field_path.field.to_xml(value, version=self.account.version)
              set_xml_value(item_elem, field_elem, version=self.account.version)
              setitemfield.append(item_elem)
              return setitemfield

          @staticmethod
          def _sorted_fields(item_model, fieldnames):
              # Take a list of fieldnames and return the (unique) fields in the order they are mentioned in item_class.FIELDS.
              # Checks that all fieldnames are valid.
              unique_fieldnames = list(OrderedDict.fromkeys(fieldnames))  # Make field names unique ,but keep ordering
              # Loop over FIELDS and not supported_fields(). Upstream should make sure not to update a non-supported field.
              for f in item_model.FIELDS:
                  if f.name in unique_fieldnames:
                      unique_fieldnames.remove(f.name)
                      yield f
              if unique_fieldnames:
                  raise ValueError("Field name(s) %s are not valid for a '%s' item" % (
                      ', '.join("'%s'" % f for f in unique_fieldnames), item_model.__name__))

          def _get_item_update_elems(self, item, fieldnames):
              from ..items import CalendarItem
              fieldnames_copy = list(fieldnames)

              if item.__class__ == CalendarItem:
                  # For CalendarItem items where we update 'start' or 'end', we want to update internal timezone fields
                  item.clean_timezone_fields(version=self.account.version)  # Possibly also sets timezone values
                  for field_name in ('start', 'end'):
                      if field_name in fieldnames_copy:
                          tz_field_name = item.tz_field_for_field_name(field_name).name
                          if tz_field_name not in fieldnames_copy:
                              fieldnames_copy.append(tz_field_name)

              for field in self._sorted_fields(item_model=item.__class__, fieldnames=fieldnames_copy):
                  if field.is_read_only:
                      raise ValueError('%s is a read-only field' % field.name)
                  value = self._get_item_value(item, field)
                  if value is None or (field.is_list and not value):
                      # A value of None or [] means we want to remove this field from the item
                      yield from self._get_delete_item_elems(field=field)
                  else:
                      yield from self._get_set_item_elems(item_model=item.__class__, field=field, value=value)

          def _get_item_value(self, item, field):
              from ..items import CalendarItem
              value = field.clean(getattr(item, field.name), version=self.account.version)  # Make sure the value is OK
              if item.__class__ == CalendarItem:
                  # For CalendarItem items where we update 'start' or 'end', we want to send values in the local timezone
                  if field.name in ('start', 'end'):
                      if type(value) is EWSDate:
                          # EWS always expects a datetime
                          return item.date_to_datetime(field_name=field.name)
                      tz_field_name = item.tz_field_for_field_name(field.name).name
                      return value.astimezone(getattr(item, tz_field_name))
              return value

          def _get_delete_item_elems(self, field):
              if field.is_required or field.is_required_after_save:
                  raise ValueError('%s is a required field and may not be deleted' % field.name)
              for field_path in FieldPath(field=field).expand(version=self.account.version):
                  yield self._delete_item_elem(field_path=field_path)

          def _get_set_item_elems(self, item_model, field, value):
              if isinstance(field, IndexedField):
                  # Generate either set or delete elements for all combinations of labels and subfields
                  supported_labels = field.value_cls.get_field_by_fieldname('label')\
                      .supported_choices(version=self.account.version)
                  seen_labels = set()
                  subfields = field.value_cls.supported_fields(version=self.account.version)
                  for v in value:
                      seen_labels.add(v.label)
                      for subfield in subfields:
                          field_path = FieldPath(field=field, label=v.label, subfield=subfield)
                          subfield_value = getattr(v, subfield.name)
                          if not subfield_value:
                              # Generate delete elements for blank subfield values
                              yield self._delete_item_elem(field_path=field_path)
                          else:
                              # Generate set elements for non-null subfield values
                              yield self._set_item_elem(
                                  item_model=item_model,
                                  field_path=field_path,
                                  value=field.value_cls(**{'label': v.label, subfield.name: subfield_value}),
                              )
                      # Generate delete elements for all subfields of all labels not mentioned in the list of values
                      for label in (label for label in supported_labels if label not in seen_labels):
                          for subfield in subfields:
                              yield self._delete_item_elem(field_path=FieldPath(field=field, label=label, subfield=subfield))
              else:
                  yield self._set_item_elem(item_model=item_model, field_path=FieldPath(field=field), value=value)

          def get_payload(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
                          suppress_read_receipts):
              # Takes a list of (Item, fieldnames) tuples where 'Item' is a instance of a subclass of Item and 'fieldnames'
              # are the attribute names that were updated. Returns the XML for an UpdateItem call.
              # an UpdateItem request.
              if self.account.version.build >= EXCHANGE_2013_SP1:
                  updateitem = create_element(
                      'm:%s' % self.SERVICE_NAME,
                      attrs=OrderedDict([
                          ('ConflictResolution', conflict_resolution),
                          ('MessageDisposition', message_disposition),
                          ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
                          ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'),
                      ])
                  )
              else:
                  updateitem = create_element(
                      'm:%s' % self.SERVICE_NAME,
                      attrs=OrderedDict([
                          ('ConflictResolution', conflict_resolution),
                          ('MessageDisposition', message_disposition),
                          ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
                      ])
                  )
              itemchanges = create_element('m:ItemChanges')
              version = self.account.version
              for item, fieldnames in items:
                  if not item.account:
                      item.account = self.account
                  if not fieldnames:
                      raise ValueError('"fieldnames" must not be empty')
                  itemchange = create_element('t:ItemChange')
                  set_xml_value(itemchange, to_item_id(item, ItemId, version=version), version=version)
                  updates = create_element('t:Updates')
                  for elem in self._get_item_update_elems(item=item, fieldnames=fieldnames):
                      updates.append(elem)
                  itemchange.append(updates)
                  itemchanges.append(itemchange)
              if not len(itemchanges):
                  raise ValueError('"items" must not be empty')
              updateitem.append(itemchanges)
              return updateitem

  ### Ancestors

  - EWSAccountService
  - EWSService

  ### Class variables

  `var SERVICE_NAME`
  `var element_container_name`

  ### Methods

  `def call(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations, suppress_read_receipts)`
    Expand source code

        def call(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
                 suppress_read_receipts):
            from ..items import CONFLICT_RESOLUTION_CHOICES, MESSAGE_DISPOSITION_CHOICES, \
                SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES, SEND_ONLY
            if conflict_resolution not in CONFLICT_RESOLUTION_CHOICES:
                raise ValueError("'conflict_resolution' %s must be one of %s" % (
                    conflict_resolution, CONFLICT_RESOLUTION_CHOICES
                ))
            if message_disposition not in MESSAGE_DISPOSITION_CHOICES:
                raise ValueError("'message_disposition' %s must be one of %s" % (
                    message_disposition, MESSAGE_DISPOSITION_CHOICES
                ))
            if send_meeting_invitations_or_cancellations not in SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES:
                raise ValueError("'send_meeting_invitations_or_cancellations' %s must be one of %s" % (
                    send_meeting_invitations_or_cancellations, SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES
                ))
            if suppress_read_receipts not in (True, False):
                raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts)
            if message_disposition == SEND_ONLY:
                raise ValueError('Cannot send-only existing objects. Use SendItem service instead')
            return self._elems_to_objs(self._chunked_get_elements(
                self.get_payload,
                items=items,
                conflict_resolution=conflict_resolution,
                message_disposition=message_disposition,
                send_meeting_invitations_or_cancellations=send_meeting_invitations_or_cancellations,
                suppress_read_receipts=suppress_read_receipts,
            ))

  `def get_payload(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations, suppress_read_receipts)`
    Expand source code

        def get_payload(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
                        suppress_read_receipts):
            # Takes a list of (Item, fieldnames) tuples where 'Item' is a instance of a subclass of Item and 'fieldnames'
            # are the attribute names that were updated. Returns the XML for an UpdateItem call.
            # an UpdateItem request.
            if self.account.version.build >= EXCHANGE_2013_SP1:
                updateitem = create_element(
                    'm:%s' % self.SERVICE_NAME,
                    attrs=OrderedDict([
                        ('ConflictResolution', conflict_resolution),
                        ('MessageDisposition', message_disposition),
                        ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
                        ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'),
                    ])
                )
            else:
                updateitem = create_element(
                    'm:%s' % self.SERVICE_NAME,
                    attrs=OrderedDict([
                        ('ConflictResolution', conflict_resolution),
                        ('MessageDisposition', message_disposition),
                        ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
                    ])
                )
            itemchanges = create_element('m:ItemChanges')
            version = self.account.version
            for item, fieldnames in items:
                if not item.account:
                    item.account = self.account
                if not fieldnames:
                    raise ValueError('"fieldnames" must not be empty')
                itemchange = create_element('t:ItemChange')
                set_xml_value(itemchange, to_item_id(item, ItemId, version=version), version=version)
                updates = create_element('t:Updates')
                for elem in self._get_item_update_elems(item=item, fieldnames=fieldnames):
                    updates.append(elem)
                itemchange.append(updates)
                itemchanges.append(itemchange)
            if not len(itemchanges):
                raise ValueError('"items" must not be empty')
            updateitem.append(itemchanges)
            return updateitem

  ### Inherited members

  - `EWSAccountService`:
    - `NO_VALID_SERVER_VERSIONS`
    - `WARNINGS_TO_CATCH_IN_RESPONSE`
    - `get`
    - `parse`