exchangelib.version API documentation

# 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

          @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

            @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

            @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

            @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

            @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

  - BaseFolder
  - EWSService

  ### Class variables

  `var deprecated_from`

    The type of the None singleton.

  `var supported_from`

    The type of the None singleton.

  ### 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

  - Choice
  - Field

  ### 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)