Module exchangelib.services.get_streaming_events
Expand source code
import logging
from .common import EWSAccountService, add_xml_child
from ..properties import Notification
from ..util import create_element, get_xml_attr, get_xml_attrs, MNS, DocumentYielder, DummyResponse
log = logging.getLogger(__name__)
xml_log = logging.getLogger('%s.xml' % __name__)
class GetStreamingEvents(EWSAccountService):
"""MSDN:
https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getstreamingevents-operation
"""
SERVICE_NAME = 'GetStreamingEvents'
element_container_name = '{%s}Notifications' % MNS
streaming = True
prefer_affinity = True
# Connection status values
OK = 'OK'
CLOSED = 'Closed'
def __init__(self, *args, **kwargs):
# These values are set each time call() is consumed
self.connection_status = None
self.error_subscription_ids = []
super().__init__(*args, **kwargs)
def call(self, subscription_ids, connection_timeout):
if connection_timeout < 1:
raise ValueError("'connection_timeout' must be a positive integer")
return self._elems_to_objs(self._get_elements(payload=self.get_payload(
subscription_ids=subscription_ids, connection_timeout=connection_timeout,
)))
def _elems_to_objs(self, elems):
for elem in elems:
if isinstance(elem, Exception):
yield elem
continue
yield Notification.from_xml(elem=elem, account=None)
@classmethod
def _get_soap_parts(cls, response, **parse_opts):
# Pass the response unaltered. We want to use our custom document yielder
return None, response
def _get_soap_messages(self, body, **parse_opts):
# 'body' is actually the raw response passed on by '_get_soap_parts'. We want to continuously read the content,
# looking for complete XML documents. When we have a full document, we want to parse it as if it was a normal
# XML response.
r = body
for i, doc in enumerate(DocumentYielder(r.iter_content()), start=1):
xml_log.debug('''Response XML (docs received: %(i)s): %(xml_response)s''', dict(i=i, xml_response=doc))
response = DummyResponse(url=None, headers=None, request_headers=None, content=doc)
try:
_, body = super()._get_soap_parts(response=response, **parse_opts)
except Exception:
r.close() # Release memory
raise
# TODO: We're skipping ._update_api_version() here because we don't have access to the 'api_version' used.
# TODO: We should be doing a lot of error handling for ._get_soap_messages().
yield from super()._get_soap_messages(body=body, **parse_opts)
if self.connection_status == self.CLOSED:
# Don't wait for the TCP connection to timeout
break
def _get_element_container(self, message, name=None):
error_ids_elem = message.find('{%s}ErrorSubscriptionIds' % MNS)
if error_ids_elem is not None:
self.error_subscription_ids = get_xml_attrs(error_ids_elem, '{%s}ErrorSubscriptionId' % MNS)
log.debug('These subscription IDs are invalid: %s', self.error_subscription_ids)
self.connection_status = get_xml_attr(message, '{%s}ConnectionStatus' % MNS) # Either 'OK' or 'Closed'
log.debug('Connection status is: %s', self.connection_status)
# Upstream expects to find a 'name' tag but our response does not always have it. Return an empty element.
if message.find(name) is None:
return []
return super()._get_element_container(message=message, name=name)
def get_payload(self, subscription_ids, connection_timeout):
getstreamingevents = create_element('m:%s' % self.SERVICE_NAME)
subscriptions_elem = create_element('m:SubscriptionIds')
for subscription_id in subscription_ids:
add_xml_child(subscriptions_elem, 't:SubscriptionId', subscription_id)
if not len(subscriptions_elem):
raise ValueError('"subscription_ids" must not be empty')
getstreamingevents.append(subscriptions_elem)
add_xml_child(getstreamingevents, 'm:ConnectionTimeout', connection_timeout)
return getstreamingevents
Classes
class GetStreamingEvents (*args, **kwargs)
-
Expand source code
class GetStreamingEvents(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getstreamingevents-operation """ SERVICE_NAME = 'GetStreamingEvents' element_container_name = '{%s}Notifications' % MNS streaming = True prefer_affinity = True # Connection status values OK = 'OK' CLOSED = 'Closed' def __init__(self, *args, **kwargs): # These values are set each time call() is consumed self.connection_status = None self.error_subscription_ids = [] super().__init__(*args, **kwargs) def call(self, subscription_ids, connection_timeout): if connection_timeout < 1: raise ValueError("'connection_timeout' must be a positive integer") return self._elems_to_objs(self._get_elements(payload=self.get_payload( subscription_ids=subscription_ids, connection_timeout=connection_timeout, ))) def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue yield Notification.from_xml(elem=elem, account=None) @classmethod def _get_soap_parts(cls, response, **parse_opts): # Pass the response unaltered. We want to use our custom document yielder return None, response def _get_soap_messages(self, body, **parse_opts): # 'body' is actually the raw response passed on by '_get_soap_parts'. We want to continuously read the content, # looking for complete XML documents. When we have a full document, we want to parse it as if it was a normal # XML response. r = body for i, doc in enumerate(DocumentYielder(r.iter_content()), start=1): xml_log.debug('''Response XML (docs received: %(i)s): %(xml_response)s''', dict(i=i, xml_response=doc)) response = DummyResponse(url=None, headers=None, request_headers=None, content=doc) try: _, body = super()._get_soap_parts(response=response, **parse_opts) except Exception: r.close() # Release memory raise # TODO: We're skipping ._update_api_version() here because we don't have access to the 'api_version' used. # TODO: We should be doing a lot of error handling for ._get_soap_messages(). yield from super()._get_soap_messages(body=body, **parse_opts) if self.connection_status == self.CLOSED: # Don't wait for the TCP connection to timeout break def _get_element_container(self, message, name=None): error_ids_elem = message.find('{%s}ErrorSubscriptionIds' % MNS) if error_ids_elem is not None: self.error_subscription_ids = get_xml_attrs(error_ids_elem, '{%s}ErrorSubscriptionId' % MNS) log.debug('These subscription IDs are invalid: %s', self.error_subscription_ids) self.connection_status = get_xml_attr(message, '{%s}ConnectionStatus' % MNS) # Either 'OK' or 'Closed' log.debug('Connection status is: %s', self.connection_status) # Upstream expects to find a 'name' tag but our response does not always have it. Return an empty element. if message.find(name) is None: return [] return super()._get_element_container(message=message, name=name) def get_payload(self, subscription_ids, connection_timeout): getstreamingevents = create_element('m:%s' % self.SERVICE_NAME) subscriptions_elem = create_element('m:SubscriptionIds') for subscription_id in subscription_ids: add_xml_child(subscriptions_elem, 't:SubscriptionId', subscription_id) if not len(subscriptions_elem): raise ValueError('"subscription_ids" must not be empty') getstreamingevents.append(subscriptions_elem) add_xml_child(getstreamingevents, 'm:ConnectionTimeout', connection_timeout) return getstreamingevents
Ancestors
Class variables
var CLOSED
var OK
var SERVICE_NAME
var element_container_name
var prefer_affinity
var streaming
Methods
def call(self, subscription_ids, connection_timeout)
-
Expand source code
def call(self, subscription_ids, connection_timeout): if connection_timeout < 1: raise ValueError("'connection_timeout' must be a positive integer") return self._elems_to_objs(self._get_elements(payload=self.get_payload( subscription_ids=subscription_ids, connection_timeout=connection_timeout, )))
def get_payload(self, subscription_ids, connection_timeout)
-
Expand source code
def get_payload(self, subscription_ids, connection_timeout): getstreamingevents = create_element('m:%s' % self.SERVICE_NAME) subscriptions_elem = create_element('m:SubscriptionIds') for subscription_id in subscription_ids: add_xml_child(subscriptions_elem, 't:SubscriptionId', subscription_id) if not len(subscriptions_elem): raise ValueError('"subscription_ids" must not be empty') getstreamingevents.append(subscriptions_elem) add_xml_child(getstreamingevents, 'm:ConnectionTimeout', connection_timeout) return getstreamingevents
Inherited members