From a9c7303743b5787e42f2a1e4960f3b1f46b43c0e Mon Sep 17 00:00:00 2001
From: Erik Cederstrand Convert from a standard library Convert from a standard library Holds information related to an item field. Get events since the subscription was created, in streaming mode. This method will block as many minutes
as specified by 'connection_timeout'. :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming()
+ :param subscription_id_or_ids: A subscription ID, or list of IDs, as acquired by .subscribe_to_streaming()
:param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout
is reached.
:param max_notifications_returned: If specified, will exit after receiving this number of notifications
@@ -2208,11 +2208,11 @@ Get events since the subscription was created, in streaming mode. This method will block as many minutes
as specified by 'connection_timeout'. :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming()
+ :param subscription_id_or_ids: A subscription ID, or list of IDs, as acquired by .subscribe_to_streaming()
:param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout
is reached.
:param max_notifications_returned: If specified, will exit after receiving this number of notifications
@@ -2180,11 +2180,11 @@ :return: a list of either True or exception instances, in the same order as the input 'id' and 'changekey' are UUIDs generated by Exchange. Autodiscover is a Microsoft protocol for automatically getting the endpoint of the Exchange server and other
@@ -652,10 +632,6 @@ :param email: The email address to autodiscover
:param credentials: Credentials with authorization to make autodiscover lookups for this Account
-(Default value = None)
-:param auth_type:
-(Default value = None)
-:param retry_policy:
(Default value = None) Autodiscover is a Microsoft protocol for automatically getting the endpoint of the Exchange server and other
@@ -345,10 +345,6 @@ :param email: The email address to autodiscover
:param credentials: Credentials with authorization to make autodiscover lookups for this Account
-(Default value = None)
-:param auth_type:
-(Default value = None)
-:param retry_policy:
(Default value = None) Return the EWS URL contained in the response. Return the protocol containing an EWS URL. A response may contain a number of possible protocol types. EXPR is meant for EWS. See
https://techcommunity.microsoft.com/t5/blogs/blogarticleprintpage/blog-id/Exchange/article-id/16 We allow fallback to EXCH if EXPR is not available, to support installations where EXPR is not available. Additionally, some responses may contain and EXPR with no EWS URL. In that case, return the URL from EXCH, if
-available. Additionally, some responses may contain an EXPR with no EWS URL. In that case, return EXCH, if available. Implements an Exchange user object and access types. Exchange provides two different ways of granting access for a
login to a specific account. Impersonation is used mainly for service accounts that connect via EWS. Delegate is used
for ad-hoc access e.g. granted manually by the user.
-See http://blogs.msdn.com/b/exchangedev/archive/2009/06/15/exchange-impersonation-vs-delegate-access.aspx Global error type within this module. Inappropriate argument value (of correct type). Inappropriate argument type. A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for. Base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for. Like CharField, but for lists of strings. Like TextListField, but for string values with a limited length. A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or arrtibute. It
+ A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or attribute. It
may additionally be a label for searching, filtering and limiting fields. In that case, the FieldURI format will be
'itemtype:FieldName' A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for. Base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for. Read a value from the given element Convert this field to an XML element Holds values needed to call server-side sorting on a single field path. Holds values needed to call server-side sorting on a single field path. :param field_path: A FieldPath instance
+:param reverse: A bool Holds values needed to point to a single field. For indexed properties, we allow setting either field,
field and label, or field, label and subfield. This allows pointing to either the full indexed property set, a
-property with a specific label, or a particular subfield field on that property. :param field: A FieldURIField or ExtendedPropertyField instance
+:param label: a str
+:param subfield: A SubField instance A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or arrtibute. It
+ A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or attribute. It
may additionally be a label for searching, filtering and limiting fields. In that case, the FieldURI format will be
'itemtype:FieldName' Used when a field is not supported on the given Exchnage version. Used when a field is not supported on the given Exchange version. A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or arrtibute. It
+ A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or attribute. It
may additionally be a label for searching, filtering and limiting fields. In that case, the FieldURI format will be
'itemtype:FieldName' A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for. Base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for. A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for. Base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for. A field that handles timedelta values. Difference between two datetime values. Like EWSElementField, but for lists of EWSElement objects. Helper to mark strings that must conform to xsd:anyURI.
-If we want an URI validator, see http://stackoverflow.com/questions/14466585/is-this-regex-correct-for-xsdanyuriModule
def from_pytz(cls, tz):
return cls(tz.zone)
+ @classmethod
+ def from_datetime(cls, tz):
+ """Convert from a standard library `datetime.timezone` instance."""
+ return cls(tz.tzname(None))
+
@classmethod
def from_dateutil(cls, tz):
# Objects returned by dateutil.tz.tzlocal() and dateutil.tz.gettz() are not supported. They
@@ -300,10 +305,11 @@ exchangelib.ewsdatetime
Module
return {
cls.__module__.split('.')[0]: lambda z: z,
'backports': cls.from_zoneinfo,
+ 'datetime': cls.from_datetime,
'dateutil': cls.from_dateutil,
'pytz': cls.from_pytz,
'zoneinfo': cls.from_zoneinfo,
- 'pytz_deprecation_shim': lambda z: cls.from_zoneinfo(z.unwrap_shim())
+ 'pytz_deprecation_shim': lambda z: cls.from_timezone(z.unwrap_shim())
}[tz_module](tz)
except KeyError:
raise TypeError('Unsupported tzinfo type: %r' % tz)
@@ -919,6 +925,11 @@ exchangelib.ewsdatetime
Methods
def from_pytz(cls, tz):
return cls(tz.zone)
+ @classmethod
+ def from_datetime(cls, tz):
+ """Convert from a standard library `datetime.timezone` instance."""
+ return cls(tz.tzname(None))
+
@classmethod
def from_dateutil(cls, tz):
# Objects returned by dateutil.tz.tzlocal() and dateutil.tz.gettz() are not supported. They
@@ -941,10 +952,11 @@ Methods
return {
cls.__module__.split('.')[0]: lambda z: z,
'backports': cls.from_zoneinfo,
+ 'datetime': cls.from_datetime,
'dateutil': cls.from_dateutil,
'pytz': cls.from_pytz,
'zoneinfo': cls.from_zoneinfo,
- 'pytz_deprecation_shim': lambda z: cls.from_zoneinfo(z.unwrap_shim())
+ 'pytz_deprecation_shim': lambda z: cls.from_timezone(z.unwrap_shim())
}[tz_module](tz)
except KeyError:
raise TypeError('Unsupported tzinfo type: %r' % tz)
@@ -1009,6 +1021,21 @@ Class variables
Static methods
+
+def from_datetime(tz)
+
datetime.timezone
instance.
+Expand source code
+
+
+@classmethod
+def from_datetime(cls, tz):
+ """Convert from a standard library `datetime.timezone` instance."""
+ return cls(tz.tzname(None))
def from_dateutil(tz)
Static methods
return {
cls.__module__.split('.')[0]: lambda z: z,
'backports': cls.from_zoneinfo,
+ 'datetime': cls.from_datetime,
'dateutil': cls.from_dateutil,
'pytz': cls.from_pytz,
'zoneinfo': cls.from_zoneinfo,
- 'pytz_deprecation_shim': lambda z: cls.from_zoneinfo(z.unwrap_shim())
+ 'pytz_deprecation_shim': lambda z: cls.from_timezone(z.unwrap_shim())
}[tz_module](tz)
except KeyError:
raise TypeError('Unsupported tzinfo type: %r' % tz)
@@ -1252,6 +1280,7 @@
IANA_TO_MS_MAP
MS_TO_IANA_MAP
from_datetime
from_dateutil
from_ms_id
from_pytz
Module
query_string = None
log.debug(
'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
- self.folders,
self.account,
+ self.folders,
shape,
depth,
additional_fields,
@@ -287,7 +287,7 @@ exchangelib.folders.collections
Module
restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
query_string = None
yield from FindPeople(account=self.account, chunk_size=page_size).call(
- folder=[folder],
+ folder=folder,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
@@ -698,8 +698,8 @@ exchangelib.folders.collections
Classes
query_string = None
log.debug(
'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
- self.folders,
self.account,
+ self.folders,
shape,
depth,
additional_fields,
@@ -771,7 +771,7 @@ Classes
restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
query_string = None
yield from FindPeople(account=self.account, chunk_size=page_size).call(
- folder=[folder],
+ folder=folder,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
@@ -1271,8 +1271,8 @@ Examples
query_string = None
log.debug(
'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
- self.folders,
self.account,
+ self.folders,
shape,
depth,
additional_fields,
@@ -1356,7 +1356,7 @@ Examples
restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
query_string = None
yield from FindPeople(account=self.account, chunk_size=page_size).call(
- folder=[folder],
+ folder=folder,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
diff --git a/docs/exchangelib/folders/index.html b/docs/exchangelib/folders/index.html
index 9703061f..db127e2a 100644
--- a/docs/exchangelib/folders/index.html
+++ b/docs/exchangelib/folders/index.html
@@ -4663,8 +4663,8 @@ Inherited members
query_string = None
log.debug(
'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
- self.folders,
self.account,
+ self.folders,
shape,
depth,
additional_fields,
@@ -4736,7 +4736,7 @@ Inherited members
restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
query_string = None
yield from FindPeople(account=self.account, chunk_size=page_size).call(
- folder=[folder],
+ folder=folder,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
@@ -5236,8 +5236,8 @@ Examples
query_string = None
log.debug(
'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
- self.folders,
self.account,
+ self.folders,
shape,
depth,
additional_fields,
@@ -5321,7 +5321,7 @@ Examples
restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
query_string = None
yield from FindPeople(account=self.account, chunk_size=page_size).call(
- folder=[folder],
+ folder=folder,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
@@ -5734,6 +5734,7 @@ Inherited members
from .base import Folder
# Subfolders will always be of class Folder
all_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=None)
+ all_fields.update(Folder.attribute_fields())
only_fields = []
for arg in args:
for field_path in all_fields:
@@ -5943,6 +5944,7 @@ Methods
from .base import Folder
# Subfolders will always be of class Folder
all_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=None)
+ all_fields.update(Folder.attribute_fields())
only_fields = []
for arg in args:
for field_path in all_fields:
diff --git a/docs/exchangelib/folders/queryset.html b/docs/exchangelib/folders/queryset.html
index bf266603..00217b69 100644
--- a/docs/exchangelib/folders/queryset.html
+++ b/docs/exchangelib/folders/queryset.html
@@ -71,6 +71,7 @@ Module
from .base import Folder
# Subfolders will always be of class Folder
all_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=None)
+ all_fields.update(Folder.attribute_fields())
only_fields = []
for arg in args:
for field_path in all_fields:
@@ -246,6 +247,7 @@ exchangelib.folders.queryset
Classes
from .base import Folder
# Subfolders will always be of class Folder
all_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=None)
+ all_fields.update(Folder.attribute_fields())
only_fields = []
for arg in args:
for field_path in all_fields:
@@ -455,6 +457,7 @@ Methods
from .base import Folder
# Subfolders will always be of class Folder
all_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=None)
+ all_fields.update(Folder.attribute_fields())
only_fields = []
for arg in args:
for field_path in all_fields:
diff --git a/docs/exchangelib/index.html b/docs/exchangelib/index.html
index 78758a9f..e7ce07eb 100644
--- a/docs/exchangelib/index.html
+++ b/docs/exchangelib/index.html
@@ -46,7 +46,7 @@ Package
from .transport import BASIC, DIGEST, NTLM, GSSAPI, SSPI, OAUTH2, CBA
from .version import Build, Version
-__version__ = '4.6.0'
+__version__ = '4.6.1'
__all__ = [
'__version__',
@@ -5576,6 +5576,11 @@ exchangelib
Methods
def from_pytz(cls, tz):
return cls(tz.zone)
+ @classmethod
+ def from_datetime(cls, tz):
+ """Convert from a standard library `datetime.timezone` instance."""
+ return cls(tz.tzname(None))
+
@classmethod
def from_dateutil(cls, tz):
# Objects returned by dateutil.tz.tzlocal() and dateutil.tz.gettz() are not supported. They
@@ -5598,10 +5603,11 @@ Methods
return {
cls.__module__.split('.')[0]: lambda z: z,
'backports': cls.from_zoneinfo,
+ 'datetime': cls.from_datetime,
'dateutil': cls.from_dateutil,
'pytz': cls.from_pytz,
'zoneinfo': cls.from_zoneinfo,
- 'pytz_deprecation_shim': lambda z: cls.from_zoneinfo(z.unwrap_shim())
+ 'pytz_deprecation_shim': lambda z: cls.from_timezone(z.unwrap_shim())
}[tz_module](tz)
except KeyError:
raise TypeError('Unsupported tzinfo type: %r' % tz)
@@ -5666,6 +5672,21 @@ Class variables
Static methods
+
@@ -4097,6 +4101,10 @@
+def from_datetime(tz)
+
datetime.timezone
instance.
+Expand source code
+
+
+@classmethod
+def from_datetime(cls, tz):
+ """Convert from a standard library `datetime.timezone` instance."""
+ return cls(tz.tzname(None))
def from_dateutil(tz)
Static methods
return {
cls.__module__.split('.')[0]: lambda z: z,
'backports': cls.from_zoneinfo,
+ 'datetime': cls.from_datetime,
'dateutil': cls.from_dateutil,
'pytz': cls.from_pytz,
'zoneinfo': cls.from_zoneinfo,
- 'pytz_deprecation_shim': lambda z: cls.from_zoneinfo(z.unwrap_shim())
+ 'pytz_deprecation_shim': lambda z: cls.from_timezone(z.unwrap_shim())
}[tz_module](tz)
except KeyError:
raise TypeError('Unsupported tzinfo type: %r' % tz)
@@ -7481,8 +7503,8 @@ Inherited members
query_string = None
log.debug(
'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
- self.folders,
self.account,
+ self.folders,
shape,
depth,
additional_fields,
@@ -7554,7 +7576,7 @@ Inherited members
restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
query_string = None
yield from FindPeople(account=self.account, chunk_size=page_size).call(
- folder=[folder],
+ folder=folder,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
@@ -8054,8 +8076,8 @@ Examples
query_string = None
log.debug(
'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
- self.folders,
self.account,
+ self.folders,
shape,
depth,
additional_fields,
@@ -8139,7 +8161,7 @@ Examples
restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
query_string = None
yield from FindPeople(account=self.account, chunk_size=page_size).call(
- folder=[folder],
+ folder=folder,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
@@ -12765,6 +12787,7 @@ EWS
diff --git a/docs/exchangelib/services/common.html b/docs/exchangelib/services/common.html
index 45ec4c75..53a47f9b 100644
--- a/docs/exchangelib/services/common.html
+++ b/docs/exchangelib/services/common.html
@@ -692,20 +692,27 @@
IANA_TO_MS_MAP
MS_TO_IANA_MAP
from_datetime
from_dateutil
from_ms_id
from_pytz
Module
time_window = EWSElementField(value_cls=TimeWindow, is_required=True)
# Interval value is in minutes
- merged_free_busy_interval = IntegerField(field_uri='MergedFreeBusyIntervalInMinutes', min=6, max=1440, default=30,
+ merged_free_busy_interval = IntegerField(field_uri='MergedFreeBusyIntervalInMinutes', min=5, max=1440, default=30,
is_required=True)
requested_view = ChoiceField(field_uri='RequestedView', choices={Choice(c) for c in REQUESTED_VIEWS},
is_required=True) # Choice('None') is also valid, but only for responses
@@ -5258,7 +5258,7 @@ exchangelib.properties
Inherited members
time_window = EWSElementField(value_cls=TimeWindow, is_required=True)
# Interval value is in minutes
- merged_free_busy_interval = IntegerField(field_uri='MergedFreeBusyIntervalInMinutes', min=6, max=1440, default=30,
+ merged_free_busy_interval = IntegerField(field_uri='MergedFreeBusyIntervalInMinutes', min=5, max=1440, default=30,
is_required=True)
requested_view = ChoiceField(field_uri='RequestedView', choices={Choice(c) for c in REQUESTED_VIEWS},
is_required=True) # Choice('None') is also valid, but only for responsesModule
log.warning('Inconsistent next_offset values: %r. Using lowest value', next_offsets)
return min(next_offsets)
- def _paged_call(self, payload_func, max_items, expected_message_count, **kwargs):
+ def _paged_call(self, payload_func, max_items, folders, **kwargs):
"""Call a service that supports paging requests. Return a generator over all response items. Keeps track of
all paging-related counters.
"""
- paging_infos = [dict(item_count=0, next_offset=None) for _ in range(expected_message_count)]
+ paging_infos = {f: dict(item_count=0, next_offset=None) for f in folders}
common_next_offset = kwargs['offset']
total_item_count = 0
while True:
+ if not paging_infos:
+ # Paging is done for all folders
+ break
log.debug('Getting page at offset %s (max_items %s)', common_next_offset, max_items)
kwargs['offset'] = common_next_offset
- pages = self._get_pages(payload_func, kwargs, expected_message_count)
- for (page, next_offset), paging_info in zip(pages, paging_infos):
+ kwargs['folders'] = paging_infos.keys() # Only request the paging of the remaining folders.
+ pages = self._get_pages(payload_func, kwargs, len(paging_infos))
+ for (page, next_offset), (f, paging_info) in zip(pages, list(paging_infos.items())):
paging_info['next_offset'] = next_offset
if isinstance(page, Exception):
+ # Assume this folder no longer works. Don't attempt to page it again.
+ log.debug('Exception occurred for folder %s. Removing.', f)
+ del paging_infos[f]
yield page
continue
if page is not None:
@@ -718,8 +725,11 @@ exchangelib.services.common
Module
log.debug("'max_items' count reached (inner)")
break
if not paging_info['next_offset']:
- # Paging is done for this message
+ # Paging is done for this folder. Don't attempt to page it again.
+ log.debug('Paging has completed for folder %s. Removing.', f)
+ del paging_infos[f]
continue
+ log.debug('Folder %s still has items', f)
# Check sanity of paging offsets, but don't fail. When we are iterating huge collections that take a
# long time to complete, the collection may change while we are iterating. This can affect the
# 'next_offset' value and make it inconsistent with the number of already collected items.
@@ -733,24 +743,33 @@ exchangelib.services.common
Module
if max_items and total_item_count >= max_items:
log.debug("'max_items' count reached (outer)")
break
- common_next_offset = self._get_next_offset(paging_infos)
+ common_next_offset = self._get_next_offset(paging_infos.values())
if common_next_offset is None:
- # Paging is done for all messages
+ # Paging is done for all folders
break
@staticmethod
def _get_paging_values(elem):
"""Read paging information from the paging container element."""
- is_last_page = elem.get('IncludesLastItemInRange').lower() in ('true', '0')
- offset = elem.get('IndexedPagingOffset')
- if offset is None and not is_last_page:
- log.debug("Not last page in range, but Exchange didn't send a page offset. Assuming first page")
- offset = '1'
- next_offset = None if is_last_page else int(offset)
+ offset_attr = elem.get('IndexedPagingOffset')
+ next_offset = None if offset_attr is None else int(offset_attr)
item_count = int(elem.get('TotalItemsInView'))
- if not item_count and next_offset is not None:
- raise ValueError("Expected empty 'next_offset' when 'item_count' is 0")
- log.debug('Got page with next offset %s (last_page %s)', next_offset, is_last_page)
+ is_last_page = elem.get('IncludesLastItemInRange').lower() in ('true', '0')
+ log.debug('Got page with offset %s, item_count %s, last_page %s', next_offset, item_count, is_last_page)
+ # Clean up contradictory paging values
+ if next_offset is None and not is_last_page:
+ log.debug("Not last page in range, but server didn't send a page offset. Assuming first page")
+ next_offset = 1
+ if next_offset is not None and is_last_page:
+ if next_offset != item_count:
+ log.debug("Last page in range, but we still got an offset. Assuming paging has completed")
+ next_offset = None
+ if not item_count and not is_last_page:
+ log.debug("Not last page in range, but also no items left. Assuming paging has completed")
+ next_offset = None
+ if item_count and next_offset == 0:
+ log.debug("Non-zero offset, but also no items left. Assuming paging has completed")
+ next_offset = None
return item_count, next_offset
def _get_page(self, message):
@@ -1762,20 +1781,27 @@ exchangelib.services.common
Inherited members
log.warning('Inconsistent next_offset values: %r. Using lowest value', next_offsets)
return min(next_offsets)
- def _paged_call(self, payload_func, max_items, expected_message_count, **kwargs):
+ def _paged_call(self, payload_func, max_items, folders, **kwargs):
"""Call a service that supports paging requests. Return a generator over all response items. Keeps track of
all paging-related counters.
"""
- paging_infos = [dict(item_count=0, next_offset=None) for _ in range(expected_message_count)]
+ paging_infos = {f: dict(item_count=0, next_offset=None) for f in folders}
common_next_offset = kwargs['offset']
total_item_count = 0
while True:
+ if not paging_infos:
+ # Paging is done for all folders
+ break
log.debug('Getting page at offset %s (max_items %s)', common_next_offset, max_items)
kwargs['offset'] = common_next_offset
- pages = self._get_pages(payload_func, kwargs, expected_message_count)
- for (page, next_offset), paging_info in zip(pages, paging_infos):
+ kwargs['folders'] = paging_infos.keys() # Only request the paging of the remaining folders.
+ pages = self._get_pages(payload_func, kwargs, len(paging_infos))
+ for (page, next_offset), (f, paging_info) in zip(pages, list(paging_infos.items())):
paging_info['next_offset'] = next_offset
if isinstance(page, Exception):
+ # Assume this folder no longer works. Don't attempt to page it again.
+ log.debug('Exception occurred for folder %s. Removing.', f)
+ del paging_infos[f]
yield page
continue
if page is not None:
@@ -1788,8 +1814,11 @@ Inherited members
log.debug("'max_items' count reached (inner)")
break
if not paging_info['next_offset']:
- # Paging is done for this message
+ # Paging is done for this folder. Don't attempt to page it again.
+ log.debug('Paging has completed for folder %s. Removing.', f)
+ del paging_infos[f]
continue
+ log.debug('Folder %s still has items', f)
# Check sanity of paging offsets, but don't fail. When we are iterating huge collections that take a
# long time to complete, the collection may change while we are iterating. This can affect the
# 'next_offset' value and make it inconsistent with the number of already collected items.
@@ -1803,24 +1832,33 @@ Inherited members
if max_items and total_item_count >= max_items:
log.debug("'max_items' count reached (outer)")
break
- common_next_offset = self._get_next_offset(paging_infos)
+ common_next_offset = self._get_next_offset(paging_infos.values())
if common_next_offset is None:
- # Paging is done for all messages
+ # Paging is done for all folders
break
@staticmethod
def _get_paging_values(elem):
"""Read paging information from the paging container element."""
- is_last_page = elem.get('IncludesLastItemInRange').lower() in ('true', '0')
- offset = elem.get('IndexedPagingOffset')
- if offset is None and not is_last_page:
- log.debug("Not last page in range, but Exchange didn't send a page offset. Assuming first page")
- offset = '1'
- next_offset = None if is_last_page else int(offset)
+ offset_attr = elem.get('IndexedPagingOffset')
+ next_offset = None if offset_attr is None else int(offset_attr)
item_count = int(elem.get('TotalItemsInView'))
- if not item_count and next_offset is not None:
- raise ValueError("Expected empty 'next_offset' when 'item_count' is 0")
- log.debug('Got page with next offset %s (last_page %s)', next_offset, is_last_page)
+ is_last_page = elem.get('IncludesLastItemInRange').lower() in ('true', '0')
+ log.debug('Got page with offset %s, item_count %s, last_page %s', next_offset, item_count, is_last_page)
+ # Clean up contradictory paging values
+ if next_offset is None and not is_last_page:
+ log.debug("Not last page in range, but server didn't send a page offset. Assuming first page")
+ next_offset = 1
+ if next_offset is not None and is_last_page:
+ if next_offset != item_count:
+ log.debug("Last page in range, but we still got an offset. Assuming paging has completed")
+ next_offset = None
+ if not item_count and not is_last_page:
+ log.debug("Not last page in range, but also no items left. Assuming paging has completed")
+ next_offset = None
+ if item_count and next_offset == 0:
+ log.debug("Non-zero offset, but also no items left. Assuming paging has completed")
+ next_offset = None
return item_count, next_offset
def _get_page(self, message):
diff --git a/docs/exchangelib/services/find_folder.html b/docs/exchangelib/services/find_folder.html
index a8459a1a..720aea51 100644
--- a/docs/exchangelib/services/find_folder.html
+++ b/docs/exchangelib/services/find_folder.html
@@ -65,9 +65,8 @@ Module
return self._elems_to_objs(self._paged_call(
payload_func=self.get_payload,
max_items=max_items,
- expected_message_count=len(folders),
+ folders=folders,
**dict(
- folders=folders,
additional_fields=additional_fields,
restriction=restriction,
shape=shape,
@@ -163,9 +162,8 @@ exchangelib.services.find_folder
Classes
return self._elems_to_objs(self._paged_call(
payload_func=self.get_payload,
max_items=max_items,
- expected_message_count=len(folders),
+ folders=folders,
**dict(
- folders=folders,
additional_fields=additional_fields,
restriction=restriction,
shape=shape,
@@ -272,9 +270,8 @@ Methods
return self._elems_to_objs(self._paged_call(
payload_func=self.get_payload,
max_items=max_items,
- expected_message_count=len(folders),
+ folders=folders,
**dict(
- folders=folders,
additional_fields=additional_fields,
restriction=restriction,
shape=shape,
diff --git a/docs/exchangelib/services/find_item.html b/docs/exchangelib/services/find_item.html
index 9b24365d..d2f23e50 100644
--- a/docs/exchangelib/services/find_item.html
+++ b/docs/exchangelib/services/find_item.html
@@ -68,9 +68,8 @@ Module
return self._elems_to_objs(self._paged_call(
payload_func=self.get_payload,
max_items=max_items,
- expected_message_count=len(folders),
+ folders=folders,
**dict(
- folders=folders,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
@@ -187,9 +186,8 @@ exchangelib.services.find_item
Classes
return self._elems_to_objs(self._paged_call(
payload_func=self.get_payload,
max_items=max_items,
- expected_message_count=len(folders),
+ folders=folders,
**dict(
- folders=folders,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
@@ -318,9 +316,8 @@ Methods
return self._elems_to_objs(self._paged_call(
payload_func=self.get_payload,
max_items=max_items,
- expected_message_count=len(folders),
+ folders=folders,
**dict(
- folders=folders,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
diff --git a/docs/exchangelib/services/find_people.html b/docs/exchangelib/services/find_people.html
index 4e7ecd4a..8452aafe 100644
--- a/docs/exchangelib/services/find_people.html
+++ b/docs/exchangelib/services/find_people.html
@@ -70,9 +70,8 @@ Module
return self._elems_to_objs(self._paged_call(
payload_func=self.get_payload,
max_items=max_items,
- expected_message_count=1, # We can only query one folder, so there will only be one element in response
+ folders=[folder], # We can only query one folder, so there will only be one element in response
**dict(
- folder=folder,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
@@ -95,8 +94,12 @@ exchangelib.services.find_people
Module
continue
yield Persona.from_xml(elem, account=self.account)
- def get_payload(self, folder, additional_fields, restriction, order_fields, query_string, shape, depth, page_size,
+ def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, page_size,
offset=0):
+ folders = list(folders)
+ if len(folders) != 1:
+ raise ValueError('%r can only query one folder' % self.SERVICE_NAME)
+ folder = folders[0]
findpeople = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
personashape = create_shape_element(
tag='m:PersonaShape', shape=shape, additional_fields=additional_fields, version=self.account.version
@@ -193,9 +196,8 @@ exchangelib.services.find_people
Classes
return self._elems_to_objs(self._paged_call(
payload_func=self.get_payload,
max_items=max_items,
- expected_message_count=1, # We can only query one folder, so there will only be one element in response
+ folders=[folder], # We can only query one folder, so there will only be one element in response
**dict(
- folder=folder,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
@@ -218,8 +220,12 @@ Classes
continue
yield Persona.from_xml(elem, account=self.account)
- def get_payload(self, folder, additional_fields, restriction, order_fields, query_string, shape, depth, page_size,
+ def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, page_size,
offset=0):
+ folders = list(folders)
+ if len(folders) != 1:
+ raise ValueError('%r can only query one folder' % self.SERVICE_NAME)
+ folder = folders[0]
findpeople = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
personashape = create_shape_element(
tag='m:PersonaShape', shape=shape, additional_fields=additional_fields, version=self.account.version
@@ -327,9 +333,8 @@ Methods
return self._elems_to_objs(self._paged_call(
payload_func=self.get_payload,
max_items=max_items,
- expected_message_count=1, # We can only query one folder, so there will only be one element in response
+ folders=[folder], # We can only query one folder, so there will only be one element in response
**dict(
- folder=folder,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
@@ -343,7 +348,7 @@ Methods
-def get_payload(self, folder, additional_fields, restriction, order_fields, query_string, shape, depth, page_size, offset=0)
+def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, page_size, offset=0)
Methods
def get_payload(self, folder, additional_fields, restriction, order_fields, query_string, shape, depth, page_size,
+
def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, page_size,
offset=0):
+ folders = list(folders)
+ if len(folders) != 1:
+ raise ValueError('%r can only query one folder' % self.SERVICE_NAME)
+ folder = folders[0]
findpeople = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
personashape = create_shape_element(
tag='m:PersonaShape', shape=shape, additional_fields=additional_fields, version=self.account.version
diff --git a/docs/exchangelib/services/index.html b/docs/exchangelib/services/index.html
index 9c266390..cd15d9e8 100644
--- a/docs/exchangelib/services/index.html
+++ b/docs/exchangelib/services/index.html
@@ -1979,9 +1979,8 @@
Inherited members
return self._elems_to_objs(self._paged_call(
payload_func=self.get_payload,
max_items=max_items,
- expected_message_count=len(folders),
+ folders=folders,
**dict(
- folders=folders,
additional_fields=additional_fields,
restriction=restriction,
shape=shape,
@@ -2088,9 +2087,8 @@ Methods
return self._elems_to_objs(self._paged_call(
payload_func=self.get_payload,
max_items=max_items,
- expected_message_count=len(folders),
+ folders=folders,
**dict(
- folders=folders,
additional_fields=additional_fields,
restriction=restriction,
shape=shape,
@@ -2196,9 +2194,8 @@ Inherited members
return self._elems_to_objs(self._paged_call(
payload_func=self.get_payload,
max_items=max_items,
- expected_message_count=len(folders),
+ folders=folders,
**dict(
- folders=folders,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
@@ -2327,9 +2324,8 @@ Methods
return self._elems_to_objs(self._paged_call(
payload_func=self.get_payload,
max_items=max_items,
- expected_message_count=len(folders),
+ folders=folders,
**dict(
- folders=folders,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
@@ -2446,9 +2442,8 @@ Inherited members
return self._elems_to_objs(self._paged_call(
payload_func=self.get_payload,
max_items=max_items,
- expected_message_count=1, # We can only query one folder, so there will only be one element in response
+ folders=[folder], # We can only query one folder, so there will only be one element in response
**dict(
- folder=folder,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
@@ -2471,8 +2466,12 @@ Inherited members
continue
yield Persona.from_xml(elem, account=self.account)
- def get_payload(self, folder, additional_fields, restriction, order_fields, query_string, shape, depth, page_size,
+ def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, page_size,
offset=0):
+ folders = list(folders)
+ if len(folders) != 1:
+ raise ValueError('%r can only query one folder' % self.SERVICE_NAME)
+ folder = folders[0]
findpeople = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
personashape = create_shape_element(
tag='m:PersonaShape', shape=shape, additional_fields=additional_fields, version=self.account.version
@@ -2580,9 +2579,8 @@ Methods
return self._elems_to_objs(self._paged_call(
payload_func=self.get_payload,
max_items=max_items,
- expected_message_count=1, # We can only query one folder, so there will only be one element in response
+ folders=[folder], # We can only query one folder, so there will only be one element in response
**dict(
- folder=folder,
additional_fields=additional_fields,
restriction=restriction,
order_fields=order_fields,
@@ -2596,7 +2594,7 @@ Methods
-def get_payload(self, folder, additional_fields, restriction, order_fields, query_string, shape, depth, page_size, offset=0)
+def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, page_size, offset=0)
Methods
def get_payload(self, folder, additional_fields, restriction, order_fields, query_string, shape, depth, page_size,
+
+ return tuple(c.value for c in self.choices if c.supports_version(version))def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, page_size,
offset=0):
+ folders = list(folders)
+ if len(folders) != 1:
+ raise ValueError('%r can only query one folder' % self.SERVICE_NAME)
+ folder = folders[0]
findpeople = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
personashape = create_shape_element(
tag='m:PersonaShape', shape=shape, additional_fields=additional_fields, version=self.account.version
diff --git a/exchangelib/__init__.py b/exchangelib/__init__.py
index c56c0a3e..d8d1bf83 100644
--- a/exchangelib/__init__.py
+++ b/exchangelib/__init__.py
@@ -18,7 +18,7 @@
from .transport import BASIC, DIGEST, NTLM, GSSAPI, SSPI, OAUTH2, CBA
from .version import Build, Version
-__version__ = '4.6.0'
+__version__ = '4.6.1'
__all__ = [
'__version__',
From 1c951c57fcbfc1339d71dbae33f3c90fc0d6ce94 Mon Sep 17 00:00:00 2001
From: Erik Cederstrand
Module
from cached_property import threaded_cached_property
-from .autodiscover import discover
+from .autodiscover import Autodiscovery
from .configuration import Configuration
from .credentials import DELEGATE, IMPERSONATION, ACCESS_TYPES
from .errors import UnknownTimeZone
@@ -143,9 +143,9 @@ exchangelib.account
Module
credentials = config.credentials
else:
retry_policy, auth_type = None, None
- self.ad_response, self.protocol = discover(
+ self.ad_response, self.protocol = Autodiscovery(
email=primary_smtp_address, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy
- )
+ ).discover()
primary_smtp_address = self.ad_response.autodiscover_smtp_address
else:
if not config:
@@ -156,6 +156,9 @@ exchangelib.account
Module
# Other ways of identifying the account can be added later
self.identity = Identity(primary_smtp_address=primary_smtp_address)
+ # For maintaining affinity in e.g. subscriptions
+ self.affinity_cookie = None
+
# We may need to override the default server version on a per-account basis because Microsoft may report one
# server version up-front but delegate account requests to an older backend server.
self.version = self.protocol.version
@@ -752,9 +755,9 @@ exchangelib.account
Classes
credentials = config.credentials
else:
retry_policy, auth_type = None, None
- self.ad_response, self.protocol = discover(
+ self.ad_response, self.protocol = Autodiscovery(
email=primary_smtp_address, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy
- )
+ ).discover()
primary_smtp_address = self.ad_response.autodiscover_smtp_address
else:
if not config:
@@ -765,6 +768,9 @@ Classes
# Other ways of identifying the account can be added later
self.identity = Identity(primary_smtp_address=primary_smtp_address)
+ # For maintaining affinity in e.g. subscriptions
+ self.affinity_cookie = None
+
# We may need to override the default server version on a per-account basis because Microsoft may report one
# server version up-front but delegate account requests to an older backend server.
self.version = self.protocol.version
diff --git a/docs/exchangelib/fields.html b/docs/exchangelib/fields.html
index 415328a5..1bb7ae2b 100644
--- a/docs/exchangelib/fields.html
+++ b/docs/exchangelib/fields.html
@@ -921,7 +921,7 @@ Module
))
def supported_choices(self, version):
- return [c.value for c in self.choices if c.supports_version(version)]
+ return tuple(c.value for c in self.choices if c.supports_version(version))
FREE_BUSY_CHOICES = [Choice('Free'), Choice('Tentative'), Choice('Busy'), Choice('OOF'), Choice('NoData'),
@@ -1393,6 +1393,10 @@ exchangelib.fields
Module
return hash(self.name)
+class ExtendedPropertyListField(ExtendedPropertyField):
+ is_list = True
+
+
class ItemField(FieldURIField):
@property
def value_cls(self):
@@ -2704,7 +2708,7 @@ exchangelib.fields
Methods
))
def supported_choices(self, version):
- return [c.value for c in self.choices if c.supports_version(version)]Ancestors
@@ -2760,7 +2764,7 @@
Methods
Expand source code
+ return tuple(c.value for c in self.choices if c.supports_version(version))
def supported_choices(self, version):
- return [c.value for c in self.choices if c.supports_version(version)]
Ancestors
+Subclasses
+
Methods
+
@@ -4183,6 +4191,32 @@
Methods
+class ExtendedPropertyListField
+(*args, **kwargs)
+
+Expand source code
+
+
+class ExtendedPropertyListField(ExtendedPropertyField):
+ is_list = True
Ancestors
+
+Class variables
+
+
+var is_list
class Field
(name=None, is_required=False, is_required_after_save=False, is_read_only=False, is_read_only_after_send=False, is_searchable=True, is_attribute=False, default=None, supported_from=None, deprecated_from=None)
@@ -7326,6 +7360,12 @@
+ExtendedPropertyListField
+
+
+is_list
Field
-clean
Module
from .collections import FolderCollection, SyncCompleted
from .queryset import SingleFolderQuerySet, SHALLOW as SHALLOW_FOLDERS, DEEP as DEEP_FOLDERS
from ..errors import ErrorAccessDenied, ErrorFolderNotFound, ErrorCannotEmptyFolder, ErrorCannotDeleteObject, \
- ErrorDeleteDistinguishedFolder, ErrorInvalidSubscription, ErrorNoPublicFolderReplicaAvailable, ErrorItemNotFound
+ ErrorDeleteDistinguishedFolder, ErrorNoPublicFolderReplicaAvailable, ErrorItemNotFound
from ..fields import IntegerField, CharField, FieldPath, EffectiveRightsField, PermissionSetField, EWSElementField, \
Field, IdElementField, InvalidField
from ..items import CalendarItem, RegisterMixIn, ITEM_CLASSES, DELETE_TYPE_CHOICES, HARD_DELETE, \
@@ -46,7 +46,7 @@ exchangelib.folders.base
Module
CreateUserConfiguration, UpdateUserConfiguration, DeleteUserConfiguration, SubscribeToPush, SubscribeToPull, \
Unsubscribe, GetEvents, GetStreamingEvents, MoveFolder
from ..services.get_user_configuration import ALL
-from ..util import TNS, require_id
+from ..util import TNS, require_id, is_iterable
from ..version import Version, EXCHANGE_2007_SP1, EXCHANGE_2010
log = logging.getLogger(__name__)
@@ -683,11 +683,11 @@ exchangelib.folders.base
Module
if not notification.more_events:
break
- def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None):
+ def get_streaming_events(self, subscription_id_or_ids, connection_timeout=1, max_notifications_returned=None):
"""Get events since the subscription was created, in streaming mode. This method will block as many minutes
as specified by 'connection_timeout'.
- :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming()
+ :param subscription_id_or_ids: A subscription ID, or list of IDs, as acquired by .subscribe_to_streaming()
:param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout
is reached.
:param max_notifications_returned: If specified, will exit after receiving this number of notifications
@@ -699,16 +699,16 @@ exchangelib.folders.base
Module
# Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
request_timeout = connection_timeout*60 + 60
svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
+ subscription_ids = subscription_id_or_ids if is_iterable(subscription_id_or_ids, generators_allowed=True) \
+ else [subscription_id_or_ids]
for i, notification in enumerate(
- svc.call(subscription_ids=[subscription_id], connection_timeout=connection_timeout),
+ svc.call(subscription_ids=subscription_ids, connection_timeout=connection_timeout),
start=1
):
yield notification
if max_notifications_returned and i >= max_notifications_returned:
svc.stop_streaming()
break
- if svc.error_subscription_ids:
- raise ErrorInvalidSubscription('Invalid subscription IDs: %s' % svc.error_subscription_ids)
def __floordiv__(self, other):
"""Support the some_folder // 'child_folder' // 'child_of_child_folder' navigation syntax.
@@ -1568,11 +1568,11 @@ exchangelib.folders.base
Classes
if not notification.more_events:
break
- def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None):
+ def get_streaming_events(self, subscription_id_or_ids, connection_timeout=1, max_notifications_returned=None):
"""Get events since the subscription was created, in streaming mode. This method will block as many minutes
as specified by 'connection_timeout'.
- :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming()
+ :param subscription_id_or_ids: A subscription ID, or list of IDs, as acquired by .subscribe_to_streaming()
:param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout
is reached.
:param max_notifications_returned: If specified, will exit after receiving this number of notifications
@@ -1584,16 +1584,16 @@ Classes
# Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
request_timeout = connection_timeout*60 + 60
svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
+ subscription_ids = subscription_id_or_ids if is_iterable(subscription_id_or_ids, generators_allowed=True) \
+ else [subscription_id_or_ids]
for i, notification in enumerate(
- svc.call(subscription_ids=[subscription_id], connection_timeout=connection_timeout),
+ svc.call(subscription_ids=subscription_ids, connection_timeout=connection_timeout),
start=1
):
yield notification
if max_notifications_returned and i >= max_notifications_returned:
svc.stop_streaming()
break
- if svc.error_subscription_ids:
- raise ErrorInvalidSubscription('Invalid subscription IDs: %s' % svc.error_subscription_ids)
def __floordiv__(self, other):
"""Support the some_folder // 'child_folder' // 'child_of_child_folder' navigation syntax.
@@ -2192,12 +2192,12 @@ Methods
-def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None)
+def get_streaming_events(self, subscription_id_or_ids, connection_timeout=1, max_notifications_returned=None)
Methods
def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None):
+
+ breakdef get_streaming_events(self, subscription_id_or_ids, connection_timeout=1, max_notifications_returned=None):
"""Get events since the subscription was created, in streaming mode. This method will block as many minutes
as specified by 'connection_timeout'.
- :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming()
+ :param subscription_id_or_ids: A subscription ID, or list of IDs, as acquired by .subscribe_to_streaming()
:param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout
is reached.
:param max_notifications_returned: If specified, will exit after receiving this number of notifications
@@ -2224,16 +2224,16 @@
Methods
# Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
request_timeout = connection_timeout*60 + 60
svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
+ subscription_ids = subscription_id_or_ids if is_iterable(subscription_id_or_ids, generators_allowed=True) \
+ else [subscription_id_or_ids]
for i, notification in enumerate(
- svc.call(subscription_ids=[subscription_id], connection_timeout=connection_timeout),
+ svc.call(subscription_ids=subscription_ids, connection_timeout=connection_timeout),
start=1
):
yield notification
if max_notifications_returned and i >= max_notifications_returned:
svc.stop_streaming()
- break
- if svc.error_subscription_ids:
- raise ErrorInvalidSubscription('Invalid subscription IDs: %s' % svc.error_subscription_ids)
diff --git a/docs/exchangelib/folders/collections.html b/docs/exchangelib/folders/collections.html
index a51e66ae..eae00689 100644
--- a/docs/exchangelib/folders/collections.html
+++ b/docs/exchangelib/folders/collections.html
@@ -69,7 +69,7 @@
Module
@threaded_cached_property
def folders(self):
# Resolve the list of folders, in case it's a generator
- return list(self._folders)
+ return tuple(self._folders)
def __len__(self):
return len(self.folders)
@@ -553,7 +553,7 @@ exchangelib.folders.collections
Classes
@threaded_cached_property
def folders(self):
# Resolve the list of folders, in case it's a generator
- return list(self._folders)
+ return tuple(self._folders)
def __len__(self):
return len(self.folders)
diff --git a/docs/exchangelib/folders/index.html b/docs/exchangelib/folders/index.html
index db127e2a..b59eb683 100644
--- a/docs/exchangelib/folders/index.html
+++ b/docs/exchangelib/folders/index.html
@@ -1540,11 +1540,11 @@ Inherited members
if not notification.more_events:
break
- def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None):
+ def get_streaming_events(self, subscription_id_or_ids, connection_timeout=1, max_notifications_returned=None):
"""Get events since the subscription was created, in streaming mode. This method will block as many minutes
as specified by 'connection_timeout'.
- :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming()
+ :param subscription_id_or_ids: A subscription ID, or list of IDs, as acquired by .subscribe_to_streaming()
:param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout
is reached.
:param max_notifications_returned: If specified, will exit after receiving this number of notifications
@@ -1556,16 +1556,16 @@ Inherited members
# Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
request_timeout = connection_timeout*60 + 60
svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
+ subscription_ids = subscription_id_or_ids if is_iterable(subscription_id_or_ids, generators_allowed=True) \
+ else [subscription_id_or_ids]
for i, notification in enumerate(
- svc.call(subscription_ids=[subscription_id], connection_timeout=connection_timeout),
+ svc.call(subscription_ids=subscription_ids, connection_timeout=connection_timeout),
start=1
):
yield notification
if max_notifications_returned and i >= max_notifications_returned:
svc.stop_streaming()
break
- if svc.error_subscription_ids:
- raise ErrorInvalidSubscription('Invalid subscription IDs: %s' % svc.error_subscription_ids)
def __floordiv__(self, other):
"""Support the some_folder // 'child_folder' // 'child_of_child_folder' navigation syntax.
@@ -2164,12 +2164,12 @@ Methods
-def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None)
+def get_streaming_events(self, subscription_id_or_ids, connection_timeout=1, max_notifications_returned=None)
Methods
def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None):
+
+ breakdef get_streaming_events(self, subscription_id_or_ids, connection_timeout=1, max_notifications_returned=None):
"""Get events since the subscription was created, in streaming mode. This method will block as many minutes
as specified by 'connection_timeout'.
- :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming()
+ :param subscription_id_or_ids: A subscription ID, or list of IDs, as acquired by .subscribe_to_streaming()
:param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout
is reached.
:param max_notifications_returned: If specified, will exit after receiving this number of notifications
@@ -2196,16 +2196,16 @@
Methods
# Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
request_timeout = connection_timeout*60 + 60
svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
+ subscription_ids = subscription_id_or_ids if is_iterable(subscription_id_or_ids, generators_allowed=True) \
+ else [subscription_id_or_ids]
for i, notification in enumerate(
- svc.call(subscription_ids=[subscription_id], connection_timeout=connection_timeout),
+ svc.call(subscription_ids=subscription_ids, connection_timeout=connection_timeout),
start=1
):
yield notification
if max_notifications_returned and i >= max_notifications_returned:
svc.stop_streaming()
- break
- if svc.error_subscription_ids:
- raise ErrorInvalidSubscription('Invalid subscription IDs: %s' % svc.error_subscription_ids)
@@ -4518,7 +4518,7 @@
@@ -7358,7 +7361,7 @@ Inherited members
@threaded_cached_property
def folders(self):
# Resolve the list of folders, in case it's a generator
- return list(self._folders)
+ return tuple(self._folders)
def __len__(self):
return len(self.folders)
diff --git a/docs/exchangelib/index.html b/docs/exchangelib/index.html
index e7ce07eb..2644e86d 100644
--- a/docs/exchangelib/index.html
+++ b/docs/exchangelib/index.html
@@ -46,7 +46,7 @@ Package
from .transport import BASIC, DIGEST, NTLM, GSSAPI, SSPI, OAUTH2, CBA
from .version import Build, Version
-__version__ = '4.6.1'
+__version__ = '4.6.2'
__all__ = [
'__version__',
@@ -354,9 +354,9 @@ exchangelib
Inherited members
credentials = config.credentials
else:
retry_policy, auth_type = None, None
- self.ad_response, self.protocol = discover(
+ self.ad_response, self.protocol = Autodiscovery(
email=primary_smtp_address, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy
- )
+ ).discover()
primary_smtp_address = self.ad_response.autodiscover_smtp_address
else:
if not config:
@@ -367,6 +367,9 @@ Inherited members
# Other ways of identifying the account can be added later
self.identity = Identity(primary_smtp_address=primary_smtp_address)
+ # For maintaining affinity in e.g. subscriptions
+ self.affinity_cookie = None
+
# We may need to override the default server version on a per-account basis because Microsoft may report one
# server version up-front but delegate account requests to an older backend server.
self.version = self.protocol.version
@@ -3736,7 +3739,7 @@ Methods
@classmethod
def timezone_fields(cls):
- return [f for f in cls.FIELDS if isinstance(f, TimeZoneField)]
+ return tuple(f for f in cls.FIELDS if isinstance(f, TimeZoneField))
def clean_timezone_fields(self, version):
# Sets proper values on the timezone fields if they are not already set
@@ -3912,7 +3915,7 @@ Static methods
+ return tuple(f for f in cls.FIELDS if isinstance(f, TimeZoneField))@classmethod
def timezone_fields(cls):
- return [f for f in cls.FIELDS if isinstance(f, TimeZoneField)]
Inherited members
@threaded_cached_property
def folders(self):
# Resolve the list of folders, in case it's a generator
- return list(self._folders)
+ return tuple(self._folders)
def __len__(self):
return len(self.folders)
@@ -10200,17 +10203,17 @@ Inherited members
if lookup == self.LOOKUP_EXISTS:
# value=True will fall through to further processing
if not value:
- return [~self.__class__(**{key: True})]
+ return (~self.__class__(**{key: True}),)
if lookup == self.LOOKUP_RANGE:
# EWS doesn't have a 'range' operator. Emulate 'foo__range=(1, 2)' as 'foo__gte=1 and foo__lte=2'
# (both values inclusive).
if len(value) != 2:
raise ValueError("Value of lookup '%s' must have exactly 2 elements" % key)
- return [
+ return (
self.__class__(**{'%s__gte' % field_path: value[0]}),
self.__class__(**{'%s__lte' % field_path: value[1]}),
- ]
+ )
# Filtering on list types is a bit quirky. The only lookup type I have found to work is:
#
@@ -10226,19 +10229,19 @@ Inherited members
# 'Contains' element which only supports matching on string elements, not arrays.
#
# Exact matching of categories (i.e. match ['a', 'b'] but not ['a', 'b', 'c']) could be implemented by
- # post-processing items by fetch the categories field unconditionally and removing the items that don't
+ # post-processing items by fetching the categories field unconditionally and removing the items that don't
# have an exact match.
if lookup == self.LOOKUP_IN:
# EWS doesn't have an '__in' operator. Allow '__in' lookups on list and non-list field types,
# specifying a list value. We'll emulate it as a set of OR'ed exact matches.
if not is_iterable(value, generators_allowed=True):
raise ValueError("Value for lookup %r must be a list" % key)
- children = [self.__class__(**{field_path: v}) for v in value]
+ children = tuple(self.__class__(**{field_path: v}) for v in value)
if not children:
# This is an '__in' operator with an empty list as the value. We interpret it to mean "is foo
# contained in the empty set?" which is always false. Mark this Q object as such.
- return [self.__class__(conn_type=self.NEVER)]
- return [self.__class__(*children, conn_type=self.OR)]
+ return (self.__class__(conn_type=self.NEVER),)
+ return (self.__class__(*children, conn_type=self.OR),)
if lookup == self.LOOKUP_CONTAINS and is_iterable(value, generators_allowed=True):
# A '__contains' lookup with an list as the value ony makes sense for list fields, since exact match
@@ -10246,8 +10249,8 @@ Inherited members
#
# An empty list as value is allowed. We interpret it to mean "are all values in the empty set contained
# in foo?" which is always true.
- children = [self.__class__(**{field_path: v}) for v in value]
- return [self.__class__(*children, conn_type=self.AND)]
+ children = tuple(self.__class__(**{field_path: v}) for v in value)
+ return (self.__class__(*children, conn_type=self.AND),)
try:
op = self._lookup_to_op(lookup)
@@ -10257,13 +10260,13 @@ Inherited members
field_path, op = key, self.EQ
if not is_single_kwarg:
- return [self.__class__(**{key: value})]
+ return (self.__class__(**{key: value}),)
# This is a single-kwarg Q object with a lookup that requires a single value. Make this a leaf
self.field_path = field_path
self.op = op
self.value = value
- return []
+ return ()
def reduce(self):
"""Simplify this object, if possible."""
diff --git a/docs/exchangelib/items/base.html b/docs/exchangelib/items/base.html
index 30bb629c..c05cda80 100644
--- a/docs/exchangelib/items/base.html
+++ b/docs/exchangelib/items/base.html
@@ -30,7 +30,7 @@ Module
from ..extended_properties import ExtendedProperty
from ..fields import BooleanField, ExtendedPropertyField, BodyField, MailboxField, MailboxListField, EWSElementField, \
- CharField, IdElementField, AttachmentField
+ CharField, IdElementField, AttachmentField, ExtendedPropertyListField
from ..properties import InvalidField, IdChangeKeyMixIn, EWSElement, ReferenceItemId, ItemId, EWSMeta
from ..services import CreateItem
from ..util import require_account
@@ -122,7 +122,10 @@ exchangelib.items.base
Module
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item
#
# Find the correct index for the new extended property, and insert.
- field = ExtendedPropertyField(attr_name, value_cls=attr_cls)
+ if attr_cls.is_array_type():
+ field = ExtendedPropertyListField(attr_name, value_cls=attr_cls)
+ else:
+ field = ExtendedPropertyField(attr_name, value_cls=attr_cls)
cls.add_field(field, insert_after=cls.INSERT_AFTER_FIELD)
@classmethod
@@ -679,7 +682,10 @@ exchangelib.items.base
Inherited members
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item
#
# Find the correct index for the new extended property, and insert.
- field = ExtendedPropertyField(attr_name, value_cls=attr_cls)
+ if attr_cls.is_array_type():
+ field = ExtendedPropertyListField(attr_name, value_cls=attr_cls)
+ else:
+ field = ExtendedPropertyField(attr_name, value_cls=attr_cls)
cls.add_field(field, insert_after=cls.INSERT_AFTER_FIELD)
@classmethod
@@ -779,7 +785,10 @@ Static methods
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item
#
# Find the correct index for the new extended property, and insert.
- field = ExtendedPropertyField(attr_name, value_cls=attr_cls)
+ if attr_cls.is_array_type():
+ field = ExtendedPropertyListField(attr_name, value_cls=attr_cls)
+ else:
+ field = ExtendedPropertyField(attr_name, value_cls=attr_cls)
cls.add_field(field, insert_after=cls.INSERT_AFTER_FIELD)
diff --git a/docs/exchangelib/items/calendar_item.html b/docs/exchangelib/items/calendar_item.html
index c020d2d0..eb6e9df4 100644
--- a/docs/exchangelib/items/calendar_item.html
+++ b/docs/exchangelib/items/calendar_item.html
@@ -178,7 +178,7 @@ Module
@classmethod
def timezone_fields(cls):
- return [f for f in cls.FIELDS if isinstance(f, TimeZoneField)]
+ return tuple(f for f in cls.FIELDS if isinstance(f, TimeZoneField))
def clean_timezone_fields(self, version):
# Sets proper values on the timezone fields if they are not already set
@@ -1065,7 +1065,7 @@ exchangelib.items.calendar_item
Inherited members
@classmethod
def timezone_fields(cls):
- return [f for f in cls.FIELDS if isinstance(f, TimeZoneField)]
+ return tuple(f for f in cls.FIELDS if isinstance(f, TimeZoneField))
def clean_timezone_fields(self, version):
# Sets proper values on the timezone fields if they are not already set
@@ -1241,7 +1241,7 @@ Static methods
+ return tuple(f for f in cls.FIELDS if isinstance(f, TimeZoneField))
diff --git a/docs/exchangelib/items/index.html b/docs/exchangelib/items/index.html
index 72252230..5bfc22d9 100644
--- a/docs/exchangelib/items/index.html
+++ b/docs/exchangelib/items/index.html
@@ -467,7 +467,7 @@ @classmethod
def timezone_fields(cls):
- return [f for f in cls.FIELDS if isinstance(f, TimeZoneField)]
Inherited members
@classmethod
def timezone_fields(cls):
- return [f for f in cls.FIELDS if isinstance(f, TimeZoneField)]
+ return tuple(f for f in cls.FIELDS if isinstance(f, TimeZoneField))
def clean_timezone_fields(self, version):
# Sets proper values on the timezone fields if they are not already set
@@ -643,7 +643,7 @@ Static methods
+ return tuple(f for f in cls.FIELDS if isinstance(f, TimeZoneField))
@@ -4050,7 +4050,10 @@ @classmethod
def timezone_fields(cls):
- return [f for f in cls.FIELDS if isinstance(f, TimeZoneField)]
Inherited members
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item
#
# Find the correct index for the new extended property, and insert.
- field = ExtendedPropertyField(attr_name, value_cls=attr_cls)
+ if attr_cls.is_array_type():
+ field = ExtendedPropertyListField(attr_name, value_cls=attr_cls)
+ else:
+ field = ExtendedPropertyField(attr_name, value_cls=attr_cls)
cls.add_field(field, insert_after=cls.INSERT_AFTER_FIELD)
@classmethod
@@ -4150,7 +4153,10 @@ Static methods
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item
#
# Find the correct index for the new extended property, and insert.
- field = ExtendedPropertyField(attr_name, value_cls=attr_cls)
+ if attr_cls.is_array_type():
+ field = ExtendedPropertyListField(attr_name, value_cls=attr_cls)
+ else:
+ field = ExtendedPropertyField(attr_name, value_cls=attr_cls)
cls.add_field(field, insert_after=cls.INSERT_AFTER_FIELD)
diff --git a/docs/exchangelib/restriction.html b/docs/exchangelib/restriction.html
index cf22d647..ed06a805 100644
--- a/docs/exchangelib/restriction.html
+++ b/docs/exchangelib/restriction.html
@@ -131,17 +131,17 @@ Module
if lookup == self.LOOKUP_EXISTS:
# value=True will fall through to further processing
if not value:
- return [~self.__class__(**{key: True})]
+ return (~self.__class__(**{key: True}),)
if lookup == self.LOOKUP_RANGE:
# EWS doesn't have a 'range' operator. Emulate 'foo__range=(1, 2)' as 'foo__gte=1 and foo__lte=2'
# (both values inclusive).
if len(value) != 2:
raise ValueError("Value of lookup '%s' must have exactly 2 elements" % key)
- return [
+ return (
self.__class__(**{'%s__gte' % field_path: value[0]}),
self.__class__(**{'%s__lte' % field_path: value[1]}),
- ]
+ )
# Filtering on list types is a bit quirky. The only lookup type I have found to work is:
#
@@ -157,19 +157,19 @@ exchangelib.restriction
Module
# 'Contains' element which only supports matching on string elements, not arrays.
#
# Exact matching of categories (i.e. match ['a', 'b'] but not ['a', 'b', 'c']) could be implemented by
- # post-processing items by fetch the categories field unconditionally and removing the items that don't
+ # post-processing items by fetching the categories field unconditionally and removing the items that don't
# have an exact match.
if lookup == self.LOOKUP_IN:
# EWS doesn't have an '__in' operator. Allow '__in' lookups on list and non-list field types,
# specifying a list value. We'll emulate it as a set of OR'ed exact matches.
if not is_iterable(value, generators_allowed=True):
raise ValueError("Value for lookup %r must be a list" % key)
- children = [self.__class__(**{field_path: v}) for v in value]
+ children = tuple(self.__class__(**{field_path: v}) for v in value)
if not children:
# This is an '__in' operator with an empty list as the value. We interpret it to mean "is foo
# contained in the empty set?" which is always false. Mark this Q object as such.
- return [self.__class__(conn_type=self.NEVER)]
- return [self.__class__(*children, conn_type=self.OR)]
+ return (self.__class__(conn_type=self.NEVER),)
+ return (self.__class__(*children, conn_type=self.OR),)
if lookup == self.LOOKUP_CONTAINS and is_iterable(value, generators_allowed=True):
# A '__contains' lookup with an list as the value ony makes sense for list fields, since exact match
@@ -177,8 +177,8 @@ exchangelib.restriction
Module
#
# An empty list as value is allowed. We interpret it to mean "are all values in the empty set contained
# in foo?" which is always true.
- children = [self.__class__(**{field_path: v}) for v in value]
- return [self.__class__(*children, conn_type=self.AND)]
+ children = tuple(self.__class__(**{field_path: v}) for v in value)
+ return (self.__class__(*children, conn_type=self.AND),)
try:
op = self._lookup_to_op(lookup)
@@ -188,13 +188,13 @@ exchangelib.restriction
Module
field_path, op = key, self.EQ
if not is_single_kwarg:
- return [self.__class__(**{key: value})]
+ return (self.__class__(**{key: value}),)
# This is a single-kwarg Q object with a lookup that requires a single value. Make this a leaf
self.field_path = field_path
self.op = op
self.value = value
- return []
+ return ()
def reduce(self):
"""Simplify this object, if possible."""
@@ -704,17 +704,17 @@ exchangelib.restriction
Classes
if lookup == self.LOOKUP_EXISTS:
# value=True will fall through to further processing
if not value:
- return [~self.__class__(**{key: True})]
+ return (~self.__class__(**{key: True}),)
if lookup == self.LOOKUP_RANGE:
# EWS doesn't have a 'range' operator. Emulate 'foo__range=(1, 2)' as 'foo__gte=1 and foo__lte=2'
# (both values inclusive).
if len(value) != 2:
raise ValueError("Value of lookup '%s' must have exactly 2 elements" % key)
- return [
+ return (
self.__class__(**{'%s__gte' % field_path: value[0]}),
self.__class__(**{'%s__lte' % field_path: value[1]}),
- ]
+ )
# Filtering on list types is a bit quirky. The only lookup type I have found to work is:
#
@@ -730,19 +730,19 @@ Classes
# 'Contains' element which only supports matching on string elements, not arrays.
#
# Exact matching of categories (i.e. match ['a', 'b'] but not ['a', 'b', 'c']) could be implemented by
- # post-processing items by fetch the categories field unconditionally and removing the items that don't
+ # post-processing items by fetching the categories field unconditionally and removing the items that don't
# have an exact match.
if lookup == self.LOOKUP_IN:
# EWS doesn't have an '__in' operator. Allow '__in' lookups on list and non-list field types,
# specifying a list value. We'll emulate it as a set of OR'ed exact matches.
if not is_iterable(value, generators_allowed=True):
raise ValueError("Value for lookup %r must be a list" % key)
- children = [self.__class__(**{field_path: v}) for v in value]
+ children = tuple(self.__class__(**{field_path: v}) for v in value)
if not children:
# This is an '__in' operator with an empty list as the value. We interpret it to mean "is foo
# contained in the empty set?" which is always false. Mark this Q object as such.
- return [self.__class__(conn_type=self.NEVER)]
- return [self.__class__(*children, conn_type=self.OR)]
+ return (self.__class__(conn_type=self.NEVER),)
+ return (self.__class__(*children, conn_type=self.OR),)
if lookup == self.LOOKUP_CONTAINS and is_iterable(value, generators_allowed=True):
# A '__contains' lookup with an list as the value ony makes sense for list fields, since exact match
@@ -750,8 +750,8 @@ Classes
#
# An empty list as value is allowed. We interpret it to mean "are all values in the empty set contained
# in foo?" which is always true.
- children = [self.__class__(**{field_path: v}) for v in value]
- return [self.__class__(*children, conn_type=self.AND)]
+ children = tuple(self.__class__(**{field_path: v}) for v in value)
+ return (self.__class__(*children, conn_type=self.AND),)
try:
op = self._lookup_to_op(lookup)
@@ -761,13 +761,13 @@ Classes
field_path, op = key, self.EQ
if not is_single_kwarg:
- return [self.__class__(**{key: value})]
+ return (self.__class__(**{key: value}),)
# This is a single-kwarg Q object with a lookup that requires a single value. Make this a leaf
self.field_path = field_path
self.op = op
self.value = value
- return []
+ return ()
def reduce(self):
"""Simplify this object, if possible."""
diff --git a/docs/exchangelib/services/common.html b/docs/exchangelib/services/common.html
index 53a47f9b..659ca5cf 100644
--- a/docs/exchangelib/services/common.html
+++ b/docs/exchangelib/services/common.html
@@ -116,8 +116,6 @@ Module
supported_from = None
# Marks services that support paging of requested items
supports_paging = False
- # Marks services that need affinity to the backend server
- prefer_affinity = False
def __init__(self, protocol, chunk_size=None, timeout=None):
self.chunk_size = chunk_size or CHUNK_SIZE # The number of items to send in a single request
@@ -199,13 +197,7 @@ exchangelib.services.common
Module
self.protocol.config.version = value
def _extra_headers(self, session):
- headers = {}
- if self.prefer_affinity:
- headers['X-PreferServerAffinity'] = 'True'
- for cookie in session.cookies:
- if cookie.name == 'X-BackEndCookie':
- headers['X-BackEndOverrideCookie'] = cookie.value
- return headers
+ return {}
@property
def _account_to_impersonate(self):
@@ -242,6 +234,8 @@ exchangelib.services.common
Module
yield from self._get_elements(payload=payload_func(chunk, **kwargs))
def stop_streaming(self):
+ if not self.streaming:
+ raise RuntimeError('Attempt to stop a non-streaming service')
if self._streaming_response:
self._streaming_response.close() # Release memory
self._streaming_response = None
@@ -288,10 +282,15 @@ exchangelib.services.common
Module
if self.streaming:
self.stop_streaming()
+ def _handle_response_cookies(self, session):
+ pass
+
def _get_response(self, payload, api_version):
"""Send the actual HTTP request and get the response."""
session = self.protocol.get_session()
- self._streaming_session, self._streaming_response = None, None
+ if self.streaming:
+ # Make sure to clean up lingering resources
+ self.stop_streaming()
r, session = post_ratelimited(
protocol=self.protocol,
session=session,
@@ -307,6 +306,7 @@ exchangelib.services.common
Module
stream=self.streaming,
timeout=self.timeout or self.protocol.TIMEOUT,
)
+ self._handle_response_cookies(session)
if self.streaming:
# We con only release the session when we have fully consumed the response. Save session and response
# objects for later.
@@ -318,7 +318,7 @@ exchangelib.services.common
Module
@property
def _api_versions_to_try(self):
# Put the hint first in the list, and then all other versions except the hint, from newest to oldest
- return [self._version_hint.api_version] + [v for v in API_VERSIONS if v != self._version_hint.api_version]
+ return (self._version_hint.api_version,) + tuple(v for v in API_VERSIONS if v != self._version_hint.api_version)
def _get_response_xml(self, payload, **parse_opts):
"""Send the payload to the server and return relevant elements from the result. Several things happen here:
@@ -370,7 +370,9 @@ exchangelib.services.common
Module
# In streaming mode, we may not have accessed the raw stream yet. Caller must handle this.
r.close() # Release memory
- raise self.NO_VALID_SERVER_VERSIONS('Tried versions %s but all were invalid' % self._api_versions_to_try)
+ raise self.NO_VALID_SERVER_VERSIONS(
+ 'Tried versions %s but all were invalid' % ', '.join(self._api_versions_to_try)
+ )
def _handle_backoff(self, e):
"""Take a request from the server to back off and checks the retry policy for what to do. Re-raise the
@@ -787,6 +789,8 @@ exchangelib.services.common
Module
"""Base class for services that act on items concerning a single Mailbox on the server."""
NO_VALID_SERVER_VERSIONS = ErrorInvalidSchemaVersionForMailboxVersion
+ # Marks services that need affinity to the backend server
+ prefer_affinity = False
def __init__(self, *args, **kwargs):
self.account = kwargs.pop('account')
@@ -801,11 +805,28 @@ exchangelib.services.common
Module
def _version_hint(self, value):
self.account.version = value
- def _extra_headers(self, *args, **kwargs):
- headers = super()._extra_headers(*args, **kwargs)
+ def _handle_response_cookies(self, session):
+ super()._handle_response_cookies(session=session)
+
+ # See self._extra_headers() for documentation on affinity
+ if self.prefer_affinity:
+ for cookie in session.cookies:
+ if cookie.name == 'X-BackEndOverrideCookie':
+ self.account.affinity_cookie = cookie.value
+ break
+
+ def _extra_headers(self, session):
+ headers = super()._extra_headers(session=session)
# See
# https://blogs.msdn.microsoft.com/webdav_101/2015/05/11/best-practices-ews-authentication-and-access-issues/
headers['X-AnchorMailbox'] = self.account.primary_smtp_address
+
+ # See
+ # https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-maintain-affinity-between-group-of-subscriptions-and-mailbox-server
+ if self.prefer_affinity:
+ headers['X-PreferServerAffinity'] = 'True'
+ if self.account.affinity_cookie:
+ headers['X-BackEndOverrideCookie'] = self.account.affinity_cookie
return headers
@property
@@ -1087,6 +1108,8 @@ exchangelib.services.common
Classes
"""Base class for services that act on items concerning a single Mailbox on the server."""
NO_VALID_SERVER_VERSIONS = ErrorInvalidSchemaVersionForMailboxVersion
+ # Marks services that need affinity to the backend server
+ prefer_affinity = False
def __init__(self, *args, **kwargs):
self.account = kwargs.pop('account')
@@ -1101,11 +1124,28 @@ Classes
def _version_hint(self, value):
self.account.version = value
- def _extra_headers(self, *args, **kwargs):
- headers = super()._extra_headers(*args, **kwargs)
+ def _handle_response_cookies(self, session):
+ super()._handle_response_cookies(session=session)
+
+ # See self._extra_headers() for documentation on affinity
+ if self.prefer_affinity:
+ for cookie in session.cookies:
+ if cookie.name == 'X-BackEndOverrideCookie':
+ self.account.affinity_cookie = cookie.value
+ break
+
+ def _extra_headers(self, session):
+ headers = super()._extra_headers(session=session)
# See
# https://blogs.msdn.microsoft.com/webdav_101/2015/05/11/best-practices-ews-authentication-and-access-issues/
headers['X-AnchorMailbox'] = self.account.primary_smtp_address
+
+ # See
+ # https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-maintain-affinity-between-group-of-subscriptions-and-mailbox-server
+ if self.prefer_affinity:
+ headers['X-PreferServerAffinity'] = 'True'
+ if self.account.affinity_cookie:
+ headers['X-BackEndOverrideCookie'] = self.account.affinity_cookie
return headers
@property
@@ -1160,6 +1200,13 @@ Subclasses
Class variables
+
+
var prefer_affinity
Inherited members
diff --git a/docs/exchangelib/services/index.html b/docs/exchangelib/services/index.html
index cd15d9e8..662c2a94 100644
--- a/docs/exchangelib/services/index.html
+++ b/docs/exchangelib/services/index.html
@@ -2757,8 +2757,8 @@ EWSService
:
@@ -1205,8 +1252,6 @@ Inherited members
supported_from = None
# Marks services that support paging of requested items
supports_paging = False
- # Marks services that need affinity to the backend server
- prefer_affinity = False
def __init__(self, protocol, chunk_size=None, timeout=None):
self.chunk_size = chunk_size or CHUNK_SIZE # The number of items to send in a single request
@@ -1288,13 +1333,7 @@ Inherited members
self.protocol.config.version = value
def _extra_headers(self, session):
- headers = {}
- if self.prefer_affinity:
- headers['X-PreferServerAffinity'] = 'True'
- for cookie in session.cookies:
- if cookie.name == 'X-BackEndCookie':
- headers['X-BackEndOverrideCookie'] = cookie.value
- return headers
+ return {}
@property
def _account_to_impersonate(self):
@@ -1331,6 +1370,8 @@ Inherited members
yield from self._get_elements(payload=payload_func(chunk, **kwargs))
def stop_streaming(self):
+ if not self.streaming:
+ raise RuntimeError('Attempt to stop a non-streaming service')
if self._streaming_response:
self._streaming_response.close() # Release memory
self._streaming_response = None
@@ -1377,10 +1418,15 @@ Inherited members
if self.streaming:
self.stop_streaming()
+ def _handle_response_cookies(self, session):
+ pass
+
def _get_response(self, payload, api_version):
"""Send the actual HTTP request and get the response."""
session = self.protocol.get_session()
- self._streaming_session, self._streaming_response = None, None
+ if self.streaming:
+ # Make sure to clean up lingering resources
+ self.stop_streaming()
r, session = post_ratelimited(
protocol=self.protocol,
session=session,
@@ -1396,6 +1442,7 @@ Inherited members
stream=self.streaming,
timeout=self.timeout or self.protocol.TIMEOUT,
)
+ self._handle_response_cookies(session)
if self.streaming:
# We con only release the session when we have fully consumed the response. Save session and response
# objects for later.
@@ -1407,7 +1454,7 @@ Inherited members
@property
def _api_versions_to_try(self):
# Put the hint first in the list, and then all other versions except the hint, from newest to oldest
- return [self._version_hint.api_version] + [v for v in API_VERSIONS if v != self._version_hint.api_version]
+ return (self._version_hint.api_version,) + tuple(v for v in API_VERSIONS if v != self._version_hint.api_version)
def _get_response_xml(self, payload, **parse_opts):
"""Send the payload to the server and return relevant elements from the result. Several things happen here:
@@ -1459,7 +1506,9 @@ Inherited members
# In streaming mode, we may not have accessed the raw stream yet. Caller must handle this.
r.close() # Release memory
- raise self.NO_VALID_SERVER_VERSIONS('Tried versions %s but all were invalid' % self._api_versions_to_try)
+ raise self.NO_VALID_SERVER_VERSIONS(
+ 'Tried versions %s but all were invalid' % ', '.join(self._api_versions_to_try)
+ )
def _handle_backoff(self, e):
"""Take a request from the server to back off and checks the retry policy for what to do. Re-raise the
@@ -1915,10 +1964,6 @@ Class variables
var prefer_affinity
var returns_elements
Methods
Expand source code
+ self.stop_streaming()
+ self.streaming = False
def stop_streaming(self):
+ if not self.streaming:
+ raise RuntimeError('Attempt to stop a non-streaming service')
if self._streaming_response:
self._streaming_response.close() # Release memory
self._streaming_response = None
@@ -2036,6 +2083,9 @@
Index
+
EWSAccountService
@@ -2049,7 +2099,6 @@ EWSService
get
paging_container_name
parse
prefer_affinity
returns_elements
stop_streaming
supported_from
Module
# The returned content did not contain any EWS exceptions. Give up and re-raise the original exception.
raise enf
finally:
- self.streaming = False
- self.stop_streaming()exchangelib.services.get_attachment
Classes
# The returned content did not contain any EWS exceptions. Give up and re-raise the original exception.
raise enf
finally:
- self.streaming = False
- self.stop_streaming()
+ self.stop_streaming()
+ self.streaming = False
Ancestors
@@ -350,8 +350,8 @@
Methods
# The returned content did not contain any EWS exceptions. Give up and re-raise the original exception.
raise enf
finally:
- self.streaming = False
- self.stop_streaming()
+ self.stop_streaming()
+ self.streaming = False
Module
exchangelib.services.get_streaming_events
<
import logging
from .common import EWSAccountService, add_xml_child
+from ..errors import EWSError
from ..properties import Notification
from ..util import create_element, get_xml_attr, get_xml_attrs, MNS, DocumentYielder, DummyResponse
@@ -43,7 +44,6 @@
Module
exchangelib.services.get_streaming_events
<
SERVICE_NAME = 'GetStreamingEvents'
element_container_name = '{%s}Notifications' % MNS
- streaming = True
prefer_affinity = True
# Connection status values
@@ -53,8 +53,8 @@ Module
exchangelib.services.get_streaming_events
<
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)
+ self.streaming = True
def call(self, subscription_ids, connection_timeout):
if connection_timeout < 1:
@@ -97,15 +97,23 @@ Module
exchangelib.services.get_streaming_events
<
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)
+ error_ids = [] if error_ids_elem is None else get_xml_attrs(error_ids_elem, '{%s}SubscriptionId' % MNS)
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.
+ # Upstream normally expects to find a 'name' tag but our response does not always have it. We still want to
+ # call upstream, to have exceptions raised. Return an empty list if there is no 'name' tag and no errors.
if message.find(name) is None:
- return []
- return super()._get_element_container(message=message, name=name)
+ name = None
+ try:
+ res = super()._get_element_container(message=message, name=name)
+ except EWSError as e:
+ # When the request contains a combination of good and failing subscription IDs, notifications for the good
+ # subscriptions seem to never be returned even though the XML spec allows it. This means there's no point in
+ # trying to collect any notifications here and delivering a combination of errors and return values.
+ if error_ids:
+ e.value += ' (subscription IDs: %s)' % ', '.join(repr(i) for i in error_ids)
+ raise e
+ return [] if name is None else res
def get_payload(self, subscription_ids, connection_timeout):
getstreamingevents = create_element('m:%s' % self.SERVICE_NAME)
@@ -147,7 +155,6 @@ Classes
SERVICE_NAME = 'GetStreamingEvents'
element_container_name = '{%s}Notifications' % MNS
- streaming = True
prefer_affinity = True
# Connection status values
@@ -157,8 +164,8 @@ Classes
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)
+ self.streaming = True
def call(self, subscription_ids, connection_timeout):
if connection_timeout < 1:
@@ -201,15 +208,23 @@ Classes
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)
+ error_ids = [] if error_ids_elem is None else get_xml_attrs(error_ids_elem, '{%s}SubscriptionId' % MNS)
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.
+ # Upstream normally expects to find a 'name' tag but our response does not always have it. We still want to
+ # call upstream, to have exceptions raised. Return an empty list if there is no 'name' tag and no errors.
if message.find(name) is None:
- return []
- return super()._get_element_container(message=message, name=name)
+ name = None
+ try:
+ res = super()._get_element_container(message=message, name=name)
+ except EWSError as e:
+ # When the request contains a combination of good and failing subscription IDs, notifications for the good
+ # subscriptions seem to never be returned even though the XML spec allows it. This means there's no point in
+ # trying to collect any notifications here and delivering a combination of errors and return values.
+ if error_ids:
+ e.value += ' (subscription IDs: %s)' % ', '.join(repr(i) for i in error_ids)
+ raise e
+ return [] if name is None else res
def get_payload(self, subscription_ids, connection_timeout):
getstreamingevents = create_element('m:%s' % self.SERVICE_NAME)
@@ -250,10 +265,6 @@ Class variables
var streaming
Methods
@@ -335,7 +346,6 @@
element_container_name
get_payload
prefer_affinity
streaming
Inherited members
# The returned content did not contain any EWS exceptions. Give up and re-raise the original exception.
raise enf
finally:
- self.streaming = False
- self.stop_streaming()
+ self.stop_streaming()
+ self.streaming = False
Ancestors
@@ -2860,8 +2860,8 @@
Methods
# The returned content did not contain any EWS exceptions. Give up and re-raise the original exception.
raise enf
finally:
- self.streaming = False
- self.stop_streaming()
+ self.stop_streaming()
+ self.streaming = False
@@ -4118,7 +4118,6 @@ Inherited members
SERVICE_NAME = 'GetStreamingEvents'
element_container_name = '{%s}Notifications' % MNS
- streaming = True
prefer_affinity = True
# Connection status values
@@ -4128,8 +4127,8 @@ Inherited members
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)
+ self.streaming = True
def call(self, subscription_ids, connection_timeout):
if connection_timeout < 1:
@@ -4172,15 +4171,23 @@ Inherited members
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)
+ error_ids = [] if error_ids_elem is None else get_xml_attrs(error_ids_elem, '{%s}SubscriptionId' % MNS)
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.
+ # Upstream normally expects to find a 'name' tag but our response does not always have it. We still want to
+ # call upstream, to have exceptions raised. Return an empty list if there is no 'name' tag and no errors.
if message.find(name) is None:
- return []
- return super()._get_element_container(message=message, name=name)
+ name = None
+ try:
+ res = super()._get_element_container(message=message, name=name)
+ except EWSError as e:
+ # When the request contains a combination of good and failing subscription IDs, notifications for the good
+ # subscriptions seem to never be returned even though the XML spec allows it. This means there's no point in
+ # trying to collect any notifications here and delivering a combination of errors and return values.
+ if error_ids:
+ e.value += ' (subscription IDs: %s)' % ', '.join(repr(i) for i in error_ids)
+ raise e
+ return [] if name is None else res
def get_payload(self, subscription_ids, connection_timeout):
getstreamingevents = create_element('m:%s' % self.SERVICE_NAME)
@@ -4221,10 +4228,6 @@ Class variables
var streaming
Methods
@@ -5429,6 +5432,7 @@
Inherited members
class SubscribeToPull(Subscribe):
subscription_request_elem_tag = 'm:PullSubscriptionRequest'
+ prefer_affinity = True
def call(self, folders, event_types, watermark, timeout):
yield from self._partial_call(
@@ -5453,6 +5457,10 @@
Ancestors
Class variables
+
var prefer_affinity
var subscription_request_elem_tag
Inherited members
class SubscribeToStreaming(Subscribe):
subscription_request_elem_tag = 'm:StreamingSubscriptionRequest'
+ prefer_affinity = True
def call(self, folders, event_types):
yield from self._partial_call(payload_func=self.get_payload, folders=folders, event_types=event_types)
@@ -5642,6 +5651,10 @@
Ancestors
Class variables
+
var prefer_affinity
var subscription_request_elem_tag
Inherited members
SERVICE_NAME = 'Unsubscribe'
returns_elements = False
+ prefer_affinity = True
def call(self, subscription_id):
return self._get_elements(payload=self.get_payload(subscription_id=subscription_id))
@@ -6044,6 +6058,10 @@ Class variables
var prefer_affinity
var returns_elements
element_container_name
get_payload
prefer_affinity
streaming
call
get_payload
prefer_affinity
subscription_request_elem_tag
call
get_payload
prefer_affinity
subscription_request_elem_tag
SERVICE_NAME
call
get_payload
prefer_affinity
returns_elements
Module
class SubscribeToPull(Subscribe):
subscription_request_elem_tag = 'm:PullSubscriptionRequest'
+ prefer_affinity = True
def call(self, folders, event_types, watermark, timeout):
yield from self._partial_call(
@@ -126,6 +127,7 @@ exchangelib.services.subscribe
Module
class SubscribeToStreaming(Subscribe):
subscription_request_elem_tag = 'm:StreamingSubscriptionRequest'
+ prefer_affinity = True
def call(self, folders, event_types):
yield from self._partial_call(payload_func=self.get_payload, folders=folders, event_types=event_types)
@@ -263,6 +265,7 @@ exchangelib.services.subscribe
Inherited members
+ return f'{self.primary_smtp_address} ({self.fullname})'
+ return self.primary_smtp_addressclass SubscribeToPull(Subscribe):
subscription_request_elem_tag = 'm:PullSubscriptionRequest'
+ prefer_affinity = True
def call(self, folders, event_types, watermark, timeout):
yield from self._partial_call(
@@ -287,6 +290,10 @@
Ancestors
Class variables
+
var prefer_affinity
var subscription_request_elem_tag
Inherited members
class SubscribeToStreaming(Subscribe):
subscription_request_elem_tag = 'm:StreamingSubscriptionRequest'
+ prefer_affinity = True
def call(self, folders, event_types):
yield from self._partial_call(payload_func=self.get_payload, folders=folders, event_types=event_types)
@@ -476,6 +484,10 @@
Ancestors
Class variables
+
var prefer_affinity
var subscription_request_elem_tag
call
get_payload
prefer_affinity
subscription_request_elem_tag
call
get_payload
prefer_affinity
subscription_request_elem_tag
Module
SERVICE_NAME = 'Unsubscribe'
returns_elements = False
+ prefer_affinity = True
def call(self, subscription_id):
return self._get_elements(payload=self.get_payload(subscription_id=subscription_id))
@@ -76,6 +77,7 @@ exchangelib.services.unsubscribe
Classes
SERVICE_NAME = 'Unsubscribe'
returns_elements = False
+ prefer_affinity = True
def call(self, subscription_id):
return self._get_elements(payload=self.get_payload(subscription_id=subscription_id))
@@ -96,6 +98,10 @@ Class variables
var prefer_affinity
var returns_elements
SERVICE_NAME
call
get_payload
prefer_affinity
returns_elements
Module
def get_xml_attrs(tree, name):
- return [elem.text for elem in tree.findall(name) if elem.text is not None]
+ return list(elem.text for elem in tree.findall(name) if elem.text is not None)
def value_to_xml_text(value):
@@ -1120,7 +1120,7 @@ exchangelib.util
Functions
Expand source code
+ return list(elem.text for elem in tree.findall(name) if elem.text is not None)def get_xml_attrs(tree, name):
- return [elem.text for elem in tree.findall(name) if elem.text is not None]
diff --git a/exchangelib/__init__.py b/exchangelib/__init__.py
index d8d1bf83..5ebe9cd9 100644
--- a/exchangelib/__init__.py
+++ b/exchangelib/__init__.py
@@ -18,7 +18,7 @@
from .transport import BASIC, DIGEST, NTLM, GSSAPI, SSPI, OAUTH2, CBA
from .version import Build, Version
-__version__ = '4.6.1'
+__version__ = '4.6.2'
__all__ = [
'__version__',
From 8577b8f7a534de5bd19ad7e28d6cf4be082c8750 Mon Sep 17 00:00:00 2001
From: Erik Cederstrand
Module
exchangelib.account
from locale import getlocale
+
+ return f'{self.primary_smtp_address} ({self.fullname})'
+ return self.primary_smtp_addressimport locale as stdlib_locale
from logging import getLogger
from cached_property import threaded_cached_property
@@ -34,7 +34,7 @@
Module
from .autodiscover import Autodiscovery
from .configuration import Configuration
from .credentials import DELEGATE, IMPERSONATION, ACCESS_TYPES
-from .errors import UnknownTimeZone
+from .errors import UnknownTimeZone, InvalidEnumValue, InvalidTypeError
from .ewsdatetime import EWSTimeZone, UTC
from .fields import FieldPath
from .folders import Folder, AdminAuditLogs, ArchiveDeletedItems, ArchiveInbox, ArchiveMsgFolderRoot, \
@@ -44,7 +44,7 @@ exchangelib.account
Module
Notes, Outbox, PeopleConnect, PublicFoldersRoot, QuickContacts, RecipientCache, RecoverableItemsDeletions, \
RecoverableItemsPurges, RecoverableItemsRoot, RecoverableItemsVersions, Root, SearchFolders, SentItems, \
ServerFailures, SyncIssues, Tasks, ToDoSearch, VoiceMail
-from .items import HARD_DELETE, AUTO_RESOLVE, SEND_TO_NONE, SAVE_ONLY, ALL_OCCURRENCIES, ID_ONLY
+from .items import HARD_DELETE, AUTO_RESOLVE, SEND_TO_NONE, SAVE_ONLY, ALL_OCCURRENCES, ID_ONLY
from .properties import Mailbox, SendingAs
from .protocol import Protocol
from .queryset import QuerySet
@@ -73,10 +73,7 @@ exchangelib.account
Module
self.sid = sid
def __eq__(self, other):
- for k in self.__dict__:
- if getattr(self, k) != getattr(other, k):
- return False
- return True
+ return all(getattr(self, k) == getattr(other, k) for k in self.__dict__)
def __hash__(self):
return hash(repr(self))
@@ -107,25 +104,26 @@ exchangelib.account
Module
:return:
"""
if '@' not in primary_smtp_address:
- raise ValueError("primary_smtp_address %r is not an email address" % primary_smtp_address)
+ raise ValueError(f"primary_smtp_address {primary_smtp_address!r} is not an email address")
self.fullname = fullname
# Assume delegate access if individual credentials are provided. Else, assume service user with impersonation
self.access_type = access_type or (DELEGATE if credentials else IMPERSONATION)
if self.access_type not in ACCESS_TYPES:
- raise ValueError("'access_type' %r must be one of %s" % (self.access_type, ACCESS_TYPES))
+ raise InvalidEnumValue('access_type', self.access_type, ACCESS_TYPES)
try:
- self.locale = locale or getlocale()[0] or None # get_locale() might not be able to determine the locale
+ # get_locale() might not be able to determine the locale
+ self.locale = locale or stdlib_locale.getlocale()[0] or None
except ValueError as e:
# getlocale() may throw ValueError if it fails to parse the system locale
log.warning('Failed to get locale (%s)', e)
self.locale = None
if not isinstance(self.locale, (type(None), str)):
- raise ValueError("Expected 'locale' to be a string, got %r" % self.locale)
+ raise InvalidTypeError('locale', self.locale, str)
if default_timezone:
try:
self.default_timezone = EWSTimeZone.from_timezone(default_timezone)
except TypeError:
- raise ValueError("Expected 'default_timezone' to be an EWSTimeZone, got %r" % default_timezone)
+ raise InvalidTypeError('default_timezone', default_timezone, EWSTimeZone)
else:
try:
self.default_timezone = EWSTimeZone.localzone()
@@ -135,17 +133,22 @@ exchangelib.account
Module
log.warning('%s. Fallback to UTC', e.args[0])
self.default_timezone = UTC
if not isinstance(config, (Configuration, type(None))):
- raise ValueError("Expected 'config' to be a Configuration, got %r" % config)
+ raise InvalidTypeError('config', config, Configuration)
if autodiscover:
if config:
- retry_policy, auth_type = config.retry_policy, config.auth_type
+ auth_type, retry_policy, version = config.auth_type, config.retry_policy, config.version
if not credentials:
credentials = config.credentials
else:
- retry_policy, auth_type = None, None
+ auth_type, retry_policy, version = None, None, None
self.ad_response, self.protocol = Autodiscovery(
- email=primary_smtp_address, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy
+ email=primary_smtp_address, credentials=credentials
).discover()
+ # Let's not use the auth_package hint from the AD response. It could be incorrect and we can just guess.
+ self.protocol.config.auth_type = auth_type
+ self.protocol.config.retry_policy = retry_policy
+ if not self.protocol.config.version:
+ self.protocol.config.version = version
primary_smtp_address = self.ad_response.autodiscover_smtp_address
else:
if not config:
@@ -160,8 +163,9 @@ exchangelib.account
Module
self.affinity_cookie = None
# We may need to override the default server version on a per-account basis because Microsoft may report one
- # server version up-front but delegate account requests to an older backend server.
- self.version = self.protocol.version
+ # server version up-front but delegate account requests to an older backend server. Create a new instance to
+ # avoid changing the protocol version.
+ self.version = self.protocol.version.copy()
log.debug('Added account: %s', self)
@property
@@ -479,7 +483,7 @@ exchangelib.account
Module
)))
def bulk_delete(self, ids, delete_type=HARD_DELETE, send_meeting_cancellations=SEND_TO_NONE,
- affected_task_occurrences=ALL_OCCURRENCIES, suppress_read_receipts=True, chunk_size=None):
+ affected_task_occurrences=ALL_OCCURRENCES, suppress_read_receipts=True, chunk_size=None):
"""Bulk delete items.
:param ids: an iterable of either (id, changekey) tuples or Item objects.
@@ -488,14 +492,14 @@ exchangelib.account
Module
:param send_meeting_cancellations: only applicable to CalendarItem. Possible values are specified in
SEND_MEETING_CANCELLATIONS_CHOICES. (Default value = SEND_TO_NONE)
:param affected_task_occurrences: only applicable for recurring Task items. Possible values are specified in
- AFFECTED_TASK_OCCURRENCES_CHOICES. (Default value = ALL_OCCURRENCIES)
+ AFFECTED_TASK_OCCURRENCES_CHOICES. (Default value = ALL_OCCURRENCES)
:param suppress_read_receipts: only supported from Exchange 2013. True or False. (Default value = True)
:param chunk_size: The number of items to send to the server in a single request (Default value = None)
:return: a list of either True or exception instances, in the same order as the input
"""
log.debug(
- 'Deleting items for %s (delete_type: %s, send_meeting_invitations: %s, affected_task_occurences: %s)',
+ 'Deleting items for %s (delete_type: %s, send_meeting_invitations: %s, affected_task_occurrences: %s)',
self,
delete_type,
send_meeting_cancellations,
@@ -633,15 +637,11 @@ exchangelib.account
Module
# We accept generators, so it's not always convenient for caller to know up-front if 'ids' is empty. Allow
# empty 'ids' and return early.
return
- # GetPersona only accepts one persona ID per request. Crazy.
- svc = GetPersona(account=self)
- for i in ids:
- yield svc.call(persona=i)
+ yield from GetPersona(account=self).call(personas=ids)
@property
def mail_tips(self):
"""See self.oof_settings about caching considerations."""
- # mail_tips_requested must be one of properties.MAIL_TIPS_TYPES
return GetMailTips(protocol=self.protocol).get(
sending_as=SendingAs(email_address=self.primary_smtp_address),
recipients=[Mailbox(email_address=self.primary_smtp_address)],
@@ -651,18 +651,12 @@ exchangelib.account
Module
@property
def delegates(self):
"""Return a list of DelegateUser objects representing the delegates that are set on this account."""
- delegates = []
- for d in GetDelegate(account=self).call(user_ids=None, include_permissions=True):
- if isinstance(d, Exception):
- raise d
- delegates.append(d)
- return delegates
+ return list(GetDelegate(account=self).call(user_ids=None, include_permissions=True))
def __str__(self):
- txt = '%s' % self.primary_smtp_address
if self.fullname:
- txt += ' (%s)' % self.fullname
- return txtexchangelib.account
Classes
:return:
"""
if '@' not in primary_smtp_address:
- raise ValueError("primary_smtp_address %r is not an email address" % primary_smtp_address)
+ raise ValueError(f"primary_smtp_address {primary_smtp_address!r} is not an email address")
self.fullname = fullname
# Assume delegate access if individual credentials are provided. Else, assume service user with impersonation
self.access_type = access_type or (DELEGATE if credentials else IMPERSONATION)
if self.access_type not in ACCESS_TYPES:
- raise ValueError("'access_type' %r must be one of %s" % (self.access_type, ACCESS_TYPES))
+ raise InvalidEnumValue('access_type', self.access_type, ACCESS_TYPES)
try:
- self.locale = locale or getlocale()[0] or None # get_locale() might not be able to determine the locale
+ # get_locale() might not be able to determine the locale
+ self.locale = locale or stdlib_locale.getlocale()[0] or None
except ValueError as e:
# getlocale() may throw ValueError if it fails to parse the system locale
log.warning('Failed to get locale (%s)', e)
self.locale = None
if not isinstance(self.locale, (type(None), str)):
- raise ValueError("Expected 'locale' to be a string, got %r" % self.locale)
+ raise InvalidTypeError('locale', self.locale, str)
if default_timezone:
try:
self.default_timezone = EWSTimeZone.from_timezone(default_timezone)
except TypeError:
- raise ValueError("Expected 'default_timezone' to be an EWSTimeZone, got %r" % default_timezone)
+ raise InvalidTypeError('default_timezone', default_timezone, EWSTimeZone)
else:
try:
self.default_timezone = EWSTimeZone.localzone()
@@ -747,17 +742,22 @@ Classes
log.warning('%s. Fallback to UTC', e.args[0])
self.default_timezone = UTC
if not isinstance(config, (Configuration, type(None))):
- raise ValueError("Expected 'config' to be a Configuration, got %r" % config)
+ raise InvalidTypeError('config', config, Configuration)
if autodiscover:
if config:
- retry_policy, auth_type = config.retry_policy, config.auth_type
+ auth_type, retry_policy, version = config.auth_type, config.retry_policy, config.version
if not credentials:
credentials = config.credentials
else:
- retry_policy, auth_type = None, None
+ auth_type, retry_policy, version = None, None, None
self.ad_response, self.protocol = Autodiscovery(
- email=primary_smtp_address, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy
+ email=primary_smtp_address, credentials=credentials
).discover()
+ # Let's not use the auth_package hint from the AD response. It could be incorrect and we can just guess.
+ self.protocol.config.auth_type = auth_type
+ self.protocol.config.retry_policy = retry_policy
+ if not self.protocol.config.version:
+ self.protocol.config.version = version
primary_smtp_address = self.ad_response.autodiscover_smtp_address
else:
if not config:
@@ -772,8 +772,9 @@ Classes
self.affinity_cookie = None
# We may need to override the default server version on a per-account basis because Microsoft may report one
- # server version up-front but delegate account requests to an older backend server.
- self.version = self.protocol.version
+ # server version up-front but delegate account requests to an older backend server. Create a new instance to
+ # avoid changing the protocol version.
+ self.version = self.protocol.version.copy()
log.debug('Added account: %s', self)
@property
@@ -1091,7 +1092,7 @@ Classes
)))
def bulk_delete(self, ids, delete_type=HARD_DELETE, send_meeting_cancellations=SEND_TO_NONE,
- affected_task_occurrences=ALL_OCCURRENCIES, suppress_read_receipts=True, chunk_size=None):
+ affected_task_occurrences=ALL_OCCURRENCES, suppress_read_receipts=True, chunk_size=None):
"""Bulk delete items.
:param ids: an iterable of either (id, changekey) tuples or Item objects.
@@ -1100,14 +1101,14 @@ Classes
:param send_meeting_cancellations: only applicable to CalendarItem. Possible values are specified in
SEND_MEETING_CANCELLATIONS_CHOICES. (Default value = SEND_TO_NONE)
:param affected_task_occurrences: only applicable for recurring Task items. Possible values are specified in
- AFFECTED_TASK_OCCURRENCES_CHOICES. (Default value = ALL_OCCURRENCIES)
+ AFFECTED_TASK_OCCURRENCES_CHOICES. (Default value = ALL_OCCURRENCES)
:param suppress_read_receipts: only supported from Exchange 2013. True or False. (Default value = True)
:param chunk_size: The number of items to send to the server in a single request (Default value = None)
:return: a list of either True or exception instances, in the same order as the input
"""
log.debug(
- 'Deleting items for %s (delete_type: %s, send_meeting_invitations: %s, affected_task_occurences: %s)',
+ 'Deleting items for %s (delete_type: %s, send_meeting_invitations: %s, affected_task_occurrences: %s)',
self,
delete_type,
send_meeting_cancellations,
@@ -1245,15 +1246,11 @@ Classes
# We accept generators, so it's not always convenient for caller to know up-front if 'ids' is empty. Allow
# empty 'ids' and return early.
return
- # GetPersona only accepts one persona ID per request. Crazy.
- svc = GetPersona(account=self)
- for i in ids:
- yield svc.call(persona=i)
+ yield from GetPersona(account=self).call(personas=ids)
@property
def mail_tips(self):
"""See self.oof_settings about caching considerations."""
- # mail_tips_requested must be one of properties.MAIL_TIPS_TYPES
return GetMailTips(protocol=self.protocol).get(
sending_as=SendingAs(email_address=self.primary_smtp_address),
recipients=[Mailbox(email_address=self.primary_smtp_address)],
@@ -1263,18 +1260,12 @@ Classes
@property
def delegates(self):
"""Return a list of DelegateUser objects representing the delegates that are set on this account."""
- delegates = []
- for d in GetDelegate(account=self).call(user_ids=None, include_permissions=True):
- if isinstance(d, Exception):
- raise d
- delegates.append(d)
- return delegates
+ return list(GetDelegate(account=self).call(user_ids=None, include_permissions=True))
def __str__(self):
- txt = '%s' % self.primary_smtp_address
if self.fullname:
- txt += ' (%s)' % self.fullname
- return txtInstance variables
@@ -1587,12 +1578,7 @@
Instance variables
+ return list(GetDelegate(account=self).call(user_ids=None, include_permissions=True))
@property
def delegates(self):
"""Return a list of DelegateUser objects representing the delegates that are set on this account."""
- delegates = []
- for d in GetDelegate(account=self).call(user_ids=None, include_permissions=True):
- if isinstance(d, Exception):
- raise d
- delegates.append(d)
- return delegates
var directory
Instance variables
@property
def mail_tips(self):
"""See self.oof_settings about caching considerations."""
- # mail_tips_requested must be one of properties.MAIL_TIPS_TYPES
return GetMailTips(protocol=self.protocol).get(
sending_as=SendingAs(email_address=self.primary_smtp_address),
recipients=[Mailbox(email_address=self.primary_smtp_address)],
@@ -2445,7 +2430,7 @@
Methods
:param send_meeting_cancellations: only applicable to CalendarItem. Possible values are specified in
SEND_MEETING_CANCELLATIONS_CHOICES. (Default value = SEND_TO_NONE)
:param affected_task_occurrences: only applicable for recurring Task items. Possible values are specified in
-AFFECTED_TASK_OCCURRENCES_CHOICES. (Default value = ALL_OCCURRENCIES)
+AFFECTED_TASK_OCCURRENCES_CHOICES. (Default value = ALL_OCCURRENCES)
:param suppress_read_receipts: only supported from Exchange 2013. True or False. (Default value = True)
:param chunk_size: The number of items to send to the server in a single request (Default value = None)
Methods
Expand source code
+ yield from GetPersona(account=self).call(personas=ids)def bulk_delete(self, ids, delete_type=HARD_DELETE, send_meeting_cancellations=SEND_TO_NONE,
- affected_task_occurrences=ALL_OCCURRENCIES, suppress_read_receipts=True, chunk_size=None):
+ affected_task_occurrences=ALL_OCCURRENCES, suppress_read_receipts=True, chunk_size=None):
"""Bulk delete items.
:param ids: an iterable of either (id, changekey) tuples or Item objects.
@@ -2463,14 +2448,14 @@
Methods
:param send_meeting_cancellations: only applicable to CalendarItem. Possible values are specified in
SEND_MEETING_CANCELLATIONS_CHOICES. (Default value = SEND_TO_NONE)
:param affected_task_occurrences: only applicable for recurring Task items. Possible values are specified in
- AFFECTED_TASK_OCCURRENCES_CHOICES. (Default value = ALL_OCCURRENCIES)
+ AFFECTED_TASK_OCCURRENCES_CHOICES. (Default value = ALL_OCCURRENCES)
:param suppress_read_receipts: only supported from Exchange 2013. True or False. (Default value = True)
:param chunk_size: The number of items to send to the server in a single request (Default value = None)
:return: a list of either True or exception instances, in the same order as the input
"""
log.debug(
- 'Deleting items for %s (delete_type: %s, send_meeting_invitations: %s, affected_task_occurences: %s)',
+ 'Deleting items for %s (delete_type: %s, send_meeting_invitations: %s, affected_task_occurrences: %s)',
self,
delete_type,
send_meeting_cancellations,
@@ -2736,10 +2721,7 @@ Methods
# We accept generators, so it's not always convenient for caller to know up-front if 'ids' is empty. Allow
# empty 'ids' and return early.
return
- # GetPersona only accepts one persona ID per request. Crazy.
- svc = GetPersona(account=self)
- for i in ids:
- yield svc.call(persona=i)
@@ -2824,10 +2806,7 @@
+ args_str = ', '.join(
+ f'{f.name}={getattr(self, f.name)!r}' for f in self.FIELDS if f.name not in ('_item', '_content')
+ )
+ return f'{self.__class__.__name__}({args_str})'
Methods
self.sid = sid
def __eq__(self, other):
- for k in self.__dict__:
- if getattr(self, k) != getattr(other, k):
- return False
- return True
+ return all(getattr(self, k) == getattr(other, k) for k in self.__dict__)
def __hash__(self):
return hash(repr(self))
diff --git a/docs/exchangelib/attachments.html b/docs/exchangelib/attachments.html
index 1648b859..ee303c78 100644
--- a/docs/exchangelib/attachments.html
+++ b/docs/exchangelib/attachments.html
@@ -30,10 +30,10 @@ Module
import logging
import mimetypes
+from .errors import InvalidTypeError
from .fields import BooleanField, TextField, IntegerField, URIField, DateTimeField, EWSElementField, Base64Field, \
ItemField, IdField, FieldPath
from .properties import EWSElement, EWSMeta
-from .services import GetAttachment, CreateAttachment, DeleteAttachment
log = logging.getLogger(__name__)
@@ -54,6 +54,12 @@ exchangelib.attachments
Module
root_id = IdField(field_uri=ROOT_ID_ATTR)
root_changekey = IdField(field_uri=ROOT_CHANGEKEY_ATTR)
+ def __init__(self, *args, **kwargs):
+ if not kwargs:
+ # Allow to set attributes without keyword
+ kwargs = dict(zip(self._slots_keys, args))
+ super().__init__(**kwargs)
+
class Attachment(EWSElement, metaclass=EWSMeta):
"""Base class for FileAttachment and ItemAttachment."""
@@ -76,23 +82,20 @@ exchangelib.attachments
Module
def clean(self, version=None):
from .items import Item
if self.parent_item is not None and not isinstance(self.parent_item, Item):
- raise ValueError("'parent_item' value %r must be an Item instance" % self.parent_item)
+ raise InvalidTypeError('parent_item', self.parent_item, Item)
if self.content_type is None and self.name is not None:
self.content_type = mimetypes.guess_type(self.name)[0] or 'application/octet-stream'
super().clean(version=version)
def attach(self):
+ from .services import CreateAttachment
# Adds this attachment to an item and updates the changekey of the parent item
if self.attachment_id:
raise ValueError('This attachment has already been created')
if not self.parent_item or not self.parent_item.account:
- raise ValueError('Parent item %s must have an account' % self.parent_item)
+ raise ValueError(f'Parent item {self.parent_item} must have an account')
item = CreateAttachment(account=self.parent_item.account).get(parent_item=self.parent_item, items=[self])
attachment_id = item.attachment_id
- if attachment_id.root_id != self.parent_item.id:
- raise ValueError("'root_id' vs. 'id' mismatch")
- if attachment_id.root_changekey == self.parent_item.changekey:
- raise ValueError('root_id changekey match')
self.parent_item.changekey = attachment_id.root_changekey
# EWS does not like receiving root_id and root_changekey on subsequent requests
attachment_id.root_id = None
@@ -100,17 +103,13 @@ exchangelib.attachments
Module
self.attachment_id = attachment_id
def detach(self):
+ from .services import DeleteAttachment
# Deletes an attachment remotely and updates the changekey of the parent item
if not self.attachment_id:
raise ValueError('This attachment has not been created')
if not self.parent_item or not self.parent_item.account:
- raise ValueError('Parent item %s must have an account' % self.parent_item)
- root_item_id = DeleteAttachment(account=self.parent_item.account).get(items=[self.attachment_id])
- if root_item_id.id != self.parent_item.id:
- raise ValueError("'root_item_id.id' mismatch")
- if root_item_id.changekey == self.parent_item.changekey:
- raise ValueError("'root_item_id.changekey' match")
- self.parent_item.changekey = root_item_id.changekey
+ raise ValueError(f'Parent item {self.parent_item} must have an account')
+ DeleteAttachment(account=self.parent_item.account).get(items=[self.attachment_id])
self.parent_item = None
self.attachment_id = None
@@ -121,9 +120,10 @@ exchangelib.attachments
Module
return hash(tuple(getattr(self, f) for f in self._slots_keys if f != 'parent_item'))
def __repr__(self):
- return self.__class__.__name__ + '(%s)' % ', '.join(
- '%s=%r' % (f.name, getattr(self, f.name)) for f in self.FIELDS if f.name not in ('_item', '_content')
+ args_str = ', '.join(
+ f'{f.name}={getattr(self, f.name)!r}' for f in self.FIELDS if f.name not in ('_item', '_content')
)
+ return f'{self.__class__.__name__}({args_str})'
class FileAttachment(Attachment):
@@ -152,7 +152,7 @@ exchangelib.attachments
Module
# Create a file-like object for the attachment content. We try hard to reduce memory consumption so we never
# store the full attachment content in-memory.
if not self.parent_item or not self.parent_item.account:
- raise ValueError('%s must have an account' % self.__class__.__name__)
+ raise ValueError(f'{self.__class__.__name__} must have an account')
self._fp = FileAttachmentIO(attachment=self)
@property
@@ -173,7 +173,7 @@ exchangelib.attachments
Module
def content(self, value):
"""Replace the attachment content."""
if not isinstance(value, bytes):
- raise ValueError("'value' %r must be a bytes object" % value)
+ raise InvalidTypeError('value', value, bytes)
self._content = value
@classmethod
@@ -214,13 +214,14 @@ exchangelib.attachments
Module
@property
def item(self):
from .folders import BaseFolder
+ from .services import GetAttachment
if self.attachment_id is None:
return self._item
if self._item is not None:
return self._item
# We have an ID to the data but still haven't called GetAttachment to get the actual data. Do that now.
if not self.parent_item or not self.parent_item.account:
- raise ValueError('%s must have an account' % self.__class__.__name__)
+ raise ValueError(f'{self.__class__.__name__} must have an account')
additional_fields = {
FieldPath(field=f) for f in BaseFolder.allowed_item_fields(version=self.parent_item.account.version)
}
@@ -235,7 +236,7 @@ exchangelib.attachments
Module
def item(self, value):
from .items import Item
if not isinstance(value, Item):
- raise ValueError("'value' %r must be an Item object" % value)
+ raise InvalidTypeError('value', value, Item)
self._item = value
@classmethod
@@ -251,6 +252,7 @@ exchangelib.attachments
Module
def __init__(self, attachment):
self._attachment = attachment
+ self._stream = None
self._overflow = None
def readable(self):
@@ -272,6 +274,7 @@ exchangelib.attachments
Module
return len(output)
def __enter__(self):
+ from .services import GetAttachment
self._stream = GetAttachment(account=self._attachment.parent_item.account).stream_file_content(
attachment_id=self._attachment.attachment_id
)
@@ -323,23 +326,20 @@ exchangelib.attachments
Classes
def clean(self, version=None):
from .items import Item
if self.parent_item is not None and not isinstance(self.parent_item, Item):
- raise ValueError("'parent_item' value %r must be an Item instance" % self.parent_item)
+ raise InvalidTypeError('parent_item', self.parent_item, Item)
if self.content_type is None and self.name is not None:
self.content_type = mimetypes.guess_type(self.name)[0] or 'application/octet-stream'
super().clean(version=version)
def attach(self):
+ from .services import CreateAttachment
# Adds this attachment to an item and updates the changekey of the parent item
if self.attachment_id:
raise ValueError('This attachment has already been created')
if not self.parent_item or not self.parent_item.account:
- raise ValueError('Parent item %s must have an account' % self.parent_item)
+ raise ValueError(f'Parent item {self.parent_item} must have an account')
item = CreateAttachment(account=self.parent_item.account).get(parent_item=self.parent_item, items=[self])
attachment_id = item.attachment_id
- if attachment_id.root_id != self.parent_item.id:
- raise ValueError("'root_id' vs. 'id' mismatch")
- if attachment_id.root_changekey == self.parent_item.changekey:
- raise ValueError('root_id changekey match')
self.parent_item.changekey = attachment_id.root_changekey
# EWS does not like receiving root_id and root_changekey on subsequent requests
attachment_id.root_id = None
@@ -347,17 +347,13 @@ Classes
self.attachment_id = attachment_id
def detach(self):
+ from .services import DeleteAttachment
# Deletes an attachment remotely and updates the changekey of the parent item
if not self.attachment_id:
raise ValueError('This attachment has not been created')
if not self.parent_item or not self.parent_item.account:
- raise ValueError('Parent item %s must have an account' % self.parent_item)
- root_item_id = DeleteAttachment(account=self.parent_item.account).get(items=[self.attachment_id])
- if root_item_id.id != self.parent_item.id:
- raise ValueError("'root_item_id.id' mismatch")
- if root_item_id.changekey == self.parent_item.changekey:
- raise ValueError("'root_item_id.changekey' match")
- self.parent_item.changekey = root_item_id.changekey
+ raise ValueError(f'Parent item {self.parent_item} must have an account')
+ DeleteAttachment(account=self.parent_item.account).get(items=[self.attachment_id])
self.parent_item = None
self.attachment_id = None
@@ -368,9 +364,10 @@ Classes
return hash(tuple(getattr(self, f) for f in self._slots_keys if f != 'parent_item'))
def __repr__(self):
- return self.__class__.__name__ + '(%s)' % ', '.join(
- '%s=%r' % (f.name, getattr(self, f.name)) for f in self.FIELDS if f.name not in ('_item', '_content')
- )Ancestors
@@ -439,17 +436,14 @@
Methods
Expand source code
def attach(self):
+ from .services import CreateAttachment
# Adds this attachment to an item and updates the changekey of the parent item
if self.attachment_id:
raise ValueError('This attachment has already been created')
if not self.parent_item or not self.parent_item.account:
- raise ValueError('Parent item %s must have an account' % self.parent_item)
+ raise ValueError(f'Parent item {self.parent_item} must have an account')
item = CreateAttachment(account=self.parent_item.account).get(parent_item=self.parent_item, items=[self])
attachment_id = item.attachment_id
- if attachment_id.root_id != self.parent_item.id:
- raise ValueError("'root_id' vs. 'id' mismatch")
- if attachment_id.root_changekey == self.parent_item.changekey:
- raise ValueError('root_id changekey match')
self.parent_item.changekey = attachment_id.root_changekey
# EWS does not like receiving root_id and root_changekey on subsequent requests
attachment_id.root_id = None
@@ -469,7 +463,7 @@
Methods
@@ -485,17 +479,13 @@ def clean(self, version=None):
from .items import Item
if self.parent_item is not None and not isinstance(self.parent_item, Item):
- raise ValueError("'parent_item' value %r must be an Item instance" % self.parent_item)
+ raise InvalidTypeError('parent_item', self.parent_item, Item)
if self.content_type is None and self.name is not None:
self.content_type = mimetypes.guess_type(self.name)[0] or 'application/octet-stream'
super().clean(version=version)
Methods
Expand source code
@@ -515,7 +505,7 @@ def detach(self):
+ from .services import DeleteAttachment
# Deletes an attachment remotely and updates the changekey of the parent item
if not self.attachment_id:
raise ValueError('This attachment has not been created')
if not self.parent_item or not self.parent_item.account:
- raise ValueError('Parent item %s must have an account' % self.parent_item)
- root_item_id = DeleteAttachment(account=self.parent_item.account).get(items=[self.attachment_id])
- if root_item_id.id != self.parent_item.id:
- raise ValueError("'root_item_id.id' mismatch")
- if root_item_id.changekey == self.parent_item.changekey:
- raise ValueError("'root_item_id.changekey' match")
- self.parent_item.changekey = root_item_id.changekey
+ raise ValueError(f'Parent item {self.parent_item} must have an account')
+ DeleteAttachment(account=self.parent_item.account).get(items=[self.attachment_id])
self.parent_item = None
self.attachment_id = None
Inherited members
class AttachmentId
-(**kwargs)
+(*args, **kwargs)
Inherited members
id = IdField(field_uri=ID_ATTR, is_required=True)
root_id = IdField(field_uri=ROOT_ID_ATTR)
- root_changekey = IdField(field_uri=ROOT_CHANGEKEY_ATTR)
+ root_changekey = IdField(field_uri=ROOT_CHANGEKEY_ATTR)
+
+ def __init__(self, *args, **kwargs):
+ if not kwargs:
+ # Allow to set attributes without keyword
+ kwargs = dict(zip(self._slots_keys, args))
+ super().__init__(**kwargs)
Ancestors
@@ -630,7 +626,7 @@
Inherited members
# Create a file-like object for the attachment content. We try hard to reduce memory consumption so we never
# store the full attachment content in-memory.
if not self.parent_item or not self.parent_item.account:
- raise ValueError('%s must have an account' % self.__class__.__name__)
+ raise ValueError(f'{self.__class__.__name__} must have an account')
self._fp = FileAttachmentIO(attachment=self)
@property
@@ -651,7 +647,7 @@ Inherited members
def content(self, value):
"""Replace the attachment content."""
if not isinstance(value, bytes):
- raise ValueError("'value' %r must be a bytes object" % value)
+ raise InvalidTypeError('value', value, bytes)
self._content = value
@classmethod
@@ -803,6 +799,7 @@ Inherited members
def __init__(self, attachment):
self._attachment = attachment
+ self._stream = None
self._overflow = None
def readable(self):
@@ -824,6 +821,7 @@ Inherited members
return len(output)
def __enter__(self):
+ from .services import GetAttachment
self._stream = GetAttachment(account=self._attachment.parent_item.account).stream_file_content(
attachment_id=self._attachment.attachment_id
)
@@ -919,13 +917,14 @@ Methods
@property
def item(self):
from .folders import BaseFolder
+ from .services import GetAttachment
if self.attachment_id is None:
return self._item
if self._item is not None:
return self._item
# We have an ID to the data but still haven't called GetAttachment to get the actual data. Do that now.
if not self.parent_item or not self.parent_item.account:
- raise ValueError('%s must have an account' % self.__class__.__name__)
+ raise ValueError(f'{self.__class__.__name__} must have an account')
additional_fields = {
FieldPath(field=f) for f in BaseFolder.allowed_item_fields(version=self.parent_item.account.version)
}
@@ -940,7 +939,7 @@ Methods
def item(self, value):
from .items import Item
if not isinstance(value, Item):
- raise ValueError("'value' %r must be an Item object" % value)
+ raise InvalidTypeError('value', value, Item)
self._item = value
@classmethod
@@ -998,13 +997,14 @@ Instance variables
+ return f'exchangelib.{version}.cache.{user}.py{major}{minor}'
@property
def item(self):
from .folders import BaseFolder
+ from .services import GetAttachment
if self.attachment_id is None:
return self._item
if self._item is not None:
return self._item
# We have an ID to the data but still haven't called GetAttachment to get the actual data. Do that now.
if not self.parent_item or not self.parent_item.account:
- raise ValueError('%s must have an account' % self.__class__.__name__)
+ raise ValueError(f'{self.__class__.__name__} must have an account')
additional_fields = {
FieldPath(field=f) for f in BaseFolder.allowed_item_fields(version=self.parent_item.account.version)
}
diff --git a/docs/exchangelib/autodiscover/cache.html b/docs/exchangelib/autodiscover/cache.html
index 50219073..ce548cae 100644
--- a/docs/exchangelib/autodiscover/cache.html
+++ b/docs/exchangelib/autodiscover/cache.html
@@ -54,9 +54,7 @@
Module
except KeyError:
# getuser() fails on some systems. Provide a sane default. See issue #448
user = 'exchangelib'
- return 'exchangelib.{version}.cache.{user}.py{major}{minor}'.format(
- version=version, user=user, major=major, minor=minor
- )
+ return f'exchangelib.{version}.cache.{user}.py{major}{minor}'
AUTODISCOVER_PERSISTENT_STORAGE = os.path.join(tempfile.gettempdir(), shelve_filename())
@@ -215,9 +213,7 @@ exchangelib.autodiscover.cache
Functions
except KeyError:
# getuser() fails on some systems. Provide a sane default. See issue #448
user = 'exchangelib'
- return 'exchangelib.{version}.cache.{user}.py{major}{minor}'.format(
- version=version, user=user, major=major, minor=minor
- )
diff --git a/docs/exchangelib/autodiscover/discovery.html b/docs/exchangelib/autodiscover/discovery.html
index a1fe3124..ed62ba7f 100644
--- a/docs/exchangelib/autodiscover/discovery.html
+++ b/docs/exchangelib/autodiscover/discovery.html
@@ -31,28 +31,29 @@
@@ -631,7 +611,7 @@ Module
from urllib.parse import urlparse
import dns.resolver
+import dns.name
from cached_property import threaded_cached_property
from .cache import autodiscover_cache
from .properties import Autodiscover
from .protocol import AutodiscoverProtocol
from ..configuration import Configuration
-from ..credentials import OAuth2Credentials
from ..errors import AutoDiscoverFailed, AutoDiscoverCircularRedirect, TransportError, RedirectError, UnauthorizedError
from ..protocol import Protocol, FailFast
-from ..transport import get_auth_method_from_response, DEFAULT_HEADERS, NOAUTH, OAUTH2, GSSAPI, AUTH_TYPE_MAP, \
- CREDENTIALS_REQUIRED
+from ..transport import get_auth_method_from_response, DEFAULT_HEADERS, NOAUTH, GSSAPI, AUTH_TYPE_MAP
from ..util import post_ratelimited, get_domain, get_redirect_url, _back_off_if_needed, \
- DummyResponse, CONNECTION_ERRORS, TLS_ERRORS
-from ..version import Version
+ DummyResponse, ParseError, CONNECTION_ERRORS, TLS_ERRORS
log = logging.getLogger(__name__)
def discover(email, credentials=None, auth_type=None, retry_policy=None):
- return Autodiscovery(
- email=email, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy
+ ad_response, protocol = Autodiscovery(
+ email=email, credentials=credentials
).discover()
+ protocol.config.auth_typ = auth_type
+ protocol.config.retry_policy = retry_policy
+ return ad_response, protocol
class SrvRecord:
@@ -65,10 +66,7 @@ exchangelib.autodiscover.discovery
Module
self.srv = srv
def __eq__(self, other):
- for k in self.__dict__:
- if getattr(self, k) != getattr(other, k):
- return False
- return True
+ return all(getattr(self, k) == getattr(other, k) for k in self.__dict__)
class Autodiscovery:
@@ -108,19 +106,15 @@ exchangelib.autodiscover.discovery
Module
'timeout': AutodiscoverProtocol.TIMEOUT,
}
- def __init__(self, email, credentials=None, auth_type=None, retry_policy=None):
+ def __init__(self, email, credentials=None):
"""
:param email: The email address to autodiscover
:param credentials: Credentials with authorization to make autodiscover lookups for this Account
(Default value = None)
- :param auth_type: (Default value = None)
- :param retry_policy: (Default value = None)
"""
self.email = email
self.credentials = credentials
- self.auth_type = auth_type # The auth type that the resulting protocol instance should have
- self.retry_policy = retry_policy # The retry policy that the resulting protocol instance should have
self._urls_visited = [] # Collects HTTP and Autodiscover redirects
self._redirect_count = 0
self._emails_visited = [] # Collects Autodiscover email redirects
@@ -147,6 +141,7 @@ exchangelib.autodiscover.discovery
Module
ad_response = self._step_1(hostname=domain)
else:
# This will cache the result
+ log.debug('Cache miss for key %s', cache_key)
ad_response = self._step_1(hostname=domain)
log.debug('Released autodiscover_cache_lock')
@@ -184,52 +179,33 @@ exchangelib.autodiscover.discovery
Module
return resolver
def _build_response(self, ad_response):
- ews_url = ad_response.ews_url
- if not ews_url:
- raise AutoDiscoverFailed("Response is missing an 'ews_url' value")
if not ad_response.autodiscover_smtp_address:
# Autodiscover does not always return an email address. In that case, the requesting email should be used
ad_response.user.autodiscover_smtp_address = self.email
- # Get the server version. Not all protocol entries have a server version so we cheat a bit and also look at the
- # other ones that point to the same endpoint.
- for protocol in ad_response.account.protocols:
- if not protocol.ews_url or not protocol.server_version:
- continue
- if protocol.ews_url.lower() == ews_url.lower():
- version = Version(build=protocol.server_version)
- break
- else:
- version = None
- # We may not want to use the auth_package hints in the AD response. It could be incorrect and we can just guess.
protocol = Protocol(
config=Configuration(
- service_endpoint=ews_url,
+ service_endpoint=ad_response.protocol.ews_url,
credentials=self.credentials,
- version=version,
- auth_type=self.auth_type,
- retry_policy=self.retry_policy,
+ version=ad_response.version,
+ auth_type=ad_response.protocol.auth_type,
)
)
return ad_response, protocol
def _quick(self, protocol):
- # Reset auth type and retry policy if we requested non-default values
- if self.auth_type:
- protocol.config.auth_type = self.auth_type
- if self.retry_policy:
- protocol.config.retry_policy = self.retry_policy
try:
r = self._get_authenticated_response(protocol=protocol)
except TransportError as e:
- raise AutoDiscoverFailed('Response error: %s' % e)
+ raise AutoDiscoverFailed(f'Response error: {e}')
if r.status_code == 200:
try:
ad = Autodiscover.from_bytes(bytes_content=r.content)
+ except ParseError as e:
+ raise AutoDiscoverFailed(f'Invalid response: {e}')
+ else:
return self._step_5(ad=ad)
- except ValueError as e:
- raise AutoDiscoverFailed('Invalid response: %s' % e)
- raise AutoDiscoverFailed('Invalid response code: %s' % r.status_code)
+ raise AutoDiscoverFailed(f'Invalid response code: {r.status_code}')
def _redirect_url_is_valid(self, url):
"""Three separate responses can be “Redirect responses”:
@@ -280,7 +256,7 @@ exchangelib.autodiscover.discovery
Module
if not self._is_valid_hostname(hostname):
# 'requests' is really bad at reporting that a hostname cannot be resolved. Let's check this separately.
# Don't retry on DNS errors. They will most likely be persistent.
- raise TransportError('%r has no DNS entry' % hostname)
+ raise TransportError(f'{hostname!r} has no DNS entry')
kwargs = dict(
url=url, headers=DEFAULT_HEADERS.copy(), allow_redirects=False, timeout=AutodiscoverProtocol.TIMEOUT
@@ -301,7 +277,7 @@ exchangelib.autodiscover.discovery
Module
# Don't retry on TLS errors. They will most likely be persistent.
raise TransportError(str(e))
except CONNECTION_ERRORS as e:
- r = DummyResponse(url=url, headers={}, request_headers=kwargs['headers'])
+ r = DummyResponse(url=url, request_headers=kwargs['headers'])
total_wait = time.monotonic() - t_start
if self.INITIAL_RETRY_POLICY.may_retry_on_error(response=r, wait=total_wait):
log.debug("Connection error on URL %s (retry %s, error: %s). Cool down", url, retry, e)
@@ -347,8 +323,7 @@ exchangelib.autodiscover.discovery
Module
# isn't necessarily the right endpoint to use.
raise TransportError(str(e))
except RedirectError as e:
- r = DummyResponse(url=protocol.service_endpoint, headers={'location': e.url}, request_headers=None,
- status_code=302)
+ r = DummyResponse(url=protocol.service_endpoint, headers={'location': e.url}, status_code=302)
return r
def _attempt_response(self, url):
@@ -361,11 +336,6 @@ exchangelib.autodiscover.discovery
Module
log.debug('Attempting to get a valid response from %s', url)
try:
auth_type, r = self._get_unauthenticated_response(url=url)
- if isinstance(self.credentials, OAuth2Credentials):
- # This type of credentials *must* use the OAuth auth type
- auth_type = OAUTH2
- elif self.credentials is None and auth_type in CREDENTIALS_REQUIRED:
- raise ValueError('Auth type %r was detected but no credentials were provided' % auth_type)
ad_protocol = AutodiscoverProtocol(
config=Configuration(
service_endpoint=url,
@@ -388,21 +358,22 @@ exchangelib.autodiscover.discovery
Module
if r.status_code == 200:
try:
ad = Autodiscover.from_bytes(bytes_content=r.content)
+ except ParseError as e:
+ log.debug('Invalid response: %s', e)
+ else:
# We got a valid response. Unless this is a URL redirect response, we cache the result
if ad.response is None or not ad.response.redirect_url:
cache_key = self._cache_key
log.debug('Adding cache entry for key %s: %s', cache_key, ad_protocol.service_endpoint)
autodiscover_cache[cache_key] = ad_protocol
return True, ad
- except ValueError as e:
- log.debug('Invalid response: %s', e)
return False, None
def _is_valid_hostname(self, hostname):
log.debug('Checking if %s can be looked up in DNS', hostname)
try:
self.resolver.resolve(hostname)
- except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+ except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.name.EmptyLabel):
return False
return True
@@ -422,7 +393,7 @@ exchangelib.autodiscover.discovery
Module
log.debug('Attempting to get SRV records for %s', hostname)
records = []
try:
- answers = self.resolver.resolve('%s.' % hostname, 'SRV')
+ answers = self.resolver.resolve(f'{hostname}.', 'SRV')
except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) as e:
log.debug('DNS lookup failure: %s', e)
return records
@@ -447,7 +418,7 @@ exchangelib.autodiscover.discovery
Module
:param hostname:
:return:
"""
- url = 'https://%s/Autodiscover/Autodiscover.xml' % hostname
+ url = f'https://{hostname}/Autodiscover/Autodiscover.xml'
log.info('Step 1: Trying autodiscover on %r with email %r', url, self.email)
is_valid_response, ad = self._attempt_response(url=url)
if is_valid_response:
@@ -463,7 +434,7 @@ exchangelib.autodiscover.discovery
Module
:param hostname:
:return:
"""
- url = 'https://autodiscover.%s/Autodiscover/Autodiscover.xml' % hostname
+ url = f'https://autodiscover.{hostname}/Autodiscover/Autodiscover.xml'
log.info('Step 2: Trying autodiscover on %r with email %r', url, self.email)
is_valid_response, ad = self._attempt_response(url=url)
if is_valid_response:
@@ -486,20 +457,23 @@ exchangelib.autodiscover.discovery
Module
:param hostname:
:return:
"""
- url = 'http://autodiscover.%s/Autodiscover/Autodiscover.xml' % hostname
+ url = f'http://autodiscover.{hostname}/Autodiscover/Autodiscover.xml'
log.info('Step 3: Trying autodiscover on %r with email %r', url, self.email)
try:
_, r = self._get_unauthenticated_response(url=url, method='get')
except TransportError:
- r = DummyResponse(url=url, headers={}, request_headers={})
+ r = DummyResponse(url=url)
if r.status_code in (301, 302) and 'location' in r.headers:
redirect_url = get_redirect_url(r)
if self._redirect_url_is_valid(url=redirect_url):
is_valid_response, ad = self._attempt_response(url=redirect_url)
if is_valid_response:
return self._step_5(ad=ad)
+ log.debug('Got invalid response')
return self._step_4(hostname=hostname)
+ log.debug('Got invalid redirect URL')
return self._step_4(hostname=hostname)
+ log.debug('Got no redirect URL')
return self._step_4(hostname=hostname)
def _step_4(self, hostname):
@@ -519,7 +493,7 @@ exchangelib.autodiscover.discovery
Module
:param hostname:
:return:
"""
- dns_hostname = '_autodiscover._tcp.%s' % hostname
+ dns_hostname = f'_autodiscover._tcp.{hostname}'
log.info('Step 4: Trying autodiscover on %r with email %r', dns_hostname, self.email)
srv_records = self._get_srv_records(dns_hostname)
try:
@@ -528,12 +502,14 @@ exchangelib.autodiscover.discovery
Module
srv_host = None
if not srv_host:
return self._step_6()
- redirect_url = 'https://%s/Autodiscover/Autodiscover.xml' % srv_host
+ redirect_url = f'https://{srv_host}/Autodiscover/Autodiscover.xml'
if self._redirect_url_is_valid(url=redirect_url):
is_valid_response, ad = self._attempt_response(url=redirect_url)
if is_valid_response:
return self._step_5(ad=ad)
+ log.debug('Got invalid response')
return self._step_6()
+ log.debug('Got invalid redirect URL')
return self._step_6()
def _step_5(self, ad):
@@ -562,13 +538,14 @@ exchangelib.autodiscover.discovery
Module
if ad_response.redirect_url:
log.debug('Got a redirect URL: %s', ad_response.redirect_url)
# We are diverging a bit from the protocol here. We will never get an HTTP 302 since earlier steps already
- # followed the redirects where possible. Instead, we handle retirect responses here.
+ # followed the redirects where possible. Instead, we handle redirect responses here.
if self._redirect_url_is_valid(url=ad_response.redirect_url):
is_valid_response, ad = self._attempt_response(url=ad_response.redirect_url)
if is_valid_response:
return self._step_5(ad=ad)
+ log.debug('Got invalid response')
return self._step_6()
- log.debug('Invalid redirect URL: %s', ad_response.redirect_url)
+ log.debug('Invalid redirect URL')
return self._step_6()
# This could be an email redirect. Let outer layer handle this
return ad_response
@@ -579,8 +556,8 @@ exchangelib.autodiscover.discovery
Module
future requests.
"""
raise AutoDiscoverFailed(
- 'All steps in the autodiscover protocol failed for email %r. If you think this is an error, consider doing '
- 'an official test at https://testconnectivity.microsoft.com' % self.email)
+ f'All steps in the autodiscover protocol failed for email {self.email}. If you think this is an error, '
+ f'consider doing an official test at https://testconnectivity.microsoft.com')
def _select_srv_host(srv_records):
@@ -619,9 +596,12 @@ exchangelib.autodiscover.discovery
Functions
Expand source code
+ ad_response, protocol = Autodiscovery(
+ email=email, credentials=credentials
+ ).discover()
+ protocol.config.auth_typ = auth_type
+ protocol.config.retry_policy = retry_policy
+ return ad_response, protocoldef discover(email, credentials=None, auth_type=None, retry_policy=None):
- return Autodiscovery(
- email=email, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy
- ).discover()
Classes
diff --git a/docs/exchangelib/autodiscover/index.html b/docs/exchangelib/autodiscover/index.html
index 4b434692..bdb04ad8 100644
--- a/docs/exchangelib/autodiscover/index.html
+++ b/docs/exchangelib/autodiscover/index.html
@@ -111,9 +111,12 @@
class Autodiscovery
-(email, credentials=None, auth_type=None, retry_policy=None)
+(email, credentials=None)
Classes
implementation, start by doing an official test at https://testconnectivity.microsoft.com
@@ -698,19 +674,15 @@
Classes
'timeout': AutodiscoverProtocol.TIMEOUT,
}
- def __init__(self, email, credentials=None, auth_type=None, retry_policy=None):
+ def __init__(self, email, credentials=None):
"""
:param email: The email address to autodiscover
:param credentials: Credentials with authorization to make autodiscover lookups for this Account
(Default value = None)
- :param auth_type: (Default value = None)
- :param retry_policy: (Default value = None)
"""
self.email = email
self.credentials = credentials
- self.auth_type = auth_type # The auth type that the resulting protocol instance should have
- self.retry_policy = retry_policy # The retry policy that the resulting protocol instance should have
self._urls_visited = [] # Collects HTTP and Autodiscover redirects
self._redirect_count = 0
self._emails_visited = [] # Collects Autodiscover email redirects
@@ -737,6 +709,7 @@ Classes
ad_response = self._step_1(hostname=domain)
else:
# This will cache the result
+ log.debug('Cache miss for key %s', cache_key)
ad_response = self._step_1(hostname=domain)
log.debug('Released autodiscover_cache_lock')
@@ -774,52 +747,33 @@ Classes
return resolver
def _build_response(self, ad_response):
- ews_url = ad_response.ews_url
- if not ews_url:
- raise AutoDiscoverFailed("Response is missing an 'ews_url' value")
if not ad_response.autodiscover_smtp_address:
# Autodiscover does not always return an email address. In that case, the requesting email should be used
ad_response.user.autodiscover_smtp_address = self.email
- # Get the server version. Not all protocol entries have a server version so we cheat a bit and also look at the
- # other ones that point to the same endpoint.
- for protocol in ad_response.account.protocols:
- if not protocol.ews_url or not protocol.server_version:
- continue
- if protocol.ews_url.lower() == ews_url.lower():
- version = Version(build=protocol.server_version)
- break
- else:
- version = None
- # We may not want to use the auth_package hints in the AD response. It could be incorrect and we can just guess.
protocol = Protocol(
config=Configuration(
- service_endpoint=ews_url,
+ service_endpoint=ad_response.protocol.ews_url,
credentials=self.credentials,
- version=version,
- auth_type=self.auth_type,
- retry_policy=self.retry_policy,
+ version=ad_response.version,
+ auth_type=ad_response.protocol.auth_type,
)
)
return ad_response, protocol
def _quick(self, protocol):
- # Reset auth type and retry policy if we requested non-default values
- if self.auth_type:
- protocol.config.auth_type = self.auth_type
- if self.retry_policy:
- protocol.config.retry_policy = self.retry_policy
try:
r = self._get_authenticated_response(protocol=protocol)
except TransportError as e:
- raise AutoDiscoverFailed('Response error: %s' % e)
+ raise AutoDiscoverFailed(f'Response error: {e}')
if r.status_code == 200:
try:
ad = Autodiscover.from_bytes(bytes_content=r.content)
+ except ParseError as e:
+ raise AutoDiscoverFailed(f'Invalid response: {e}')
+ else:
return self._step_5(ad=ad)
- except ValueError as e:
- raise AutoDiscoverFailed('Invalid response: %s' % e)
- raise AutoDiscoverFailed('Invalid response code: %s' % r.status_code)
+ raise AutoDiscoverFailed(f'Invalid response code: {r.status_code}')
def _redirect_url_is_valid(self, url):
"""Three separate responses can be “Redirect responses”:
@@ -870,7 +824,7 @@ Classes
if not self._is_valid_hostname(hostname):
# 'requests' is really bad at reporting that a hostname cannot be resolved. Let's check this separately.
# Don't retry on DNS errors. They will most likely be persistent.
- raise TransportError('%r has no DNS entry' % hostname)
+ raise TransportError(f'{hostname!r} has no DNS entry')
kwargs = dict(
url=url, headers=DEFAULT_HEADERS.copy(), allow_redirects=False, timeout=AutodiscoverProtocol.TIMEOUT
@@ -891,7 +845,7 @@ Classes
# Don't retry on TLS errors. They will most likely be persistent.
raise TransportError(str(e))
except CONNECTION_ERRORS as e:
- r = DummyResponse(url=url, headers={}, request_headers=kwargs['headers'])
+ r = DummyResponse(url=url, request_headers=kwargs['headers'])
total_wait = time.monotonic() - t_start
if self.INITIAL_RETRY_POLICY.may_retry_on_error(response=r, wait=total_wait):
log.debug("Connection error on URL %s (retry %s, error: %s). Cool down", url, retry, e)
@@ -937,8 +891,7 @@ Classes
# isn't necessarily the right endpoint to use.
raise TransportError(str(e))
except RedirectError as e:
- r = DummyResponse(url=protocol.service_endpoint, headers={'location': e.url}, request_headers=None,
- status_code=302)
+ r = DummyResponse(url=protocol.service_endpoint, headers={'location': e.url}, status_code=302)
return r
def _attempt_response(self, url):
@@ -951,11 +904,6 @@ Classes
log.debug('Attempting to get a valid response from %s', url)
try:
auth_type, r = self._get_unauthenticated_response(url=url)
- if isinstance(self.credentials, OAuth2Credentials):
- # This type of credentials *must* use the OAuth auth type
- auth_type = OAUTH2
- elif self.credentials is None and auth_type in CREDENTIALS_REQUIRED:
- raise ValueError('Auth type %r was detected but no credentials were provided' % auth_type)
ad_protocol = AutodiscoverProtocol(
config=Configuration(
service_endpoint=url,
@@ -978,21 +926,22 @@ Classes
if r.status_code == 200:
try:
ad = Autodiscover.from_bytes(bytes_content=r.content)
+ except ParseError as e:
+ log.debug('Invalid response: %s', e)
+ else:
# We got a valid response. Unless this is a URL redirect response, we cache the result
if ad.response is None or not ad.response.redirect_url:
cache_key = self._cache_key
log.debug('Adding cache entry for key %s: %s', cache_key, ad_protocol.service_endpoint)
autodiscover_cache[cache_key] = ad_protocol
return True, ad
- except ValueError as e:
- log.debug('Invalid response: %s', e)
return False, None
def _is_valid_hostname(self, hostname):
log.debug('Checking if %s can be looked up in DNS', hostname)
try:
self.resolver.resolve(hostname)
- except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+ except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.name.EmptyLabel):
return False
return True
@@ -1012,7 +961,7 @@ Classes
log.debug('Attempting to get SRV records for %s', hostname)
records = []
try:
- answers = self.resolver.resolve('%s.' % hostname, 'SRV')
+ answers = self.resolver.resolve(f'{hostname}.', 'SRV')
except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) as e:
log.debug('DNS lookup failure: %s', e)
return records
@@ -1037,7 +986,7 @@ Classes
:param hostname:
:return:
"""
- url = 'https://%s/Autodiscover/Autodiscover.xml' % hostname
+ url = f'https://{hostname}/Autodiscover/Autodiscover.xml'
log.info('Step 1: Trying autodiscover on %r with email %r', url, self.email)
is_valid_response, ad = self._attempt_response(url=url)
if is_valid_response:
@@ -1053,7 +1002,7 @@ Classes
:param hostname:
:return:
"""
- url = 'https://autodiscover.%s/Autodiscover/Autodiscover.xml' % hostname
+ url = f'https://autodiscover.{hostname}/Autodiscover/Autodiscover.xml'
log.info('Step 2: Trying autodiscover on %r with email %r', url, self.email)
is_valid_response, ad = self._attempt_response(url=url)
if is_valid_response:
@@ -1076,20 +1025,23 @@ Classes
:param hostname:
:return:
"""
- url = 'http://autodiscover.%s/Autodiscover/Autodiscover.xml' % hostname
+ url = f'http://autodiscover.{hostname}/Autodiscover/Autodiscover.xml'
log.info('Step 3: Trying autodiscover on %r with email %r', url, self.email)
try:
_, r = self._get_unauthenticated_response(url=url, method='get')
except TransportError:
- r = DummyResponse(url=url, headers={}, request_headers={})
+ r = DummyResponse(url=url)
if r.status_code in (301, 302) and 'location' in r.headers:
redirect_url = get_redirect_url(r)
if self._redirect_url_is_valid(url=redirect_url):
is_valid_response, ad = self._attempt_response(url=redirect_url)
if is_valid_response:
return self._step_5(ad=ad)
+ log.debug('Got invalid response')
return self._step_4(hostname=hostname)
+ log.debug('Got invalid redirect URL')
return self._step_4(hostname=hostname)
+ log.debug('Got no redirect URL')
return self._step_4(hostname=hostname)
def _step_4(self, hostname):
@@ -1109,7 +1061,7 @@ Classes
:param hostname:
:return:
"""
- dns_hostname = '_autodiscover._tcp.%s' % hostname
+ dns_hostname = f'_autodiscover._tcp.{hostname}'
log.info('Step 4: Trying autodiscover on %r with email %r', dns_hostname, self.email)
srv_records = self._get_srv_records(dns_hostname)
try:
@@ -1118,12 +1070,14 @@ Classes
srv_host = None
if not srv_host:
return self._step_6()
- redirect_url = 'https://%s/Autodiscover/Autodiscover.xml' % srv_host
+ redirect_url = f'https://{srv_host}/Autodiscover/Autodiscover.xml'
if self._redirect_url_is_valid(url=redirect_url):
is_valid_response, ad = self._attempt_response(url=redirect_url)
if is_valid_response:
return self._step_5(ad=ad)
+ log.debug('Got invalid response')
return self._step_6()
+ log.debug('Got invalid redirect URL')
return self._step_6()
def _step_5(self, ad):
@@ -1152,13 +1106,14 @@ Classes
if ad_response.redirect_url:
log.debug('Got a redirect URL: %s', ad_response.redirect_url)
# We are diverging a bit from the protocol here. We will never get an HTTP 302 since earlier steps already
- # followed the redirects where possible. Instead, we handle retirect responses here.
+ # followed the redirects where possible. Instead, we handle redirect responses here.
if self._redirect_url_is_valid(url=ad_response.redirect_url):
is_valid_response, ad = self._attempt_response(url=ad_response.redirect_url)
if is_valid_response:
return self._step_5(ad=ad)
+ log.debug('Got invalid response')
return self._step_6()
- log.debug('Invalid redirect URL: %s', ad_response.redirect_url)
+ log.debug('Invalid redirect URL')
return self._step_6()
# This could be an email redirect. Let outer layer handle this
return ad_response
@@ -1169,8 +1124,8 @@ Classes
future requests.
"""
raise AutoDiscoverFailed(
- 'All steps in the autodiscover protocol failed for email %r. If you think this is an error, consider doing '
- 'an official test at https://testconnectivity.microsoft.com' % self.email)
+ f'All steps in the autodiscover protocol failed for email {self.email}. If you think this is an error, '
+ f'consider doing an official test at https://testconnectivity.microsoft.com')
Class variables
@@ -1270,6 +1225,7 @@
Methods
ad_response = self._step_1(hostname=domain)
else:
# This will cache the result
+ log.debug('Cache miss for key %s', cache_key)
ad_response = self._step_1(hostname=domain)
log.debug('Released autodiscover_cache_lock')
@@ -1309,10 +1265,7 @@ Methods
self.srv = srv
def __eq__(self, other):
- for k in self.__dict__:
- if getattr(self, k) != getattr(other, k):
- return False
- return True
+ return all(getattr(self, k) == getattr(other, k) for k in self.__dict__)
Functions
Expand source code
+ ad_response, protocol = Autodiscovery(
+ email=email, credentials=credentials
+ ).discover()
+ protocol.config.auth_typ = auth_type
+ protocol.config.retry_policy = retry_policy
+ return ad_response, protocol
@@ -293,12 +296,9 @@ def discover(email, credentials=None, auth_type=None, retry_policy=None):
- return Autodiscovery(
- email=email, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy
- ).discover()
Methods
TIMEOUT = 10 # Seconds
def __str__(self):
- return '''\
-Autodiscover endpoint: %s
-Auth type: %s''' % (
- self.service_endpoint,
- self.auth_type,
- )
+ return f'''\
+Autodiscover endpoint: {self.service_endpoint}
+Auth type: {self.auth_type}'''
Ancestors
@@ -324,7 +324,7 @@
Inherited members
class Autodiscovery
-(email, credentials=None, auth_type=None, retry_policy=None)
+(email, credentials=None)
Inherited members
implementation, start by doing an official test at https://testconnectivity.microsoft.com
@@ -391,19 +387,15 @@
Inherited members
'timeout': AutodiscoverProtocol.TIMEOUT,
}
- def __init__(self, email, credentials=None, auth_type=None, retry_policy=None):
+ def __init__(self, email, credentials=None):
"""
:param email: The email address to autodiscover
:param credentials: Credentials with authorization to make autodiscover lookups for this Account
(Default value = None)
- :param auth_type: (Default value = None)
- :param retry_policy: (Default value = None)
"""
self.email = email
self.credentials = credentials
- self.auth_type = auth_type # The auth type that the resulting protocol instance should have
- self.retry_policy = retry_policy # The retry policy that the resulting protocol instance should have
self._urls_visited = [] # Collects HTTP and Autodiscover redirects
self._redirect_count = 0
self._emails_visited = [] # Collects Autodiscover email redirects
@@ -430,6 +422,7 @@ Inherited members
ad_response = self._step_1(hostname=domain)
else:
# This will cache the result
+ log.debug('Cache miss for key %s', cache_key)
ad_response = self._step_1(hostname=domain)
log.debug('Released autodiscover_cache_lock')
@@ -467,52 +460,33 @@ Inherited members
return resolver
def _build_response(self, ad_response):
- ews_url = ad_response.ews_url
- if not ews_url:
- raise AutoDiscoverFailed("Response is missing an 'ews_url' value")
if not ad_response.autodiscover_smtp_address:
# Autodiscover does not always return an email address. In that case, the requesting email should be used
ad_response.user.autodiscover_smtp_address = self.email
- # Get the server version. Not all protocol entries have a server version so we cheat a bit and also look at the
- # other ones that point to the same endpoint.
- for protocol in ad_response.account.protocols:
- if not protocol.ews_url or not protocol.server_version:
- continue
- if protocol.ews_url.lower() == ews_url.lower():
- version = Version(build=protocol.server_version)
- break
- else:
- version = None
- # We may not want to use the auth_package hints in the AD response. It could be incorrect and we can just guess.
protocol = Protocol(
config=Configuration(
- service_endpoint=ews_url,
+ service_endpoint=ad_response.protocol.ews_url,
credentials=self.credentials,
- version=version,
- auth_type=self.auth_type,
- retry_policy=self.retry_policy,
+ version=ad_response.version,
+ auth_type=ad_response.protocol.auth_type,
)
)
return ad_response, protocol
def _quick(self, protocol):
- # Reset auth type and retry policy if we requested non-default values
- if self.auth_type:
- protocol.config.auth_type = self.auth_type
- if self.retry_policy:
- protocol.config.retry_policy = self.retry_policy
try:
r = self._get_authenticated_response(protocol=protocol)
except TransportError as e:
- raise AutoDiscoverFailed('Response error: %s' % e)
+ raise AutoDiscoverFailed(f'Response error: {e}')
if r.status_code == 200:
try:
ad = Autodiscover.from_bytes(bytes_content=r.content)
+ except ParseError as e:
+ raise AutoDiscoverFailed(f'Invalid response: {e}')
+ else:
return self._step_5(ad=ad)
- except ValueError as e:
- raise AutoDiscoverFailed('Invalid response: %s' % e)
- raise AutoDiscoverFailed('Invalid response code: %s' % r.status_code)
+ raise AutoDiscoverFailed(f'Invalid response code: {r.status_code}')
def _redirect_url_is_valid(self, url):
"""Three separate responses can be “Redirect responses”:
@@ -563,7 +537,7 @@ Inherited members
if not self._is_valid_hostname(hostname):
# 'requests' is really bad at reporting that a hostname cannot be resolved. Let's check this separately.
# Don't retry on DNS errors. They will most likely be persistent.
- raise TransportError('%r has no DNS entry' % hostname)
+ raise TransportError(f'{hostname!r} has no DNS entry')
kwargs = dict(
url=url, headers=DEFAULT_HEADERS.copy(), allow_redirects=False, timeout=AutodiscoverProtocol.TIMEOUT
@@ -584,7 +558,7 @@ Inherited members
# Don't retry on TLS errors. They will most likely be persistent.
raise TransportError(str(e))
except CONNECTION_ERRORS as e:
- r = DummyResponse(url=url, headers={}, request_headers=kwargs['headers'])
+ r = DummyResponse(url=url, request_headers=kwargs['headers'])
total_wait = time.monotonic() - t_start
if self.INITIAL_RETRY_POLICY.may_retry_on_error(response=r, wait=total_wait):
log.debug("Connection error on URL %s (retry %s, error: %s). Cool down", url, retry, e)
@@ -630,8 +604,7 @@ Inherited members
# isn't necessarily the right endpoint to use.
raise TransportError(str(e))
except RedirectError as e:
- r = DummyResponse(url=protocol.service_endpoint, headers={'location': e.url}, request_headers=None,
- status_code=302)
+ r = DummyResponse(url=protocol.service_endpoint, headers={'location': e.url}, status_code=302)
return r
def _attempt_response(self, url):
@@ -644,11 +617,6 @@ Inherited members
log.debug('Attempting to get a valid response from %s', url)
try:
auth_type, r = self._get_unauthenticated_response(url=url)
- if isinstance(self.credentials, OAuth2Credentials):
- # This type of credentials *must* use the OAuth auth type
- auth_type = OAUTH2
- elif self.credentials is None and auth_type in CREDENTIALS_REQUIRED:
- raise ValueError('Auth type %r was detected but no credentials were provided' % auth_type)
ad_protocol = AutodiscoverProtocol(
config=Configuration(
service_endpoint=url,
@@ -671,21 +639,22 @@ Inherited members
if r.status_code == 200:
try:
ad = Autodiscover.from_bytes(bytes_content=r.content)
+ except ParseError as e:
+ log.debug('Invalid response: %s', e)
+ else:
# We got a valid response. Unless this is a URL redirect response, we cache the result
if ad.response is None or not ad.response.redirect_url:
cache_key = self._cache_key
log.debug('Adding cache entry for key %s: %s', cache_key, ad_protocol.service_endpoint)
autodiscover_cache[cache_key] = ad_protocol
return True, ad
- except ValueError as e:
- log.debug('Invalid response: %s', e)
return False, None
def _is_valid_hostname(self, hostname):
log.debug('Checking if %s can be looked up in DNS', hostname)
try:
self.resolver.resolve(hostname)
- except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+ except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.name.EmptyLabel):
return False
return True
@@ -705,7 +674,7 @@ Inherited members
log.debug('Attempting to get SRV records for %s', hostname)
records = []
try:
- answers = self.resolver.resolve('%s.' % hostname, 'SRV')
+ answers = self.resolver.resolve(f'{hostname}.', 'SRV')
except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) as e:
log.debug('DNS lookup failure: %s', e)
return records
@@ -730,7 +699,7 @@ Inherited members
:param hostname:
:return:
"""
- url = 'https://%s/Autodiscover/Autodiscover.xml' % hostname
+ url = f'https://{hostname}/Autodiscover/Autodiscover.xml'
log.info('Step 1: Trying autodiscover on %r with email %r', url, self.email)
is_valid_response, ad = self._attempt_response(url=url)
if is_valid_response:
@@ -746,7 +715,7 @@ Inherited members
:param hostname:
:return:
"""
- url = 'https://autodiscover.%s/Autodiscover/Autodiscover.xml' % hostname
+ url = f'https://autodiscover.{hostname}/Autodiscover/Autodiscover.xml'
log.info('Step 2: Trying autodiscover on %r with email %r', url, self.email)
is_valid_response, ad = self._attempt_response(url=url)
if is_valid_response:
@@ -769,20 +738,23 @@ Inherited members
:param hostname:
:return:
"""
- url = 'http://autodiscover.%s/Autodiscover/Autodiscover.xml' % hostname
+ url = f'http://autodiscover.{hostname}/Autodiscover/Autodiscover.xml'
log.info('Step 3: Trying autodiscover on %r with email %r', url, self.email)
try:
_, r = self._get_unauthenticated_response(url=url, method='get')
except TransportError:
- r = DummyResponse(url=url, headers={}, request_headers={})
+ r = DummyResponse(url=url)
if r.status_code in (301, 302) and 'location' in r.headers:
redirect_url = get_redirect_url(r)
if self._redirect_url_is_valid(url=redirect_url):
is_valid_response, ad = self._attempt_response(url=redirect_url)
if is_valid_response:
return self._step_5(ad=ad)
+ log.debug('Got invalid response')
return self._step_4(hostname=hostname)
+ log.debug('Got invalid redirect URL')
return self._step_4(hostname=hostname)
+ log.debug('Got no redirect URL')
return self._step_4(hostname=hostname)
def _step_4(self, hostname):
@@ -802,7 +774,7 @@ Inherited members
:param hostname:
:return:
"""
- dns_hostname = '_autodiscover._tcp.%s' % hostname
+ dns_hostname = f'_autodiscover._tcp.{hostname}'
log.info('Step 4: Trying autodiscover on %r with email %r', dns_hostname, self.email)
srv_records = self._get_srv_records(dns_hostname)
try:
@@ -811,12 +783,14 @@ Inherited members
srv_host = None
if not srv_host:
return self._step_6()
- redirect_url = 'https://%s/Autodiscover/Autodiscover.xml' % srv_host
+ redirect_url = f'https://{srv_host}/Autodiscover/Autodiscover.xml'
if self._redirect_url_is_valid(url=redirect_url):
is_valid_response, ad = self._attempt_response(url=redirect_url)
if is_valid_response:
return self._step_5(ad=ad)
+ log.debug('Got invalid response')
return self._step_6()
+ log.debug('Got invalid redirect URL')
return self._step_6()
def _step_5(self, ad):
@@ -845,13 +819,14 @@ Inherited members
if ad_response.redirect_url:
log.debug('Got a redirect URL: %s', ad_response.redirect_url)
# We are diverging a bit from the protocol here. We will never get an HTTP 302 since earlier steps already
- # followed the redirects where possible. Instead, we handle retirect responses here.
+ # followed the redirects where possible. Instead, we handle redirect responses here.
if self._redirect_url_is_valid(url=ad_response.redirect_url):
is_valid_response, ad = self._attempt_response(url=ad_response.redirect_url)
if is_valid_response:
return self._step_5(ad=ad)
+ log.debug('Got invalid response')
return self._step_6()
- log.debug('Invalid redirect URL: %s', ad_response.redirect_url)
+ log.debug('Invalid redirect URL')
return self._step_6()
# This could be an email redirect. Let outer layer handle this
return ad_response
@@ -862,8 +837,8 @@ Inherited members
future requests.
"""
raise AutoDiscoverFailed(
- 'All steps in the autodiscover protocol failed for email %r. If you think this is an error, consider doing '
- 'an official test at https://testconnectivity.microsoft.com' % self.email)
+ f'All steps in the autodiscover protocol failed for email {self.email}. If you think this is an error, '
+ f'consider doing an official test at https://testconnectivity.microsoft.com')
Class variables
@@ -963,6 +938,7 @@
Methods
ad_response = self._step_1(hostname=domain)
else:
# This will cache the result
+ log.debug('Cache miss for key %s', cache_key)
ad_response = self._step_1(hostname=domain)
log.debug('Released autodiscover_cache_lock')
diff --git a/docs/exchangelib/autodiscover/properties.html b/docs/exchangelib/autodiscover/properties.html
index f25380a3..a5955442 100644
--- a/docs/exchangelib/autodiscover/properties.html
+++ b/docs/exchangelib/autodiscover/properties.html
@@ -33,6 +33,7 @@ Module
from ..transport import DEFAULT_ENCODING, NOAUTH, NTLM, BASIC, GSSAPI, SSPI, CBA
from ..util import create_element, add_xml_child, to_xml, is_xml, xml_to_str, AUTODISCOVER_REQUEST_NS, \
AUTODISCOVER_BASE_NS, AUTODISCOVER_RESPONSE_NS as RNS, ParseError
+from ..version import Version
class AutodiscoverBase(EWSElement):
@@ -171,6 +172,8 @@ exchangelib.autodiscover.properties
Module
# Translates 'auth_package' value to our own 'auth_type' enum vals
if not self.auth_required:
return NOAUTH
+ if not self.auth_package:
+ return None
return {
# Missing in list are DIGEST and OAUTH2
'basic': BASIC,
@@ -181,7 +184,7 @@ exchangelib.autodiscover.properties
Module
'negotiate': SSPI, # Unsure about this one
'nego2': GSSAPI,
'anonymous': NOAUTH, # Seen in some docs even though it's not mentioned in MSDN
- }.get(self.auth_package.lower(), NTLM) # Default to NTLM
+ }.get(self.auth_package.lower())
class Error(EWSElement):
@@ -220,7 +223,7 @@ exchangelib.autodiscover.properties
Module
@classmethod
def from_xml(cls, elem, account):
kwargs = {}
- public_folder_information = elem.find('{%s}PublicFolderInformation' % cls.NAMESPACE)
+ public_folder_information = elem.find(f'{{{cls.NAMESPACE}}}PublicFolderInformation')
for f in cls.FIELDS:
if f.name == 'public_folder_smtp_address':
if public_folder_information is None:
@@ -269,24 +272,35 @@ exchangelib.autodiscover.properties
Module
return None
@property
- def ews_url(self):
- """Return the EWS URL contained in the response.
+ def version(self):
+ # Get the server version. Not all protocol entries have a server version so we cheat a bit and also look at the
+ # other ones that point to the same endpoint.
+ ews_url = self.protocol.ews_url
+ for protocol in self.account.protocols:
+ if not protocol.ews_url or not protocol.server_version:
+ continue
+ if protocol.ews_url.lower() == ews_url.lower():
+ return Version(build=protocol.server_version)
+ return None
+
+ @property
+ def protocol(self):
+ """Return the protocol containing an EWS URL.
A response may contain a number of possible protocol types. EXPR is meant for EWS. See
https://techcommunity.microsoft.com/t5/blogs/blogarticleprintpage/blog-id/Exchange/article-id/16
We allow fallback to EXCH if EXPR is not available, to support installations where EXPR is not available.
- Additionally, some responses may contain and EXPR with no EWS URL. In that case, return the URL from EXCH, if
- available.
+ Additionally, some responses may contain an EXPR with no EWS URL. In that case, return EXCH, if available.
"""
protocols = {p.type: p for p in self.account.protocols if p.ews_url}
if Protocol.EXPR in protocols:
- return protocols[Protocol.EXPR].ews_url
+ return protocols[Protocol.EXPR]
if Protocol.EXCH in protocols:
- return protocols[Protocol.EXCH].ews_url
+ return protocols[Protocol.EXCH]
raise ValueError(
- 'No EWS URL found in any of the available protocols: %s' % [str(p) for p in self.account.protocols]
+ f'No EWS URL found in any of the available protocols: {[str(p) for p in self.account.protocols]}'
)
@@ -322,14 +336,11 @@ exchangelib.autodiscover.properties
Module
:param bytes_content:
:return:
"""
- if not is_xml(bytes_content) and not is_xml(bytes_content, expected_prefix=b'<Autodiscover '):
- raise ValueError('Response is not XML: %s' % bytes_content)
- try:
- root = to_xml(bytes_content).getroot()
- except ParseError:
- raise ValueError('Error parsing XML: %s' % bytes_content)
+ if not is_xml(bytes_content):
+ raise ParseError(f'Response is not XML: {bytes_content}', '<not from file>', -1, 0)
+ root = to_xml(bytes_content).getroot() # May raise ParseError
if root.tag != cls.response_tag():
- raise ValueError('Unknown root element in XML: %s' % bytes_content)
+ raise ParseError(f'Unknown root element in XML: {bytes_content}', '<not from file>', -1, 0)
return cls.from_xml(elem=root, account=None)
def raise_errors(self):
@@ -339,9 +350,9 @@ exchangelib.autodiscover.properties
Module
message = self.error_response.error.message
if message in ('The e-mail address cannot be found.', "The email address can't be found."):
raise ErrorNonExistentMailbox('The SMTP address has no mailbox associated with it')
- raise AutoDiscoverFailed('Unknown error %s: %s' % (errorcode, message))
+ raise AutoDiscoverFailed(f'Unknown error {errorcode}: {message}')
except AttributeError:
- raise AutoDiscoverFailed('Unknown autodiscover error response: %s' % self)
+ raise AutoDiscoverFailed(f'Unknown autodiscover error response: {self.error_response}')
@staticmethod
def payload(email):
@@ -396,7 +407,7 @@ exchangelib.autodiscover.properties
Classes
@classmethod
def from_xml(cls, elem, account):
kwargs = {}
- public_folder_information = elem.find('{%s}PublicFolderInformation' % cls.NAMESPACE)
+ public_folder_information = elem.find(f'{{{cls.NAMESPACE}}}PublicFolderInformation')
for f in cls.FIELDS:
if f.name == 'public_folder_smtp_address':
if public_folder_information is None:
@@ -453,7 +464,7 @@ Static methods
@classmethod
def from_xml(cls, elem, account):
kwargs = {}
- public_folder_information = elem.find('{%s}PublicFolderInformation' % cls.NAMESPACE)
+ public_folder_information = elem.find(f'{{{cls.NAMESPACE}}}PublicFolderInformation')
for f in cls.FIELDS:
if f.name == 'public_folder_smtp_address':
if public_folder_information is None:
@@ -587,14 +598,11 @@
Inherited members
:param bytes_content:
:return:
"""
- if not is_xml(bytes_content) and not is_xml(bytes_content, expected_prefix=b'<Autodiscover '):
- raise ValueError('Response is not XML: %s' % bytes_content)
- try:
- root = to_xml(bytes_content).getroot()
- except ParseError:
- raise ValueError('Error parsing XML: %s' % bytes_content)
+ if not is_xml(bytes_content):
+ raise ParseError(f'Response is not XML: {bytes_content}', '<not from file>', -1, 0)
+ root = to_xml(bytes_content).getroot() # May raise ParseError
if root.tag != cls.response_tag():
- raise ValueError('Unknown root element in XML: %s' % bytes_content)
+ raise ParseError(f'Unknown root element in XML: {bytes_content}', '<not from file>', -1, 0)
return cls.from_xml(elem=root, account=None)
def raise_errors(self):
@@ -604,9 +612,9 @@ Inherited members
message = self.error_response.error.message
if message in ('The e-mail address cannot be found.', "The email address can't be found."):
raise ErrorNonExistentMailbox('The SMTP address has no mailbox associated with it')
- raise AutoDiscoverFailed('Unknown error %s: %s' % (errorcode, message))
+ raise AutoDiscoverFailed(f'Unknown error {errorcode}: {message}')
except AttributeError:
- raise AutoDiscoverFailed('Unknown autodiscover error response: %s' % self)
+ raise AutoDiscoverFailed(f'Unknown autodiscover error response: {self.error_response}')
@staticmethod
def payload(email):
@@ -659,14 +667,11 @@ Static methods
:param bytes_content:
:return:
"""
- if not is_xml(bytes_content) and not is_xml(bytes_content, expected_prefix=b'<Autodiscover '):
- raise ValueError('Response is not XML: %s' % bytes_content)
- try:
- root = to_xml(bytes_content).getroot()
- except ParseError:
- raise ValueError('Error parsing XML: %s' % bytes_content)
+ if not is_xml(bytes_content):
+ raise ParseError(f'Response is not XML: {bytes_content}', '<not from file>', -1, 0)
+ root = to_xml(bytes_content).getroot() # May raise ParseError
if root.tag != cls.response_tag():
- raise ValueError('Unknown root element in XML: %s' % bytes_content)
+ raise ParseError(f'Unknown root element in XML: {bytes_content}', '<not from file>', -1, 0)
return cls.from_xml(elem=root, account=None)Methods
message = self.error_response.error.message
if message in ('The e-mail address cannot be found.', "The email address can't be found."):
raise ErrorNonExistentMailbox('The SMTP address has no mailbox associated with it')
- raise AutoDiscoverFailed('Unknown error %s: %s' % (errorcode, message))
+ raise AutoDiscoverFailed(f'Unknown error {errorcode}: {message}')
except AttributeError:
- raise AutoDiscoverFailed('Unknown autodiscover error response: %s' % self)
+ raise AutoDiscoverFailed(f'Unknown autodiscover error response: {self.error_response}')
@@ -1284,6 +1289,8 @@ Inherited members
# Translates 'auth_package' value to our own 'auth_type' enum vals
if not self.auth_required:
return NOAUTH
+ if not self.auth_package:
+ return None
return {
# Missing in list are DIGEST and OAUTH2
'basic': BASIC,
@@ -1294,7 +1301,7 @@ Inherited members
'negotiate': SSPI, # Unsure about this one
'nego2': GSSAPI,
'anonymous': NOAUTH, # Seen in some docs even though it's not mentioned in MSDN
- }.get(self.auth_package.lower(), NTLM) # Default to NTLM
+ }.get(self.auth_package.lower())
Ancestors
@@ -1335,6 +1342,8 @@
-Instance variables
# Translates 'auth_package' value to our own 'auth_type' enum vals
if not self.auth_required:
return NOAUTH
+ if not self.auth_package:
+ return None
return {
# Missing in list are DIGEST and OAUTH2
'basic': BASIC,
@@ -1345,7 +1354,7 @@ Instance variables
'negotiate': SSPI, # Unsure about this one
'nego2': GSSAPI,
'anonymous': NOAUTH, # Seen in some docs even though it's not mentioned in MSDN
- }.get(self.auth_package.lower(), NTLM) # Default to NTLM
+ }.get(self.auth_package.lower())
var cert_principal_name
Inherited members
return None
@property
- def ews_url(self):
- """Return the EWS URL contained in the response.
+ def version(self):
+ # Get the server version. Not all protocol entries have a server version so we cheat a bit and also look at the
+ # other ones that point to the same endpoint.
+ ews_url = self.protocol.ews_url
+ for protocol in self.account.protocols:
+ if not protocol.ews_url or not protocol.server_version:
+ continue
+ if protocol.ews_url.lower() == ews_url.lower():
+ return Version(build=protocol.server_version)
+ return None
+
+ @property
+ def protocol(self):
+ """Return the protocol containing an EWS URL.
A response may contain a number of possible protocol types. EXPR is meant for EWS. See
https://techcommunity.microsoft.com/t5/blogs/blogarticleprintpage/blog-id/Exchange/article-id/16
We allow fallback to EXCH if EXPR is not available, to support installations where EXPR is not available.
- Additionally, some responses may contain and EXPR with no EWS URL. In that case, return the URL from EXCH, if
- available.
+ Additionally, some responses may contain an EXPR with no EWS URL. In that case, return EXCH, if available.
"""
protocols = {p.type: p for p in self.account.protocols if p.ews_url}
if Protocol.EXPR in protocols:
- return protocols[Protocol.EXPR].ews_url
+ return protocols[Protocol.EXPR]
if Protocol.EXCH in protocols:
- return protocols[Protocol.EXCH].ews_url
+ return protocols[Protocol.EXCH]
raise ValueError(
- 'No EWS URL found in any of the available protocols: %s' % [str(p) for p in self.account.protocols]
+ f'No EWS URL found in any of the available protocols: {[str(p) for p in self.account.protocols]}'
)
Ancestors
@@ -1636,37 +1656,35 @@ Instance variables
return None
-var ews_url
var protocol
Expand source code
@property
-def ews_url(self):
- """Return the EWS URL contained in the response.
+def protocol(self):
+ """Return the protocol containing an EWS URL.
A response may contain a number of possible protocol types. EXPR is meant for EWS. See
https://techcommunity.microsoft.com/t5/blogs/blogarticleprintpage/blog-id/Exchange/article-id/16
We allow fallback to EXCH if EXPR is not available, to support installations where EXPR is not available.
- Additionally, some responses may contain and EXPR with no EWS URL. In that case, return the URL from EXCH, if
- available.
+ Additionally, some responses may contain an EXPR with no EWS URL. In that case, return EXCH, if available.
"""
protocols = {p.type: p for p in self.account.protocols if p.ews_url}
if Protocol.EXPR in protocols:
- return protocols[Protocol.EXPR].ews_url
+ return protocols[Protocol.EXPR]
if Protocol.EXCH in protocols:
- return protocols[Protocol.EXCH].ews_url
+ return protocols[Protocol.EXCH]
raise ValueError(
- 'No EWS URL found in any of the available protocols: %s' % [str(p) for p in self.account.protocols]
+ f'No EWS URL found in any of the available protocols: {[str(p) for p in self.account.protocols]}'
)
Instance variables
var version
+Expand source code
+
+
+@property
+def version(self):
+ # Get the server version. Not all protocol entries have a server version so we cheat a bit and also look at the
+ # other ones that point to the same endpoint.
+ ews_url = self.protocol.ews_url
+ for protocol in self.account.protocols:
+ if not protocol.ews_url or not protocol.server_version:
+ continue
+ if protocol.ews_url.lower() == ews_url.lower():
+ return Version(build=protocol.server_version)
+ return None
Inherited members
@@ -2066,10 +2104,11 @@
FIELDS
account
autodiscover_smtp_address
ews_url
protocol
redirect_address
redirect_url
user
version
Module
TIMEOUT = 10 # Seconds
def __str__(self):
- return '''\
-Autodiscover endpoint: %s
-Auth type: %s''' % (
- self.service_endpoint,
- self.auth_type,
- )
+ return f'''\
+Autodiscover endpoint: {self.service_endpoint}
+Auth type: {self.auth_type}'''
exchangelib.autodiscover.protocol
Classes
TIMEOUT = 10 # Seconds
def __str__(self):
- return '''\
-Autodiscover endpoint: %s
-Auth type: %s''' % (
- self.service_endpoint,
- self.auth_type,
- )
+ return f'''\
+Autodiscover endpoint: {self.service_endpoint}
+Auth type: {self.auth_type}'''
Ancestors
diff --git a/docs/exchangelib/configuration.html b/docs/exchangelib/configuration.html
index b8eacd5c..015f089f 100644
--- a/docs/exchangelib/configuration.html
+++ b/docs/exchangelib/configuration.html
@@ -30,14 +30,21 @@
Module
from cached_property import threaded_cached_property
-from .credentials import BaseCredentials, OAuth2Credentials
+from .errors import InvalidEnumValue, InvalidTypeError
+from .credentials import BaseCredentials, OAuth2Credentials, OAuth2AuthorizationCodeCredentials
from .protocol import RetryPolicy, FailFast
-from .transport import AUTH_TYPE_MAP, OAUTH2
+from .transport import AUTH_TYPE_MAP, OAUTH2, CREDENTIALS_REQUIRED
from .util import split_url
from .version import Version
log = logging.getLogger(__name__)
+DEFAULT_AUTH_TYPE = {
+ # This type of credentials *must* use the OAuth auth type
+ OAuth2Credentials: OAUTH2,
+ OAuth2AuthorizationCodeCredentials: OAUTH2,
+}
+
class Configuration:
"""Contains information needed to create an authenticated connection to an EWS endpoint.
@@ -70,26 +77,27 @@ exchangelib.configuration
Module
def __init__(self, credentials=None, server=None, service_endpoint=None, auth_type=None, version=None,
retry_policy=None, max_connections=None):
if not isinstance(credentials, (BaseCredentials, type(None))):
- raise ValueError("'credentials' %r must be a Credentials instance" % credentials)
- if isinstance(credentials, OAuth2Credentials) and auth_type is None:
- # This type of credentials *must* use the OAuth auth type
- auth_type = OAUTH2
+ raise InvalidTypeError('credentials', credentials, BaseCredentials)
+ if auth_type is None:
+ # Set a default auth type for the credentials where this makes sense
+ auth_type = DEFAULT_AUTH_TYPE.get(type(credentials))
+ elif credentials is None and auth_type in CREDENTIALS_REQUIRED:
+ raise ValueError(f'Auth type {auth_type!r} was detected but no credentials were provided')
if server and service_endpoint:
raise AttributeError("Only one of 'server' or 'service_endpoint' must be provided")
if auth_type is not None and auth_type not in AUTH_TYPE_MAP:
- raise ValueError("'auth_type' %r must be one of %s"
- % (auth_type, ', '.join("'%s'" % k for k in sorted(AUTH_TYPE_MAP))))
+ raise InvalidEnumValue('auth_type', auth_type, AUTH_TYPE_MAP)
if not retry_policy:
retry_policy = FailFast()
if not isinstance(version, (Version, type(None))):
- raise ValueError("'version' %r must be a Version instance" % version)
+ raise InvalidTypeError('version', version, Version)
if not isinstance(retry_policy, RetryPolicy):
- raise ValueError("'retry_policy' %r must be a RetryPolicy instance" % retry_policy)
+ raise InvalidTypeError('retry_policy', retry_policy, RetryPolicy)
if not isinstance(max_connections, (int, type(None))):
- raise ValueError("'max_connections' must be an integer")
+ raise InvalidTypeError('max_connections', max_connections, int)
self._credentials = credentials
if server:
- self.service_endpoint = 'https://%s/EWS/Exchange.asmx' % server
+ self.service_endpoint = f'https://{server}/EWS/Exchange.asmx'
else:
self.service_endpoint = service_endpoint
self.auth_type = auth_type
@@ -109,9 +117,10 @@ exchangelib.configuration
Module
return split_url(self.service_endpoint)[1]
def __repr__(self):
- return self.__class__.__name__ + '(%s)' % ', '.join('%s=%r' % (k, getattr(self, k)) for k in (
+ args_str = ', '.join(f'{k}={getattr(self, k)!r}' for k in (
'credentials', 'service_endpoint', 'auth_type', 'version', 'retry_policy'
- ))
+ ))
+ return f'{self.__class__.__name__}({args_str})'
exchangelib.configuration
Classes
def __init__(self, credentials=None, server=None, service_endpoint=None, auth_type=None, version=None,
retry_policy=None, max_connections=None):
if not isinstance(credentials, (BaseCredentials, type(None))):
- raise ValueError("'credentials' %r must be a Credentials instance" % credentials)
- if isinstance(credentials, OAuth2Credentials) and auth_type is None:
- # This type of credentials *must* use the OAuth auth type
- auth_type = OAUTH2
+ raise InvalidTypeError('credentials', credentials, BaseCredentials)
+ if auth_type is None:
+ # Set a default auth type for the credentials where this makes sense
+ auth_type = DEFAULT_AUTH_TYPE.get(type(credentials))
+ elif credentials is None and auth_type in CREDENTIALS_REQUIRED:
+ raise ValueError(f'Auth type {auth_type!r} was detected but no credentials were provided')
if server and service_endpoint:
raise AttributeError("Only one of 'server' or 'service_endpoint' must be provided")
if auth_type is not None and auth_type not in AUTH_TYPE_MAP:
- raise ValueError("'auth_type' %r must be one of %s"
- % (auth_type, ', '.join("'%s'" % k for k in sorted(AUTH_TYPE_MAP))))
+ raise InvalidEnumValue('auth_type', auth_type, AUTH_TYPE_MAP)
if not retry_policy:
retry_policy = FailFast()
if not isinstance(version, (Version, type(None))):
- raise ValueError("'version' %r must be a Version instance" % version)
+ raise InvalidTypeError('version', version, Version)
if not isinstance(retry_policy, RetryPolicy):
- raise ValueError("'retry_policy' %r must be a RetryPolicy instance" % retry_policy)
+ raise InvalidTypeError('retry_policy', retry_policy, RetryPolicy)
if not isinstance(max_connections, (int, type(None))):
- raise ValueError("'max_connections' must be an integer")
+ raise InvalidTypeError('max_connections', max_connections, int)
self._credentials = credentials
if server:
- self.service_endpoint = 'https://%s/EWS/Exchange.asmx' % server
+ self.service_endpoint = f'https://{server}/EWS/Exchange.asmx'
else:
self.service_endpoint = service_endpoint
self.auth_type = auth_type
@@ -221,9 +231,10 @@ Classes
return split_url(self.service_endpoint)[1]
def __repr__(self):
- return self.__class__.__name__ + '(%s)' % ', '.join('%s=%r' % (k, getattr(self, k)) for k in (
+ args_str = ', '.join(f'{k}={getattr(self, k)!r}' for k in (
'credentials', 'service_endpoint', 'auth_type', 'version', 'retry_policy'
- ))
+ ))
+ return f'{self.__class__.__name__}({args_str})'
Instance variables
diff --git a/docs/exchangelib/credentials.html b/docs/exchangelib/credentials.html
index bad277b3..ebdaabe9 100644
--- a/docs/exchangelib/credentials.html
+++ b/docs/exchangelib/credentials.html
@@ -26,7 +26,7 @@
Module
exchangelib.credentials
Expand source code
@@ -35,12 +35,16 @@
Module
Implements an Exchange user object and access types. Exchange provides two different ways of granting access for a
login to a specific account. Impersonation is used mainly for service accounts that connect via EWS. Delegate is used
for ad-hoc access e.g. granted manually by the user.
-See http://blogs.msdn.com/b/exchangedev/archive/2009/06/15/exchange-impersonation-vs-delegate-access.aspx
+See https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/impersonation-and-ews-in-exchange
"""
import abc
import logging
from threading import RLock
+from oauthlib.oauth2 import OAuth2Token
+
+from .errors import InvalidTypeError
+
log = logging.getLogger(__name__)
IMPERSONATION = 'impersonation'
@@ -76,12 +80,7 @@ exchangelib.credentials
Module
return (getattr(self, k) for k in self.__dict__ if k != '_lock')
def __eq__(self, other):
- for k in self.__dict__:
- if k == '_lock':
- continue
- if getattr(self, k) != getattr(other, k):
- return False
- return True
+ return all(getattr(self, k) == getattr(other, k) for k in self.__dict__ if k != '_lock')
def __hash__(self):
return hash(tuple(self._get_hash_values()))
@@ -173,7 +172,7 @@ exchangelib.credentials
Module
"""
# Ensure we don't update the object in the middle of a new session being created, which could cause a race.
if not isinstance(access_token, dict):
- raise ValueError("'access_token' must be an OAuth2Token")
+ raise InvalidTypeError('access_token', access_token, OAuth2Token)
with self.lock:
log.debug('%s auth token for %s', 'Refreshing' if self.access_token else 'Setting', self.client_id)
self.access_token = access_token
@@ -233,7 +232,7 @@ exchangelib.credentials
Module
super().__init__(**kwargs)
self.authorization_code = authorization_code
if access_token is not None and not isinstance(access_token, dict):
- raise ValueError("'access_token' must be an OAuth2Token")
+ raise InvalidTypeError('access_token', access_token, OAuth2Token)
self.access_token = access_token
def __repr__(self):
@@ -297,12 +296,7 @@ exchangelib.credentials
Classes
return (getattr(self, k) for k in self.__dict__ if k != '_lock')
def __eq__(self, other):
- for k in self.__dict__:
- if k == '_lock':
- continue
- if getattr(self, k) != getattr(other, k):
- return False
- return True
+ return all(getattr(self, k) == getattr(other, k) for k in self.__dict__ if k != '_lock')
def __hash__(self):
return hash(tuple(self._get_hash_values()))
@@ -501,7 +495,7 @@ Inherited members
super().__init__(**kwargs)
self.authorization_code = authorization_code
if access_token is not None and not isinstance(access_token, dict):
- raise ValueError("'access_token' must be an OAuth2Token")
+ raise InvalidTypeError('access_token', access_token, OAuth2Token)
self.access_token = access_token
def __repr__(self):
@@ -589,7 +583,7 @@ Inherited members
"""
# Ensure we don't update the object in the middle of a new session being created, which could cause a race.
if not isinstance(access_token, dict):
- raise ValueError("'access_token' must be an OAuth2Token")
+ raise InvalidTypeError('access_token', access_token, OAuth2Token)
with self.lock:
log.debug('%s auth token for %s', 'Refreshing' if self.access_token else 'Setting', self.client_id)
self.access_token = access_token
@@ -651,7 +645,7 @@ Methods
"""
# Ensure we don't update the object in the middle of a new session being created, which could cause a race.
if not isinstance(access_token, dict):
- raise ValueError("'access_token' must be an OAuth2Token")
+ raise InvalidTypeError('access_token', access_token, OAuth2Token)
with self.lock:
log.debug('%s auth token for %s', 'Refreshing' if self.access_token else 'Setting', self.client_id)
self.access_token = access_token
diff --git a/docs/exchangelib/errors.html b/docs/exchangelib/errors.html
index 83b6f544..f73471dd 100644
--- a/docs/exchangelib/errors.html
+++ b/docs/exchangelib/errors.html
@@ -40,6 +40,28 @@ Module
pass
+class InvalidEnumValue(ValueError):
+ def __init__(self, field_name, value, choices):
+ self.field_name = field_name
+ self.value = value
+ self.choices = choices
+ super().__init__(str(self))
+
+ def __str__(self):
+ return f'{self.field_name!r} {self.value!r} must be one of {sorted(self.choices)}'
+
+
+class InvalidTypeError(TypeError):
+ def __init__(self, field_name, value, valid_type):
+ self.field_name = field_name
+ self.value = value
+ self.valid_type = valid_type
+ super().__init__(str(self))
+
+ def __str__(self):
+ return f'{self.field_name!r} {self.value!r} must be of type {self.valid_type}'
+
+
class EWSError(Exception):
"""Global error type within this module."""
@@ -72,10 +94,8 @@ exchangelib.errors
Module
self.total_wait = total_wait
def __str__(self):
- return str(
- '{value} (gave up after {total_wait:.3f} seconds. URL {url} returned status code {status_code})'.format(
- value=self.value, url=self.url, status_code=self.status_code, total_wait=self.total_wait)
- )
+ return f'{self.value} (gave up after {self.total_wait:.3f} seconds. ' \
+ f'URL {self.url} returned status code {self.status_code})'
class SOAPError(TransportError):
@@ -99,7 +119,7 @@ exchangelib.errors
Module
super().__init__(str(self))
def __str__(self):
- return 'We were redirected to %s' % self.url
+ return f'We were redirected to {self.url}'
class RelativeRedirect(TransportError):
@@ -118,22 +138,10 @@ exchangelib.errors
Module
pass
-class AutoDiscoverRedirect(AutoDiscoverError):
- def __init__(self, redirect_email):
- self.redirect_email = redirect_email
- super().__init__(str(self))
-
- def __str__(self):
- return 'AutoDiscover redirects to %s' % self.redirect_email
-
-
class NaiveDateTimeNotAllowed(ValueError):
def __init__(self, local_dt):
super().__init__()
- from .ewsdatetime import EWSDateTime
- if not isinstance(local_dt, EWSDateTime):
- raise ValueError("'local_dt' value %r must be an EWSDateTime" % local_dt)
- self.local_dt = local_dt
+ self.local_dt = local_dt # An EWSDateTime instance
class UnknownTimeZone(EWSError):
@@ -167,7 +175,7 @@ exchangelib.errors
Module
super().__init__(str(self))
def __str__(self):
- return 'CAS error: %s' % self.cas_error
+ return f'CAS error: {self.cas_error}'
# Somewhat-authoritative list of possible response message error types from EWS. See full list at
@@ -641,7 +649,6 @@ exchangelib.errors
Subclasses
@@ -666,33 +673,6 @@
Ancestors
-class AutoDiscoverRedirect
-(redirect_email)
-
-Expand source code
-
-
-class AutoDiscoverRedirect(AutoDiscoverError):
- def __init__(self, redirect_email):
- self.redirect_email = redirect_email
- super().__init__(str(self))
-
- def __str__(self):
- return 'AutoDiscover redirects to %s' % self.redirect_email
Ancestors
-
-
-
class CASError
(cas_error, response)
@@ -715,7 +695,7 @@
+ return f'CAS error: {self.cas_error}'
Ancestors
super().__init__(str(self))
def __str__(self):
- return 'CAS error: %s' % self.cas_errorAncestors
@@ -8870,6 +8850,60 @@
+Ancestors
+class InvalidEnumValue
+(field_name, value, choices)
+
+Expand source code
+
+
+class InvalidEnumValue(ValueError):
+ def __init__(self, field_name, value, choices):
+ self.field_name = field_name
+ self.value = value
+ self.choices = choices
+ super().__init__(str(self))
+
+ def __str__(self):
+ return f'{self.field_name!r} {self.value!r} must be one of {sorted(self.choices)}'
Ancestors
+
+
+
+class InvalidTypeError
+(field_name, value, valid_type)
+
+Expand source code
+
+
+class InvalidTypeError(TypeError):
+ def __init__(self, field_name, value, valid_type):
+ self.field_name = field_name
+ self.value = value
+ self.valid_type = valid_type
+ super().__init__(str(self))
+
+ def __str__(self):
+ return f'{self.field_name!r} {self.value!r} must be of type {self.valid_type}'
Ancestors
+
+
+
class MalformedResponseError
(value)
@@ -8923,10 +8957,7 @@
Ancestors
+ self.local_dt = local_dt # An EWSDateTime instanceclass NaiveDateTimeNotAllowed(ValueError):
def __init__(self, local_dt):
super().__init__()
- from .ewsdatetime import EWSDateTime
- if not isinstance(local_dt, EWSDateTime):
- raise ValueError("'local_dt' value %r must be an EWSDateTime" % local_dt)
- self.local_dt = local_dt
Ancestors
@@ -8953,10 +8984,8 @@
Ancestors
self.total_wait = total_wait
def __str__(self):
- return str(
- '{value} (gave up after {total_wait:.3f} seconds. URL {url} returned status code {status_code})'.format(
- value=self.value, url=self.url, status_code=self.status_code, total_wait=self.total_wait)
- )
+ return f'{self.value} (gave up after {self.total_wait:.3f} seconds. ' \
+ f'URL {self.url} returned status code {self.status_code})'
Ancestors
@@ -8985,7 +9014,7 @@
diff --git a/docs/exchangelib/extended_properties.html b/docs/exchangelib/extended_properties.html
index a1f3a864..f085765f 100644
--- a/docs/exchangelib/extended_properties.html
+++ b/docs/exchangelib/extended_properties.html
@@ -29,6 +29,7 @@ Ancestors
super().__init__(str(self))
def __str__(self):
- return 'We were redirected to %s' % self.url
+ return f'We were redirected to {self.url}'
Ancestors
@@ -9601,9 +9630,6 @@
AutoDiscoverFailed
-AutoDiscoverRedirect
CASError
ErrorWrongServerVersionDelegate
+InvalidEnumValue
+InvalidTypeError
MalformedResponseError
Module
exchangelib.ewsdatetime
import datetime
import logging
-import warnings
try:
import zoneinfo
@@ -36,7 +35,7 @@
Module
from backports import zoneinfo
import tzlocal
-from .errors import NaiveDateTimeNotAllowed, UnknownTimeZone
+from .errors import NaiveDateTimeNotAllowed, UnknownTimeZone, InvalidTypeError
from .winzone import IANA_TO_MS_TIMEZONE_MAP, MS_TIMEZONE_TO_IANA_MAP
log = logging.getLogger(__name__)
@@ -81,7 +80,7 @@ exchangelib.ewsdatetime
Module
@classmethod
def from_date(cls, d):
if type(d) is not datetime.date:
- raise ValueError("%r must be a date instance" % d)
+ raise InvalidTypeError('d', d, datetime.date)
return cls(d.year, d.month, d.day)
@classmethod
@@ -118,7 +117,7 @@ exchangelib.ewsdatetime
Module
# Don't allow pytz or dateutil timezones here. They are not safe to use as direct input for datetime()
tzinfo = EWSTimeZone.from_timezone(tzinfo)
if not isinstance(tzinfo, (EWSTimeZone, type(None))):
- raise ValueError('tzinfo %r must be an EWSTimeZone instance' % tzinfo)
+ raise InvalidTypeError('tzinfo', tzinfo, EWSTimeZone)
if len(args) == 8:
args = args[:7] + (tzinfo,)
else:
@@ -131,7 +130,7 @@ exchangelib.ewsdatetime
Module
* 2009-01-15T13:45:56+01:00
"""
if not self.tzinfo:
- raise ValueError('%r must be timezone-aware' % self)
+ raise ValueError(f'{self!r} must be timezone-aware')
if self.tzinfo.key == 'UTC':
if self.microsecond:
return self.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
@@ -141,7 +140,7 @@ exchangelib.ewsdatetime
Module
@classmethod
def from_datetime(cls, d):
if type(d) is not datetime.datetime:
- raise ValueError("%r must be a datetime instance" % d)
+ raise InvalidTypeError('d', d, datetime.datetime)
if d.tzinfo is None:
tz = None
elif isinstance(d.tzinfo, EWSTimeZone):
@@ -245,7 +244,7 @@ exchangelib.ewsdatetime
Module
try:
instance.ms_id = cls.IANA_TO_MS_MAP[instance.key][0]
except KeyError:
- raise UnknownTimeZone('No Windows timezone name found for timezone "%s"' % instance.key)
+ raise UnknownTimeZone(f'No Windows timezone name found for timezone {instance.key!r}')
# We don't need the Windows long-format timezone name in long format. It's used in timezone XML elements, but
# EWS happily accepts empty strings. For a full list of timezones supported by the target server, including
@@ -272,7 +271,7 @@ exchangelib.ewsdatetime
Module
# EWS sometimes returns an ID that has a region/location format, e.g. 'Europe/Copenhagen'. Try the
# string unaltered.
return cls(ms_id)
- raise UnknownTimeZone("Windows timezone ID '%s' is unknown by CLDR" % ms_id)
+ raise UnknownTimeZone(f'Windows timezone ID {ms_id!r} is unknown by CLDR')
@classmethod
def from_pytz(cls, tz):
@@ -312,7 +311,7 @@ exchangelib.ewsdatetime
Module
'pytz_deprecation_shim': lambda z: cls.from_timezone(z.unwrap_shim())
}[tz_module](tz)
except KeyError:
- raise TypeError('Unsupported tzinfo type: %r' % tz)
+ raise TypeError(f'Unsupported tzinfo type: {tz!r}')
@classmethod
def localzone(cls):
@@ -324,32 +323,6 @@ exchangelib.ewsdatetime
Module
# Handle both old and new versions of tzlocal that may return pytz or zoneinfo objects, respectively
return cls.from_timezone(tz)
- @classmethod
- def timezone(cls, location):
- warnings.warn('replace EWSTimeZone.timezone() with just EWSTimeZone()', DeprecationWarning, stacklevel=2)
- return cls(location)
-
- def normalize(self, dt, is_dst=False):
- warnings.warn('normalization is now handled gracefully', DeprecationWarning, stacklevel=2)
- return dt
-
- def localize(self, dt, is_dst=False):
- warnings.warn('replace tz.localize() with dt.replace(tzinfo=tz)', DeprecationWarning, stacklevel=2)
- if dt.tzinfo is not None:
- raise ValueError('%r must be timezone-unaware' % dt)
- dt = dt.replace(tzinfo=self)
- if is_dst is not None:
- # DST dates are assumed to always be after non-DST dates
- dt_before = dt.replace(fold=0)
- dt_after = dt.replace(fold=1)
- dst_before = dt_before.dst()
- dst_after = dt_after.dst()
- if dst_before > dst_after:
- dt = dt_before if is_dst else dt_after
- elif dst_before < dst_after:
- dt = dt_after if is_dst else dt_before
- return dt
-
def fromutc(self, dt):
t = super().fromutc(dt)
if isinstance(t, EWSDateTime):
@@ -434,7 +407,7 @@ exchangelib.ewsdatetime
Classes
@classmethod
def from_date(cls, d):
if type(d) is not datetime.date:
- raise ValueError("%r must be a date instance" % d)
+ raise InvalidTypeError('d', d, datetime.date)
return cls(d.year, d.month, d.day)
@classmethod
@@ -472,7 +445,7 @@ Static methods
@@ -565,7 +538,7 @@ @classmethod
def from_date(cls, d):
if type(d) is not datetime.date:
- raise ValueError("%r must be a date instance" % d)
+ raise InvalidTypeError('d', d, datetime.date)
return cls(d.year, d.month, d.day)
Methods
# Don't allow pytz or dateutil timezones here. They are not safe to use as direct input for datetime()
tzinfo = EWSTimeZone.from_timezone(tzinfo)
if not isinstance(tzinfo, (EWSTimeZone, type(None))):
- raise ValueError('tzinfo %r must be an EWSTimeZone instance' % tzinfo)
+ raise InvalidTypeError('tzinfo', tzinfo, EWSTimeZone)
if len(args) == 8:
args = args[:7] + (tzinfo,)
else:
@@ -578,7 +551,7 @@ Methods
* 2009-01-15T13:45:56+01:00
"""
if not self.tzinfo:
- raise ValueError('%r must be timezone-aware' % self)
+ raise ValueError(f'{self!r} must be timezone-aware')
if self.tzinfo.key == 'UTC':
if self.microsecond:
return self.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
@@ -588,7 +561,7 @@ Methods
@classmethod
def from_datetime(cls, d):
if type(d) is not datetime.datetime:
- raise ValueError("%r must be a datetime instance" % d)
+ raise InvalidTypeError('d', d, datetime.datetime)
if d.tzinfo is None:
tz = None
elif isinstance(d.tzinfo, EWSTimeZone):
@@ -694,7 +667,7 @@ Static methods
+ raise UnknownTimeZone(f'Windows timezone ID {ms_id!r} is unknown by CLDR')@classmethod
def from_datetime(cls, d):
if type(d) is not datetime.datetime:
- raise ValueError("%r must be a datetime instance" % d)
+ raise InvalidTypeError('d', d, datetime.datetime)
if d.tzinfo is None:
tz = None
elif isinstance(d.tzinfo, EWSTimeZone):
@@ -855,7 +828,7 @@
Methods
* 2009-01-15T13:45:56+01:00
"""
if not self.tzinfo:
- raise ValueError('%r must be timezone-aware' % self)
+ raise ValueError(f'{self!r} must be timezone-aware')
if self.tzinfo.key == 'UTC':
if self.microsecond:
return self.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
@@ -892,7 +865,7 @@ Methods
try:
instance.ms_id = cls.IANA_TO_MS_MAP[instance.key][0]
except KeyError:
- raise UnknownTimeZone('No Windows timezone name found for timezone "%s"' % instance.key)
+ raise UnknownTimeZone(f'No Windows timezone name found for timezone {instance.key!r}')
# We don't need the Windows long-format timezone name in long format. It's used in timezone XML elements, but
# EWS happily accepts empty strings. For a full list of timezones supported by the target server, including
@@ -919,7 +892,7 @@ Methods
# EWS sometimes returns an ID that has a region/location format, e.g. 'Europe/Copenhagen'. Try the
# string unaltered.
return cls(ms_id)
- raise UnknownTimeZone("Windows timezone ID '%s' is unknown by CLDR" % ms_id)
+ raise UnknownTimeZone(f'Windows timezone ID {ms_id!r} is unknown by CLDR')
@classmethod
def from_pytz(cls, tz):
@@ -959,7 +932,7 @@ Methods
'pytz_deprecation_shim': lambda z: cls.from_timezone(z.unwrap_shim())
}[tz_module](tz)
except KeyError:
- raise TypeError('Unsupported tzinfo type: %r' % tz)
+ raise TypeError(f'Unsupported tzinfo type: {tz!r}')
@classmethod
def localzone(cls):
@@ -971,32 +944,6 @@ Methods
# Handle both old and new versions of tzlocal that may return pytz or zoneinfo objects, respectively
return cls.from_timezone(tz)
- @classmethod
- def timezone(cls, location):
- warnings.warn('replace EWSTimeZone.timezone() with just EWSTimeZone()', DeprecationWarning, stacklevel=2)
- return cls(location)
-
- def normalize(self, dt, is_dst=False):
- warnings.warn('normalization is now handled gracefully', DeprecationWarning, stacklevel=2)
- return dt
-
- def localize(self, dt, is_dst=False):
- warnings.warn('replace tz.localize() with dt.replace(tzinfo=tz)', DeprecationWarning, stacklevel=2)
- if dt.tzinfo is not None:
- raise ValueError('%r must be timezone-unaware' % dt)
- dt = dt.replace(tzinfo=self)
- if is_dst is not None:
- # DST dates are assumed to always be after non-DST dates
- dt_before = dt.replace(fold=0)
- dt_after = dt.replace(fold=1)
- dst_before = dt_before.dst()
- dst_after = dt_after.dst()
- if dst_before > dst_after:
- dt = dt_before if is_dst else dt_after
- elif dst_before < dst_after:
- dt = dt_after if is_dst else dt_before
- return dt
-
def fromutc(self, dt):
t = super().fromutc(dt)
if isinstance(t, EWSDateTime):
@@ -1075,7 +1022,7 @@ Static methods
# EWS sometimes returns an ID that has a region/location format, e.g. 'Europe/Copenhagen'. Try the
# string unaltered.
return cls(ms_id)
- raise UnknownTimeZone("Windows timezone ID '%s' is unknown by CLDR" % ms_id)
@@ -1117,7 +1064,7 @@
+ raise TypeError(f'Unsupported tzinfo type: {tz!r}')
Static methods
'pytz_deprecation_shim': lambda z: cls.from_timezone(z.unwrap_shim())
}[tz_module](tz)
except KeyError:
- raise TypeError('Unsupported tzinfo type: %r' % tz)
@@ -1154,21 +1101,6 @@
-Static methods
return cls.from_timezone(tz)
-def timezone(location)
-
-Expand source code
-
-
-@classmethod
-def timezone(cls, location):
- warnings.warn('replace EWSTimeZone.timezone() with just EWSTimeZone()', DeprecationWarning, stacklevel=2)
- return cls(location)
Methods
@@ -1188,47 +1120,6 @@
@@ -1287,10 +1178,7 @@ Methods
return EWSDateTime.from_datetime(t) # We want to return EWSDateTime objects
-
-def localize(self, dt, is_dst=False)
-
-Expand source code
-
-
-def localize(self, dt, is_dst=False):
- warnings.warn('replace tz.localize() with dt.replace(tzinfo=tz)', DeprecationWarning, stacklevel=2)
- if dt.tzinfo is not None:
- raise ValueError('%r must be timezone-unaware' % dt)
- dt = dt.replace(tzinfo=self)
- if is_dst is not None:
- # DST dates are assumed to always be after non-DST dates
- dt_before = dt.replace(fold=0)
- dt_after = dt.replace(fold=1)
- dst_before = dt_before.dst()
- dst_after = dt_after.dst()
- if dst_before > dst_after:
- dt = dt_before if is_dst else dt_after
- elif dst_before < dst_after:
- dt = dt_after if is_dst else dt_before
- return dt
-def normalize(self, dt, is_dst=False)
-
-Expand source code
-
-
-def normalize(self, dt, is_dst=False):
- warnings.warn('normalization is now handled gracefully', DeprecationWarning, stacklevel=2)
- return dt
from_timezone
from_zoneinfo
fromutc
localize
localzone
normalize
timezone
Module
exchangelib.extended_properties
+ raise TypeError(f"Field {self.__class__.__name__!r} value {self.value!r} must be of type {python_type}")
import logging
from decimal import Decimal
+from .errors import InvalidEnumValue
from .ewsdatetime import EWSDateTime
from .properties import EWSElement, ExtendedFieldURI
from .util import create_element, add_xml_child, get_xml_attrs, get_xml_attr, set_xml_value, value_to_xml_text, \
@@ -147,9 +148,8 @@
Module
"When 'distinguished_property_set_id' is set, 'property_id' or 'property_name' must also be set"
)
if cls.distinguished_property_set_id not in cls.DISTINGUISHED_SETS:
- raise ValueError(
- "'distinguished_property_set_id' %r must be one of %s"
- % (cls.distinguished_property_set_id, sorted(cls.DISTINGUISHED_SETS))
+ raise InvalidEnumValue(
+ 'distinguished_property_set_id', cls.distinguished_property_set_id, cls.DISTINGUISHED_SETS
)
@classmethod
@@ -173,7 +173,7 @@ exchangelib.extended_properties
Module
raise ValueError("When 'property_tag' is set, only 'property_type' must be set")
if 0x8000 <= cls.property_tag_as_int() <= 0xFFFE:
raise ValueError(
- "'property_tag' value '%s' is reserved for custom properties" % cls.property_tag_as_hex()
+ f"'property_tag' value {cls.property_tag_as_hex()!r} is reserved for custom properties"
)
@classmethod
@@ -199,24 +199,20 @@ exchangelib.extended_properties
Module
@classmethod
def _validate_property_type(cls):
if cls.property_type not in cls.PROPERTY_TYPES:
- raise ValueError(
- "'property_type' %r must be one of %s" % (cls.property_type, sorted(cls.PROPERTY_TYPES))
- )
+ raise InvalidEnumValue('property_type', cls.property_type, cls.PROPERTY_TYPES)
def clean(self, version=None):
self.validate_cls()
python_type = self.python_type()
if self.is_array_type():
if not is_iterable(self.value):
- raise ValueError("'%s' value %r must be a list" % (self.__class__.__name__, self.value))
+ raise TypeError(f"Field {self.__class__.__name__!r} value {self.value!r} must be of type {list}")
for v in self.value:
if not isinstance(v, python_type):
- raise TypeError(
- "'%s' value element %r must be an instance of %s" % (self.__class__.__name__, v, python_type))
+ raise TypeError(f"Field {self.__class__.__name__!r} list value {v!r} must be of type {python_type}")
else:
if not isinstance(self.value, python_type):
- raise TypeError(
- "'%s' value %r must be an instance of %s" % (self.__class__.__name__, self.value, python_type))
+ raise TypeError(f"Field {self.__class__.__name__!r} value {self.value!r} must be of type {python_type}")
@classmethod
def _normalize_obj(cls, obj):
@@ -251,12 +247,12 @@ exchangelib.extended_properties
Module
# Gets value of this specific ExtendedProperty from a list of 'ExtendedProperty' XML elements
python_type = cls.python_type()
if cls.is_array_type():
- values = elem.find('{%s}Values' % TNS)
+ values = elem.find(f'{{{TNS}}}Values')
return [
xml_text_to_value(value=val, value_type=python_type)
- for val in get_xml_attrs(values, '{%s}Value' % TNS)
+ for val in get_xml_attrs(values, f'{{{TNS}}}Value')
]
- extended_field_value = xml_text_to_value(value=get_xml_attr(elem, '{%s}Value' % TNS), value_type=python_type)
+ extended_field_value = xml_text_to_value(value=get_xml_attr(elem, f'{{{TNS}}}Value'), value_type=python_type)
if python_type == str and not extended_field_value:
# For string types, we want to return the empty string instead of None if the element was
# actually found, but there was no XML value. For other types, it would be more problematic
@@ -468,9 +464,8 @@ exchangelib.extended_properties
Classes
"When 'distinguished_property_set_id' is set, 'property_id' or 'property_name' must also be set"
)
if cls.distinguished_property_set_id not in cls.DISTINGUISHED_SETS:
- raise ValueError(
- "'distinguished_property_set_id' %r must be one of %s"
- % (cls.distinguished_property_set_id, sorted(cls.DISTINGUISHED_SETS))
+ raise InvalidEnumValue(
+ 'distinguished_property_set_id', cls.distinguished_property_set_id, cls.DISTINGUISHED_SETS
)
@classmethod
@@ -494,7 +489,7 @@ Classes
raise ValueError("When 'property_tag' is set, only 'property_type' must be set")
if 0x8000 <= cls.property_tag_as_int() <= 0xFFFE:
raise ValueError(
- "'property_tag' value '%s' is reserved for custom properties" % cls.property_tag_as_hex()
+ f"'property_tag' value {cls.property_tag_as_hex()!r} is reserved for custom properties"
)
@classmethod
@@ -520,24 +515,20 @@ Classes
@classmethod
def _validate_property_type(cls):
if cls.property_type not in cls.PROPERTY_TYPES:
- raise ValueError(
- "'property_type' %r must be one of %s" % (cls.property_type, sorted(cls.PROPERTY_TYPES))
- )
+ raise InvalidEnumValue('property_type', cls.property_type, cls.PROPERTY_TYPES)
def clean(self, version=None):
self.validate_cls()
python_type = self.python_type()
if self.is_array_type():
if not is_iterable(self.value):
- raise ValueError("'%s' value %r must be a list" % (self.__class__.__name__, self.value))
+ raise TypeError(f"Field {self.__class__.__name__!r} value {self.value!r} must be of type {list}")
for v in self.value:
if not isinstance(v, python_type):
- raise TypeError(
- "'%s' value element %r must be an instance of %s" % (self.__class__.__name__, v, python_type))
+ raise TypeError(f"Field {self.__class__.__name__!r} list value {v!r} must be of type {python_type}")
else:
if not isinstance(self.value, python_type):
- raise TypeError(
- "'%s' value %r must be an instance of %s" % (self.__class__.__name__, self.value, python_type))
+ raise TypeError(f"Field {self.__class__.__name__!r} value {self.value!r} must be of type {python_type}")
@classmethod
def _normalize_obj(cls, obj):
@@ -572,12 +563,12 @@ Classes
# Gets value of this specific ExtendedProperty from a list of 'ExtendedProperty' XML elements
python_type = cls.python_type()
if cls.is_array_type():
- values = elem.find('{%s}Values' % TNS)
+ values = elem.find(f'{{{TNS}}}Values')
return [
xml_text_to_value(value=val, value_type=python_type)
- for val in get_xml_attrs(values, '{%s}Value' % TNS)
+ for val in get_xml_attrs(values, f'{{{TNS}}}Value')
]
- extended_field_value = xml_text_to_value(value=get_xml_attr(elem, '{%s}Value' % TNS), value_type=python_type)
+ extended_field_value = xml_text_to_value(value=get_xml_attr(elem, f'{{{TNS}}}Value'), value_type=python_type)
if python_type == str and not extended_field_value:
# For string types, we want to return the empty string instead of None if the element was
# actually found, but there was no XML value. For other types, it would be more problematic
@@ -732,12 +723,12 @@ Static methods
# Gets value of this specific ExtendedProperty from a list of 'ExtendedProperty' XML elements
python_type = cls.python_type()
if cls.is_array_type():
- values = elem.find('{%s}Values' % TNS)
+ values = elem.find(f'{{{TNS}}}Values')
return [
xml_text_to_value(value=val, value_type=python_type)
- for val in get_xml_attrs(values, '{%s}Value' % TNS)
+ for val in get_xml_attrs(values, f'{{{TNS}}}Value')
]
- extended_field_value = xml_text_to_value(value=get_xml_attr(elem, '{%s}Value' % TNS), value_type=python_type)
+ extended_field_value = xml_text_to_value(value=get_xml_attr(elem, f'{{{TNS}}}Value'), value_type=python_type)
if python_type == str and not extended_field_value:
# For string types, we want to return the empty string instead of None if the element was
# actually found, but there was no XML value. For other types, it would be more problematic
@@ -890,15 +881,13 @@ Methods
python_type = self.python_type()
if self.is_array_type():
if not is_iterable(self.value):
- raise ValueError("'%s' value %r must be a list" % (self.__class__.__name__, self.value))
+ raise TypeError(f"Field {self.__class__.__name__!r} value {self.value!r} must be of type {list}")
for v in self.value:
if not isinstance(v, python_type):
- raise TypeError(
- "'%s' value element %r must be an instance of %s" % (self.__class__.__name__, v, python_type))
+ raise TypeError(f"Field {self.__class__.__name__!r} list value {v!r} must be of type {python_type}")
else:
if not isinstance(self.value, python_type):
- raise TypeError(
- "'%s' value %r must be an instance of %s" % (self.__class__.__name__, self.value, python_type))
diff --git a/docs/exchangelib/fields.html b/docs/exchangelib/fields.html
index 1bb7ae2b..f49e88ea 100644
--- a/docs/exchangelib/fields.html
+++ b/docs/exchangelib/fields.html
@@ -29,14 +29,14 @@
Module
exchangelib.fields
@@ -1772,7 +1811,7 @@ import abc
import datetime
import logging
-from collections import OrderedDict
from decimal import Decimal, InvalidOperation
from importlib import import_module
+from .errors import InvalidTypeError
from .ewsdatetime import EWSDateTime, EWSDate, EWSTimeZone, NaiveDateTimeNotAllowed, UnknownTimeZone, UTC
from .util import create_element, get_xml_attr, get_xml_attrs, set_xml_value, value_to_xml_text, is_iterable, \
xml_text_to_value, TNS
-from .version import Build, Version, EXCHANGE_2013
+from .version import Build, EXCHANGE_2013
log = logging.getLogger(__name__)
@@ -91,7 +91,7 @@
Module
class InvalidFieldForVersion(ValueError):
- """Used when a field is not supported on the given Exchnage version."""
+ """Used when a field is not supported on the given Exchange version."""
class InvalidChoiceForVersion(ValueError):
@@ -109,7 +109,7 @@ exchangelib.fields
Module
'physical_addresses__Home__street' -> ('physical_addresses', 'Home', 'street')
"""
if not isinstance(field_path, str):
- raise ValueError("Field path %r must be a string" % field_path)
+ raise InvalidTypeError('field_path', field_path, str)
search_parts = field_path.split('__')
field = search_parts[0]
try:
@@ -128,56 +128,51 @@ exchangelib.fields
Module
label and SubField object.
"""
from .indexed_properties import SingleFieldIndexedElement, MultiFieldIndexedElement
- fieldname, label, subfieldname = split_field_path(field_path)
+ fieldname, label, subfield_name = split_field_path(field_path)
field = folder.get_item_field_by_fieldname(fieldname)
subfield = None
if isinstance(field, IndexedField):
if strict and not label:
raise ValueError(
- "IndexedField path '%s' must specify label, e.g. '%s__%s'"
- % (field_path, fieldname, field.value_cls.get_field_by_fieldname('label').default)
+ f"IndexedField path {field_path!r} must specify label, e.g. "
+ f"'{fieldname}__{field.value_cls.get_field_by_fieldname('label').default}'"
)
valid_labels = field.value_cls.get_field_by_fieldname('label').supported_choices(
version=folder.account.version
)
if label and label not in valid_labels:
raise ValueError(
- "Label '%s' on IndexedField path '%s' must be one of %s"
- % (label, field_path, ', '.join(valid_labels))
+ f"Label {label!r} on IndexedField path {field_path!r} must be one of {sorted(valid_labels)}"
)
if issubclass(field.value_cls, MultiFieldIndexedElement):
- if strict and not subfieldname:
+ if strict and not subfield_name:
raise ValueError(
- "IndexedField path '%s' must specify subfield, e.g. '%s__%s__%s'"
- % (field_path, fieldname, label, field.value_cls.FIELDS[1].name)
+ f"IndexedField path {field_path!r} must specify subfield, e.g. "
+ f"'{fieldname}__{label}__{field.value_cls.FIELDS[1].name}'"
)
- if subfieldname:
+ if subfield_name:
try:
- subfield = field.value_cls.get_field_by_fieldname(subfieldname)
+ subfield = field.value_cls.get_field_by_fieldname(subfield_name)
except ValueError:
- fnames = ', '.join(f.name for f in field.value_cls.supported_fields(
+ field_names = ', '.join(f.name for f in field.value_cls.supported_fields(
version=folder.account.version
))
raise ValueError(
- "Subfield '%s' on IndexedField path '%s' must be one of %s"
- % (subfieldname, field_path, fnames)
+ f"Subfield {subfield_name!r} on IndexedField path {field_path!r} "
+ f"must be one of {sorted(field_names)}"
)
else:
if not issubclass(field.value_cls, SingleFieldIndexedElement):
- raise ValueError("'field.value_cls' %r must be an SingleFieldIndexedElement instance" % field.value_cls)
- if subfieldname:
+ raise InvalidTypeError('field.value_cls', field.value_cls, SingleFieldIndexedElement)
+ if subfield_name:
raise ValueError(
- "IndexedField path '%s' must not specify subfield, e.g. just '%s__%s'"
- % (field_path, fieldname, label)
+ f"IndexedField path {field_path!r} must not specify subfield, e.g. just {fieldname}__{label}'"
)
subfield = field.value_cls.value_field(version=folder.account.version)
else:
- if label or subfieldname:
- raise ValueError(
- "Field path '%s' must not specify label or subfield, e.g. just '%s'"
- % (field_path, fieldname)
- )
+ if label or subfield_name:
+ raise ValueError(f"Field path {field_path!r} must not specify label or subfield, e.g. just {fieldname!r}")
return field, label, subfield
@@ -188,13 +183,13 @@ exchangelib.fields
Module
"""
def __init__(self, field, label=None, subfield=None):
+ """
+
+ :param field: A FieldURIField or ExtendedPropertyField instance
+ :param label: a str
+ :param subfield: A SubField instance
+ """
# 'label' and 'subfield' are only used for IndexedField fields
- if not isinstance(field, (FieldURIField, ExtendedPropertyField)):
- raise ValueError("'field' %r must be an FieldURIField, of ExtendedPropertyField instance" % field)
- if label and not isinstance(label, str):
- raise ValueError("'label' %r must be a %s instance" % (label, str))
- if subfield and not isinstance(subfield, SubField):
- raise ValueError("'subfield' %r must be a SubField instance" % subfield)
self.field = field
self.label = label
self.subfield = subfield
@@ -208,11 +203,11 @@ exchangelib.fields
Module
# For indexed properties, get either the full property set, the property with matching label, or a particular
# subfield.
if self.label:
- for subitem in getattr(item, self.field.name):
- if subitem.label == self.label:
+ for sub_item in getattr(item, self.field.name):
+ if sub_item.label == self.label:
if self.subfield:
- return getattr(subitem, self.subfield.name)
- return subitem
+ return getattr(sub_item, self.subfield.name)
+ return sub_item
return None # No item with this label
return getattr(item, self.field.name)
@@ -226,7 +221,7 @@ exchangelib.fields
Module
def to_xml(self):
if isinstance(self.field, IndexedField):
if not self.label or not self.subfield:
- raise ValueError("Field path for indexed field '%s' is missing label and/or subfield" % self.field.name)
+ raise ValueError(f"Field path for indexed field {self.field.name!r} is missing label and/or subfield")
return self.subfield.field_uri_xml(field_uri=self.field.field_uri, label=self.label)
return self.field.field_uri_xml()
@@ -248,8 +243,8 @@ exchangelib.fields
Module
if self.label:
from .indexed_properties import SingleFieldIndexedElement
if issubclass(self.field.value_cls, SingleFieldIndexedElement) or not self.subfield:
- return '%s__%s' % (self.field.name, self.label)
- return '%s__%s__%s' % (self.field.name, self.label, self.subfield.name)
+ return f'{self.field.name}__{self.label}'
+ return f'{self.field.name}__{self.label}__{self.subfield.name}'
return self.field.name
def __eq__(self, other):
@@ -267,12 +262,12 @@ exchangelib.fields
Module
class FieldOrder:
"""Holds values needed to call server-side sorting on a single field path."""
-
def __init__(self, field_path, reverse=False):
- if not isinstance(field_path, FieldPath):
- raise ValueError("'field_path' %r must be a FieldPath instance" % field_path)
- if not isinstance(reverse, bool):
- raise ValueError("'reverse' %r must be a boolean" % reverse)
+ """
+
+ :param field_path: A FieldPath instance
+ :param reverse: A bool
+ """
self.field_path = field_path
self.reverse = reverse
@@ -305,7 +300,7 @@ exchangelib.fields
Module
def __init__(self, name=None, is_required=False, is_required_after_save=False, is_read_only=False,
is_read_only_after_send=False, is_searchable=True, is_attribute=False, default=None,
supported_from=None, deprecated_from=None):
- self.name = name
+ self.name = name # Usually set by the EWSMeta metaclass
self.default = default # Default value if none is given
self.is_required = is_required
# Some fields cannot be deleted on update. Default to True if 'is_required' is set
@@ -322,49 +317,48 @@ exchangelib.fields
Module
# The Exchange build when this field was introduced. When talking with versions prior to this version,
# we will ignore this field.
if supported_from is not None and not isinstance(supported_from, Build):
- raise ValueError("'supported_from' %r must be a Build instance" % supported_from)
+ raise InvalidTypeError('supported_from', supported_from, Build)
self.supported_from = supported_from
# The Exchange build when this field was deprecated. When talking with versions at or later than this version,
# we will ignore this field.
if deprecated_from is not None and not isinstance(deprecated_from, Build):
- raise ValueError("'deprecated_from' %r must be a Build instance" % deprecated_from)
+ raise InvalidTypeError('deprecated_from', deprecated_from, Build)
self.deprecated_from = deprecated_from
def clean(self, value, version=None):
if version and not self.supports_version(version):
- raise InvalidFieldForVersion("Field '%s' does not support EWS builds prior to %s (server has %s)" % (
- self.name, self.supported_from, version))
+ raise InvalidFieldForVersion(
+ f"Field {self.name!r} does not support EWS builds prior to {self.supported_from} (server has {version})"
+ )
if value is None:
if self.is_required and self.default is None:
- raise ValueError("'%s' is a required field with no default" % self.name)
+ raise ValueError(f"{self.name!r} is a required field with no default")
return self.default
if self.is_list:
if not is_iterable(value):
- raise ValueError("Field '%s' value %r must be a list" % (self.name, value))
+ raise TypeError(f"Field {self.name!r} value {value!r} must be of type {list}")
for v in value:
if not isinstance(v, self.value_cls):
- raise TypeError("Field '%s' value %r must be of type %s" % (self.name, v, self.value_cls))
+ raise TypeError(f"Field {self.name!r} value {v!r} must be of type {self.value_cls}")
if hasattr(v, 'clean'):
v.clean(version=version)
else:
if not isinstance(value, self.value_cls):
- raise TypeError("Field '%s' value %r must be of type %s" % (self.name, value, self.value_cls))
+ raise TypeError(f"Field {self.name!r} value {value!r} must be of type {self.value_cls}")
if hasattr(value, 'clean'):
value.clean(version=version)
return value
@abc.abstractmethod
def from_xml(self, elem, account):
- pass
+ """Read a value from the given element"""
@abc.abstractmethod
def to_xml(self, value, version):
- pass
+ """Convert this field to an XML element"""
def supports_version(self, version):
# 'version' is a Version instance, for convenience by callers
- if not isinstance(version, Version):
- raise ValueError("'version' %r must be a Version instance" % version)
if self.supported_from and version.build < self.supported_from:
return False
if self.deprecated_from and version.build >= self.deprecated_from:
@@ -376,15 +370,16 @@ exchangelib.fields
Module
@abc.abstractmethod
def __hash__(self):
- pass
+ """Field instances must be hashable"""
def __repr__(self):
- return self.__class__.__name__ + '(%s)' % ', '.join('%s=%r' % (f, getattr(self, f)) for f in (
+ args_str = ', '.join(f'{f}={getattr(self, f)!r}' for f in (
'name', 'value_cls', 'is_list', 'is_complex', 'default'))
+ return f'{self.__class__.__name__}({args_str})'
class FieldURIField(Field):
- """A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or arrtibute. It
+ """A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or attribute. It
may additionally be a label for searching, filtering and limiting fields. In that case, the FieldURI format will be
'itemtype:FieldName'
"""
@@ -425,18 +420,18 @@ exchangelib.fields
Module
def field_uri_xml(self):
from .properties import FieldURI
if not self.field_uri:
- raise ValueError("'field_uri' value is missing")
+ raise ValueError(f"'field_uri' value is missing on field '{self.name}'")
return FieldURI(field_uri=self.field_uri).to_xml(version=None)
def request_tag(self):
if not self.field_uri_postfix:
- raise ValueError("'field_uri_postfix' value is missing")
- return 't:%s' % self.field_uri_postfix
+ raise ValueError(f"'field_uri_postfix' value is missing on field '{self.name}'")
+ return f't:{self.field_uri_postfix}'
def response_tag(self):
if not self.field_uri_postfix:
- raise ValueError("'field_uri_postfix' value is missing")
- return '{%s}%s' % (self.namespace, self.field_uri_postfix)
+ raise ValueError(f"'field_uri_postfix' value is missing on field '{self.name}'")
+ return f'{{{self.namespace}}}{self.field_uri_postfix}'
def __hash__(self):
return hash(self.field_uri)
@@ -465,9 +460,9 @@ exchangelib.fields
Module
def _clean_single_value(self, v):
if self.min is not None and v < self.min:
raise ValueError(
- "Value %r on field '%s' must be greater than %s" % (v, self.name, self.min))
+ f"Value {v!r} on field {self.name!r} must be greater than {self.min}")
if self.max is not None and v > self.max:
- raise ValueError("Value %r on field '%s' must be less than %s" % (v, self.name, self.max))
+ raise ValueError(f"Value {v!r} on field {self.name!r} must be less than {self.max}")
def clean(self, value, version=None):
value = super().clean(value, version=version)
@@ -506,25 +501,21 @@ exchangelib.fields
Module
for i, v in enumerate(value):
if isinstance(v, str):
if v not in self.enum:
- raise ValueError(
- "List value '%s' on field '%s' must be one of %s" % (v, self.name, self.enum))
+ raise ValueError(f"List value {v!r} on field {self.name!r} must be one of {sorted(self.enum)}")
value[i] = self.enum.index(v) + 1
if not value:
- raise ValueError("Value '%s' on field '%s' must not be empty" % (value, self.name))
+ raise ValueError(f"Value {value!r} on field {self.name!r} must not be empty")
if len(value) > len(set(value)):
- raise ValueError("List entries '%s' on field '%s' must be unique" % (value, self.name))
+ raise ValueError(f"List entries {value!r} on field {self.name!r} must be unique")
else:
if isinstance(value, str):
if value not in self.enum:
- raise ValueError(
- "Value '%s' on field '%s' must be one of %s" % (value, self.name, self.enum))
+ raise ValueError(f"Value {value!r} on field {self.name!r} must be one of {sorted(self.enum)}")
value = self.enum.index(value) + 1
return super().clean(value, version=version)
def as_string(self, value):
# Converts an integer in the enum to its equivalent string
- if isinstance(value, str):
- return value
if self.is_list:
return [self.enum[v - 1] for v in sorted(value)]
return self.enum[value - 1]
@@ -554,6 +545,15 @@ exchangelib.fields
Module
is_list = True
+class WeekdaysField(EnumListField):
+ """Like EnumListField, allow a single value instead of a 1-element list."""
+
+ def clean(self, value, version=None):
+ if isinstance(value, (int, str)):
+ value = [value]
+ return super().clean(value, version)
+
+
class EnumAsIntField(EnumField):
"""Like EnumField, but communicates values with EWS in integers."""
@@ -669,6 +669,25 @@ exchangelib.fields
Module
return self.default
+class TimeDeltaField(FieldURIField):
+ """A field that handles timedelta values."""
+
+ value_cls = datetime.timedelta
+
+ def __init__(self, *args, **kwargs):
+ self.min = kwargs.pop('min', datetime.timedelta(0))
+ self.max = kwargs.pop('max', datetime.timedelta(days=1))
+ super().__init__(*args, **kwargs)
+
+ def clean(self, value, version=None):
+ if self.min is not None and value < self.min:
+ raise ValueError(
+ f"Value {value!r} on field {self.name!r} must be greater than {self.min}")
+ if self.max is not None and value > self.max:
+ raise ValueError(f"Value {value!r} on field {self.name!r} must be less than {self.max}")
+ return super().clean(value, version=version)
+
+
class DateTimeField(FieldURIField):
"""A field that handles datetime values."""
@@ -677,7 +696,7 @@ exchangelib.fields
Module
def clean(self, value, version=None):
if isinstance(value, datetime.datetime):
if not value.tzinfo:
- raise ValueError("Value '%s' on field '%s' must be timezone aware" % (value, self.name))
+ raise ValueError(f"Value {value!r} on field {self.name!r} must be timezone aware")
if type(value) is datetime.datetime:
value = self.value_cls.from_datetime(value)
return super().clean(value, version=version)
@@ -761,7 +780,7 @@ exchangelib.fields
Module
return self.default
def to_xml(self, value, version):
- attrs = OrderedDict([('Id', value.ms_id)])
+ attrs = dict(Id=value.ms_id)
if value.ms_name:
attrs['Name'] = value.ms_name
return create_element(self.request_tag(), attrs=attrs)
@@ -779,12 +798,28 @@ exchangelib.fields
Module
is_list = True
+ def __init__(self, *args, **kwargs):
+ self.list_elem_name = kwargs.pop('list_elem_name', 'String')
+ super().__init__(*args, **kwargs)
+
+ def list_elem_request_tag(self):
+ return f't:{self.list_elem_name}'
+
+ def list_elem_response_tag(self):
+ return f'{{{self.namespace}}}{self.list_elem_name}'
+
def from_xml(self, elem, account):
iter_elem = elem.find(self.response_tag())
if iter_elem is not None:
- return get_xml_attrs(iter_elem, '{%s}String' % TNS)
+ return get_xml_attrs(iter_elem, self.list_elem_response_tag())
return self.default
+ def to_xml(self, value, version):
+ field_elem = create_element(self.request_tag())
+ for v in value:
+ field_elem.append(set_xml_value(create_element(self.list_elem_request_tag()), v, version=version))
+ return field_elem
+
class MessageField(TextField):
"""A field that handles the Message element."""
@@ -795,14 +830,14 @@ exchangelib.fields
Module
reply = elem.find(self.response_tag())
if reply is None:
return None
- message = reply.find('{%s}%s' % (TNS, self.INNER_ELEMENT_NAME))
+ message = reply.find(f'{{{TNS}}}{self.INNER_ELEMENT_NAME}')
if message is None:
return None
return message.text
def to_xml(self, value, version):
field_elem = create_element(self.request_tag())
- message = create_element('t:%s' % self.INNER_ELEMENT_NAME)
+ message = create_element(f't:{self.INNER_ELEMENT_NAME}')
message.text = value
return set_xml_value(field_elem, message, version=version)
@@ -821,14 +856,8 @@ exchangelib.fields
Module
def clean(self, value, version=None):
value = super().clean(value, version=version)
- if value is not None:
- if self.is_list:
- for v in value:
- if len(v) > self.max_length:
- raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, v, self.max_length))
- else:
- if len(value) > self.max_length:
- raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, value, self.max_length))
+ if value is not None and len(value) > self.max_length:
+ raise ValueError(f"{self.name!r} value {value!r} exceeds length {self.max_length}")
return value
@@ -845,28 +874,28 @@ exchangelib.fields
Module
self.is_attribute = True
-class CharListField(CharField):
- """Like CharField, but for lists of strings."""
-
- is_list = True
+class CharListField(TextListField):
+ """Like TextListField, but for string values with a limited length."""
def __init__(self, *args, **kwargs):
- self.list_elem_name = kwargs.pop('list_elem_name', 'String')
+ self.max_length = kwargs.pop('max_length', 255)
+ if not 1 <= self.max_length <= 255:
+ # A field supporting messages longer than 255 chars should be TextField
+ raise ValueError("'max_length' must be in the range 1-255")
super().__init__(*args, **kwargs)
- def list_elem_tag(self):
- return '{%s}%s' % (self.namespace, self.list_elem_name)
-
- def from_xml(self, elem, account):
- iter_elem = elem.find(self.response_tag())
- if iter_elem is not None:
- return get_xml_attrs(iter_elem, self.list_elem_tag())
- return self.default
+ def clean(self, value, version=None):
+ value = super().clean(value, version=version)
+ if value is not None:
+ for v in value:
+ if len(v) > self.max_length:
+ raise ValueError(f"{self.name!r} value {v!r} exceeds length {self.max_length}")
+ return value
class URIField(TextField):
"""Helper to mark strings that must conform to xsd:anyURI.
- If we want an URI validator, see http://stackoverflow.com/questions/14466585/is-this-regex-correct-for-xsdanyuri
+ If we want a URI validator, see https://stackoverflow.com/questions/14466585/is-this-regex-correct-for-xsdanyuri
"""
@@ -887,8 +916,6 @@ exchangelib.fields
Module
def supports_version(self, version):
# 'version' is a Version instance, for convenience by callers
- if not isinstance(version, Version):
- raise ValueError("'version' %r must be a Version instance" % version)
if not self.supported_from:
return True
return version.build >= self.supported_from
@@ -911,14 +938,14 @@ exchangelib.fields
Module
if value in valid_choices_for_version:
return value
if value in valid_choices:
- raise InvalidChoiceForVersion("Choice '%s' only supports EWS builds from %s to %s (server has %s)" % (
- self.name, self.supported_from or '*', self.deprecated_from or '*', version))
+ raise InvalidChoiceForVersion(
+ f"Choice {self.name!r} only supports EWS builds from {self.supported_from or '*'} to "
+ f"{self.deprecated_from or '*'} (server has {version})"
+ )
else:
if value in valid_choices:
return value
- raise ValueError("Invalid choice '%s' for field '%s'. Valid choices are: %s" % (
- value, self.name, ', '.join(valid_choices)
- ))
+ raise ValueError(f"Invalid choice {value!r} for field {self.name!r}. Valid choices are {sorted(valid_choices)}")
def supported_choices(self, version):
return tuple(c.value for c in self.choices if c.supports_version(version))
@@ -963,12 +990,11 @@ exchangelib.fields
Module
def to_xml(self, value, version):
from .properties import Body, HTMLBody
- field_elem = create_element(self.request_tag())
body_type = {
Body: Body.body_type,
HTMLBody: HTMLBody.body_type,
}[type(value)]
- field_elem.set('BodyType', body_type)
+ field_elem = create_element(self.request_tag(), attrs=dict(BodyType=body_type))
return set_xml_value(field_elem, value, version=version)
@@ -1018,6 +1044,21 @@ exchangelib.fields
Module
is_complex = True
+class TransitionListField(EWSElementListField):
+ def __init__(self, *args, **kwargs):
+ from .properties import BaseTransition
+ kwargs['value_cls'] = BaseTransition
+ super().__init__(*args, **kwargs)
+
+ def from_xml(self, elem, account):
+ iter_elem = elem.find(self.response_tag()) if self.field_uri else elem
+ if iter_elem is not None:
+ return [
+ self.value_cls.transition_model_from_tag(e.tag).from_xml(elem=e, account=account) for e in iter_elem
+ ]
+ return self.default
+
+
class AssociatedCalendarItemIdField(EWSElementField):
is_complex = True
@@ -1081,8 +1122,8 @@ exchangelib.fields
Module
super().__init__(*args, **kwargs)
-class BaseEmailField(EWSElementField):
- """A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for."""
+class BaseEmailField(EWSElementField, metaclass=abc.ABCMeta):
+ """Base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for."""
is_complex = True # FindItem only returns the name, not the email address
@@ -1102,7 +1143,7 @@ exchangelib.fields
Module
nested_elem = sub_elem.find(self.value_cls.response_tag())
if nested_elem is None:
raise ValueError(
- 'Expected XML element %r missing on field %r' % (self.value_cls.response_tag(), self.name)
+ f'Expected XML element {self.value_cls.response_tag()!r} missing on field {self.name!r}'
)
return self.value_cls.from_xml(elem=nested_elem, account=account)
return self.value_cls.from_xml(elem=sub_elem, account=account)
@@ -1224,7 +1265,7 @@ exchangelib.fields
Module
def clean(self, value, version=None):
value = super().clean(value, version=version)
if self.is_required and not value:
- raise ValueError('Value for subfield %r must be non-empty' % self.name)
+ raise ValueError(f'Value for subfield {self.name!r} must be non-empty')
return value
def __hash__(self):
@@ -1264,16 +1305,16 @@ exchangelib.fields
Module
def field_uri_xml(self, field_uri, label):
from .properties import IndexedFieldURI
- return IndexedFieldURI(field_uri='%s:%s' % (field_uri, self.field_uri), field_index=label).to_xml(version=None)
+ return IndexedFieldURI(field_uri=f'{field_uri}:{self.field_uri}', field_index=label).to_xml(version=None)
def request_tag(self):
- return 't:%s' % self.field_uri
+ return f't:{self.field_uri}'
def response_tag(self):
- return '{%s}%s' % (self.namespace, self.field_uri)
+ return f'{{{self.namespace}}}{self.field_uri}'
-class IndexedField(EWSElementField):
+class IndexedField(EWSElementField, metaclass=abc.ABCMeta):
"""A base class for all indexed fields."""
PARENT_ELEMENT_NAME = None
@@ -1282,14 +1323,14 @@ exchangelib.fields
Module
from .indexed_properties import IndexedElement
value_cls = kwargs['value_cls']
if not issubclass(value_cls, IndexedElement):
- raise ValueError("'value_cls' %r must be a subclass of IndexedElement" % value_cls)
+ raise TypeError(f"'value_cls' {value_cls!r} must be a subclass of type {IndexedElement}")
super().__init__(*args, **kwargs)
def to_xml(self, value, version):
- return set_xml_value(create_element('t:%s' % self.PARENT_ELEMENT_NAME), value, version)
+ return set_xml_value(create_element(f't:{self.PARENT_ELEMENT_NAME}'), value, version=version)
def response_tag(self):
- return '{%s}%s' % (self.namespace, self.PARENT_ELEMENT_NAME)
+ return f'{{{self.namespace}}}{self.PARENT_ELEMENT_NAME}'
def __hash__(self):
return hash(self.field_uri)
@@ -1297,6 +1338,7 @@ exchangelib.fields
Module
class EmailAddressesField(IndexedField):
is_list = True
+ is_complex = True
PARENT_ELEMENT_NAME = 'EmailAddresses'
@@ -1309,7 +1351,7 @@ exchangelib.fields
Module
if value is not None:
default_labels = self.value_cls.LABEL_CHOICES
if len(value) > len(default_labels):
- raise ValueError('This field can handle at most %s values (value: %r)' % (len(default_labels), value))
+ raise ValueError(f'This field can handle at most {len(default_labels)} values (value: {value})')
tmp = []
for s, default_label in zip(value, default_labels):
if not isinstance(s, str):
@@ -1322,6 +1364,7 @@ exchangelib.fields
Module
class PhoneNumberField(IndexedField):
is_list = True
+ is_complex = True
PARENT_ELEMENT_NAME = 'PhoneNumbers'
@@ -1333,6 +1376,7 @@ exchangelib.fields
Module
class PhysicalAddressField(IndexedField):
is_list = True
+ is_complex = True
PARENT_ELEMENT_NAME = 'PhysicalAddresses'
@@ -1343,6 +1387,8 @@ exchangelib.fields
Module
class ExtendedPropertyField(Field):
+ is_complex = True
+
def __init__(self, *args, **kwargs):
self.value_cls = kwargs.pop('value_cls')
super().__init__(*args, **kwargs)
@@ -1350,7 +1396,7 @@ exchangelib.fields
Module
def clean(self, value, version=None):
if value is None:
if self.is_required:
- raise ValueError("'%s' is a required field" % self.name)
+ raise ValueError(f"{self.name!r} is a required field")
return self.default
if not isinstance(value, self.value_cls):
# Allow keeping ExtendedProperty field values as their simple Python type, but run clean() anyway
@@ -1383,11 +1429,9 @@ exchangelib.fields
Module
extended_property = create_element(self.value_cls.request_tag())
set_xml_value(extended_property, self.field_uri_xml(), version=version)
if isinstance(value, self.value_cls):
- set_xml_value(extended_property, value, version=version)
- else:
- # Allow keeping ExtendedProperty field values as their simple Python type
- set_xml_value(extended_property, self.value_cls(value), version=version)
- return extended_property
+ return set_xml_value(extended_property, value, version=version)
+ # Allow keeping ExtendedProperty field values as their simple Python type
+ return set_xml_value(extended_property, self.value_cls(value), version=version)
def __hash__(self):
return hash(self.name)
@@ -1418,7 +1462,7 @@ exchangelib.fields
Module
class UnknownEntriesField(CharListField):
def list_elem_tag(self):
- return '{%s}UnknownEntry' % self.namespace
+ return f'{{{self.namespace}}}UnknownEntry'
class PermissionSetField(EWSElementField):
@@ -1513,9 +1557,9 @@ exchangelib.fields
Module
first = next(iter(value))
except StopIteration:
first = None
- value_type = '%sArray' % cls.TYPES_MAP_REVERSED[type(first)]
+ value_type = f'{cls.TYPES_MAP_REVERSED[type(first)]}Array'
if value_type not in cls.TYPES_MAP:
- raise ValueError('%r is not a supported type' % value)
+ raise ValueError(f'{value!r} is not a supported type')
return value_type
return cls.TYPES_MAP_REVERSED[type(value)]
@@ -1526,7 +1570,7 @@ exchangelib.fields
Module
def clean(self, value, version=None):
if value is None:
if self.is_required and self.default is None:
- raise ValueError("'%s' is a required field with no default" % self.name)
+ raise ValueError(f"{self.name!r} is a required field with no default")
return self.default
return value
@@ -1534,8 +1578,8 @@ exchangelib.fields
Module
field_elem = elem.find(self.response_tag())
if field_elem is None:
return self.default
- value_type_str = get_xml_attr(field_elem, '{%s}Type' % TNS)
- value = get_xml_attr(field_elem, '{%s}Value' % TNS)
+ value_type_str = get_xml_attr(field_elem, f'{{{TNS}}}Type')
+ value = get_xml_attr(field_elem, f'{{{TNS}}}Value')
if value_type_str == 'Byte':
try:
# The value is an unsigned integer in the range 0 -> 255. Convert it to a single byte
@@ -1694,56 +1738,51 @@ exchangelib.fields
Functions
label and SubField object.
"""
from .indexed_properties import SingleFieldIndexedElement, MultiFieldIndexedElement
- fieldname, label, subfieldname = split_field_path(field_path)
+ fieldname, label, subfield_name = split_field_path(field_path)
field = folder.get_item_field_by_fieldname(fieldname)
subfield = None
if isinstance(field, IndexedField):
if strict and not label:
raise ValueError(
- "IndexedField path '%s' must specify label, e.g. '%s__%s'"
- % (field_path, fieldname, field.value_cls.get_field_by_fieldname('label').default)
+ f"IndexedField path {field_path!r} must specify label, e.g. "
+ f"'{fieldname}__{field.value_cls.get_field_by_fieldname('label').default}'"
)
valid_labels = field.value_cls.get_field_by_fieldname('label').supported_choices(
version=folder.account.version
)
if label and label not in valid_labels:
raise ValueError(
- "Label '%s' on IndexedField path '%s' must be one of %s"
- % (label, field_path, ', '.join(valid_labels))
+ f"Label {label!r} on IndexedField path {field_path!r} must be one of {sorted(valid_labels)}"
)
if issubclass(field.value_cls, MultiFieldIndexedElement):
- if strict and not subfieldname:
+ if strict and not subfield_name:
raise ValueError(
- "IndexedField path '%s' must specify subfield, e.g. '%s__%s__%s'"
- % (field_path, fieldname, label, field.value_cls.FIELDS[1].name)
+ f"IndexedField path {field_path!r} must specify subfield, e.g. "
+ f"'{fieldname}__{label}__{field.value_cls.FIELDS[1].name}'"
)
- if subfieldname:
+ if subfield_name:
try:
- subfield = field.value_cls.get_field_by_fieldname(subfieldname)
+ subfield = field.value_cls.get_field_by_fieldname(subfield_name)
except ValueError:
- fnames = ', '.join(f.name for f in field.value_cls.supported_fields(
+ field_names = ', '.join(f.name for f in field.value_cls.supported_fields(
version=folder.account.version
))
raise ValueError(
- "Subfield '%s' on IndexedField path '%s' must be one of %s"
- % (subfieldname, field_path, fnames)
+ f"Subfield {subfield_name!r} on IndexedField path {field_path!r} "
+ f"must be one of {sorted(field_names)}"
)
else:
if not issubclass(field.value_cls, SingleFieldIndexedElement):
- raise ValueError("'field.value_cls' %r must be an SingleFieldIndexedElement instance" % field.value_cls)
- if subfieldname:
+ raise InvalidTypeError('field.value_cls', field.value_cls, SingleFieldIndexedElement)
+ if subfield_name:
raise ValueError(
- "IndexedField path '%s' must not specify subfield, e.g. just '%s__%s'"
- % (field_path, fieldname, label)
+ f"IndexedField path {field_path!r} must not specify subfield, e.g. just {fieldname}__{label}'"
)
subfield = field.value_cls.value_field(version=folder.account.version)
else:
- if label or subfieldname:
- raise ValueError(
- "Field path '%s' must not specify label or subfield, e.g. just '%s'"
- % (field_path, fieldname)
- )
+ if label or subfield_name:
+ raise ValueError(f"Field path {field_path!r} must not specify label or subfield, e.g. just {fieldname!r}")
return field, label, subfieldFunctions
'physical_addresses__Home__street' -> ('physical_addresses', 'Home', 'street')
"""
if not isinstance(field_path, str):
- raise ValueError("Field path %r must be a string" % field_path)
+ raise InvalidTypeError('field_path', field_path, str)
search_parts = field_path.split('__')
field = search_parts[0]
try:
@@ -1850,29 +1889,12 @@ Class variables
-Methods
-
-
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- val = super().from_xml(elem=elem, account=account)
- if val is None:
- return val
- return tuple(name for name, mask in self.STATES.items() if bool(val & mask))
Inherited members
-IntegerField
:
Class variables
-Methods
-
-
+
-def to_xml(self, value, version)
-
-Expand source code
-
-
-def to_xml(self, value, version):
- return value.to_xml(version=version)
Inherited members
+
+
EWSElementField
:
+
+
class AttachmentField
@@ -1967,32 +1982,15 @@
Ancestors
Methods
-
-
+
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- from .attachments import FileAttachment, ItemAttachment
- iter_elem = elem.find(self.response_tag())
- # Look for both FileAttachment and ItemAttachment
- if iter_elem is not None:
- attachments = []
- for att_type in (ItemAttachment, FileAttachment):
- attachments.extend(
- [att_type.from_xml(elem=e, account=account) for e in iter_elem.findall(att_type.response_tag())]
- )
- return attachments
- return self.default
Inherited members
+
+
EWSElementListField
:
+
+
class AttendeesField
@@ -2044,6 +2042,15 @@
Methods
+Inherited members
+
+
EWSElementListField
:
+
+
class Base64Field
@@ -2095,19 +2102,28 @@
Class variables
- an integerInherited members
+
+
FieldURIField
:
+
+
class BaseEmailField
(*args, **kwargs)
Expand source code
-class BaseEmailField(EWSElementField):
- """A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for."""
+
class BaseEmailField(EWSElementField, metaclass=abc.ABCMeta):
+ """Base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for."""
is_complex = True # FindItem only returns the name, not the email address
@@ -2127,7 +2143,7 @@
Class variables
nested_elem = sub_elem.find(self.value_cls.response_tag())
if nested_elem is None:
raise ValueError(
- 'Expected XML element %r missing on field %r' % (self.value_cls.response_tag(), self.name)
+ f'Expected XML element {self.value_cls.response_tag()!r} missing on field {self.name!r}'
)
return self.value_cls.from_xml(elem=nested_elem, account=account)
return self.value_cls.from_xml(elem=sub_elem, account=account)
@@ -2169,34 +2185,16 @@ Methods
return super().clean(value, version=version)
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- if self.field_uri is None:
- sub_elem = elem.find(self.value_cls.response_tag())
- else:
- sub_elem = elem.find(self.response_tag())
- if sub_elem is not None:
- if self.field_uri is not None:
- # We want the nested Mailbox, not the wrapper element
- nested_elem = sub_elem.find(self.value_cls.response_tag())
- if nested_elem is None:
- raise ValueError(
- 'Expected XML element %r missing on field %r' % (self.value_cls.response_tag(), self.name)
- )
- return self.value_cls.from_xml(elem=nested_elem, account=account)
- return self.value_cls.from_xml(elem=sub_elem, account=account)
- return self.default
Inherited members
+
+
EWSElementField
:
+
+
class BodyContentAttributedValueField
@@ -2229,6 +2227,15 @@
Class variables
+Inherited members
+
+
EWSElementField
:
+
+
class BodyField
@@ -2267,12 +2274,11 @@
Class variables
def to_xml(self, value, version):
from .properties import Body, HTMLBody
- field_elem = create_element(self.request_tag())
body_type = {
Body: Body.body_type,
HTMLBody: HTMLBody.body_type,
}[type(value)]
- field_elem.set('BodyType', body_type)
+ field_elem = create_element(self.request_tag(), attrs=dict(BodyType=body_type))
return set_xml_value(field_elem, value, version=version)Ancestors
@@ -2298,53 +2304,13 @@ Methods
return super().clean(value, version=version)
-
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- from .properties import Body, HTMLBody
- field_elem = elem.find(self.response_tag())
- val = None if field_elem is None else field_elem.text or None
- if val is not None:
- body_type = field_elem.get('BodyType')
- return {
- Body.body_type: Body,
- HTMLBody.body_type: HTMLBody,
- }[body_type](val)
- return self.default
-def to_xml(self, value, version)
-
-Expand source code
-
-
-def to_xml(self, value, version):
- from .properties import Body, HTMLBody
- field_elem = create_element(self.request_tag())
- body_type = {
- Body: Body.body_type,
- HTMLBody: HTMLBody.body_type,
- }[type(value)]
- field_elem.set('BodyType', body_type)
- return set_xml_value(field_elem, value, version=version)
Inherited members
Inherited members
+
+
FieldURIField
:
+
+
class BuildField
@@ -2416,32 +2391,12 @@
Ancestors
Methods
-
-
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- val = self._get_val_from_elem(elem)
- if val:
- try:
- return self.value_cls.from_hex_string(val)
- except (TypeError, ValueError):
- log.warning('Invalid server version string: %r', val)
- return val
Inherited members
CharField
:
Inherited members
def clean(self, value, version=None):
value = super().clean(value, version=version)
- if value is not None:
- if self.is_list:
- for v in value:
- if len(v) > self.max_length:
- raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, v, self.max_length))
- else:
- if len(value) > self.max_length:
- raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, value, self.max_length))
+ if value is not None and len(value) > self.max_length:
+ raise ValueError(f"{self.name!r} value {value!r} exceeds length {self.max_length}")
return valueAncestors
@@ -2490,7 +2439,6 @@ Ancestors
Subclasses
Methods
@@ -2532,6 +2474,8 @@ def clean(self, value, version=None):
value = super().clean(value, version=version)
- if value is not None:
- if self.is_list:
- for v in value:
- if len(v) > self.max_length:
- raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, v, self.max_length))
- else:
- if len(value) > self.max_length:
- raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, value, self.max_length))
+ if value is not None and len(value) > self.max_length:
+ raise ValueError(f"{self.name!r} value {value!r} exceeds length {self.max_length}")
return value
Inherited members
Expand source code
-class CharListField(CharField):
- """Like CharField, but for lists of strings."""
-
- is_list = True
+
+ def clean(self, value, version=None):
+ value = super().clean(value, version=version)
+ if value is not None:
+ for v in value:
+ if len(v) > self.max_length:
+ raise ValueError(f"{self.name!r} value {v!r} exceeds length {self.max_length}")
+ return valueclass CharListField(TextListField):
+ """Like TextListField, but for string values with a limited length."""
def __init__(self, *args, **kwargs):
- self.list_elem_name = kwargs.pop('list_elem_name', 'String')
+ self.max_length = kwargs.pop('max_length', 255)
+ if not 1 <= self.max_length <= 255:
+ # A field supporting messages longer than 255 chars should be TextField
+ raise ValueError("'max_length' must be in the range 1-255")
super().__init__(*args, **kwargs)
- def list_elem_tag(self):
- return '{%s}%s' % (self.namespace, self.list_elem_name)
-
- def from_xml(self, elem, account):
- iter_elem = elem.find(self.response_tag())
- if iter_elem is not None:
- return get_xml_attrs(iter_elem, self.list_elem_tag())
- return self.default
Ancestors
-
Subclasses
-Class variables
-
-
var is_list
Methods
-
-def from_xml(self, elem, account)
+
+def clean(self, value, version=None)
Methods
-
-def from_xml(self, elem, account):
- iter_elem = elem.find(self.response_tag())
- if iter_elem is not None:
- return get_xml_attrs(iter_elem, self.list_elem_tag())
- return self.default
-def list_elem_tag(self)
-
-Expand source code
-
-
+def list_elem_tag(self):
- return '{%s}%s' % (self.namespace, self.list_elem_name)
def clean(self, value, version=None):
+ value = super().clean(value, version=version)
+ if value is not None:
+ for v in value:
+ if len(v) > self.max_length:
+ raise ValueError(f"{self.name!r} value {v!r} exceeds length {self.max_length}")
+ return value
Inherited members
@@ -2643,8 +2571,6 @@ Inherited members
def supports_version(self, version):
# 'version' is a Version instance, for convenience by callers
- if not isinstance(version, Version):
- raise ValueError("'version' %r must be a Version instance" % version)
if not self.supported_from:
return True
return version.build >= self.supported_from
@@ -2662,8 +2588,6 @@ Methods
@@ -2698,14 +2622,14 @@ def supports_version(self, version):
# 'version' is a Version instance, for convenience by callers
- if not isinstance(version, Version):
- raise ValueError("'version' %r must be a Version instance" % version)
if not self.supported_from:
return True
return version.build >= self.supported_from
Methods
if value in valid_choices_for_version:
return value
if value in valid_choices:
- raise InvalidChoiceForVersion("Choice '%s' only supports EWS builds from %s to %s (server has %s)" % (
- self.name, self.supported_from or '*', self.deprecated_from or '*', version))
+ raise InvalidChoiceForVersion(
+ f"Choice {self.name!r} only supports EWS builds from {self.supported_from or '*'} to "
+ f"{self.deprecated_from or '*'} (server has {version})"
+ )
else:
if value in valid_choices:
return value
- raise ValueError("Invalid choice '%s' for field '%s'. Valid choices are: %s" % (
- value, self.name, ', '.join(valid_choices)
- ))
+ raise ValueError(f"Invalid choice {value!r} for field {self.name!r}. Valid choices are {sorted(valid_choices)}")
def supported_choices(self, version):
return tuple(c.value for c in self.choices if c.supports_version(version))
@@ -2744,14 +2668,14 @@ Methods
if value in valid_choices_for_version:
return value
if value in valid_choices:
- raise InvalidChoiceForVersion("Choice '%s' only supports EWS builds from %s to %s (server has %s)" % (
- self.name, self.supported_from or '*', self.deprecated_from or '*', version))
+ raise InvalidChoiceForVersion(
+ f"Choice {self.name!r} only supports EWS builds from {self.supported_from or '*'} to "
+ f"{self.deprecated_from or '*'} (server has {version})"
+ )
else:
if value in valid_choices:
return value
- raise ValueError("Invalid choice '%s' for field '%s'. Valid choices are: %s" % (
- value, self.name, ', '.join(valid_choices)
- ))
+ raise ValueError(f"Invalid choice {value!r} for field {self.name!r}. Valid choices are {sorted(valid_choices)}")
@@ -2772,6 +2696,8 @@
-Inherited members
CharField
:
Inherited members
CharField
:
Methods
+Inherited members
+
+
FieldURIField
:
+
+
class DateOrDateTimeField
@@ -2933,28 +2870,13 @@
-Methods
return super().clean(value=value, version=version)
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- val = self._get_val_from_elem(elem)
- if val is not None and len(val) == 16:
- # This is a date format with timezone info, as sent by task recurrences. Eg: '2006-01-09+01:00'
- return self._date_field.from_xml(elem=elem, account=account)
- return super().from_xml(elem=elem, account=account)
Inherited members
DateTimeField
:
Methods
return self._datetime_field.value_cls.combine(value, self._default_time).replace(tzinfo=UTC)
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- val = self._get_val_from_elem(elem)
- if val is not None and len(val) == 25:
- # This is a datetime string with timezone info, e.g. '2021-03-01T21:55:54+00:00'. We don't want to have
- # datetime values converted to UTC before converting to date. EWSDateTime.from_string() insists on
- # converting to UTC, but we don't have an EWSTimeZone we can convert the timezone info to. Instead, parse
- # the string with .fromisoformat().
- return datetime.datetime.fromisoformat(val).date()
- # Revert to default parsing of datetime strings
- res = self._datetime_field.from_xml(elem=elem, account=account)
- if res is None:
- return res
- return res.date()
-def to_xml(self, value, version)
-
-Expand source code
-
-
-def to_xml(self, value, version):
- # Convert date to datetime
- value = self.date_to_datetime(value)
- return self._datetime_field.to_xml(value=value, version=version)
Inherited members
DateField
:
Inherited members
def clean(self, value, version=None):
if isinstance(value, datetime.datetime):
if not value.tzinfo:
- raise ValueError("Value '%s' on field '%s' must be timezone aware" % (value, self.name))
+ raise ValueError(f"Value {value!r} on field {self.name!r} must be timezone aware")
if type(value) is datetime.datetime:
value = self.value_cls.from_datetime(value)
return super().clean(value, version=version)
@@ -3145,43 +3030,22 @@ Methods
-def clean(self, value, version=None):
if isinstance(value, datetime.datetime):
if not value.tzinfo:
- raise ValueError("Value '%s' on field '%s' must be timezone aware" % (value, self.name))
+ raise ValueError(f"Value {value!r} on field {self.name!r} must be timezone aware")
if type(value) is datetime.datetime:
value = self.value_cls.from_datetime(value)
return super().clean(value, version=version)
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- val = self._get_val_from_elem(elem)
- if val is not None:
- try:
- return self.value_cls.from_string(val)
- except ValueError as e:
- if isinstance(e, NaiveDateTimeNotAllowed):
- # We encountered a naive datetime
- if account:
- # Convert to timezone-aware datetime using the default timezone of the account
- tz = account.default_timezone
- log.info('Found naive datetime %s on field %s. Assuming timezone %s', e.local_dt, self.name, tz)
- return e.local_dt.replace(tzinfo=tz)
- # There's nothing we can do but return the naive date. It's better than assuming e.g. UTC.
- log.warning('Returning naive datetime %s on field %s', e.local_dt, self.name)
- return e.local_dt
- log.info("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
- return None
- return self.default
Inherited members
+
+
FieldURIField
:
+
+
class DecimalField
@@ -3208,6 +3072,8 @@
Inherited members
IntegerField
:
Inherited members
(*args, **kwargs)
Methods
return super().clean(value=value, version=version)
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- from .properties import DictionaryEntry
- iter_elem = elem.find(self.response_tag())
- if iter_elem is not None:
- entries = [
- DictionaryEntry.from_xml(elem=e, account=account)
- for e in iter_elem.findall(DictionaryEntry.response_tag())
- ]
- return {e.key: e.value for e in entries}
- return self.default
-def to_xml(self, value, version)
-
-Expand source code
-
-
-def to_xml(self, value, version):
- from .properties import DictionaryEntry
- field_elem = create_element(self.request_tag())
- entries = [DictionaryEntry(key=k, value=v) for k, v in value.items()]
- return set_xml_value(field_elem, entries, version=version)
Inherited members
+
+
FieldURIField
:
+
+
class EWSElementField
@@ -3431,50 +3269,15 @@
Instance variables
-Methods
-
-
+
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- if self.is_list:
- iter_elem = elem.find(self.response_tag())
- if iter_elem is not None:
- return [self.value_cls.from_xml(elem=e, account=account)
- for e in iter_elem.findall(self.value_cls.response_tag())]
- else:
- if self.field_uri is None:
- sub_elem = elem.find(self.value_cls.response_tag())
- else:
- sub_elem = elem.find(self.response_tag())
- if sub_elem is not None:
- return self.value_cls.from_xml(elem=sub_elem, account=account)
- return self.default
-def to_xml(self, value, version)
-
-Expand source code
-
-
-def to_xml(self, value, version):
- if self.field_uri is None:
- return value.to_xml(version=version)
- field_elem = create_element(self.request_tag())
- return set_xml_value(field_elem, value, version=version)
Inherited members
+
+
FieldURIField
:
+
+
class EWSElementListField
@@ -3510,6 +3313,7 @@
@@ -4431,19 +4194,21 @@ Subclasses
Class variables
@@ -3522,6 +3326,15 @@
+Class variables
Inherited members
+
+
EWSElementField
:
+
+
class EffectiveRightsField
@@ -3545,6 +3358,15 @@
Ancestors
Inherited members
+
+
EWSElementField
:
+
+
class EmailAddressAttributedValueField
@@ -3569,6 +3391,15 @@
Ancestors
Inherited members
+
+
EWSElementListField
:
+
+
class EmailAddressField
@@ -3594,6 +3425,8 @@
Inherited members
-class EmailAddressesField(IndexedField):
is_list = True
+ is_complex = True
PARENT_ELEMENT_NAME = 'EmailAddresses'
@@ -3623,7 +3457,7 @@
Inherited members
if value is not None:
default_labels = self.value_cls.LABEL_CHOICES
if len(value) > len(default_labels):
- raise ValueError('This field can handle at most %s values (value: %r)' % (len(default_labels), value))
+ raise ValueError(f'This field can handle at most {len(default_labels)} values (value: {value})')
tmp = []
for s, default_label in zip(value, default_labels):
if not isinstance(s, str):
@@ -3646,6 +3480,10 @@ Class variables
var is_complex
var is_list
Methods
if value is not None:
default_labels = self.value_cls.LABEL_CHOICES
if len(value) > len(default_labels):
- raise ValueError('This field can handle at most %s values (value: %r)' % (len(default_labels), value))
+ raise ValueError(f'This field can handle at most {len(default_labels)} values (value: {value})')
tmp = []
for s, default_label in zip(value, default_labels):
if not isinstance(s, str):
@@ -3678,13 +3516,22 @@ Methods
Inherited members
+
+
IndexedField
:
+
+
class EmailField
(*args, **kwargs)
Expand source code
@@ -3702,6 +3549,15 @@
Ancestors
Inherited members
+
+
BaseEmailField
:
+
+
class EmailSubField
@@ -3726,26 +3582,12 @@
Ancestors
Methods
-
-
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- return elem.text or elem.get('Name') # Sometimes elem.text is empty. Exchange saves the same in 'Name' attr
Inherited members
-Methods
-
-
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- return super(EnumField, self).from_xml(elem=elem, account=account)
-def to_xml(self, value, version)
-
-Expand source code
-
-
-def to_xml(self, value, version):
- field_elem = create_element(self.request_tag())
- return set_xml_value(field_elem, value, version=version)
Inherited members
EnumField
:
Inherited members
for i, v in enumerate(value):
if isinstance(v, str):
if v not in self.enum:
- raise ValueError(
- "List value '%s' on field '%s' must be one of %s" % (v, self.name, self.enum))
+ raise ValueError(f"List value {v!r} on field {self.name!r} must be one of {sorted(self.enum)}")
value[i] = self.enum.index(v) + 1
if not value:
- raise ValueError("Value '%s' on field '%s' must not be empty" % (value, self.name))
+ raise ValueError(f"Value {value!r} on field {self.name!r} must not be empty")
if len(value) > len(set(value)):
- raise ValueError("List entries '%s' on field '%s' must be unique" % (value, self.name))
+ raise ValueError(f"List entries {value!r} on field {self.name!r} must be unique")
else:
if isinstance(value, str):
if value not in self.enum:
- raise ValueError(
- "Value '%s' on field '%s' must be one of %s" % (value, self.name, self.enum))
+ raise ValueError(f"Value {value!r} on field {self.name!r} must be one of {sorted(self.enum)}")
value = self.enum.index(value) + 1
return super().clean(value, version=version)
def as_string(self, value):
# Converts an integer in the enum to its equivalent string
- if isinstance(value, str):
- return value
if self.is_list:
return [self.enum[v - 1] for v in sorted(value)]
return self.enum[value - 1]
@@ -3913,8 +3723,6 @@ Methods
@@ -3935,65 +3743,27 @@ def as_string(self, value):
# Converts an integer in the enum to its equivalent string
- if isinstance(value, str):
- return value
if self.is_list:
return [self.enum[v - 1] for v in sorted(value)]
return self.enum[value - 1]
Methods
for i, v in enumerate(value):
if isinstance(v, str):
if v not in self.enum:
- raise ValueError(
- "List value '%s' on field '%s' must be one of %s" % (v, self.name, self.enum))
+ raise ValueError(f"List value {v!r} on field {self.name!r} must be one of {sorted(self.enum)}")
value[i] = self.enum.index(v) + 1
if not value:
- raise ValueError("Value '%s' on field '%s' must not be empty" % (value, self.name))
+ raise ValueError(f"Value {value!r} on field {self.name!r} must not be empty")
if len(value) > len(set(value)):
- raise ValueError("List entries '%s' on field '%s' must be unique" % (value, self.name))
+ raise ValueError(f"List entries {value!r} on field {self.name!r} must be unique")
else:
if isinstance(value, str):
if value not in self.enum:
- raise ValueError(
- "Value '%s' on field '%s' must be one of %s" % (value, self.name, self.enum))
+ raise ValueError(f"Value {value!r} on field {self.name!r} must be one of {sorted(self.enum)}")
value = self.enum.index(value) + 1
return super().clean(value, version=version)
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- val = self._get_val_from_elem(elem)
- if val is not None:
- try:
- if self.is_list:
- return [self.enum.index(v) + 1 for v in val.split(' ')]
- return self.enum.index(val) + 1
- except ValueError:
- log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
- return None
- return self.default
-def to_xml(self, value, version)
-
-Expand source code
-
-
-def to_xml(self, value, version):
- field_elem = create_element(self.request_tag())
- if self.is_list:
- return set_xml_value(field_elem, ' '.join(self.as_string(value)), version=version)
- return set_xml_value(field_elem, self.as_string(value), version=version)
Inherited members
+IntegerField
:
Ancestors
Subclasses
+
Class variables
+var is_list
Inherited members
@@ -4105,6 +3881,13 @@ class ExtendedPropertyField(Field):
+ is_complex = True
+
def __init__(self, *args, **kwargs):
self.value_cls = kwargs.pop('value_cls')
super().__init__(*args, **kwargs)
@@ -4055,7 +3833,7 @@
Inherited members
def clean(self, value, version=None):
if value is None:
if self.is_required:
- raise ValueError("'%s' is a required field" % self.name)
+ raise ValueError(f"{self.name!r} is a required field")
return self.default
if not isinstance(value, self.value_cls):
# Allow keeping ExtendedProperty field values as their simple Python type, but run clean() anyway
@@ -4088,11 +3866,9 @@ Inherited members
extended_property = create_element(self.value_cls.request_tag())
set_xml_value(extended_property, self.field_uri_xml(), version=version)
if isinstance(value, self.value_cls):
- set_xml_value(extended_property, value, version=version)
- else:
- # Allow keeping ExtendedProperty field values as their simple Python type
- set_xml_value(extended_property, self.value_cls(value), version=version)
- return extended_property
+ return set_xml_value(extended_property, value, version=version)
+ # Allow keeping ExtendedProperty field values as their simple Python type
+ return set_xml_value(extended_property, self.value_cls(value), version=version)
def __hash__(self):
return hash(self.name)Subclasses
+Class variables
+
+
var is_complex
Methods
+
@@ -4119,7 +3902,7 @@
Methods
-def clean(self, value, version=None):
if value is None:
if self.is_required:
- raise ValueError("'%s' is a required field" % self.name)
+ raise ValueError(f"{self.name!r} is a required field")
return self.default
if not isinstance(value, self.value_cls):
# Allow keeping ExtendedProperty field values as their simple Python type, but run clean() anyway
@@ -4152,44 +3935,16 @@
Methods
).to_xml(version=None)
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- extended_properties = elem.findall(self.value_cls.response_tag())
- for extended_property in extended_properties:
- if self.value_cls.is_property_instance(extended_property):
- return self.value_cls.from_xml(elem=extended_property, account=account)
- return self.default
-def to_xml(self, value, version)
-
-Expand source code
-
-
-def to_xml(self, value, version):
- extended_property = create_element(self.value_cls.request_tag())
- set_xml_value(extended_property, self.field_uri_xml(), version=version)
- if isinstance(value, self.value_cls):
- set_xml_value(extended_property, value, version=version)
- else:
- # Allow keeping ExtendedProperty field values as their simple Python type
- set_xml_value(extended_property, self.value_cls(value), version=version)
- return extended_property
Inherited members
+
class ExtendedPropertyListField
@@ -4216,6 +3971,15 @@
Class variables
Inherited members
+
+
ExtendedPropertyField
:
+
+
class Field
@@ -4243,7 +4007,7 @@
+ args_str = ', '.join(f'{f}={getattr(self, f)!r}' for f in (
+ 'name', 'value_cls', 'is_list', 'is_complex', 'default'))
+ return f'{self.__class__.__name__}({args_str})'Class variables
def __init__(self, name=None, is_required=False, is_required_after_save=False, is_read_only=False,
is_read_only_after_send=False, is_searchable=True, is_attribute=False, default=None,
supported_from=None, deprecated_from=None):
- self.name = name
+ self.name = name # Usually set by the EWSMeta metaclass
self.default = default # Default value if none is given
self.is_required = is_required
# Some fields cannot be deleted on update. Default to True if 'is_required' is set
@@ -4260,49 +4024,48 @@ Class variables
# The Exchange build when this field was introduced. When talking with versions prior to this version,
# we will ignore this field.
if supported_from is not None and not isinstance(supported_from, Build):
- raise ValueError("'supported_from' %r must be a Build instance" % supported_from)
+ raise InvalidTypeError('supported_from', supported_from, Build)
self.supported_from = supported_from
# The Exchange build when this field was deprecated. When talking with versions at or later than this version,
# we will ignore this field.
if deprecated_from is not None and not isinstance(deprecated_from, Build):
- raise ValueError("'deprecated_from' %r must be a Build instance" % deprecated_from)
+ raise InvalidTypeError('deprecated_from', deprecated_from, Build)
self.deprecated_from = deprecated_from
def clean(self, value, version=None):
if version and not self.supports_version(version):
- raise InvalidFieldForVersion("Field '%s' does not support EWS builds prior to %s (server has %s)" % (
- self.name, self.supported_from, version))
+ raise InvalidFieldForVersion(
+ f"Field {self.name!r} does not support EWS builds prior to {self.supported_from} (server has {version})"
+ )
if value is None:
if self.is_required and self.default is None:
- raise ValueError("'%s' is a required field with no default" % self.name)
+ raise ValueError(f"{self.name!r} is a required field with no default")
return self.default
if self.is_list:
if not is_iterable(value):
- raise ValueError("Field '%s' value %r must be a list" % (self.name, value))
+ raise TypeError(f"Field {self.name!r} value {value!r} must be of type {list}")
for v in value:
if not isinstance(v, self.value_cls):
- raise TypeError("Field '%s' value %r must be of type %s" % (self.name, v, self.value_cls))
+ raise TypeError(f"Field {self.name!r} value {v!r} must be of type {self.value_cls}")
if hasattr(v, 'clean'):
v.clean(version=version)
else:
if not isinstance(value, self.value_cls):
- raise TypeError("Field '%s' value %r must be of type %s" % (self.name, value, self.value_cls))
+ raise TypeError(f"Field {self.name!r} value {value!r} must be of type {self.value_cls}")
if hasattr(value, 'clean'):
value.clean(version=version)
return value
@abc.abstractmethod
def from_xml(self, elem, account):
- pass
+ """Read a value from the given element"""
@abc.abstractmethod
def to_xml(self, value, version):
- pass
+ """Convert this field to an XML element"""
def supports_version(self, version):
# 'version' is a Version instance, for convenience by callers
- if not isinstance(version, Version):
- raise ValueError("'version' %r must be a Version instance" % version)
if self.supported_from and version.build < self.supported_from:
return False
if self.deprecated_from and version.build >= self.deprecated_from:
@@ -4314,11 +4077,12 @@ Class variables
@abc.abstractmethod
def __hash__(self):
- pass
+ """Field instances must be hashable"""
def __repr__(self):
- return self.__class__.__name__ + '(%s)' % ', '.join('%s=%r' % (f, getattr(self, f)) for f in (
- 'name', 'value_cls', 'is_list', 'is_complex', 'default'))Subclasses
@@ -4354,23 +4118,24 @@
Methods
@@ -4380,14 +4145,14 @@ def clean(self, value, version=None):
if version and not self.supports_version(version):
- raise InvalidFieldForVersion("Field '%s' does not support EWS builds prior to %s (server has %s)" % (
- self.name, self.supported_from, version))
+ raise InvalidFieldForVersion(
+ f"Field {self.name!r} does not support EWS builds prior to {self.supported_from} (server has {version})"
+ )
if value is None:
if self.is_required and self.default is None:
- raise ValueError("'%s' is a required field with no default" % self.name)
+ raise ValueError(f"{self.name!r} is a required field with no default")
return self.default
if self.is_list:
if not is_iterable(value):
- raise ValueError("Field '%s' value %r must be a list" % (self.name, value))
+ raise TypeError(f"Field {self.name!r} value {value!r} must be of type {list}")
for v in value:
if not isinstance(v, self.value_cls):
- raise TypeError("Field '%s' value %r must be of type %s" % (self.name, v, self.value_cls))
+ raise TypeError(f"Field {self.name!r} value {v!r} must be of type {self.value_cls}")
if hasattr(v, 'clean'):
v.clean(version=version)
else:
if not isinstance(value, self.value_cls):
- raise TypeError("Field '%s' value %r must be of type %s" % (self.name, value, self.value_cls))
+ raise TypeError(f"Field {self.name!r} value {value!r} must be of type {self.value_cls}")
if hasattr(value, 'clean'):
value.clean(version=version)
return value
Methods
def from_xml(self, elem, account)
Expand source code
+ """Read a value from the given element"""@abc.abstractmethod
def from_xml(self, elem, account):
- pass
@@ -4401,8 +4166,6 @@
Methods
def supports_version(self, version):
# 'version' is a Version instance, for convenience by callers
- if not isinstance(version, Version):
- raise ValueError("'version' %r must be a Version instance" % version)
if self.supported_from and version.build < self.supported_from:
return False
if self.deprecated_from and version.build >= self.deprecated_from:
@@ -4414,14 +4177,14 @@
Methods
def to_xml(self, value, version)
Expand source code
+ """Convert this field to an XML element"""@abc.abstractmethod
def to_xml(self, value, version):
- pass
Methods
(field_path, reverse=False)
Expand source code
class FieldOrder:
"""Holds values needed to call server-side sorting on a single field path."""
-
def __init__(self, field_path, reverse=False):
- if not isinstance(field_path, FieldPath):
- raise ValueError("'field_path' %r must be a FieldPath instance" % field_path)
- if not isinstance(reverse, bool):
- raise ValueError("'reverse' %r must be a boolean" % reverse)
+ """
+
+ :param field_path: A FieldPath instance
+ :param reverse: A bool
+ """
self.field_path = field_path
self.reverse = reverse
@@ -4505,7 +4270,10 @@
Methods
Expand source code
@@ -4517,13 +4285,13 @@
Methods
"""
def __init__(self, field, label=None, subfield=None):
+ """
+
+ :param field: A FieldURIField or ExtendedPropertyField instance
+ :param label: a str
+ :param subfield: A SubField instance
+ """
# 'label' and 'subfield' are only used for IndexedField fields
- if not isinstance(field, (FieldURIField, ExtendedPropertyField)):
- raise ValueError("'field' %r must be an FieldURIField, of ExtendedPropertyField instance" % field)
- if label and not isinstance(label, str):
- raise ValueError("'label' %r must be a %s instance" % (label, str))
- if subfield and not isinstance(subfield, SubField):
- raise ValueError("'subfield' %r must be a SubField instance" % subfield)
self.field = field
self.label = label
self.subfield = subfield
@@ -4537,11 +4305,11 @@ Methods
# For indexed properties, get either the full property set, the property with matching label, or a particular
# subfield.
if self.label:
- for subitem in getattr(item, self.field.name):
- if subitem.label == self.label:
+ for sub_item in getattr(item, self.field.name):
+ if sub_item.label == self.label:
if self.subfield:
- return getattr(subitem, self.subfield.name)
- return subitem
+ return getattr(sub_item, self.subfield.name)
+ return sub_item
return None # No item with this label
return getattr(item, self.field.name)
@@ -4555,7 +4323,7 @@ Methods
def to_xml(self):
if isinstance(self.field, IndexedField):
if not self.label or not self.subfield:
- raise ValueError("Field path for indexed field '%s' is missing label and/or subfield" % self.field.name)
+ raise ValueError(f"Field path for indexed field {self.field.name!r} is missing label and/or subfield")
return self.subfield.field_uri_xml(field_uri=self.field.field_uri, label=self.label)
return self.field.field_uri_xml()
@@ -4577,8 +4345,8 @@ Methods
if self.label:
from .indexed_properties import SingleFieldIndexedElement
if issubclass(self.field.value_cls, SingleFieldIndexedElement) or not self.subfield:
- return '%s__%s' % (self.field.name, self.label)
- return '%s__%s__%s' % (self.field.name, self.label, self.subfield.name)
+ return f'{self.field.name}__{self.label}'
+ return f'{self.field.name}__{self.label}__{self.subfield.name}'
return self.field.name
def __eq__(self, other):
@@ -4625,8 +4393,8 @@ Instance variables
if self.label:
from .indexed_properties import SingleFieldIndexedElement
if issubclass(self.field.value_cls, SingleFieldIndexedElement) or not self.subfield:
- return '%s__%s' % (self.field.name, self.label)
- return '%s__%s__%s' % (self.field.name, self.label, self.subfield.name)
+ return f'{self.field.name}__{self.label}'
+ return f'{self.field.name}__{self.label}__{self.subfield.name}'
return self.field.name
Methods
# For indexed properties, get either the full property set, the property with matching label, or a particular
# subfield.
if self.label:
- for subitem in getattr(item, self.field.name):
- if subitem.label == self.label:
+ for sub_item in getattr(item, self.field.name):
+ if sub_item.label == self.label:
if self.subfield:
- return getattr(subitem, self.subfield.name)
- return subitem
+ return getattr(sub_item, self.subfield.name)
+ return sub_item
return None # No item with this label
return getattr(item, self.field.name)
@@ -4707,7 +4475,7 @@ Methods
@@ -4719,7 +4487,7 @@ def to_xml(self):
if isinstance(self.field, IndexedField):
if not self.label or not self.subfield:
- raise ValueError("Field path for indexed field '%s' is missing label and/or subfield" % self.field.name)
+ raise ValueError(f"Field path for indexed field {self.field.name!r} is missing label and/or subfield")
return self.subfield.field_uri_xml(field_uri=self.field.field_uri, label=self.label)
return self.field.field_uri_xml()
Methods
(*args, **kwargs)
Methods
Expand source code
@@ -4799,6 +4567,7 @@ class FieldURIField(Field):
- """A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or arrtibute. It
+ """A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or attribute. It
may additionally be a label for searching, filtering and limiting fields. In that case, the FieldURI format will be
'itemtype:FieldName'
"""
@@ -4768,18 +4536,18 @@
Methods
def field_uri_xml(self):
from .properties import FieldURI
if not self.field_uri:
- raise ValueError("'field_uri' value is missing")
+ raise ValueError(f"'field_uri' value is missing on field '{self.name}'")
return FieldURI(field_uri=self.field_uri).to_xml(version=None)
def request_tag(self):
if not self.field_uri_postfix:
- raise ValueError("'field_uri_postfix' value is missing")
- return 't:%s' % self.field_uri_postfix
+ raise ValueError(f"'field_uri_postfix' value is missing on field '{self.name}'")
+ return f't:{self.field_uri_postfix}'
def response_tag(self):
if not self.field_uri_postfix:
- raise ValueError("'field_uri_postfix' value is missing")
- return '{%s}%s' % (self.namespace, self.field_uri_postfix)
+ raise ValueError(f"'field_uri_postfix' value is missing on field '{self.name}'")
+ return f'{{{self.namespace}}}{self.field_uri_postfix}'
def __hash__(self):
return hash(self.field_uri)Subclasses
Methods
def field_uri_xml(self):
from .properties import FieldURI
if not self.field_uri:
- raise ValueError("'field_uri' value is missing")
+ raise ValueError(f"'field_uri' value is missing on field '{self.name}'")
return FieldURI(field_uri=self.field_uri).to_xml(version=None)
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- val = self._get_val_from_elem(elem)
- if val is not None:
- try:
- return xml_text_to_value(val, self.value_cls)
- except (ValueError, InvalidOperation):
- log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
- return None
- return self.default
def request_tag(self)
Methods
+ raise ValueError(f"'field_uri_postfix' value is missing on field '{self.name}'")
+ return f't:{self.field_uri_postfix}'
def request_tag(self):
if not self.field_uri_postfix:
- raise ValueError("'field_uri_postfix' value is missing")
- return 't:%s' % self.field_uri_postfix
@@ -4867,25 +4616,20 @@
+Methods
-
-
-def response_tag(self):
if not self.field_uri_postfix:
- raise ValueError("'field_uri_postfix' value is missing")
- return '{%s}%s' % (self.namespace, self.field_uri_postfix)
-def to_xml(self, value, version)
-
-Expand source code
-
-
+ raise ValueError(f"'field_uri_postfix' value is missing on field '{self.name}'")
+ return f'{{{self.namespace}}}{self.field_uri_postfix}'def to_xml(self, value, version):
- field_elem = create_element(self.request_tag())
- return set_xml_value(field_elem, value, version=version)
Inherited members
+
class FreeBusyStatusField
@@ -4916,6 +4660,8 @@
Inherited members
+ChoiceField
:
Class variables
-Methods
-
-
+
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- events = []
- for event in elem:
- # This may or may not be an event element. Could also be other child elements of Notification
- try:
- value_cls = self._event_types_map[event.tag]
- except KeyError:
- continue
- events.append(value_cls.from_xml(elem=event, account=account))
- return events or self.default
Inherited members
+
+
EWSElementField
:
+
+
class IdElementField
@@ -5022,6 +4753,15 @@
Ancestors
Inherited members
+
+
EWSElementField
:
+
+
class IdField
@@ -5058,6 +4798,8 @@
Inherited members
+class IndexedField(EWSElementField):
+
@@ -5125,23 +4867,19 @@ class IndexedField(EWSElementField, metaclass=abc.ABCMeta):
"""A base class for all indexed fields."""
PARENT_ELEMENT_NAME = None
@@ -5082,14 +4824,14 @@
Inherited members
from .indexed_properties import IndexedElement
value_cls = kwargs['value_cls']
if not issubclass(value_cls, IndexedElement):
- raise ValueError("'value_cls' %r must be a subclass of IndexedElement" % value_cls)
+ raise TypeError(f"'value_cls' {value_cls!r} must be a subclass of type {IndexedElement}")
super().__init__(*args, **kwargs)
def to_xml(self, value, version):
- return set_xml_value(create_element('t:%s' % self.PARENT_ELEMENT_NAME), value, version)
+ return set_xml_value(create_element(f't:{self.PARENT_ELEMENT_NAME}'), value, version=version)
def response_tag(self):
- return '{%s}%s' % (self.namespace, self.PARENT_ELEMENT_NAME)
+ return f'{{{self.namespace}}}{self.PARENT_ELEMENT_NAME}'
def __hash__(self):
return hash(self.field_uri)Methods
Expand source code
-
-
-def response_tag(self):
- return '{%s}%s' % (self.namespace, self.PARENT_ELEMENT_NAME)
-def to_xml(self, value, version)
-
-Expand source code
-
-
+ return f'{{{self.namespace}}}{self.PARENT_ELEMENT_NAME}'def to_xml(self, value, version):
- return set_xml_value(create_element('t:%s' % self.PARENT_ELEMENT_NAME), value, version)
Inherited members
+
+
EWSElementField
:
+
+
class IntegerField
@@ -5166,9 +4904,9 @@
Methods
def _clean_single_value(self, v):
if self.min is not None and v < self.min:
raise ValueError(
- "Value %r on field '%s' must be greater than %s" % (v, self.name, self.min))
+ f"Value {v!r} on field {self.name!r} must be greater than {self.min}")
if self.max is not None and v > self.max:
- raise ValueError("Value %r on field '%s' must be less than %s" % (v, self.name, self.max))
+ raise ValueError(f"Value {v!r} on field {self.name!r} must be less than {self.max}")
def clean(self, value, version=None):
value = super().clean(value, version=version)
@@ -5238,6 +4976,15 @@ Methods
+Inherited members
+
+
FieldURIField
:
+
+
class InvalidChoiceForVersion
@@ -5284,13 +5031,13 @@
Ancestors
(*args, **kwargs)
Expand source code
+ """Used when a field is not supported on the given Exchange version."""class InvalidFieldForVersion(ValueError):
- """Used when a field is not supported on the given Exchnage version."""
Ancestors
@@ -5304,7 +5051,7 @@
Ancestors
(*args, **kwargs)
Instance variables
Methods
-
-
+
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- from .items import ITEM_CLASSES
- for item_cls in ITEM_CLASSES:
- item_elem = elem.find(item_cls.response_tag())
- if item_elem is not None:
- return item_cls.from_xml(elem=item_elem, account=account)
- return None
-def to_xml(self, value, version)
-
-Expand source code
-
-
-def to_xml(self, value, version):
- # We don't want to wrap in an Item element
- return value.to_xml(version=version)
Inherited members
+
+
FieldURIField
:
+
+
class LabelField
@@ -5414,26 +5135,12 @@
Ancestors
Methods
-
-
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- return elem.get(self.field_uri)
Inherited members
ChoiceField
:
Inherited members
(*args, **kwargs)
Expand source code
@@ -5462,6 +5169,15 @@
Ancestors
Inherited members
+
+
BaseEmailField
:
+
+
class MailboxListField
@@ -5509,6 +5225,15 @@
Methods
+Inherited members
+
+
EWSElementListField
:
+
+
class MemberListField
@@ -5562,6 +5287,15 @@
Methods
+Inherited members
+
+
EWSElementListField
:
+
+
class MessageField
@@ -5582,14 +5316,14 @@
@@ -5606,48 +5340,12 @@ Methods
reply = elem.find(self.response_tag())
if reply is None:
return None
- message = reply.find('{%s}%s' % (TNS, self.INNER_ELEMENT_NAME))
+ message = reply.find(f'{{{TNS}}}{self.INNER_ELEMENT_NAME}')
if message is None:
return None
return message.text
def to_xml(self, value, version):
field_elem = create_element(self.request_tag())
- message = create_element('t:%s' % self.INNER_ELEMENT_NAME)
+ message = create_element(f't:{self.INNER_ELEMENT_NAME}')
message.text = value
return set_xml_value(field_elem, message, version=version)Class variables
-Methods
-
-
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- reply = elem.find(self.response_tag())
- if reply is None:
- return None
- message = reply.find('{%s}%s' % (TNS, self.INNER_ELEMENT_NAME))
- if message is None:
- return None
- return message.text
-def to_xml(self, value, version)
-
-Expand source code
-
-
-def to_xml(self, value, version):
- field_elem = create_element(self.request_tag())
- message = create_element('t:%s' % self.INNER_ELEMENT_NAME)
- message.text = value
- return set_xml_value(field_elem, message, version=version)
Inherited members
+Inherited members
+
+
EWSElementListField
:
+
+
class MimeContentField
@@ -5705,6 +5412,8 @@
+ return f'{{{self.namespace}}}{self.field_uri}'Inherited members
Base64Field
:
Inherited members
def field_uri_xml(self, field_uri, label):
from .properties import IndexedFieldURI
- return IndexedFieldURI(field_uri='%s:%s' % (field_uri, self.field_uri), field_index=label).to_xml(version=None)
+ return IndexedFieldURI(field_uri=f'{field_uri}:{self.field_uri}', field_index=label).to_xml(version=None)
def request_tag(self):
- return 't:%s' % self.field_uri
+ return f't:{self.field_uri}'
def response_tag(self):
- return '{%s}%s' % (self.namespace, self.field_uri)Ancestors
@@ -5770,24 +5479,7 @@
Methods
-
-
-def field_uri_xml(self, field_uri, label):
from .properties import IndexedFieldURI
- return IndexedFieldURI(field_uri='%s:%s' % (field_uri, self.field_uri), field_index=label).to_xml(version=None)
-def from_xml(self, elem, account)
-
-Expand source code
-
-
+ return IndexedFieldURI(field_uri=f'{field_uri}:{self.field_uri}', field_index=label).to_xml(version=None)def from_xml(self, elem, account):
- field_elem = elem.find(self.response_tag())
- val = None if field_elem is None else field_elem.text or None
- if val is not None:
- return val
- return self.default
@@ -5800,7 +5492,7 @@
Methods
Expand source code
+ return f't:{self.field_uri}'def request_tag(self):
- return 't:%s' % self.field_uri
@@ -5813,21 +5505,7 @@
@@ -5835,6 +5513,8 @@ Methods
Expand source code
-
-
-def response_tag(self):
- return '{%s}%s' % (self.namespace, self.field_uri)
-def to_xml(self, value, version)
-
-Expand source code
-
-
+ return f'{{{self.namespace}}}{self.field_uri}'def to_xml(self, value, version):
- field_elem = create_element(self.request_tag())
- return set_xml_value(field_elem, value, version=version)
Inherited members
+SubField
:
Class variables
+Inherited members
+
+
EWSElementField
:
+
+
class OccurrenceListField
@@ -5898,6 +5587,15 @@
Class variables
+Inherited members
+
+
OccurrenceField
:
+
+
class OnOffField
@@ -5922,6 +5620,8 @@
Inherited members
+BooleanField
:
Class variables
+Inherited members
+
+
EWSElementField
:
+
+
class PersonaPhoneNumberField
@@ -5990,6 +5699,15 @@
Class variables
+Inherited members
+
+
EWSElementField
:
+
+
class PhoneNumberAttributedValueField
@@ -6014,6 +5732,15 @@
Ancestors
Inherited members
+
+
EWSElementListField
:
+
+
class PhoneNumberField
@@ -6027,6 +5754,7 @@
Ancestors
class PhoneNumberField(IndexedField):
is_list = True
+ is_complex = True
PARENT_ELEMENT_NAME = 'PhoneNumbers'
@@ -6048,11 +5776,24 @@
Class variables
var is_complex
var is_list
Inherited members
+
+
IndexedField
:
+
+
class PhysicalAddressField
@@ -6066,6 +5807,7 @@
Class variables
class PhysicalAddressField(IndexedField):
is_list = True
+ is_complex = True
PARENT_ELEMENT_NAME = 'PhysicalAddresses'
@@ -6087,11 +5829,24 @@
Class variables
var is_complex
var is_list
Inherited members
+
+
IndexedField
:
+
+
class PostalAddressAttributedValueField
@@ -6116,6 +5871,15 @@
Ancestors
Inherited members
+
+
EWSElementListField
:
+
+
class ProtocolListField
@@ -6144,29 +5908,22 @@
Ancestors
Methods
-
-
+
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- return [self.value_cls.from_xml(elem=e, account=account) for e in elem.findall(self.value_cls.response_tag())]
Inherited members
+
+
EWSElementListField
:
+
+
class RecipientAddressField
(*args, **kwargs)
Expand source code
@@ -6184,6 +5941,15 @@
Ancestors
Inherited members
+
+
BaseEmailField
:
+
+
class RecurrenceField
@@ -6219,22 +5985,15 @@
Class variables
-Methods
-
-
+
-def to_xml(self, value, version)
-
-Expand source code
-
-
-def to_xml(self, value, version):
- return value.to_xml(version=version)
Inherited members
+
+
EWSElementField
:
+
+
class ReferenceItemIdField
@@ -6270,22 +6029,15 @@
Class variables
-Methods
-
-
+
-def to_xml(self, value, version)
-
-Expand source code
-
-
-def to_xml(self, value, version):
- return value.to_xml(version=version)
Inherited members
+
+
EWSElementField
:
+
+
class RoutingTypeField
@@ -6315,6 +6067,8 @@
Inherited members
+ChoiceField
:
Ancestors
Inherited members
+
+
EWSElementListField
:
+
+
class SubField
@@ -6374,7 +6137,7 @@
Ancestors
def clean(self, value, version=None):
value = super().clean(value, version=version)
if self.is_required and not value:
- raise ValueError('Value for subfield %r must be non-empty' % self.name)
+ raise ValueError(f'Value for subfield {self.name!r} must be non-empty')
return value
def __hash__(self):
@@ -6440,37 +6203,20 @@ Methods
-
-
-def clean(self, value, version=None):
value = super().clean(value, version=version)
if self.is_required and not value:
- raise ValueError('Value for subfield %r must be non-empty' % self.name)
- return value
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- return elem.text
-def to_xml(self, value, version)
-
-Expand source code
-
-def to_xml(self, value, version):
+ raise ValueError(f'Value for subfield {self.name!r} must be non-empty')
return value
Inherited members
+
class TaskRecurrenceField
@@ -6506,22 +6252,15 @@
Class variables
-Methods
-
-
+
-def to_xml(self, value, version)
-
-Expand source code
-
-
-def to_xml(self, value, version):
- return value.to_xml(version=version)
Inherited members
+
+
EWSElementField
:
+
+
class TextField
@@ -6571,6 +6310,15 @@
Class variables
errors defaults to 'strict'.Inherited members
+
+
FieldURIField
:
+
+
class TextListField
@@ -6587,11 +6335,27 @@
+ return get_xml_attrs(iter_elem, self.list_elem_response_tag())
+ return self.default
+
+ def to_xml(self, value, version):
+ field_elem = create_element(self.request_tag())
+ for v in value:
+ field_elem.append(set_xml_value(create_element(self.list_elem_request_tag()), v, version=version))
+ return field_elemClass variables
is_list = True
+ def __init__(self, *args, **kwargs):
+ self.list_elem_name = kwargs.pop('list_elem_name', 'String')
+ super().__init__(*args, **kwargs)
+
+ def list_elem_request_tag(self):
+ return f't:{self.list_elem_name}'
+
+ def list_elem_response_tag(self):
+ return f'{{{self.namespace}}}{self.list_elem_name}'
+
def from_xml(self, elem, account):
iter_elem = elem.find(self.response_tag())
if iter_elem is not None:
- return get_xml_attrs(iter_elem, '{%s}String' % TNS)
- return self.defaultAncestors
@@ -6599,6 +6363,10 @@
+Ancestors
Subclasses
+
Class variables
var is_list
Class variables
Methods
-
@@ -6629,11 +6407,84 @@
-def from_xml(self, elem, account)
+
+def list_elem_request_tag(self)
+
+Expand source code
+
+
+def list_elem_request_tag(self):
+ return f't:{self.list_elem_name}'
+def list_elem_response_tag(self)
Methods
+def from_xml(self, elem, account):
- iter_elem = elem.find(self.response_tag())
- if iter_elem is not None:
- return get_xml_attrs(iter_elem, '{%s}String' % TNS)
- return self.default
def list_elem_response_tag(self):
+ return f'{{{self.namespace}}}{self.list_elem_name}'
Inherited members
+
+class TimeDeltaField
+(*args, **kwargs)
+
+Expand source code
+
+
+class TimeDeltaField(FieldURIField):
+ """A field that handles timedelta values."""
+
+ value_cls = datetime.timedelta
+
+ def __init__(self, *args, **kwargs):
+ self.min = kwargs.pop('min', datetime.timedelta(0))
+ self.max = kwargs.pop('max', datetime.timedelta(days=1))
+ super().__init__(*args, **kwargs)
+
+ def clean(self, value, version=None):
+ if self.min is not None and value < self.min:
+ raise ValueError(
+ f"Value {value!r} on field {self.name!r} must be greater than {self.min}")
+ if self.max is not None and value > self.max:
+ raise ValueError(f"Value {value!r} on field {self.name!r} must be less than {self.max}")
+ return super().clean(value, version=version)
Ancestors
+
+
+Class variables
+
+
+var value_cls
Methods
+
+
+
+def clean(self, value, version=None)
+
+Expand source code
+
+
+def clean(self, value, version=None):
+ if self.min is not None and value < self.min:
+ raise ValueError(
+ f"Value {value!r} on field {self.name!r} must be greater than {self.min}")
+ if self.max is not None and value > self.max:
+ raise ValueError(f"Value {value!r} on field {self.name!r} must be less than {self.max}")
+ return super().clean(value, version=version)
Inherited members
+
+
+FieldURIField
:
+
+
class TimeField
(*args, **kwargs)
@@ -6676,32 +6527,15 @@
Class variables
a tzinfo subclass. The remaining arguments may be ints.Methods
-
-
+
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- val = self._get_val_from_elem(elem)
- if val is not None:
- try:
- if ':' in val:
- # Assume a string of the form HH:MM:SS
- return datetime.datetime.strptime(val, '%H:%M:%S').time()
- # Assume an integer in minutes since midnight
- return (datetime.datetime(2000, 1, 1) + datetime.timedelta(minutes=int(val))).time()
- except ValueError:
- pass
- return self.default
Inherited members
+
+
FieldURIField
:
+
+
class TimeZoneField
@@ -6740,7 +6574,7 @@
@@ -6776,48 +6610,57 @@ Methods
return self.default
def to_xml(self, value, version):
- attrs = OrderedDict([('Id', value.ms_id)])
+ attrs = dict(Id=value.ms_id)
if value.ms_name:
attrs['Name'] = value.ms_name
return create_element(self.request_tag(), attrs=attrs)Methods
return super().clean(value=value, version=version)
-
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- field_elem = elem.find(self.response_tag())
- if field_elem is not None:
- ms_id = field_elem.get('Id')
- ms_name = field_elem.get('Name')
- try:
- return self.value_cls.from_ms_id(ms_id or ms_name)
- except UnknownTimeZone:
- log.warning(
- "Cannot convert value '%s' on field '%s' to type %s (unknown timezone ID)",
- (ms_id or ms_name), self.name, self.value_cls
- )
- return None
- return self.default
Inherited members
+
+
FieldURIField
:
+
+
-def to_xml(self, value, version)
+
+class TransitionListField
+(*args, **kwargs)
Expand source code
-
+def to_xml(self, value, version):
- attrs = OrderedDict([('Id', value.ms_id)])
- if value.ms_name:
- attrs['Name'] = value.ms_name
- return create_element(self.request_tag(), attrs=attrs)
class TransitionListField(EWSElementListField):
+ def __init__(self, *args, **kwargs):
+ from .properties import BaseTransition
+ kwargs['value_cls'] = BaseTransition
+ super().__init__(*args, **kwargs)
+
+ def from_xml(self, elem, account):
+ iter_elem = elem.find(self.response_tag()) if self.field_uri else elem
+ if iter_elem is not None:
+ return [
+ self.value_cls.transition_model_from_tag(e.tag).from_xml(elem=e, account=account) for e in iter_elem
+ ]
+ return self.default
Ancestors
+
+Inherited members
+
+
EWSElementListField
:
+
+
class TypeValueField
@@ -6865,9 +6708,9 @@
@@ -6986,62 +6829,21 @@ Methods
first = next(iter(value))
except StopIteration:
first = None
- value_type = '%sArray' % cls.TYPES_MAP_REVERSED[type(first)]
+ value_type = f'{cls.TYPES_MAP_REVERSED[type(first)]}Array'
if value_type not in cls.TYPES_MAP:
- raise ValueError('%r is not a supported type' % value)
+ raise ValueError(f'{value!r} is not a supported type')
return value_type
return cls.TYPES_MAP_REVERSED[type(value)]
@@ -6878,7 +6721,7 @@ Methods
def clean(self, value, version=None):
if value is None:
if self.is_required and self.default is None:
- raise ValueError("'%s' is a required field with no default" % self.name)
+ raise ValueError(f"{self.name!r} is a required field with no default")
return self.default
return value
@@ -6886,8 +6729,8 @@ Methods
field_elem = elem.find(self.response_tag())
if field_elem is None:
return self.default
- value_type_str = get_xml_attr(field_elem, '{%s}Type' % TNS)
- value = get_xml_attr(field_elem, '{%s}Value' % TNS)
+ value_type_str = get_xml_attr(field_elem, f'{{{TNS}}}Type')
+ value = get_xml_attr(field_elem, f'{{{TNS}}}Value')
if value_type_str == 'Byte':
try:
# The value is an unsigned integer in the range 0 -> 255. Convert it to a single byte
@@ -6950,9 +6793,9 @@ Static methods
first = next(iter(value))
except StopIteration:
first = None
- value_type = '%sArray' % cls.TYPES_MAP_REVERSED[type(first)]
+ value_type = f'{cls.TYPES_MAP_REVERSED[type(first)]}Array'
if value_type not in cls.TYPES_MAP:
- raise ValueError('%r is not a supported type' % value)
+ raise ValueError(f'{value!r} is not a supported type')
return value_type
return cls.TYPES_MAP_REVERSED[type(value)]Methods
-def clean(self, value, version=None):
if value is None:
if self.is_required and self.default is None:
- raise ValueError("'%s' is a required field with no default" % self.name)
+ raise ValueError(f"{self.name!r} is a required field with no default")
return self.default
return value
-def from_xml(self, elem, account)
-
-Expand source code
-
-
-def from_xml(self, elem, account):
- field_elem = elem.find(self.response_tag())
- if field_elem is None:
- return self.default
- value_type_str = get_xml_attr(field_elem, '{%s}Type' % TNS)
- value = get_xml_attr(field_elem, '{%s}Value' % TNS)
- if value_type_str == 'Byte':
- try:
- # The value is an unsigned integer in the range 0 -> 255. Convert it to a single byte
- return xml_text_to_value(value, int).to_bytes(1, 'little', signed=False)
- except OverflowError as e:
- log.warning('Invalid byte value %r (%e)', value, e)
- return None
- value_type = self.TYPES_MAP[value_type_str]
- if self. is_array_type(value_type_str):
- return tuple(xml_text_to_value(value=v, value_type=value_type) for v in value.split(' '))
- return xml_text_to_value(value=value, value_type=value_type)
-def to_xml(self, value, version)
-
-Expand source code
-
-
-def to_xml(self, value, version):
- value_type_str = self.get_type(value)
- if value_type_str == 'Byte':
- # A single byte is encoded to an unsigned integer in the range 0 -> 255
- value = int.from_bytes(value, byteorder='little', signed=False)
- elif is_iterable(value):
- value = ' '.join(value_to_xml_text(v) for v in value)
- field_elem = create_element(self.request_tag())
- field_elem.append(set_xml_value(create_element('t:Type'), value_type_str, version=version))
- field_elem.append(set_xml_value(create_element('t:Value'), value, version=version))
- return field_elem
Inherited members
+
+
FieldURIField
:
+
+
class URIField
@@ -7049,14 +6851,14 @@
Methods
class URIField(TextField):
"""Helper to mark strings that must conform to xsd:anyURI.
- If we want an URI validator, see http://stackoverflow.com/questions/14466585/is-this-regex-correct-for-xsdanyuri
+ If we want a URI validator, see https://stackoverflow.com/questions/14466585/is-this-regex-correct-for-xsdanyuri
"""
TextField
:
Like CharField, but for lists of strings.
Like TextListField, but for string values with a limited length.
class UnknownEntriesField(CharListField):
def list_elem_tag(self):
- return '{%s}UnknownEntry' % self.namespace
+ return f'{{{self.namespace}}}UnknownEntry'
def list_elem_tag(self):
- return '{%s}UnknownEntry' % self.namespace
+ return f'{{{self.namespace}}}UnknownEntry'
+class WeekdaysField
+(*args, **kwargs)
+
Like EnumListField, allow a single value instead of a 1-element list.
class WeekdaysField(EnumListField):
+ """Like EnumListField, allow a single value instead of a 1-element list."""
+
+ def clean(self, value, version=None):
+ if isinstance(value, (int, str)):
+ value = [value]
+ return super().clean(value, version)
+
+def clean(self, value, version=None)
+
def clean(self, value, version=None):
+ if isinstance(value, (int, str)):
+ value = [value]
+ return super().clean(value, version)
+EnumListField
:
+
+AppointmentStateField
AssociatedCalendarItemIdField
AttachmentField
from_xml
AttendeesField
BaseEmailField
BodyField
BuildField
from_xml
CharField
CharListField
DateOrDateTimeField
DateTimeBackedDateField
DateTimeField
DictionaryField
EWSElementField
PARENT_ELEMENT_NAME
clean
+is_complex
is_list
EmailSubField
from_xml
EnumAsIntField
EnumField
clean
field_uri_xml
-from_xml
-to_xml
+is_complex
FieldURIField
GenericEventListField
PARENT_ELEMENT_NAME
response_tag
-to_xml
@@ -7448,16 +7275,11 @@ ItemField
LabelField
-
-from_xml
-
MailboxField
@@ -7478,8 +7300,6 @@ MessageField
@@ -7492,10 +7312,8 @@ NamedSubField
@@ -7532,6 +7350,7 @@ PhoneNumberField
@@ -7539,6 +7358,7 @@ PhysicalAddressField
@@ -7547,9 +7367,6 @@ ProtocolListField
-
-from_xml
-
RecipientAddressField
@@ -7558,14 +7375,12 @@ RecurrenceField
ReferenceItemIdField
@@ -7576,12 +7391,10 @@ SubField
-
@@ -7589,7 +7402,6 @@ TaskRecurrenceField
@@ -7602,14 +7414,21 @@ TextListField
+
+
+TimeDeltaField
+
TimeField
@@ -7617,21 +7436,20 @@ TimeZoneField
+TransitionListField
+
+
TypeValueField
-
@@ -7643,6 +7461,12 @@ list_elem_tag
+
+WeekdaysField
+
+clean
+
+
diff --git a/docs/exchangelib/folders/base.html b/docs/exchangelib/folders/base.html
index ce911729..fc009503 100644
--- a/docs/exchangelib/folders/base.html
+++ b/docs/exchangelib/folders/base.html
@@ -31,28 +31,21 @@ Module exchangelib.folders.base
from fnmatch import fnmatch
from operator import attrgetter
-from .collections import FolderCollection, SyncCompleted
-from .queryset import SingleFolderQuerySet, SHALLOW as SHALLOW_FOLDERS, DEEP as DEEP_FOLDERS
+from .collections import FolderCollection, SyncCompleted, PullSubscription, PushSubscription, StreamingSubscription
+from .queryset import SingleFolderQuerySet, MISSING_FOLDER_ERRORS, SHALLOW as SHALLOW_FOLDERS, DEEP as DEEP_FOLDERS
from ..errors import ErrorAccessDenied, ErrorFolderNotFound, ErrorCannotEmptyFolder, ErrorCannotDeleteObject, \
- ErrorDeleteDistinguishedFolder, ErrorNoPublicFolderReplicaAvailable, ErrorItemNotFound
+ ErrorDeleteDistinguishedFolder, ErrorItemNotFound, InvalidTypeError
from ..fields import IntegerField, CharField, FieldPath, EffectiveRightsField, PermissionSetField, EWSElementField, \
Field, IdElementField, InvalidField
-from ..items import CalendarItem, RegisterMixIn, ITEM_CLASSES, DELETE_TYPE_CHOICES, HARD_DELETE, \
- SHALLOW as SHALLOW_ITEMS
+from ..items import CalendarItem, RegisterMixIn, ITEM_CLASSES, HARD_DELETE, SHALLOW as SHALLOW_ITEMS
from ..properties import Mailbox, FolderId, ParentFolderId, DistinguishedFolderId, UserConfiguration, \
UserConfigurationName, UserConfigurationNameMNS, EWSMeta
from ..queryset import SearchableMixIn, DoesNotExist
-from ..services import CreateFolder, UpdateFolder, DeleteFolder, EmptyFolder, GetUserConfiguration, \
- CreateUserConfiguration, UpdateUserConfiguration, DeleteUserConfiguration, SubscribeToPush, SubscribeToPull, \
- Unsubscribe, GetEvents, GetStreamingEvents, MoveFolder
-from ..services.get_user_configuration import ALL
from ..util import TNS, require_id, is_iterable
-from ..version import Version, EXCHANGE_2007_SP1, EXCHANGE_2010
+from ..version import EXCHANGE_2007_SP1, EXCHANGE_2010
log = logging.getLogger(__name__)
-MISSING_FOLDER_ERRORS = (ErrorFolderNotFound, ErrorItemNotFound, ErrorNoPublicFolderReplicaAvailable)
-
class BaseFolder(RegisterMixIn, SearchableMixIn, metaclass=EWSMeta):
"""Base class for all classes that implement a folder."""
@@ -99,17 +92,17 @@ Module exchangelib.folders.base
@property
@abc.abstractmethod
def account(self):
- pass
+ """Return the account this folder belongs to"""
@property
@abc.abstractmethod
def root(self):
- pass
+ """Return the root folder this folder belongs to"""
@property
@abc.abstractmethod
def parent(self):
- pass
+ """Return the parent folder of this folder"""
@property
def is_deletable(self):
@@ -138,7 +131,7 @@ Module exchangelib.folders.base
@property
def absolute(self):
- return ''.join('/%s' % p.name for p in self.parts)
+ return ''.join(f'/{p.name}' for p in self.parts)
def _walk(self):
for c in self.children:
@@ -192,29 +185,27 @@ Module exchangelib.folders.base
├── exchangelib issues
└── Mom
"""
- tree = '%s\n' % self.name
+ tree = f'{self.name}\n'
children = list(self.children)
for i, c in enumerate(sorted(children, key=attrgetter('name')), start=1):
nodes = c.tree().split('\n')
for j, node in enumerate(nodes, start=1):
if i != len(children) and j == 1:
# Not the last child, but the first node, which is the name of the child
- tree += '├── %s\n' % node
+ tree += f'├── {node}\n'
elif i != len(children) and j > 1:
# Not the last child, and not name of child
- tree += '│ %s\n' % node
+ tree += f'│ {node}\n'
elif i == len(children) and j == 1:
# Not the last child, but the first node, which is the name of the child
- tree += '└── %s\n' % node
+ tree += f'└── {node}\n'
else: # Last child, and not name of child
- tree += ' %s\n' % node
+ tree += f' {node}\n'
return tree.strip()
@classmethod
def supports_version(cls, version):
# 'version' is a Version instance, for convenience by callers
- if not isinstance(version, Version):
- raise ValueError("'version' %r must be a Version instance" % version)
if not cls.supported_from:
return True
return version.build >= cls.supported_from
@@ -251,31 +242,18 @@ Module exchangelib.folders.base
try:
return cls.ITEM_MODEL_MAP[tag]
except KeyError:
- raise ValueError('Item type %s was unexpected in a %s folder' % (tag, cls.__name__))
+ raise ValueError(f'Item type {tag} was unexpected in a {cls.__name__} folder')
@classmethod
def allowed_item_fields(cls, version):
# Return non-ID fields of all item classes allowed in this folder type
fields = set()
for item_model in cls.supported_item_models:
- fields.update(
- set(item_model.supported_fields(version=version))
- )
+ fields.update(set(item_model.supported_fields(version=version)))
return fields
def validate_item_field(self, field, version):
- # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
- # for the item types supported by this folder.
-
- # For each field, check if the field is valid for any of the item models supported by this folder
- for item_model in self.supported_item_models:
- try:
- item_model.validate_field(field=field, version=version)
- break
- except InvalidField:
- continue
- else:
- raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))
+ FolderCollection(account=self.account, folders=[self]).validate_item_field(field=field, version=version)
def normalize_fields(self, fields):
# Takes a list of fieldnames, Field or FieldPath objects pointing to item fields. Turns them into FieldPath
@@ -290,8 +268,6 @@ Module exchangelib.folders.base
elif isinstance(field_path, Field):
field_path = FieldPath(field=field_path)
fields[i] = field_path
- if not isinstance(field_path, FieldPath):
- raise ValueError("Field %r must be a string or FieldPath instance" % field_path)
if field_path.field.name == 'start':
has_start = True
elif field_path.field.name == 'end':
@@ -317,7 +293,7 @@ Module exchangelib.folders.base
return item_model.get_field_by_fieldname(fieldname)
except InvalidField:
pass
- raise InvalidField("%r is not a valid field name on %s" % (fieldname, cls.supported_item_models))
+ raise InvalidField(f"{fieldname!r} is not a valid field name on {cls.supported_item_models}")
def get(self, *args, **kwargs):
return FolderCollection(account=self.account, folders=[self]).get(*args, **kwargs)
@@ -342,6 +318,7 @@ Module exchangelib.folders.base
return self.account.bulk_create(folder=self, items=items, *args, **kwargs)
def save(self, update_fields=None):
+ from ..services import CreateFolder, UpdateFolder
if self.id is None:
# New folder
if update_fields:
@@ -375,6 +352,7 @@ Module exchangelib.folders.base
return self
def move(self, to_folder):
+ from ..services import MoveFolder
res = MoveFolder(account=self.account).get(folders=[self], to_folder=to_folder)
folder_id, changekey = res.id, res.changekey
if self.id != folder_id:
@@ -385,15 +363,13 @@ Module exchangelib.folders.base
self.root.update_folder(self) # Update the folder in the cache
def delete(self, delete_type=HARD_DELETE):
- if delete_type not in DELETE_TYPE_CHOICES:
- raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
+ from ..services import DeleteFolder
DeleteFolder(account=self.account).get(folders=[self], delete_type=delete_type)
self.root.remove_folder(self) # Remove the updated folder from the cache
self._id = None
def empty(self, delete_type=HARD_DELETE, delete_sub_folders=False):
- if delete_type not in DELETE_TYPE_CHOICES:
- raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
+ from ..services import EmptyFolder
EmptyFolder(account=self.account).get(
folders=[self], delete_type=delete_type, delete_sub_folders=delete_sub_folders
)
@@ -401,14 +377,14 @@ Module exchangelib.folders.base
# We don't know exactly what was deleted, so invalidate the entire folder cache to be safe
self.root.clear_cache()
- def wipe(self, page_size=None, _seen=None, _level=0):
+ def wipe(self, page_size=None, chunk_size=None, _seen=None, _level=0):
# Recursively deletes all items in this folder, and all subfolders and their content. Attempts to protect
# distinguished folders from being deleted. Use with caution!
_seen = _seen or set()
if self.id in _seen:
- raise RecursionError('We already tried to wipe %s' % self)
+ raise RecursionError(f'We already tried to wipe {self}')
if _level > 16:
- raise RecursionError('Max recursion level reached: %s' % _level)
+ raise RecursionError(f'Max recursion level reached: {_level}')
_seen.add(self.id)
log.warning('Wiping %s', self)
has_distinguished_subfolders = any(f.is_distinguished for f in self.children)
@@ -424,13 +400,18 @@ Module exchangelib.folders.base
self.empty(delete_sub_folders=False)
except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
log.warning('Not allowed to empty %s. Trying to delete items instead', self)
+ kwargs = {}
+ if page_size is not None:
+ kwargs['page_size'] = page_size
+ if chunk_size is not None:
+ kwargs['chunk_size'] = chunk_size
try:
- self.all().delete(**dict(page_size=page_size) if page_size else {})
+ self.all().delete(**kwargs)
except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound):
log.warning('Not allowed to delete items in %s', self)
_level += 1
for f in self.children:
- f.wipe(page_size=page_size, _seen=_seen, _level=_level)
+ f.wipe(page_size=page_size, chunk_size=chunk_size, _seen=_seen, _level=_level)
# Remove non-distinguished children that are empty and have no subfolders
if f.is_deletable and not f.children:
log.warning('Deleting folder %s', f)
@@ -457,7 +438,7 @@ Module exchangelib.folders.base
kwargs['name'] = cls.DISTINGUISHED_FOLDER_ID
return kwargs
- def to_folder_id(self):
+ def to_id(self):
if self.is_distinguished:
# Don't add the changekey here. When modifying folder content, we usually don't care if others have changed
# the folder content since we fetched the changekey.
@@ -471,29 +452,19 @@ Module exchangelib.folders.base
return FolderId(id=self.id, changekey=self.changekey)
raise ValueError('Must be a distinguished folder or have an ID')
- def to_xml(self, version):
- try:
- return self.to_folder_id().to_xml(version=version)
- except ValueError:
- return super().to_xml(version=version)
-
- def to_id_xml(self, version):
- # Folder(name='Foo') is a perfectly valid ID to e.g. create a folder
- return self.to_xml(version=version)
-
@classmethod
def resolve(cls, account, folder):
# Resolve a single folder
folders = list(FolderCollection(account=account, folders=[folder]).resolve())
if not folders:
- raise ErrorFolderNotFound('Could not find folder %r' % folder)
+ raise ErrorFolderNotFound(f'Could not find folder {folder!r}')
if len(folders) != 1:
- raise ValueError('Expected result length 1, but got %s' % folders)
+ raise ValueError(f'Expected result length 1, but got {folders}')
f = folders[0]
if isinstance(f, Exception):
raise f
if f.__class__ != cls:
- raise ValueError("Expected folder %r to be a %s instance" % (f, cls))
+ raise ValueError(f"Expected folder {f!r} to be a {cls} instance")
return f
@require_id
@@ -507,7 +478,11 @@ Module exchangelib.folders.base
return self
@require_id
- def get_user_configuration(self, name, properties=ALL):
+ def get_user_configuration(self, name, properties=None):
+ from ..services import GetUserConfiguration
+ from ..services.get_user_configuration import ALL
+ if properties is None:
+ properties = ALL
return GetUserConfiguration(account=self.account).get(
user_configuration_name=UserConfigurationNameMNS(name=name, folder=self),
properties=properties,
@@ -515,6 +490,7 @@ Module exchangelib.folders.base
@require_id
def create_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
+ from ..services import CreateUserConfiguration
user_configuration = UserConfiguration(
user_configuration_name=UserConfigurationName(name=name, folder=self),
dictionary=dictionary,
@@ -525,6 +501,7 @@ Module exchangelib.folders.base
@require_id
def update_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
+ from ..services import UpdateUserConfiguration
user_configuration = UserConfiguration(
user_configuration_name=UserConfigurationName(name=name, folder=self),
dictionary=dictionary,
@@ -535,12 +512,13 @@ Module exchangelib.folders.base
@require_id
def delete_user_configuration(self, name):
+ from ..services import DeleteUserConfiguration
return DeleteUserConfiguration(account=self.account).get(
user_configuration_name=UserConfigurationNameMNS(name=name, folder=self)
)
@require_id
- def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
+ def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60):
"""Create a pull subscription.
:param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPull.EVENT_TYPES
@@ -549,19 +527,15 @@ Module exchangelib.folders.base
GetEvents request for this subscription.
:return: The subscription ID and a watermark
"""
- s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_pull(
+ from ..services import SubscribeToPull
+ if event_types is None:
+ event_types = SubscribeToPull.EVENT_TYPES
+ return FolderCollection(account=self.account, folders=[self]).subscribe_to_pull(
event_types=event_types, watermark=watermark, timeout=timeout,
- ))
- if len(s_ids) != 1:
- raise ValueError('Expected result length 1, but got %s' % s_ids)
- s_id = s_ids[0]
- if isinstance(s_id, Exception):
- raise s_id
- return s_id
+ )
@require_id
- def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
- status_frequency=1):
+ def subscribe_to_push(self, callback_url, event_types=None, watermark=None, status_frequency=1):
"""Create a push subscription.
:param callback_url: A client-defined URL that the server will call
@@ -570,32 +544,24 @@ Module exchangelib.folders.base
:param status_frequency: The frequency, in minutes, that the callback URL will be called with.
:return: The subscription ID and a watermark
"""
- s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_push(
+ from ..services import SubscribeToPush
+ if event_types is None:
+ event_types = SubscribeToPush.EVENT_TYPES
+ return FolderCollection(account=self.account, folders=[self]).subscribe_to_push(
event_types=event_types, watermark=watermark, status_frequency=status_frequency, callback_url=callback_url,
- ))
- if len(s_ids) != 1:
- raise ValueError('Expected result length 1, but got %s' % s_ids)
- s_id = s_ids[0]
- if isinstance(s_id, Exception):
- raise s_id
- return s_id
+ )
@require_id
- def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
+ def subscribe_to_streaming(self, event_types=None):
"""Create a streaming subscription.
:param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES
:return: The subscription ID
"""
- s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming(
- event_types=event_types,
- ))
- if len(s_ids) != 1:
- raise ValueError('Expected result length 1, but got %s' % s_ids)
- s_id = s_ids[0]
- if isinstance(s_id, Exception):
- raise s_id
- return s_id
+ from ..services import SubscribeToStreaming
+ if event_types is None:
+ event_types = SubscribeToStreaming.EVENT_TYPES
+ return FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming(event_types=event_types)
@require_id
def pull_subscription(self, **kwargs):
@@ -618,18 +584,19 @@ Module exchangelib.folders.base
This method doesn't need the current folder instance, but it makes sense to keep the method along the other
sync methods.
"""
+ from ..services import Unsubscribe
return Unsubscribe(account=self.account).get(subscription_id=subscription_id)
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
"""Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after
this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.
- :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
+ :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderItems service.
:param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
:param ignore: A list of Item IDs to ignore in the sync
:param max_changes_returned: The max number of change
:param sync_scope: Specify whether to return just items, or items and folder associated information. Possible
- values are specified in SyncFolderitems.SYNC_SCOPES
+ values are specified in SyncFolderItems.SYNC_SCOPES
:return: A generator of (change_type, item) tuples
"""
if not sync_state:
@@ -651,7 +618,7 @@ Module exchangelib.folders.base
changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new
sync state.
- :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
+ :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderItems service.
:param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
:return:
"""
@@ -676,6 +643,7 @@ Module exchangelib.folders.base
This method doesn't need the current folder instance, but it makes sense to keep the method along the other
sync methods.
"""
+ from ..services import GetEvents
svc = GetEvents(account=self.account)
while True:
notification = svc.get(subscription_id=subscription_id, watermark=watermark)
@@ -696,9 +664,8 @@ Module exchangelib.folders.base
This method doesn't need the current folder instance, but it makes sense to keep the method along the other
sync methods.
"""
- # Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
- request_timeout = connection_timeout*60 + 60
- svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
+ from ..services import GetStreamingEvents
+ svc = GetStreamingEvents(account=self.account)
subscription_ids = subscription_id_or_ids if is_iterable(subscription_id_or_ids, generators_allowed=True) \
else [subscription_id_or_ids]
for i, notification in enumerate(
@@ -730,7 +697,7 @@ Module exchangelib.folders.base
try:
return SingleFolderQuerySet(account=self.account, folder=self).depth(SHALLOW_FOLDERS).get(name=other)
except DoesNotExist:
- raise ErrorFolderNotFound("No subfolder with name '%s'" % other)
+ raise ErrorFolderNotFound(f"No subfolder with name {other!r}")
def __truediv__(self, other):
"""Support the some_folder / 'child_folder' / 'child_of_child_folder' navigation syntax."""
@@ -743,7 +710,7 @@ Module exchangelib.folders.base
for c in self.children:
if c.name == other:
return c
- raise ErrorFolderNotFound("No subfolder with name '%s'" % other)
+ raise ErrorFolderNotFound(f"No subfolder with name {other!r}")
def __repr__(self):
return self.__class__.__name__ + \
@@ -751,7 +718,7 @@ Module exchangelib.folders.base
self.folder_class, self.id, self.changekey))
def __str__(self):
- return '%s (%s)' % (self.__class__.__name__, self.name)
+ return f'{self.__class__.__name__} ({self.name})'
class Folder(BaseFolder):
@@ -816,7 +783,7 @@ Module exchangelib.folders.base
folder=cls(root=root, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
)
except MISSING_FOLDER_ERRORS:
- raise ErrorFolderNotFound('Could not find distinguished folder %r' % cls.DISTINGUISHED_FOLDER_ID)
+ raise ErrorFolderNotFound(f'Could not find distinguished folder {cls.DISTINGUISHED_FOLDER_ID!r}')
@property
def parent(self):
@@ -833,7 +800,7 @@ Module exchangelib.folders.base
self.parent_folder_id = None
else:
if not isinstance(value, BaseFolder):
- raise ValueError("'value' %r must be a Folder instance" % value)
+ raise InvalidTypeError('value', value, BaseFolder)
self.root = value.root
self.parent_folder_id = ParentFolderId(id=value.id, changekey=value.changekey)
@@ -841,7 +808,7 @@ Module exchangelib.folders.base
from .roots import RootOfHierarchy
super().clean(version=version)
if self.root and not isinstance(self.root, RootOfHierarchy):
- raise ValueError("'root' %r must be a RootOfHierarchy instance" % self.root)
+ raise InvalidTypeError('root', self.root, RootOfHierarchy)
@classmethod
def from_xml_with_root(cls, elem, root):
@@ -881,43 +848,7 @@ Module exchangelib.folders.base
pass
if folder_cls == Folder:
log.debug('Fallback to class Folder (folder_class %s, name %s)', folder.folder_class, folder.name)
- return folder_cls(root=root, **{f.name: getattr(folder, f.name) for f in folder.FIELDS})
-
-
-class BaseSubscription(metaclass=abc.ABCMeta):
- def __init__(self, folder, **subscription_kwargs):
- self.folder = folder
- self.subscription_kwargs = subscription_kwargs
- self.subscription_id = None
-
- def __enter__(self):
- pass
-
- def __exit__(self, *args, **kwargs):
- self.folder.unsubscribe(subscription_id=self.subscription_id)
- self.subscription_id = None
-
-
-class PullSubscription(BaseSubscription):
- def __enter__(self):
- self.subscription_id, watermark = self.folder.subscribe_to_pull(**self.subscription_kwargs)
- return self.subscription_id, watermark
-
-
-class PushSubscription(BaseSubscription):
- def __enter__(self):
- self.subscription_id, watermark = self.folder.subscribe_to_push(**self.subscription_kwargs)
- return self.subscription_id, watermark
-
- def __exit__(self, *args, **kwargs):
- # Cannot unsubscribe to push subscriptions
- pass
-
-
-class StreamingSubscription(BaseSubscription):
- def __enter__(self):
- self.subscription_id = self.folder.subscribe_to_streaming(**self.subscription_kwargs)
- return self.subscription_id
+ return folder_cls(root=root, **{f.name: getattr(folder, f.name) for f in folder.FIELDS})
@@ -1794,7 +1697,7 @@ Static methods
try:
return cls.ITEM_MODEL_MAP[tag]
except KeyError:
- raise ValueError('Item type %s was unexpected in a %s folder' % (tag, cls.__name__))
+ raise ValueError(f'Item type {tag} was unexpected in a {cls.__name__} folder')
@@ -1827,14 +1730,14 @@ Static methods
# Resolve a single folder
folders = list(FolderCollection(account=account, folders=[folder]).resolve())
if not folders:
- raise ErrorFolderNotFound('Could not find folder %r' % folder)
+ raise ErrorFolderNotFound(f'Could not find folder {folder!r}')
if len(folders) != 1:
- raise ValueError('Expected result length 1, but got %s' % folders)
+ raise ValueError(f'Expected result length 1, but got {folders}')
f = folders[0]
if isinstance(f, Exception):
raise f
if f.__class__ != cls:
- raise ValueError("Expected folder %r to be a %s instance" % (f, cls))
+ raise ValueError(f"Expected folder {f!r} to be a {cls} instance")
return f
@@ -1850,8 +1753,6 @@ @classmethod
def supports_version(cls, version):
# 'version' is a Version instance, for convenience by callers
- if not isinstance(version, Version):
- raise ValueError("'version' %r must be a Version instance" % version)
if not cls.supported_from:
return True
return version.build >= cls.supported_from
@@ -1869,12 +1770,12 @@ @property
def absolute(self):
- return ''.join('/%s' % p.name for p in self.parts)
+ return ''.join(f'/{p.name}' for p in self.parts)
var account
Return the account this folder belongs to
@property
@abc.abstractmethod
def account(self):
- pass
+ """Return the account this folder belongs to"""
var child_folder_count
var parent
Return the parent folder of this folder
@property
@abc.abstractmethod
def parent(self):
- pass
+ """Return the parent folder of this folder"""
var parent_folder_id
var root
Return the root folder this folder belongs to
@property
@abc.abstractmethod
def root(self):
- pass
+ """Return the root folder this folder belongs to"""
var total_count
-def all(self)
-
def all(self):
- return FolderCollection(account=self.account, folders=[self]).all()
-
def bulk_create(self, items, *args, **kwargs)
@require_id
def create_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
+ from ..services import CreateUserConfiguration
user_configuration = UserConfiguration(
user_configuration_name=UserConfigurationName(name=name, folder=self),
dictionary=dictionary,
@@ -2077,8 +1966,7 @@ Methods
Expand source code
def delete(self, delete_type=HARD_DELETE):
- if delete_type not in DELETE_TYPE_CHOICES:
- raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
+ from ..services import DeleteFolder
DeleteFolder(account=self.account).get(folders=[self], delete_type=delete_type)
self.root.remove_folder(self) # Remove the updated folder from the cache
self._id = None
@@ -2095,6 +1983,7 @@ Methods
@require_id
def delete_user_configuration(self, name):
+ from ..services import DeleteUserConfiguration
return DeleteUserConfiguration(account=self.account).get(
user_configuration_name=UserConfigurationNameMNS(name=name, folder=self)
)
@@ -2110,8 +1999,7 @@ Methods
Expand source code
def empty(self, delete_type=HARD_DELETE, delete_sub_folders=False):
- if delete_type not in DELETE_TYPE_CHOICES:
- raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
+ from ..services import EmptyFolder
EmptyFolder(account=self.account).get(
folders=[self], delete_type=delete_type, delete_sub_folders=delete_sub_folders
)
@@ -2120,45 +2008,6 @@ Methods
self.root.clear_cache()
-
-def exclude(self, *args, **kwargs)
-
--
-
-
-
-Expand source code
-
-def exclude(self, *args, **kwargs):
- return FolderCollection(account=self.account, folders=[self]).exclude(*args, **kwargs)
-
-
-
-def filter(self, *args, **kwargs)
-
--
-
-
-
-Expand source code
-
-def filter(self, *args, **kwargs):
- return FolderCollection(account=self.account, folders=[self]).filter(*args, **kwargs)
-
-
-
-def get(self, *args, **kwargs)
-
--
-
-
-
-Expand source code
-
-def get(self, *args, **kwargs):
- return FolderCollection(account=self.account, folders=[self]).get(*args, **kwargs)
-
-
def get_events(self, subscription_id, watermark)
@@ -2183,6 +2032,7 @@ Methods
This method doesn't need the current folder instance, but it makes sense to keep the method along the other
sync methods.
"""
+ from ..services import GetEvents
svc = GetEvents(account=self.account)
while True:
notification = svc.get(subscription_id=subscription_id, watermark=watermark)
@@ -2221,9 +2071,8 @@ Methods
This method doesn't need the current folder instance, but it makes sense to keep the method along the other
sync methods.
"""
- # Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
- request_timeout = connection_timeout*60 + 60
- svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
+ from ..services import GetStreamingEvents
+ svc = GetStreamingEvents(account=self.account)
subscription_ids = subscription_id_or_ids if is_iterable(subscription_id_or_ids, generators_allowed=True) \
else [subscription_id_or_ids]
for i, notification in enumerate(
@@ -2237,7 +2086,7 @@ Methods
-def get_user_configuration(self, name, properties='All')
+def get_user_configuration(self, name, properties=None)
-
@@ -2246,7 +2095,11 @@
Methods
Expand source code
@require_id
-def get_user_configuration(self, name, properties=ALL):
+def get_user_configuration(self, name, properties=None):
+ from ..services import GetUserConfiguration
+ from ..services.get_user_configuration import ALL
+ if properties is None:
+ properties = ALL
return GetUserConfiguration(account=self.account).get(
user_configuration_name=UserConfigurationNameMNS(name=name, folder=self),
properties=properties,
@@ -2276,6 +2129,7 @@ Methods
Expand source code
def move(self, to_folder):
+ from ..services import MoveFolder
res = MoveFolder(account=self.account).get(folders=[self], to_folder=to_folder)
folder_id, changekey = res.id, res.changekey
if self.id != folder_id:
@@ -2286,19 +2140,6 @@ Methods
self.root.update_folder(self) # Update the folder in the cache
-
-def none(self)
-
--
-
-
-
-Expand source code
-
-def none(self):
- return FolderCollection(account=self.account, folders=[self]).none()
-
-
def normalize_fields(self, fields)
@@ -2321,8 +2162,6 @@ Methods
elif isinstance(field_path, Field):
field_path = FieldPath(field=field_path)
fields[i] = field_path
- if not isinstance(field_path, FieldPath):
- raise ValueError("Field %r must be a string or FieldPath instance" % field_path)
if field_path.field.name == 'start':
has_start = True
elif field_path.field.name == 'end':
@@ -2342,20 +2181,6 @@ Methods
return fields
-
-def people(self)
-
def people(self):
- # No point in using a FolderCollection because FindPeople only supports one folder
- return FolderCollection(account=self.account, folders=[self]).people()
-
def pull_subscription(self, **kwargs)
def save(self, update_fields=None):
+ from ..services import CreateFolder, UpdateFolder
if self.id is None:
# New folder
if update_fields:
@@ -2462,7 +2288,7 @@ Methods
-def subscribe_to_pull(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), watermark=None, timeout=60)
+def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60)
-
Create a pull subscription.
@@ -2476,7 +2302,7 @@ Methods
Expand source code
@require_id
-def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
+def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60):
"""Create a pull subscription.
:param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPull.EVENT_TYPES
@@ -2485,19 +2311,16 @@ Methods
GetEvents request for this subscription.
:return: The subscription ID and a watermark
"""
- s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_pull(
+ from ..services import SubscribeToPull
+ if event_types is None:
+ event_types = SubscribeToPull.EVENT_TYPES
+ return FolderCollection(account=self.account, folders=[self]).subscribe_to_pull(
event_types=event_types, watermark=watermark, timeout=timeout,
- ))
- if len(s_ids) != 1:
- raise ValueError('Expected result length 1, but got %s' % s_ids)
- s_id = s_ids[0]
- if isinstance(s_id, Exception):
- raise s_id
- return s_id
+ )
-def subscribe_to_push(self, callback_url, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), watermark=None, status_frequency=1)
+def subscribe_to_push(self, callback_url, event_types=None, watermark=None, status_frequency=1)
-
Create a push subscription.
@@ -2511,8 +2334,7 @@ Methods
Expand source code
@require_id
-def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
- status_frequency=1):
+def subscribe_to_push(self, callback_url, event_types=None, watermark=None, status_frequency=1):
"""Create a push subscription.
:param callback_url: A client-defined URL that the server will call
@@ -2521,19 +2343,16 @@ Methods
:param status_frequency: The frequency, in minutes, that the callback URL will be called with.
:return: The subscription ID and a watermark
"""
- s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_push(
+ from ..services import SubscribeToPush
+ if event_types is None:
+ event_types = SubscribeToPush.EVENT_TYPES
+ return FolderCollection(account=self.account, folders=[self]).subscribe_to_push(
event_types=event_types, watermark=watermark, status_frequency=status_frequency, callback_url=callback_url,
- ))
- if len(s_ids) != 1:
- raise ValueError('Expected result length 1, but got %s' % s_ids)
- s_id = s_ids[0]
- if isinstance(s_id, Exception):
- raise s_id
- return s_id
+ )
-def subscribe_to_streaming(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'))
+def subscribe_to_streaming(self, event_types=None)
-
Create a streaming subscription.
@@ -2544,21 +2363,16 @@ Methods
Expand source code
@require_id
-def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
+def subscribe_to_streaming(self, event_types=None):
"""Create a streaming subscription.
:param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES
:return: The subscription ID
"""
- s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming(
- event_types=event_types,
- ))
- if len(s_ids) != 1:
- raise ValueError('Expected result length 1, but got %s' % s_ids)
- s_id = s_ids[0]
- if isinstance(s_id, Exception):
- raise s_id
- return s_id
+ from ..services import SubscribeToStreaming
+ if event_types is None:
+ event_types = SubscribeToStreaming.EVENT_TYPES
+ return FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming(event_types=event_types)
@@ -2568,7 +2382,7 @@ Methods
Return all folder changes to a folder hierarchy, as a generator. If sync_state is specified, get all folder
changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new
sync state.
-:param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
+
:param sync_state: The state of the sync. Returned by a successful call to the SyncFolderItems service.
:param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
:return:
@@ -2580,7 +2394,7 @@ Methods
changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new
sync state.
- :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
+ :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderItems service.
:param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
:return:
"""
@@ -2602,12 +2416,12 @@ Methods
-
Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after
this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.
-:param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
+
:param sync_state: The state of the sync. Returned by a successful call to the SyncFolderItems service.
:param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
:param ignore: A list of Item IDs to ignore in the sync
:param max_changes_returned: The max number of change
:param sync_scope: Specify whether to return just items, or items and folder associated information. Possible
-values are specified in SyncFolderitems.SYNC_SCOPES
+values are specified in SyncFolderItems.SYNC_SCOPES
:return: A generator of (change_type, item) tuples
@@ -2617,12 +2431,12 @@ Methods
"""Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after
this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.
- :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
+ :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderItems service.
:param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
:param ignore: A list of Item IDs to ignore in the sync
:param max_changes_returned: The max number of change
:param sync_scope: Specify whether to return just items, or items and folder associated information. Possible
- values are specified in SyncFolderitems.SYNC_SCOPES
+ values are specified in SyncFolderItems.SYNC_SCOPES
:return: A generator of (change_type, item) tuples
"""
if not sync_state:
@@ -2658,8 +2472,8 @@ Methods
return True
-
-def to_folder_id(self)
+
+def to_id(self)
-
@@ -2667,7 +2481,7 @@
Methods
Expand source code
-def to_folder_id(self):
+def to_id(self):
if self.is_distinguished:
# Don't add the changekey here. When modifying folder content, we usually don't care if others have changed
# the folder content since we fetched the changekey.
@@ -2682,36 +2496,6 @@ Methods
raise ValueError('Must be a distinguished folder or have an ID')
-
-def to_id_xml(self, version)
-
--
-
-
-
-Expand source code
-
-def to_id_xml(self, version):
- # Folder(name='Foo') is a perfectly valid ID to e.g. create a folder
- return self.to_xml(version=version)
-
-
-
-def to_xml(self, version)
-
--
-
-
-
-Expand source code
-
-def to_xml(self, version):
- try:
- return self.to_folder_id().to_xml(version=version)
- except ValueError:
- return super().to_xml(version=version)
-
-
def tree(self)
@@ -2740,22 +2524,22 @@ Methods
├── exchangelib issues
└── Mom
"""
- tree = '%s\n' % self.name
+ tree = f'{self.name}\n'
children = list(self.children)
for i, c in enumerate(sorted(children, key=attrgetter('name')), start=1):
nodes = c.tree().split('\n')
for j, node in enumerate(nodes, start=1):
if i != len(children) and j == 1:
# Not the last child, but the first node, which is the name of the child
- tree += '├── %s\n' % node
+ tree += f'├── {node}\n'
elif i != len(children) and j > 1:
# Not the last child, and not name of child
- tree += '│ %s\n' % node
+ tree += f'│ {node}\n'
elif i == len(children) and j == 1:
# Not the last child, but the first node, which is the name of the child
- tree += '└── %s\n' % node
+ tree += f'└── {node}\n'
else: # Last child, and not name of child
- tree += ' %s\n' % node
+ tree += f' {node}\n'
return tree.strip()
@@ -2781,6 +2565,7 @@ Methods
This method doesn't need the current folder instance, but it makes sense to keep the method along the other
sync methods.
"""
+ from ..services import Unsubscribe
return Unsubscribe(account=self.account).get(subscription_id=subscription_id)
@@ -2795,6 +2580,7 @@ Methods
@require_id
def update_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
+ from ..services import UpdateUserConfiguration
user_configuration = UserConfiguration(
user_configuration_name=UserConfigurationName(name=name, folder=self),
dictionary=dictionary,
@@ -2814,18 +2600,7 @@ Methods
Expand source code
def validate_item_field(self, field, version):
- # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
- # for the item types supported by this folder.
-
- # For each field, check if the field is valid for any of the item models supported by this folder
- for item_model in self.supported_item_models:
- try:
- item_model.validate_field(field=field, version=version)
- break
- except InvalidField:
- continue
- else:
- raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))
+ FolderCollection(account=self.account, folders=[self]).validate_item_field(field=field, version=version)
@@ -2842,7 +2617,7 @@ Methods
-def wipe(self, page_size=None)
+def wipe(self, page_size=None, chunk_size=None)
-
@@ -2850,14 +2625,14 @@
Methods
Expand source code
-def wipe(self, page_size=None, _seen=None, _level=0):
+def wipe(self, page_size=None, chunk_size=None, _seen=None, _level=0):
# Recursively deletes all items in this folder, and all subfolders and their content. Attempts to protect
# distinguished folders from being deleted. Use with caution!
_seen = _seen or set()
if self.id in _seen:
- raise RecursionError('We already tried to wipe %s' % self)
+ raise RecursionError(f'We already tried to wipe {self}')
if _level > 16:
- raise RecursionError('Max recursion level reached: %s' % _level)
+ raise RecursionError(f'Max recursion level reached: {_level}')
_seen.add(self.id)
log.warning('Wiping %s', self)
has_distinguished_subfolders = any(f.is_distinguished for f in self.children)
@@ -2873,13 +2648,18 @@ Methods
self.empty(delete_sub_folders=False)
except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
log.warning('Not allowed to empty %s. Trying to delete items instead', self)
+ kwargs = {}
+ if page_size is not None:
+ kwargs['page_size'] = page_size
+ if chunk_size is not None:
+ kwargs['chunk_size'] = chunk_size
try:
- self.all().delete(**dict(page_size=page_size) if page_size else {})
+ self.all().delete(**kwargs)
except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound):
log.warning('Not allowed to delete items in %s', self)
_level += 1
for f in self.children:
- f.wipe(page_size=page_size, _seen=_seen, _level=_level)
+ f.wipe(page_size=page_size, chunk_size=chunk_size, _seen=_seen, _level=_level)
# Remove non-distinguished children that are empty and have no subfolders
if f.is_deletable and not f.children:
log.warning('Deleting folder %s', f)
@@ -2902,36 +2682,16 @@ Inherited members
validate_field
-
-
-
-class BaseSubscription
-(folder, **subscription_kwargs)
-
--
-
-
-
-Expand source code
-
-class BaseSubscription(metaclass=abc.ABCMeta):
- def __init__(self, folder, **subscription_kwargs):
- self.folder = folder
- self.subscription_kwargs = subscription_kwargs
- self.subscription_id = None
-
- def __enter__(self):
- pass
-
- def __exit__(self, *args, **kwargs):
- self.folder.unsubscribe(subscription_id=self.subscription_id)
- self.subscription_id = None
-
-Subclasses
+SearchableMixIn
:
+
@@ -3006,7 +2766,7 @@ Subclasses
folder=cls(root=root, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
)
except MISSING_FOLDER_ERRORS:
- raise ErrorFolderNotFound('Could not find distinguished folder %r' % cls.DISTINGUISHED_FOLDER_ID)
+ raise ErrorFolderNotFound(f'Could not find distinguished folder {cls.DISTINGUISHED_FOLDER_ID!r}')
@property
def parent(self):
@@ -3023,7 +2783,7 @@ Subclasses
self.parent_folder_id = None
else:
if not isinstance(value, BaseFolder):
- raise ValueError("'value' %r must be a Folder instance" % value)
+ raise InvalidTypeError('value', value, BaseFolder)
self.root = value.root
self.parent_folder_id = ParentFolderId(id=value.id, changekey=value.changekey)
@@ -3031,7 +2791,7 @@ Subclasses
from .roots import RootOfHierarchy
super().clean(version=version)
if self.root and not isinstance(self.root, RootOfHierarchy):
- raise ValueError("'root' %r must be a RootOfHierarchy instance" % self.root)
+ raise InvalidTypeError('root', self.root, RootOfHierarchy)
@classmethod
def from_xml_with_root(cls, elem, root):
@@ -3202,63 +2962,20 @@ Static methods
folder=cls(root=root, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
)
except MISSING_FOLDER_ERRORS:
- raise ErrorFolderNotFound('Could not find distinguished folder %r' % cls.DISTINGUISHED_FOLDER_ID)
+ raise ErrorFolderNotFound(f'Could not find distinguished folder {cls.DISTINGUISHED_FOLDER_ID!r}')
Instance variables
-var account
--
-
-
-
-Expand source code
-
-@property
-def account(self):
- if self.root is None:
- return None
- return self.root.account
-
-
var effective_rights
-
-var parent
--
-
-
-
-Expand source code
-
-@property
-def parent(self):
- if not self.parent_folder_id:
- return None
- if self.parent_folder_id.id == self.id:
- # Some folders have a parent that references itself. Avoid circular references here
- return None
- return self.root.get_folder(self.parent_folder_id)
-
-
var permission_set
-
-var root
--
-
-
-
-Expand source code
-
-@property
-def root(self):
- return self._root
-
-
Methods
@@ -3275,7 +2992,7 @@ Methods
from .roots import RootOfHierarchy
super().clean(version=version)
if self.root and not isinstance(self.root, RootOfHierarchy):
- raise ValueError("'root' %r must be a RootOfHierarchy instance" % self.root)
+ raise InvalidTypeError('root', self.root, RootOfHierarchy)
@@ -3284,16 +3001,25 @@ Inherited members
BaseFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3308,70 +3034,6 @@ Inherited members
-
-class PullSubscription
-(folder, **subscription_kwargs)
-
--
-
-
-
-Expand source code
-
-class PullSubscription(BaseSubscription):
- def __enter__(self):
- self.subscription_id, watermark = self.folder.subscribe_to_pull(**self.subscription_kwargs)
- return self.subscription_id, watermark
-
-Ancestors
-
-
-
-class PushSubscription
-(folder, **subscription_kwargs)
-
--
-
-
-
-Expand source code
-
-class PushSubscription(BaseSubscription):
- def __enter__(self):
- self.subscription_id, watermark = self.folder.subscribe_to_push(**self.subscription_kwargs)
- return self.subscription_id, watermark
-
- def __exit__(self, *args, **kwargs):
- # Cannot unsubscribe to push subscriptions
- pass
-
-Ancestors
-
-
-
-class StreamingSubscription
-(folder, **subscription_kwargs)
-
--
-
-
-
-Expand source code
-
-class StreamingSubscription(BaseSubscription):
- def __enter__(self):
- self.subscription_id = self.folder.subscribe_to_streaming(**self.subscription_kwargs)
- return self.subscription_id
-
-Ancestors
-
-
@@ -3404,7 +3066,6 @@ NAMESPACE
absolute
account
-all
allowed_item_fields
bulk_create
child_folder_count
@@ -3414,12 +3075,9 @@ delete
delete_user_configuration
empty
-exclude
-filter
folder_class
folder_cls_from_container_class
folder_sync_state
-get
get_events
get_folder_allowed
get_item_field_by_fieldname
@@ -3434,12 +3092,10 @@ localized_names
move
name
-none
normalize_fields
parent
parent_folder_id
parts
-people
pull_subscription
push_subscription
refresh
@@ -3456,9 +3112,7 @@ sync_hierarchy
sync_items
test_access
-to_folder_id
-to_id_xml
-to_xml
+to_id
total_count
tree
unread_count
@@ -3470,31 +3124,16 @@ BaseSubscription
-
--
Folder
--
-
PullSubscription
-
--
-
PushSubscription
-
--
-
StreamingSubscription
-
diff --git a/docs/exchangelib/folders/collections.html b/docs/exchangelib/folders/collections.html
index eae00689..3a871adc 100644
--- a/docs/exchangelib/folders/collections.html
+++ b/docs/exchangelib/folders/collections.html
@@ -26,18 +26,17 @@ Module exchangelib.folders.collections
Expand source code
-import logging
+import abc
+import logging
from cached_property import threaded_cached_property
-from .queryset import FOLDER_TRAVERSAL_CHOICES
+from ..errors import InvalidTypeError
from ..fields import FieldPath, InvalidField
-from ..items import Persona, ITEM_TRAVERSAL_CHOICES, SHAPE_CHOICES, ID_ONLY
+from ..items import Persona, ID_ONLY
from ..properties import CalendarView
from ..queryset import QuerySet, SearchableMixIn, Q
from ..restriction import Restriction
-from ..services import FindFolder, GetFolder, FindItem, FindPeople, SyncFolderItems, SyncFolderHierarchy, \
- SubscribeToPull, SubscribeToPush, SubscribeToStreaming
from ..util import require_account
log = logging.getLogger(__name__)
@@ -154,7 +153,8 @@ Module exchangelib.folders.collections
return tuple(item_model for folder in self.folders for item_model in folder.supported_item_models)
def validate_item_field(self, field, version):
- # For each field, check if the field is valid for any of the item models supported by this folder
+ # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
+ # for the item types supported by this folder collection.
for item_model in self.supported_item_models:
try:
item_model.validate_field(field=field, version=version)
@@ -162,14 +162,35 @@ Module exchangelib.folders.collections
except InvalidField:
continue
else:
- raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))
+ raise InvalidField(f"{field!r} is not a valid field on {self.supported_item_models}")
+
+ def _rinse_args(self, q, depth, additional_fields, field_validator):
+ if depth is None:
+ depth = self._get_default_item_traversal_depth()
+ if additional_fields:
+ for f in additional_fields:
+ field_validator(field=f, version=self.account.version)
+ if f.field.is_complex:
+ raise ValueError(f"Field {f.field.name!r} not supported for this service")
+
+ # Build up any restrictions
+ if q.is_empty():
+ restriction = None
+ query_string = None
+ elif q.query_string:
+ restriction = None
+ query_string = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
+ else:
+ restriction = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
+ query_string = None
+ return depth, restriction, query_string
def find_items(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
calendar_view=None, page_size=None, max_items=None, offset=0):
"""Private method to call the FindItem service.
:param q: a Q instance containing any restrictions
- :param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is
+ :param shape: controls whether to return (id, changekey) tuples or Item objects. If additional_fields is
non-null, we always return Item objects. (Default value = ID_ONLY)
:param depth: controls the whether to return soft-deleted items or not. (Default value = None)
:param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware
@@ -182,36 +203,20 @@ Module exchangelib.folders.collections
:return: a generator for the returned item IDs or items
"""
+ from ..services import FindItem
if not self.folders:
log.debug('Folder list is empty')
return
if q.is_never():
log.debug('Query will never return results')
return
- if shape not in SHAPE_CHOICES:
- raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
- if depth is None:
- depth = self._get_default_item_traversal_depth()
- if depth not in ITEM_TRAVERSAL_CHOICES:
- raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
- if additional_fields:
- for f in additional_fields:
- self.validate_item_field(field=f, version=self.account.version)
- if f.field.is_complex:
- raise ValueError("find_items() does not support field '%s'. Use fetch() instead" % f.field.name)
+ depth, restriction, query_string = self._rinse_args(
+ q=q, depth=depth, additional_fields=additional_fields,
+ field_validator=self.validate_item_field
+ )
if calendar_view is not None and not isinstance(calendar_view, CalendarView):
- raise ValueError("'calendar_view' %s must be a CalendarView instance" % calendar_view)
+ raise InvalidTypeError('calendar_view', calendar_view, CalendarView)
- # Build up any restrictions
- if q.is_empty():
- restriction = None
- query_string = None
- elif q.query_string:
- restriction = None
- query_string = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
- else:
- restriction = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
- query_string = None
log.debug(
'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
self.account,
@@ -221,7 +226,7 @@ Module exchangelib.folders.collections
additional_fields,
restriction.q if restriction else None,
)
- yield from FindItem(account=self.account, chunk_size=page_size).call(
+ yield from FindItem(account=self.account, page_size=page_size).call(
folders=self.folders,
additional_fields=additional_fields,
restriction=restriction,
@@ -247,7 +252,7 @@ Module exchangelib.folders.collections
"""Private method to call the FindPeople service.
:param q: a Q instance containing any restrictions
- :param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is
+ :param shape: controls whether to return (id, changekey) tuples or Persona objects. If additional_fields is
non-null, we always return Persona objects. (Default value = ID_ONLY)
:param depth: controls the whether to return soft-deleted items or not. (Default value = None)
:param additional_fields: the extra properties we want on the return objects. Default is no properties.
@@ -258,35 +263,17 @@ Module exchangelib.folders.collections
:return: a generator for the returned personas
"""
+ from ..services import FindPeople
folder = self._get_single_folder()
- if not folder:
- return
if q.is_never():
log.debug('Query will never return results')
return
- if shape not in SHAPE_CHOICES:
- raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
- if depth is None:
- depth = self._get_default_item_traversal_depth()
- if depth not in ITEM_TRAVERSAL_CHOICES:
- raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
- if additional_fields:
- for f in additional_fields:
- Persona.validate_field(field=f, version=self.account.version)
- if f.field.is_complex:
- raise ValueError("find_people() does not support field '%s'" % f.field.name)
+ depth, restriction, query_string = self._rinse_args(
+ q=q, depth=depth, additional_fields=additional_fields,
+ field_validator=Persona.validate_field
+ )
- # Build up any restrictions
- if q.is_empty():
- restriction = None
- query_string = None
- elif q.query_string:
- restriction = None
- query_string = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
- else:
- restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
- query_string = None
- yield from FindPeople(account=self.account, chunk_size=page_size).call(
+ yield from FindPeople(account=self.account, page_size=page_size).call(
folder=folder,
additional_fields=additional_fields,
restriction=restriction,
@@ -314,11 +301,11 @@ Module exchangelib.folders.collections
for f in self.folders:
if isinstance(f, RootOfHierarchy):
if has_non_roots:
- raise ValueError('Cannot call GetFolder on a mix of folder types: {}'.format(self.folders))
+ raise ValueError(f'Cannot call GetFolder on a mix of folder types: {self.folders}')
has_roots = True
else:
if has_roots:
- raise ValueError('Cannot call GetFolder on a mix of folder types: {}'.format(self.folders))
+ raise ValueError(f'Cannot call GetFolder on a mix of folder types: {self.folders}')
has_non_roots = True
return RootOfHierarchy if has_roots else Folder
@@ -327,8 +314,8 @@ Module exchangelib.folders.collections
if len(unique_depths) == 1:
return unique_depths.pop()
raise ValueError(
- 'Folders in this collection do not have a common %s value. You need to define an explicit traversal depth'
- 'with QuerySet.depth() (values: %s)' % (traversal_attr, unique_depths)
+ f'Folders in this collection do not have a common {traversal_attr} value. You need to define an explicit '
+ f'traversal depth with QuerySet.depth() (values: {unique_depths})'
)
def _get_default_item_traversal_depth(self):
@@ -358,6 +345,7 @@ Module exchangelib.folders.collections
@require_account
def find_folders(self, q=None, shape=ID_ONLY, depth=None, additional_fields=None, page_size=None, max_items=None,
offset=0):
+ from ..services import FindFolder
# 'depth' controls whether to return direct children or recurse into sub-folders
from .base import BaseFolder, Folder
if q is None:
@@ -372,26 +360,22 @@ Module exchangelib.folders.collections
restriction = None
else:
restriction = Restriction(q, folders=self.folders, applies_to=Restriction.FOLDERS)
- if shape not in SHAPE_CHOICES:
- raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
if depth is None:
depth = self._get_default_folder_traversal_depth()
- if depth not in FOLDER_TRAVERSAL_CHOICES:
- raise ValueError("'depth' %s must be one of %s" % (depth, FOLDER_TRAVERSAL_CHOICES))
if additional_fields is None:
# Default to all non-complex properties. Subfolders will always be of class Folder
additional_fields = self.get_folder_fields(target_cls=Folder, is_complex=False)
else:
for f in additional_fields:
if f.field.is_complex:
- raise ValueError("find_folders() does not support field '%s'. Use get_folders()." % f.field.name)
+ raise ValueError(f"find_folders() does not support field {f.field.name!r}. Use get_folders().")
# Add required fields
additional_fields.update(
(FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
)
- yield from FindFolder(account=self.account, chunk_size=page_size).call(
+ yield from FindFolder(account=self.account, page_size=page_size).call(
folders=self.folders,
additional_fields=additional_fields,
restriction=restriction,
@@ -402,6 +386,7 @@ Module exchangelib.folders.collections
)
def get_folders(self, additional_fields=None):
+ from ..services import GetFolder
# Expand folders with their full set of properties
from .base import BaseFolder
if not self.folders:
@@ -422,34 +407,62 @@ Module exchangelib.folders.collections
shape=ID_ONLY,
)
- def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
+ def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60):
+ from ..services import SubscribeToPull
if not self.folders:
log.debug('Folder list is empty')
- return
- yield from SubscribeToPull(account=self.account).call(
+ return None
+ if event_types is None:
+ event_types = SubscribeToPull.EVENT_TYPES
+ return SubscribeToPull(account=self.account).get(
folders=self.folders, event_types=event_types, watermark=watermark, timeout=timeout,
)
- def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
- status_frequency=1):
+ def subscribe_to_push(self, callback_url, event_types=None, watermark=None, status_frequency=1):
+ from ..services import SubscribeToPush
if not self.folders:
log.debug('Folder list is empty')
- return
- yield from SubscribeToPush(account=self.account).call(
+ return None
+ if event_types is None:
+ event_types = SubscribeToPush.EVENT_TYPES
+ return SubscribeToPush(account=self.account).get(
folders=self.folders, event_types=event_types, watermark=watermark, status_frequency=status_frequency,
url=callback_url,
)
- def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
+ def subscribe_to_streaming(self, event_types=None):
+ from ..services import SubscribeToStreaming
if not self.folders:
log.debug('Folder list is empty')
- return
- yield from SubscribeToStreaming(account=self.account).call(folders=self.folders, event_types=event_types)
+ return None
+ if event_types is None:
+ event_types = SubscribeToStreaming.EVENT_TYPES
+ return SubscribeToStreaming(account=self.account).get(folders=self.folders, event_types=event_types)
+
+ def pull_subscription(self, **kwargs):
+ return PullSubscription(folder=self, **kwargs)
+
+ def push_subscription(self, **kwargs):
+ return PushSubscription(folder=self, **kwargs)
+
+ def streaming_subscription(self, **kwargs):
+ return StreamingSubscription(folder=self, **kwargs)
+
+ def unsubscribe(self, subscription_id):
+ """Unsubscribe. Only applies to pull and streaming notifications.
+
+ :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|streaming]()
+ :return: True
+
+ This method doesn't need the current collection instance, but it makes sense to keep the method along the other
+ sync methods.
+ """
+ from ..services import Unsubscribe
+ return Unsubscribe(account=self.account).get(subscription_id=subscription_id)
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
+ from ..services import SyncFolderItems
folder = self._get_single_folder()
- if not folder:
- return
if only_fields is None:
# We didn't restrict list of field paths. Get all fields from the server, including extended properties.
additional_fields = {FieldPath(field=f) for f in folder.allowed_item_fields(version=self.account.version)}
@@ -479,17 +492,19 @@ Module exchangelib.folders.collections
raise SyncCompleted(sync_state=svc.sync_state)
def sync_hierarchy(self, sync_state=None, only_fields=None):
+ from ..services import SyncFolderHierarchy
folder = self._get_single_folder()
- if not folder:
- return
if only_fields is None:
# We didn't restrict list of field paths. Get all fields from the server, including extended properties.
additional_fields = {FieldPath(field=f) for f in folder.supported_fields(version=self.account.version)}
else:
- for f in only_fields:
- folder.validate_field(field=f, version=self.account.version)
- # Remove ItemId and ChangeKey. We get them unconditionally
- additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}
+ additional_fields = set()
+ for field_name in only_fields:
+ folder.validate_field(field=field_name, version=self.account.version)
+ f = folder.get_field_by_fieldname(fieldname=field_name)
+ if not f.is_attribute:
+ # Remove ItemId and ChangeKey. We get them unconditionally
+ additional_fields.add(FieldPath(field=f))
# Add required fields
additional_fields.update(
@@ -510,7 +525,44 @@ Module exchangelib.folders.collections
sync_state = svc.sync_state # Set the new sync state in the next call
if svc.includes_last_item_in_range: # Try again if there are more items
break
- raise SyncCompleted(sync_state=svc.sync_state)
+ raise SyncCompleted(sync_state=svc.sync_state)
+
+
+class BaseSubscription(metaclass=abc.ABCMeta):
+ def __init__(self, folder, **subscription_kwargs):
+ self.folder = folder
+ self.subscription_kwargs = subscription_kwargs
+ self.subscription_id = None
+
+ @abc.abstractmethod
+ def __enter__(self):
+ """Create the subscription"""
+
+ def __exit__(self, *args, **kwargs):
+ self.folder.unsubscribe(subscription_id=self.subscription_id)
+ self.subscription_id = None
+
+
+class PullSubscription(BaseSubscription):
+ def __enter__(self):
+ self.subscription_id, watermark = self.folder.subscribe_to_pull(**self.subscription_kwargs)
+ return self.subscription_id, watermark
+
+
+class PushSubscription(BaseSubscription):
+ def __enter__(self):
+ self.subscription_id, watermark = self.folder.subscribe_to_push(**self.subscription_kwargs)
+ return self.subscription_id, watermark
+
+ def __exit__(self, *args, **kwargs):
+ # Cannot unsubscribe to push subscriptions
+ pass
+
+
+class StreamingSubscription(BaseSubscription):
+ def __enter__(self):
+ self.subscription_id = self.folder.subscribe_to_streaming(**self.subscription_kwargs)
+ return self.subscription_id
@@ -522,6 +574,37 @@ Module exchangelib.folders.collections
Classes
+
+class BaseSubscription
+(folder, **subscription_kwargs)
+
+-
+
+
+
+Expand source code
+
+class BaseSubscription(metaclass=abc.ABCMeta):
+ def __init__(self, folder, **subscription_kwargs):
+ self.folder = folder
+ self.subscription_kwargs = subscription_kwargs
+ self.subscription_id = None
+
+ @abc.abstractmethod
+ def __enter__(self):
+ """Create the subscription"""
+
+ def __exit__(self, *args, **kwargs):
+ self.folder.unsubscribe(subscription_id=self.subscription_id)
+ self.subscription_id = None
+
+Subclasses
+
+
class FolderCollection
(account, folders)
@@ -638,7 +721,8 @@ Classes
return tuple(item_model for folder in self.folders for item_model in folder.supported_item_models)
def validate_item_field(self, field, version):
- # For each field, check if the field is valid for any of the item models supported by this folder
+ # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
+ # for the item types supported by this folder collection.
for item_model in self.supported_item_models:
try:
item_model.validate_field(field=field, version=version)
@@ -646,14 +730,35 @@ Classes
except InvalidField:
continue
else:
- raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))
+ raise InvalidField(f"{field!r} is not a valid field on {self.supported_item_models}")
+
+ def _rinse_args(self, q, depth, additional_fields, field_validator):
+ if depth is None:
+ depth = self._get_default_item_traversal_depth()
+ if additional_fields:
+ for f in additional_fields:
+ field_validator(field=f, version=self.account.version)
+ if f.field.is_complex:
+ raise ValueError(f"Field {f.field.name!r} not supported for this service")
+
+ # Build up any restrictions
+ if q.is_empty():
+ restriction = None
+ query_string = None
+ elif q.query_string:
+ restriction = None
+ query_string = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
+ else:
+ restriction = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
+ query_string = None
+ return depth, restriction, query_string
def find_items(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
calendar_view=None, page_size=None, max_items=None, offset=0):
"""Private method to call the FindItem service.
:param q: a Q instance containing any restrictions
- :param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is
+ :param shape: controls whether to return (id, changekey) tuples or Item objects. If additional_fields is
non-null, we always return Item objects. (Default value = ID_ONLY)
:param depth: controls the whether to return soft-deleted items or not. (Default value = None)
:param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware
@@ -666,36 +771,20 @@ Classes
:return: a generator for the returned item IDs or items
"""
+ from ..services import FindItem
if not self.folders:
log.debug('Folder list is empty')
return
if q.is_never():
log.debug('Query will never return results')
return
- if shape not in SHAPE_CHOICES:
- raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
- if depth is None:
- depth = self._get_default_item_traversal_depth()
- if depth not in ITEM_TRAVERSAL_CHOICES:
- raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
- if additional_fields:
- for f in additional_fields:
- self.validate_item_field(field=f, version=self.account.version)
- if f.field.is_complex:
- raise ValueError("find_items() does not support field '%s'. Use fetch() instead" % f.field.name)
+ depth, restriction, query_string = self._rinse_args(
+ q=q, depth=depth, additional_fields=additional_fields,
+ field_validator=self.validate_item_field
+ )
if calendar_view is not None and not isinstance(calendar_view, CalendarView):
- raise ValueError("'calendar_view' %s must be a CalendarView instance" % calendar_view)
+ raise InvalidTypeError('calendar_view', calendar_view, CalendarView)
- # Build up any restrictions
- if q.is_empty():
- restriction = None
- query_string = None
- elif q.query_string:
- restriction = None
- query_string = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
- else:
- restriction = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
- query_string = None
log.debug(
'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
self.account,
@@ -705,7 +794,7 @@ Classes
additional_fields,
restriction.q if restriction else None,
)
- yield from FindItem(account=self.account, chunk_size=page_size).call(
+ yield from FindItem(account=self.account, page_size=page_size).call(
folders=self.folders,
additional_fields=additional_fields,
restriction=restriction,
@@ -731,7 +820,7 @@ Classes
"""Private method to call the FindPeople service.
:param q: a Q instance containing any restrictions
- :param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is
+ :param shape: controls whether to return (id, changekey) tuples or Persona objects. If additional_fields is
non-null, we always return Persona objects. (Default value = ID_ONLY)
:param depth: controls the whether to return soft-deleted items or not. (Default value = None)
:param additional_fields: the extra properties we want on the return objects. Default is no properties.
@@ -742,35 +831,17 @@ Classes
:return: a generator for the returned personas
"""
+ from ..services import FindPeople
folder = self._get_single_folder()
- if not folder:
- return
if q.is_never():
log.debug('Query will never return results')
return
- if shape not in SHAPE_CHOICES:
- raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
- if depth is None:
- depth = self._get_default_item_traversal_depth()
- if depth not in ITEM_TRAVERSAL_CHOICES:
- raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
- if additional_fields:
- for f in additional_fields:
- Persona.validate_field(field=f, version=self.account.version)
- if f.field.is_complex:
- raise ValueError("find_people() does not support field '%s'" % f.field.name)
+ depth, restriction, query_string = self._rinse_args(
+ q=q, depth=depth, additional_fields=additional_fields,
+ field_validator=Persona.validate_field
+ )
- # Build up any restrictions
- if q.is_empty():
- restriction = None
- query_string = None
- elif q.query_string:
- restriction = None
- query_string = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
- else:
- restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
- query_string = None
- yield from FindPeople(account=self.account, chunk_size=page_size).call(
+ yield from FindPeople(account=self.account, page_size=page_size).call(
folder=folder,
additional_fields=additional_fields,
restriction=restriction,
@@ -798,11 +869,11 @@ Classes
for f in self.folders:
if isinstance(f, RootOfHierarchy):
if has_non_roots:
- raise ValueError('Cannot call GetFolder on a mix of folder types: {}'.format(self.folders))
+ raise ValueError(f'Cannot call GetFolder on a mix of folder types: {self.folders}')
has_roots = True
else:
if has_roots:
- raise ValueError('Cannot call GetFolder on a mix of folder types: {}'.format(self.folders))
+ raise ValueError(f'Cannot call GetFolder on a mix of folder types: {self.folders}')
has_non_roots = True
return RootOfHierarchy if has_roots else Folder
@@ -811,8 +882,8 @@ Classes
if len(unique_depths) == 1:
return unique_depths.pop()
raise ValueError(
- 'Folders in this collection do not have a common %s value. You need to define an explicit traversal depth'
- 'with QuerySet.depth() (values: %s)' % (traversal_attr, unique_depths)
+ f'Folders in this collection do not have a common {traversal_attr} value. You need to define an explicit '
+ f'traversal depth with QuerySet.depth() (values: {unique_depths})'
)
def _get_default_item_traversal_depth(self):
@@ -842,6 +913,7 @@ Classes
@require_account
def find_folders(self, q=None, shape=ID_ONLY, depth=None, additional_fields=None, page_size=None, max_items=None,
offset=0):
+ from ..services import FindFolder
# 'depth' controls whether to return direct children or recurse into sub-folders
from .base import BaseFolder, Folder
if q is None:
@@ -856,26 +928,22 @@ Classes
restriction = None
else:
restriction = Restriction(q, folders=self.folders, applies_to=Restriction.FOLDERS)
- if shape not in SHAPE_CHOICES:
- raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
if depth is None:
depth = self._get_default_folder_traversal_depth()
- if depth not in FOLDER_TRAVERSAL_CHOICES:
- raise ValueError("'depth' %s must be one of %s" % (depth, FOLDER_TRAVERSAL_CHOICES))
if additional_fields is None:
# Default to all non-complex properties. Subfolders will always be of class Folder
additional_fields = self.get_folder_fields(target_cls=Folder, is_complex=False)
else:
for f in additional_fields:
if f.field.is_complex:
- raise ValueError("find_folders() does not support field '%s'. Use get_folders()." % f.field.name)
+ raise ValueError(f"find_folders() does not support field {f.field.name!r}. Use get_folders().")
# Add required fields
additional_fields.update(
(FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
)
- yield from FindFolder(account=self.account, chunk_size=page_size).call(
+ yield from FindFolder(account=self.account, page_size=page_size).call(
folders=self.folders,
additional_fields=additional_fields,
restriction=restriction,
@@ -886,6 +954,7 @@ Classes
)
def get_folders(self, additional_fields=None):
+ from ..services import GetFolder
# Expand folders with their full set of properties
from .base import BaseFolder
if not self.folders:
@@ -906,34 +975,62 @@ Classes
shape=ID_ONLY,
)
- def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
+ def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60):
+ from ..services import SubscribeToPull
if not self.folders:
log.debug('Folder list is empty')
- return
- yield from SubscribeToPull(account=self.account).call(
+ return None
+ if event_types is None:
+ event_types = SubscribeToPull.EVENT_TYPES
+ return SubscribeToPull(account=self.account).get(
folders=self.folders, event_types=event_types, watermark=watermark, timeout=timeout,
)
- def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
- status_frequency=1):
+ def subscribe_to_push(self, callback_url, event_types=None, watermark=None, status_frequency=1):
+ from ..services import SubscribeToPush
if not self.folders:
log.debug('Folder list is empty')
- return
- yield from SubscribeToPush(account=self.account).call(
+ return None
+ if event_types is None:
+ event_types = SubscribeToPush.EVENT_TYPES
+ return SubscribeToPush(account=self.account).get(
folders=self.folders, event_types=event_types, watermark=watermark, status_frequency=status_frequency,
url=callback_url,
)
- def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
+ def subscribe_to_streaming(self, event_types=None):
+ from ..services import SubscribeToStreaming
if not self.folders:
log.debug('Folder list is empty')
- return
- yield from SubscribeToStreaming(account=self.account).call(folders=self.folders, event_types=event_types)
+ return None
+ if event_types is None:
+ event_types = SubscribeToStreaming.EVENT_TYPES
+ return SubscribeToStreaming(account=self.account).get(folders=self.folders, event_types=event_types)
+
+ def pull_subscription(self, **kwargs):
+ return PullSubscription(folder=self, **kwargs)
+
+ def push_subscription(self, **kwargs):
+ return PushSubscription(folder=self, **kwargs)
+
+ def streaming_subscription(self, **kwargs):
+ return StreamingSubscription(folder=self, **kwargs)
+
+ def unsubscribe(self, subscription_id):
+ """Unsubscribe. Only applies to pull and streaming notifications.
+
+ :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|streaming]()
+ :return: True
+
+ This method doesn't need the current collection instance, but it makes sense to keep the method along the other
+ sync methods.
+ """
+ from ..services import Unsubscribe
+ return Unsubscribe(account=self.account).get(subscription_id=subscription_id)
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
+ from ..services import SyncFolderItems
folder = self._get_single_folder()
- if not folder:
- return
if only_fields is None:
# We didn't restrict list of field paths. Get all fields from the server, including extended properties.
additional_fields = {FieldPath(field=f) for f in folder.allowed_item_fields(version=self.account.version)}
@@ -963,17 +1060,19 @@ Classes
raise SyncCompleted(sync_state=svc.sync_state)
def sync_hierarchy(self, sync_state=None, only_fields=None):
+ from ..services import SyncFolderHierarchy
folder = self._get_single_folder()
- if not folder:
- return
if only_fields is None:
# We didn't restrict list of field paths. Get all fields from the server, including extended properties.
additional_fields = {FieldPath(field=f) for f in folder.supported_fields(version=self.account.version)}
else:
- for f in only_fields:
- folder.validate_field(field=f, version=self.account.version)
- # Remove ItemId and ChangeKey. We get them unconditionally
- additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}
+ additional_fields = set()
+ for field_name in only_fields:
+ folder.validate_field(field=field_name, version=self.account.version)
+ f = folder.get_field_by_fieldname(fieldname=field_name)
+ if not f.is_attribute:
+ # Remove ItemId and ChangeKey. We get them unconditionally
+ additional_fields.add(FieldPath(field=f))
# Add required fields
additional_fields.update(
@@ -1047,19 +1146,6 @@ Instance variables
Methods
-
-def all(self)
-
--
-
-
-
-Expand source code
-
-def all(self):
- return QuerySet(self).all()
-
-
def allowed_item_fields(self)
@@ -1077,19 +1163,6 @@ Methods
return fields
-
-def exclude(self, *args, **kwargs)
-
--
-
-
-
-Expand source code
-
-def exclude(self, *args, **kwargs):
- return QuerySet(self).exclude(*args, **kwargs)
-
-
def filter(self, *args, **kwargs)
@@ -1156,6 +1229,7 @@ Examples
@require_account
def find_folders(self, q=None, shape=ID_ONLY, depth=None, additional_fields=None, page_size=None, max_items=None,
offset=0):
+ from ..services import FindFolder
# 'depth' controls whether to return direct children or recurse into sub-folders
from .base import BaseFolder, Folder
if q is None:
@@ -1170,26 +1244,22 @@ Examples
restriction = None
else:
restriction = Restriction(q, folders=self.folders, applies_to=Restriction.FOLDERS)
- if shape not in SHAPE_CHOICES:
- raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
if depth is None:
depth = self._get_default_folder_traversal_depth()
- if depth not in FOLDER_TRAVERSAL_CHOICES:
- raise ValueError("'depth' %s must be one of %s" % (depth, FOLDER_TRAVERSAL_CHOICES))
if additional_fields is None:
# Default to all non-complex properties. Subfolders will always be of class Folder
additional_fields = self.get_folder_fields(target_cls=Folder, is_complex=False)
else:
for f in additional_fields:
if f.field.is_complex:
- raise ValueError("find_folders() does not support field '%s'. Use get_folders()." % f.field.name)
+ raise ValueError(f"find_folders() does not support field {f.field.name!r}. Use get_folders().")
# Add required fields
additional_fields.update(
(FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
)
- yield from FindFolder(account=self.account, chunk_size=page_size).call(
+ yield from FindFolder(account=self.account, page_size=page_size).call(
folders=self.folders,
additional_fields=additional_fields,
restriction=restriction,
@@ -1206,7 +1276,7 @@ Examples
-
Private method to call the FindItem service.
:param q: a Q instance containing any restrictions
-:param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is
+:param shape: controls whether to return (id, changekey) tuples or Item objects. If additional_fields is
non-null, we always return Item objects. (Default value = ID_ONLY)
:param depth: controls the whether to return soft-deleted items or not. (Default value = None)
:param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware
@@ -1226,7 +1296,7 @@
Examples
"""Private method to call the FindItem service.
:param q: a Q instance containing any restrictions
- :param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is
+ :param shape: controls whether to return (id, changekey) tuples or Item objects. If additional_fields is
non-null, we always return Item objects. (Default value = ID_ONLY)
:param depth: controls the whether to return soft-deleted items or not. (Default value = None)
:param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware
@@ -1239,36 +1309,20 @@ Examples
:return: a generator for the returned item IDs or items
"""
+ from ..services import FindItem
if not self.folders:
log.debug('Folder list is empty')
return
if q.is_never():
log.debug('Query will never return results')
return
- if shape not in SHAPE_CHOICES:
- raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
- if depth is None:
- depth = self._get_default_item_traversal_depth()
- if depth not in ITEM_TRAVERSAL_CHOICES:
- raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
- if additional_fields:
- for f in additional_fields:
- self.validate_item_field(field=f, version=self.account.version)
- if f.field.is_complex:
- raise ValueError("find_items() does not support field '%s'. Use fetch() instead" % f.field.name)
+ depth, restriction, query_string = self._rinse_args(
+ q=q, depth=depth, additional_fields=additional_fields,
+ field_validator=self.validate_item_field
+ )
if calendar_view is not None and not isinstance(calendar_view, CalendarView):
- raise ValueError("'calendar_view' %s must be a CalendarView instance" % calendar_view)
+ raise InvalidTypeError('calendar_view', calendar_view, CalendarView)
- # Build up any restrictions
- if q.is_empty():
- restriction = None
- query_string = None
- elif q.query_string:
- restriction = None
- query_string = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
- else:
- restriction = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
- query_string = None
log.debug(
'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
self.account,
@@ -1278,7 +1332,7 @@ Examples
additional_fields,
restriction.q if restriction else None,
)
- yield from FindItem(account=self.account, chunk_size=page_size).call(
+ yield from FindItem(account=self.account, page_size=page_size).call(
folders=self.folders,
additional_fields=additional_fields,
restriction=restriction,
@@ -1298,7 +1352,7 @@ Examples
-
Private method to call the FindPeople service.
:param q: a Q instance containing any restrictions
-:param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is
+:param shape: controls whether to return (id, changekey) tuples or Persona objects. If additional_fields is
non-null, we always return Persona objects. (Default value = ID_ONLY)
:param depth: controls the whether to return soft-deleted items or not. (Default value = None)
:param additional_fields: the extra properties we want on the return objects. Default is no properties.
@@ -1316,7 +1370,7 @@
Examples
"""Private method to call the FindPeople service.
:param q: a Q instance containing any restrictions
- :param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is
+ :param shape: controls whether to return (id, changekey) tuples or Persona objects. If additional_fields is
non-null, we always return Persona objects. (Default value = ID_ONLY)
:param depth: controls the whether to return soft-deleted items or not. (Default value = None)
:param additional_fields: the extra properties we want on the return objects. Default is no properties.
@@ -1327,35 +1381,17 @@ Examples
:return: a generator for the returned personas
"""
+ from ..services import FindPeople
folder = self._get_single_folder()
- if not folder:
- return
if q.is_never():
log.debug('Query will never return results')
return
- if shape not in SHAPE_CHOICES:
- raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
- if depth is None:
- depth = self._get_default_item_traversal_depth()
- if depth not in ITEM_TRAVERSAL_CHOICES:
- raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
- if additional_fields:
- for f in additional_fields:
- Persona.validate_field(field=f, version=self.account.version)
- if f.field.is_complex:
- raise ValueError("find_people() does not support field '%s'" % f.field.name)
+ depth, restriction, query_string = self._rinse_args(
+ q=q, depth=depth, additional_fields=additional_fields,
+ field_validator=Persona.validate_field
+ )
- # Build up any restrictions
- if q.is_empty():
- restriction = None
- query_string = None
- elif q.query_string:
- restriction = None
- query_string = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
- else:
- restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
- query_string = None
- yield from FindPeople(account=self.account, chunk_size=page_size).call(
+ yield from FindPeople(account=self.account, page_size=page_size).call(
folder=folder,
additional_fields=additional_fields,
restriction=restriction,
@@ -1368,19 +1404,6 @@ Examples
)
-
-def get(self, *args, **kwargs)
-
--
-
-
-
-Expand source code
-
-def get(self, *args, **kwargs):
- return QuerySet(self).get(*args, **kwargs)
-
-
def get_folder_fields(self, target_cls, is_complex=None)
@@ -1407,6 +1430,7 @@ Examples
Expand source code
def get_folders(self, additional_fields=None):
+ from ..services import GetFolder
# Expand folders with their full set of properties
from .base import BaseFolder
if not self.folders:
@@ -1428,8 +1452,8 @@ Examples
)
-
-def none(self)
+
+def pull_subscription(self, **kwargs)
-
@@ -1437,12 +1461,12 @@
Examples
Expand source code
-def none(self):
- return QuerySet(self).none()
+def pull_subscription(self, **kwargs):
+ return PullSubscription(folder=self, **kwargs)
-
-def people(self)
+
+def push_subscription(self, **kwargs)
-
@@ -1450,8 +1474,8 @@
Examples
Expand source code
-def people(self):
- return QuerySet(self).people()
+def push_subscription(self, **kwargs):
+ return PushSubscription(folder=self, **kwargs)
@@ -1480,8 +1504,21 @@ Examples
)
+
+def streaming_subscription(self, **kwargs)
+
+-
+
+
+
+Expand source code
+
+def streaming_subscription(self, **kwargs):
+ return StreamingSubscription(folder=self, **kwargs)
+
+
-def subscribe_to_pull(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), watermark=None, timeout=60)
+def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60)
-
@@ -1489,17 +1526,20 @@
Examples
Expand source code
-def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
+def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60):
+ from ..services import SubscribeToPull
if not self.folders:
log.debug('Folder list is empty')
- return
- yield from SubscribeToPull(account=self.account).call(
+ return None
+ if event_types is None:
+ event_types = SubscribeToPull.EVENT_TYPES
+ return SubscribeToPull(account=self.account).get(
folders=self.folders, event_types=event_types, watermark=watermark, timeout=timeout,
)
-def subscribe_to_push(self, callback_url, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), watermark=None, status_frequency=1)
+def subscribe_to_push(self, callback_url, event_types=None, watermark=None, status_frequency=1)
-
@@ -1507,19 +1547,21 @@
Examples
Expand source code
-def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
- status_frequency=1):
+def subscribe_to_push(self, callback_url, event_types=None, watermark=None, status_frequency=1):
+ from ..services import SubscribeToPush
if not self.folders:
log.debug('Folder list is empty')
- return
- yield from SubscribeToPush(account=self.account).call(
+ return None
+ if event_types is None:
+ event_types = SubscribeToPush.EVENT_TYPES
+ return SubscribeToPush(account=self.account).get(
folders=self.folders, event_types=event_types, watermark=watermark, status_frequency=status_frequency,
url=callback_url,
)
-def subscribe_to_streaming(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'))
+def subscribe_to_streaming(self, event_types=None)
-
@@ -1527,11 +1569,14 @@
Examples
Expand source code
-def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
+def subscribe_to_streaming(self, event_types=None):
+ from ..services import SubscribeToStreaming
if not self.folders:
log.debug('Folder list is empty')
- return
- yield from SubscribeToStreaming(account=self.account).call(folders=self.folders, event_types=event_types)
+ return None
+ if event_types is None:
+ event_types = SubscribeToStreaming.EVENT_TYPES
+ return SubscribeToStreaming(account=self.account).get(folders=self.folders, event_types=event_types)
@@ -1544,17 +1589,19 @@ Examples
Expand source code
def sync_hierarchy(self, sync_state=None, only_fields=None):
+ from ..services import SyncFolderHierarchy
folder = self._get_single_folder()
- if not folder:
- return
if only_fields is None:
# We didn't restrict list of field paths. Get all fields from the server, including extended properties.
additional_fields = {FieldPath(field=f) for f in folder.supported_fields(version=self.account.version)}
else:
- for f in only_fields:
- folder.validate_field(field=f, version=self.account.version)
- # Remove ItemId and ChangeKey. We get them unconditionally
- additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}
+ additional_fields = set()
+ for field_name in only_fields:
+ folder.validate_field(field=field_name, version=self.account.version)
+ f = folder.get_field_by_fieldname(fieldname=field_name)
+ if not f.is_attribute:
+ # Remove ItemId and ChangeKey. We get them unconditionally
+ additional_fields.add(FieldPath(field=f))
# Add required fields
additional_fields.update(
@@ -1588,9 +1635,8 @@ Examples
Expand source code
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
+ from ..services import SyncFolderItems
folder = self._get_single_folder()
- if not folder:
- return
if only_fields is None:
# We didn't restrict list of field paths. Get all fields from the server, including extended properties.
additional_fields = {FieldPath(field=f) for f in folder.allowed_item_fields(version=self.account.version)}
@@ -1620,6 +1666,32 @@ Examples
raise SyncCompleted(sync_state=svc.sync_state)
+
+def unsubscribe(self, subscription_id)
+
+-
+
Unsubscribe. Only applies to pull and streaming notifications.
+:param subscription_id: A subscription ID as acquired by .subscribe_to_pull|streaming
+:return: True
+This method doesn't need the current collection instance, but it makes sense to keep the method along the other
+sync methods.
+
+
+Expand source code
+
+def unsubscribe(self, subscription_id):
+ """Unsubscribe. Only applies to pull and streaming notifications.
+
+ :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|streaming]()
+ :return: True
+
+ This method doesn't need the current collection instance, but it makes sense to keep the method along the other
+ sync methods.
+ """
+ from ..services import Unsubscribe
+ return Unsubscribe(account=self.account).get(subscription_id=subscription_id)
+
+
def validate_item_field(self, field, version)
@@ -1630,7 +1702,8 @@ Examples
Expand source code
def validate_item_field(self, field, version):
- # For each field, check if the field is valid for any of the item models supported by this folder
+ # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
+ # for the item types supported by this folder collection.
for item_model in self.supported_item_models:
try:
item_model.validate_field(field=field, version=version)
@@ -1638,7 +1711,7 @@ Examples
except InvalidField:
continue
else:
- raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))
+ raise InvalidField(f"{field!r} is not a valid field on {self.supported_item_models}")
@@ -1686,6 +1759,82 @@ Examples
+Inherited members
+
+
+
+class PullSubscription
+(folder, **subscription_kwargs)
+
+-
+
+
+
+Expand source code
+
+class PullSubscription(BaseSubscription):
+ def __enter__(self):
+ self.subscription_id, watermark = self.folder.subscribe_to_pull(**self.subscription_kwargs)
+ return self.subscription_id, watermark
+
+Ancestors
+
+
+
+class PushSubscription
+(folder, **subscription_kwargs)
+
+-
+
+
+
+Expand source code
+
+class PushSubscription(BaseSubscription):
+ def __enter__(self):
+ self.subscription_id, watermark = self.folder.subscribe_to_push(**self.subscription_kwargs)
+ return self.subscription_id, watermark
+
+ def __exit__(self, *args, **kwargs):
+ # Cannot unsubscribe to push subscriptions
+ pass
+
+Ancestors
+
+
+
+class StreamingSubscription
+(folder, **subscription_kwargs)
+
+-
+
+
+
+Expand source code
+
+class StreamingSubscription(BaseSubscription):
+ def __enter__(self):
+ self.subscription_id = self.folder.subscribe_to_streaming(**self.subscription_kwargs)
+ return self.subscription_id
+
+Ancestors
+
class SyncCompleted
@@ -1727,34 +1876,45 @@ Index
Classes
-
+
BaseSubscription
+
+-
FolderCollection
REQUIRED_FOLDER_FIELDS
-all
allowed_item_fields
-exclude
filter
find_folders
find_items
find_people
folders
-get
get_folder_fields
get_folders
-none
-people
+pull_subscription
+push_subscription
resolve
+streaming_subscription
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
supported_item_models
sync_hierarchy
sync_items
+unsubscribe
validate_item_field
view
-
+
PullSubscription
+
+-
+
PushSubscription
+
+-
+
StreamingSubscription
+
+-
SyncCompleted
diff --git a/docs/exchangelib/folders/index.html b/docs/exchangelib/folders/index.html
index b59eb683..98d925b3 100644
--- a/docs/exchangelib/folders/index.html
+++ b/docs/exchangelib/folders/index.html
@@ -104,7 +104,7 @@ Classes
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -144,17 +144,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -213,17 +222,26 @@ Inherited members
Contacts
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -281,17 +299,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -311,7 +338,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -346,17 +373,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -376,7 +412,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -411,17 +447,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -441,7 +486,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -476,17 +521,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -506,7 +560,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -541,17 +595,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -571,7 +634,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -606,17 +669,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -636,7 +708,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -671,17 +743,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -701,7 +782,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -736,17 +817,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -807,19 +897,28 @@ Inherited members
RootOfHierarchy
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_cls_from_folder_name
folder_sync_state
+get
get_default_folder
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -876,17 +975,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -956,17 +1064,17 @@ Inherited members
@property
@abc.abstractmethod
def account(self):
- pass
+ """Return the account this folder belongs to"""
@property
@abc.abstractmethod
def root(self):
- pass
+ """Return the root folder this folder belongs to"""
@property
@abc.abstractmethod
def parent(self):
- pass
+ """Return the parent folder of this folder"""
@property
def is_deletable(self):
@@ -995,7 +1103,7 @@ Inherited members
@property
def absolute(self):
- return ''.join('/%s' % p.name for p in self.parts)
+ return ''.join(f'/{p.name}' for p in self.parts)
def _walk(self):
for c in self.children:
@@ -1049,29 +1157,27 @@ Inherited members
├── exchangelib issues
└── Mom
"""
- tree = '%s\n' % self.name
+ tree = f'{self.name}\n'
children = list(self.children)
for i, c in enumerate(sorted(children, key=attrgetter('name')), start=1):
nodes = c.tree().split('\n')
for j, node in enumerate(nodes, start=1):
if i != len(children) and j == 1:
# Not the last child, but the first node, which is the name of the child
- tree += '├── %s\n' % node
+ tree += f'├── {node}\n'
elif i != len(children) and j > 1:
# Not the last child, and not name of child
- tree += '│ %s\n' % node
+ tree += f'│ {node}\n'
elif i == len(children) and j == 1:
# Not the last child, but the first node, which is the name of the child
- tree += '└── %s\n' % node
+ tree += f'└── {node}\n'
else: # Last child, and not name of child
- tree += ' %s\n' % node
+ tree += f' {node}\n'
return tree.strip()
@classmethod
def supports_version(cls, version):
# 'version' is a Version instance, for convenience by callers
- if not isinstance(version, Version):
- raise ValueError("'version' %r must be a Version instance" % version)
if not cls.supported_from:
return True
return version.build >= cls.supported_from
@@ -1108,31 +1214,18 @@ Inherited members
try:
return cls.ITEM_MODEL_MAP[tag]
except KeyError:
- raise ValueError('Item type %s was unexpected in a %s folder' % (tag, cls.__name__))
+ raise ValueError(f'Item type {tag} was unexpected in a {cls.__name__} folder')
@classmethod
def allowed_item_fields(cls, version):
# Return non-ID fields of all item classes allowed in this folder type
fields = set()
for item_model in cls.supported_item_models:
- fields.update(
- set(item_model.supported_fields(version=version))
- )
+ fields.update(set(item_model.supported_fields(version=version)))
return fields
def validate_item_field(self, field, version):
- # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
- # for the item types supported by this folder.
-
- # For each field, check if the field is valid for any of the item models supported by this folder
- for item_model in self.supported_item_models:
- try:
- item_model.validate_field(field=field, version=version)
- break
- except InvalidField:
- continue
- else:
- raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))
+ FolderCollection(account=self.account, folders=[self]).validate_item_field(field=field, version=version)
def normalize_fields(self, fields):
# Takes a list of fieldnames, Field or FieldPath objects pointing to item fields. Turns them into FieldPath
@@ -1147,8 +1240,6 @@ Inherited members
elif isinstance(field_path, Field):
field_path = FieldPath(field=field_path)
fields[i] = field_path
- if not isinstance(field_path, FieldPath):
- raise ValueError("Field %r must be a string or FieldPath instance" % field_path)
if field_path.field.name == 'start':
has_start = True
elif field_path.field.name == 'end':
@@ -1174,7 +1265,7 @@ Inherited members
return item_model.get_field_by_fieldname(fieldname)
except InvalidField:
pass
- raise InvalidField("%r is not a valid field name on %s" % (fieldname, cls.supported_item_models))
+ raise InvalidField(f"{fieldname!r} is not a valid field name on {cls.supported_item_models}")
def get(self, *args, **kwargs):
return FolderCollection(account=self.account, folders=[self]).get(*args, **kwargs)
@@ -1199,6 +1290,7 @@ Inherited members
return self.account.bulk_create(folder=self, items=items, *args, **kwargs)
def save(self, update_fields=None):
+ from ..services import CreateFolder, UpdateFolder
if self.id is None:
# New folder
if update_fields:
@@ -1232,6 +1324,7 @@ Inherited members
return self
def move(self, to_folder):
+ from ..services import MoveFolder
res = MoveFolder(account=self.account).get(folders=[self], to_folder=to_folder)
folder_id, changekey = res.id, res.changekey
if self.id != folder_id:
@@ -1242,15 +1335,13 @@ Inherited members
self.root.update_folder(self) # Update the folder in the cache
def delete(self, delete_type=HARD_DELETE):
- if delete_type not in DELETE_TYPE_CHOICES:
- raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
+ from ..services import DeleteFolder
DeleteFolder(account=self.account).get(folders=[self], delete_type=delete_type)
self.root.remove_folder(self) # Remove the updated folder from the cache
self._id = None
def empty(self, delete_type=HARD_DELETE, delete_sub_folders=False):
- if delete_type not in DELETE_TYPE_CHOICES:
- raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
+ from ..services import EmptyFolder
EmptyFolder(account=self.account).get(
folders=[self], delete_type=delete_type, delete_sub_folders=delete_sub_folders
)
@@ -1258,14 +1349,14 @@ Inherited members
# We don't know exactly what was deleted, so invalidate the entire folder cache to be safe
self.root.clear_cache()
- def wipe(self, page_size=None, _seen=None, _level=0):
+ def wipe(self, page_size=None, chunk_size=None, _seen=None, _level=0):
# Recursively deletes all items in this folder, and all subfolders and their content. Attempts to protect
# distinguished folders from being deleted. Use with caution!
_seen = _seen or set()
if self.id in _seen:
- raise RecursionError('We already tried to wipe %s' % self)
+ raise RecursionError(f'We already tried to wipe {self}')
if _level > 16:
- raise RecursionError('Max recursion level reached: %s' % _level)
+ raise RecursionError(f'Max recursion level reached: {_level}')
_seen.add(self.id)
log.warning('Wiping %s', self)
has_distinguished_subfolders = any(f.is_distinguished for f in self.children)
@@ -1281,13 +1372,18 @@ Inherited members
self.empty(delete_sub_folders=False)
except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
log.warning('Not allowed to empty %s. Trying to delete items instead', self)
+ kwargs = {}
+ if page_size is not None:
+ kwargs['page_size'] = page_size
+ if chunk_size is not None:
+ kwargs['chunk_size'] = chunk_size
try:
- self.all().delete(**dict(page_size=page_size) if page_size else {})
+ self.all().delete(**kwargs)
except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound):
log.warning('Not allowed to delete items in %s', self)
_level += 1
for f in self.children:
- f.wipe(page_size=page_size, _seen=_seen, _level=_level)
+ f.wipe(page_size=page_size, chunk_size=chunk_size, _seen=_seen, _level=_level)
# Remove non-distinguished children that are empty and have no subfolders
if f.is_deletable and not f.children:
log.warning('Deleting folder %s', f)
@@ -1314,7 +1410,7 @@ Inherited members
kwargs['name'] = cls.DISTINGUISHED_FOLDER_ID
return kwargs
- def to_folder_id(self):
+ def to_id(self):
if self.is_distinguished:
# Don't add the changekey here. When modifying folder content, we usually don't care if others have changed
# the folder content since we fetched the changekey.
@@ -1328,29 +1424,19 @@ Inherited members
return FolderId(id=self.id, changekey=self.changekey)
raise ValueError('Must be a distinguished folder or have an ID')
- def to_xml(self, version):
- try:
- return self.to_folder_id().to_xml(version=version)
- except ValueError:
- return super().to_xml(version=version)
-
- def to_id_xml(self, version):
- # Folder(name='Foo') is a perfectly valid ID to e.g. create a folder
- return self.to_xml(version=version)
-
@classmethod
def resolve(cls, account, folder):
# Resolve a single folder
folders = list(FolderCollection(account=account, folders=[folder]).resolve())
if not folders:
- raise ErrorFolderNotFound('Could not find folder %r' % folder)
+ raise ErrorFolderNotFound(f'Could not find folder {folder!r}')
if len(folders) != 1:
- raise ValueError('Expected result length 1, but got %s' % folders)
+ raise ValueError(f'Expected result length 1, but got {folders}')
f = folders[0]
if isinstance(f, Exception):
raise f
if f.__class__ != cls:
- raise ValueError("Expected folder %r to be a %s instance" % (f, cls))
+ raise ValueError(f"Expected folder {f!r} to be a {cls} instance")
return f
@require_id
@@ -1364,7 +1450,11 @@ Inherited members
return self
@require_id
- def get_user_configuration(self, name, properties=ALL):
+ def get_user_configuration(self, name, properties=None):
+ from ..services import GetUserConfiguration
+ from ..services.get_user_configuration import ALL
+ if properties is None:
+ properties = ALL
return GetUserConfiguration(account=self.account).get(
user_configuration_name=UserConfigurationNameMNS(name=name, folder=self),
properties=properties,
@@ -1372,6 +1462,7 @@ Inherited members
@require_id
def create_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
+ from ..services import CreateUserConfiguration
user_configuration = UserConfiguration(
user_configuration_name=UserConfigurationName(name=name, folder=self),
dictionary=dictionary,
@@ -1382,6 +1473,7 @@ Inherited members
@require_id
def update_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
+ from ..services import UpdateUserConfiguration
user_configuration = UserConfiguration(
user_configuration_name=UserConfigurationName(name=name, folder=self),
dictionary=dictionary,
@@ -1392,12 +1484,13 @@ Inherited members
@require_id
def delete_user_configuration(self, name):
+ from ..services import DeleteUserConfiguration
return DeleteUserConfiguration(account=self.account).get(
user_configuration_name=UserConfigurationNameMNS(name=name, folder=self)
)
@require_id
- def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
+ def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60):
"""Create a pull subscription.
:param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPull.EVENT_TYPES
@@ -1406,19 +1499,15 @@ Inherited members
GetEvents request for this subscription.
:return: The subscription ID and a watermark
"""
- s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_pull(
+ from ..services import SubscribeToPull
+ if event_types is None:
+ event_types = SubscribeToPull.EVENT_TYPES
+ return FolderCollection(account=self.account, folders=[self]).subscribe_to_pull(
event_types=event_types, watermark=watermark, timeout=timeout,
- ))
- if len(s_ids) != 1:
- raise ValueError('Expected result length 1, but got %s' % s_ids)
- s_id = s_ids[0]
- if isinstance(s_id, Exception):
- raise s_id
- return s_id
+ )
@require_id
- def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
- status_frequency=1):
+ def subscribe_to_push(self, callback_url, event_types=None, watermark=None, status_frequency=1):
"""Create a push subscription.
:param callback_url: A client-defined URL that the server will call
@@ -1427,32 +1516,24 @@ Inherited members
:param status_frequency: The frequency, in minutes, that the callback URL will be called with.
:return: The subscription ID and a watermark
"""
- s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_push(
+ from ..services import SubscribeToPush
+ if event_types is None:
+ event_types = SubscribeToPush.EVENT_TYPES
+ return FolderCollection(account=self.account, folders=[self]).subscribe_to_push(
event_types=event_types, watermark=watermark, status_frequency=status_frequency, callback_url=callback_url,
- ))
- if len(s_ids) != 1:
- raise ValueError('Expected result length 1, but got %s' % s_ids)
- s_id = s_ids[0]
- if isinstance(s_id, Exception):
- raise s_id
- return s_id
+ )
@require_id
- def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
+ def subscribe_to_streaming(self, event_types=None):
"""Create a streaming subscription.
:param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES
:return: The subscription ID
"""
- s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming(
- event_types=event_types,
- ))
- if len(s_ids) != 1:
- raise ValueError('Expected result length 1, but got %s' % s_ids)
- s_id = s_ids[0]
- if isinstance(s_id, Exception):
- raise s_id
- return s_id
+ from ..services import SubscribeToStreaming
+ if event_types is None:
+ event_types = SubscribeToStreaming.EVENT_TYPES
+ return FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming(event_types=event_types)
@require_id
def pull_subscription(self, **kwargs):
@@ -1475,18 +1556,19 @@ Inherited members
This method doesn't need the current folder instance, but it makes sense to keep the method along the other
sync methods.
"""
+ from ..services import Unsubscribe
return Unsubscribe(account=self.account).get(subscription_id=subscription_id)
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
"""Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after
this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.
- :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
+ :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderItems service.
:param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
:param ignore: A list of Item IDs to ignore in the sync
:param max_changes_returned: The max number of change
:param sync_scope: Specify whether to return just items, or items and folder associated information. Possible
- values are specified in SyncFolderitems.SYNC_SCOPES
+ values are specified in SyncFolderItems.SYNC_SCOPES
:return: A generator of (change_type, item) tuples
"""
if not sync_state:
@@ -1508,7 +1590,7 @@ Inherited members
changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new
sync state.
- :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
+ :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderItems service.
:param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
:return:
"""
@@ -1533,6 +1615,7 @@ Inherited members
This method doesn't need the current folder instance, but it makes sense to keep the method along the other
sync methods.
"""
+ from ..services import GetEvents
svc = GetEvents(account=self.account)
while True:
notification = svc.get(subscription_id=subscription_id, watermark=watermark)
@@ -1553,9 +1636,8 @@ Inherited members
This method doesn't need the current folder instance, but it makes sense to keep the method along the other
sync methods.
"""
- # Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
- request_timeout = connection_timeout*60 + 60
- svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
+ from ..services import GetStreamingEvents
+ svc = GetStreamingEvents(account=self.account)
subscription_ids = subscription_id_or_ids if is_iterable(subscription_id_or_ids, generators_allowed=True) \
else [subscription_id_or_ids]
for i, notification in enumerate(
@@ -1587,7 +1669,7 @@ Inherited members
try:
return SingleFolderQuerySet(account=self.account, folder=self).depth(SHALLOW_FOLDERS).get(name=other)
except DoesNotExist:
- raise ErrorFolderNotFound("No subfolder with name '%s'" % other)
+ raise ErrorFolderNotFound(f"No subfolder with name {other!r}")
def __truediv__(self, other):
"""Support the some_folder / 'child_folder' / 'child_of_child_folder' navigation syntax."""
@@ -1600,7 +1682,7 @@ Inherited members
for c in self.children:
if c.name == other:
return c
- raise ErrorFolderNotFound("No subfolder with name '%s'" % other)
+ raise ErrorFolderNotFound(f"No subfolder with name {other!r}")
def __repr__(self):
return self.__class__.__name__ + \
@@ -1608,7 +1690,7 @@ Inherited members
self.folder_class, self.id, self.changekey))
def __str__(self):
- return '%s (%s)' % (self.__class__.__name__, self.name)
+ return f'{self.__class__.__name__} ({self.name})'
Ancestors
@@ -1697,9 +1779,7 @@ Static methods
# Return non-ID fields of all item classes allowed in this folder type
fields = set()
for item_model in cls.supported_item_models:
- fields.update(
- set(item_model.supported_fields(version=version))
- )
+ fields.update(set(item_model.supported_fields(version=version)))
return fields
@@ -1749,7 +1829,7 @@ Static methods
return item_model.get_field_by_fieldname(fieldname)
except InvalidField:
pass
- raise InvalidField("%r is not a valid field name on %s" % (fieldname, cls.supported_item_models))
+ raise InvalidField(f"{fieldname!r} is not a valid field name on {cls.supported_item_models}")
@@ -1766,7 +1846,7 @@ Static methods
try:
return cls.ITEM_MODEL_MAP[tag]
except KeyError:
- raise ValueError('Item type %s was unexpected in a %s folder' % (tag, cls.__name__))
+ raise ValueError(f'Item type {tag} was unexpected in a {cls.__name__} folder')
@@ -1799,14 +1879,14 @@ Static methods
# Resolve a single folder
folders = list(FolderCollection(account=account, folders=[folder]).resolve())
if not folders:
- raise ErrorFolderNotFound('Could not find folder %r' % folder)
+ raise ErrorFolderNotFound(f'Could not find folder {folder!r}')
if len(folders) != 1:
- raise ValueError('Expected result length 1, but got %s' % folders)
+ raise ValueError(f'Expected result length 1, but got {folders}')
f = folders[0]
if isinstance(f, Exception):
raise f
if f.__class__ != cls:
- raise ValueError("Expected folder %r to be a %s instance" % (f, cls))
+ raise ValueError(f"Expected folder {f!r} to be a {cls} instance")
return f
@@ -1822,8 +1902,6 @@ Static methods
@classmethod
def supports_version(cls, version):
# 'version' is a Version instance, for convenience by callers
- if not isinstance(version, Version):
- raise ValueError("'version' %r must be a Version instance" % version)
if not cls.supported_from:
return True
return version.build >= cls.supported_from
@@ -1841,12 +1919,12 @@ Instance variables
@property
def absolute(self):
- return ''.join('/%s' % p.name for p in self.parts)
+ return ''.join(f'/{p.name}' for p in self.parts)
var account
-
-
+
Return the account this folder belongs to
Expand source code
@@ -1854,7 +1932,7 @@ Instance variables
@property
@abc.abstractmethod
def account(self):
- pass
+ """Return the account this folder belongs to"""
var child_folder_count
@@ -1921,7 +1999,7 @@ Instance variables
var parent
-
-
+
Return the parent folder of this folder
Expand source code
@@ -1929,7 +2007,7 @@ Instance variables
@property
@abc.abstractmethod
def parent(self):
- pass
+ """Return the parent folder of this folder"""
var parent_folder_id
@@ -1955,7 +2033,7 @@ Instance variables
var root
-
-
+
Return the root folder this folder belongs to
Expand source code
@@ -1963,7 +2041,7 @@ Instance variables
@property
@abc.abstractmethod
def root(self):
- pass
+ """Return the root folder this folder belongs to"""
var total_count
@@ -1977,19 +2055,6 @@ Instance variables
Methods
-
-def all(self)
-
--
-
-
-
-Expand source code
-
-def all(self):
- return FolderCollection(account=self.account, folders=[self]).all()
-
-
def bulk_create(self, items, *args, **kwargs)
@@ -2030,6 +2095,7 @@ Methods
@require_id
def create_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
+ from ..services import CreateUserConfiguration
user_configuration = UserConfiguration(
user_configuration_name=UserConfigurationName(name=name, folder=self),
dictionary=dictionary,
@@ -2049,8 +2115,7 @@ Methods
Expand source code
def delete(self, delete_type=HARD_DELETE):
- if delete_type not in DELETE_TYPE_CHOICES:
- raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
+ from ..services import DeleteFolder
DeleteFolder(account=self.account).get(folders=[self], delete_type=delete_type)
self.root.remove_folder(self) # Remove the updated folder from the cache
self._id = None
@@ -2067,6 +2132,7 @@ Methods
@require_id
def delete_user_configuration(self, name):
+ from ..services import DeleteUserConfiguration
return DeleteUserConfiguration(account=self.account).get(
user_configuration_name=UserConfigurationNameMNS(name=name, folder=self)
)
@@ -2082,8 +2148,7 @@ Methods
Expand source code
def empty(self, delete_type=HARD_DELETE, delete_sub_folders=False):
- if delete_type not in DELETE_TYPE_CHOICES:
- raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
+ from ..services import EmptyFolder
EmptyFolder(account=self.account).get(
folders=[self], delete_type=delete_type, delete_sub_folders=delete_sub_folders
)
@@ -2092,45 +2157,6 @@ Methods
self.root.clear_cache()
-
-def exclude(self, *args, **kwargs)
-
--
-
-
-
-Expand source code
-
-def exclude(self, *args, **kwargs):
- return FolderCollection(account=self.account, folders=[self]).exclude(*args, **kwargs)
-
-
-
-def filter(self, *args, **kwargs)
-
--
-
-
-
-Expand source code
-
-def filter(self, *args, **kwargs):
- return FolderCollection(account=self.account, folders=[self]).filter(*args, **kwargs)
-
-
-
-def get(self, *args, **kwargs)
-
--
-
-
-
-Expand source code
-
-def get(self, *args, **kwargs):
- return FolderCollection(account=self.account, folders=[self]).get(*args, **kwargs)
-
-
def get_events(self, subscription_id, watermark)
@@ -2155,6 +2181,7 @@ Methods
This method doesn't need the current folder instance, but it makes sense to keep the method along the other
sync methods.
"""
+ from ..services import GetEvents
svc = GetEvents(account=self.account)
while True:
notification = svc.get(subscription_id=subscription_id, watermark=watermark)
@@ -2193,9 +2220,8 @@ Methods
This method doesn't need the current folder instance, but it makes sense to keep the method along the other
sync methods.
"""
- # Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
- request_timeout = connection_timeout*60 + 60
- svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
+ from ..services import GetStreamingEvents
+ svc = GetStreamingEvents(account=self.account)
subscription_ids = subscription_id_or_ids if is_iterable(subscription_id_or_ids, generators_allowed=True) \
else [subscription_id_or_ids]
for i, notification in enumerate(
@@ -2209,7 +2235,7 @@ Methods
-def get_user_configuration(self, name, properties='All')
+def get_user_configuration(self, name, properties=None)
-
@@ -2218,7 +2244,11 @@
Methods
Expand source code
@require_id
-def get_user_configuration(self, name, properties=ALL):
+def get_user_configuration(self, name, properties=None):
+ from ..services import GetUserConfiguration
+ from ..services.get_user_configuration import ALL
+ if properties is None:
+ properties = ALL
return GetUserConfiguration(account=self.account).get(
user_configuration_name=UserConfigurationNameMNS(name=name, folder=self),
properties=properties,
@@ -2248,6 +2278,7 @@ Methods
Expand source code
def move(self, to_folder):
+ from ..services import MoveFolder
res = MoveFolder(account=self.account).get(folders=[self], to_folder=to_folder)
folder_id, changekey = res.id, res.changekey
if self.id != folder_id:
@@ -2258,19 +2289,6 @@ Methods
self.root.update_folder(self) # Update the folder in the cache
-
-def none(self)
-
--
-
-
-
-Expand source code
-
-def none(self):
- return FolderCollection(account=self.account, folders=[self]).none()
-
-
def normalize_fields(self, fields)
@@ -2293,8 +2311,6 @@ Methods
elif isinstance(field_path, Field):
field_path = FieldPath(field=field_path)
fields[i] = field_path
- if not isinstance(field_path, FieldPath):
- raise ValueError("Field %r must be a string or FieldPath instance" % field_path)
if field_path.field.name == 'start':
has_start = True
elif field_path.field.name == 'end':
@@ -2314,20 +2330,6 @@ Methods
return fields
-
-def people(self)
-
--
-
-
-
-Expand source code
-
-def people(self):
- # No point in using a FolderCollection because FindPeople only supports one folder
- return FolderCollection(account=self.account, folders=[self]).people()
-
-
def pull_subscription(self, **kwargs)
@@ -2386,6 +2388,7 @@ Methods
Expand source code
def save(self, update_fields=None):
+ from ..services import CreateFolder, UpdateFolder
if self.id is None:
# New folder
if update_fields:
@@ -2434,7 +2437,7 @@ Methods
-def subscribe_to_pull(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), watermark=None, timeout=60)
+def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60)
-
Create a pull subscription.
@@ -2448,7 +2451,7 @@ Methods
Expand source code
@require_id
-def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
+def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60):
"""Create a pull subscription.
:param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPull.EVENT_TYPES
@@ -2457,19 +2460,16 @@ Methods
GetEvents request for this subscription.
:return: The subscription ID and a watermark
"""
- s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_pull(
+ from ..services import SubscribeToPull
+ if event_types is None:
+ event_types = SubscribeToPull.EVENT_TYPES
+ return FolderCollection(account=self.account, folders=[self]).subscribe_to_pull(
event_types=event_types, watermark=watermark, timeout=timeout,
- ))
- if len(s_ids) != 1:
- raise ValueError('Expected result length 1, but got %s' % s_ids)
- s_id = s_ids[0]
- if isinstance(s_id, Exception):
- raise s_id
- return s_id
+ )
-def subscribe_to_push(self, callback_url, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), watermark=None, status_frequency=1)
+def subscribe_to_push(self, callback_url, event_types=None, watermark=None, status_frequency=1)
-
Create a push subscription.
@@ -2483,8 +2483,7 @@ Methods
Expand source code
@require_id
-def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
- status_frequency=1):
+def subscribe_to_push(self, callback_url, event_types=None, watermark=None, status_frequency=1):
"""Create a push subscription.
:param callback_url: A client-defined URL that the server will call
@@ -2493,19 +2492,16 @@ Methods
:param status_frequency: The frequency, in minutes, that the callback URL will be called with.
:return: The subscription ID and a watermark
"""
- s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_push(
+ from ..services import SubscribeToPush
+ if event_types is None:
+ event_types = SubscribeToPush.EVENT_TYPES
+ return FolderCollection(account=self.account, folders=[self]).subscribe_to_push(
event_types=event_types, watermark=watermark, status_frequency=status_frequency, callback_url=callback_url,
- ))
- if len(s_ids) != 1:
- raise ValueError('Expected result length 1, but got %s' % s_ids)
- s_id = s_ids[0]
- if isinstance(s_id, Exception):
- raise s_id
- return s_id
+ )
-def subscribe_to_streaming(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'))
+def subscribe_to_streaming(self, event_types=None)
-
Create a streaming subscription.
@@ -2516,21 +2512,16 @@ Methods
Expand source code
@require_id
-def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
+def subscribe_to_streaming(self, event_types=None):
"""Create a streaming subscription.
:param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES
:return: The subscription ID
"""
- s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming(
- event_types=event_types,
- ))
- if len(s_ids) != 1:
- raise ValueError('Expected result length 1, but got %s' % s_ids)
- s_id = s_ids[0]
- if isinstance(s_id, Exception):
- raise s_id
- return s_id
+ from ..services import SubscribeToStreaming
+ if event_types is None:
+ event_types = SubscribeToStreaming.EVENT_TYPES
+ return FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming(event_types=event_types)
@@ -2540,7 +2531,7 @@ Methods
Return all folder changes to a folder hierarchy, as a generator. If sync_state is specified, get all folder
changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new
sync state.
-:param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
+
:param sync_state: The state of the sync. Returned by a successful call to the SyncFolderItems service.
:param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
:return:
@@ -2552,7 +2543,7 @@ Methods
changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new
sync state.
- :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
+ :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderItems service.
:param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
:return:
"""
@@ -2574,12 +2565,12 @@ Methods
-
Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after
this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.
-:param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
+
:param sync_state: The state of the sync. Returned by a successful call to the SyncFolderItems service.
:param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
:param ignore: A list of Item IDs to ignore in the sync
:param max_changes_returned: The max number of change
:param sync_scope: Specify whether to return just items, or items and folder associated information. Possible
-values are specified in SyncFolderitems.SYNC_SCOPES
+values are specified in SyncFolderItems.SYNC_SCOPES
:return: A generator of (change_type, item) tuples
@@ -2589,12 +2580,12 @@ Methods
"""Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after
this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.
- :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
+ :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderItems service.
:param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
:param ignore: A list of Item IDs to ignore in the sync
:param max_changes_returned: The max number of change
:param sync_scope: Specify whether to return just items, or items and folder associated information. Possible
- values are specified in SyncFolderitems.SYNC_SCOPES
+ values are specified in SyncFolderItems.SYNC_SCOPES
:return: A generator of (change_type, item) tuples
"""
if not sync_state:
@@ -2630,8 +2621,8 @@ Methods
return True
-
-def to_folder_id(self)
+
+def to_id(self)
-
@@ -2639,7 +2630,7 @@
Methods
Expand source code
-def to_folder_id(self):
+def to_id(self):
if self.is_distinguished:
# Don't add the changekey here. When modifying folder content, we usually don't care if others have changed
# the folder content since we fetched the changekey.
@@ -2654,36 +2645,6 @@ Methods
raise ValueError('Must be a distinguished folder or have an ID')
-
-def to_id_xml(self, version)
-
--
-
-
-
-Expand source code
-
-def to_id_xml(self, version):
- # Folder(name='Foo') is a perfectly valid ID to e.g. create a folder
- return self.to_xml(version=version)
-
-
-
-def to_xml(self, version)
-
--
-
-
-
-Expand source code
-
-def to_xml(self, version):
- try:
- return self.to_folder_id().to_xml(version=version)
- except ValueError:
- return super().to_xml(version=version)
-
-
def tree(self)
@@ -2712,22 +2673,22 @@ Methods
├── exchangelib issues
└── Mom
"""
- tree = '%s\n' % self.name
+ tree = f'{self.name}\n'
children = list(self.children)
for i, c in enumerate(sorted(children, key=attrgetter('name')), start=1):
nodes = c.tree().split('\n')
for j, node in enumerate(nodes, start=1):
if i != len(children) and j == 1:
# Not the last child, but the first node, which is the name of the child
- tree += '├── %s\n' % node
+ tree += f'├── {node}\n'
elif i != len(children) and j > 1:
# Not the last child, and not name of child
- tree += '│ %s\n' % node
+ tree += f'│ {node}\n'
elif i == len(children) and j == 1:
# Not the last child, but the first node, which is the name of the child
- tree += '└── %s\n' % node
+ tree += f'└── {node}\n'
else: # Last child, and not name of child
- tree += ' %s\n' % node
+ tree += f' {node}\n'
return tree.strip()
@@ -2753,6 +2714,7 @@ Methods
This method doesn't need the current folder instance, but it makes sense to keep the method along the other
sync methods.
"""
+ from ..services import Unsubscribe
return Unsubscribe(account=self.account).get(subscription_id=subscription_id)
@@ -2767,6 +2729,7 @@ Methods
@require_id
def update_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
+ from ..services import UpdateUserConfiguration
user_configuration = UserConfiguration(
user_configuration_name=UserConfigurationName(name=name, folder=self),
dictionary=dictionary,
@@ -2786,18 +2749,7 @@ Methods
Expand source code
def validate_item_field(self, field, version):
- # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
- # for the item types supported by this folder.
-
- # For each field, check if the field is valid for any of the item models supported by this folder
- for item_model in self.supported_item_models:
- try:
- item_model.validate_field(field=field, version=version)
- break
- except InvalidField:
- continue
- else:
- raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))
+ FolderCollection(account=self.account, folders=[self]).validate_item_field(field=field, version=version)
@@ -2814,7 +2766,7 @@ Methods
-def wipe(self, page_size=None)
+def wipe(self, page_size=None, chunk_size=None)
-
@@ -2822,14 +2774,14 @@
Methods
Expand source code
-def wipe(self, page_size=None, _seen=None, _level=0):
+def wipe(self, page_size=None, chunk_size=None, _seen=None, _level=0):
# Recursively deletes all items in this folder, and all subfolders and their content. Attempts to protect
# distinguished folders from being deleted. Use with caution!
_seen = _seen or set()
if self.id in _seen:
- raise RecursionError('We already tried to wipe %s' % self)
+ raise RecursionError(f'We already tried to wipe {self}')
if _level > 16:
- raise RecursionError('Max recursion level reached: %s' % _level)
+ raise RecursionError(f'Max recursion level reached: {_level}')
_seen.add(self.id)
log.warning('Wiping %s', self)
has_distinguished_subfolders = any(f.is_distinguished for f in self.children)
@@ -2845,13 +2797,18 @@ Methods
self.empty(delete_sub_folders=False)
except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
log.warning('Not allowed to empty %s. Trying to delete items instead', self)
+ kwargs = {}
+ if page_size is not None:
+ kwargs['page_size'] = page_size
+ if chunk_size is not None:
+ kwargs['chunk_size'] = chunk_size
try:
- self.all().delete(**dict(page_size=page_size) if page_size else {})
+ self.all().delete(**kwargs)
except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound):
log.warning('Not allowed to delete items in %s', self)
_level += 1
for f in self.children:
- f.wipe(page_size=page_size, _seen=_seen, _level=_level)
+ f.wipe(page_size=page_size, chunk_size=chunk_size, _seen=_seen, _level=_level)
# Remove non-distinguished children that are empty and have no subfolders
if f.is_deletable and not f.children:
log.warning('Deleting folder %s', f)
@@ -2874,6 +2831,16 @@ Inherited members
validate_field
+SearchableMixIn
:
+
+
@@ -2957,17 +2924,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3019,17 +2995,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3086,17 +3071,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3159,17 +3153,26 @@ Inherited members
Contacts
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3189,7 +3192,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -3224,17 +3227,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3320,17 +3332,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3350,7 +3371,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -3385,17 +3406,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3452,17 +3482,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3519,17 +3558,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3581,17 +3629,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3666,17 +3723,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3696,7 +3762,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -3731,17 +3797,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3888,17 +3963,26 @@ Inherited members
Messages
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3950,17 +4034,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3980,7 +4073,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -4020,17 +4113,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -4088,17 +4190,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -4185,7 +4296,7 @@ Inherited members
folder=cls(root=root, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
)
except MISSING_FOLDER_ERRORS:
- raise ErrorFolderNotFound('Could not find distinguished folder %r' % cls.DISTINGUISHED_FOLDER_ID)
+ raise ErrorFolderNotFound(f'Could not find distinguished folder {cls.DISTINGUISHED_FOLDER_ID!r}')
@property
def parent(self):
@@ -4202,7 +4313,7 @@ Inherited members
self.parent_folder_id = None
else:
if not isinstance(value, BaseFolder):
- raise ValueError("'value' %r must be a Folder instance" % value)
+ raise InvalidTypeError('value', value, BaseFolder)
self.root = value.root
self.parent_folder_id = ParentFolderId(id=value.id, changekey=value.changekey)
@@ -4210,7 +4321,7 @@ Inherited members
from .roots import RootOfHierarchy
super().clean(version=version)
if self.root and not isinstance(self.root, RootOfHierarchy):
- raise ValueError("'root' %r must be a RootOfHierarchy instance" % self.root)
+ raise InvalidTypeError('root', self.root, RootOfHierarchy)
@classmethod
def from_xml_with_root(cls, elem, root):
@@ -4381,63 +4492,20 @@ Static methods
folder=cls(root=root, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
)
except MISSING_FOLDER_ERRORS:
- raise ErrorFolderNotFound('Could not find distinguished folder %r' % cls.DISTINGUISHED_FOLDER_ID)
+ raise ErrorFolderNotFound(f'Could not find distinguished folder {cls.DISTINGUISHED_FOLDER_ID!r}')
Instance variables
-var account
--
-
-
-
-Expand source code
-
-@property
-def account(self):
- if self.root is None:
- return None
- return self.root.account
-
-
var effective_rights
-
-var parent
--
-
-
-
-Expand source code
-
-@property
-def parent(self):
- if not self.parent_folder_id:
- return None
- if self.parent_folder_id.id == self.id:
- # Some folders have a parent that references itself. Avoid circular references here
- return None
- return self.root.get_folder(self.parent_folder_id)
-
-
var permission_set
-
-var root
--
-
-
-
-Expand source code
-
-@property
-def root(self):
- return self._root
-
-
Methods
@@ -4454,7 +4522,7 @@ Methods
from .roots import RootOfHierarchy
super().clean(version=version)
if self.root and not isinstance(self.root, RootOfHierarchy):
- raise ValueError("'root' %r must be a RootOfHierarchy instance" % self.root)
+ raise InvalidTypeError('root', self.root, RootOfHierarchy)
@@ -4463,16 +4531,25 @@ Inherited members
BaseFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -4603,7 +4680,8 @@ Inherited members
return tuple(item_model for folder in self.folders for item_model in folder.supported_item_models)
def validate_item_field(self, field, version):
- # For each field, check if the field is valid for any of the item models supported by this folder
+ # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
+ # for the item types supported by this folder collection.
for item_model in self.supported_item_models:
try:
item_model.validate_field(field=field, version=version)
@@ -4611,14 +4689,35 @@ Inherited members
except InvalidField:
continue
else:
- raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))
+ raise InvalidField(f"{field!r} is not a valid field on {self.supported_item_models}")
+
+ def _rinse_args(self, q, depth, additional_fields, field_validator):
+ if depth is None:
+ depth = self._get_default_item_traversal_depth()
+ if additional_fields:
+ for f in additional_fields:
+ field_validator(field=f, version=self.account.version)
+ if f.field.is_complex:
+ raise ValueError(f"Field {f.field.name!r} not supported for this service")
+
+ # Build up any restrictions
+ if q.is_empty():
+ restriction = None
+ query_string = None
+ elif q.query_string:
+ restriction = None
+ query_string = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
+ else:
+ restriction = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
+ query_string = None
+ return depth, restriction, query_string
def find_items(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
calendar_view=None, page_size=None, max_items=None, offset=0):
"""Private method to call the FindItem service.
:param q: a Q instance containing any restrictions
- :param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is
+ :param shape: controls whether to return (id, changekey) tuples or Item objects. If additional_fields is
non-null, we always return Item objects. (Default value = ID_ONLY)
:param depth: controls the whether to return soft-deleted items or not. (Default value = None)
:param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware
@@ -4631,36 +4730,20 @@ Inherited members
:return: a generator for the returned item IDs or items
"""
+ from ..services import FindItem
if not self.folders:
log.debug('Folder list is empty')
return
if q.is_never():
log.debug('Query will never return results')
return
- if shape not in SHAPE_CHOICES:
- raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
- if depth is None:
- depth = self._get_default_item_traversal_depth()
- if depth not in ITEM_TRAVERSAL_CHOICES:
- raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
- if additional_fields:
- for f in additional_fields:
- self.validate_item_field(field=f, version=self.account.version)
- if f.field.is_complex:
- raise ValueError("find_items() does not support field '%s'. Use fetch() instead" % f.field.name)
+ depth, restriction, query_string = self._rinse_args(
+ q=q, depth=depth, additional_fields=additional_fields,
+ field_validator=self.validate_item_field
+ )
if calendar_view is not None and not isinstance(calendar_view, CalendarView):
- raise ValueError("'calendar_view' %s must be a CalendarView instance" % calendar_view)
+ raise InvalidTypeError('calendar_view', calendar_view, CalendarView)
- # Build up any restrictions
- if q.is_empty():
- restriction = None
- query_string = None
- elif q.query_string:
- restriction = None
- query_string = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
- else:
- restriction = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
- query_string = None
log.debug(
'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
self.account,
@@ -4670,7 +4753,7 @@ Inherited members
additional_fields,
restriction.q if restriction else None,
)
- yield from FindItem(account=self.account, chunk_size=page_size).call(
+ yield from FindItem(account=self.account, page_size=page_size).call(
folders=self.folders,
additional_fields=additional_fields,
restriction=restriction,
@@ -4696,7 +4779,7 @@ Inherited members
"""Private method to call the FindPeople service.
:param q: a Q instance containing any restrictions
- :param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is
+ :param shape: controls whether to return (id, changekey) tuples or Persona objects. If additional_fields is
non-null, we always return Persona objects. (Default value = ID_ONLY)
:param depth: controls the whether to return soft-deleted items or not. (Default value = None)
:param additional_fields: the extra properties we want on the return objects. Default is no properties.
@@ -4707,35 +4790,17 @@ Inherited members
:return: a generator for the returned personas
"""
+ from ..services import FindPeople
folder = self._get_single_folder()
- if not folder:
- return
if q.is_never():
log.debug('Query will never return results')
return
- if shape not in SHAPE_CHOICES:
- raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
- if depth is None:
- depth = self._get_default_item_traversal_depth()
- if depth not in ITEM_TRAVERSAL_CHOICES:
- raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
- if additional_fields:
- for f in additional_fields:
- Persona.validate_field(field=f, version=self.account.version)
- if f.field.is_complex:
- raise ValueError("find_people() does not support field '%s'" % f.field.name)
+ depth, restriction, query_string = self._rinse_args(
+ q=q, depth=depth, additional_fields=additional_fields,
+ field_validator=Persona.validate_field
+ )
- # Build up any restrictions
- if q.is_empty():
- restriction = None
- query_string = None
- elif q.query_string:
- restriction = None
- query_string = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
- else:
- restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
- query_string = None
- yield from FindPeople(account=self.account, chunk_size=page_size).call(
+ yield from FindPeople(account=self.account, page_size=page_size).call(
folder=folder,
additional_fields=additional_fields,
restriction=restriction,
@@ -4763,11 +4828,11 @@ Inherited members
for f in self.folders:
if isinstance(f, RootOfHierarchy):
if has_non_roots:
- raise ValueError('Cannot call GetFolder on a mix of folder types: {}'.format(self.folders))
+ raise ValueError(f'Cannot call GetFolder on a mix of folder types: {self.folders}')
has_roots = True
else:
if has_roots:
- raise ValueError('Cannot call GetFolder on a mix of folder types: {}'.format(self.folders))
+ raise ValueError(f'Cannot call GetFolder on a mix of folder types: {self.folders}')
has_non_roots = True
return RootOfHierarchy if has_roots else Folder
@@ -4776,8 +4841,8 @@ Inherited members
if len(unique_depths) == 1:
return unique_depths.pop()
raise ValueError(
- 'Folders in this collection do not have a common %s value. You need to define an explicit traversal depth'
- 'with QuerySet.depth() (values: %s)' % (traversal_attr, unique_depths)
+ f'Folders in this collection do not have a common {traversal_attr} value. You need to define an explicit '
+ f'traversal depth with QuerySet.depth() (values: {unique_depths})'
)
def _get_default_item_traversal_depth(self):
@@ -4807,6 +4872,7 @@ Inherited members
@require_account
def find_folders(self, q=None, shape=ID_ONLY, depth=None, additional_fields=None, page_size=None, max_items=None,
offset=0):
+ from ..services import FindFolder
# 'depth' controls whether to return direct children or recurse into sub-folders
from .base import BaseFolder, Folder
if q is None:
@@ -4821,26 +4887,22 @@ Inherited members
restriction = None
else:
restriction = Restriction(q, folders=self.folders, applies_to=Restriction.FOLDERS)
- if shape not in SHAPE_CHOICES:
- raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
if depth is None:
depth = self._get_default_folder_traversal_depth()
- if depth not in FOLDER_TRAVERSAL_CHOICES:
- raise ValueError("'depth' %s must be one of %s" % (depth, FOLDER_TRAVERSAL_CHOICES))
if additional_fields is None:
# Default to all non-complex properties. Subfolders will always be of class Folder
additional_fields = self.get_folder_fields(target_cls=Folder, is_complex=False)
else:
for f in additional_fields:
if f.field.is_complex:
- raise ValueError("find_folders() does not support field '%s'. Use get_folders()." % f.field.name)
+ raise ValueError(f"find_folders() does not support field {f.field.name!r}. Use get_folders().")
# Add required fields
additional_fields.update(
(FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
)
- yield from FindFolder(account=self.account, chunk_size=page_size).call(
+ yield from FindFolder(account=self.account, page_size=page_size).call(
folders=self.folders,
additional_fields=additional_fields,
restriction=restriction,
@@ -4851,6 +4913,7 @@ Inherited members
)
def get_folders(self, additional_fields=None):
+ from ..services import GetFolder
# Expand folders with their full set of properties
from .base import BaseFolder
if not self.folders:
@@ -4871,34 +4934,62 @@ Inherited members
shape=ID_ONLY,
)
- def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
+ def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60):
+ from ..services import SubscribeToPull
if not self.folders:
log.debug('Folder list is empty')
- return
- yield from SubscribeToPull(account=self.account).call(
+ return None
+ if event_types is None:
+ event_types = SubscribeToPull.EVENT_TYPES
+ return SubscribeToPull(account=self.account).get(
folders=self.folders, event_types=event_types, watermark=watermark, timeout=timeout,
)
- def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
- status_frequency=1):
+ def subscribe_to_push(self, callback_url, event_types=None, watermark=None, status_frequency=1):
+ from ..services import SubscribeToPush
if not self.folders:
log.debug('Folder list is empty')
- return
- yield from SubscribeToPush(account=self.account).call(
+ return None
+ if event_types is None:
+ event_types = SubscribeToPush.EVENT_TYPES
+ return SubscribeToPush(account=self.account).get(
folders=self.folders, event_types=event_types, watermark=watermark, status_frequency=status_frequency,
url=callback_url,
)
- def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
+ def subscribe_to_streaming(self, event_types=None):
+ from ..services import SubscribeToStreaming
if not self.folders:
log.debug('Folder list is empty')
- return
- yield from SubscribeToStreaming(account=self.account).call(folders=self.folders, event_types=event_types)
+ return None
+ if event_types is None:
+ event_types = SubscribeToStreaming.EVENT_TYPES
+ return SubscribeToStreaming(account=self.account).get(folders=self.folders, event_types=event_types)
+
+ def pull_subscription(self, **kwargs):
+ return PullSubscription(folder=self, **kwargs)
+
+ def push_subscription(self, **kwargs):
+ return PushSubscription(folder=self, **kwargs)
+
+ def streaming_subscription(self, **kwargs):
+ return StreamingSubscription(folder=self, **kwargs)
+
+ def unsubscribe(self, subscription_id):
+ """Unsubscribe. Only applies to pull and streaming notifications.
+
+ :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|streaming]()
+ :return: True
+
+ This method doesn't need the current collection instance, but it makes sense to keep the method along the other
+ sync methods.
+ """
+ from ..services import Unsubscribe
+ return Unsubscribe(account=self.account).get(subscription_id=subscription_id)
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
+ from ..services import SyncFolderItems
folder = self._get_single_folder()
- if not folder:
- return
if only_fields is None:
# We didn't restrict list of field paths. Get all fields from the server, including extended properties.
additional_fields = {FieldPath(field=f) for f in folder.allowed_item_fields(version=self.account.version)}
@@ -4928,17 +5019,19 @@ Inherited members
raise SyncCompleted(sync_state=svc.sync_state)
def sync_hierarchy(self, sync_state=None, only_fields=None):
+ from ..services import SyncFolderHierarchy
folder = self._get_single_folder()
- if not folder:
- return
if only_fields is None:
# We didn't restrict list of field paths. Get all fields from the server, including extended properties.
additional_fields = {FieldPath(field=f) for f in folder.supported_fields(version=self.account.version)}
else:
- for f in only_fields:
- folder.validate_field(field=f, version=self.account.version)
- # Remove ItemId and ChangeKey. We get them unconditionally
- additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}
+ additional_fields = set()
+ for field_name in only_fields:
+ folder.validate_field(field=field_name, version=self.account.version)
+ f = folder.get_field_by_fieldname(fieldname=field_name)
+ if not f.is_attribute:
+ # Remove ItemId and ChangeKey. We get them unconditionally
+ additional_fields.add(FieldPath(field=f))
# Add required fields
additional_fields.update(
@@ -5012,19 +5105,6 @@ Instance variables
Methods
-
-def all(self)
-
--
-
-
-
-Expand source code
-
-def all(self):
- return QuerySet(self).all()
-
-
def allowed_item_fields(self)
@@ -5042,19 +5122,6 @@ Methods
return fields
-
-def exclude(self, *args, **kwargs)
-
--
-
-
-
-Expand source code
-
-def exclude(self, *args, **kwargs):
- return QuerySet(self).exclude(*args, **kwargs)
-
-
def filter(self, *args, **kwargs)
@@ -5121,6 +5188,7 @@ Examples
@require_account
def find_folders(self, q=None, shape=ID_ONLY, depth=None, additional_fields=None, page_size=None, max_items=None,
offset=0):
+ from ..services import FindFolder
# 'depth' controls whether to return direct children or recurse into sub-folders
from .base import BaseFolder, Folder
if q is None:
@@ -5135,26 +5203,22 @@ Examples
restriction = None
else:
restriction = Restriction(q, folders=self.folders, applies_to=Restriction.FOLDERS)
- if shape not in SHAPE_CHOICES:
- raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
if depth is None:
depth = self._get_default_folder_traversal_depth()
- if depth not in FOLDER_TRAVERSAL_CHOICES:
- raise ValueError("'depth' %s must be one of %s" % (depth, FOLDER_TRAVERSAL_CHOICES))
if additional_fields is None:
# Default to all non-complex properties. Subfolders will always be of class Folder
additional_fields = self.get_folder_fields(target_cls=Folder, is_complex=False)
else:
for f in additional_fields:
if f.field.is_complex:
- raise ValueError("find_folders() does not support field '%s'. Use get_folders()." % f.field.name)
+ raise ValueError(f"find_folders() does not support field {f.field.name!r}. Use get_folders().")
# Add required fields
additional_fields.update(
(FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
)
- yield from FindFolder(account=self.account, chunk_size=page_size).call(
+ yield from FindFolder(account=self.account, page_size=page_size).call(
folders=self.folders,
additional_fields=additional_fields,
restriction=restriction,
@@ -5171,7 +5235,7 @@ Examples
-
Private method to call the FindItem service.
:param q: a Q instance containing any restrictions
-:param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is
+:param shape: controls whether to return (id, changekey) tuples or Item objects. If additional_fields is
non-null, we always return Item objects. (Default value = ID_ONLY)
:param depth: controls the whether to return soft-deleted items or not. (Default value = None)
:param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware
@@ -5191,7 +5255,7 @@
Examples
"""Private method to call the FindItem service.
:param q: a Q instance containing any restrictions
- :param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is
+ :param shape: controls whether to return (id, changekey) tuples or Item objects. If additional_fields is
non-null, we always return Item objects. (Default value = ID_ONLY)
:param depth: controls the whether to return soft-deleted items or not. (Default value = None)
:param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware
@@ -5204,36 +5268,20 @@ Examples
:return: a generator for the returned item IDs or items
"""
+ from ..services import FindItem
if not self.folders:
log.debug('Folder list is empty')
return
if q.is_never():
log.debug('Query will never return results')
return
- if shape not in SHAPE_CHOICES:
- raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
- if depth is None:
- depth = self._get_default_item_traversal_depth()
- if depth not in ITEM_TRAVERSAL_CHOICES:
- raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
- if additional_fields:
- for f in additional_fields:
- self.validate_item_field(field=f, version=self.account.version)
- if f.field.is_complex:
- raise ValueError("find_items() does not support field '%s'. Use fetch() instead" % f.field.name)
+ depth, restriction, query_string = self._rinse_args(
+ q=q, depth=depth, additional_fields=additional_fields,
+ field_validator=self.validate_item_field
+ )
if calendar_view is not None and not isinstance(calendar_view, CalendarView):
- raise ValueError("'calendar_view' %s must be a CalendarView instance" % calendar_view)
+ raise InvalidTypeError('calendar_view', calendar_view, CalendarView)
- # Build up any restrictions
- if q.is_empty():
- restriction = None
- query_string = None
- elif q.query_string:
- restriction = None
- query_string = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
- else:
- restriction = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
- query_string = None
log.debug(
'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
self.account,
@@ -5243,7 +5291,7 @@ Examples
additional_fields,
restriction.q if restriction else None,
)
- yield from FindItem(account=self.account, chunk_size=page_size).call(
+ yield from FindItem(account=self.account, page_size=page_size).call(
folders=self.folders,
additional_fields=additional_fields,
restriction=restriction,
@@ -5263,7 +5311,7 @@ Examples
-
Private method to call the FindPeople service.
:param q: a Q instance containing any restrictions
-:param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is
+:param shape: controls whether to return (id, changekey) tuples or Persona objects. If additional_fields is
non-null, we always return Persona objects. (Default value = ID_ONLY)
:param depth: controls the whether to return soft-deleted items or not. (Default value = None)
:param additional_fields: the extra properties we want on the return objects. Default is no properties.
@@ -5281,7 +5329,7 @@
Examples
"""Private method to call the FindPeople service.
:param q: a Q instance containing any restrictions
- :param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is
+ :param shape: controls whether to return (id, changekey) tuples or Persona objects. If additional_fields is
non-null, we always return Persona objects. (Default value = ID_ONLY)
:param depth: controls the whether to return soft-deleted items or not. (Default value = None)
:param additional_fields: the extra properties we want on the return objects. Default is no properties.
@@ -5292,35 +5340,17 @@ Examples
:return: a generator for the returned personas
"""
+ from ..services import FindPeople
folder = self._get_single_folder()
- if not folder:
- return
if q.is_never():
log.debug('Query will never return results')
return
- if shape not in SHAPE_CHOICES:
- raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
- if depth is None:
- depth = self._get_default_item_traversal_depth()
- if depth not in ITEM_TRAVERSAL_CHOICES:
- raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
- if additional_fields:
- for f in additional_fields:
- Persona.validate_field(field=f, version=self.account.version)
- if f.field.is_complex:
- raise ValueError("find_people() does not support field '%s'" % f.field.name)
+ depth, restriction, query_string = self._rinse_args(
+ q=q, depth=depth, additional_fields=additional_fields,
+ field_validator=Persona.validate_field
+ )
- # Build up any restrictions
- if q.is_empty():
- restriction = None
- query_string = None
- elif q.query_string:
- restriction = None
- query_string = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
- else:
- restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
- query_string = None
- yield from FindPeople(account=self.account, chunk_size=page_size).call(
+ yield from FindPeople(account=self.account, page_size=page_size).call(
folder=folder,
additional_fields=additional_fields,
restriction=restriction,
@@ -5333,19 +5363,6 @@ Examples
)
-
-def get(self, *args, **kwargs)
-
--
-
-
-
-Expand source code
-
-def get(self, *args, **kwargs):
- return QuerySet(self).get(*args, **kwargs)
-
-
def get_folder_fields(self, target_cls, is_complex=None)
@@ -5372,6 +5389,7 @@ Examples
Expand source code
def get_folders(self, additional_fields=None):
+ from ..services import GetFolder
# Expand folders with their full set of properties
from .base import BaseFolder
if not self.folders:
@@ -5393,8 +5411,8 @@ Examples
)
-
-def none(self)
+
+def pull_subscription(self, **kwargs)
-
@@ -5402,12 +5420,12 @@
Examples
Expand source code
-def none(self):
- return QuerySet(self).none()
+def pull_subscription(self, **kwargs):
+ return PullSubscription(folder=self, **kwargs)
-
-def people(self)
+
+def push_subscription(self, **kwargs)
-
@@ -5415,8 +5433,8 @@
Examples
Expand source code
-def people(self):
- return QuerySet(self).people()
+def push_subscription(self, **kwargs):
+ return PushSubscription(folder=self, **kwargs)
@@ -5445,8 +5463,21 @@ Examples
)
+
+def streaming_subscription(self, **kwargs)
+
+-
+
+
+
+Expand source code
+
+def streaming_subscription(self, **kwargs):
+ return StreamingSubscription(folder=self, **kwargs)
+
+
-def subscribe_to_pull(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), watermark=None, timeout=60)
+def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60)
-
@@ -5454,17 +5485,20 @@
Examples
Expand source code
-def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
+def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60):
+ from ..services import SubscribeToPull
if not self.folders:
log.debug('Folder list is empty')
- return
- yield from SubscribeToPull(account=self.account).call(
+ return None
+ if event_types is None:
+ event_types = SubscribeToPull.EVENT_TYPES
+ return SubscribeToPull(account=self.account).get(
folders=self.folders, event_types=event_types, watermark=watermark, timeout=timeout,
)
-def subscribe_to_push(self, callback_url, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), watermark=None, status_frequency=1)
+def subscribe_to_push(self, callback_url, event_types=None, watermark=None, status_frequency=1)
-
@@ -5472,19 +5506,21 @@
Examples
Expand source code
-def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
- status_frequency=1):
+def subscribe_to_push(self, callback_url, event_types=None, watermark=None, status_frequency=1):
+ from ..services import SubscribeToPush
if not self.folders:
log.debug('Folder list is empty')
- return
- yield from SubscribeToPush(account=self.account).call(
+ return None
+ if event_types is None:
+ event_types = SubscribeToPush.EVENT_TYPES
+ return SubscribeToPush(account=self.account).get(
folders=self.folders, event_types=event_types, watermark=watermark, status_frequency=status_frequency,
url=callback_url,
)
-def subscribe_to_streaming(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'))
+def subscribe_to_streaming(self, event_types=None)
-
@@ -5492,11 +5528,14 @@
Examples
Expand source code
-def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
+def subscribe_to_streaming(self, event_types=None):
+ from ..services import SubscribeToStreaming
if not self.folders:
log.debug('Folder list is empty')
- return
- yield from SubscribeToStreaming(account=self.account).call(folders=self.folders, event_types=event_types)
+ return None
+ if event_types is None:
+ event_types = SubscribeToStreaming.EVENT_TYPES
+ return SubscribeToStreaming(account=self.account).get(folders=self.folders, event_types=event_types)
@@ -5509,17 +5548,19 @@ Examples
Expand source code
def sync_hierarchy(self, sync_state=None, only_fields=None):
+ from ..services import SyncFolderHierarchy
folder = self._get_single_folder()
- if not folder:
- return
if only_fields is None:
# We didn't restrict list of field paths. Get all fields from the server, including extended properties.
additional_fields = {FieldPath(field=f) for f in folder.supported_fields(version=self.account.version)}
else:
- for f in only_fields:
- folder.validate_field(field=f, version=self.account.version)
- # Remove ItemId and ChangeKey. We get them unconditionally
- additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}
+ additional_fields = set()
+ for field_name in only_fields:
+ folder.validate_field(field=field_name, version=self.account.version)
+ f = folder.get_field_by_fieldname(fieldname=field_name)
+ if not f.is_attribute:
+ # Remove ItemId and ChangeKey. We get them unconditionally
+ additional_fields.add(FieldPath(field=f))
# Add required fields
additional_fields.update(
@@ -5553,9 +5594,8 @@ Examples
Expand source code
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
+ from ..services import SyncFolderItems
folder = self._get_single_folder()
- if not folder:
- return
if only_fields is None:
# We didn't restrict list of field paths. Get all fields from the server, including extended properties.
additional_fields = {FieldPath(field=f) for f in folder.allowed_item_fields(version=self.account.version)}
@@ -5585,6 +5625,32 @@ Examples
raise SyncCompleted(sync_state=svc.sync_state)
+
+def unsubscribe(self, subscription_id)
+
+-
+
Unsubscribe. Only applies to pull and streaming notifications.
+:param subscription_id: A subscription ID as acquired by .subscribe_to_pull|streaming
+:return: True
+This method doesn't need the current collection instance, but it makes sense to keep the method along the other
+sync methods.
+
+
+Expand source code
+
+def unsubscribe(self, subscription_id):
+ """Unsubscribe. Only applies to pull and streaming notifications.
+
+ :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|streaming]()
+ :return: True
+
+ This method doesn't need the current collection instance, but it makes sense to keep the method along the other
+ sync methods.
+ """
+ from ..services import Unsubscribe
+ return Unsubscribe(account=self.account).get(subscription_id=subscription_id)
+
+
def validate_item_field(self, field, version)
@@ -5595,7 +5661,8 @@ Examples
Expand source code
def validate_item_field(self, field, version):
- # For each field, check if the field is valid for any of the item models supported by this folder
+ # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
+ # for the item types supported by this folder collection.
for item_model in self.supported_item_models:
try:
item_model.validate_field(field=field, version=version)
@@ -5603,7 +5670,7 @@ Examples
except InvalidField:
continue
else:
- raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))
+ raise InvalidField(f"{field!r} is not a valid field on {self.supported_item_models}")
@@ -5651,6 +5718,18 @@ Examples
+Inherited members
+
class FolderId
@@ -5712,7 +5791,7 @@ Inherited members
def __init__(self, folder_collection):
from .collections import FolderCollection
if not isinstance(folder_collection, FolderCollection):
- raise ValueError("'folder_collection' %r must be a FolderCollection instance" % folder_collection)
+ raise InvalidTypeError('folder_collection', folder_collection, FolderCollection)
self.folder_collection = folder_collection
self.q = Q() # Default to no restrictions
self.only_fields = None
@@ -5742,7 +5821,7 @@ Inherited members
only_fields.append(field_path)
break
else:
- raise InvalidField("Unknown field %r on folders %s" % (arg, self.folder_collection.folders))
+ raise InvalidField(f"Unknown field {arg!r} on folders {self.folder_collection.folders}")
new_qs = self._copy_self()
new_qs.only_fields = only_fields
return new_qs
@@ -5772,7 +5851,7 @@ Inherited members
if not folders:
raise DoesNotExist('Could not find a child folder matching the query')
if len(folders) != 1:
- raise MultipleObjectsReturned('Expected result length 1, but got %s' % folders)
+ raise MultipleObjectsReturned(f'Expected result length 1, but got {folders}')
f = folders[0]
if isinstance(f, Exception):
raise f
@@ -5814,6 +5893,9 @@ Inherited members
# Fetch all properties for the found folders
resolveable_folders = []
for f in folders:
+ if isinstance(f, Exception):
+ yield f
+ continue
if not f.get_folder_allowed:
log.debug('GetFolder not allowed on folder %s. Non-complex fields must be fetched with FindFolder', f)
yield f
@@ -5825,15 +5907,15 @@ Inherited members
account=self.folder_collection.account, folders=resolveable_folders
).get_folders(additional_fields=complex_fields)
for f, complex_f in zip(resolveable_folders, complex_folders):
- if isinstance(f, Exception):
- yield f
+ if isinstance(f, MISSING_FOLDER_ERRORS):
+ # We were unlucky. The folder disappeared between the FindFolder and the GetFolder calls
continue
if isinstance(complex_f, Exception):
yield complex_f
continue
# Add the extra field values to the folders we fetched with find_folders()
if f.__class__ != complex_f.__class__:
- raise ValueError('Type mismatch: %s vs %s' % (f, complex_f))
+ raise ValueError(f'Type mismatch: {f} vs {complex_f}')
for complex_field in complex_fields:
field_name = complex_field.field.name
setattr(f, field_name, getattr(complex_f, field_name))
@@ -5923,7 +6005,7 @@ Methods
if not folders:
raise DoesNotExist('Could not find a child folder matching the query')
if len(folders) != 1:
- raise MultipleObjectsReturned('Expected result length 1, but got %s' % folders)
+ raise MultipleObjectsReturned(f'Expected result length 1, but got {folders}')
f = folders[0]
if isinstance(f, Exception):
raise f
@@ -5952,7 +6034,7 @@ Methods
only_fields.append(field_path)
break
else:
- raise InvalidField("Unknown field %r on folders %s" % (arg, self.folder_collection.folders))
+ raise InvalidField(f"Unknown field {arg!r} on folders {self.folder_collection.folders}")
new_qs = self._copy_self()
new_qs.only_fields = only_fields
return new_qs
@@ -5997,17 +6079,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -6066,17 +6157,26 @@ Inherited members
Contacts
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -6140,17 +6240,26 @@ Inherited members
Contacts
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -6207,17 +6316,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -6237,7 +6355,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -6277,17 +6395,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -6353,17 +6480,26 @@ Inherited members
Messages
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -6383,7 +6519,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -6418,17 +6554,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -6494,17 +6639,26 @@ Inherited members
Messages
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -6524,7 +6678,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -6559,17 +6713,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -6621,17 +6784,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -6683,17 +6855,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -6755,17 +6936,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -6824,17 +7014,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -6854,7 +7053,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -6894,17 +7093,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -6962,17 +7170,26 @@ Inherited members
Contacts
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -7063,7 +7280,7 @@ Instance variables
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -7105,17 +7322,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -7178,17 +7404,26 @@ Inherited members
Contacts
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -7254,17 +7489,26 @@ Inherited members
Messages
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -7321,17 +7565,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -7388,17 +7641,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -7455,17 +7717,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -7528,17 +7799,26 @@ Inherited members
Contacts
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -7558,7 +7838,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -7593,17 +7873,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -7655,6 +7944,9 @@ Inherited members
for f in SingleFolderQuerySet(account=self.account, folder=folder).depth(
self.DEFAULT_FOLDER_TRAVERSAL_DEPTH
).all():
+ if isinstance(f, MISSING_FOLDER_ERRORS):
+ # We were unlucky. The folder disappeared between the FindFolder and the GetFolder calls
+ continue
if isinstance(f, Exception):
raise f
children_map[f.id] = f
@@ -7724,6 +8016,9 @@ Methods
for f in SingleFolderQuerySet(account=self.account, folder=folder).depth(
self.DEFAULT_FOLDER_TRAVERSAL_DEPTH
).all():
+ if isinstance(f, MISSING_FOLDER_ERRORS):
+ # We were unlucky. The folder disappeared between the FindFolder and the GetFolder calls
+ continue
if isinstance(f, Exception):
raise f
children_map[f.id] = f
@@ -7745,19 +8040,28 @@ Inherited members
RootOfHierarchy
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_cls_from_folder_name
folder_sync_state
+get
get_default_folder
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -7777,7 +8081,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -7817,17 +8121,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -7884,17 +8197,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -7960,17 +8282,26 @@ Inherited members
Contacts
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -7990,7 +8321,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -8025,17 +8356,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -8055,7 +8395,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -8090,17 +8430,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -8120,7 +8469,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -8155,17 +8504,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -8185,7 +8543,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -8220,17 +8578,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -8287,17 +8654,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -8343,47 +8719,46 @@ Inherited members
# Try to pick a suitable default folder. we do this by:
# 1. Searching the full folder list for a folder with the distinguished folder name
# 2. Searching TOIS for a direct child folder of the same type that is marked as distinguished
- # 3. Searching TOIS for a direct child folder of the same type that is has a localized name
+ # 3. Searching TOIS for a direct child folder of the same type that has a localized name
# 4. Searching root for a direct child folder of the same type that is marked as distinguished
- # 5. Searching root for a direct child folder of the same type that is has a localized name
+ # 5. Searching root for a direct child folder of the same type that has a localized name
log.debug('Searching default %s folder in full folder list', folder_cls)
for f in self._folders_map.values():
- # Require exact class to not match e.g. RecipientCache instead of Contacts
+ # Require exact type, to avoid matching with subclasses (e.g. RecipientCache and Contacts)
if f.__class__ == folder_cls and f.has_distinguished_name:
log.debug('Found cached %s folder with default distinguished name', folder_cls)
return f
- # Try direct children of TOIS first, unless we're trying to get the TOIS folder. TOIS might not exist.
+ # Try direct children of TOIS first, unless we're trying to get the TOIS folder
if folder_cls != MsgFolderRoot:
try:
return self._get_candidate(folder_cls=folder_cls, folder_coll=self.tois.children)
except MISSING_FOLDER_ERRORS:
- # No candidates, or TOIS does not exist, or we don't have access
+ # No candidates, or TOIS does not exist, or we don't have access to TOIS
pass
- # No candidates in TOIS. Try direct children of root.
+ # Finally, try direct children of root
return self._get_candidate(folder_cls=folder_cls, folder_coll=self.children)
def _get_candidate(self, folder_cls, folder_coll):
- # Get a single the folder of the same type in folder_coll
+ # Look for a single useful folder of type folder_cls in folder_coll
same_type = [f for f in folder_coll if f.__class__ == folder_cls]
are_distinguished = [f for f in same_type if f.is_distinguished]
if are_distinguished:
candidates = are_distinguished
else:
candidates = [f for f in same_type if f.name.lower() in folder_cls.localized_names(self.account.locale)]
- if candidates:
- if len(candidates) > 1:
- raise ValueError(
- 'Multiple possible default %s folders: %s' % (folder_cls, [f.name for f in candidates])
- )
- if candidates[0].is_distinguished:
- log.debug('Found cached distinguished %s folder', folder_cls)
- else:
- log.debug('Found cached %s folder with localized name', folder_cls)
- return candidates[0]
- raise ErrorFolderNotFound('No usable default %s folders' % folder_cls)
+ if not candidates:
+ raise ErrorFolderNotFound(f'No usable default {folder_cls} folders')
+ if len(candidates) > 1:
+ raise ValueError(f'Multiple possible default {folder_cls} folders: {[f.name for f in candidates]}')
+ candidate = candidates[0]
+ if candidate.is_distinguished:
+ log.debug('Found distinguished %s folder', folder_cls)
+ else:
+ log.debug('Found %s folder with localized name %s', folder_cls, candidate.name)
+ return candidate
Ancestors
@@ -8427,19 +8802,28 @@ Inherited members
RootOfHierarchy
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_cls_from_folder_name
folder_sync_state
+get
get_default_folder
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -8516,7 +8900,7 @@ Inherited members
def get_folder(self, folder):
if not folder.id:
raise ValueError("'folder' must have an ID")
- return self._folders_map.get(folder.id, None)
+ return self._folders_map.get(folder.id)
def add_folder(self, folder):
if not folder.id:
@@ -8554,21 +8938,21 @@ Inherited members
:param account:
"""
if not cls.DISTINGUISHED_FOLDER_ID:
- raise ValueError('Class %s must have a DISTINGUISHED_FOLDER_ID value' % cls)
+ raise ValueError(f'Class {cls} must have a DISTINGUISHED_FOLDER_ID value')
try:
return cls.resolve(
account=account,
folder=cls(account=account, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
)
except MISSING_FOLDER_ERRORS:
- raise ErrorFolderNotFound('Could not find distinguished folder %s' % cls.DISTINGUISHED_FOLDER_ID)
+ raise ErrorFolderNotFound(f'Could not find distinguished folder {cls.DISTINGUISHED_FOLDER_ID}')
def get_default_folder(self, folder_cls):
"""Return the distinguished folder instance of type folder_cls belonging to this account. If no distinguished
folder was found, try as best we can to return the default folder of type 'folder_cls'
"""
if not folder_cls.DISTINGUISHED_FOLDER_ID:
- raise ValueError("'folder_cls' %s must have a DISTINGUISHED_FOLDER_ID value" % folder_cls)
+ raise ValueError(f"'folder_cls' {folder_cls} must have a DISTINGUISHED_FOLDER_ID value")
# Use cached distinguished folder instance, but only if cache has already been prepped. This is an optimization
# for accessing e.g. 'account.contacts' without fetching all folders of the account.
if self._subfolders is not None:
@@ -8589,7 +8973,7 @@ Inherited members
except MISSING_FOLDER_ERRORS:
# The Exchange server does not return a distinguished folder of this type
pass
- raise ErrorFolderNotFound('No usable default %s folders' % folder_cls)
+ raise ErrorFolderNotFound(f'No usable default {folder_cls} folders')
@property
def _folders_map(self):
@@ -8626,6 +9010,9 @@ Inherited members
if isinstance(f, ErrorAccessDenied):
# We may not have FindFolder access, or GetFolder access, either to this folder or at all
continue
+ if isinstance(f, MISSING_FOLDER_ERRORS):
+ # We were unlucky. The folder disappeared between the FindFolder and the GetFolder calls
+ continue
if isinstance(f, Exception):
raise f
if f.id in folders_map:
@@ -8743,59 +9130,23 @@ Static methods
:param account:
"""
if not cls.DISTINGUISHED_FOLDER_ID:
- raise ValueError('Class %s must have a DISTINGUISHED_FOLDER_ID value' % cls)
+ raise ValueError(f'Class {cls} must have a DISTINGUISHED_FOLDER_ID value')
try:
return cls.resolve(
account=account,
folder=cls(account=account, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
)
except MISSING_FOLDER_ERRORS:
- raise ErrorFolderNotFound('Could not find distinguished folder %s' % cls.DISTINGUISHED_FOLDER_ID)
+ raise ErrorFolderNotFound(f'Could not find distinguished folder {cls.DISTINGUISHED_FOLDER_ID}')
Instance variables
-var account
--
-
-
-
-Expand source code
-
-@property
-def account(self):
- return self._account
-
-
var effective_rights
-
-var parent
--
-
-
-
-Expand source code
-
-@property
-def parent(self):
- return None
-
-
-var root
--
-
-
-
-Expand source code
-
-@property
-def root(self):
- return self
-
-
Methods
@@ -8860,7 +9211,7 @@ Methods
folder was found, try as best we can to return the default folder of type 'folder_cls'
"""
if not folder_cls.DISTINGUISHED_FOLDER_ID:
- raise ValueError("'folder_cls' %s must have a DISTINGUISHED_FOLDER_ID value" % folder_cls)
+ raise ValueError(f"'folder_cls' {folder_cls} must have a DISTINGUISHED_FOLDER_ID value")
# Use cached distinguished folder instance, but only if cache has already been prepped. This is an optimization
# for accessing e.g. 'account.contacts' without fetching all folders of the account.
if self._subfolders is not None:
@@ -8881,7 +9232,7 @@ Methods
except MISSING_FOLDER_ERRORS:
# The Exchange server does not return a distinguished folder of this type
pass
- raise ErrorFolderNotFound('No usable default %s folders' % folder_cls)
+ raise ErrorFolderNotFound(f'No usable default {folder_cls} folders')
@@ -8896,7 +9247,7 @@ Methods
def get_folder(self, folder):
if not folder.id:
raise ValueError("'folder' must have an ID")
- return self._folders_map.get(folder.id, None)
+ return self._folders_map.get(folder.id)
@@ -8938,16 +9289,25 @@ Inherited members
BaseFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -8999,17 +9359,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -9029,7 +9398,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -9059,17 +9428,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -9135,17 +9513,26 @@ Inherited members
Messages
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -9165,7 +9552,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -9200,17 +9587,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -9267,17 +9663,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -9329,17 +9734,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -9396,17 +9810,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -9443,15 +9866,7 @@ Inherited members
return self.__class__(account=self.folder_collection.account, folder=self.folder_collection.folders[0])
def resolve(self):
- folders = list(self.folder_collection.resolve())
- if not folders:
- raise DoesNotExist('Could not find a folder matching the query')
- if len(folders) != 1:
- raise MultipleObjectsReturned('Expected result length 1, but got %s' % folders)
- f = folders[0]
- if isinstance(f, Exception):
- raise f
- return f
+ return list(self.folder_collection.resolve())[0]
Ancestors
@@ -9469,15 +9884,7 @@ Methods
Expand source code
def resolve(self):
- folders = list(self.folder_collection.resolve())
- if not folders:
- raise DoesNotExist('Could not find a folder matching the query')
- if len(folders) != 1:
- raise MultipleObjectsReturned('Expected result length 1, but got %s' % folders)
- f = folders[0]
- if isinstance(f, Exception):
- raise f
- return f
+ return list(self.folder_collection.resolve())[0]
@@ -9535,17 +9942,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -9597,17 +10013,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -9627,7 +10052,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -9667,17 +10092,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -9734,17 +10168,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -9819,17 +10262,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -9881,17 +10333,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -9911,7 +10372,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -9959,17 +10420,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -10021,17 +10491,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -10051,7 +10530,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -10093,17 +10572,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -10123,13 +10611,13 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
-class WellknownFolder(Folder):
- """A base class to use until we have a more specific folder implementation for this folder."""
+class WellknownFolder(Folder, metaclass=EWSMeta):
+ """Base class to use until we have a more specific folder implementation for this folder."""
supported_item_models = ITEM_CLASSES
@@ -10186,17 +10674,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -10248,17 +10745,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -10400,7 +10906,6 @@ NAMESPACE
absolute
account
-all
allowed_item_fields
bulk_create
child_folder_count
@@ -10410,12 +10915,9 @@ delete
delete_user_configuration
empty
-exclude
-filter
folder_class
folder_cls_from_container_class
folder_sync_state
-get
get_events
get_folder_allowed
get_item_field_by_fieldname
@@ -10430,12 +10932,10 @@ localized_names
move
name
-none
normalize_fields
parent
parent_folder_id
parts
-people
pull_subscription
push_subscription
refresh
@@ -10452,9 +10952,7 @@ sync_hierarchy
sync_items
test_access
-to_folder_id
-to_id_xml
-to_xml
+to_id
total_count
tree
unread_count
@@ -10596,40 +11094,36 @@ Folder
-
FolderCollection
REQUIRED_FOLDER_FIELDS
-all
allowed_item_fields
-exclude
filter
find_folders
find_items
find_people
folders
-get
get_folder_fields
get_folders
-none
-people
+pull_subscription
+push_subscription
resolve
+streaming_subscription
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
supported_item_models
sync_hierarchy
sync_items
+unsubscribe
validate_item_field
view
@@ -10901,7 +11395,6 @@
FIELDS
WELLKNOWN_FOLDERS
-account
add_folder
clear_cache
effective_rights
@@ -10911,9 +11404,7 @@ get_default_folder
get_distinguished
get_folder
-parent
remove_folder
-root
update_folder
diff --git a/docs/exchangelib/folders/known_folders.html b/docs/exchangelib/folders/known_folders.html
index 2259fd07..6fbd6162 100644
--- a/docs/exchangelib/folders/known_folders.html
+++ b/docs/exchangelib/folders/known_folders.html
@@ -30,6 +30,7 @@ Module exchangelib.folders.known_folders
from .collections import FolderCollection
from ..items import CalendarItem, Contact, Message, Task, DistributionList, MeetingRequest, MeetingResponse, \
MeetingCancellation, ITEM_CLASSES, ASSOCIATED
+from ..properties import EWSMeta
from ..version import EXCHANGE_2010_SP1, EXCHANGE_2013, EXCHANGE_2013_SP1
@@ -195,8 +196,8 @@ Module exchangelib.folders.known_folders
}
-class WellknownFolder(Folder):
- """A base class to use until we have a more specific folder implementation for this folder."""
+class WellknownFolder(Folder, metaclass=EWSMeta):
+ """Base class to use until we have a more specific folder implementation for this folder."""
supported_item_models = ITEM_CLASSES
@@ -732,7 +733,7 @@ Classes
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -772,17 +773,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -841,17 +851,26 @@ Inherited members
Contacts
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -909,17 +928,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -939,7 +967,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -974,17 +1002,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -1004,7 +1041,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -1039,17 +1076,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -1069,7 +1115,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -1104,17 +1150,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -1134,7 +1189,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -1169,17 +1224,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -1199,7 +1263,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -1234,17 +1298,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -1264,7 +1337,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -1299,17 +1372,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -1329,7 +1411,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -1364,17 +1446,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -1431,17 +1522,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -1537,17 +1637,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -1599,17 +1708,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -1666,17 +1784,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -1739,17 +1866,26 @@ Inherited members
Contacts
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -1769,7 +1905,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -1804,17 +1940,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -1900,17 +2045,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -1930,7 +2084,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -1965,17 +2119,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -2032,17 +2195,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -2099,17 +2271,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -2161,17 +2342,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -2246,17 +2436,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -2276,7 +2475,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -2311,17 +2510,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -2387,17 +2595,26 @@ Inherited members
Messages
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -2449,17 +2666,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -2479,7 +2705,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -2519,17 +2745,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -2587,17 +2822,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -2649,17 +2893,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -2718,17 +2971,26 @@ Inherited members
Contacts
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -2792,17 +3054,26 @@ Inherited members
Contacts
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -2859,17 +3130,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -2889,7 +3169,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -2929,17 +3209,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3005,17 +3294,26 @@ Inherited members
Messages
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3035,7 +3333,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -3070,17 +3368,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3146,17 +3453,26 @@ Inherited members
Messages
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3176,7 +3492,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -3211,17 +3527,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3273,17 +3598,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3335,17 +3669,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3407,17 +3750,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3476,17 +3828,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3506,7 +3867,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -3546,17 +3907,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3614,17 +3984,26 @@ Inherited members
Contacts
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3715,7 +4094,7 @@ Instance variables
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -3757,17 +4136,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3830,17 +4218,26 @@ Inherited members
Contacts
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3906,17 +4303,26 @@ Inherited members
Messages
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -3973,17 +4379,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -4040,17 +4455,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -4107,17 +4531,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -4180,17 +4613,26 @@ Inherited members
Contacts
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -4210,7 +4652,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -4245,17 +4687,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -4275,7 +4726,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.
Expand source code
@@ -4315,17 +4766,26 @@ Inherited members
WellknownFolder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -4382,17 +4842,26 @@ Inherited members
Folder
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -4458,17 +4927,26 @@ Inherited members
Contacts
:
ID_ELEMENT_CLS
+account
add_field
+all
deregister
+exclude
+filter
folder_cls_from_container_class
folder_sync_state
+get
get_distinguished
get_events
get_streaming_events
is_distinguished
item_sync_state
+none
+parent
+people
register
remove_field
+root
subscribe_to_pull
subscribe_to_push
subscribe_to_streaming
@@ -4488,7 +4966,7 @@ Inherited members
(**kwargs)
-
-
A base class to use until we have a more specific folder implementation for this folder.
+Base class to use until we have a more specific folder implementation for this folder.