Module exchangelib.version
Classes
class Build (major_version, minor_version, major_build=0, minor_build=0)
-
Expand source code
class Build: """Holds methods for working with build numbers.""" __slots__ = "major_version", "minor_version", "major_build", "minor_build" def __init__(self, major_version, minor_version, major_build=0, minor_build=0): if not isinstance(major_version, int): raise InvalidTypeError("major_version", major_version, int) if not isinstance(minor_version, int): raise InvalidTypeError("minor_version", minor_version, int) if not isinstance(major_build, int): raise InvalidTypeError("major_build", major_build, int) if not isinstance(minor_build, int): raise InvalidTypeError("minor_build", minor_build, int) self.major_version = major_version self.minor_version = minor_version self.major_build = major_build self.minor_build = minor_build if major_version < 8: raise ValueError(f"Exchange major versions below 8 don't support EWS ({self})") @classmethod def from_xml(cls, elem): xml_elems_map = { "major_version": "MajorVersion", "minor_version": "MinorVersion", "major_build": "MajorBuildNumber", "minor_build": "MinorBuildNumber", } kwargs = {} for k, xml_elem in xml_elems_map.items(): v = elem.get(xml_elem) if v is None: v = get_xml_attr(elem, f"{{{ANS}}}{xml_elem}") if v is None: raise ValueError() kwargs[k] = int(v) # Also raises ValueError return cls(**kwargs) def api_version(self): for build, api_version, _ in VERSIONS: if self.major_version != build.major_version or self.minor_version != build.minor_version: continue if self >= build: return api_version raise ValueError(f"API version for build {self} is unknown") def __cmp__(self, other): # __cmp__ is not a magic method in Python3. We'll just use it here to implement comparison operators c = (self.major_version > other.major_version) - (self.major_version < other.major_version) if c != 0: return c c = (self.minor_version > other.minor_version) - (self.minor_version < other.minor_version) if c != 0: return c c = (self.major_build > other.major_build) - (self.major_build < other.major_build) if c != 0: return c return (self.minor_build > other.minor_build) - (self.minor_build < other.minor_build) def __eq__(self, other): return self.__cmp__(other) == 0 def __hash__(self): return hash(repr(self)) def __ne__(self, other): return self.__cmp__(other) != 0 def __lt__(self, other): return self.__cmp__(other) < 0 def __le__(self, other): return self.__cmp__(other) <= 0 def __gt__(self, other): return self.__cmp__(other) > 0 def __ge__(self, other): return self.__cmp__(other) >= 0 def __str__(self): return f"{self.major_version}.{self.minor_version}.{self.major_build}.{self.minor_build}" def __repr__(self): return self.__class__.__name__ + repr( (self.major_version, self.minor_version, self.major_build, self.minor_build) )
Holds methods for working with build numbers.
Static methods
def from_xml(elem)
Instance variables
var major_build
-
Expand source code
class Build: """Holds methods for working with build numbers.""" __slots__ = "major_version", "minor_version", "major_build", "minor_build" def __init__(self, major_version, minor_version, major_build=0, minor_build=0): if not isinstance(major_version, int): raise InvalidTypeError("major_version", major_version, int) if not isinstance(minor_version, int): raise InvalidTypeError("minor_version", minor_version, int) if not isinstance(major_build, int): raise InvalidTypeError("major_build", major_build, int) if not isinstance(minor_build, int): raise InvalidTypeError("minor_build", minor_build, int) self.major_version = major_version self.minor_version = minor_version self.major_build = major_build self.minor_build = minor_build if major_version < 8: raise ValueError(f"Exchange major versions below 8 don't support EWS ({self})") @classmethod def from_xml(cls, elem): xml_elems_map = { "major_version": "MajorVersion", "minor_version": "MinorVersion", "major_build": "MajorBuildNumber", "minor_build": "MinorBuildNumber", } kwargs = {} for k, xml_elem in xml_elems_map.items(): v = elem.get(xml_elem) if v is None: v = get_xml_attr(elem, f"{{{ANS}}}{xml_elem}") if v is None: raise ValueError() kwargs[k] = int(v) # Also raises ValueError return cls(**kwargs) def api_version(self): for build, api_version, _ in VERSIONS: if self.major_version != build.major_version or self.minor_version != build.minor_version: continue if self >= build: return api_version raise ValueError(f"API version for build {self} is unknown") def __cmp__(self, other): # __cmp__ is not a magic method in Python3. We'll just use it here to implement comparison operators c = (self.major_version > other.major_version) - (self.major_version < other.major_version) if c != 0: return c c = (self.minor_version > other.minor_version) - (self.minor_version < other.minor_version) if c != 0: return c c = (self.major_build > other.major_build) - (self.major_build < other.major_build) if c != 0: return c return (self.minor_build > other.minor_build) - (self.minor_build < other.minor_build) def __eq__(self, other): return self.__cmp__(other) == 0 def __hash__(self): return hash(repr(self)) def __ne__(self, other): return self.__cmp__(other) != 0 def __lt__(self, other): return self.__cmp__(other) < 0 def __le__(self, other): return self.__cmp__(other) <= 0 def __gt__(self, other): return self.__cmp__(other) > 0 def __ge__(self, other): return self.__cmp__(other) >= 0 def __str__(self): return f"{self.major_version}.{self.minor_version}.{self.major_build}.{self.minor_build}" def __repr__(self): return self.__class__.__name__ + repr( (self.major_version, self.minor_version, self.major_build, self.minor_build) )
var major_version
-
Expand source code
class Build: """Holds methods for working with build numbers.""" __slots__ = "major_version", "minor_version", "major_build", "minor_build" def __init__(self, major_version, minor_version, major_build=0, minor_build=0): if not isinstance(major_version, int): raise InvalidTypeError("major_version", major_version, int) if not isinstance(minor_version, int): raise InvalidTypeError("minor_version", minor_version, int) if not isinstance(major_build, int): raise InvalidTypeError("major_build", major_build, int) if not isinstance(minor_build, int): raise InvalidTypeError("minor_build", minor_build, int) self.major_version = major_version self.minor_version = minor_version self.major_build = major_build self.minor_build = minor_build if major_version < 8: raise ValueError(f"Exchange major versions below 8 don't support EWS ({self})") @classmethod def from_xml(cls, elem): xml_elems_map = { "major_version": "MajorVersion", "minor_version": "MinorVersion", "major_build": "MajorBuildNumber", "minor_build": "MinorBuildNumber", } kwargs = {} for k, xml_elem in xml_elems_map.items(): v = elem.get(xml_elem) if v is None: v = get_xml_attr(elem, f"{{{ANS}}}{xml_elem}") if v is None: raise ValueError() kwargs[k] = int(v) # Also raises ValueError return cls(**kwargs) def api_version(self): for build, api_version, _ in VERSIONS: if self.major_version != build.major_version or self.minor_version != build.minor_version: continue if self >= build: return api_version raise ValueError(f"API version for build {self} is unknown") def __cmp__(self, other): # __cmp__ is not a magic method in Python3. We'll just use it here to implement comparison operators c = (self.major_version > other.major_version) - (self.major_version < other.major_version) if c != 0: return c c = (self.minor_version > other.minor_version) - (self.minor_version < other.minor_version) if c != 0: return c c = (self.major_build > other.major_build) - (self.major_build < other.major_build) if c != 0: return c return (self.minor_build > other.minor_build) - (self.minor_build < other.minor_build) def __eq__(self, other): return self.__cmp__(other) == 0 def __hash__(self): return hash(repr(self)) def __ne__(self, other): return self.__cmp__(other) != 0 def __lt__(self, other): return self.__cmp__(other) < 0 def __le__(self, other): return self.__cmp__(other) <= 0 def __gt__(self, other): return self.__cmp__(other) > 0 def __ge__(self, other): return self.__cmp__(other) >= 0 def __str__(self): return f"{self.major_version}.{self.minor_version}.{self.major_build}.{self.minor_build}" def __repr__(self): return self.__class__.__name__ + repr( (self.major_version, self.minor_version, self.major_build, self.minor_build) )
var minor_build
-
Expand source code
class Build: """Holds methods for working with build numbers.""" __slots__ = "major_version", "minor_version", "major_build", "minor_build" def __init__(self, major_version, minor_version, major_build=0, minor_build=0): if not isinstance(major_version, int): raise InvalidTypeError("major_version", major_version, int) if not isinstance(minor_version, int): raise InvalidTypeError("minor_version", minor_version, int) if not isinstance(major_build, int): raise InvalidTypeError("major_build", major_build, int) if not isinstance(minor_build, int): raise InvalidTypeError("minor_build", minor_build, int) self.major_version = major_version self.minor_version = minor_version self.major_build = major_build self.minor_build = minor_build if major_version < 8: raise ValueError(f"Exchange major versions below 8 don't support EWS ({self})") @classmethod def from_xml(cls, elem): xml_elems_map = { "major_version": "MajorVersion", "minor_version": "MinorVersion", "major_build": "MajorBuildNumber", "minor_build": "MinorBuildNumber", } kwargs = {} for k, xml_elem in xml_elems_map.items(): v = elem.get(xml_elem) if v is None: v = get_xml_attr(elem, f"{{{ANS}}}{xml_elem}") if v is None: raise ValueError() kwargs[k] = int(v) # Also raises ValueError return cls(**kwargs) def api_version(self): for build, api_version, _ in VERSIONS: if self.major_version != build.major_version or self.minor_version != build.minor_version: continue if self >= build: return api_version raise ValueError(f"API version for build {self} is unknown") def __cmp__(self, other): # __cmp__ is not a magic method in Python3. We'll just use it here to implement comparison operators c = (self.major_version > other.major_version) - (self.major_version < other.major_version) if c != 0: return c c = (self.minor_version > other.minor_version) - (self.minor_version < other.minor_version) if c != 0: return c c = (self.major_build > other.major_build) - (self.major_build < other.major_build) if c != 0: return c return (self.minor_build > other.minor_build) - (self.minor_build < other.minor_build) def __eq__(self, other): return self.__cmp__(other) == 0 def __hash__(self): return hash(repr(self)) def __ne__(self, other): return self.__cmp__(other) != 0 def __lt__(self, other): return self.__cmp__(other) < 0 def __le__(self, other): return self.__cmp__(other) <= 0 def __gt__(self, other): return self.__cmp__(other) > 0 def __ge__(self, other): return self.__cmp__(other) >= 0 def __str__(self): return f"{self.major_version}.{self.minor_version}.{self.major_build}.{self.minor_build}" def __repr__(self): return self.__class__.__name__ + repr( (self.major_version, self.minor_version, self.major_build, self.minor_build) )
var minor_version
-
Expand source code
class Build: """Holds methods for working with build numbers.""" __slots__ = "major_version", "minor_version", "major_build", "minor_build" def __init__(self, major_version, minor_version, major_build=0, minor_build=0): if not isinstance(major_version, int): raise InvalidTypeError("major_version", major_version, int) if not isinstance(minor_version, int): raise InvalidTypeError("minor_version", minor_version, int) if not isinstance(major_build, int): raise InvalidTypeError("major_build", major_build, int) if not isinstance(minor_build, int): raise InvalidTypeError("minor_build", minor_build, int) self.major_version = major_version self.minor_version = minor_version self.major_build = major_build self.minor_build = minor_build if major_version < 8: raise ValueError(f"Exchange major versions below 8 don't support EWS ({self})") @classmethod def from_xml(cls, elem): xml_elems_map = { "major_version": "MajorVersion", "minor_version": "MinorVersion", "major_build": "MajorBuildNumber", "minor_build": "MinorBuildNumber", } kwargs = {} for k, xml_elem in xml_elems_map.items(): v = elem.get(xml_elem) if v is None: v = get_xml_attr(elem, f"{{{ANS}}}{xml_elem}") if v is None: raise ValueError() kwargs[k] = int(v) # Also raises ValueError return cls(**kwargs) def api_version(self): for build, api_version, _ in VERSIONS: if self.major_version != build.major_version or self.minor_version != build.minor_version: continue if self >= build: return api_version raise ValueError(f"API version for build {self} is unknown") def __cmp__(self, other): # __cmp__ is not a magic method in Python3. We'll just use it here to implement comparison operators c = (self.major_version > other.major_version) - (self.major_version < other.major_version) if c != 0: return c c = (self.minor_version > other.minor_version) - (self.minor_version < other.minor_version) if c != 0: return c c = (self.major_build > other.major_build) - (self.major_build < other.major_build) if c != 0: return c return (self.minor_build > other.minor_build) - (self.minor_build < other.minor_build) def __eq__(self, other): return self.__cmp__(other) == 0 def __hash__(self): return hash(repr(self)) def __ne__(self, other): return self.__cmp__(other) != 0 def __lt__(self, other): return self.__cmp__(other) < 0 def __le__(self, other): return self.__cmp__(other) <= 0 def __gt__(self, other): return self.__cmp__(other) > 0 def __ge__(self, other): return self.__cmp__(other) >= 0 def __str__(self): return f"{self.major_version}.{self.minor_version}.{self.major_build}.{self.minor_build}" def __repr__(self): return self.__class__.__name__ + repr( (self.major_version, self.minor_version, self.major_build, self.minor_build) )
Methods
def api_version(self)
-
Expand source code
def api_version(self): for build, api_version, _ in VERSIONS: if self.major_version != build.major_version or self.minor_version != build.minor_version: continue if self >= build: return api_version raise ValueError(f"API version for build {self} is unknown")
class SupportedVersionClassMixIn (*args, **kwargs)
-
Expand source code
class SupportedVersionClassMixIn: """Supports specifying the supported versions of services, fields, folders etc. For distinguished folders, a possibly authoritative source is: # https://github.com/OfficeDev/ews-managed-api/blob/master/Enumerations/WellKnownFolderName.cs """ supported_from = None # The Exchange build when this element was introduced deprecated_from = None # The Exchange build when this element was deprecated @classmethod def __new__(cls, *args, **kwargs): _check(cls.supported_from, cls.deprecated_from) return super().__new__(cls) @classmethod def supports_version(cls, version): return _supports_version(cls.supported_from, cls.deprecated_from, version)
Supports specifying the supported versions of services, fields, folders etc.
For distinguished folders, a possibly authoritative source is:
https://github.com/OfficeDev/ews-managed-api/blob/master/Enumerations/WellKnownFolderName.cs
Subclasses
Class variables
var deprecated_from
var supported_from
Static methods
def supports_version(version)
class SupportedVersionInstanceMixIn (supported_from=None, deprecated_from=None)
-
Expand source code
class SupportedVersionInstanceMixIn: """Like SupportedVersionClassMixIn but for class instances""" def __init__(self, supported_from=None, deprecated_from=None): _check(supported_from, deprecated_from) self.supported_from = supported_from self.deprecated_from = deprecated_from def supports_version(self, version): return _supports_version(self.supported_from, self.deprecated_from, version)
Like SupportedVersionClassMixIn but for class instances
Subclasses
Methods
def supports_version(self, version)
-
Expand source code
def supports_version(self, version): return _supports_version(self.supported_from, self.deprecated_from, version)
class Version (build, api_version=None)
-
Expand source code
class Version: """Holds information about the server version.""" __slots__ = "build", "api_version" def __init__(self, build, api_version=None): if api_version is None: if not isinstance(build, Build): raise InvalidTypeError("build", build, Build) self.api_version = build.api_version() else: if not isinstance(build, (Build, type(None))): raise InvalidTypeError("build", build, Build) if not isinstance(api_version, str): raise InvalidTypeError("api_version", api_version, str) self.api_version = api_version self.build = build @property def fullname(self): for build, api_version, full_name in VERSIONS: if self.build and ( self.build.major_version != build.major_version or self.build.minor_version != build.minor_version ): continue if self.api_version == api_version: return full_name log.warning("Full name for API version %s build %s is unknown", self.api_version, self.build) return "UNKNOWN" @classmethod def guess(cls, protocol, api_version_hint=None): """Ask the server which version it has. We haven't set up an Account object yet, so we generate requests by hand. We only need a response header containing a ServerVersionInfo element. To get API version and build numbers from the server, we need to send a valid SOAP request. We can't do that without a valid API version. To solve this chicken-and-egg problem, we try all possible API versions that this package supports, until we get a valid response. :param protocol: :param api_version_hint: (Default value = None) """ from .properties import ENTRY_ID, EWS_ID, AlternateId from .services import ConvertId # The protocol doesn't have a version yet, so default to the latest supported version if we don't have a hint. api_version = api_version_hint or ConvertId.supported_api_versions()[0] log.debug("Asking server for version info using API version %s", api_version) # We don't know the build version yet. Hopefully, the server will report it in the SOAP header. Lots of # places expect a version to have a build, so this is a bit dangerous, but passing a fake build around is also # dangerous. protocol.config.version = Version(build=None, api_version=api_version) # Use ConvertId as a minimal request to the server to test if the version is correct. If not, ConvertId will # try to guess the version automatically. Make sure the call to ConvertId does not require a version build. try: list(ConvertId(protocol=protocol).call([AlternateId(id="DUMMY", format=EWS_ID, mailbox="DUMMY")], ENTRY_ID)) except ResponseMessageError as e: # We may have survived long enough to get a new version if not protocol.config.version.build: raise TransportError(f"No valid version headers found in response ({e!r})") if not protocol.config.version.build: raise TransportError("No valid version headers found in response") return protocol.config.version @staticmethod def _is_invalid_version_string(version): # Check if a version string is bogus, e.g. V2_, V2015_ or V2018_ return re.match(r"V[0-9]{1,4}_.*", version) @classmethod def from_soap_header(cls, requested_api_version, header): info = header.find(f"{{{TNS}}}ServerVersionInfo") if info is None: info = header.find(f"{{{ANS}}}ServerVersionInfo") if info is None: raise TransportError(f"No ServerVersionInfo in header: {xml_to_str(header)!r}") try: build = Build.from_xml(elem=info) except ValueError: raise TransportError(f"Bad ServerVersionInfo in response: {xml_to_str(header)!r}") # Not all Exchange servers send the Version element api_version_from_server = info.get("Version") or get_xml_attr(info, f"{{{ANS}}}Version") or build.api_version() if api_version_from_server != requested_api_version: if cls._is_invalid_version_string(api_version_from_server): # For unknown reasons, Office 365 may respond with an API version strings that is invalid in a request. # Detect these, so we can fall back to a valid version string. log.debug( 'API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version, api_version_from_server, requested_api_version, ) api_version_from_server = requested_api_version else: # Trust API version from server response log.debug( 'API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version, api_version_from_server, api_version_from_server, ) return cls(build=build, api_version=api_version_from_server) def copy(self): return self.__class__(build=self.build, api_version=self.api_version) @classmethod def all_versions(cls): # Return all supported versions, sorted newest to oldest return [cls(build=build, api_version=api_version) for build, api_version, _ in VERSIONS] def __hash__(self): return hash((self.build, self.api_version)) def __eq__(self, other): if self.api_version != other.api_version: return False if self.build and not other.build: return False if other.build and not self.build: return False return self.build == other.build def __repr__(self): return self.__class__.__name__ + repr((self.build, self.api_version)) def __str__(self): return f"Build={self.build}, API={self.api_version}, Fullname={self.fullname}"
Holds information about the server version.
Static methods
def all_versions()
def from_soap_header(requested_api_version, header)
def guess(protocol, api_version_hint=None)
-
Ask the server which version it has. We haven't set up an Account object yet, so we generate requests by hand. We only need a response header containing a ServerVersionInfo element.
To get API version and build numbers from the server, we need to send a valid SOAP request. We can't do that without a valid API version. To solve this chicken-and-egg problem, we try all possible API versions that this package supports, until we get a valid response.
:param protocol: :param api_version_hint: (Default value = None)
Instance variables
var api_version
-
Expand source code
class Version: """Holds information about the server version.""" __slots__ = "build", "api_version" def __init__(self, build, api_version=None): if api_version is None: if not isinstance(build, Build): raise InvalidTypeError("build", build, Build) self.api_version = build.api_version() else: if not isinstance(build, (Build, type(None))): raise InvalidTypeError("build", build, Build) if not isinstance(api_version, str): raise InvalidTypeError("api_version", api_version, str) self.api_version = api_version self.build = build @property def fullname(self): for build, api_version, full_name in VERSIONS: if self.build and ( self.build.major_version != build.major_version or self.build.minor_version != build.minor_version ): continue if self.api_version == api_version: return full_name log.warning("Full name for API version %s build %s is unknown", self.api_version, self.build) return "UNKNOWN" @classmethod def guess(cls, protocol, api_version_hint=None): """Ask the server which version it has. We haven't set up an Account object yet, so we generate requests by hand. We only need a response header containing a ServerVersionInfo element. To get API version and build numbers from the server, we need to send a valid SOAP request. We can't do that without a valid API version. To solve this chicken-and-egg problem, we try all possible API versions that this package supports, until we get a valid response. :param protocol: :param api_version_hint: (Default value = None) """ from .properties import ENTRY_ID, EWS_ID, AlternateId from .services import ConvertId # The protocol doesn't have a version yet, so default to the latest supported version if we don't have a hint. api_version = api_version_hint or ConvertId.supported_api_versions()[0] log.debug("Asking server for version info using API version %s", api_version) # We don't know the build version yet. Hopefully, the server will report it in the SOAP header. Lots of # places expect a version to have a build, so this is a bit dangerous, but passing a fake build around is also # dangerous. protocol.config.version = Version(build=None, api_version=api_version) # Use ConvertId as a minimal request to the server to test if the version is correct. If not, ConvertId will # try to guess the version automatically. Make sure the call to ConvertId does not require a version build. try: list(ConvertId(protocol=protocol).call([AlternateId(id="DUMMY", format=EWS_ID, mailbox="DUMMY")], ENTRY_ID)) except ResponseMessageError as e: # We may have survived long enough to get a new version if not protocol.config.version.build: raise TransportError(f"No valid version headers found in response ({e!r})") if not protocol.config.version.build: raise TransportError("No valid version headers found in response") return protocol.config.version @staticmethod def _is_invalid_version_string(version): # Check if a version string is bogus, e.g. V2_, V2015_ or V2018_ return re.match(r"V[0-9]{1,4}_.*", version) @classmethod def from_soap_header(cls, requested_api_version, header): info = header.find(f"{{{TNS}}}ServerVersionInfo") if info is None: info = header.find(f"{{{ANS}}}ServerVersionInfo") if info is None: raise TransportError(f"No ServerVersionInfo in header: {xml_to_str(header)!r}") try: build = Build.from_xml(elem=info) except ValueError: raise TransportError(f"Bad ServerVersionInfo in response: {xml_to_str(header)!r}") # Not all Exchange servers send the Version element api_version_from_server = info.get("Version") or get_xml_attr(info, f"{{{ANS}}}Version") or build.api_version() if api_version_from_server != requested_api_version: if cls._is_invalid_version_string(api_version_from_server): # For unknown reasons, Office 365 may respond with an API version strings that is invalid in a request. # Detect these, so we can fall back to a valid version string. log.debug( 'API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version, api_version_from_server, requested_api_version, ) api_version_from_server = requested_api_version else: # Trust API version from server response log.debug( 'API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version, api_version_from_server, api_version_from_server, ) return cls(build=build, api_version=api_version_from_server) def copy(self): return self.__class__(build=self.build, api_version=self.api_version) @classmethod def all_versions(cls): # Return all supported versions, sorted newest to oldest return [cls(build=build, api_version=api_version) for build, api_version, _ in VERSIONS] def __hash__(self): return hash((self.build, self.api_version)) def __eq__(self, other): if self.api_version != other.api_version: return False if self.build and not other.build: return False if other.build and not self.build: return False return self.build == other.build def __repr__(self): return self.__class__.__name__ + repr((self.build, self.api_version)) def __str__(self): return f"Build={self.build}, API={self.api_version}, Fullname={self.fullname}"
var build
-
Expand source code
class Version: """Holds information about the server version.""" __slots__ = "build", "api_version" def __init__(self, build, api_version=None): if api_version is None: if not isinstance(build, Build): raise InvalidTypeError("build", build, Build) self.api_version = build.api_version() else: if not isinstance(build, (Build, type(None))): raise InvalidTypeError("build", build, Build) if not isinstance(api_version, str): raise InvalidTypeError("api_version", api_version, str) self.api_version = api_version self.build = build @property def fullname(self): for build, api_version, full_name in VERSIONS: if self.build and ( self.build.major_version != build.major_version or self.build.minor_version != build.minor_version ): continue if self.api_version == api_version: return full_name log.warning("Full name for API version %s build %s is unknown", self.api_version, self.build) return "UNKNOWN" @classmethod def guess(cls, protocol, api_version_hint=None): """Ask the server which version it has. We haven't set up an Account object yet, so we generate requests by hand. We only need a response header containing a ServerVersionInfo element. To get API version and build numbers from the server, we need to send a valid SOAP request. We can't do that without a valid API version. To solve this chicken-and-egg problem, we try all possible API versions that this package supports, until we get a valid response. :param protocol: :param api_version_hint: (Default value = None) """ from .properties import ENTRY_ID, EWS_ID, AlternateId from .services import ConvertId # The protocol doesn't have a version yet, so default to the latest supported version if we don't have a hint. api_version = api_version_hint or ConvertId.supported_api_versions()[0] log.debug("Asking server for version info using API version %s", api_version) # We don't know the build version yet. Hopefully, the server will report it in the SOAP header. Lots of # places expect a version to have a build, so this is a bit dangerous, but passing a fake build around is also # dangerous. protocol.config.version = Version(build=None, api_version=api_version) # Use ConvertId as a minimal request to the server to test if the version is correct. If not, ConvertId will # try to guess the version automatically. Make sure the call to ConvertId does not require a version build. try: list(ConvertId(protocol=protocol).call([AlternateId(id="DUMMY", format=EWS_ID, mailbox="DUMMY")], ENTRY_ID)) except ResponseMessageError as e: # We may have survived long enough to get a new version if not protocol.config.version.build: raise TransportError(f"No valid version headers found in response ({e!r})") if not protocol.config.version.build: raise TransportError("No valid version headers found in response") return protocol.config.version @staticmethod def _is_invalid_version_string(version): # Check if a version string is bogus, e.g. V2_, V2015_ or V2018_ return re.match(r"V[0-9]{1,4}_.*", version) @classmethod def from_soap_header(cls, requested_api_version, header): info = header.find(f"{{{TNS}}}ServerVersionInfo") if info is None: info = header.find(f"{{{ANS}}}ServerVersionInfo") if info is None: raise TransportError(f"No ServerVersionInfo in header: {xml_to_str(header)!r}") try: build = Build.from_xml(elem=info) except ValueError: raise TransportError(f"Bad ServerVersionInfo in response: {xml_to_str(header)!r}") # Not all Exchange servers send the Version element api_version_from_server = info.get("Version") or get_xml_attr(info, f"{{{ANS}}}Version") or build.api_version() if api_version_from_server != requested_api_version: if cls._is_invalid_version_string(api_version_from_server): # For unknown reasons, Office 365 may respond with an API version strings that is invalid in a request. # Detect these, so we can fall back to a valid version string. log.debug( 'API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version, api_version_from_server, requested_api_version, ) api_version_from_server = requested_api_version else: # Trust API version from server response log.debug( 'API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version, api_version_from_server, api_version_from_server, ) return cls(build=build, api_version=api_version_from_server) def copy(self): return self.__class__(build=self.build, api_version=self.api_version) @classmethod def all_versions(cls): # Return all supported versions, sorted newest to oldest return [cls(build=build, api_version=api_version) for build, api_version, _ in VERSIONS] def __hash__(self): return hash((self.build, self.api_version)) def __eq__(self, other): if self.api_version != other.api_version: return False if self.build and not other.build: return False if other.build and not self.build: return False return self.build == other.build def __repr__(self): return self.__class__.__name__ + repr((self.build, self.api_version)) def __str__(self): return f"Build={self.build}, API={self.api_version}, Fullname={self.fullname}"
prop fullname
-
Expand source code
@property def fullname(self): for build, api_version, full_name in VERSIONS: if self.build and ( self.build.major_version != build.major_version or self.build.minor_version != build.minor_version ): continue if self.api_version == api_version: return full_name log.warning("Full name for API version %s build %s is unknown", self.api_version, self.build) return "UNKNOWN"
Methods
def copy(self)
-
Expand source code
def copy(self): return self.__class__(build=self.build, api_version=self.api_version)